session_manager_test.go 33 KB

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