package integration import ( "bytes" "context" "encoding/base64" "encoding/json" "errors" "strings" "sync" "testing" "git.sundynix.cn/Blizzard/Veyra/internal/apperror" "git.sundynix.cn/Blizzard/Veyra/internal/model" "git.sundynix.cn/Blizzard/Veyra/internal/repository" "git.sundynix.cn/Blizzard/Veyra/internal/secret" "git.sundynix.cn/Blizzard/Veyra/internal/service" "github.com/google/uuid" "github.com/jackc/pgx/v5/pgxpool" ) func testProviderLifecycle(t *testing.T, ctx context.Context, pool *pgxpool.Pool, actor, ordinary model.User) { t.Helper() vault, err := secret.NewVault(base64.StdEncoding.EncodeToString(bytes.Repeat([]byte{9}, 32))) if err != nil { t.Fatal(err) } repo := repository.NewProviderRepository(pool) svc := service.NewProviderService(repo, vault) configs, err := svc.List(ctx, actor) if err != nil || len(configs) != 0 { t.Fatalf("configuration must start empty: %v", err) } input := service.ProviderInput{TimeoutSeconds: 15} config, err := svc.Save(ctx, actor, "model_primary", uuid.NewString(), input) if err != nil || config.Enabled || config.BaseURL != "" || config.LastTestStatus != "NOT_CONFIGURED" || config.WireAPI != "responses" || !config.DisableResponseStorage || !config.RequiresOpenAIAuth { t.Fatalf("blank configuration: %v", err) } if _, err := svc.List(ctx, ordinary); !errors.Is(err, apperror.ErrForbidden) { t.Fatal("ordinary user accessed provider configuration") } if _, err := svc.Save(ctx, actor, "model_primary", uuid.NewString(), input); !errors.Is(err, apperror.ErrConflict) { t.Fatal("stale configuration accepted") } token := "test-private-provider-key" input = service.ProviderInput{Name: "configured vendor", BaseURL: "https://provider.example/v1", ModelName: "configured-model", ModelCatalogURL: "https://provider.example/v1/models", TimeoutSeconds: 120, Enabled: true, APIKey: &token, ExpectedVersion: config.Version} config, err = svc.Save(ctx, actor, "model_primary", uuid.NewString(), input) if err != nil { t.Fatal(err) } if config.TimeoutSeconds != 120 { t.Fatal("model deadline was not persisted") } data, _ := json.Marshal(config) if bytes.Contains(data, []byte(token)) || bytes.Contains(data, []byte("encrypted_api_key")) { t.Fatal("API exposes credential") } loaded, err := repo.Get(ctx, "model_primary") if err != nil { t.Fatal(err) } value, err := vault.Decrypt(loaded.EncryptedAPIKey, "model_primary") if err != nil || value != token { t.Fatal("credential was not persisted encrypted") } input.ExpectedVersion = config.Version input.ModelCatalogURL = "https://another.example/models" if _, err := svc.Save(ctx, actor, "model_primary", uuid.NewString(), input); !errors.Is(err, apperror.ErrInvalidInput) { t.Fatal("cross-origin model catalog accepted") } input.ModelCatalogURL = "https://provider.example/v1/models" input.WireAPI = "unknown" if _, err := svc.Save(ctx, actor, "model_primary", uuid.NewString(), input); !errors.Is(err, apperror.ErrInvalidInput) { t.Fatal("unsupported protocol accepted") } input.WireAPI = "responses" input.ExpectedVersion = config.Version input.APIKey = nil config, err = svc.Save(ctx, actor, "model_primary", uuid.NewString(), input) if err != nil || !config.HasAPIKey { t.Fatal("omitted key did not preserve existing credential") } input.ExpectedVersion = config.Version input.BaseURL = "https://different.example/v1" if _, err := svc.Save(ctx, actor, "model_primary", uuid.NewString(), input); !errors.Is(err, apperror.ErrInvalidInput) { t.Fatal("changed endpoint reused existing secret") } input.BaseURL = "https://provider.example/v1?api_key=secret" if _, err := svc.Save(ctx, actor, "model_primary", uuid.NewString(), input); !errors.Is(err, apperror.ErrInvalidInput) { t.Fatal("query credentials accepted") } input.BaseURL = config.BaseURL input.Enabled = false input.ClearAPIKey = true disabled := false input.DisableResponseStorage = &disabled input.RequiresOpenAIAuth = &disabled config, err = svc.Save(ctx, actor, "model_primary", uuid.NewString(), input) if err != nil || config.HasAPIKey || config.DisableResponseStorage || config.RequiresOpenAIAuth { t.Fatal("key clear failed") } input.DisableResponseStorage = nil input.RequiresOpenAIAuth = nil // Two writers of the same version must produce one successor and one conflict. input.ExpectedVersion = config.Version var wg sync.WaitGroup results := make(chan error, 2) for i := 0; i < 2; i++ { wg.Add(1) go func() { defer wg.Done() _, err := svc.Save(ctx, actor, "model_primary", uuid.NewString(), input) results <- err }() } wg.Wait() close(results) succeeded, conflicted := 0, 0 for err := range results { if err == nil { succeeded++ } else if errors.Is(err, apperror.ErrConflict) { conflicted++ } else { t.Fatal(err) } } latest, err := repo.Get(ctx, "model_primary") if err != nil || latest.DisableResponseStorage || latest.RequiresOpenAIAuth { t.Fatal("omitted flags did not preserve configuration") } if succeeded != 1 || conflicted != 1 { t.Fatal("concurrent version protection failed") } var snapshots string var historyCount int if err := pool.QueryRow(ctx, `SELECT count(*),string_agg(snapshot::text,'') FROM veyra_provider_config_history`).Scan(&historyCount, &snapshots); err != nil { t.Fatal(err) } if historyCount != 5 || strings.Contains(snapshots, token) { t.Fatal("history count or credential sanitization failed") } events, err := svc.Audit(ctx, actor) if err != nil || len(events) != 5 { t.Fatalf("atomic audit: %v", err) } encoded, _ := json.Marshal(events) if bytes.Contains(encoded, []byte(token)) { t.Fatal("audit exposed credential") } }