276 lines
10 KiB
Go
276 lines
10 KiB
Go
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.Filesystem](), reflect.TypeFor[configkit.ResourceContext](),
|
|
reflect.TypeFor[configkit.Input](), reflect.TypeFor[configkit.Underlay](),
|
|
reflect.TypeFor[configkit.LayeredOptions[struct{}]](),
|
|
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)
|
|
}
|
|
}
|