package configkit import ( "encoding/json" "errors" "fmt" "os" "reflect" "strings" "testing" ) type secretReferenceConfig struct { Required Secret `yaml:"required"` Optional *Secret `yaml:"optional"` Map map[string]Secret `yaml:"map"` Pointers map[string]*Secret `yaml:"pointers"` Sequence []Secret `yaml:"sequence"` } func TestSecretReferencesResolveTypedShapesOncePerName(t *testing.T) { input := `required: SHARED_SECRET optional: OPTIONAL_SECRET map: first: SHARED_SECRET pointers: nil: null value: POINTER_SECRET sequence: [SHARED_SECRET, SEQUENCE_SECRET] ` values := map[string]string{ "SHARED_SECRET": "shared-value", "OPTIONAL_SECRET": "optional-value", "POINTER_SECRET": "pointer-value", "SEQUENCE_SECRET": "sequence-value", } counts := map[string]int{} lookup := func(name string) (string, bool) { counts[name]++ value, present := values[name] return value, present } validationCalled := false result, err := loadWithSecretLookup(Bytes("secrets.yml", []byte(input)), lookup, Options[secretReferenceConfig]{ Definition: Definition[secretReferenceConfig]{Validate: func(configuration *secretReferenceConfig, validation *Validation) { validationCalled = true if configuration.Required.Value() != "shared-value" || !configuration.Required.IsResolved() || configuration.Optional.Value() != "optional-value" || configuration.Pointers["value"].Value() != "pointer-value" || configuration.Sequence[1].Value() != "sequence-value" { validation.Error(Keys("required"), "references were not resolved before validation") } }}, }) if err != nil { t.Fatalf("loadWithSecretLookup() error = %v", err) } if !validationCalled || result.Config.Optional == nil || result.Config.Pointers["nil"] != nil { t.Fatalf("resolved config = %#v", result.Config) } if result.Config.Required.EnvName() != "SHARED_SECRET" || result.Config.Map["first"].Value() != "shared-value" { t.Fatalf("reference names/values = %#v", result.Config) } if !reflect.DeepEqual(counts, map[string]int{"SHARED_SECRET": 1, "OPTIONAL_SECRET": 1, "POINTER_SECRET": 1, "SEQUENCE_SECRET": 1}) { t.Fatalf("lookup counts = %#v", counts) } } func TestSecretReferenceErrorsAreSafeAndPathSpecific(t *testing.T) { for _, test := range []struct { name string input string lookup secretLookup code Code }{ {name: "absent", input: "{}\n", code: CodeSecretRequired}, {name: "null", input: "required: null\n", code: CodeSecretRequired}, {name: "empty name", input: "required: ''\n", code: CodeSecretInvalidName}, {name: "invalid name", input: "required: INVALID-NAME\n", code: CodeSecretInvalidName}, {name: "unset", input: "required: MISSING_SECRET\n", code: CodeSecretUnset}, {name: "empty environment", input: "required: EMPTY_SECRET\n", lookup: func(string) (string, bool) { return "", true }, code: CodeSecretEmpty}, } { t.Run(test.name, func(t *testing.T) { lookup := test.lookup if lookup == nil { lookup = func(string) (string, bool) { return "", false } } _, err := loadWithSecretLookup(Bytes("secret-errors.yml", []byte(test.input)), lookup, Options[secretReferenceConfig]{}) loadError := requireSecretLoadCode(t, err, test.code) diagnostic := loadError.Diagnostics()[0] if diagnostic.Path.String() != "/required" || strings.Contains(fmt.Sprintf("%v %#v", err, err), "CREDENTIAL-CANARY") { t.Fatalf("diagnostic/error = %#v / %v", diagnostic, err) } }) } t.Run("non-nil optional is required to resolve", func(t *testing.T) { type config struct { Optional *Secret `yaml:"optional"` } _, err := loadWithSecretLookup(Bytes("optional.yml", []byte("optional: ''\n")), func(string) (string, bool) { return "", false }, Options[config]{}) requireSecretLoadCode(t, err, CodeSecretInvalidName) result, err := loadWithSecretLookup(Bytes("optional.yml", []byte("optional: null\n")), func(string) (string, bool) { return "", false }, Options[config]{}) if err != nil || result.Config.Optional != nil { t.Fatalf("nil optional result = %#v, %v", result, err) } }) t.Run("failed lookup is cached", func(t *testing.T) { type config struct { Secrets []Secret `yaml:"secrets"` } lookups := 0 _, err := loadWithSecretLookup(Bytes("missing-twice.yml", []byte("secrets: [MISSING_SHARED, MISSING_SHARED]\n")), func(string) (string, bool) { lookups++ return "", false }, Options[config]{}) loadError := requireSecretLoadCode(t, err, CodeSecretUnset) if lookups != 1 || len(loadError.Diagnostics()) != 2 { t.Fatalf("lookups = %d, diagnostics = %#v", lookups, loadError.Diagnostics()) } }) } func TestResolutionSelectsEffectiveSecretNameBeforeLookup(t *testing.T) { type config struct { Name string `yaml:"name"` Secret Secret `yaml:"secret"` } lookedUp := []string{} validated := "" result, err := loadWithSecretLookup(Bytes("resolved-secret.yml", []byte("name: SELECTED_SECRET\nsecret: OLD_SECRET\n")), func(name string) (string, bool) { lookedUp = append(lookedUp, name) return "resolved-value", true }, Options[config]{Definition: Definition[config]{ Resolve: func(configuration *config, resolution *Resolution) error { Normalize(resolution, Keys("secret"), &configuration.Secret, SecretEnv(configuration.Name), "selected named credential", Keys("name")) return nil }, Validate: func(configuration *config, validation *Validation) { validated = configuration.Secret.Value() }, }}) if err != nil { t.Fatalf("loadWithSecretLookup() error = %v", err) } if !reflect.DeepEqual(lookedUp, []string{"SELECTED_SECRET"}) || validated != "resolved-value" || result.Config.Secret.EnvName() != "SELECTED_SECRET" { t.Fatalf("lookups = %#v, validated = %q, config = %#v", lookedUp, validated, result.Config) } history := result.Metadata().History(Keys("secret")) if len(history) != 3 || history[len(history)-1].Origin.Operation != ChangeNormalization { t.Fatalf("secret history = %#v", history) } } func TestValidationCannotReplaceResolvedSecretWithSameName(t *testing.T) { type config struct { Secret Secret `yaml:"secret"` } _, err := loadWithSecretLookup(Bytes("validator.yml", []byte("secret: VALIDATOR_SECRET\n")), func(string) (string, bool) { return "CREDENTIAL-CANARY", true }, Options[config]{Definition: Definition[config]{Validate: func(configuration *config, validation *Validation) { configuration.Secret = SecretEnv(configuration.Secret.EnvName()) }}}) loadError := requireSecretLoadCode(t, err, CodeValidateMutation) if loadError.Diagnostics()[0].Path.String() != "/secret" || strings.Contains(fmt.Sprintf("%v %#v", err, err), "CREDENTIAL-CANARY") { t.Fatalf("validator mutation error = %v %#v", err, err) } } func TestResolvedDefaultsAreRebuiltAndLaterLoadsRelookup(t *testing.T) { type config struct { Secret Secret `yaml:"secret"` } first, err := loadWithSecretLookup(NoSource(), func(string) (string, bool) { return "first-value", true }, Options[config]{ Definition: Definition[config]{Defaults: func() config { return config{Secret: SecretEnv("DEFAULT_SECRET")} }}, }) if err != nil { t.Fatal(err) } second, err := loadWithSecretLookup(NoSource(), func(string) (string, bool) { return "second-value", true }, Options[config]{ Definition: Definition[config]{Defaults: func() config { return first.Config }}, }) if err != nil { t.Fatal(err) } if first.Config.Secret.Value() != "first-value" || second.Config.Secret.Value() != "second-value" { t.Fatalf("resolved defaults retained stale state: %q, %q", first.Config.Secret.Value(), second.Config.Secret.Value()) } t.Setenv("CONFIGKIT_RELOOKUP_SECRET", "one") options := Options[config]{Definition: Definition[config]{Defaults: func() config { return config{Secret: SecretEnv("CONFIGKIT_RELOOKUP_SECRET")} }}} realFirst, err := Load(NoSource(), options) if err != nil { t.Fatal(err) } if err := os.Setenv("CONFIGKIT_RELOOKUP_SECRET", "two"); err != nil { t.Fatal(err) } realSecond, err := Load(NoSource(), options) if err != nil { t.Fatal(err) } if realFirst.Config.Secret.Value() != "one" || realSecond.Config.Secret.Value() != "two" { t.Fatalf("later loads = %q, %q", realFirst.Config.Secret.Value(), realSecond.Config.Secret.Value()) } } func TestSecretSafeFormattingJSONProjectionAndHistory(t *testing.T) { const credential = "CREDENTIAL-CANARY" secret := Secret{name: "SAFE_SECRET_NAME", value: credential, specified: true, resolved: true} for _, format := range []string{"%v", "%+v", "%#v", "%s", "%q", "%x", "%d"} { formatted := fmt.Sprintf(format, secret) if strings.Contains(formatted, credential) || !strings.Contains(formatted, "SAFE_SECRET_NAME") { t.Fatalf("format %q produced %q", format, formatted) } } encoded, err := json.Marshal(secret) if err != nil || string(encoded) != `"SAFE_SECRET_NAME"` || strings.Contains(string(encoded), credential) { t.Fatalf("MarshalJSON() = %s, %v", encoded, err) } type config struct { Secret Secret `yaml:"secret"` } result, err := loadWithSecretLookup(Bytes("safe-secret.yml", []byte("secret: SAFE_SECRET_NAME\n")), func(string) (string, bool) { return credential, true }, Options[config]{}) if err != nil { t.Fatal(err) } encodedConfig, err := json.Marshal(result.Config) if err != nil || strings.Contains(string(encodedConfig), credential) || !strings.Contains(string(encodedConfig), "SAFE_SECRET_NAME") { t.Fatalf("configuration JSON = %s, %v", encodedConfig, err) } var effective strings.Builder if err := result.WriteEffective(&effective); err != nil { t.Fatal(err) } outputs := []string{effective.String(), fmt.Sprintf("%#v", result.Metadata()), fmt.Sprintf("%#v", result.Metadata().History(Keys("secret"))), fmt.Sprintf("%#v", result.Config)} for _, output := range outputs { if strings.Contains(output, credential) { t.Fatalf("inspection leaked credential: %q", output) } } if !strings.Contains(effective.String(), "SAFE_SECRET_NAME") { t.Fatalf("effective reference name missing: %q", effective.String()) } } func TestBootstrapExistingEmptyWinsAndReferenceFails(t *testing.T) { const name = "CONFIGKIT_BLOCKED_EMPTY_SECRET" t.Setenv(name, "") directory := t.TempDir() writeSecretTestFile(t, directory, name, "directory-value") report, err := LoadSecretsDir(directory) if err != nil { t.Fatal(err) } entries := report.Entries() if len(entries) != 1 || entries[0].Status != SecretFileSkippedExisting || !entries[0].ExistingEmpty { t.Fatalf("bootstrap report = %#v", entries) } type config struct { Secret Secret `yaml:"secret"` } _, err = Load[config](Bytes("blocked.yml", []byte("secret: "+name+"\n"))) requireSecretLoadCode(t, err, CodeSecretEmpty) if value, present := os.LookupEnv(name); !present || value != "" { t.Fatalf("environment changed to %q, %v", value, present) } } func TestDynamicValuesDoNotEstablishSecretReferences(t *testing.T) { type dynamic struct { Secret Secret `yaml:"secret"` } type config struct { Dynamic any `yaml:"dynamic"` } schema := mustSchemaFor[config](t) for _, value := range []any{SecretEnv("DYNAMIC_SECRET"), dynamic{Secret: SecretEnv("DYNAMIC_SECRET")}} { _, issue := projectConfiguration(schema, reflect.ValueOf(config{Dynamic: value}), defaultProjectionLimits()) if issue == nil || issue.code != CodeSchemaDynamicType { t.Fatalf("dynamic Secret projection issue = %#v", issue) } } lookups := 0 result, err := loadWithSecretLookup(Bytes("dynamic.yml", []byte("dynamic: ORDINARY_STRING\n")), func(string) (string, bool) { lookups++ return "unexpected", true }, Options[config]{}) if err != nil || result.Config.Dynamic != "ORDINARY_STRING" || lookups != 0 { t.Fatalf("dynamic string result = %#v, lookups = %d, error = %v", result, lookups, err) } } func TestLoadDoesNotMutateSecretEnvironment(t *testing.T) { const name = "CONFIGKIT_UNCHANGED_SECRET" t.Setenv(name, "original") type config struct { Secret Secret `yaml:"secret"` } result, err := Load[config](Bytes("unchanged.yml", []byte("secret: "+name+"\n"))) if err != nil || result.Config.Secret.Value() != "original" { t.Fatalf("Load() = %#v, %v", result, err) } if got := os.Getenv(name); got != "original" { t.Fatalf("environment = %q", got) } } func requireSecretLoadCode(t *testing.T, err error, code Code) *LoadError { t.Helper() var loadError *LoadError if !errors.As(err, &loadError) { t.Fatalf("error = %T %v, want *LoadError", err, err) } diagnostics := loadError.Diagnostics() if len(diagnostics) == 0 || diagnostics[0].Code != code { t.Fatalf("diagnostics = %#v, want first code %q", diagnostics, code) } return loadError }