session_manager_test.go 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110
  1. package state
  2. import (
  3. "context"
  4. "log/slog"
  5. "math/rand"
  6. "sync"
  7. "testing"
  8. "testing/synctest"
  9. "time"
  10. "github.com/mk6i/open-oscar-server/wire"
  11. "github.com/stretchr/testify/require"
  12. "github.com/stretchr/testify/assert"
  13. )
  14. func TestInMemorySessionManager_AddSession(t *testing.T) {
  15. sm := NewInMemorySessionManager(slog.Default())
  16. ctx := context.Background()
  17. sess1, err := sm.AddSession(ctx, "user-screen-name", false)
  18. assert.NoError(t, err)
  19. sess1.SetSignonComplete()
  20. go func() {
  21. <-sess1.Closed()
  22. sm.RemoveSession(sess1.Session())
  23. }()
  24. sess2, err := sm.AddSession(ctx, "user-screen-name", false)
  25. assert.NoError(t, err)
  26. sess2.SetSignonComplete()
  27. assert.NotSame(t, sess1, sess2)
  28. assert.Contains(t, sm.AllSessions(), sess2.Session())
  29. }
  30. func TestInMemorySessionManager_AddSession_AppliesCfgToSession(t *testing.T) {
  31. sm := NewInMemorySessionManager(slog.Default())
  32. wantUIN := uint32(424242)
  33. wantCookie := "cfg-test-cookie"
  34. instance, err := sm.AddSession(context.Background(), "user-screen-name", false,
  35. func(sess *Session) {
  36. sess.SetUIN(wantUIN)
  37. sess.SetChatRoomCookie(wantCookie)
  38. },
  39. )
  40. assert.NoError(t, err)
  41. s := instance.Session()
  42. assert.Equal(t, wantUIN, s.UIN(), "cfg mutates Session before AddInstance / store insert")
  43. assert.Equal(t, wantCookie, s.ChatRoomCookie())
  44. }
  45. func TestInMemorySessionManager_AddSession_Timeout(t *testing.T) {
  46. sm := NewInMemorySessionManager(slog.Default())
  47. ctx, cancel := context.WithCancel(context.Background())
  48. sess1, err := sm.AddSession(ctx, "user-screen-name", false)
  49. assert.NoError(t, err)
  50. sess1.SetSignonComplete()
  51. go func() {
  52. <-sess1.Closed()
  53. cancel()
  54. }()
  55. sess2, err := sm.AddSession(ctx, "user-screen-name", false)
  56. assert.Nil(t, sess2)
  57. assert.ErrorIs(t, err, context.Canceled)
  58. }
  59. func TestInMemorySessionManager_Remove_Existing(t *testing.T) {
  60. sm := NewInMemorySessionManager(slog.Default())
  61. user1Old, err := sm.AddSession(context.Background(), "user-screen-name-1", false)
  62. assert.NoError(t, err)
  63. // Verify the old session is in the store
  64. rec, ok := sm.store[user1Old.IdentScreenName()]
  65. assert.True(t, ok)
  66. assert.Equal(t, user1Old.Session(), rec.session)
  67. // Remove the session
  68. sm.RemoveSession(user1Old.Session())
  69. // Verify the session is no longer in the store
  70. _, ok = sm.store[user1Old.IdentScreenName()]
  71. assert.False(t, ok)
  72. // Verify the removed channel was closed
  73. select {
  74. case <-rec.removed:
  75. // Channel was closed, as expected
  76. default:
  77. assert.Fail(t, "removed channel should be closed")
  78. }
  79. user1New, err := sm.AddSession(context.Background(), "user-screen-name-1", false)
  80. assert.NoError(t, err)
  81. user1New.SetSignonComplete()
  82. user2, err := sm.AddSession(context.Background(), "user-screen-name-2", false)
  83. assert.NoError(t, err)
  84. user2.SetSignonComplete()
  85. // Remove user1New and verify it's gone
  86. sm.RemoveSession(user1New.Session())
  87. _, ok = sm.store[user1New.IdentScreenName()]
  88. assert.False(t, ok)
  89. if assert.Len(t, sm.AllSessions(), 1) {
  90. assert.NotContains(t, sm.AllSessions(), user1Old.Session())
  91. assert.NotContains(t, sm.AllSessions(), user1New.Session())
  92. assert.Contains(t, sm.AllSessions(), user2.Session())
  93. }
  94. }
  95. func TestInMemorySessionManager_Remove_MissingSameScreenName(t *testing.T) {
  96. sm := NewInMemorySessionManager(slog.Default())
  97. user1Old, err := sm.AddSession(context.Background(), "user-screen-name-1", false)
  98. assert.NoError(t, err)
  99. // Verify the old session is in the store
  100. recOld, ok := sm.store[user1Old.IdentScreenName()]
  101. assert.True(t, ok)
  102. assert.Equal(t, user1Old.Session(), recOld.session)
  103. // Remove the old session
  104. sm.RemoveSession(user1Old.Session())
  105. _, ok = sm.store[user1Old.IdentScreenName()]
  106. assert.False(t, ok)
  107. // Create a new session with the same screen name but different Session
  108. user1New, err := sm.AddSession(context.Background(), "user-screen-name-1", false)
  109. assert.NoError(t, err)
  110. user1New.SetSignonComplete()
  111. // Verify the new session is in the store with a different Session
  112. recNew, ok := sm.store[user1New.IdentScreenName()]
  113. assert.True(t, ok)
  114. assert.Equal(t, user1New.Session(), recNew.session)
  115. assert.NotEqual(t, user1Old.Session(), user1New.Session())
  116. user2, err := sm.AddSession(context.Background(), "user-screen-name-2", false)
  117. assert.NoError(t, err)
  118. user2.SetSignonComplete()
  119. // Try to remove the old session again - should do nothing because Session doesn't match
  120. sm.RemoveSession(user1Old.Session())
  121. // Verify the new session is still in the store (not removed)
  122. recNewAfter, ok := sm.store[user1New.IdentScreenName()]
  123. assert.True(t, ok, "new session should still be in store")
  124. assert.Equal(t, user1New.Session(), recNewAfter.session)
  125. if assert.Len(t, sm.AllSessions(), 2) {
  126. assert.NotContains(t, sm.AllSessions(), user1Old.Session())
  127. assert.Contains(t, sm.AllSessions(), user1New.Session())
  128. assert.Contains(t, sm.AllSessions(), user2.Session())
  129. }
  130. }
  131. func TestInMemorySessionManager_Empty(t *testing.T) {
  132. tests := []struct {
  133. name string
  134. given []DisplayScreenName
  135. want bool
  136. }{
  137. {
  138. name: "session manager is not empty",
  139. given: []DisplayScreenName{
  140. "user-screen-name-1",
  141. },
  142. want: false,
  143. },
  144. {
  145. name: "session manager is empty",
  146. given: []DisplayScreenName{},
  147. want: true,
  148. },
  149. }
  150. for _, tt := range tests {
  151. t.Run(tt.name, func(t *testing.T) {
  152. sm := NewInMemorySessionManager(slog.Default())
  153. for _, screenName := range tt.given {
  154. sess, err := sm.AddSession(context.Background(), screenName, false)
  155. assert.NoError(t, err)
  156. sess.SetSignonComplete()
  157. }
  158. have := sm.Empty()
  159. assert.Equal(t, tt.want, have)
  160. })
  161. }
  162. }
  163. func TestInMemorySessionManager_Retrieve(t *testing.T) {
  164. tests := []struct {
  165. name string
  166. given []DisplayScreenName
  167. lookupScreenName IdentScreenName
  168. wantScreenName IdentScreenName
  169. }{
  170. {
  171. name: "lookup finds match",
  172. given: []DisplayScreenName{
  173. "user-screen-name-1",
  174. "user-screen-name-2",
  175. },
  176. lookupScreenName: NewIdentScreenName("user-screen-name-2"),
  177. wantScreenName: NewIdentScreenName("user-screen-name-2"),
  178. },
  179. {
  180. name: "lookup does not find match",
  181. given: []DisplayScreenName{},
  182. lookupScreenName: NewIdentScreenName("user-screen-name-3"),
  183. wantScreenName: NewIdentScreenName(""),
  184. },
  185. }
  186. for _, tt := range tests {
  187. t.Run(tt.name, func(t *testing.T) {
  188. sm := NewInMemorySessionManager(slog.Default())
  189. for _, screenName := range tt.given {
  190. sess, err := sm.AddSession(context.Background(), screenName, false)
  191. assert.NoError(t, err)
  192. sess.SetSignonComplete()
  193. }
  194. have := sm.RetrieveSession(tt.lookupScreenName)
  195. if have == nil {
  196. assert.Empty(t, tt.wantScreenName)
  197. } else {
  198. assert.Equal(t, tt.wantScreenName, have.IdentScreenName())
  199. }
  200. })
  201. }
  202. }
  203. func TestInMemorySessionManager_RelayToScreenNames(t *testing.T) {
  204. sm := NewInMemorySessionManager(slog.Default())
  205. user1, err := sm.AddSession(context.Background(), "user-screen-name-1", false)
  206. assert.NoError(t, err)
  207. user1.SetSignonComplete()
  208. user2, err := sm.AddSession(context.Background(), "user-screen-name-2", false)
  209. assert.NoError(t, err)
  210. user2.SetSignonComplete()
  211. user3, err := sm.AddSession(context.Background(), "user-screen-name-3", false)
  212. assert.NoError(t, err)
  213. user3.SetSignonComplete()
  214. want := wire.SNACMessage{Frame: wire.SNACFrame{FoodGroup: wire.ICBM}}
  215. recips := []IdentScreenName{
  216. NewIdentScreenName("user-screen-name-1"),
  217. NewIdentScreenName("user-screen-name-2"),
  218. }
  219. sm.RelayToScreenNames(context.Background(), recips, want)
  220. have := <-user1.ReceiveMessage()
  221. assert.Equal(t, want, have)
  222. have = <-user2.ReceiveMessage()
  223. assert.Equal(t, want, have)
  224. select {
  225. case <-user3.ReceiveMessage():
  226. assert.Fail(t, "user 3 should not receive a message")
  227. default:
  228. }
  229. }
  230. func TestInMemorySessionManager_Broadcast(t *testing.T) {
  231. sm := NewInMemorySessionManager(slog.Default())
  232. user1, err := sm.AddSession(context.Background(), "user-screen-name-1", false)
  233. assert.NoError(t, err)
  234. user1.SetSignonComplete()
  235. user2, err := sm.AddSession(context.Background(), "user-screen-name-2", false)
  236. assert.NoError(t, err)
  237. user2.SetSignonComplete()
  238. want := wire.SNACMessage{Frame: wire.SNACFrame{FoodGroup: wire.ICBM}}
  239. sm.RelayToAll(context.Background(), want)
  240. have := <-user1.ReceiveMessage()
  241. assert.Equal(t, want, have)
  242. have = <-user2.ReceiveMessage()
  243. assert.Equal(t, want, have)
  244. }
  245. func TestInMemorySessionManager_Broadcast_SkipClosedSession(t *testing.T) {
  246. sm := NewInMemorySessionManager(slog.Default())
  247. user1, err := sm.AddSession(context.Background(), "user-screen-name-1", false)
  248. assert.NoError(t, err)
  249. user1.SetSignonComplete()
  250. user2, err := sm.AddSession(context.Background(), "user-screen-name-2", false)
  251. assert.NoError(t, err)
  252. user2.SetSignonComplete()
  253. user2.CloseInstance()
  254. want := wire.SNACMessage{Frame: wire.SNACFrame{FoodGroup: wire.ICBM}}
  255. sm.RelayToAll(context.Background(), want)
  256. have := <-user1.ReceiveMessage()
  257. assert.Equal(t, want, have)
  258. select {
  259. case <-user2.ReceiveMessage():
  260. assert.Fail(t, "user 2 should not receive a message")
  261. default:
  262. }
  263. }
  264. func TestInMemorySessionManager_RelayToScreenName_SessionExists(t *testing.T) {
  265. sm := NewInMemorySessionManager(slog.Default())
  266. user1, err := sm.AddSession(context.Background(), "user-screen-name-1", false)
  267. assert.NoError(t, err)
  268. user1.SetSignonComplete()
  269. user2, err := sm.AddSession(context.Background(), "user-screen-name-2", false)
  270. assert.NoError(t, err)
  271. user2.SetSignonComplete()
  272. want := wire.SNACMessage{Frame: wire.SNACFrame{FoodGroup: wire.ICBM}}
  273. recip := NewIdentScreenName("user-screen-name-1")
  274. sm.RelayToScreenName(context.Background(), recip, want)
  275. have := <-user1.ReceiveMessage()
  276. assert.Equal(t, want, have)
  277. select {
  278. case <-user2.ReceiveMessage():
  279. assert.Fail(t, "user 2 should not receive a message")
  280. default:
  281. }
  282. }
  283. func TestInMemorySessionManager_RelayToScreenName_SessionNotExist(t *testing.T) {
  284. sm := NewInMemorySessionManager(slog.Default())
  285. user1, err := sm.AddSession(context.Background(), "user-screen-name-1", false)
  286. assert.NoError(t, err)
  287. user1.SetSignonComplete()
  288. want := wire.SNACMessage{Frame: wire.SNACFrame{FoodGroup: wire.ICBM}}
  289. recip := NewIdentScreenName("user-screen-name-2")
  290. sm.RelayToScreenName(context.Background(), recip, want)
  291. select {
  292. case <-user1.ReceiveMessage():
  293. assert.Fail(t, "user 1 should not receive a message")
  294. default:
  295. }
  296. }
  297. func TestInMemorySessionManager_RelayToScreenName_SkipFullSession(t *testing.T) {
  298. sm := NewInMemorySessionManager(slog.Default())
  299. user1, err := sm.AddSession(context.Background(), "user-screen-name-1", false)
  300. assert.NoError(t, err)
  301. user1.SetSignonComplete()
  302. msg := wire.SNACMessage{Frame: wire.SNACFrame{FoodGroup: wire.ICBM}}
  303. wantCount := 0
  304. for user1.RelayMessageToInstance(msg) != SessQueueFull {
  305. wantCount++
  306. }
  307. recip := NewIdentScreenName("user-screen-name-1")
  308. sm.RelayToScreenName(context.Background(), recip, msg)
  309. haveCount := 0
  310. loop:
  311. for {
  312. select {
  313. case <-user1.ReceiveMessage():
  314. haveCount++
  315. default:
  316. break loop
  317. }
  318. }
  319. assert.Equal(t, wantCount, haveCount)
  320. }
  321. func TestInMemorySessionManager_SessionReplacement_NoMultiSess_NoMultiSess(t *testing.T) {
  322. synctest.Test(t, func(t *testing.T) {
  323. sm := NewInMemorySessionManager(slog.Default())
  324. sess1, err := sm.AddSession(context.Background(), "user-screen-name-1", false)
  325. assert.NoError(t, err)
  326. sess1.SetSignonComplete()
  327. wg := &sync.WaitGroup{}
  328. wg.Add(1)
  329. go func() {
  330. defer wg.Done()
  331. // add the session again. this call blocks until RemoveSession makes
  332. // room for the new session
  333. sess2, err := sm.AddSession(context.Background(), "user-screen-name-1", false)
  334. assert.NoError(t, err)
  335. if assert.NotNil(t, sess2) {
  336. sess2.SetSignonComplete()
  337. assert.Equal(t, sess1.DisplayScreenName(), sess2.DisplayScreenName())
  338. }
  339. }()
  340. // wait for second call to AddSession() to block
  341. synctest.Wait()
  342. // AddSession() is blocked waiting for the lock, now unblock it
  343. sm.RemoveSession(sess1.Session())
  344. wg.Wait()
  345. // make sure we got a brand new session
  346. got := sm.RetrieveSession(NewIdentScreenName("user-screen-name-1"))
  347. assert.NotEqual(t, sess1, got)
  348. assert.Equal(t, 1, got.InstanceCount())
  349. })
  350. }
  351. func TestInMemorySessionManager_SessionReplacement_MultiSess_NoMultiSess(t *testing.T) {
  352. synctest.Test(t, func(t *testing.T) {
  353. sm := NewInMemorySessionManager(slog.Default())
  354. sm.maxConcurrentSessions = 5
  355. var sessList []*SessionInstance
  356. for i := 0; i < sm.maxConcurrentSessions; i++ {
  357. sess, err := sm.AddSession(context.Background(), "user-screen-name-1", true)
  358. assert.NoError(t, err)
  359. sess.SetSignonComplete()
  360. sessList = append(sessList, sess)
  361. }
  362. assert.Equal(t, len(sessList), sessList[0].Session().InstanceCount())
  363. wg := &sync.WaitGroup{}
  364. wg.Add(1)
  365. go func() {
  366. defer wg.Done()
  367. // add the session again. this call blocks until RemoveSession makes
  368. // room for the new session
  369. sess, err := sm.AddSession(context.Background(), "user-screen-name-1", false)
  370. assert.NoError(t, err)
  371. assert.NotNil(t, sess)
  372. sess.SetSignonComplete()
  373. assert.Equal(t, "user-screen-name-1", sess.DisplayScreenName().String())
  374. assert.Equal(t, 1, sess.Session().InstanceCount())
  375. }()
  376. // wait for the last call to AddSession() to block
  377. synctest.Wait()
  378. // AddSession() is blocked waiting for the lock, now unblock it
  379. for _, sess := range sessList {
  380. sm.RemoveSession(sess.Session())
  381. }
  382. wg.Wait()
  383. got := sm.RetrieveSession(NewIdentScreenName("user-screen-name-1"))
  384. for _, sess := range sessList {
  385. assert.NotSame(t, sess, got)
  386. }
  387. assert.Equal(t, 1, got.InstanceCount())
  388. })
  389. }
  390. func TestInMemorySessionManager_SessionReplacement_NoMultiSess_MultiSess(t *testing.T) {
  391. synctest.Test(t, func(t *testing.T) {
  392. sm := NewInMemorySessionManager(slog.Default())
  393. sess1, err := sm.AddSession(context.Background(), "user-screen-name-1", false)
  394. assert.NoError(t, err)
  395. sess1.SetSignonComplete()
  396. wg := &sync.WaitGroup{}
  397. wg.Add(1)
  398. go func() {
  399. defer wg.Done()
  400. // add the session again. this call blocks until RemoveSession makes
  401. // room for the new session
  402. sess2, err := sm.AddSession(context.Background(), "user-screen-name-1", true)
  403. assert.NoError(t, err)
  404. assert.NotNil(t, sess2)
  405. assert.Equal(t, sess1.DisplayScreenName(), sess2.DisplayScreenName())
  406. sess2.SetSignonComplete()
  407. }()
  408. // wait for second call to AddSession() to block
  409. synctest.Wait()
  410. // AddSession() is blocked waiting for the lock, now unblock it
  411. sm.RemoveSession(sess1.Session())
  412. wg.Wait()
  413. got := sm.RetrieveSession(NewIdentScreenName("user-screen-name-1"))
  414. if assert.NotNil(t, got) {
  415. assert.NotSame(t, sess1, got)
  416. assert.Equal(t, 1, got.InstanceCount())
  417. }
  418. })
  419. }
  420. func TestInMemorySessionManager_RemoveSession_DoubleLogin_NoMultiSess_Chaos(t *testing.T) {
  421. wg := &sync.WaitGroup{}
  422. sm := NewInMemorySessionManager(slog.Default())
  423. for i := 0; i < 1000; i++ { // shake out race conditions
  424. wg.Add(1)
  425. time.Sleep(time.Duration(rand.Intn(1000)) * time.Microsecond)
  426. go func() {
  427. defer wg.Done()
  428. sess1, err := sm.AddSession(context.Background(), "user-screen-name-1", false)
  429. assert.NoError(t, err)
  430. time.Sleep(time.Duration(rand.Intn(1000)) * time.Microsecond)
  431. sm.RemoveSession(sess1.Session())
  432. }()
  433. }
  434. wg.Wait()
  435. }
  436. func TestInMemoryChatSessionManager_AddSession_AppliesCfgToSession(t *testing.T) {
  437. sm := NewInMemoryChatSessionManager(slog.Default())
  438. chatCookie := "chat-room-cfg"
  439. wantUIN := uint32(777001)
  440. instance, err := sm.AddSession(context.Background(), chatCookie, "user-screen-name",
  441. func(sess *Session) {
  442. sess.SetUIN(wantUIN)
  443. },
  444. )
  445. assert.NoError(t, err)
  446. s := instance.Session()
  447. assert.Equal(t, wantUIN, s.UIN(), "cfg mutates Session via inner AddSession before instance is returned")
  448. assert.Equal(t, chatCookie, s.ChatRoomCookie())
  449. }
  450. func TestInMemoryChatSessionManager_RelayToAllExcept_HappyPath(t *testing.T) {
  451. sm := NewInMemoryChatSessionManager(slog.Default())
  452. cookie := "the-cookie"
  453. user1, err := sm.AddSession(context.Background(), cookie, "user-screen-name-1")
  454. assert.NoError(t, err)
  455. user1.SetSignonComplete()
  456. user2, err := sm.AddSession(context.Background(), cookie, "user-screen-name-2")
  457. assert.NoError(t, err)
  458. user2.SetSignonComplete()
  459. user3, err := sm.AddSession(context.Background(), cookie, "user-screen-name-3")
  460. assert.NoError(t, err)
  461. user3.SetSignonComplete()
  462. want := wire.SNACMessage{Frame: wire.SNACFrame{FoodGroup: wire.ICBM}}
  463. sm.RelayToAllExcept(context.Background(), cookie, user2.IdentScreenName(), want)
  464. have := <-user1.ReceiveMessage()
  465. assert.Equal(t, want, have)
  466. select {
  467. case <-user2.ReceiveMessage():
  468. assert.Fail(t, "user 2 should not receive a message")
  469. default:
  470. }
  471. have = <-user3.ReceiveMessage()
  472. assert.Equal(t, want, have)
  473. }
  474. //func TestInMemoryChatSessionManager_AllSessions_RoomExists(t *testing.T) {
  475. // sm := NewInMemoryChatSessionManager(slog.Default())
  476. //
  477. // user1, err := sm.AddSession(context.Background(), "the-cookie", "user-screen-name-1")
  478. // assert.NoError(t, err)
  479. // user1.SetSignonComplete()
  480. // user2, err := sm.AddSession(context.Background(), "the-cookie", "user-screen-name-2")
  481. // assert.NoError(t, err)
  482. // user2.SetSignonComplete()
  483. //
  484. // sessions := sm.AllSessions("the-cookie")
  485. // assert.Len(t, sessions, 2)
  486. //
  487. // lookup := make(map[*SessionInstance]bool)
  488. // for _, session := range sessions {
  489. // lookup[session] = true
  490. // }
  491. //
  492. // assert.True(t, lookup[user1])
  493. // assert.True(t, lookup[user2])
  494. //}
  495. func TestInMemoryChatSessionManager_RelayToScreenName_SessionAndChatRoomExist(t *testing.T) {
  496. sm := NewInMemoryChatSessionManager(slog.Default())
  497. user1, err := sm.AddSession(context.Background(), "chat-room-1", "user-screen-name-1")
  498. assert.NoError(t, err)
  499. user1.SetSignonComplete()
  500. user2, err := sm.AddSession(context.Background(), "chat-room-1", "user-screen-name-2")
  501. assert.NoError(t, err)
  502. user2.SetSignonComplete()
  503. want := wire.SNACMessage{Frame: wire.SNACFrame{FoodGroup: wire.ICBM}}
  504. recip := NewIdentScreenName("user-screen-name-1")
  505. sm.RelayToScreenName(context.Background(), "chat-room-1", recip, want)
  506. have := <-user1.ReceiveMessage()
  507. assert.Equal(t, want, have)
  508. select {
  509. case <-user2.ReceiveMessage():
  510. assert.Fail(t, "user 2 should not receive a message")
  511. default:
  512. }
  513. }
  514. func TestInMemoryChatSessionManager_RemoveSession(t *testing.T) {
  515. sm := NewInMemoryChatSessionManager(slog.Default())
  516. user1, err := sm.AddSession(context.Background(), "chat-room-1", "user-screen-name-1")
  517. assert.NoError(t, err)
  518. user1.SetSignonComplete()
  519. user2, err := sm.AddSession(context.Background(), "chat-room-1", "user-screen-name-2")
  520. assert.NoError(t, err)
  521. user2.SetSignonComplete()
  522. assert.Len(t, sm.AllSessions("chat-room-1"), 2)
  523. sm.RemoveSession(user1.Session())
  524. sm.RemoveSession(user2.Session())
  525. assert.Empty(t, sm.AllSessions("chat-room-1"))
  526. }
  527. func TestInMemoryChatSessionManager_RemoveSession_DoubleLogin(t *testing.T) {
  528. for i := 0; i < 50; i++ { // shake out race conditions
  529. synctest.Test(t, func(t *testing.T) {
  530. sm := NewInMemoryChatSessionManager(slog.Default())
  531. chatSess1, err := sm.AddSession(context.Background(), "chat-room-1", "user-screen-name-1")
  532. assert.NoError(t, err)
  533. chatSess1.SetSignonComplete()
  534. wg := &sync.WaitGroup{}
  535. wg.Add(1)
  536. go func() {
  537. // add the session again. this call blocks until RemoveSession makes
  538. // room for the new session
  539. chatSess2, err := sm.AddSession(context.Background(), "chat-room-1", "user-screen-name-1")
  540. assert.NoError(t, err)
  541. assert.NotNil(t, chatSess2)
  542. chatSess2.SetSignonComplete()
  543. assert.Equal(t, chatSess1.DisplayScreenName(), chatSess2.DisplayScreenName())
  544. wg.Done()
  545. }()
  546. // wait for second call to AddSession() to block
  547. synctest.Wait()
  548. // AddSession() is blocked waiting for the lock, now unblock it
  549. sm.RemoveSession(chatSess1.Session())
  550. wg.Wait()
  551. })
  552. }
  553. }
  554. func TestInMemoryChatSessionManager_RemoveUserFromAllChats(t *testing.T) {
  555. sm := NewInMemoryChatSessionManager(slog.Default())
  556. user1 := NewIdentScreenName("user-screen-name-1")
  557. user1sess, err := sm.AddSession(context.Background(), "chat-room-1", "user-screen-name-1")
  558. assert.NoError(t, err)
  559. user1sess.SetSignonComplete()
  560. user2sess, err := sm.AddSession(context.Background(), "chat-room-1", "user-screen-name-2")
  561. assert.NoError(t, err)
  562. user2sess.SetSignonComplete()
  563. assert.Len(t, sm.AllSessions("chat-room-1"), 2)
  564. sm.RemoveUserFromAllChats(user1)
  565. lookup := make(map[*Session]bool)
  566. for _, session := range sm.AllSessions("chat-room-1") {
  567. lookup[session] = true
  568. }
  569. assert.False(t, lookup[user1sess.Session()])
  570. assert.True(t, lookup[user2sess.Session()])
  571. }
  572. // TestInMemoryChatSessionManager_NoDeadlockOnCloseHookReentry verifies that
  573. // session close hooks don't deadlock when they re-enter
  574. // InMemoryChatSessionManager.
  575. //
  576. // The deadlock scenario under test:
  577. //
  578. // 1. BOS session closes, triggering its close hook.
  579. // 2. The close hook calls RemoveUserFromAllChats, which iterates chat rooms
  580. // and calls CloseSession on the user's chat session.
  581. // 3. The chat session's close hook calls AllSessions, which acquires
  582. // mapMutex.RLock.
  583. //
  584. // If RemoveUserFromAllChats naively held mapMutex.RLock while calling
  585. // CloseSession in step 2, the AllSessions call in step 3 would attempt a
  586. // recursive read lock on the same goroutine—something sync.RWMutex forbids—and
  587. // deadlock. The production code avoids this by copying the session managers and
  588. // releasing the lock before calling CloseSession.
  589. func TestInMemoryChatSessionManager_NoDeadlockOnCloseHookReentry(t *testing.T) {
  590. user := DisplayScreenName("user-screen-name-1")
  591. cookie := "chat-room-1"
  592. bosSM := NewInMemorySessionManager(slog.Default())
  593. chatSM := NewInMemoryChatSessionManager(slog.Default())
  594. bosSess, err := bosSM.AddSession(context.Background(), user, false)
  595. require.NoError(t, err)
  596. bosSess.Session().SetIdentScreenName(user.IdentScreenName())
  597. bosSess.SetSignonComplete()
  598. chatSess, err := chatSM.AddSession(context.Background(), cookie, user)
  599. require.NoError(t, err)
  600. chatSess.SetSignonComplete()
  601. // Simulate the real signoff flow: when the BOS session closes, remove
  602. // the user from all chat rooms.
  603. bosSess.Session().OnSessionClose(func() {
  604. chatSM.RemoveUserFromAllChats(user.IdentScreenName())
  605. })
  606. // When the chat session closes, re-enter the chat session manager to
  607. // simulate some cleanup operations, which would deadlock if the manager's
  608. // lock were still held.
  609. chatSess.Session().OnSessionClose(func() {
  610. chatSM.AllSessions(cookie)
  611. })
  612. done := make(chan struct{})
  613. go func() {
  614. defer close(done)
  615. bosSess.Session().CloseSession()
  616. }()
  617. select {
  618. case <-done:
  619. case <-time.After(5 * time.Second):
  620. t.Fatal("timed out waiting for session close — probable deadlock in close hook chain")
  621. }
  622. }
  623. func TestInMemorySessionManager_RelayToAll_SkipIncompleteSignon(t *testing.T) {
  624. sm := NewInMemorySessionManager(slog.Default())
  625. user1, err := sm.AddSession(context.Background(), "user-screen-name-1", false)
  626. assert.NoError(t, err)
  627. user1.SetSignonComplete()
  628. user2, err := sm.AddSession(context.Background(), "user-screen-name-2", false)
  629. assert.NoError(t, err)
  630. // user2 has not completed signon
  631. want := wire.SNACMessage{Frame: wire.SNACFrame{FoodGroup: wire.ICBM}}
  632. sm.RelayToAll(context.Background(), want)
  633. have := <-user1.ReceiveMessage()
  634. assert.Equal(t, want, have)
  635. select {
  636. case <-user2.ReceiveMessage():
  637. assert.Fail(t, "user 2 should not receive a message because signon is incomplete")
  638. default:
  639. }
  640. }
  641. func TestInMemorySessionManager_RetrieveSession_IncompleteSignon(t *testing.T) {
  642. sm := NewInMemorySessionManager(slog.Default())
  643. user1, err := sm.AddSession(context.Background(), "user-screen-name-1", false)
  644. assert.NoError(t, err)
  645. // user1 has not completed signon
  646. sess := sm.RetrieveSession(NewIdentScreenName("user-screen-name-1"))
  647. assert.Nil(t, sess, "should return nil for session with incomplete signon")
  648. user1.SetSignonComplete()
  649. sess = sm.RetrieveSession(NewIdentScreenName("user-screen-name-1"))
  650. assert.NotNil(t, sess, "should return session after signon is complete")
  651. assert.Equal(t, user1.Session(), sess)
  652. }
  653. func TestInMemorySessionManager_RetrieveSession_CompleteSignon(t *testing.T) {
  654. sm := NewInMemorySessionManager(slog.Default())
  655. user1, err := sm.AddSession(context.Background(), "user-screen-name-1", false)
  656. assert.NoError(t, err)
  657. user1.SetSignonComplete()
  658. sess := sm.RetrieveSession(NewIdentScreenName("user-screen-name-1"))
  659. assert.NotNil(t, sess)
  660. assert.Equal(t, user1.Session(), sess)
  661. }
  662. func TestInMemorySessionManager_RelayToScreenNames_SkipIncompleteSignon(t *testing.T) {
  663. sm := NewInMemorySessionManager(slog.Default())
  664. user1, err := sm.AddSession(context.Background(), "user-screen-name-1", false)
  665. assert.NoError(t, err)
  666. user1.SetSignonComplete()
  667. user2, err := sm.AddSession(context.Background(), "user-screen-name-2", false)
  668. assert.NoError(t, err)
  669. // user2 has not completed signon
  670. user3, err := sm.AddSession(context.Background(), "user-screen-name-3", false)
  671. assert.NoError(t, err)
  672. user3.SetSignonComplete()
  673. want := wire.SNACMessage{Frame: wire.SNACFrame{FoodGroup: wire.ICBM}}
  674. recips := []IdentScreenName{
  675. NewIdentScreenName("user-screen-name-1"),
  676. NewIdentScreenName("user-screen-name-2"), // incomplete signon
  677. NewIdentScreenName("user-screen-name-3"),
  678. }
  679. sm.RelayToScreenNames(context.Background(), recips, want)
  680. have := <-user1.ReceiveMessage()
  681. assert.Equal(t, want, have)
  682. select {
  683. case <-user2.ReceiveMessage():
  684. assert.Fail(t, "user 2 should not receive a message because signon is incomplete")
  685. default:
  686. }
  687. have = <-user3.ReceiveMessage()
  688. assert.Equal(t, want, have)
  689. }
  690. func TestInMemorySessionManager_AllSessions_SkipIncompleteSignon(t *testing.T) {
  691. sm := NewInMemorySessionManager(slog.Default())
  692. user1, err := sm.AddSession(context.Background(), "user-screen-name-1", false)
  693. assert.NoError(t, err)
  694. user1.SetSignonComplete()
  695. user2, err := sm.AddSession(context.Background(), "user-screen-name-2", false)
  696. assert.NoError(t, err)
  697. // user2 has not completed signon
  698. user3, err := sm.AddSession(context.Background(), "user-screen-name-3", false)
  699. assert.NoError(t, err)
  700. user3.SetSignonComplete()
  701. sessions := sm.AllSessions()
  702. assert.Len(t, sessions, 2, "should only return sessions with complete signon")
  703. // Check that we have sessions for user1 and user3 (by checking Session identity)
  704. user1Found := false
  705. user3Found := false
  706. user2Found := false
  707. for _, session := range sessions {
  708. if session == user1.Session() {
  709. user1Found = true
  710. }
  711. if session == user2.Session() {
  712. user2Found = true
  713. }
  714. if session == user3.Session() {
  715. user3Found = true
  716. }
  717. }
  718. assert.True(t, user1Found, "user1 should be included (complete signon)")
  719. assert.False(t, user2Found, "user2 should not be included (incomplete signon)")
  720. assert.True(t, user3Found, "user3 should be included (complete signon)")
  721. }
  722. func TestInMemorySessionManager_RelayToScreenName_IncompleteSignon(t *testing.T) {
  723. sm := NewInMemorySessionManager(slog.Default())
  724. user1, err := sm.AddSession(context.Background(), "user-screen-name-1", false)
  725. assert.NoError(t, err)
  726. // user1 has not completed signon
  727. want := wire.SNACMessage{Frame: wire.SNACFrame{FoodGroup: wire.ICBM}}
  728. recip := NewIdentScreenName("user-screen-name-1")
  729. sm.RelayToScreenName(context.Background(), recip, want)
  730. select {
  731. case <-user1.ReceiveMessage():
  732. assert.Fail(t, "user 1 should not receive a message because signon is incomplete")
  733. default:
  734. }
  735. }
  736. func TestInMemorySessionManager_RelayToOtherInstances_SkipsNonLiveInstances(t *testing.T) {
  737. sm := NewInMemorySessionManager(slog.Default())
  738. // Create a session with multiple instances
  739. user1, err := sm.AddSession(context.Background(), "user-screen-name-1", true)
  740. assert.NoError(t, err)
  741. user1.SetSignonComplete()
  742. // Add a second instance that hasn't completed signon
  743. user1Instance2, err := sm.AddSession(context.Background(), "user-screen-name-1", true)
  744. assert.NoError(t, err)
  745. // user1Instance2 has not completed signon, so this instance is not live
  746. // Add a third instance that has completed signon
  747. user1Instance3, err := sm.AddSession(context.Background(), "user-screen-name-1", true)
  748. assert.NoError(t, err)
  749. user1Instance3.SetSignonComplete()
  750. // Verify instance-level live() behavior
  751. assert.True(t, user1.live(), "user1 should be live (not closed and signon complete)")
  752. assert.False(t, user1Instance2.live(), "user1Instance2 should not be live (signon not complete)")
  753. assert.True(t, user1Instance3.live(), "user1Instance3 should be live (not closed and signon complete)")
  754. want := wire.SNACMessage{Frame: wire.SNACFrame{FoodGroup: wire.ICBM}}
  755. // Relay to other instances from user1
  756. sm.RelayToOtherInstances(context.Background(), user1, want)
  757. // user1 should not receive the message (it's the sender)
  758. select {
  759. case <-user1.ReceiveMessage():
  760. assert.Fail(t, "user1 should not receive a message relayed from itself")
  761. default:
  762. }
  763. // user1Instance2 should not receive the message (not live - signon incomplete)
  764. select {
  765. case <-user1Instance2.ReceiveMessage():
  766. assert.Fail(t, "user1Instance2 should not receive a message because it's not live")
  767. default:
  768. }
  769. // user1Instance3 should receive the message (is live)
  770. select {
  771. case have := <-user1Instance3.ReceiveMessage():
  772. assert.Equal(t, want, have)
  773. default:
  774. assert.Fail(t, "user1Instance3 should receive the message")
  775. }
  776. }
  777. func TestInMemorySessionManager_MaybeRelayMessage_SkipsNonLiveInstances(t *testing.T) {
  778. sm := NewInMemorySessionManager(slog.Default())
  779. // Create a session with multiple instances
  780. user1, err := sm.AddSession(context.Background(), "user-screen-name-1", true)
  781. assert.NoError(t, err)
  782. user1.SetSignonComplete()
  783. // Add a third instance that has completed signon
  784. user1Instance3, err := sm.AddSession(context.Background(), "user-screen-name-1", true)
  785. assert.NoError(t, err)
  786. user1Instance3.SetSignonComplete()
  787. // Create a separate session with incomplete signon to test that non-live instances are skipped
  788. user2, err := sm.AddSession(context.Background(), "user-screen-name-2", false)
  789. assert.NoError(t, err)
  790. // user2 has not completed signon, so this instance is not live
  791. assert.False(t, user2.live(), "instance should not be live when signon is incomplete")
  792. want := wire.SNACMessage{Frame: wire.SNACFrame{FoodGroup: wire.ICBM}}
  793. // Use maybeRelayMessage (called internally by RelayToScreenName)
  794. // This should relay to all live instances in the session
  795. sm.RelayToScreenName(context.Background(), user1.IdentScreenName(), want)
  796. // user1 should receive the message
  797. select {
  798. case have := <-user1.ReceiveMessage():
  799. assert.Equal(t, want, have)
  800. default:
  801. assert.Fail(t, "user1 should receive the message")
  802. }
  803. // user1Instance3 should receive the message (session is live)
  804. select {
  805. case have := <-user1Instance3.ReceiveMessage():
  806. assert.Equal(t, want, have)
  807. default:
  808. assert.Fail(t, "user1Instance3 should receive the message")
  809. }
  810. // Test that non-live instances are skipped in RelayToAll (which calls maybeRelayMessage)
  811. sm.RelayToAll(context.Background(), want)
  812. // user2 should not receive the message (instance is not live, so maybeRelayMessage skips it)
  813. select {
  814. case <-user2.ReceiveMessage():
  815. assert.Fail(t, "user2 should not receive a message because the instance is not live")
  816. default:
  817. }
  818. }
  819. func TestInMemorySessionManager_AddSession_MaxConcurrentSessions(t *testing.T) {
  820. t.Run("enforces limit", func(t *testing.T) {
  821. sm := NewInMemorySessionManager(slog.Default())
  822. sm.maxConcurrentSessions = 5
  823. // Create sessions up to the limit (5)
  824. var sessList []*SessionInstance
  825. for i := 0; i < sm.maxConcurrentSessions; i++ {
  826. sess, err := sm.AddSession(context.Background(), "user-screen-name-1", true)
  827. assert.NoError(t, err)
  828. sess.SetSignonComplete()
  829. sessList = append(sessList, sess)
  830. }
  831. // Verify we have exactly 5 instances
  832. assert.Equal(t, sm.maxConcurrentSessions, sessList[0].Session().InstanceCount())
  833. // Try to add one more session - should fail with ErrMaxConcurrentSessionsReached
  834. sess, err := sm.AddSession(context.Background(), "user-screen-name-1", true)
  835. assert.Nil(t, sess)
  836. assert.ErrorIs(t, err, ErrMaxConcurrentSessionsReached)
  837. // Verify we still have exactly 5 instances
  838. assert.Equal(t, sm.maxConcurrentSessions, sessList[0].Session().InstanceCount())
  839. })
  840. t.Run("allows new session after removal", func(t *testing.T) {
  841. sm := NewInMemorySessionManager(slog.Default())
  842. sm.maxConcurrentSessions = 5
  843. // Create sessions up to the limit (5)
  844. var sessList []*SessionInstance
  845. for i := 0; i < sm.maxConcurrentSessions; i++ {
  846. sess, err := sm.AddSession(context.Background(), "user-screen-name-1", true)
  847. assert.NoError(t, err)
  848. sess.SetSignonComplete()
  849. sessList = append(sessList, sess)
  850. }
  851. // Verify we have exactly 5 instances
  852. assert.Equal(t, sm.maxConcurrentSessions, sessList[0].Session().InstanceCount())
  853. // Try to add one more session - should fail
  854. sess, err := sm.AddSession(context.Background(), "user-screen-name-1", true)
  855. assert.Nil(t, sess)
  856. assert.ErrorIs(t, err, ErrMaxConcurrentSessionsReached)
  857. // Close one instance (this removes it from the Session)
  858. sessList[0].CloseInstance()
  859. // Now we should be able to add a new instance to the same session
  860. newSess, err := sm.AddSession(context.Background(), "user-screen-name-1", true)
  861. assert.NoError(t, err)
  862. assert.NotNil(t, newSess)
  863. newSess.SetSignonComplete()
  864. // Verify we have exactly 5 instances again (4 remaining + 1 new = 5)
  865. assert.Equal(t, sm.maxConcurrentSessions, newSess.Session().InstanceCount())
  866. })
  867. t.Run("no limit for non-multi-session", func(t *testing.T) {
  868. sm := NewInMemorySessionManager(slog.Default())
  869. // Create multiple non-multi-session sessions - should not be limited
  870. // (though they will replace each other, but that's expected behavior)
  871. sess1, err := sm.AddSession(context.Background(), "user-screen-name-1", false)
  872. assert.NoError(t, err)
  873. sess1.SetSignonComplete()
  874. // Close and remove the first session to allow a new one
  875. go func() {
  876. <-sess1.Closed()
  877. sm.RemoveSession(sess1.Session())
  878. }()
  879. sess2, err := sm.AddSession(context.Background(), "user-screen-name-1", false)
  880. assert.NoError(t, err)
  881. sess2.SetSignonComplete()
  882. // Verify the limit doesn't apply to non-multi-session
  883. assert.Equal(t, 1, sess2.Session().InstanceCount())
  884. })
  885. }