session_limit_release_test.go 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121
  1. package service
  2. import (
  3. "context"
  4. "errors"
  5. "testing"
  6. "github.com/stretchr/testify/require"
  7. )
  8. // sessionLimitReleaseCacheStub 记录 UnregisterSession 调用,用于验证释放逻辑
  9. type sessionLimitReleaseCacheStub struct {
  10. SessionLimitCache
  11. unregistered map[int64][]string
  12. err error
  13. }
  14. func newSessionLimitReleaseCacheStub() *sessionLimitReleaseCacheStub {
  15. return &sessionLimitReleaseCacheStub{
  16. unregistered: make(map[int64][]string),
  17. }
  18. }
  19. func (s *sessionLimitReleaseCacheStub) UnregisterSession(_ context.Context, accountID int64, sessionUUID string) error {
  20. if s.err != nil {
  21. return s.err
  22. }
  23. s.unregistered[accountID] = append(s.unregistered[accountID], sessionUUID)
  24. return nil
  25. }
  26. func newSessionLimitTestAccount() *Account {
  27. return &Account{
  28. ID: 42,
  29. Platform: PlatformAnthropic,
  30. Type: AccountTypeOAuth,
  31. Extra: map[string]any{"max_sessions": 1},
  32. }
  33. }
  34. // TestReleaseAccountSession_ReleasesRegisteredSlot 验证:
  35. // 对启用会话限制的 Anthropic OAuth 账号,ReleaseAccountSession 必须立即移除
  36. // 该账号上注册的会话(不等待空闲超时)。
  37. func TestReleaseAccountSession_ReleasesRegisteredSlot(t *testing.T) {
  38. cache := newSessionLimitReleaseCacheStub()
  39. svc := &GatewayService{sessionLimitCache: cache}
  40. acc := newSessionLimitTestAccount()
  41. svc.ReleaseAccountSession(context.Background(), acc, "session-hash-1")
  42. require.Equal(t, []string{"session-hash-1"}, cache.unregistered[42],
  43. "应立即移除注册的会话槽")
  44. }
  45. // TestReleaseAccountSession_NoOpForInapplicableAccounts 验证:
  46. // - 非 Anthropic OAuth/SetupToken 账号
  47. // - 未启用 max_sessions 的账号
  48. // - 空 sessionID
  49. // 以上场景均为 no-op,不得触发 UnregisterSession。
  50. func TestReleaseAccountSession_NoOpForInapplicableAccounts(t *testing.T) {
  51. apiKeyAcc := &Account{
  52. ID: 43,
  53. Platform: PlatformAnthropic,
  54. Type: AccountTypeAPIKey,
  55. Extra: map[string]any{"max_sessions": 1},
  56. }
  57. noLimitAcc := &Account{
  58. ID: 44,
  59. Platform: PlatformAnthropic,
  60. Type: AccountTypeOAuth,
  61. }
  62. enabledAcc := newSessionLimitTestAccount()
  63. cases := []struct {
  64. name string
  65. account *Account
  66. sessionID string
  67. }{
  68. {"api_key_account", apiKeyAcc, "session-hash"},
  69. {"max_sessions_disabled", noLimitAcc, "session-hash"},
  70. {"empty_session_id", enabledAcc, ""},
  71. {"nil_account", nil, "session-hash"},
  72. }
  73. for _, tc := range cases {
  74. t.Run(tc.name, func(t *testing.T) {
  75. cache := newSessionLimitReleaseCacheStub()
  76. svc := &GatewayService{sessionLimitCache: cache}
  77. svc.ReleaseAccountSession(context.Background(), tc.account, tc.sessionID)
  78. require.Empty(t, cache.unregistered, "不适用账号不应触发释放")
  79. })
  80. }
  81. }
  82. // TestReleaseAccountSession_NilCacheAndErrorTolerance 验证:
  83. // sessionLimitCache 不可用时 no-op;UnregisterSession 返回错误时不 panic(仅记录日志)。
  84. func TestReleaseAccountSession_NilCacheAndErrorTolerance(t *testing.T) {
  85. // nil cache:no-op
  86. svc := &GatewayService{}
  87. svc.ReleaseAccountSession(context.Background(), newSessionLimitTestAccount(), "session-hash")
  88. // 底层错误:不 panic
  89. cache := &sessionLimitReleaseCacheStub{err: errors.New("redis down")}
  90. svc = &GatewayService{sessionLimitCache: cache}
  91. svc.ReleaseAccountSession(context.Background(), newSessionLimitTestAccount(), "session-hash")
  92. }
  93. // TestReleaseAccountSession_Idempotent 验证释放操作幂等,可安全重复调用
  94. // (failover 链上按次释放 + defer 兜底可能对同一账号重复释放)。
  95. func TestReleaseAccountSession_Idempotent(t *testing.T) {
  96. cache := newSessionLimitReleaseCacheStub()
  97. svc := &GatewayService{sessionLimitCache: cache}
  98. acc := newSessionLimitTestAccount()
  99. svc.ReleaseAccountSession(context.Background(), acc, "session-hash")
  100. svc.ReleaseAccountSession(context.Background(), acc, "session-hash")
  101. // 两次调用都透传到缓存层(Redis ZREM 本身幂等,重复移除无副作用)
  102. require.Len(t, cache.unregistered[42], 2)
  103. }