| 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() |
|
|
| |
| token := &model.AuthToken{ |
| Token: "test-token-hash", |
| Description: "Test Token", |
| IsActive: true, |
| CostLimitMicroUSD: 1000000, |
| 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) |
| } |
|
|
| |
| 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) |
| } |
|
|
| |
| 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) |
| } |
|
|
| |
| _, 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() |
|
|
| |
| 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, |
| CreatedAt: time.Now(), |
| } |
| if err := store.CreateAuthToken(ctx, token); err != nil { |
| t.Fatalf("create token %d: %v", i, err) |
| } |
| } |
|
|
| |
| 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)) |
| } |
|
|
| |
| 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() |
|
|
| |
| 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.Description = "Updated Description" |
| token.IsActive = false |
| token.CostLimitMicroUSD = 5000000 |
| 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 := &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) |
| } |
|
|
| |
| 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 := &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) |
| } |
|
|
| |
| 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) |
| } |
|
|
| |
| 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) |
| } |
| } |
|
|