UserRepository.Update and APIKeyRepository.Update rewrote the whole row on every call, regardless of which fields the caller meant to change. Several columns on those tables are maintained by dedicated atomic paths (balance deduction, quota and rate-limit counters, limit adjustments, activity timestamps), so a caller holding a slightly older snapshot could silently roll them back - a lost update. Both methods now take an explicit column mask and persist only the columns the caller declares; everything else keeps its current database value. - All user and API-key call sites declare exactly what they mutate, which turns admin edits and profile saves into genuine partial updates. - Email uniqueness locking/lookup and allowed_groups sync only run when those fields are part of the update. - UserUpdateFields deliberately has no balance/total_recharged members, so Update cannot touch them. New AdjustBalance/SetBalance apply the change in a single statement and return before/after values; admin balance adjustment uses them instead of read-modify-write. - promo_codes.used_count is no longer written by Update; it is only ever incremented by the redemption path. - The billing hot path that marks an API key quota-exhausted writes only status. - Dropped a no-op row write in RevokeAllUserTokens: users has no token_version column, so it persisted nothing while still overwriting concurrently-updated columns. Adds integration coverage that a stale snapshot cannot revert concurrent atomic writes, and unit coverage pinning the column set each entry point declares.
502 lines
18 KiB
Go
502 lines
18 KiB
Go
//go:build unit
|
|
|
|
// API Key 服务删除方法的单元测试
|
|
// 测试 APIKeyService.Delete 方法在各种场景下的行为,
|
|
// 包括权限验证、缓存清理和错误处理
|
|
|
|
package service
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/Wei-Shaw/sub2api/internal/pkg/pagination"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// apiKeyRepoStub 是 APIKeyRepository 接口的测试桩实现。
|
|
// 用于隔离测试 APIKeyService.Delete 方法,避免依赖真实数据库。
|
|
//
|
|
// 设计说明:
|
|
// - apiKey/getByIDErr: 模拟 GetKeyAndOwnerID 返回的记录与错误
|
|
// - deleteErr: 模拟 Delete 返回的错误
|
|
// - deletedIDs: 记录被调用删除的 API Key ID,用于断言验证
|
|
type apiKeyRepoStub struct {
|
|
apiKey *APIKey // GetKeyAndOwnerID 的返回值
|
|
getByIDErr error // GetKeyAndOwnerID 的错误返回值
|
|
deleteErr error // Delete 的错误返回值
|
|
updateErr error // Update 的错误返回值
|
|
deletedIDs []int64 // 记录已删除的 API Key ID 列表
|
|
updatedKeys []APIKey
|
|
allowListByUserID bool
|
|
listByUserIDKeys []APIKey
|
|
listByUserIDErr error
|
|
listByUserIDCalls []int64
|
|
listByUserIDParams []pagination.PaginationParams
|
|
listByUserIDFilters []APIKeyListFilters
|
|
allowListAllByUserID bool
|
|
listAllByUserIDKeys []APIKey
|
|
listAllByUserIDErr error
|
|
listAllByUserIDCalls []int64
|
|
listAllByUserIDFilters []APIKeyListFilters
|
|
updateLastUsed func(ctx context.Context, id int64, usedAt time.Time) error
|
|
touchedIDs []int64
|
|
touchedUsedAts []time.Time
|
|
}
|
|
|
|
// 以下方法在本测试中不应被调用,使用 panic 确保测试失败时能快速定位问题
|
|
|
|
func (s *apiKeyRepoStub) Create(ctx context.Context, key *APIKey) error {
|
|
panic("unexpected Create call")
|
|
}
|
|
|
|
func (s *apiKeyRepoStub) GetByID(ctx context.Context, id int64) (*APIKey, error) {
|
|
if s.getByIDErr != nil {
|
|
return nil, s.getByIDErr
|
|
}
|
|
if s.apiKey != nil {
|
|
clone := *s.apiKey
|
|
return &clone, nil
|
|
}
|
|
panic("unexpected GetByID call")
|
|
}
|
|
|
|
func (s *apiKeyRepoStub) GetKeyAndOwnerID(ctx context.Context, id int64) (string, int64, error) {
|
|
if s.getByIDErr != nil {
|
|
return "", 0, s.getByIDErr
|
|
}
|
|
if s.apiKey != nil {
|
|
return s.apiKey.Key, s.apiKey.UserID, nil
|
|
}
|
|
return "", 0, ErrAPIKeyNotFound
|
|
}
|
|
|
|
func (s *apiKeyRepoStub) GetByKey(ctx context.Context, key string) (*APIKey, error) {
|
|
panic("unexpected GetByKey call")
|
|
}
|
|
|
|
func (s *apiKeyRepoStub) GetByKeyForAuth(ctx context.Context, key string) (*APIKey, error) {
|
|
panic("unexpected GetByKeyForAuth call")
|
|
}
|
|
|
|
func (s *apiKeyRepoStub) Update(ctx context.Context, key *APIKey, _ APIKeyUpdateFields) error {
|
|
if key != nil {
|
|
s.updatedKeys = append(s.updatedKeys, *key)
|
|
}
|
|
return s.updateErr
|
|
}
|
|
|
|
// Delete 记录被删除的 API Key ID 并返回预设的错误。
|
|
// 通过 deletedIDs 可以验证删除操作是否被正确调用。
|
|
func (s *apiKeyRepoStub) Delete(ctx context.Context, id int64) error {
|
|
s.deletedIDs = append(s.deletedIDs, id)
|
|
return s.deleteErr
|
|
}
|
|
|
|
// DeleteWithAudit 与 Delete 一样记录被删除的 ID,供 service 测试断言。
|
|
func (s *apiKeyRepoStub) DeleteWithAudit(ctx context.Context, id int64) error {
|
|
s.deletedIDs = append(s.deletedIDs, id)
|
|
return s.deleteErr
|
|
}
|
|
|
|
// 以下是接口要求实现但本测试不关心的方法
|
|
|
|
func (s *apiKeyRepoStub) ListByUserID(ctx context.Context, userID int64, params pagination.PaginationParams, filters APIKeyListFilters) ([]APIKey, *pagination.PaginationResult, error) {
|
|
if !s.allowListByUserID {
|
|
panic("unexpected ListByUserID call")
|
|
}
|
|
s.listByUserIDCalls = append(s.listByUserIDCalls, userID)
|
|
s.listByUserIDParams = append(s.listByUserIDParams, params)
|
|
s.listByUserIDFilters = append(s.listByUserIDFilters, filters)
|
|
if s.listByUserIDErr != nil {
|
|
return nil, nil, s.listByUserIDErr
|
|
}
|
|
keys := append([]APIKey(nil), s.listByUserIDKeys...)
|
|
return keys, &pagination.PaginationResult{
|
|
Total: int64(len(keys)),
|
|
Page: params.Page,
|
|
PageSize: params.PageSize,
|
|
Pages: 1,
|
|
}, nil
|
|
}
|
|
|
|
func (s *apiKeyRepoStub) ListAllByUserID(ctx context.Context, userID int64, filters APIKeyListFilters) ([]APIKey, error) {
|
|
if !s.allowListAllByUserID {
|
|
panic("unexpected ListAllByUserID call")
|
|
}
|
|
s.listAllByUserIDCalls = append(s.listAllByUserIDCalls, userID)
|
|
s.listAllByUserIDFilters = append(s.listAllByUserIDFilters, filters)
|
|
if s.listAllByUserIDErr != nil {
|
|
return nil, s.listAllByUserIDErr
|
|
}
|
|
source := s.listByUserIDKeys
|
|
if s.listAllByUserIDKeys != nil {
|
|
source = s.listAllByUserIDKeys
|
|
}
|
|
return filterAPIKeyStubKeys(userID, source, filters), nil
|
|
}
|
|
|
|
func filterAPIKeyStubKeys(userID int64, keys []APIKey, filters APIKeyListFilters) []APIKey {
|
|
result := make([]APIKey, 0, len(keys))
|
|
search := strings.ToLower(filters.Search)
|
|
for _, key := range keys {
|
|
if key.UserID != userID {
|
|
continue
|
|
}
|
|
if search != "" &&
|
|
!strings.Contains(strings.ToLower(key.Name), search) &&
|
|
!strings.Contains(strings.ToLower(key.Key), search) {
|
|
continue
|
|
}
|
|
if filters.Status != "" && key.Status != filters.Status {
|
|
continue
|
|
}
|
|
if filters.GroupID != nil {
|
|
if *filters.GroupID == 0 {
|
|
if key.GroupID != nil {
|
|
continue
|
|
}
|
|
} else if key.GroupID == nil || *key.GroupID != *filters.GroupID {
|
|
continue
|
|
}
|
|
}
|
|
result = append(result, key)
|
|
}
|
|
return result
|
|
}
|
|
|
|
func (s *apiKeyRepoStub) VerifyOwnership(ctx context.Context, userID int64, apiKeyIDs []int64) ([]int64, error) {
|
|
panic("unexpected VerifyOwnership call")
|
|
}
|
|
|
|
func (s *apiKeyRepoStub) CountByUserID(ctx context.Context, userID int64) (int64, error) {
|
|
panic("unexpected CountByUserID call")
|
|
}
|
|
|
|
func (s *apiKeyRepoStub) ExistsByKey(ctx context.Context, key string) (bool, error) {
|
|
panic("unexpected ExistsByKey call")
|
|
}
|
|
|
|
func (s *apiKeyRepoStub) ListByGroupID(ctx context.Context, groupID int64, params pagination.PaginationParams) ([]APIKey, *pagination.PaginationResult, error) {
|
|
panic("unexpected ListByGroupID call")
|
|
}
|
|
|
|
func (s *apiKeyRepoStub) SearchAPIKeys(ctx context.Context, userID int64, keyword string, limit int) ([]APIKey, error) {
|
|
panic("unexpected SearchAPIKeys call")
|
|
}
|
|
|
|
func (s *apiKeyRepoStub) ClearGroupIDByGroupID(ctx context.Context, groupID int64) (int64, error) {
|
|
panic("unexpected ClearGroupIDByGroupID call")
|
|
}
|
|
func (s *apiKeyRepoStub) UpdateGroupIDByUserAndGroup(ctx context.Context, userID, oldGroupID, newGroupID int64) (int64, error) {
|
|
panic("unexpected UpdateGroupIDByUserAndGroup call")
|
|
}
|
|
|
|
func (s *apiKeyRepoStub) CountByGroupID(ctx context.Context, groupID int64) (int64, error) {
|
|
panic("unexpected CountByGroupID call")
|
|
}
|
|
|
|
func (s *apiKeyRepoStub) ListKeysByUserID(ctx context.Context, userID int64) ([]string, error) {
|
|
panic("unexpected ListKeysByUserID call")
|
|
}
|
|
|
|
func (s *apiKeyRepoStub) ListKeysByGroupID(ctx context.Context, groupID int64) ([]string, error) {
|
|
panic("unexpected ListKeysByGroupID call")
|
|
}
|
|
|
|
func (s *apiKeyRepoStub) IncrementQuotaUsed(ctx context.Context, id int64, amount float64) (float64, error) {
|
|
panic("unexpected IncrementQuotaUsed call")
|
|
}
|
|
|
|
func (s *apiKeyRepoStub) UpdateLastUsed(ctx context.Context, id int64, usedAt time.Time) error {
|
|
s.touchedIDs = append(s.touchedIDs, id)
|
|
s.touchedUsedAts = append(s.touchedUsedAts, usedAt)
|
|
if s.updateLastUsed != nil {
|
|
return s.updateLastUsed(ctx, id, usedAt)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *apiKeyRepoStub) IncrementRateLimitUsage(ctx context.Context, id int64, cost float64) error {
|
|
panic("unexpected IncrementRateLimitUsage call")
|
|
}
|
|
|
|
func (s *apiKeyRepoStub) ResetRateLimitWindows(ctx context.Context, id int64) error {
|
|
panic("unexpected ResetRateLimitWindows call")
|
|
}
|
|
|
|
func (s *apiKeyRepoStub) GetRateLimitData(ctx context.Context, id int64) (*APIKeyRateLimitData, error) {
|
|
panic("unexpected GetRateLimitData call")
|
|
}
|
|
|
|
// apiKeyCacheStub 是 APIKeyCache 接口的测试桩实现。
|
|
// 用于验证删除操作时缓存清理逻辑是否被正确调用。
|
|
//
|
|
// 设计说明:
|
|
// - invalidated: 记录被清除缓存的用户 ID 列表
|
|
type apiKeyCacheStub struct {
|
|
invalidated []int64 // 记录调用 DeleteCreateAttemptCount 时传入的用户 ID
|
|
deleteAuthKeys []string // 记录调用 DeleteAuthCache 时传入的缓存 key
|
|
}
|
|
|
|
// GetCreateAttemptCount 返回 0,表示用户未超过创建次数限制
|
|
func (s *apiKeyCacheStub) GetCreateAttemptCount(ctx context.Context, userID int64) (int, error) {
|
|
return 0, nil
|
|
}
|
|
|
|
// IncrementCreateAttemptCount 空实现,本测试不验证此行为
|
|
func (s *apiKeyCacheStub) IncrementCreateAttemptCount(ctx context.Context, userID int64) error {
|
|
return nil
|
|
}
|
|
|
|
// DeleteCreateAttemptCount 记录被清除缓存的用户 ID。
|
|
// 删除 API Key 时会调用此方法清除用户的创建尝试计数缓存。
|
|
func (s *apiKeyCacheStub) DeleteCreateAttemptCount(ctx context.Context, userID int64) error {
|
|
s.invalidated = append(s.invalidated, userID)
|
|
return nil
|
|
}
|
|
|
|
// IncrementDailyUsage 空实现,本测试不验证此行为
|
|
func (s *apiKeyCacheStub) IncrementDailyUsage(ctx context.Context, apiKey string) error {
|
|
return nil
|
|
}
|
|
|
|
// SetDailyUsageExpiry 空实现,本测试不验证此行为
|
|
func (s *apiKeyCacheStub) SetDailyUsageExpiry(ctx context.Context, apiKey string, ttl time.Duration) error {
|
|
return nil
|
|
}
|
|
|
|
func (s *apiKeyCacheStub) GetAuthCache(ctx context.Context, key string) (*APIKeyAuthCacheEntry, error) {
|
|
return nil, nil
|
|
}
|
|
|
|
func (s *apiKeyCacheStub) SetAuthCache(ctx context.Context, key string, entry *APIKeyAuthCacheEntry, ttl time.Duration) error {
|
|
return nil
|
|
}
|
|
|
|
func (s *apiKeyCacheStub) DeleteAuthCache(ctx context.Context, key string) error {
|
|
s.deleteAuthKeys = append(s.deleteAuthKeys, key)
|
|
return nil
|
|
}
|
|
|
|
func (s *apiKeyCacheStub) PublishAuthCacheInvalidation(ctx context.Context, cacheKey string) error {
|
|
return nil
|
|
}
|
|
|
|
func (s *apiKeyCacheStub) SubscribeAuthCacheInvalidation(ctx context.Context, handler func(cacheKey string)) error {
|
|
return nil
|
|
}
|
|
|
|
// TestApiKeyService_Delete_OwnerMismatch 测试非所有者尝试删除时返回权限错误。
|
|
// 预期行为:
|
|
// - GetKeyAndOwnerID 返回所有者 ID 为 1
|
|
// - 调用者 userID 为 2(不匹配)
|
|
// - 返回 ErrInsufficientPerms 错误
|
|
// - Delete 方法不被调用
|
|
// - 缓存不被清除
|
|
func TestApiKeyService_Delete_OwnerMismatch(t *testing.T) {
|
|
repo := &apiKeyRepoStub{
|
|
apiKey: &APIKey{ID: 10, UserID: 1, Key: "k"},
|
|
}
|
|
cache := &apiKeyCacheStub{}
|
|
svc := &APIKeyService{apiKeyRepo: repo, cache: cache}
|
|
|
|
err := svc.Delete(context.Background(), 10, 2) // API Key ID=10, 调用者 userID=2
|
|
require.ErrorIs(t, err, ErrInsufficientPerms)
|
|
require.Empty(t, repo.deletedIDs) // 验证删除操作未被调用
|
|
require.Empty(t, cache.invalidated) // 验证缓存未被清除
|
|
require.Empty(t, cache.deleteAuthKeys)
|
|
}
|
|
|
|
// TestApiKeyService_Delete_Success 测试所有者成功删除 API Key 的场景。
|
|
// 预期行为:
|
|
// - GetKeyAndOwnerID 返回所有者 ID 为 7
|
|
// - 调用者 userID 为 7(匹配)
|
|
// - Delete 成功执行
|
|
// - 缓存被正确清除(使用 ownerID)
|
|
// - 返回 nil 错误
|
|
func TestApiKeyService_Delete_Success(t *testing.T) {
|
|
repo := &apiKeyRepoStub{
|
|
apiKey: &APIKey{ID: 42, UserID: 7, Key: "k"},
|
|
}
|
|
cache := &apiKeyCacheStub{}
|
|
svc := &APIKeyService{apiKeyRepo: repo, cache: cache}
|
|
svc.lastUsedTouchL1.Store(int64(42), time.Now())
|
|
|
|
err := svc.Delete(context.Background(), 42, 7) // API Key ID=42, 调用者 userID=7
|
|
require.NoError(t, err)
|
|
require.Equal(t, []int64{42}, repo.deletedIDs) // 验证正确的 API Key 被删除
|
|
require.Equal(t, []int64{7}, cache.invalidated) // 验证所有者的缓存被清除
|
|
require.Equal(t, []string{svc.authCacheKey("k")}, cache.deleteAuthKeys)
|
|
_, exists := svc.lastUsedTouchL1.Load(int64(42))
|
|
require.False(t, exists, "delete should clear touch debounce cache")
|
|
}
|
|
|
|
// TestApiKeyService_Delete_NotFound 测试删除不存在的 API Key 时返回正确的错误。
|
|
// 预期行为:
|
|
// - GetKeyAndOwnerID 返回 ErrAPIKeyNotFound 错误
|
|
// - 返回 ErrAPIKeyNotFound 错误(被 fmt.Errorf 包装)
|
|
// - Delete 方法不被调用
|
|
// - 缓存不被清除
|
|
func TestApiKeyService_Delete_NotFound(t *testing.T) {
|
|
repo := &apiKeyRepoStub{getByIDErr: ErrAPIKeyNotFound}
|
|
cache := &apiKeyCacheStub{}
|
|
svc := &APIKeyService{apiKeyRepo: repo, cache: cache}
|
|
|
|
err := svc.Delete(context.Background(), 99, 1)
|
|
require.ErrorIs(t, err, ErrAPIKeyNotFound)
|
|
require.Empty(t, repo.deletedIDs)
|
|
require.Empty(t, cache.invalidated)
|
|
require.Empty(t, cache.deleteAuthKeys)
|
|
}
|
|
|
|
func TestAPIKeyService_List_FillsCurrentConcurrency(t *testing.T) {
|
|
repo := &apiKeyRepoStub{
|
|
allowListByUserID: true,
|
|
listByUserIDKeys: []APIKey{
|
|
{ID: 10, UserID: 7, Key: "sk-10", Name: "key-10"},
|
|
{ID: 11, UserID: 7, Key: "sk-11", Name: "key-11"},
|
|
},
|
|
}
|
|
concurrency := NewConcurrencyService(&stubConcurrencyCacheForTest{
|
|
apiKeyConcurrency: map[int64]int{10: 2, 11: 0},
|
|
})
|
|
svc := &APIKeyService{apiKeyRepo: repo, concurrencyService: concurrency}
|
|
|
|
keys, _, err := svc.List(context.Background(), 7, pagination.PaginationParams{Page: 1, PageSize: 20}, APIKeyListFilters{})
|
|
require.NoError(t, err)
|
|
require.Len(t, keys, 2)
|
|
require.Equal(t, 2, keys[0].CurrentConcurrency)
|
|
require.Equal(t, 0, keys[1].CurrentConcurrency)
|
|
}
|
|
|
|
func TestAPIKeyService_List_SortByCurrentConcurrency(t *testing.T) {
|
|
groupID := int64(42)
|
|
keys := []APIKey{
|
|
{ID: 1, UserID: 7, Key: "sk-target-1", Name: "target-one", GroupID: &groupID, Status: StatusActive},
|
|
{ID: 2, UserID: 7, Key: "sk-target-2", Name: "target-two", GroupID: &groupID, Status: StatusActive},
|
|
{ID: 3, UserID: 7, Key: "sk-target-3", Name: "target-three", GroupID: &groupID, Status: StatusActive},
|
|
{ID: 4, UserID: 7, Key: "sk-target-4", Name: "target-four", GroupID: &groupID, Status: StatusActive},
|
|
{ID: 9, UserID: 7, Key: "sk-target-9", Name: "target-inactive", GroupID: &groupID, Status: StatusDisabled},
|
|
{ID: 10, UserID: 7, Key: "sk-other-10", Name: "other", GroupID: &groupID, Status: StatusActive},
|
|
{ID: 11, UserID: 7, Key: "sk-target-11", Name: "target-no-group", Status: StatusActive},
|
|
{ID: 12, UserID: 8, Key: "sk-target-12", Name: "target-other-user", GroupID: &groupID, Status: StatusActive},
|
|
}
|
|
filters := APIKeyListFilters{
|
|
Search: "target",
|
|
Status: StatusActive,
|
|
GroupID: &groupID,
|
|
}
|
|
repo := &apiKeyRepoStub{
|
|
allowListAllByUserID: true,
|
|
listAllByUserIDKeys: keys,
|
|
}
|
|
concurrency := NewConcurrencyService(&stubConcurrencyCacheForTest{
|
|
apiKeyConcurrency: map[int64]int{
|
|
1: 5,
|
|
2: 5,
|
|
3: 2,
|
|
4: 8,
|
|
9: 99,
|
|
10: 99,
|
|
11: 99,
|
|
12: 99,
|
|
},
|
|
})
|
|
svc := &APIKeyService{apiKeyRepo: repo, concurrencyService: concurrency}
|
|
|
|
got, page, err := svc.List(context.Background(), 7, pagination.PaginationParams{
|
|
Page: 2,
|
|
PageSize: 2,
|
|
SortBy: "current_concurrency",
|
|
SortOrder: "desc",
|
|
}, filters)
|
|
require.NoError(t, err)
|
|
require.Equal(t, []int64{1, 3}, apiKeyTestIDs(got))
|
|
require.Equal(t, int64(4), page.Total)
|
|
require.Equal(t, 2, page.Page)
|
|
require.Equal(t, 2, page.PageSize)
|
|
require.Equal(t, 2, page.Pages)
|
|
require.Empty(t, repo.listByUserIDCalls)
|
|
require.Equal(t, []int64{7}, repo.listAllByUserIDCalls)
|
|
require.Len(t, repo.listAllByUserIDFilters, 1)
|
|
require.Equal(t, filters.Search, repo.listAllByUserIDFilters[0].Search)
|
|
require.Equal(t, filters.Status, repo.listAllByUserIDFilters[0].Status)
|
|
require.NotNil(t, repo.listAllByUserIDFilters[0].GroupID)
|
|
require.Equal(t, groupID, *repo.listAllByUserIDFilters[0].GroupID)
|
|
}
|
|
|
|
func TestAPIKeyService_List_SortByCurrentConcurrencyAscTiesByID(t *testing.T) {
|
|
repo := &apiKeyRepoStub{
|
|
allowListAllByUserID: true,
|
|
listAllByUserIDKeys: []APIKey{
|
|
{ID: 1, UserID: 7, Key: "sk-1", Name: "one", Status: StatusActive},
|
|
{ID: 2, UserID: 7, Key: "sk-2", Name: "two", Status: StatusActive},
|
|
{ID: 3, UserID: 7, Key: "sk-3", Name: "three", Status: StatusActive},
|
|
{ID: 4, UserID: 7, Key: "sk-4", Name: "four", Status: StatusActive},
|
|
},
|
|
}
|
|
concurrency := NewConcurrencyService(&stubConcurrencyCacheForTest{
|
|
apiKeyConcurrency: map[int64]int{1: 5, 2: 5, 3: 2, 4: 8},
|
|
})
|
|
svc := &APIKeyService{apiKeyRepo: repo, concurrencyService: concurrency}
|
|
|
|
got, page, err := svc.List(context.Background(), 7, pagination.PaginationParams{
|
|
Page: 1,
|
|
PageSize: 4,
|
|
SortBy: "current_concurrency",
|
|
SortOrder: "asc",
|
|
}, APIKeyListFilters{})
|
|
require.NoError(t, err)
|
|
require.Equal(t, []int64{3, 1, 2, 4}, apiKeyTestIDs(got))
|
|
require.Equal(t, 4, page.PageSize)
|
|
}
|
|
|
|
func apiKeyTestIDs(keys []APIKey) []int64 {
|
|
ids := make([]int64, 0, len(keys))
|
|
for _, key := range keys {
|
|
ids = append(ids, key.ID)
|
|
}
|
|
return ids
|
|
}
|
|
|
|
func TestAPIKeyService_GetByID_FillsCurrentConcurrency(t *testing.T) {
|
|
repo := &apiKeyRepoStub{
|
|
apiKey: &APIKey{ID: 10, UserID: 7, Key: "sk-10", Name: "key-10"},
|
|
}
|
|
concurrency := NewConcurrencyService(&stubConcurrencyCacheForTest{
|
|
apiKeyConcurrency: map[int64]int{10: 4},
|
|
})
|
|
svc := &APIKeyService{apiKeyRepo: repo, concurrencyService: concurrency}
|
|
|
|
key, err := svc.GetByID(context.Background(), 10)
|
|
require.NoError(t, err)
|
|
require.Equal(t, 4, key.CurrentConcurrency)
|
|
}
|
|
|
|
// TestApiKeyService_Delete_DeleteFails 测试删除操作失败时的错误处理。
|
|
// 预期行为:
|
|
// - GetKeyAndOwnerID 返回正确的所有者 ID
|
|
// - 所有权验证通过
|
|
// - DeleteWithAudit 被调用但返回错误
|
|
// - 删除失败时缓存不被清除(缓存清理在删除成功后执行,消除竞态)
|
|
// - 返回包含 "delete api key" 的错误信息
|
|
func TestApiKeyService_Delete_DeleteFails(t *testing.T) {
|
|
repo := &apiKeyRepoStub{
|
|
apiKey: &APIKey{ID: 42, UserID: 3, Key: "k"},
|
|
deleteErr: errors.New("delete failed"),
|
|
}
|
|
cache := &apiKeyCacheStub{}
|
|
svc := &APIKeyService{apiKeyRepo: repo, cache: cache}
|
|
|
|
err := svc.Delete(context.Background(), 3, 3) // API Key ID=3, 调用者 userID=3
|
|
require.Error(t, err)
|
|
require.ErrorContains(t, err, "delete api key")
|
|
require.Equal(t, []int64{3}, repo.deletedIDs) // 验证 DeleteWithAudit 被调用
|
|
require.Empty(t, cache.invalidated) // 验证删除失败时缓存未被清除(新顺序:先删后清)
|
|
require.Empty(t, cache.deleteAuthKeys) // 验证删除失败时 auth 缓存未被清除
|
|
}
|