session_limit_cache_integration_test.go 3.6 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192
  1. //go:build integration
  2. package repository
  3. import (
  4. "testing"
  5. "time"
  6. "github.com/Wei-Shaw/sub2api/internal/service"
  7. "github.com/stretchr/testify/suite"
  8. )
  9. type SessionLimitCacheSuite struct {
  10. IntegrationRedisSuite
  11. cache service.SessionLimitCache
  12. }
  13. func (s *SessionLimitCacheSuite) SetupTest() {
  14. s.IntegrationRedisSuite.SetupTest()
  15. s.cache = NewSessionLimitCache(s.rdb, 5)
  16. }
  17. // TestUnregisterSession_FreesSlotForNewSession 复现并验证修复场景:
  18. // max_sessions=1 的账号上,会话 A 注册后若请求转发失败,会话槽会被 A 占住
  19. // 整个空闲超时窗口(默认 5 分钟),导致窗口内新会话 B 全部被拒。
  20. // UnregisterSession 必须立即释放槽位,让 B 无需等待空闲超时即可注册。
  21. func (s *SessionLimitCacheSuite) TestUnregisterSession_FreesSlotForNewSession() {
  22. const accountID = int64(101)
  23. maxSessions := 1
  24. idleTimeout := 5 * time.Minute
  25. // 会话 A 注册成功(请求开始转发)
  26. allowed, err := s.cache.RegisterSession(s.ctx, accountID, "session-A", maxSessions, idleTimeout)
  27. s.RequireNoError(err, "RegisterSession A")
  28. s.True(allowed, "首个会话应被允许")
  29. // 转发失败后的新会话 B 在空闲超时窗口内被拒(修复前的行为)
  30. allowed, err = s.cache.RegisterSession(s.ctx, accountID, "session-B", maxSessions, idleTimeout)
  31. s.RequireNoError(err, "RegisterSession B before release")
  32. s.False(allowed, "槽位被 A 占用时新会话应被拒绝")
  33. // 请求最终失败:立即释放 A 的会话注册(不等待空闲超时)
  34. s.RequireNoError(s.cache.UnregisterSession(s.ctx, accountID, "session-A"), "UnregisterSession A")
  35. // A 释放后 B 立即可注册,无需等待空闲超时
  36. allowed, err = s.cache.RegisterSession(s.ctx, accountID, "session-B", maxSessions, idleTimeout)
  37. s.RequireNoError(err, "RegisterSession B after release")
  38. s.True(allowed, "释放后新会话应立即可注册")
  39. count, err := s.cache.GetActiveSessionCount(s.ctx, accountID)
  40. s.RequireNoError(err, "GetActiveSessionCount")
  41. s.Equal(1, count, "释放后活跃会话数应为 1(仅 B)")
  42. }
  43. // TestUnregisterSession_IdempotentAndMissing 验证释放操作幂等:
  44. // 重复释放与释放不存在的会话均不应报错。
  45. func (s *SessionLimitCacheSuite) TestUnregisterSession_IdempotentAndMissing() {
  46. const accountID = int64(102)
  47. s.RequireNoError(s.cache.UnregisterSession(s.ctx, accountID, "missing"), "释放不存在的会话")
  48. s.RequireNoError(s.cache.UnregisterSession(s.ctx, accountID, "missing"), "重复释放")
  49. s.RequireNoError(s.cache.UnregisterSession(s.ctx, accountID, ""), "空 sessionUUID 应为 no-op")
  50. }
  51. // TestUnregisterSession_OnlyTargetsSpecifiedSession 验证释放只影响目标会话,
  52. // 不影响同账号上的其他活跃会话。
  53. func (s *SessionLimitCacheSuite) TestUnregisterSession_OnlyTargetsSpecifiedSession() {
  54. const accountID = int64(103)
  55. maxSessions := 2
  56. idleTimeout := 5 * time.Minute
  57. for _, sid := range []string{"session-A", "session-B"} {
  58. allowed, err := s.cache.RegisterSession(s.ctx, accountID, sid, maxSessions, idleTimeout)
  59. s.RequireNoError(err, "RegisterSession "+sid)
  60. s.True(allowed)
  61. }
  62. s.RequireNoError(s.cache.UnregisterSession(s.ctx, accountID, "session-A"), "UnregisterSession A")
  63. active, err := s.cache.IsSessionActive(s.ctx, accountID, "session-A")
  64. s.RequireNoError(err, "IsSessionActive A")
  65. s.False(active, "A 应已释放")
  66. active, err = s.cache.IsSessionActive(s.ctx, accountID, "session-B")
  67. s.RequireNoError(err, "IsSessionActive B")
  68. s.True(active, "B 不应受影响")
  69. }
  70. func TestSessionLimitCacheSuite(t *testing.T) {
  71. suite.Run(t, new(SessionLimitCacheSuite))
  72. }