session_manager_test.go 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137
  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. select {
  221. case have := <-user1.ReceiveMessage():
  222. assert.Equal(t, want, have)
  223. }
  224. select {
  225. case have := <-user2.ReceiveMessage():
  226. assert.Equal(t, want, have)
  227. }
  228. select {
  229. case <-user3.ReceiveMessage():
  230. assert.Fail(t, "user 3 should not receive a message")
  231. default:
  232. }
  233. }
  234. func TestInMemorySessionManager_Broadcast(t *testing.T) {
  235. sm := NewInMemorySessionManager(slog.Default())
  236. user1, err := sm.AddSession(context.Background(), "user-screen-name-1", false)
  237. assert.NoError(t, err)
  238. user1.SetSignonComplete()
  239. user2, err := sm.AddSession(context.Background(), "user-screen-name-2", false)
  240. assert.NoError(t, err)
  241. user2.SetSignonComplete()
  242. want := wire.SNACMessage{Frame: wire.SNACFrame{FoodGroup: wire.ICBM}}
  243. sm.RelayToAll(context.Background(), want)
  244. select {
  245. case have := <-user1.ReceiveMessage():
  246. assert.Equal(t, want, have)
  247. }
  248. select {
  249. case have := <-user2.ReceiveMessage():
  250. assert.Equal(t, want, have)
  251. }
  252. }
  253. func TestInMemorySessionManager_Broadcast_SkipClosedSession(t *testing.T) {
  254. sm := NewInMemorySessionManager(slog.Default())
  255. user1, err := sm.AddSession(context.Background(), "user-screen-name-1", false)
  256. assert.NoError(t, err)
  257. user1.SetSignonComplete()
  258. user2, err := sm.AddSession(context.Background(), "user-screen-name-2", false)
  259. assert.NoError(t, err)
  260. user2.SetSignonComplete()
  261. user2.CloseInstance()
  262. want := wire.SNACMessage{Frame: wire.SNACFrame{FoodGroup: wire.ICBM}}
  263. sm.RelayToAll(context.Background(), want)
  264. select {
  265. case have := <-user1.ReceiveMessage():
  266. assert.Equal(t, want, have)
  267. }
  268. select {
  269. case <-user2.ReceiveMessage():
  270. assert.Fail(t, "user 2 should not receive a message")
  271. default:
  272. }
  273. }
  274. func TestInMemorySessionManager_RelayToScreenName_SessionExists(t *testing.T) {
  275. sm := NewInMemorySessionManager(slog.Default())
  276. user1, err := sm.AddSession(context.Background(), "user-screen-name-1", false)
  277. assert.NoError(t, err)
  278. user1.SetSignonComplete()
  279. user2, err := sm.AddSession(context.Background(), "user-screen-name-2", false)
  280. assert.NoError(t, err)
  281. user2.SetSignonComplete()
  282. want := wire.SNACMessage{Frame: wire.SNACFrame{FoodGroup: wire.ICBM}}
  283. recip := NewIdentScreenName("user-screen-name-1")
  284. sm.RelayToScreenName(context.Background(), recip, want)
  285. select {
  286. case have := <-user1.ReceiveMessage():
  287. assert.Equal(t, want, have)
  288. }
  289. select {
  290. case <-user2.ReceiveMessage():
  291. assert.Fail(t, "user 2 should not receive a message")
  292. default:
  293. }
  294. }
  295. func TestInMemorySessionManager_RelayToScreenName_SessionNotExist(t *testing.T) {
  296. sm := NewInMemorySessionManager(slog.Default())
  297. user1, err := sm.AddSession(context.Background(), "user-screen-name-1", false)
  298. assert.NoError(t, err)
  299. user1.SetSignonComplete()
  300. want := wire.SNACMessage{Frame: wire.SNACFrame{FoodGroup: wire.ICBM}}
  301. recip := NewIdentScreenName("user-screen-name-2")
  302. sm.RelayToScreenName(context.Background(), recip, want)
  303. select {
  304. case <-user1.ReceiveMessage():
  305. assert.Fail(t, "user 1 should not receive a message")
  306. default:
  307. }
  308. }
  309. func TestInMemorySessionManager_RelayToScreenName_SkipFullSession(t *testing.T) {
  310. sm := NewInMemorySessionManager(slog.Default())
  311. user1, err := sm.AddSession(context.Background(), "user-screen-name-1", false)
  312. assert.NoError(t, err)
  313. user1.SetSignonComplete()
  314. msg := wire.SNACMessage{Frame: wire.SNACFrame{FoodGroup: wire.ICBM}}
  315. wantCount := 0
  316. for {
  317. if user1.RelayMessageToInstance(msg) == SessQueueFull {
  318. break
  319. }
  320. wantCount++
  321. }
  322. recip := NewIdentScreenName("user-screen-name-1")
  323. sm.RelayToScreenName(context.Background(), recip, msg)
  324. haveCount := 0
  325. loop:
  326. for {
  327. select {
  328. case <-user1.ReceiveMessage():
  329. haveCount++
  330. default:
  331. break loop
  332. }
  333. }
  334. assert.Equal(t, wantCount, haveCount)
  335. }
  336. func TestInMemorySessionManager_SessionReplacement_NoMultiSess_NoMultiSess(t *testing.T) {
  337. synctest.Test(t, func(t *testing.T) {
  338. sm := NewInMemorySessionManager(slog.Default())
  339. sess1, err := sm.AddSession(context.Background(), "user-screen-name-1", false)
  340. assert.NoError(t, err)
  341. sess1.SetSignonComplete()
  342. wg := &sync.WaitGroup{}
  343. wg.Add(1)
  344. go func() {
  345. defer wg.Done()
  346. // add the session again. this call blocks until RemoveSession makes
  347. // room for the new session
  348. sess2, err := sm.AddSession(context.Background(), "user-screen-name-1", false)
  349. assert.NoError(t, err)
  350. if assert.NotNil(t, sess2) {
  351. sess2.SetSignonComplete()
  352. assert.Equal(t, sess1.DisplayScreenName(), sess2.DisplayScreenName())
  353. }
  354. }()
  355. // wait for second call to AddSession() to block
  356. synctest.Wait()
  357. // AddSession() is blocked waiting for the lock, now unblock it
  358. sm.RemoveSession(sess1.Session())
  359. wg.Wait()
  360. // make sure we got a brand new session
  361. got := sm.RetrieveSession(NewIdentScreenName("user-screen-name-1"))
  362. assert.NotEqual(t, sess1, got)
  363. assert.Equal(t, 1, got.InstanceCount())
  364. })
  365. }
  366. func TestInMemorySessionManager_SessionReplacement_MultiSess_NoMultiSess(t *testing.T) {
  367. synctest.Test(t, func(t *testing.T) {
  368. sm := NewInMemorySessionManager(slog.Default())
  369. sm.maxConcurrentSessions = 5
  370. var sessList []*SessionInstance
  371. for i := 0; i < sm.maxConcurrentSessions; i++ {
  372. sess, err := sm.AddSession(context.Background(), "user-screen-name-1", true)
  373. assert.NoError(t, err)
  374. sess.SetSignonComplete()
  375. sessList = append(sessList, sess)
  376. }
  377. assert.Equal(t, len(sessList), sessList[0].Session().InstanceCount())
  378. wg := &sync.WaitGroup{}
  379. wg.Add(1)
  380. go func() {
  381. defer wg.Done()
  382. // add the session again. this call blocks until RemoveSession makes
  383. // room for the new session
  384. sess, err := sm.AddSession(context.Background(), "user-screen-name-1", false)
  385. assert.NoError(t, err)
  386. assert.NotNil(t, sess)
  387. sess.SetSignonComplete()
  388. assert.Equal(t, "user-screen-name-1", sess.DisplayScreenName().String())
  389. assert.Equal(t, 1, sess.Session().InstanceCount())
  390. }()
  391. // wait for the last call to AddSession() to block
  392. synctest.Wait()
  393. // AddSession() is blocked waiting for the lock, now unblock it
  394. for _, sess := range sessList {
  395. sm.RemoveSession(sess.Session())
  396. }
  397. wg.Wait()
  398. got := sm.RetrieveSession(NewIdentScreenName("user-screen-name-1"))
  399. for _, sess := range sessList {
  400. assert.NotSame(t, sess, got)
  401. }
  402. assert.Equal(t, 1, got.InstanceCount())
  403. })
  404. }
  405. func TestInMemorySessionManager_SessionReplacement_NoMultiSess_MultiSess(t *testing.T) {
  406. synctest.Test(t, func(t *testing.T) {
  407. sm := NewInMemorySessionManager(slog.Default())
  408. sess1, err := sm.AddSession(context.Background(), "user-screen-name-1", false)
  409. assert.NoError(t, err)
  410. sess1.SetSignonComplete()
  411. wg := &sync.WaitGroup{}
  412. wg.Add(1)
  413. go func() {
  414. defer wg.Done()
  415. // add the session again. this call blocks until RemoveSession makes
  416. // room for the new session
  417. sess2, err := sm.AddSession(context.Background(), "user-screen-name-1", true)
  418. assert.NoError(t, err)
  419. assert.NotNil(t, sess2)
  420. assert.Equal(t, sess1.DisplayScreenName(), sess2.DisplayScreenName())
  421. sess2.SetSignonComplete()
  422. }()
  423. // wait for second call to AddSession() to block
  424. synctest.Wait()
  425. // AddSession() is blocked waiting for the lock, now unblock it
  426. sm.RemoveSession(sess1.Session())
  427. wg.Wait()
  428. got := sm.RetrieveSession(NewIdentScreenName("user-screen-name-1"))
  429. if assert.NotNil(t, got) {
  430. assert.NotSame(t, sess1, got)
  431. assert.Equal(t, 1, got.InstanceCount())
  432. }
  433. })
  434. }
  435. func TestInMemorySessionManager_RemoveSession_DoubleLogin_NoMultiSess_Chaos(t *testing.T) {
  436. wg := &sync.WaitGroup{}
  437. sm := NewInMemorySessionManager(slog.Default())
  438. for i := 0; i < 1000; i++ { // shake out race conditions
  439. wg.Add(1)
  440. time.Sleep(time.Duration(rand.Intn(1000)) * time.Microsecond)
  441. go func() {
  442. defer wg.Done()
  443. sess1, err := sm.AddSession(context.Background(), "user-screen-name-1", false)
  444. assert.NoError(t, err)
  445. time.Sleep(time.Duration(rand.Intn(1000)) * time.Microsecond)
  446. sm.RemoveSession(sess1.Session())
  447. }()
  448. }
  449. wg.Wait()
  450. }
  451. func TestInMemoryChatSessionManager_AddSession_AppliesCfgToSession(t *testing.T) {
  452. sm := NewInMemoryChatSessionManager(slog.Default())
  453. chatCookie := "chat-room-cfg"
  454. wantUIN := uint32(777001)
  455. instance, err := sm.AddSession(context.Background(), chatCookie, "user-screen-name",
  456. func(sess *Session) {
  457. sess.SetUIN(wantUIN)
  458. },
  459. )
  460. assert.NoError(t, err)
  461. s := instance.Session()
  462. assert.Equal(t, wantUIN, s.UIN(), "cfg mutates Session via inner AddSession before instance is returned")
  463. assert.Equal(t, chatCookie, s.ChatRoomCookie())
  464. }
  465. func TestInMemoryChatSessionManager_RelayToAllExcept_HappyPath(t *testing.T) {
  466. sm := NewInMemoryChatSessionManager(slog.Default())
  467. cookie := "the-cookie"
  468. user1, err := sm.AddSession(context.Background(), cookie, "user-screen-name-1")
  469. assert.NoError(t, err)
  470. user1.SetSignonComplete()
  471. user2, err := sm.AddSession(context.Background(), cookie, "user-screen-name-2")
  472. assert.NoError(t, err)
  473. user2.SetSignonComplete()
  474. user3, err := sm.AddSession(context.Background(), cookie, "user-screen-name-3")
  475. assert.NoError(t, err)
  476. user3.SetSignonComplete()
  477. want := wire.SNACMessage{Frame: wire.SNACFrame{FoodGroup: wire.ICBM}}
  478. sm.RelayToAllExcept(context.Background(), cookie, user2.IdentScreenName(), want)
  479. select {
  480. case have := <-user1.ReceiveMessage():
  481. assert.Equal(t, want, have)
  482. }
  483. select {
  484. case <-user2.ReceiveMessage():
  485. assert.Fail(t, "user 2 should not receive a message")
  486. default:
  487. }
  488. select {
  489. case have := <-user3.ReceiveMessage():
  490. assert.Equal(t, want, have)
  491. }
  492. }
  493. //func TestInMemoryChatSessionManager_AllSessions_RoomExists(t *testing.T) {
  494. // sm := NewInMemoryChatSessionManager(slog.Default())
  495. //
  496. // user1, err := sm.AddSession(context.Background(), "the-cookie", "user-screen-name-1")
  497. // assert.NoError(t, err)
  498. // user1.SetSignonComplete()
  499. // user2, err := sm.AddSession(context.Background(), "the-cookie", "user-screen-name-2")
  500. // assert.NoError(t, err)
  501. // user2.SetSignonComplete()
  502. //
  503. // sessions := sm.AllSessions("the-cookie")
  504. // assert.Len(t, sessions, 2)
  505. //
  506. // lookup := make(map[*SessionInstance]bool)
  507. // for _, session := range sessions {
  508. // lookup[session] = true
  509. // }
  510. //
  511. // assert.True(t, lookup[user1])
  512. // assert.True(t, lookup[user2])
  513. //}
  514. func TestInMemoryChatSessionManager_RelayToScreenName_SessionAndChatRoomExist(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. want := wire.SNACMessage{Frame: wire.SNACFrame{FoodGroup: wire.ICBM}}
  523. recip := NewIdentScreenName("user-screen-name-1")
  524. sm.RelayToScreenName(context.Background(), "chat-room-1", recip, want)
  525. select {
  526. case have := <-user1.ReceiveMessage():
  527. assert.Equal(t, want, have)
  528. }
  529. select {
  530. case <-user2.ReceiveMessage():
  531. assert.Fail(t, "user 2 should not receive a message")
  532. default:
  533. }
  534. }
  535. func TestInMemoryChatSessionManager_RemoveSession(t *testing.T) {
  536. sm := NewInMemoryChatSessionManager(slog.Default())
  537. user1, err := sm.AddSession(context.Background(), "chat-room-1", "user-screen-name-1")
  538. assert.NoError(t, err)
  539. user1.SetSignonComplete()
  540. user2, err := sm.AddSession(context.Background(), "chat-room-1", "user-screen-name-2")
  541. assert.NoError(t, err)
  542. user2.SetSignonComplete()
  543. assert.Len(t, sm.AllSessions("chat-room-1"), 2)
  544. sm.RemoveSession(user1.Session())
  545. sm.RemoveSession(user2.Session())
  546. assert.Empty(t, sm.AllSessions("chat-room-1"))
  547. }
  548. func TestInMemoryChatSessionManager_RemoveSession_DoubleLogin(t *testing.T) {
  549. for i := 0; i < 50; i++ { // shake out race conditions
  550. synctest.Test(t, func(t *testing.T) {
  551. sm := NewInMemoryChatSessionManager(slog.Default())
  552. chatSess1, err := sm.AddSession(context.Background(), "chat-room-1", "user-screen-name-1")
  553. assert.NoError(t, err)
  554. chatSess1.SetSignonComplete()
  555. wg := &sync.WaitGroup{}
  556. wg.Add(1)
  557. go func() {
  558. // add the session again. this call blocks until RemoveSession makes
  559. // room for the new session
  560. chatSess2, err := sm.AddSession(context.Background(), "chat-room-1", "user-screen-name-1")
  561. assert.NoError(t, err)
  562. assert.NotNil(t, chatSess2)
  563. chatSess2.SetSignonComplete()
  564. assert.Equal(t, chatSess1.DisplayScreenName(), chatSess2.DisplayScreenName())
  565. wg.Done()
  566. }()
  567. // wait for second call to AddSession() to block
  568. synctest.Wait()
  569. // AddSession() is blocked waiting for the lock, now unblock it
  570. sm.RemoveSession(chatSess1.Session())
  571. wg.Wait()
  572. })
  573. }
  574. }
  575. func TestInMemoryChatSessionManager_RemoveUserFromAllChats(t *testing.T) {
  576. sm := NewInMemoryChatSessionManager(slog.Default())
  577. user1 := NewIdentScreenName("user-screen-name-1")
  578. user1sess, err := sm.AddSession(context.Background(), "chat-room-1", "user-screen-name-1")
  579. assert.NoError(t, err)
  580. user1sess.SetSignonComplete()
  581. user2sess, err := sm.AddSession(context.Background(), "chat-room-1", "user-screen-name-2")
  582. assert.NoError(t, err)
  583. user2sess.SetSignonComplete()
  584. assert.Len(t, sm.AllSessions("chat-room-1"), 2)
  585. sm.RemoveUserFromAllChats(user1)
  586. lookup := make(map[*Session]bool)
  587. for _, session := range sm.AllSessions("chat-room-1") {
  588. lookup[session] = true
  589. }
  590. assert.False(t, lookup[user1sess.Session()])
  591. assert.True(t, lookup[user2sess.Session()])
  592. }
  593. // TestInMemoryChatSessionManager_NoDeadlockOnCloseHookReentry verifies that
  594. // session close hooks don't deadlock when they re-enter
  595. // InMemoryChatSessionManager.
  596. //
  597. // The deadlock scenario under test:
  598. //
  599. // 1. BOS session closes, triggering its close hook.
  600. // 2. The close hook calls RemoveUserFromAllChats, which iterates chat rooms
  601. // and calls CloseSession on the user's chat session.
  602. // 3. The chat session's close hook calls AllSessions, which acquires
  603. // mapMutex.RLock.
  604. //
  605. // If RemoveUserFromAllChats naively held mapMutex.RLock while calling
  606. // CloseSession in step 2, the AllSessions call in step 3 would attempt a
  607. // recursive read lock on the same goroutine—something sync.RWMutex forbids—and
  608. // deadlock. The production code avoids this by copying the session managers and
  609. // releasing the lock before calling CloseSession.
  610. func TestInMemoryChatSessionManager_NoDeadlockOnCloseHookReentry(t *testing.T) {
  611. user := DisplayScreenName("user-screen-name-1")
  612. cookie := "chat-room-1"
  613. bosSM := NewInMemorySessionManager(slog.Default())
  614. chatSM := NewInMemoryChatSessionManager(slog.Default())
  615. bosSess, err := bosSM.AddSession(context.Background(), user, false)
  616. require.NoError(t, err)
  617. bosSess.Session().SetIdentScreenName(user.IdentScreenName())
  618. bosSess.SetSignonComplete()
  619. chatSess, err := chatSM.AddSession(context.Background(), cookie, user)
  620. require.NoError(t, err)
  621. chatSess.SetSignonComplete()
  622. // Simulate the real signoff flow: when the BOS session closes, remove
  623. // the user from all chat rooms.
  624. bosSess.Session().OnSessionClose(func() {
  625. chatSM.RemoveUserFromAllChats(user.IdentScreenName())
  626. })
  627. // When the chat session closes, re-enter the chat session manager to
  628. // simulate some cleanup operations, which would deadlock if the manager's
  629. // lock were still held.
  630. chatSess.Session().OnSessionClose(func() {
  631. chatSM.AllSessions(cookie)
  632. })
  633. done := make(chan struct{})
  634. go func() {
  635. defer close(done)
  636. bosSess.Session().CloseSession()
  637. }()
  638. select {
  639. case <-done:
  640. case <-time.After(5 * time.Second):
  641. t.Fatal("timed out waiting for session close — probable deadlock in close hook chain")
  642. }
  643. }
  644. func TestInMemorySessionManager_RelayToAll_SkipIncompleteSignon(t *testing.T) {
  645. sm := NewInMemorySessionManager(slog.Default())
  646. user1, err := sm.AddSession(context.Background(), "user-screen-name-1", false)
  647. assert.NoError(t, err)
  648. user1.SetSignonComplete()
  649. user2, err := sm.AddSession(context.Background(), "user-screen-name-2", false)
  650. assert.NoError(t, err)
  651. // user2 has not completed signon
  652. want := wire.SNACMessage{Frame: wire.SNACFrame{FoodGroup: wire.ICBM}}
  653. sm.RelayToAll(context.Background(), want)
  654. select {
  655. case have := <-user1.ReceiveMessage():
  656. assert.Equal(t, want, have)
  657. }
  658. select {
  659. case <-user2.ReceiveMessage():
  660. assert.Fail(t, "user 2 should not receive a message because signon is incomplete")
  661. default:
  662. }
  663. }
  664. func TestInMemorySessionManager_RetrieveSession_IncompleteSignon(t *testing.T) {
  665. sm := NewInMemorySessionManager(slog.Default())
  666. user1, err := sm.AddSession(context.Background(), "user-screen-name-1", false)
  667. assert.NoError(t, err)
  668. // user1 has not completed signon
  669. sess := sm.RetrieveSession(NewIdentScreenName("user-screen-name-1"))
  670. assert.Nil(t, sess, "should return nil for session with incomplete signon")
  671. user1.SetSignonComplete()
  672. sess = sm.RetrieveSession(NewIdentScreenName("user-screen-name-1"))
  673. assert.NotNil(t, sess, "should return session after signon is complete")
  674. assert.Equal(t, user1.Session(), sess)
  675. }
  676. func TestInMemorySessionManager_RetrieveSession_CompleteSignon(t *testing.T) {
  677. sm := NewInMemorySessionManager(slog.Default())
  678. user1, err := sm.AddSession(context.Background(), "user-screen-name-1", false)
  679. assert.NoError(t, err)
  680. user1.SetSignonComplete()
  681. sess := sm.RetrieveSession(NewIdentScreenName("user-screen-name-1"))
  682. assert.NotNil(t, sess)
  683. assert.Equal(t, user1.Session(), sess)
  684. }
  685. func TestInMemorySessionManager_RelayToScreenNames_SkipIncompleteSignon(t *testing.T) {
  686. sm := NewInMemorySessionManager(slog.Default())
  687. user1, err := sm.AddSession(context.Background(), "user-screen-name-1", false)
  688. assert.NoError(t, err)
  689. user1.SetSignonComplete()
  690. user2, err := sm.AddSession(context.Background(), "user-screen-name-2", false)
  691. assert.NoError(t, err)
  692. // user2 has not completed signon
  693. user3, err := sm.AddSession(context.Background(), "user-screen-name-3", false)
  694. assert.NoError(t, err)
  695. user3.SetSignonComplete()
  696. want := wire.SNACMessage{Frame: wire.SNACFrame{FoodGroup: wire.ICBM}}
  697. recips := []IdentScreenName{
  698. NewIdentScreenName("user-screen-name-1"),
  699. NewIdentScreenName("user-screen-name-2"), // incomplete signon
  700. NewIdentScreenName("user-screen-name-3"),
  701. }
  702. sm.RelayToScreenNames(context.Background(), recips, want)
  703. select {
  704. case have := <-user1.ReceiveMessage():
  705. assert.Equal(t, want, have)
  706. }
  707. select {
  708. case <-user2.ReceiveMessage():
  709. assert.Fail(t, "user 2 should not receive a message because signon is incomplete")
  710. default:
  711. }
  712. select {
  713. case have := <-user3.ReceiveMessage():
  714. assert.Equal(t, want, have)
  715. }
  716. }
  717. func TestInMemorySessionManager_AllSessions_SkipIncompleteSignon(t *testing.T) {
  718. sm := NewInMemorySessionManager(slog.Default())
  719. user1, err := sm.AddSession(context.Background(), "user-screen-name-1", false)
  720. assert.NoError(t, err)
  721. user1.SetSignonComplete()
  722. user2, err := sm.AddSession(context.Background(), "user-screen-name-2", false)
  723. assert.NoError(t, err)
  724. // user2 has not completed signon
  725. user3, err := sm.AddSession(context.Background(), "user-screen-name-3", false)
  726. assert.NoError(t, err)
  727. user3.SetSignonComplete()
  728. sessions := sm.AllSessions()
  729. assert.Len(t, sessions, 2, "should only return sessions with complete signon")
  730. // Check that we have sessions for user1 and user3 (by checking Session identity)
  731. user1Found := false
  732. user3Found := false
  733. user2Found := false
  734. for _, session := range sessions {
  735. if session == user1.Session() {
  736. user1Found = true
  737. }
  738. if session == user2.Session() {
  739. user2Found = true
  740. }
  741. if session == user3.Session() {
  742. user3Found = true
  743. }
  744. }
  745. assert.True(t, user1Found, "user1 should be included (complete signon)")
  746. assert.False(t, user2Found, "user2 should not be included (incomplete signon)")
  747. assert.True(t, user3Found, "user3 should be included (complete signon)")
  748. }
  749. func TestInMemorySessionManager_RelayToScreenName_IncompleteSignon(t *testing.T) {
  750. sm := NewInMemorySessionManager(slog.Default())
  751. user1, err := sm.AddSession(context.Background(), "user-screen-name-1", false)
  752. assert.NoError(t, err)
  753. // user1 has not completed signon
  754. want := wire.SNACMessage{Frame: wire.SNACFrame{FoodGroup: wire.ICBM}}
  755. recip := NewIdentScreenName("user-screen-name-1")
  756. sm.RelayToScreenName(context.Background(), recip, want)
  757. select {
  758. case <-user1.ReceiveMessage():
  759. assert.Fail(t, "user 1 should not receive a message because signon is incomplete")
  760. default:
  761. }
  762. }
  763. func TestInMemorySessionManager_RelayToOtherInstances_SkipsNonLiveInstances(t *testing.T) {
  764. sm := NewInMemorySessionManager(slog.Default())
  765. // Create a session with multiple instances
  766. user1, err := sm.AddSession(context.Background(), "user-screen-name-1", true)
  767. assert.NoError(t, err)
  768. user1.SetSignonComplete()
  769. // Add a second instance that hasn't completed signon
  770. user1Instance2, err := sm.AddSession(context.Background(), "user-screen-name-1", true)
  771. assert.NoError(t, err)
  772. // user1Instance2 has not completed signon, so this instance is not live
  773. // Add a third instance that has completed signon
  774. user1Instance3, err := sm.AddSession(context.Background(), "user-screen-name-1", true)
  775. assert.NoError(t, err)
  776. user1Instance3.SetSignonComplete()
  777. // Verify instance-level live() behavior
  778. assert.True(t, user1.live(), "user1 should be live (not closed and signon complete)")
  779. assert.False(t, user1Instance2.live(), "user1Instance2 should not be live (signon not complete)")
  780. assert.True(t, user1Instance3.live(), "user1Instance3 should be live (not closed and signon complete)")
  781. want := wire.SNACMessage{Frame: wire.SNACFrame{FoodGroup: wire.ICBM}}
  782. // Relay to other instances from user1
  783. sm.RelayToOtherInstances(context.Background(), user1, want)
  784. // user1 should not receive the message (it's the sender)
  785. select {
  786. case <-user1.ReceiveMessage():
  787. assert.Fail(t, "user1 should not receive a message relayed from itself")
  788. default:
  789. }
  790. // user1Instance2 should not receive the message (not live - signon incomplete)
  791. select {
  792. case <-user1Instance2.ReceiveMessage():
  793. assert.Fail(t, "user1Instance2 should not receive a message because it's not live")
  794. default:
  795. }
  796. // user1Instance3 should receive the message (is live)
  797. select {
  798. case have := <-user1Instance3.ReceiveMessage():
  799. assert.Equal(t, want, have)
  800. default:
  801. assert.Fail(t, "user1Instance3 should receive the message")
  802. }
  803. }
  804. func TestInMemorySessionManager_MaybeRelayMessage_SkipsNonLiveInstances(t *testing.T) {
  805. sm := NewInMemorySessionManager(slog.Default())
  806. // Create a session with multiple instances
  807. user1, err := sm.AddSession(context.Background(), "user-screen-name-1", true)
  808. assert.NoError(t, err)
  809. user1.SetSignonComplete()
  810. // Add a third instance that has completed signon
  811. user1Instance3, err := sm.AddSession(context.Background(), "user-screen-name-1", true)
  812. assert.NoError(t, err)
  813. user1Instance3.SetSignonComplete()
  814. // Create a separate session with incomplete signon to test that non-live instances are skipped
  815. user2, err := sm.AddSession(context.Background(), "user-screen-name-2", false)
  816. assert.NoError(t, err)
  817. // user2 has not completed signon, so this instance is not live
  818. assert.False(t, user2.live(), "instance should not be live when signon is incomplete")
  819. want := wire.SNACMessage{Frame: wire.SNACFrame{FoodGroup: wire.ICBM}}
  820. // Use maybeRelayMessage (called internally by RelayToScreenName)
  821. // This should relay to all live instances in the session
  822. sm.RelayToScreenName(context.Background(), user1.IdentScreenName(), want)
  823. // user1 should receive the message
  824. select {
  825. case have := <-user1.ReceiveMessage():
  826. assert.Equal(t, want, have)
  827. default:
  828. assert.Fail(t, "user1 should receive the message")
  829. }
  830. // user1Instance3 should receive the message (session is live)
  831. select {
  832. case have := <-user1Instance3.ReceiveMessage():
  833. assert.Equal(t, want, have)
  834. default:
  835. assert.Fail(t, "user1Instance3 should receive the message")
  836. }
  837. // Test that non-live instances are skipped in RelayToAll (which calls maybeRelayMessage)
  838. sm.RelayToAll(context.Background(), want)
  839. // user2 should not receive the message (instance is not live, so maybeRelayMessage skips it)
  840. select {
  841. case <-user2.ReceiveMessage():
  842. assert.Fail(t, "user2 should not receive a message because the instance is not live")
  843. default:
  844. }
  845. }
  846. func TestInMemorySessionManager_AddSession_MaxConcurrentSessions(t *testing.T) {
  847. t.Run("enforces limit", func(t *testing.T) {
  848. sm := NewInMemorySessionManager(slog.Default())
  849. sm.maxConcurrentSessions = 5
  850. // Create sessions up to the limit (5)
  851. var sessList []*SessionInstance
  852. for i := 0; i < sm.maxConcurrentSessions; i++ {
  853. sess, err := sm.AddSession(context.Background(), "user-screen-name-1", true)
  854. assert.NoError(t, err)
  855. sess.SetSignonComplete()
  856. sessList = append(sessList, sess)
  857. }
  858. // Verify we have exactly 5 instances
  859. assert.Equal(t, sm.maxConcurrentSessions, sessList[0].Session().InstanceCount())
  860. // Try to add one more session - should fail with ErrMaxConcurrentSessionsReached
  861. sess, err := sm.AddSession(context.Background(), "user-screen-name-1", true)
  862. assert.Nil(t, sess)
  863. assert.ErrorIs(t, err, ErrMaxConcurrentSessionsReached)
  864. // Verify we still have exactly 5 instances
  865. assert.Equal(t, sm.maxConcurrentSessions, sessList[0].Session().InstanceCount())
  866. })
  867. t.Run("allows new session after removal", func(t *testing.T) {
  868. sm := NewInMemorySessionManager(slog.Default())
  869. sm.maxConcurrentSessions = 5
  870. // Create sessions up to the limit (5)
  871. var sessList []*SessionInstance
  872. for i := 0; i < sm.maxConcurrentSessions; i++ {
  873. sess, err := sm.AddSession(context.Background(), "user-screen-name-1", true)
  874. assert.NoError(t, err)
  875. sess.SetSignonComplete()
  876. sessList = append(sessList, sess)
  877. }
  878. // Verify we have exactly 5 instances
  879. assert.Equal(t, sm.maxConcurrentSessions, sessList[0].Session().InstanceCount())
  880. // Try to add one more session - should fail
  881. sess, err := sm.AddSession(context.Background(), "user-screen-name-1", true)
  882. assert.Nil(t, sess)
  883. assert.ErrorIs(t, err, ErrMaxConcurrentSessionsReached)
  884. // Close one instance (this removes it from the Session)
  885. sessList[0].CloseInstance()
  886. // Now we should be able to add a new instance to the same session
  887. newSess, err := sm.AddSession(context.Background(), "user-screen-name-1", true)
  888. assert.NoError(t, err)
  889. assert.NotNil(t, newSess)
  890. newSess.SetSignonComplete()
  891. // Verify we have exactly 5 instances again (4 remaining + 1 new = 5)
  892. assert.Equal(t, sm.maxConcurrentSessions, newSess.Session().InstanceCount())
  893. })
  894. t.Run("no limit for non-multi-session", func(t *testing.T) {
  895. sm := NewInMemorySessionManager(slog.Default())
  896. // Create multiple non-multi-session sessions - should not be limited
  897. // (though they will replace each other, but that's expected behavior)
  898. sess1, err := sm.AddSession(context.Background(), "user-screen-name-1", false)
  899. assert.NoError(t, err)
  900. sess1.SetSignonComplete()
  901. // Close and remove the first session to allow a new one
  902. go func() {
  903. <-sess1.Closed()
  904. sm.RemoveSession(sess1.Session())
  905. }()
  906. sess2, err := sm.AddSession(context.Background(), "user-screen-name-1", false)
  907. assert.NoError(t, err)
  908. sess2.SetSignonComplete()
  909. // Verify the limit doesn't apply to non-multi-session
  910. assert.Equal(t, 1, sess2.Session().InstanceCount())
  911. })
  912. }