| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110 |
- package state
- import (
- "context"
- "log/slog"
- "math/rand"
- "sync"
- "testing"
- "testing/synctest"
- "time"
- "github.com/mk6i/open-oscar-server/wire"
- "github.com/stretchr/testify/require"
- "github.com/stretchr/testify/assert"
- )
- func TestInMemorySessionManager_AddSession(t *testing.T) {
- sm := NewInMemorySessionManager(slog.Default())
- ctx := context.Background()
- sess1, err := sm.AddSession(ctx, "user-screen-name", false)
- assert.NoError(t, err)
- sess1.SetSignonComplete()
- go func() {
- <-sess1.Closed()
- sm.RemoveSession(sess1.Session())
- }()
- sess2, err := sm.AddSession(ctx, "user-screen-name", false)
- assert.NoError(t, err)
- sess2.SetSignonComplete()
- assert.NotSame(t, sess1, sess2)
- assert.Contains(t, sm.AllSessions(), sess2.Session())
- }
- func TestInMemorySessionManager_AddSession_AppliesCfgToSession(t *testing.T) {
- sm := NewInMemorySessionManager(slog.Default())
- wantUIN := uint32(424242)
- wantCookie := "cfg-test-cookie"
- instance, err := sm.AddSession(context.Background(), "user-screen-name", false,
- func(sess *Session) {
- sess.SetUIN(wantUIN)
- sess.SetChatRoomCookie(wantCookie)
- },
- )
- assert.NoError(t, err)
- s := instance.Session()
- assert.Equal(t, wantUIN, s.UIN(), "cfg mutates Session before AddInstance / store insert")
- assert.Equal(t, wantCookie, s.ChatRoomCookie())
- }
- func TestInMemorySessionManager_AddSession_Timeout(t *testing.T) {
- sm := NewInMemorySessionManager(slog.Default())
- ctx, cancel := context.WithCancel(context.Background())
- sess1, err := sm.AddSession(ctx, "user-screen-name", false)
- assert.NoError(t, err)
- sess1.SetSignonComplete()
- go func() {
- <-sess1.Closed()
- cancel()
- }()
- sess2, err := sm.AddSession(ctx, "user-screen-name", false)
- assert.Nil(t, sess2)
- assert.ErrorIs(t, err, context.Canceled)
- }
- func TestInMemorySessionManager_Remove_Existing(t *testing.T) {
- sm := NewInMemorySessionManager(slog.Default())
- user1Old, err := sm.AddSession(context.Background(), "user-screen-name-1", false)
- assert.NoError(t, err)
- // Verify the old session is in the store
- rec, ok := sm.store[user1Old.IdentScreenName()]
- assert.True(t, ok)
- assert.Equal(t, user1Old.Session(), rec.session)
- // Remove the session
- sm.RemoveSession(user1Old.Session())
- // Verify the session is no longer in the store
- _, ok = sm.store[user1Old.IdentScreenName()]
- assert.False(t, ok)
- // Verify the removed channel was closed
- select {
- case <-rec.removed:
- // Channel was closed, as expected
- default:
- assert.Fail(t, "removed channel should be closed")
- }
- user1New, err := sm.AddSession(context.Background(), "user-screen-name-1", false)
- assert.NoError(t, err)
- user1New.SetSignonComplete()
- user2, err := sm.AddSession(context.Background(), "user-screen-name-2", false)
- assert.NoError(t, err)
- user2.SetSignonComplete()
- // Remove user1New and verify it's gone
- sm.RemoveSession(user1New.Session())
- _, ok = sm.store[user1New.IdentScreenName()]
- assert.False(t, ok)
- if assert.Len(t, sm.AllSessions(), 1) {
- assert.NotContains(t, sm.AllSessions(), user1Old.Session())
- assert.NotContains(t, sm.AllSessions(), user1New.Session())
- assert.Contains(t, sm.AllSessions(), user2.Session())
- }
- }
- func TestInMemorySessionManager_Remove_MissingSameScreenName(t *testing.T) {
- sm := NewInMemorySessionManager(slog.Default())
- user1Old, err := sm.AddSession(context.Background(), "user-screen-name-1", false)
- assert.NoError(t, err)
- // Verify the old session is in the store
- recOld, ok := sm.store[user1Old.IdentScreenName()]
- assert.True(t, ok)
- assert.Equal(t, user1Old.Session(), recOld.session)
- // Remove the old session
- sm.RemoveSession(user1Old.Session())
- _, ok = sm.store[user1Old.IdentScreenName()]
- assert.False(t, ok)
- // Create a new session with the same screen name but different Session
- user1New, err := sm.AddSession(context.Background(), "user-screen-name-1", false)
- assert.NoError(t, err)
- user1New.SetSignonComplete()
- // Verify the new session is in the store with a different Session
- recNew, ok := sm.store[user1New.IdentScreenName()]
- assert.True(t, ok)
- assert.Equal(t, user1New.Session(), recNew.session)
- assert.NotEqual(t, user1Old.Session(), user1New.Session())
- user2, err := sm.AddSession(context.Background(), "user-screen-name-2", false)
- assert.NoError(t, err)
- user2.SetSignonComplete()
- // Try to remove the old session again - should do nothing because Session doesn't match
- sm.RemoveSession(user1Old.Session())
- // Verify the new session is still in the store (not removed)
- recNewAfter, ok := sm.store[user1New.IdentScreenName()]
- assert.True(t, ok, "new session should still be in store")
- assert.Equal(t, user1New.Session(), recNewAfter.session)
- if assert.Len(t, sm.AllSessions(), 2) {
- assert.NotContains(t, sm.AllSessions(), user1Old.Session())
- assert.Contains(t, sm.AllSessions(), user1New.Session())
- assert.Contains(t, sm.AllSessions(), user2.Session())
- }
- }
- func TestInMemorySessionManager_Empty(t *testing.T) {
- tests := []struct {
- name string
- given []DisplayScreenName
- want bool
- }{
- {
- name: "session manager is not empty",
- given: []DisplayScreenName{
- "user-screen-name-1",
- },
- want: false,
- },
- {
- name: "session manager is empty",
- given: []DisplayScreenName{},
- want: true,
- },
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- sm := NewInMemorySessionManager(slog.Default())
- for _, screenName := range tt.given {
- sess, err := sm.AddSession(context.Background(), screenName, false)
- assert.NoError(t, err)
- sess.SetSignonComplete()
- }
- have := sm.Empty()
- assert.Equal(t, tt.want, have)
- })
- }
- }
- func TestInMemorySessionManager_Retrieve(t *testing.T) {
- tests := []struct {
- name string
- given []DisplayScreenName
- lookupScreenName IdentScreenName
- wantScreenName IdentScreenName
- }{
- {
- name: "lookup finds match",
- given: []DisplayScreenName{
- "user-screen-name-1",
- "user-screen-name-2",
- },
- lookupScreenName: NewIdentScreenName("user-screen-name-2"),
- wantScreenName: NewIdentScreenName("user-screen-name-2"),
- },
- {
- name: "lookup does not find match",
- given: []DisplayScreenName{},
- lookupScreenName: NewIdentScreenName("user-screen-name-3"),
- wantScreenName: NewIdentScreenName(""),
- },
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- sm := NewInMemorySessionManager(slog.Default())
- for _, screenName := range tt.given {
- sess, err := sm.AddSession(context.Background(), screenName, false)
- assert.NoError(t, err)
- sess.SetSignonComplete()
- }
- have := sm.RetrieveSession(tt.lookupScreenName)
- if have == nil {
- assert.Empty(t, tt.wantScreenName)
- } else {
- assert.Equal(t, tt.wantScreenName, have.IdentScreenName())
- }
- })
- }
- }
- func TestInMemorySessionManager_RelayToScreenNames(t *testing.T) {
- sm := NewInMemorySessionManager(slog.Default())
- user1, err := sm.AddSession(context.Background(), "user-screen-name-1", false)
- assert.NoError(t, err)
- user1.SetSignonComplete()
- user2, err := sm.AddSession(context.Background(), "user-screen-name-2", false)
- assert.NoError(t, err)
- user2.SetSignonComplete()
- user3, err := sm.AddSession(context.Background(), "user-screen-name-3", false)
- assert.NoError(t, err)
- user3.SetSignonComplete()
- want := wire.SNACMessage{Frame: wire.SNACFrame{FoodGroup: wire.ICBM}}
- recips := []IdentScreenName{
- NewIdentScreenName("user-screen-name-1"),
- NewIdentScreenName("user-screen-name-2"),
- }
- sm.RelayToScreenNames(context.Background(), recips, want)
- have := <-user1.ReceiveMessage()
- assert.Equal(t, want, have)
- have = <-user2.ReceiveMessage()
- assert.Equal(t, want, have)
- select {
- case <-user3.ReceiveMessage():
- assert.Fail(t, "user 3 should not receive a message")
- default:
- }
- }
- func TestInMemorySessionManager_Broadcast(t *testing.T) {
- sm := NewInMemorySessionManager(slog.Default())
- user1, err := sm.AddSession(context.Background(), "user-screen-name-1", false)
- assert.NoError(t, err)
- user1.SetSignonComplete()
- user2, err := sm.AddSession(context.Background(), "user-screen-name-2", false)
- assert.NoError(t, err)
- user2.SetSignonComplete()
- want := wire.SNACMessage{Frame: wire.SNACFrame{FoodGroup: wire.ICBM}}
- sm.RelayToAll(context.Background(), want)
- have := <-user1.ReceiveMessage()
- assert.Equal(t, want, have)
- have = <-user2.ReceiveMessage()
- assert.Equal(t, want, have)
- }
- func TestInMemorySessionManager_Broadcast_SkipClosedSession(t *testing.T) {
- sm := NewInMemorySessionManager(slog.Default())
- user1, err := sm.AddSession(context.Background(), "user-screen-name-1", false)
- assert.NoError(t, err)
- user1.SetSignonComplete()
- user2, err := sm.AddSession(context.Background(), "user-screen-name-2", false)
- assert.NoError(t, err)
- user2.SetSignonComplete()
- user2.CloseInstance()
- want := wire.SNACMessage{Frame: wire.SNACFrame{FoodGroup: wire.ICBM}}
- sm.RelayToAll(context.Background(), want)
- have := <-user1.ReceiveMessage()
- assert.Equal(t, want, have)
- select {
- case <-user2.ReceiveMessage():
- assert.Fail(t, "user 2 should not receive a message")
- default:
- }
- }
- func TestInMemorySessionManager_RelayToScreenName_SessionExists(t *testing.T) {
- sm := NewInMemorySessionManager(slog.Default())
- user1, err := sm.AddSession(context.Background(), "user-screen-name-1", false)
- assert.NoError(t, err)
- user1.SetSignonComplete()
- user2, err := sm.AddSession(context.Background(), "user-screen-name-2", false)
- assert.NoError(t, err)
- user2.SetSignonComplete()
- want := wire.SNACMessage{Frame: wire.SNACFrame{FoodGroup: wire.ICBM}}
- recip := NewIdentScreenName("user-screen-name-1")
- sm.RelayToScreenName(context.Background(), recip, want)
- have := <-user1.ReceiveMessage()
- assert.Equal(t, want, have)
- select {
- case <-user2.ReceiveMessage():
- assert.Fail(t, "user 2 should not receive a message")
- default:
- }
- }
- func TestInMemorySessionManager_RelayToScreenName_SessionNotExist(t *testing.T) {
- sm := NewInMemorySessionManager(slog.Default())
- user1, err := sm.AddSession(context.Background(), "user-screen-name-1", false)
- assert.NoError(t, err)
- user1.SetSignonComplete()
- want := wire.SNACMessage{Frame: wire.SNACFrame{FoodGroup: wire.ICBM}}
- recip := NewIdentScreenName("user-screen-name-2")
- sm.RelayToScreenName(context.Background(), recip, want)
- select {
- case <-user1.ReceiveMessage():
- assert.Fail(t, "user 1 should not receive a message")
- default:
- }
- }
- func TestInMemorySessionManager_RelayToScreenName_SkipFullSession(t *testing.T) {
- sm := NewInMemorySessionManager(slog.Default())
- user1, err := sm.AddSession(context.Background(), "user-screen-name-1", false)
- assert.NoError(t, err)
- user1.SetSignonComplete()
- msg := wire.SNACMessage{Frame: wire.SNACFrame{FoodGroup: wire.ICBM}}
- wantCount := 0
- for user1.RelayMessageToInstance(msg) != SessQueueFull {
- wantCount++
- }
- recip := NewIdentScreenName("user-screen-name-1")
- sm.RelayToScreenName(context.Background(), recip, msg)
- haveCount := 0
- loop:
- for {
- select {
- case <-user1.ReceiveMessage():
- haveCount++
- default:
- break loop
- }
- }
- assert.Equal(t, wantCount, haveCount)
- }
- func TestInMemorySessionManager_SessionReplacement_NoMultiSess_NoMultiSess(t *testing.T) {
- synctest.Test(t, func(t *testing.T) {
- sm := NewInMemorySessionManager(slog.Default())
- sess1, err := sm.AddSession(context.Background(), "user-screen-name-1", false)
- assert.NoError(t, err)
- sess1.SetSignonComplete()
- wg := &sync.WaitGroup{}
- wg.Add(1)
- go func() {
- defer wg.Done()
- // add the session again. this call blocks until RemoveSession makes
- // room for the new session
- sess2, err := sm.AddSession(context.Background(), "user-screen-name-1", false)
- assert.NoError(t, err)
- if assert.NotNil(t, sess2) {
- sess2.SetSignonComplete()
- assert.Equal(t, sess1.DisplayScreenName(), sess2.DisplayScreenName())
- }
- }()
- // wait for second call to AddSession() to block
- synctest.Wait()
- // AddSession() is blocked waiting for the lock, now unblock it
- sm.RemoveSession(sess1.Session())
- wg.Wait()
- // make sure we got a brand new session
- got := sm.RetrieveSession(NewIdentScreenName("user-screen-name-1"))
- assert.NotEqual(t, sess1, got)
- assert.Equal(t, 1, got.InstanceCount())
- })
- }
- func TestInMemorySessionManager_SessionReplacement_MultiSess_NoMultiSess(t *testing.T) {
- synctest.Test(t, func(t *testing.T) {
- sm := NewInMemorySessionManager(slog.Default())
- sm.maxConcurrentSessions = 5
- var sessList []*SessionInstance
- for i := 0; i < sm.maxConcurrentSessions; i++ {
- sess, err := sm.AddSession(context.Background(), "user-screen-name-1", true)
- assert.NoError(t, err)
- sess.SetSignonComplete()
- sessList = append(sessList, sess)
- }
- assert.Equal(t, len(sessList), sessList[0].Session().InstanceCount())
- wg := &sync.WaitGroup{}
- wg.Add(1)
- go func() {
- defer wg.Done()
- // add the session again. this call blocks until RemoveSession makes
- // room for the new session
- sess, err := sm.AddSession(context.Background(), "user-screen-name-1", false)
- assert.NoError(t, err)
- assert.NotNil(t, sess)
- sess.SetSignonComplete()
- assert.Equal(t, "user-screen-name-1", sess.DisplayScreenName().String())
- assert.Equal(t, 1, sess.Session().InstanceCount())
- }()
- // wait for the last call to AddSession() to block
- synctest.Wait()
- // AddSession() is blocked waiting for the lock, now unblock it
- for _, sess := range sessList {
- sm.RemoveSession(sess.Session())
- }
- wg.Wait()
- got := sm.RetrieveSession(NewIdentScreenName("user-screen-name-1"))
- for _, sess := range sessList {
- assert.NotSame(t, sess, got)
- }
- assert.Equal(t, 1, got.InstanceCount())
- })
- }
- func TestInMemorySessionManager_SessionReplacement_NoMultiSess_MultiSess(t *testing.T) {
- synctest.Test(t, func(t *testing.T) {
- sm := NewInMemorySessionManager(slog.Default())
- sess1, err := sm.AddSession(context.Background(), "user-screen-name-1", false)
- assert.NoError(t, err)
- sess1.SetSignonComplete()
- wg := &sync.WaitGroup{}
- wg.Add(1)
- go func() {
- defer wg.Done()
- // add the session again. this call blocks until RemoveSession makes
- // room for the new session
- sess2, err := sm.AddSession(context.Background(), "user-screen-name-1", true)
- assert.NoError(t, err)
- assert.NotNil(t, sess2)
- assert.Equal(t, sess1.DisplayScreenName(), sess2.DisplayScreenName())
- sess2.SetSignonComplete()
- }()
- // wait for second call to AddSession() to block
- synctest.Wait()
- // AddSession() is blocked waiting for the lock, now unblock it
- sm.RemoveSession(sess1.Session())
- wg.Wait()
- got := sm.RetrieveSession(NewIdentScreenName("user-screen-name-1"))
- if assert.NotNil(t, got) {
- assert.NotSame(t, sess1, got)
- assert.Equal(t, 1, got.InstanceCount())
- }
- })
- }
- func TestInMemorySessionManager_RemoveSession_DoubleLogin_NoMultiSess_Chaos(t *testing.T) {
- wg := &sync.WaitGroup{}
- sm := NewInMemorySessionManager(slog.Default())
- for i := 0; i < 1000; i++ { // shake out race conditions
- wg.Add(1)
- time.Sleep(time.Duration(rand.Intn(1000)) * time.Microsecond)
- go func() {
- defer wg.Done()
- sess1, err := sm.AddSession(context.Background(), "user-screen-name-1", false)
- assert.NoError(t, err)
- time.Sleep(time.Duration(rand.Intn(1000)) * time.Microsecond)
- sm.RemoveSession(sess1.Session())
- }()
- }
- wg.Wait()
- }
- func TestInMemoryChatSessionManager_AddSession_AppliesCfgToSession(t *testing.T) {
- sm := NewInMemoryChatSessionManager(slog.Default())
- chatCookie := "chat-room-cfg"
- wantUIN := uint32(777001)
- instance, err := sm.AddSession(context.Background(), chatCookie, "user-screen-name",
- func(sess *Session) {
- sess.SetUIN(wantUIN)
- },
- )
- assert.NoError(t, err)
- s := instance.Session()
- assert.Equal(t, wantUIN, s.UIN(), "cfg mutates Session via inner AddSession before instance is returned")
- assert.Equal(t, chatCookie, s.ChatRoomCookie())
- }
- func TestInMemoryChatSessionManager_RelayToAllExcept_HappyPath(t *testing.T) {
- sm := NewInMemoryChatSessionManager(slog.Default())
- cookie := "the-cookie"
- user1, err := sm.AddSession(context.Background(), cookie, "user-screen-name-1")
- assert.NoError(t, err)
- user1.SetSignonComplete()
- user2, err := sm.AddSession(context.Background(), cookie, "user-screen-name-2")
- assert.NoError(t, err)
- user2.SetSignonComplete()
- user3, err := sm.AddSession(context.Background(), cookie, "user-screen-name-3")
- assert.NoError(t, err)
- user3.SetSignonComplete()
- want := wire.SNACMessage{Frame: wire.SNACFrame{FoodGroup: wire.ICBM}}
- sm.RelayToAllExcept(context.Background(), cookie, user2.IdentScreenName(), want)
- have := <-user1.ReceiveMessage()
- assert.Equal(t, want, have)
- select {
- case <-user2.ReceiveMessage():
- assert.Fail(t, "user 2 should not receive a message")
- default:
- }
- have = <-user3.ReceiveMessage()
- assert.Equal(t, want, have)
- }
- //func TestInMemoryChatSessionManager_AllSessions_RoomExists(t *testing.T) {
- // sm := NewInMemoryChatSessionManager(slog.Default())
- //
- // user1, err := sm.AddSession(context.Background(), "the-cookie", "user-screen-name-1")
- // assert.NoError(t, err)
- // user1.SetSignonComplete()
- // user2, err := sm.AddSession(context.Background(), "the-cookie", "user-screen-name-2")
- // assert.NoError(t, err)
- // user2.SetSignonComplete()
- //
- // sessions := sm.AllSessions("the-cookie")
- // assert.Len(t, sessions, 2)
- //
- // lookup := make(map[*SessionInstance]bool)
- // for _, session := range sessions {
- // lookup[session] = true
- // }
- //
- // assert.True(t, lookup[user1])
- // assert.True(t, lookup[user2])
- //}
- func TestInMemoryChatSessionManager_RelayToScreenName_SessionAndChatRoomExist(t *testing.T) {
- sm := NewInMemoryChatSessionManager(slog.Default())
- user1, err := sm.AddSession(context.Background(), "chat-room-1", "user-screen-name-1")
- assert.NoError(t, err)
- user1.SetSignonComplete()
- user2, err := sm.AddSession(context.Background(), "chat-room-1", "user-screen-name-2")
- assert.NoError(t, err)
- user2.SetSignonComplete()
- want := wire.SNACMessage{Frame: wire.SNACFrame{FoodGroup: wire.ICBM}}
- recip := NewIdentScreenName("user-screen-name-1")
- sm.RelayToScreenName(context.Background(), "chat-room-1", recip, want)
- have := <-user1.ReceiveMessage()
- assert.Equal(t, want, have)
- select {
- case <-user2.ReceiveMessage():
- assert.Fail(t, "user 2 should not receive a message")
- default:
- }
- }
- func TestInMemoryChatSessionManager_RemoveSession(t *testing.T) {
- sm := NewInMemoryChatSessionManager(slog.Default())
- user1, err := sm.AddSession(context.Background(), "chat-room-1", "user-screen-name-1")
- assert.NoError(t, err)
- user1.SetSignonComplete()
- user2, err := sm.AddSession(context.Background(), "chat-room-1", "user-screen-name-2")
- assert.NoError(t, err)
- user2.SetSignonComplete()
- assert.Len(t, sm.AllSessions("chat-room-1"), 2)
- sm.RemoveSession(user1.Session())
- sm.RemoveSession(user2.Session())
- assert.Empty(t, sm.AllSessions("chat-room-1"))
- }
- func TestInMemoryChatSessionManager_RemoveSession_DoubleLogin(t *testing.T) {
- for i := 0; i < 50; i++ { // shake out race conditions
- synctest.Test(t, func(t *testing.T) {
- sm := NewInMemoryChatSessionManager(slog.Default())
- chatSess1, err := sm.AddSession(context.Background(), "chat-room-1", "user-screen-name-1")
- assert.NoError(t, err)
- chatSess1.SetSignonComplete()
- wg := &sync.WaitGroup{}
- wg.Add(1)
- go func() {
- // add the session again. this call blocks until RemoveSession makes
- // room for the new session
- chatSess2, err := sm.AddSession(context.Background(), "chat-room-1", "user-screen-name-1")
- assert.NoError(t, err)
- assert.NotNil(t, chatSess2)
- chatSess2.SetSignonComplete()
- assert.Equal(t, chatSess1.DisplayScreenName(), chatSess2.DisplayScreenName())
- wg.Done()
- }()
- // wait for second call to AddSession() to block
- synctest.Wait()
- // AddSession() is blocked waiting for the lock, now unblock it
- sm.RemoveSession(chatSess1.Session())
- wg.Wait()
- })
- }
- }
- func TestInMemoryChatSessionManager_RemoveUserFromAllChats(t *testing.T) {
- sm := NewInMemoryChatSessionManager(slog.Default())
- user1 := NewIdentScreenName("user-screen-name-1")
- user1sess, err := sm.AddSession(context.Background(), "chat-room-1", "user-screen-name-1")
- assert.NoError(t, err)
- user1sess.SetSignonComplete()
- user2sess, err := sm.AddSession(context.Background(), "chat-room-1", "user-screen-name-2")
- assert.NoError(t, err)
- user2sess.SetSignonComplete()
- assert.Len(t, sm.AllSessions("chat-room-1"), 2)
- sm.RemoveUserFromAllChats(user1)
- lookup := make(map[*Session]bool)
- for _, session := range sm.AllSessions("chat-room-1") {
- lookup[session] = true
- }
- assert.False(t, lookup[user1sess.Session()])
- assert.True(t, lookup[user2sess.Session()])
- }
- // TestInMemoryChatSessionManager_NoDeadlockOnCloseHookReentry verifies that
- // session close hooks don't deadlock when they re-enter
- // InMemoryChatSessionManager.
- //
- // The deadlock scenario under test:
- //
- // 1. BOS session closes, triggering its close hook.
- // 2. The close hook calls RemoveUserFromAllChats, which iterates chat rooms
- // and calls CloseSession on the user's chat session.
- // 3. The chat session's close hook calls AllSessions, which acquires
- // mapMutex.RLock.
- //
- // If RemoveUserFromAllChats naively held mapMutex.RLock while calling
- // CloseSession in step 2, the AllSessions call in step 3 would attempt a
- // recursive read lock on the same goroutine—something sync.RWMutex forbids—and
- // deadlock. The production code avoids this by copying the session managers and
- // releasing the lock before calling CloseSession.
- func TestInMemoryChatSessionManager_NoDeadlockOnCloseHookReentry(t *testing.T) {
- user := DisplayScreenName("user-screen-name-1")
- cookie := "chat-room-1"
- bosSM := NewInMemorySessionManager(slog.Default())
- chatSM := NewInMemoryChatSessionManager(slog.Default())
- bosSess, err := bosSM.AddSession(context.Background(), user, false)
- require.NoError(t, err)
- bosSess.Session().SetIdentScreenName(user.IdentScreenName())
- bosSess.SetSignonComplete()
- chatSess, err := chatSM.AddSession(context.Background(), cookie, user)
- require.NoError(t, err)
- chatSess.SetSignonComplete()
- // Simulate the real signoff flow: when the BOS session closes, remove
- // the user from all chat rooms.
- bosSess.Session().OnSessionClose(func() {
- chatSM.RemoveUserFromAllChats(user.IdentScreenName())
- })
- // When the chat session closes, re-enter the chat session manager to
- // simulate some cleanup operations, which would deadlock if the manager's
- // lock were still held.
- chatSess.Session().OnSessionClose(func() {
- chatSM.AllSessions(cookie)
- })
- done := make(chan struct{})
- go func() {
- defer close(done)
- bosSess.Session().CloseSession()
- }()
- select {
- case <-done:
- case <-time.After(5 * time.Second):
- t.Fatal("timed out waiting for session close — probable deadlock in close hook chain")
- }
- }
- func TestInMemorySessionManager_RelayToAll_SkipIncompleteSignon(t *testing.T) {
- sm := NewInMemorySessionManager(slog.Default())
- user1, err := sm.AddSession(context.Background(), "user-screen-name-1", false)
- assert.NoError(t, err)
- user1.SetSignonComplete()
- user2, err := sm.AddSession(context.Background(), "user-screen-name-2", false)
- assert.NoError(t, err)
- // user2 has not completed signon
- want := wire.SNACMessage{Frame: wire.SNACFrame{FoodGroup: wire.ICBM}}
- sm.RelayToAll(context.Background(), want)
- have := <-user1.ReceiveMessage()
- assert.Equal(t, want, have)
- select {
- case <-user2.ReceiveMessage():
- assert.Fail(t, "user 2 should not receive a message because signon is incomplete")
- default:
- }
- }
- func TestInMemorySessionManager_RetrieveSession_IncompleteSignon(t *testing.T) {
- sm := NewInMemorySessionManager(slog.Default())
- user1, err := sm.AddSession(context.Background(), "user-screen-name-1", false)
- assert.NoError(t, err)
- // user1 has not completed signon
- sess := sm.RetrieveSession(NewIdentScreenName("user-screen-name-1"))
- assert.Nil(t, sess, "should return nil for session with incomplete signon")
- user1.SetSignonComplete()
- sess = sm.RetrieveSession(NewIdentScreenName("user-screen-name-1"))
- assert.NotNil(t, sess, "should return session after signon is complete")
- assert.Equal(t, user1.Session(), sess)
- }
- func TestInMemorySessionManager_RetrieveSession_CompleteSignon(t *testing.T) {
- sm := NewInMemorySessionManager(slog.Default())
- user1, err := sm.AddSession(context.Background(), "user-screen-name-1", false)
- assert.NoError(t, err)
- user1.SetSignonComplete()
- sess := sm.RetrieveSession(NewIdentScreenName("user-screen-name-1"))
- assert.NotNil(t, sess)
- assert.Equal(t, user1.Session(), sess)
- }
- func TestInMemorySessionManager_RelayToScreenNames_SkipIncompleteSignon(t *testing.T) {
- sm := NewInMemorySessionManager(slog.Default())
- user1, err := sm.AddSession(context.Background(), "user-screen-name-1", false)
- assert.NoError(t, err)
- user1.SetSignonComplete()
- user2, err := sm.AddSession(context.Background(), "user-screen-name-2", false)
- assert.NoError(t, err)
- // user2 has not completed signon
- user3, err := sm.AddSession(context.Background(), "user-screen-name-3", false)
- assert.NoError(t, err)
- user3.SetSignonComplete()
- want := wire.SNACMessage{Frame: wire.SNACFrame{FoodGroup: wire.ICBM}}
- recips := []IdentScreenName{
- NewIdentScreenName("user-screen-name-1"),
- NewIdentScreenName("user-screen-name-2"), // incomplete signon
- NewIdentScreenName("user-screen-name-3"),
- }
- sm.RelayToScreenNames(context.Background(), recips, want)
- have := <-user1.ReceiveMessage()
- assert.Equal(t, want, have)
- select {
- case <-user2.ReceiveMessage():
- assert.Fail(t, "user 2 should not receive a message because signon is incomplete")
- default:
- }
- have = <-user3.ReceiveMessage()
- assert.Equal(t, want, have)
- }
- func TestInMemorySessionManager_AllSessions_SkipIncompleteSignon(t *testing.T) {
- sm := NewInMemorySessionManager(slog.Default())
- user1, err := sm.AddSession(context.Background(), "user-screen-name-1", false)
- assert.NoError(t, err)
- user1.SetSignonComplete()
- user2, err := sm.AddSession(context.Background(), "user-screen-name-2", false)
- assert.NoError(t, err)
- // user2 has not completed signon
- user3, err := sm.AddSession(context.Background(), "user-screen-name-3", false)
- assert.NoError(t, err)
- user3.SetSignonComplete()
- sessions := sm.AllSessions()
- assert.Len(t, sessions, 2, "should only return sessions with complete signon")
- // Check that we have sessions for user1 and user3 (by checking Session identity)
- user1Found := false
- user3Found := false
- user2Found := false
- for _, session := range sessions {
- if session == user1.Session() {
- user1Found = true
- }
- if session == user2.Session() {
- user2Found = true
- }
- if session == user3.Session() {
- user3Found = true
- }
- }
- assert.True(t, user1Found, "user1 should be included (complete signon)")
- assert.False(t, user2Found, "user2 should not be included (incomplete signon)")
- assert.True(t, user3Found, "user3 should be included (complete signon)")
- }
- func TestInMemorySessionManager_RelayToScreenName_IncompleteSignon(t *testing.T) {
- sm := NewInMemorySessionManager(slog.Default())
- user1, err := sm.AddSession(context.Background(), "user-screen-name-1", false)
- assert.NoError(t, err)
- // user1 has not completed signon
- want := wire.SNACMessage{Frame: wire.SNACFrame{FoodGroup: wire.ICBM}}
- recip := NewIdentScreenName("user-screen-name-1")
- sm.RelayToScreenName(context.Background(), recip, want)
- select {
- case <-user1.ReceiveMessage():
- assert.Fail(t, "user 1 should not receive a message because signon is incomplete")
- default:
- }
- }
- func TestInMemorySessionManager_RelayToOtherInstances_SkipsNonLiveInstances(t *testing.T) {
- sm := NewInMemorySessionManager(slog.Default())
- // Create a session with multiple instances
- user1, err := sm.AddSession(context.Background(), "user-screen-name-1", true)
- assert.NoError(t, err)
- user1.SetSignonComplete()
- // Add a second instance that hasn't completed signon
- user1Instance2, err := sm.AddSession(context.Background(), "user-screen-name-1", true)
- assert.NoError(t, err)
- // user1Instance2 has not completed signon, so this instance is not live
- // Add a third instance that has completed signon
- user1Instance3, err := sm.AddSession(context.Background(), "user-screen-name-1", true)
- assert.NoError(t, err)
- user1Instance3.SetSignonComplete()
- // Verify instance-level live() behavior
- assert.True(t, user1.live(), "user1 should be live (not closed and signon complete)")
- assert.False(t, user1Instance2.live(), "user1Instance2 should not be live (signon not complete)")
- assert.True(t, user1Instance3.live(), "user1Instance3 should be live (not closed and signon complete)")
- want := wire.SNACMessage{Frame: wire.SNACFrame{FoodGroup: wire.ICBM}}
- // Relay to other instances from user1
- sm.RelayToOtherInstances(context.Background(), user1, want)
- // user1 should not receive the message (it's the sender)
- select {
- case <-user1.ReceiveMessage():
- assert.Fail(t, "user1 should not receive a message relayed from itself")
- default:
- }
- // user1Instance2 should not receive the message (not live - signon incomplete)
- select {
- case <-user1Instance2.ReceiveMessage():
- assert.Fail(t, "user1Instance2 should not receive a message because it's not live")
- default:
- }
- // user1Instance3 should receive the message (is live)
- select {
- case have := <-user1Instance3.ReceiveMessage():
- assert.Equal(t, want, have)
- default:
- assert.Fail(t, "user1Instance3 should receive the message")
- }
- }
- func TestInMemorySessionManager_MaybeRelayMessage_SkipsNonLiveInstances(t *testing.T) {
- sm := NewInMemorySessionManager(slog.Default())
- // Create a session with multiple instances
- user1, err := sm.AddSession(context.Background(), "user-screen-name-1", true)
- assert.NoError(t, err)
- user1.SetSignonComplete()
- // Add a third instance that has completed signon
- user1Instance3, err := sm.AddSession(context.Background(), "user-screen-name-1", true)
- assert.NoError(t, err)
- user1Instance3.SetSignonComplete()
- // Create a separate session with incomplete signon to test that non-live instances are skipped
- user2, err := sm.AddSession(context.Background(), "user-screen-name-2", false)
- assert.NoError(t, err)
- // user2 has not completed signon, so this instance is not live
- assert.False(t, user2.live(), "instance should not be live when signon is incomplete")
- want := wire.SNACMessage{Frame: wire.SNACFrame{FoodGroup: wire.ICBM}}
- // Use maybeRelayMessage (called internally by RelayToScreenName)
- // This should relay to all live instances in the session
- sm.RelayToScreenName(context.Background(), user1.IdentScreenName(), want)
- // user1 should receive the message
- select {
- case have := <-user1.ReceiveMessage():
- assert.Equal(t, want, have)
- default:
- assert.Fail(t, "user1 should receive the message")
- }
- // user1Instance3 should receive the message (session is live)
- select {
- case have := <-user1Instance3.ReceiveMessage():
- assert.Equal(t, want, have)
- default:
- assert.Fail(t, "user1Instance3 should receive the message")
- }
- // Test that non-live instances are skipped in RelayToAll (which calls maybeRelayMessage)
- sm.RelayToAll(context.Background(), want)
- // user2 should not receive the message (instance is not live, so maybeRelayMessage skips it)
- select {
- case <-user2.ReceiveMessage():
- assert.Fail(t, "user2 should not receive a message because the instance is not live")
- default:
- }
- }
- func TestInMemorySessionManager_AddSession_MaxConcurrentSessions(t *testing.T) {
- t.Run("enforces limit", func(t *testing.T) {
- sm := NewInMemorySessionManager(slog.Default())
- sm.maxConcurrentSessions = 5
- // Create sessions up to the limit (5)
- var sessList []*SessionInstance
- for i := 0; i < sm.maxConcurrentSessions; i++ {
- sess, err := sm.AddSession(context.Background(), "user-screen-name-1", true)
- assert.NoError(t, err)
- sess.SetSignonComplete()
- sessList = append(sessList, sess)
- }
- // Verify we have exactly 5 instances
- assert.Equal(t, sm.maxConcurrentSessions, sessList[0].Session().InstanceCount())
- // Try to add one more session - should fail with ErrMaxConcurrentSessionsReached
- sess, err := sm.AddSession(context.Background(), "user-screen-name-1", true)
- assert.Nil(t, sess)
- assert.ErrorIs(t, err, ErrMaxConcurrentSessionsReached)
- // Verify we still have exactly 5 instances
- assert.Equal(t, sm.maxConcurrentSessions, sessList[0].Session().InstanceCount())
- })
- t.Run("allows new session after removal", func(t *testing.T) {
- sm := NewInMemorySessionManager(slog.Default())
- sm.maxConcurrentSessions = 5
- // Create sessions up to the limit (5)
- var sessList []*SessionInstance
- for i := 0; i < sm.maxConcurrentSessions; i++ {
- sess, err := sm.AddSession(context.Background(), "user-screen-name-1", true)
- assert.NoError(t, err)
- sess.SetSignonComplete()
- sessList = append(sessList, sess)
- }
- // Verify we have exactly 5 instances
- assert.Equal(t, sm.maxConcurrentSessions, sessList[0].Session().InstanceCount())
- // Try to add one more session - should fail
- sess, err := sm.AddSession(context.Background(), "user-screen-name-1", true)
- assert.Nil(t, sess)
- assert.ErrorIs(t, err, ErrMaxConcurrentSessionsReached)
- // Close one instance (this removes it from the Session)
- sessList[0].CloseInstance()
- // Now we should be able to add a new instance to the same session
- newSess, err := sm.AddSession(context.Background(), "user-screen-name-1", true)
- assert.NoError(t, err)
- assert.NotNil(t, newSess)
- newSess.SetSignonComplete()
- // Verify we have exactly 5 instances again (4 remaining + 1 new = 5)
- assert.Equal(t, sm.maxConcurrentSessions, newSess.Session().InstanceCount())
- })
- t.Run("no limit for non-multi-session", func(t *testing.T) {
- sm := NewInMemorySessionManager(slog.Default())
- // Create multiple non-multi-session sessions - should not be limited
- // (though they will replace each other, but that's expected behavior)
- sess1, err := sm.AddSession(context.Background(), "user-screen-name-1", false)
- assert.NoError(t, err)
- sess1.SetSignonComplete()
- // Close and remove the first session to allow a new one
- go func() {
- <-sess1.Closed()
- sm.RemoveSession(sess1.Session())
- }()
- sess2, err := sm.AddSession(context.Background(), "user-screen-name-1", false)
- assert.NoError(t, err)
- sess2.SetSignonComplete()
- // Verify the limit doesn't apply to non-multi-session
- assert.Equal(t, 1, sess2.Session().InstanceCount())
- })
- }
|