session_manager_test.go 31 KB

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