| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547 |
- //go:build unit
- package service
- import (
- "context"
- "errors"
- "testing"
- "time"
- "github.com/stretchr/testify/require"
- )
- type geminiTokenCacheStub struct {
- deletedKeys []string
- deleteErr error
- }
- func (s *geminiTokenCacheStub) GetAccessToken(ctx context.Context, cacheKey string) (string, error) {
- return "", nil
- }
- func (s *geminiTokenCacheStub) SetAccessToken(ctx context.Context, cacheKey string, token string, ttl time.Duration) error {
- return nil
- }
- func (s *geminiTokenCacheStub) DeleteAccessToken(ctx context.Context, cacheKey string) error {
- s.deletedKeys = append(s.deletedKeys, cacheKey)
- return s.deleteErr
- }
- func (s *geminiTokenCacheStub) AcquireRefreshLock(ctx context.Context, cacheKey string, ttl time.Duration) (bool, error) {
- return true, nil
- }
- func (s *geminiTokenCacheStub) ReleaseRefreshLock(ctx context.Context, cacheKey string) error {
- return nil
- }
- func TestCompositeTokenCacheInvalidator_Gemini(t *testing.T) {
- cache := &geminiTokenCacheStub{}
- invalidator := NewCompositeTokenCacheInvalidator(cache)
- account := &Account{
- ID: 10,
- Platform: PlatformGemini,
- Type: AccountTypeOAuth,
- Credentials: map[string]any{
- "project_id": "project-x",
- },
- }
- err := invalidator.InvalidateToken(context.Background(), account)
- require.NoError(t, err)
- // 新行为:同时删除基于 project_id 和 account_id 的缓存键
- // 这是为了处理:首次获取 token 时可能没有 project_id,之后自动检测到后会使用新 key
- require.Equal(t, []string{"gemini:project-x", "gemini:account:10"}, cache.deletedKeys)
- }
- func TestCompositeTokenCacheInvalidator_GeminiWithoutProjectID(t *testing.T) {
- cache := &geminiTokenCacheStub{}
- invalidator := NewCompositeTokenCacheInvalidator(cache)
- account := &Account{
- ID: 10,
- Platform: PlatformGemini,
- Type: AccountTypeOAuth,
- Credentials: map[string]any{
- "access_token": "gemini-token",
- },
- }
- err := invalidator.InvalidateToken(context.Background(), account)
- require.NoError(t, err)
- // 没有 project_id 时,两个 key 相同,去重后只删除一个
- require.Equal(t, []string{"gemini:account:10"}, cache.deletedKeys)
- }
- func TestCompositeTokenCacheInvalidator_Antigravity(t *testing.T) {
- cache := &geminiTokenCacheStub{}
- invalidator := NewCompositeTokenCacheInvalidator(cache)
- account := &Account{
- ID: 99,
- Platform: PlatformAntigravity,
- Type: AccountTypeOAuth,
- Credentials: map[string]any{
- "project_id": "ag-project",
- },
- }
- err := invalidator.InvalidateToken(context.Background(), account)
- require.NoError(t, err)
- // 新行为:同时删除基于 project_id 和 account_id 的缓存键
- require.Equal(t, []string{"ag:ag-project", "ag:account:99"}, cache.deletedKeys)
- }
- func TestCompositeTokenCacheInvalidator_AntigravityWithoutProjectID(t *testing.T) {
- cache := &geminiTokenCacheStub{}
- invalidator := NewCompositeTokenCacheInvalidator(cache)
- account := &Account{
- ID: 99,
- Platform: PlatformAntigravity,
- Type: AccountTypeOAuth,
- Credentials: map[string]any{
- "access_token": "ag-token",
- },
- }
- err := invalidator.InvalidateToken(context.Background(), account)
- require.NoError(t, err)
- // 没有 project_id 时,两个 key 相同,去重后只删除一个
- require.Equal(t, []string{"ag:account:99"}, cache.deletedKeys)
- }
- func TestCompositeTokenCacheInvalidator_OpenAI(t *testing.T) {
- cache := &geminiTokenCacheStub{}
- invalidator := NewCompositeTokenCacheInvalidator(cache)
- account := &Account{
- ID: 500,
- Platform: PlatformOpenAI,
- Type: AccountTypeOAuth,
- Credentials: map[string]any{
- "access_token": "openai-token",
- },
- }
- err := invalidator.InvalidateToken(context.Background(), account)
- require.NoError(t, err)
- require.Equal(t, []string{"openai:account:500"}, cache.deletedKeys)
- }
- func TestCompositeTokenCacheInvalidator_Claude(t *testing.T) {
- cache := &geminiTokenCacheStub{}
- invalidator := NewCompositeTokenCacheInvalidator(cache)
- account := &Account{
- ID: 600,
- Platform: PlatformAnthropic,
- Type: AccountTypeOAuth,
- Credentials: map[string]any{
- "access_token": "claude-token",
- },
- }
- err := invalidator.InvalidateToken(context.Background(), account)
- require.NoError(t, err)
- require.Equal(t, []string{"claude:account:600"}, cache.deletedKeys)
- }
- func TestCompositeTokenCacheInvalidator_SkipNonOAuth(t *testing.T) {
- cache := &geminiTokenCacheStub{}
- invalidator := NewCompositeTokenCacheInvalidator(cache)
- tests := []struct {
- name string
- account *Account
- }{
- {
- name: "gemini_api_key",
- account: &Account{
- ID: 1,
- Platform: PlatformGemini,
- Type: AccountTypeAPIKey,
- },
- },
- {
- name: "openai_api_key",
- account: &Account{
- ID: 2,
- Platform: PlatformOpenAI,
- Type: AccountTypeAPIKey,
- },
- },
- {
- name: "claude_api_key",
- account: &Account{
- ID: 3,
- Platform: PlatformAnthropic,
- Type: AccountTypeAPIKey,
- },
- },
- {
- name: "claude_setup_token",
- account: &Account{
- ID: 4,
- Platform: PlatformAnthropic,
- Type: AccountTypeSetupToken,
- },
- },
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- cache.deletedKeys = nil
- err := invalidator.InvalidateToken(context.Background(), tt.account)
- require.NoError(t, err)
- require.Empty(t, cache.deletedKeys)
- })
- }
- }
- func TestCompositeTokenCacheInvalidator_SkipUnsupportedPlatform(t *testing.T) {
- cache := &geminiTokenCacheStub{}
- invalidator := NewCompositeTokenCacheInvalidator(cache)
- account := &Account{
- ID: 100,
- Platform: "unknown-platform",
- Type: AccountTypeOAuth,
- }
- err := invalidator.InvalidateToken(context.Background(), account)
- require.NoError(t, err)
- require.Empty(t, cache.deletedKeys)
- }
- func TestCompositeTokenCacheInvalidator_NilCache(t *testing.T) {
- invalidator := NewCompositeTokenCacheInvalidator(nil)
- account := &Account{
- ID: 2,
- Platform: PlatformGemini,
- Type: AccountTypeOAuth,
- }
- err := invalidator.InvalidateToken(context.Background(), account)
- require.NoError(t, err)
- }
- func TestCompositeTokenCacheInvalidator_NilAccount(t *testing.T) {
- cache := &geminiTokenCacheStub{}
- invalidator := NewCompositeTokenCacheInvalidator(cache)
- err := invalidator.InvalidateToken(context.Background(), nil)
- require.NoError(t, err)
- require.Empty(t, cache.deletedKeys)
- }
- func TestCompositeTokenCacheInvalidator_NilInvalidator(t *testing.T) {
- var invalidator *CompositeTokenCacheInvalidator
- account := &Account{
- ID: 5,
- Platform: PlatformGemini,
- Type: AccountTypeOAuth,
- }
- err := invalidator.InvalidateToken(context.Background(), account)
- require.NoError(t, err)
- }
- func TestCompositeTokenCacheInvalidator_DeleteError(t *testing.T) {
- expectedErr := errors.New("redis connection failed")
- cache := &geminiTokenCacheStub{deleteErr: expectedErr}
- invalidator := NewCompositeTokenCacheInvalidator(cache)
- tests := []struct {
- name string
- account *Account
- }{
- {
- name: "openai_delete_error",
- account: &Account{
- ID: 700,
- Platform: PlatformOpenAI,
- Type: AccountTypeOAuth,
- },
- },
- {
- name: "claude_delete_error",
- account: &Account{
- ID: 800,
- Platform: PlatformAnthropic,
- Type: AccountTypeOAuth,
- },
- },
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- // 新行为:删除失败只记录日志,不返回错误
- // 这是因为缓存失效失败不应影响主业务流程
- err := invalidator.InvalidateToken(context.Background(), tt.account)
- require.NoError(t, err)
- })
- }
- }
- func TestCompositeTokenCacheInvalidator_AllPlatformsIntegration(t *testing.T) {
- // 测试所有平台的缓存键生成和删除
- cache := &geminiTokenCacheStub{}
- invalidator := NewCompositeTokenCacheInvalidator(cache)
- accounts := []*Account{
- {ID: 1, Platform: PlatformGemini, Type: AccountTypeOAuth, Credentials: map[string]any{"project_id": "gemini-proj"}},
- {ID: 2, Platform: PlatformAntigravity, Type: AccountTypeOAuth, Credentials: map[string]any{"project_id": "ag-proj"}},
- {ID: 3, Platform: PlatformOpenAI, Type: AccountTypeOAuth},
- {ID: 4, Platform: PlatformAnthropic, Type: AccountTypeOAuth},
- }
- // 新行为:Gemini 和 Antigravity 会同时删除基于 project_id 和 account_id 的键
- expectedKeys := []string{
- "gemini:gemini-proj",
- "gemini:account:1",
- "ag:ag-proj",
- "ag:account:2",
- "openai:account:3",
- "claude:account:4",
- }
- for _, acc := range accounts {
- err := invalidator.InvalidateToken(context.Background(), acc)
- require.NoError(t, err)
- }
- require.Equal(t, expectedKeys, cache.deletedKeys)
- }
- // ========== GetCredentialAsInt64 测试 ==========
- func TestAccount_GetCredentialAsInt64(t *testing.T) {
- tests := []struct {
- name string
- credentials map[string]any
- key string
- expected int64
- }{
- {
- name: "int64_value",
- credentials: map[string]any{"_token_version": int64(1737654321000)},
- key: "_token_version",
- expected: 1737654321000,
- },
- {
- name: "float64_value",
- credentials: map[string]any{"_token_version": float64(1737654321000)},
- key: "_token_version",
- expected: 1737654321000,
- },
- {
- name: "int_value",
- credentials: map[string]any{"_token_version": 12345},
- key: "_token_version",
- expected: 12345,
- },
- {
- name: "string_value",
- credentials: map[string]any{"_token_version": "1737654321000"},
- key: "_token_version",
- expected: 1737654321000,
- },
- {
- name: "string_with_spaces",
- credentials: map[string]any{"_token_version": " 1737654321000 "},
- key: "_token_version",
- expected: 1737654321000,
- },
- {
- name: "nil_credentials",
- credentials: nil,
- key: "_token_version",
- expected: 0,
- },
- {
- name: "missing_key",
- credentials: map[string]any{"other_key": 123},
- key: "_token_version",
- expected: 0,
- },
- {
- name: "nil_value",
- credentials: map[string]any{"_token_version": nil},
- key: "_token_version",
- expected: 0,
- },
- {
- name: "invalid_string",
- credentials: map[string]any{"_token_version": "not_a_number"},
- key: "_token_version",
- expected: 0,
- },
- {
- name: "empty_string",
- credentials: map[string]any{"_token_version": ""},
- key: "_token_version",
- expected: 0,
- },
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- account := &Account{Credentials: tt.credentials}
- result := account.GetCredentialAsInt64(tt.key)
- require.Equal(t, tt.expected, result)
- })
- }
- }
- func TestAccount_GetCredentialAsInt64_NilAccount(t *testing.T) {
- var account *Account
- result := account.GetCredentialAsInt64("_token_version")
- require.Equal(t, int64(0), result)
- }
- // ========== CheckTokenVersion 测试 ==========
- func TestCheckTokenVersion(t *testing.T) {
- tests := []struct {
- name string
- account *Account
- latestAccount *Account
- repoErr error
- expectedStale bool
- }{
- {
- name: "nil_account",
- account: nil,
- latestAccount: nil,
- expectedStale: false,
- },
- {
- name: "no_version_in_account_but_db_has_version",
- account: &Account{
- ID: 1,
- Credentials: map[string]any{},
- },
- latestAccount: &Account{
- ID: 1,
- Credentials: map[string]any{"_token_version": int64(100)},
- },
- expectedStale: true, // 当前 account 无版本但 DB 有,说明已被异步刷新,当前已过时
- },
- {
- name: "both_no_version",
- account: &Account{
- ID: 1,
- Credentials: map[string]any{},
- },
- latestAccount: &Account{
- ID: 1,
- Credentials: map[string]any{},
- },
- expectedStale: false, // 两边都没有版本号,说明从未被异步刷新过,允许缓存
- },
- {
- name: "same_version",
- account: &Account{
- ID: 1,
- Credentials: map[string]any{"_token_version": int64(100)},
- },
- latestAccount: &Account{
- ID: 1,
- Credentials: map[string]any{"_token_version": int64(100)},
- },
- expectedStale: false,
- },
- {
- name: "current_version_newer",
- account: &Account{
- ID: 1,
- Credentials: map[string]any{"_token_version": int64(200)},
- },
- latestAccount: &Account{
- ID: 1,
- Credentials: map[string]any{"_token_version": int64(100)},
- },
- expectedStale: false,
- },
- {
- name: "current_version_older_stale",
- account: &Account{
- ID: 1,
- Credentials: map[string]any{"_token_version": int64(100)},
- },
- latestAccount: &Account{
- ID: 1,
- Credentials: map[string]any{"_token_version": int64(200)},
- },
- expectedStale: true, // 当前版本过时
- },
- {
- name: "repo_error",
- account: &Account{
- ID: 1,
- Credentials: map[string]any{"_token_version": int64(100)},
- },
- latestAccount: nil,
- repoErr: errors.New("db error"),
- expectedStale: false, // 查询失败,默认允许缓存
- },
- {
- name: "repo_returns_nil",
- account: &Account{
- ID: 1,
- Credentials: map[string]any{"_token_version": int64(100)},
- },
- latestAccount: nil,
- repoErr: nil,
- expectedStale: false, // 查询返回 nil,默认允许缓存
- },
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- // 由于 CheckTokenVersion 接受 AccountRepository 接口,而创建完整的 mock 很繁琐
- // 这里我们直接测试函数的核心逻辑来验证行为
- if tt.name == "nil_account" {
- _, isStale := CheckTokenVersion(context.Background(), nil, nil)
- require.Equal(t, tt.expectedStale, isStale)
- return
- }
- // 模拟 CheckTokenVersion 的核心逻辑
- account := tt.account
- currentVersion := account.GetCredentialAsInt64("_token_version")
- // 模拟 repo 查询
- latestAccount := tt.latestAccount
- if tt.repoErr != nil || latestAccount == nil {
- require.Equal(t, tt.expectedStale, false)
- return
- }
- latestVersion := latestAccount.GetCredentialAsInt64("_token_version")
- // 情况1: 当前 account 没有版本号,但 DB 中已有版本号
- if currentVersion == 0 && latestVersion > 0 {
- require.Equal(t, tt.expectedStale, true)
- return
- }
- // 情况2: 两边都没有版本号
- if currentVersion == 0 && latestVersion == 0 {
- require.Equal(t, tt.expectedStale, false)
- return
- }
- // 情况3: 比较版本号
- isStale := latestVersion > currentVersion
- require.Equal(t, tt.expectedStale, isStale)
- })
- }
- }
- func TestCheckTokenVersion_NilRepo(t *testing.T) {
- account := &Account{
- ID: 1,
- Credentials: map[string]any{"_token_version": int64(100)},
- }
- _, isStale := CheckTokenVersion(context.Background(), account, nil)
- require.False(t, isStale) // nil repo,默认允许缓存
- }
|