- Add deployment-name branding to titles, mastheads, and OG tags - Share one grant delivery-state query with lineage across grants surfaces - Show pool status/usage, org owners, and config readiness - Make billing views projection-aware with recency and sync vocabulary - Guard FedWiki creation without domains and render route-aware 404s
250 lines
9.2 KiB
Go
250 lines
9.2 KiB
Go
package config
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/spf13/viper"
|
|
)
|
|
|
|
// validConfig resets the global Viper and sets a complete, well-formed set of the
|
|
// unconditionally-required keys. Individual cases then mutate one key to assert a
|
|
// specific failure.
|
|
func validConfig() {
|
|
viper.Reset()
|
|
viper.Set("db-dsn", "postgres://u:p@localhost:5432/db?sslmode=disable")
|
|
viper.Set("valkey-addr", "localhost:6379")
|
|
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("csrf-secret", "0123456789abcdef0123456789abcdef") // exactly 32 bytes
|
|
viper.Set("deployment-name", DefaultDeploymentName)
|
|
}
|
|
|
|
// stripeSpec mirrors the ConfigKey declarations internal/integrations/stripe
|
|
// registers for its ConfigProvider capability (stripe-api-key and
|
|
// stripe-webhook-secret required together). Tests below pass it as
|
|
// ValidateStart's integrationSpecs argument instead of importing the real
|
|
// adapter tree, keeping this package's tests independent of internal/
|
|
// integrations.
|
|
var stripeSpec = []ConfigKey{
|
|
{Name: "stripe-api-key", Secret: true, RequiredGroup: "Stripe"},
|
|
{Name: "stripe-webhook-secret", Secret: true, RequiredGroup: "Stripe"},
|
|
{Name: "stripe-mode", Default: "test"},
|
|
}
|
|
|
|
func TestValidateStart_Valid(t *testing.T) {
|
|
validConfig()
|
|
if err := ValidateStart(stripeSpec); err != nil {
|
|
t.Fatalf("expected a complete config to pass, got: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestValidateStart_Errors(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
mutate func()
|
|
want string
|
|
}{
|
|
{"missing db-dsn", func() { viper.Set("db-dsn", "") }, "db-dsn is required"},
|
|
{"malformed db-dsn", func() { viper.Set("db-dsn", "mysql://x") }, "db-dsn is not a valid PostgreSQL URL"},
|
|
{"missing valkey-addr", func() { viper.Set("valkey-addr", "") }, "valkey-addr is required"},
|
|
{"missing issuer", func() { viper.Set("oidc-idp-issuer-url", "") }, "oidc-idp-issuer-url is required"},
|
|
{"malformed issuer", func() { viper.Set("oidc-idp-issuer-url", "not-a-url") }, "oidc-idp-issuer-url is not a valid URL"},
|
|
{"missing client id", func() { viper.Set("oidc-sp-client-id", "") }, "oidc-sp-client-id is required"},
|
|
{"missing base-url", func() { viper.Set("base-url", "") }, "base-url is required"},
|
|
{"short csrf", func() { viper.Set("csrf-secret", "tooshort") }, "csrf-secret is invalid"},
|
|
{"blank deployment-name", func() { viper.Set("deployment-name", "") }, "deployment-name cannot be blank"},
|
|
{"whitespace-only deployment-name", func() { viper.Set("deployment-name", " ") }, "deployment-name cannot be blank"},
|
|
{"partial stripe", func() { viper.Set("stripe-api-key", "sk_live_x") }, "Stripe is partially configured"},
|
|
{"partial temporal oauth", func() {
|
|
viper.Set("temporal-host", "localhost:7233")
|
|
viper.Set("temporal-namespace", "default")
|
|
viper.Set("temporal-oauth-client-id", "id-only")
|
|
}, "temporal OAuth is partially configured"},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
validConfig()
|
|
tc.mutate()
|
|
err := ValidateStart(stripeSpec)
|
|
if err == nil {
|
|
t.Fatalf("expected an error containing %q, got nil", tc.want)
|
|
}
|
|
if !strings.Contains(err.Error(), tc.want) {
|
|
t.Fatalf("expected an error containing %q, got: %v", tc.want, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestValidateStart_AggregatesAll asserts that an empty config reports every
|
|
// missing required key in a single aggregated error, not just the first.
|
|
func TestValidateStart_AggregatesAll(t *testing.T) {
|
|
viper.Reset()
|
|
err := ValidateStart(stripeSpec)
|
|
if err == nil {
|
|
t.Fatal("expected errors for an empty config, got nil")
|
|
}
|
|
for _, want := range []string{
|
|
"db-dsn is required",
|
|
"valkey-addr is required",
|
|
"oidc-idp-issuer-url is required",
|
|
"oidc-sp-client-id is required",
|
|
"base-url is required",
|
|
"csrf-secret is invalid",
|
|
"deployment-name cannot be blank",
|
|
} {
|
|
if !strings.Contains(err.Error(), want) {
|
|
t.Errorf("aggregated error missing %q; got: %v", want, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// enumSpec mirrors the four env-sourced enum ConfigKey declarations covered
|
|
// by design D8 (schema-hardening): stripe-mode, fedwiki-site-scheme,
|
|
// discourse-linkage-mode, discourse-auto-create-users (see internal/
|
|
// integrations/stripe/stripe.go, internal/integrations/fedwiki/fedwiki.go,
|
|
// internal/integrations/discourse/discourse.go). Tests below pass it as
|
|
// ValidateStart's integrationSpecs argument instead of importing the real
|
|
// adapter trees, mirroring stripeSpec above.
|
|
var enumSpec = []ConfigKey{
|
|
{Name: "stripe-mode", Default: "test", Enum: []string{"test", "live"}},
|
|
{Name: "fedwiki-site-scheme", Default: "https", Enum: []string{"http", "https"}},
|
|
{Name: "discourse-linkage-mode", Default: "oidc", Enum: []string{"email", "oidc", "discourseconnect"}},
|
|
{Name: "discourse-auto-create-users", Default: "false", Enum: []string{"false", "true"}},
|
|
}
|
|
|
|
// TestValidateStart_EnumKnobs exercises ValidateStart's enum-membership
|
|
// check (design D8) through the four real declared enum knobs: a value
|
|
// outside the declared set fails naming the key, a member value passes, and
|
|
// an unset key passes (presence is a separate concern).
|
|
func TestValidateStart_EnumKnobs(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
key string
|
|
value string // empty means leave the key unset
|
|
valid bool
|
|
}{
|
|
{"stripe-mode: bad value fails", "stripe-mode", "sandbox", false},
|
|
{"stripe-mode: valid value passes", "stripe-mode", "live", true},
|
|
{"stripe-mode: unset passes", "stripe-mode", "", true},
|
|
|
|
{"fedwiki-site-scheme: bad value fails", "fedwiki-site-scheme", "ftp", false},
|
|
{"fedwiki-site-scheme: valid value passes", "fedwiki-site-scheme", "http", true},
|
|
{"fedwiki-site-scheme: unset passes", "fedwiki-site-scheme", "", true},
|
|
|
|
{"discourse-linkage-mode: bad value fails", "discourse-linkage-mode", "sso", false},
|
|
{"discourse-linkage-mode: valid value passes", "discourse-linkage-mode", "discourseconnect", true},
|
|
{"discourse-linkage-mode: unset passes", "discourse-linkage-mode", "", true},
|
|
|
|
{"discourse-auto-create-users: bad value fails", "discourse-auto-create-users", "yes", false},
|
|
{"discourse-auto-create-users: valid value passes", "discourse-auto-create-users", "true", true},
|
|
{"discourse-auto-create-users: unset passes", "discourse-auto-create-users", "", true},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
validConfig()
|
|
if tc.value != "" {
|
|
viper.Set(tc.key, tc.value)
|
|
}
|
|
err := ValidateStart(enumSpec)
|
|
if tc.valid {
|
|
if err != nil {
|
|
t.Fatalf("expected a valid config to pass, got: %v", err)
|
|
}
|
|
return
|
|
}
|
|
if err == nil {
|
|
t.Fatalf("expected an error naming %q, got nil", tc.key)
|
|
}
|
|
if !strings.Contains(err.Error(), tc.key) {
|
|
t.Fatalf("expected error naming %q, got: %v", tc.key, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestValidateEnums exercises the generic enum-membership mechanism
|
|
// directly, independent of any real integration's declared keys — mirrors
|
|
// TestValidateRequiredGroups below for the sibling check.
|
|
func TestValidateEnums(t *testing.T) {
|
|
specs := []ConfigKey{
|
|
{Name: "widget-mode", Enum: []string{"a", "b"}},
|
|
{Name: "solo-key"}, // no Enum: never flagged
|
|
}
|
|
|
|
t.Run("unset passes", func(t *testing.T) {
|
|
viper.Reset()
|
|
if errs := validateEnums(specs); len(errs) != 0 {
|
|
t.Fatalf("expected no errors, got: %v", errs)
|
|
}
|
|
})
|
|
|
|
t.Run("member value passes", func(t *testing.T) {
|
|
viper.Reset()
|
|
viper.Set("widget-mode", "b")
|
|
if errs := validateEnums(specs); len(errs) != 0 {
|
|
t.Fatalf("expected no errors, got: %v", errs)
|
|
}
|
|
})
|
|
|
|
t.Run("non-member value is rejected naming key, value, and allowed set", func(t *testing.T) {
|
|
viper.Reset()
|
|
viper.Set("widget-mode", "c")
|
|
errs := validateEnums(specs)
|
|
if len(errs) != 1 {
|
|
t.Fatalf("expected exactly one error, got: %v", errs)
|
|
}
|
|
got := errs[0].Error()
|
|
for _, want := range []string{"widget-mode", `"c"`, "a|b"} {
|
|
if !strings.Contains(got, want) {
|
|
t.Errorf("error %q missing %q", got, want)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
// TestValidateRequiredGroups exercises the generic required-together
|
|
// mechanism directly, independent of any real integration's declared keys —
|
|
// this is the behavior that replaced validate.go's hardcoded "Conditional:
|
|
// Stripe" block.
|
|
func TestValidateRequiredGroups(t *testing.T) {
|
|
specs := []ConfigKey{
|
|
{Name: "widget-key", RequiredGroup: "Widget"},
|
|
{Name: "widget-secret", RequiredGroup: "Widget"},
|
|
{Name: "solo-key"}, // no group: never flagged
|
|
}
|
|
|
|
t.Run("neither set is fine", func(t *testing.T) {
|
|
viper.Reset()
|
|
if errs := validateRequiredGroups(specs); len(errs) != 0 {
|
|
t.Fatalf("expected no errors, got: %v", errs)
|
|
}
|
|
})
|
|
|
|
t.Run("both set is fine", func(t *testing.T) {
|
|
viper.Reset()
|
|
viper.Set("widget-key", "k")
|
|
viper.Set("widget-secret", "s")
|
|
if errs := validateRequiredGroups(specs); len(errs) != 0 {
|
|
t.Fatalf("expected no errors, got: %v", errs)
|
|
}
|
|
})
|
|
|
|
t.Run("partial is rejected naming both keys", func(t *testing.T) {
|
|
viper.Reset()
|
|
viper.Set("widget-key", "k")
|
|
errs := validateRequiredGroups(specs)
|
|
if len(errs) != 1 {
|
|
t.Fatalf("expected exactly one error, got: %v", errs)
|
|
}
|
|
got := errs[0].Error()
|
|
for _, want := range []string{"Widget is partially configured", "widget-key", "widget-secret"} {
|
|
if !strings.Contains(got, want) {
|
|
t.Errorf("error %q missing %q", got, want)
|
|
}
|
|
}
|
|
})
|
|
}
|