// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Commercial // SPDX-FileCopyrightText: 2025-2026 Christian Galo package cmd import ( "context" "database/sql" "os" "strings" "testing" "time" _ "github.com/jackc/pgx/v5/stdlib" "github.com/spf13/viper" "git.coopcloud.tech/wiki-cafe/member-console/internal/config" "git.coopcloud.tech/wiki-cafe/member-console/internal/db" "git.coopcloud.tech/wiki-cafe/member-console/internal/integration" ) // vcSpec is a brand-neutral fixture spec for validate-config's DB-backed // tests: one duration key, the shape the maintainer's bogus // fedwiki-sync-interval save exposed (proposal.md, typed-config-keys). var vcSpec = []config.ConfigKey{ {Name: "widget-sync-interval", Default: time.Hour, Usage: "widget sync interval"}, } // validVCConfig resets the global Viper and sets a complete, well-formed // core configuration pointed at dsn, mirroring internal/config's own // validConfig() test helper (this package cannot import that unexported // helper, so it is restated here against the same key set ValidateStart // checks unconditionally). func validVCConfig(dsn string) { viper.Reset() viper.Set("db-dsn", dsn) viper.Set("valkey-addr", "localhost:6379") viper.Set("valkey-password", "test-store-password") viper.Set("oidc-idp-issuer-url", "https://idp.example.com/realms/main") viper.Set("oidc-sp-client-id", "member-console") viper.Set("base-url", "https://console.example.com") viper.Set("deployment-name", config.DefaultDeploymentName) } // TestRunValidateConfig_DB covers validate-config's two failure paths (D10, // task 2.5) and its clean-exit path against a real (scratch) database: a // bad stored override row, a bad environment value for the same key, and a // clean configuration. func TestRunValidateConfig_DB(t *testing.T) { dsn := os.Getenv("TEST_DATABASE_URL") if dsn == "" { t.Skip("TEST_DATABASE_URL not set, skipping integration test") } database, err := sql.Open("pgx", dsn) if err != nil { t.Fatalf("open database: %v", err) } t.Cleanup(func() { database.Close() }) if err := db.RunMigrations(database, migrationSources()); err != nil { t.Fatalf("run migrations: %v", err) } q := integration.New(database) clean := func() { if _, err := q.DeleteConfigOverride(context.Background(), "widget-sync-interval"); err != nil { t.Fatalf("clear override: %v", err) } } clean() t.Cleanup(clean) t.Run("a bad stored row fails naming the key, the parser's sentence, and the remediation", func(t *testing.T) { validVCConfig(dsn) if err := q.UpsertConfigOverride(context.Background(), integration.UpsertConfigOverrideParams{ Key: "widget-sync-interval", Value: "hello", }); err != nil { t.Fatalf("seed bad override: %v", err) } t.Cleanup(clean) ok, message := runValidateConfig(context.Background(), vcSpec) if ok { t.Fatalf("want failure, got ok: %s", message) } for _, want := range []string{ "widget-sync-interval", `is not a duration; for example 30m or 1h30m`, "clear it: member-console config clear widget-sync-interval", } { if !strings.Contains(message, want) { t.Errorf("message %q missing %q", message, want) } } }) t.Run("a bad environment value fails with the parser's sentence", func(t *testing.T) { validVCConfig(dsn) viper.Set("widget-sync-interval", "hello") ok, message := runValidateConfig(context.Background(), vcSpec) if ok { t.Fatalf("want failure, got ok: %s", message) } if !strings.Contains(message, `is not a duration; for example 30m or 1h30m`) { t.Errorf("message %q missing the parser's sentence", message) } }) t.Run("a clean configuration exits ok", func(t *testing.T) { validVCConfig(dsn) ok, message := runValidateConfig(context.Background(), vcSpec) if !ok { t.Fatalf("want ok, got failure: %s", message) } if message != "config validate: ok" { t.Errorf("message = %q", message) } }) }