package sql_test import ( "context" "database/sql" "path/filepath" "testing" "time" "ccLoad/internal/model" "ccLoad/internal/storage" ) func TestAuthToken_CreateAndGet(t *testing.T) { t.Parallel() store := newTestStore(t, "auth_tokens.db") ctx := context.Background() // 创建 Auth Token token := &model.AuthToken{ Token: "test-token-hash", Description: "Test Token", IsActive: true, CostLimitMicroUSD: 1000000, // $1 AllowedModels: []string{"gpt-4", "claude-3"}, AllowedChannelIDs: []int64{11, 22}, MaxConcurrency: 3, CreatedAt: time.Now(), } if err := store.CreateAuthToken(ctx, token); err != nil { t.Fatalf("create auth token: %v", err) } // 通过 ID 获取 got, err := store.GetAuthToken(ctx, token.ID) if err != nil { t.Fatalf("get auth token by id: %v", err) } if got.Description != "Test Token" { t.Errorf("description: got %q, want %q", got.Description, "Test Token") } if !got.IsActive { t.Error("expected is_active=true") } if len(got.AllowedChannelIDs) != 2 || got.AllowedChannelIDs[0] != 11 || got.AllowedChannelIDs[1] != 22 { t.Fatalf("allowed_channel_ids: got %+v, want [11 22]", got.AllowedChannelIDs) } if got.MaxConcurrency != 3 { t.Fatalf("max_concurrency: got %d, want 3", got.MaxConcurrency) } // 通过 Token 值获取 gotByValue, err := store.GetAuthTokenByValue(ctx, "test-token-hash") if err != nil { t.Fatalf("get auth token by value: %v", err) } if gotByValue.ID != got.ID { t.Errorf("id mismatch: by value=%d, by id=%d", gotByValue.ID, got.ID) } // 获取不存在的 token _, err = store.GetAuthToken(ctx, 99999) if err == nil { t.Error("expected error for non-existent token") } } func TestAuthToken_InvalidAllowedChannelIDsJSON_ReturnsError(t *testing.T) { t.Parallel() tmp := t.TempDir() dbPath := filepath.Join(tmp, "invalid_allowed_channel_ids.db") store, err := storage.CreateSQLiteStore(dbPath) if err != nil { t.Fatalf("create sqlite store: %v", err) } t.Cleanup(func() { _ = store.Close() }) ctx := context.Background() token := &model.AuthToken{ Token: "bad-channel-json-token", Description: "Bad Channel JSON Token", IsActive: true, AllowedChannelIDs: []int64{1}, CreatedAt: time.Now(), } if err := store.CreateAuthToken(ctx, token); err != nil { t.Fatalf("create auth token: %v", err) } if err := store.Close(); err != nil { t.Fatalf("close store: %v", err) } db, err := sql.Open("sqlite", "file:"+dbPath) if err != nil { t.Fatalf("open sqlite db: %v", err) } _, err = db.ExecContext(ctx, `UPDATE auth_tokens SET allowed_channel_ids = ? WHERE id = ?`, `{not-json`, token.ID) _ = db.Close() if err != nil { t.Fatalf("tamper allowed_channel_ids: %v", err) } store2, err := storage.CreateSQLiteStore(dbPath) if err == nil { _ = store2.Close() t.Fatal("expected reopen sqlite store to fail due to invalid allowed_channel_ids json") } } func TestAuthToken_InvalidAllowedModelsJSON_ReturnsError(t *testing.T) { t.Parallel() tmp := t.TempDir() dbPath := filepath.Join(tmp, "invalid_allowed_models.db") store, err := storage.CreateSQLiteStore(dbPath) if err != nil { t.Fatalf("create sqlite store: %v", err) } t.Cleanup(func() { _ = store.Close() }) ctx := context.Background() token := &model.AuthToken{ Token: "bad-json-token", Description: "Bad JSON Token", IsActive: true, AllowedModels: []string{"gpt-4"}, CreatedAt: time.Now(), } if err := store.CreateAuthToken(ctx, token); err != nil { t.Fatalf("create auth token: %v", err) } if err := store.Close(); err != nil { t.Fatalf("close store: %v", err) } db, err := sql.Open("sqlite", "file:"+dbPath) if err != nil { t.Fatalf("open sqlite db: %v", err) } _, err = db.ExecContext(ctx, `UPDATE auth_tokens SET allowed_models = ? WHERE id = ?`, `{not-json`, token.ID) _ = db.Close() if err != nil { t.Fatalf("tamper allowed_models: %v", err) } store2, err := storage.CreateSQLiteStore(dbPath) if err == nil { _ = store2.Close() t.Fatal("expected reopen sqlite store to fail due to invalid allowed_models json") } } func TestAuthToken_NegativeMaxConcurrency_ReturnsError(t *testing.T) { t.Parallel() tmp := t.TempDir() dbPath := filepath.Join(tmp, "negative_max_concurrency.db") store, err := storage.CreateSQLiteStore(dbPath) if err != nil { t.Fatalf("create sqlite store: %v", err) } t.Cleanup(func() { _ = store.Close() }) ctx := context.Background() token := &model.AuthToken{ Token: "negative-max-concurrency-token", Description: "Negative Max Concurrency Token", IsActive: true, MaxConcurrency: 1, CreatedAt: time.Now(), } if err := store.CreateAuthToken(ctx, token); err != nil { t.Fatalf("create auth token: %v", err) } if err := store.Close(); err != nil { t.Fatalf("close store: %v", err) } db, err := sql.Open("sqlite", "file:"+dbPath) if err != nil { t.Fatalf("open sqlite db: %v", err) } _, err = db.ExecContext(ctx, `UPDATE auth_tokens SET max_concurrency = ? WHERE id = ?`, -1, token.ID) _ = db.Close() if err != nil { t.Fatalf("tamper max_concurrency: %v", err) } store2, err := storage.CreateSQLiteStore(dbPath) if err == nil { _ = store2.Close() t.Fatal("expected reopen sqlite store to fail due to negative max_concurrency") } } func TestAuthToken_List(t *testing.T) { t.Parallel() store := newTestStore(t, "list.db") ctx := context.Background() // 创建多个 Auth Tokens for i := 0; i < 3; i++ { token := &model.AuthToken{ Token: "token-" + string(rune('A'+i)), Description: "Token " + string(rune('A'+i)), IsActive: i%2 == 0, // A, C 是 active CreatedAt: time.Now(), } if err := store.CreateAuthToken(ctx, token); err != nil { t.Fatalf("create token %d: %v", i, err) } } // 列出所有 tokens allTokens, err := store.ListAuthTokens(ctx) if err != nil { t.Fatalf("list auth tokens: %v", err) } if len(allTokens) != 3 { t.Errorf("expected 3 tokens, got %d", len(allTokens)) } // 列出活跃的 tokens activeTokens, err := store.ListActiveAuthTokens(ctx) if err != nil { t.Fatalf("list active auth tokens: %v", err) } if len(activeTokens) != 2 { t.Errorf("expected 2 active tokens, got %d", len(activeTokens)) } } func TestAuthToken_Update(t *testing.T) { t.Parallel() store := newTestStore(t, "update.db") ctx := context.Background() // 创建 token expiresAt := time.Now().Add(30 * 24 * time.Hour).UnixMilli() lastUsedAt := time.Now().UnixMilli() token := &model.AuthToken{ Token: "update-test-token", Description: "Original Description", IsActive: true, ExpiresAt: &expiresAt, LastUsedAt: &lastUsedAt, CreatedAt: time.Now(), } if err := store.CreateAuthToken(ctx, token); err != nil { t.Fatalf("create auth token: %v", err) } // 更新 token token.Description = "Updated Description" token.IsActive = false token.CostLimitMicroUSD = 5000000 // $5 token.AllowedChannelIDs = []int64{33} token.MaxConcurrency = 2 if err := store.UpdateAuthToken(ctx, token); err != nil { t.Fatalf("update auth token: %v", err) } // 验证更新 got, err := store.GetAuthToken(ctx, token.ID) if err != nil { t.Fatalf("get auth token: %v", err) } if got.Description != "Updated Description" { t.Errorf("description: got %q, want %q", got.Description, "Updated Description") } if got.IsActive { t.Error("expected is_active=false") } if got.CostLimitMicroUSD != 5000000 { t.Errorf("cost limit: got %d, want %d", got.CostLimitMicroUSD, 5000000) } if len(got.AllowedChannelIDs) != 1 || got.AllowedChannelIDs[0] != 33 { t.Fatalf("allowed_channel_ids: got %+v, want [33]", got.AllowedChannelIDs) } if got.MaxConcurrency != 2 { t.Fatalf("max_concurrency: got %d, want 2", got.MaxConcurrency) } } func TestAuthToken_Delete(t *testing.T) { t.Parallel() store := newTestStore(t, "delete.db") ctx := context.Background() // 创建 token token := &model.AuthToken{ Token: "delete-test-token", Description: "To Delete", IsActive: true, CreatedAt: time.Now(), } if err := store.CreateAuthToken(ctx, token); err != nil { t.Fatalf("create auth token: %v", err) } // 删除 token if err := store.DeleteAuthToken(ctx, token.ID); err != nil { t.Fatalf("delete auth token: %v", err) } // 验证已删除 _, err := store.GetAuthToken(ctx, token.ID) if err == nil { t.Error("expected error after delete") } } func TestAuthToken_UpdateLastUsed(t *testing.T) { t.Parallel() store := newTestStore(t, "last_used.db") ctx := context.Background() // 创建 token token := &model.AuthToken{ Token: "last-used-test", Description: "Last Used Test", IsActive: true, CreatedAt: time.Now(), } if err := store.CreateAuthToken(ctx, token); err != nil { t.Fatalf("create auth token: %v", err) } // 初始时 last_used_at 在 DB 是 0,但 scan 会把 0 映射为 nil(omitempty 语义) got, err := store.GetAuthToken(ctx, token.ID) if err != nil { t.Fatalf("get auth token: %v", err) } if got.LastUsedAt != nil { t.Fatalf("expected last_used_at to be nil initially, got=%v", got.LastUsedAt) } // 更新 last_used_at if err := store.UpdateTokenLastUsed(ctx, "last-used-test", time.Now()); err != nil { t.Fatalf("update token last used: %v", err) } // 验证更新 got, err = store.GetAuthToken(ctx, token.ID) if err != nil { t.Fatalf("get auth token after update: %v", err) } if got.LastUsedAt == nil || *got.LastUsedAt <= 0 { t.Fatalf("expected last_used_at to be set, got=%v", got.LastUsedAt) } }