package config_test import ( "strings" "testing" "time" "cmbuyer/admin/internal/config" "golang.org/x/crypto/bcrypt" ) func TestLoad(t *testing.T) { hash, err := bcrypt.GenerateFromPassword([]byte("test-password"), bcrypt.MinCost) if err != nil { t.Fatalf("generate bcrypt hash: %v", err) } values := map[string]string{ "CMBUYER_ADMIN_USERNAME": "admin", "CMBUYER_ADMIN_PASSWORD_BCRYPT": string(hash), "CMBUYER_SESSION_SECRET": strings.Repeat("s", 32), "CMBUYER_COOKIE_SECURE": "true", "CMBUYER_DATABASE_SOURCE": ":memory:", "CMBUYER_AUTHORIZATION_TTL": "10m", "CMBUYER_MAX_TASK_QUANTITY": "99", "CMBUYER_MAX_TOTAL_PRICE": "999.99", "CMBUYER_EVIDENCE_DIR": t.TempDir(), "CMBUYER_CLAIM_TOKEN_SECRET": strings.Repeat("ab", 32), "CMBUYER_CLAIM_LEASE_TTL": "1m", } got, err := config.Load(lookup(values)) if err != nil { t.Fatalf("Load: %v", err) } if got.AdminUsername != "admin" || !got.CookieSecure || len(got.ClaimTokenSecret) != 32 || got.ClaimLeaseTTL != time.Minute { t.Fatalf("Load returned unexpected public configuration: %#v", got) } } func TestLoadRejectsMissingOrInvalidConfiguration(t *testing.T) { hash, err := bcrypt.GenerateFromPassword([]byte("test-password"), bcrypt.MinCost) if err != nil { t.Fatalf("generate bcrypt hash: %v", err) } base := map[string]string{ "CMBUYER_ADMIN_USERNAME": "admin", "CMBUYER_ADMIN_PASSWORD_BCRYPT": string(hash), "CMBUYER_SESSION_SECRET": strings.Repeat("s", 32), "CMBUYER_DATABASE_SOURCE": ":memory:", "CMBUYER_AUTHORIZATION_TTL": "10m", "CMBUYER_MAX_TASK_QUANTITY": "99", "CMBUYER_MAX_TOTAL_PRICE": "999.99", "CMBUYER_EVIDENCE_DIR": t.TempDir(), "CMBUYER_CLAIM_TOKEN_SECRET": strings.Repeat("ab", 32), "CMBUYER_CLAIM_LEASE_TTL": "1m", } tests := []struct { name string mutate func(map[string]string) want string }{ {"missing username", func(values map[string]string) { delete(values, "CMBUYER_ADMIN_USERNAME") }, "CMBUYER_ADMIN_USERNAME"}, {"invalid bcrypt", func(values map[string]string) { values["CMBUYER_ADMIN_PASSWORD_BCRYPT"] = "not-a-bcrypt-hash" }, "CMBUYER_ADMIN_PASSWORD_BCRYPT"}, {"short secret", func(values map[string]string) { values["CMBUYER_SESSION_SECRET"] = "short" }, "CMBUYER_SESSION_SECRET"}, {"invalid secure flag", func(values map[string]string) { values["CMBUYER_COOKIE_SECURE"] = "1" }, "CMBUYER_COOKIE_SECURE"}, {"missing database", func(values map[string]string) { delete(values, "CMBUYER_DATABASE_SOURCE") }, "CMBUYER_DATABASE_SOURCE"}, {"invalid authorization ttl", func(values map[string]string) { values["CMBUYER_AUTHORIZATION_TTL"] = "0s" }, "CMBUYER_AUTHORIZATION_TTL"}, {"invalid maximum quantity", func(values map[string]string) { values["CMBUYER_MAX_TASK_QUANTITY"] = "0" }, "CMBUYER_MAX_TASK_QUANTITY"}, {"invalid maximum total price", func(values map[string]string) { values["CMBUYER_MAX_TOTAL_PRICE"] = "1" }, "CMBUYER_MAX_TOTAL_PRICE"}, {"missing evidence directory", func(values map[string]string) { delete(values, "CMBUYER_EVIDENCE_DIR") }, "CMBUYER_EVIDENCE_DIR"}, {"relative evidence directory", func(values map[string]string) { values["CMBUYER_EVIDENCE_DIR"] = "evidence" }, "CMBUYER_EVIDENCE_DIR"}, {"invalid claim secret", func(values map[string]string) { values["CMBUYER_CLAIM_TOKEN_SECRET"] = strings.Repeat("A", 64) }, "CMBUYER_CLAIM_TOKEN_SECRET"}, {"claim secret same raw session secret", func(values map[string]string) { values["CMBUYER_SESSION_SECRET"] = values["CMBUYER_CLAIM_TOKEN_SECRET"] }, "CMBUYER_CLAIM_TOKEN_SECRET"}, {"claim secret same decoded session secret", func(values map[string]string) { values["CMBUYER_SESSION_SECRET"] = strings.Repeat("\xab", 32) }, "CMBUYER_CLAIM_TOKEN_SECRET"}, {"invalid claim lease ttl", func(values map[string]string) { values["CMBUYER_CLAIM_LEASE_TTL"] = "10m" }, "CMBUYER_CLAIM_LEASE_TTL"}, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { values := copyValues(base) test.mutate(values) _, err := config.Load(lookup(values)) if err == nil || !strings.Contains(err.Error(), test.want) { t.Fatalf("Load error = %v, want mention of %s", err, test.want) } }) } } func lookup(values map[string]string) func(string) (string, bool) { return func(key string) (string, bool) { value, ok := values[key] return value, ok } } func copyValues(values map[string]string) map[string]string { copy := make(map[string]string, len(values)) for key, value := range values { copy[key] = value } return copy }