session_manager_test.go 40 KB

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