package configkit_test import ( "bytes" "encoding/json" "errors" "fmt" "io" "os" "reflect" "strings" "testing" configkit "gitea.maximumdirect.net/eric/ConfigKit" ) var ( _ error = (*configkit.LoadError)(nil) _ fmt.Formatter = configkit.Secret{} _ fmt.Stringer = configkit.Path{} _ json.Marshaler = configkit.Secret{} ) func TestFacadeValuesRemainRootOwned(t *testing.T) { t.Parallel() const packagePath = "gitea.maximumdirect.net/eric/ConfigKit" for _, candidate := range []reflect.Type{ reflect.TypeFor[configkit.Path](), reflect.TypeFor[configkit.Source](), reflect.TypeFor[configkit.Secret](), reflect.TypeFor[configkit.Diagnostic](), reflect.TypeFor[configkit.LoadError](), reflect.TypeFor[configkit.Metadata](), reflect.TypeFor[configkit.Resolution](), reflect.TypeFor[configkit.Validation](), reflect.TypeFor[configkit.Version](), reflect.TypeFor[configkit.VersionRule](), reflect.TypeFor[configkit.FilesystemContext](), reflect.TypeFor[configkit.Override](), reflect.TypeFor[configkit.Definition[struct{}]](), reflect.TypeFor[configkit.Options[struct{}]](), reflect.TypeFor[configkit.Result[struct{}]](), reflect.TypeFor[configkit.ValueSnapshot](), reflect.TypeFor[configkit.ValueChange](), reflect.TypeFor[configkit.SecretReferenceInfo](), reflect.TypeFor[configkit.Explanation](), reflect.TypeFor[configkit.SecretFileResult](), reflect.TypeFor[configkit.SecretsReport](), reflect.TypeFor[configkit.SecretsError](), } { if candidate.PkgPath() != packagePath { t.Fatalf("type %s belongs to %q", candidate, candidate.PkgPath()) } } secret := configkit.SecretEnv("SAFE_NAME") formatted := fmt.Sprintf("%#v", secret) encoded, err := json.Marshal(secret) if formatted == "" || err != nil || string(encoded) != `"SAFE_NAME"` { t.Fatalf("secret rendering = %q, %s, %v", formatted, encoded, err) } if got := configkit.Keys("a/b~").Index(0).String(); got != "/a~1b~0/0" { t.Fatalf("path = %q", got) } if got := (&configkit.LoadError{}).Error(); got != "configkit: configuration load failed" { t.Fatalf("zero LoadError = %q", got) } } func TestFacadeInspectionUsesFrozenState(t *testing.T) { t.Setenv("FACADE_INSPECTION_SECRET", "credential") type configuration struct { Value string `yaml:"value"` Secret configkit.Secret `yaml:"secret"` } result, err := configkit.Load[configuration](configkit.Bytes("inspection.yml", []byte("value: original\nsecret: FACADE_INSPECTION_SECRET\n"))) if err != nil { t.Fatal(err) } if !result.Config.Secret.IsResolved() || result.Config.Secret.Value() != "credential" { t.Fatalf("secret = %#v", result.Config.Secret) } explanation, ok := result.Explain(configkit.Keys("value")) if !ok || explanation.Effective.YAML != "original\n" { t.Fatalf("explanation = %#v, %v", explanation, ok) } explanation.Effective.YAML = "mutated\n" result.Config.Value = "mutated" var effective bytes.Buffer if err := result.WriteEffective(&effective); err != nil || !strings.Contains(effective.String(), "value: original\n") || strings.Contains(effective.String(), "credential") { t.Fatalf("effective = %q, %v", effective.String(), err) } again, ok := result.Explain(configkit.Keys("value")) if !ok || again.Effective.YAML != "original\n" { t.Fatalf("inspection changed = %#v, %v", again, ok) } secret, ok := result.Explain(configkit.Keys("secret")) if !ok || secret.SecretReference == nil || secret.SecretReference.Name != "FACADE_INSPECTION_SECRET" { t.Fatalf("secret explanation = %#v, %v", secret, ok) } var explanationOutput bytes.Buffer if err := result.WriteExplanation(&explanationOutput, configkit.Keys("value")); err != nil || !strings.Contains(explanationOutput.String(), `path: "/value"`) { t.Fatalf("explanation output = %q, %v", explanationOutput.String(), err) } unknown := bytes.NewBufferString("unchanged") if err := result.WriteExplanation(unknown, configkit.Keys("unknown")); err == nil || unknown.String() != "unchanged" { t.Fatalf("unknown explanation = %q, %v", unknown.String(), err) } var nilResult *configkit.Result[configuration] if _, ok := nilResult.Explain(configkit.Keys("value")); ok { t.Fatal("nil result explained a path") } if err := nilResult.WriteEffective(io.Discard); err == nil { t.Fatal("nil result rendered effective configuration") } } func TestFacadeSecretsBootstrapUsesFacadeRecords(t *testing.T) { const ( loadedName = "FACADE_BOOTSTRAP_LOADED" existingName = "FACADE_BOOTSTRAP_EXISTING" ) restore := func(name string) { value, present := os.LookupEnv(name) t.Cleanup(func() { if present { _ = os.Setenv(name, value) } else { _ = os.Unsetenv(name) } }) } restore(loadedName) restore(existingName) if err := os.Unsetenv(loadedName); err != nil { t.Fatal(err) } if err := os.Setenv(existingName, ""); err != nil { t.Fatal(err) } directory := t.TempDir() if err := os.WriteFile(directory+"/"+loadedName, []byte("bootstrap\n"), 0o600); err != nil { t.Fatal(err) } if err := os.WriteFile(directory+"/"+existingName, []byte("ignored\n"), 0o600); err != nil { t.Fatal(err) } report, err := configkit.LoadSecretsDir(directory) if err != nil { t.Fatal(err) } entries := report.Entries() if len(entries) != 2 || os.Getenv(loadedName) != "bootstrap" { t.Fatalf("entries/environment = %#v / %q", entries, os.Getenv(loadedName)) } status := map[string]configkit.SecretFileResult{} for _, entry := range entries { status[entry.Name] = entry } if status[loadedName].Status != configkit.SecretFileLoaded || status[existingName].Status != configkit.SecretFileSkippedExisting || !status[existingName].ExistingEmpty { t.Fatalf("statuses = %#v", status) } entries[0].Name = "mutated" if report.Entries()[0].Name == "mutated" { t.Fatal("report entries share mutable state") } var rendered bytes.Buffer if _, err := report.WriteTo(&rendered); err != nil || strings.Contains(rendered.String(), "bootstrap") || strings.Contains(rendered.String(), "ignored") { t.Fatalf("report rendering = %q, %v", rendered.String(), err) } failedReport, err := configkit.LoadSecretsDir("") var secretsError *configkit.SecretsError if failedReport == nil || !errors.As(err, &secretsError) || len(secretsError.Diagnostics()) != 1 || secretsError.Diagnostics()[0].Code != configkit.CodeSecretsInvalidPath { t.Fatalf("bootstrap failure = %#v / %#v", failedReport, err) } } func TestFacadeLoadBridgesLifecycleCallbacks(t *testing.T) { t.Setenv("FACADE_SECRET_NAME", "credential") type configuration struct { Version string `yaml:"version"` Input int `yaml:"input"` Derived int `yaml:"derived"` Secret configkit.Secret `yaml:"secret"` Labels map[string]string `yaml:"labels"` } var retainedResolution *configkit.Resolution var retainedValidation *configkit.Validation versionCalls, defaultsCalls, resolveCalls, validateCalls := 0, 0, 0, 0 definition := configkit.Definition[configuration]{ Version: &configkit.VersionRule{Path: configkit.Keys("version"), Check: func(version configkit.Version, validation *configkit.Validation) error { versionCalls++ retainedValidation = validation if !version.Present || version.Text != "1" { validation.Error(configkit.Keys("version"), "unsupported version") } return nil }}, Defaults: func() configuration { defaultsCalls++ return configuration{Derived: 1, Labels: map[string]string{"default": "kept"}} }, Resolve: func(configuration *configuration, resolution *configkit.Resolution) error { resolveCalls++ retainedResolution = resolution if !resolution.IsExplicit(configkit.Keys("input")) { resolution.Error(configkit.Keys("input"), "input is required") } input, ok := resolution.InputInfo(configkit.Keys("input")) if !ok || !input.Explicit { resolution.Error(configkit.Keys("input"), "missing input provenance") } source, ok := resolution.Source("source-1") if !ok || source.Label != "facade.yml" { resolution.Error(configkit.Keys("input"), "missing source") } configkit.Default(resolution, configkit.Keys("derived"), &configuration.Derived, configuration.Input*2, "derived from input", configkit.Keys("input")) return nil }, Validate: func(configuration *configuration, validation *configkit.Validation) { validateCalls++ retainedValidation = validation if !configuration.Secret.IsResolved() || configuration.Secret.Value() != "credential" { validation.Error(configkit.Keys("secret"), "secret did not resolve") } }, } if versionCalls != 0 || defaultsCalls != 0 || resolveCalls != 0 || validateCalls != 0 { t.Fatal("callbacks ran while constructing options") } result, err := configkit.Load(configkit.Bytes("facade.yml", []byte("version: 1\ninput: 3\nsecret: FACADE_SECRET_NAME\n")), configkit.Options[configuration]{Definition: definition}) if err != nil { t.Fatal(err) } if versionCalls != 1 || defaultsCalls != 1 || resolveCalls != 1 || validateCalls != 1 || result.Config.Derived != 6 { t.Fatalf("callback counts/configuration = %d/%d/%d/%d %#v", versionCalls, defaultsCalls, resolveCalls, validateCalls, result.Config) } derived, ok := result.Metadata().Lookup(configkit.Keys("derived")) if !ok || derived.Explicit || derived.Origin.Operation != configkit.ChangeDerivedDefault { t.Fatalf("derived metadata = %#v, %v", derived, ok) } if sources := result.Sources(); len(sources) != 1 || sources[0].Label != "facade.yml" { t.Fatalf("sources = %#v", sources) } if result.Diagnostics() != nil { t.Fatalf("diagnostics = %#v", result.Diagnostics()) } for _, context := range []func(){ func() { retainedResolution.IsExplicit(configkit.Keys("input")) }, func() { retainedValidation.Warning(configkit.Keys("input"), "too late") }, } { func() { defer func() { if recover() == nil { t.Fatal("retained callback context remained active") } }() context() }() } } func TestFacadeLoadReturnsFacadeLoadError(t *testing.T) { t.Parallel() type configuration struct { Value int `yaml:"value"` } result, err := configkit.Load[configuration](configkit.Bytes("invalid.yml", []byte("value: wrong\n"))) if result != nil || err == nil { t.Fatalf("result, err = %#v, %v", result, err) } var loadError *configkit.LoadError if !errors.As(err, &loadError) || len(loadError.Diagnostics()) != 1 || loadError.Diagnostics()[0].Code != configkit.CodeDecodeType { t.Fatalf("facade error = %#v", err) } }