250 lines
10 KiB
Go
250 lines
10 KiB
Go
package configkit_test
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
"testing"
|
|
|
|
"gitea.maximumdirect.net/eric/ConfigKit"
|
|
)
|
|
|
|
func TestFinalSequenceIdentityTreatsLateExtendsFieldsAsPayload(t *testing.T) {
|
|
for _, phase := range []string{"defaults", "resolution"} {
|
|
for _, component := range []any{"application-data", nil} {
|
|
for _, duplicate := range []bool{false, true} {
|
|
t.Run(fmt.Sprintf("%s/%v/duplicate=%t", phase, component, duplicate), func(t *testing.T) {
|
|
items := []any{map[string]any{"extends": component}}
|
|
if duplicate {
|
|
items = append(items, map[string]any{"extends": component})
|
|
}
|
|
definition := configkit.Definition[map[string]any]{
|
|
SequenceIdentities: []configkit.SequenceIdentity{configkit.SequenceIdentityAt(configkit.Keys("items"), "extends")},
|
|
Defaults: func() map[string]any {
|
|
if phase == "defaults" {
|
|
return map[string]any{"items": items}
|
|
}
|
|
return map[string]any{}
|
|
},
|
|
}
|
|
if phase == "resolution" {
|
|
definition.Resolve = func(value *map[string]any, resolution *configkit.Resolution) error {
|
|
(*value)["items"] = items
|
|
resolution.Record(configkit.Keys("items"), configkit.ChangeNormalization, "supply application payload")
|
|
return nil
|
|
}
|
|
}
|
|
loaded, err := configkit.Load(configkit.NoSource(), configkit.Options[map[string]any]{Definition: definition})
|
|
if duplicate {
|
|
loadErr := requireFacadeLoadError(t, err)
|
|
diagnostics := loadErr.Diagnostics()
|
|
if loaded != nil || len(diagnostics) != 1 || diagnostics[0].Code != configkit.CodeSequenceConflict || diagnostics[0].Path.String() != "/items" {
|
|
t.Fatalf("duplicate effective payload diagnostics = %#v", diagnostics)
|
|
}
|
|
return
|
|
}
|
|
if err != nil || loaded == nil {
|
|
t.Fatalf("ordinary effective identity was interpreted as a directive: %v", err)
|
|
}
|
|
value := loaded.Config["items"].([]any)[0].(map[string]any)
|
|
if actual, present := value["extends"]; !present || actual != component {
|
|
t.Fatalf("effective payload = %#v", value)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestFinalSequenceIdentityValidatesLateValues(t *testing.T) {
|
|
identity := configkit.SequenceIdentityAt(configkit.Keys("items"), "id")
|
|
for _, source := range []string{"defaults", "CLI", "resolution"} {
|
|
for _, shape := range []string{"duplicate", "missing", "non-scalar"} {
|
|
t.Run(source+"/"+shape, func(t *testing.T) {
|
|
items := finalIdentityRecords(shape)
|
|
definition := configkit.Definition[map[string]any]{
|
|
SequenceIdentities: []configkit.SequenceIdentity{identity},
|
|
Defaults: func() map[string]any {
|
|
if source == "defaults" {
|
|
return map[string]any{"items": items}
|
|
}
|
|
return map[string]any{}
|
|
},
|
|
}
|
|
options := configkit.Options[map[string]any]{Definition: definition}
|
|
switch source {
|
|
case "CLI":
|
|
options.Overrides = []configkit.Override{{Path: configkit.Keys("items"), Value: items, Flag: "--items"}}
|
|
case "resolution":
|
|
options.Definition.Resolve = func(value *map[string]any, resolution *configkit.Resolution) error {
|
|
(*value)["items"] = items
|
|
resolution.Record(configkit.Keys("items"), configkit.ChangeNormalization, "create effective records")
|
|
return nil
|
|
}
|
|
}
|
|
result, err := configkit.Load(configkit.NoSource(), options)
|
|
if result != nil {
|
|
t.Fatalf("invalid final identities returned a result: %#v", result.Config)
|
|
}
|
|
loadErr := requireFacadeLoadError(t, err)
|
|
if len(loadErr.Diagnostics()) != 1 {
|
|
t.Fatalf("diagnostics = %#v", loadErr.Diagnostics())
|
|
}
|
|
diagnostic := loadErr.Diagnostics()[0]
|
|
wantCode, wantPath := configkit.CodeSequenceInvalid, "/items/0/id"
|
|
if shape == "duplicate" {
|
|
wantCode, wantPath = configkit.CodeSequenceConflict, "/items"
|
|
}
|
|
if diagnostic.Code != wantCode || diagnostic.Path.String() != wantPath {
|
|
t.Fatalf("diagnostic = %#v, want %s at %s", diagnostic, wantCode, wantPath)
|
|
}
|
|
if source == "CLI" && (diagnostic.Primary == nil || diagnostic.Primary.SourceID != "source-1") {
|
|
t.Fatalf("CLI identity diagnostic lost its source: %#v", diagnostic)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestFinalSequenceIdentityMatchesLateDynamicNamespaces(t *testing.T) {
|
|
selector := configkit.SequenceSelector{
|
|
configkit.SequenceLiteralKey("collections"),
|
|
configkit.SequenceAnyMappingKey(),
|
|
configkit.SequenceLiteralKey("items"),
|
|
}
|
|
for _, source := range []string{"defaults", "CLI", "resolution"} {
|
|
t.Run(source, func(t *testing.T) {
|
|
name := map[string]string{"defaults": "team", "CLI": "cloud", "resolution": "orbit"}[source]
|
|
path := configkit.Keys("collections", name, "items")
|
|
items := finalIdentityRecords("duplicate")
|
|
definition := configkit.Definition[map[string]any]{
|
|
SequenceIdentities: []configkit.SequenceIdentity{
|
|
configkit.SequenceIdentityMatching(selector, "id"),
|
|
},
|
|
Defaults: func() map[string]any {
|
|
if source == "defaults" {
|
|
return map[string]any{"collections": map[string]any{name: map[string]any{"items": items}}}
|
|
}
|
|
return map[string]any{}
|
|
},
|
|
}
|
|
options := configkit.Options[map[string]any]{Definition: definition}
|
|
switch source {
|
|
case "CLI":
|
|
options.Overrides = []configkit.Override{{Path: path, Value: items, Flag: "--items"}}
|
|
case "resolution":
|
|
options.Definition.Resolve = func(value *map[string]any, resolution *configkit.Resolution) error {
|
|
(*value)["collections"] = map[string]any{name: map[string]any{"items": items}}
|
|
resolution.Record(configkit.Keys("collections"), configkit.ChangeNormalization, "create namespaced records")
|
|
return nil
|
|
}
|
|
}
|
|
result, err := configkit.Load(configkit.NoSource(), options)
|
|
if result != nil {
|
|
t.Fatalf("invalid dynamic identity returned a result: %#v", result.Config)
|
|
}
|
|
loadErr := requireFacadeLoadError(t, err)
|
|
if diagnostics := loadErr.Diagnostics(); len(diagnostics) != 1 || diagnostics[0].Code != configkit.CodeSequenceConflict || diagnostics[0].Path.String() != path.String() {
|
|
t.Fatalf("dynamic identity diagnostics = %#v", diagnostics)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestFinalSequenceIdentityAppliesToLayeredLoads(t *testing.T) {
|
|
definition := configkit.Definition[map[string]any]{
|
|
SequenceIdentities: []configkit.SequenceIdentity{configkit.SequenceIdentityAt(configkit.Keys("items"), "id")},
|
|
Defaults: func() map[string]any {
|
|
return map[string]any{"items": finalIdentityRecords("duplicate")}
|
|
},
|
|
}
|
|
result, err := configkit.LoadLayered(configkit.Input{Source: configkit.NoSource()}, configkit.LayeredOptions[map[string]any]{Definition: definition})
|
|
if result != nil {
|
|
t.Fatalf("invalid layered final identities returned a result: %#v", result.Config)
|
|
}
|
|
loadErr := requireFacadeLoadError(t, err)
|
|
if diagnostics := loadErr.Diagnostics(); len(diagnostics) != 1 || diagnostics[0].Code != configkit.CodeSequenceConflict {
|
|
t.Fatalf("layered identity diagnostics = %#v", diagnostics)
|
|
}
|
|
}
|
|
|
|
func TestFinalSequenceIdentityUsesTypedNumbersAndIgnoresUnmatchedShapes(t *testing.T) {
|
|
definition := configkit.Definition[map[string]any]{
|
|
SequenceIdentities: []configkit.SequenceIdentity{
|
|
configkit.SequenceIdentityAt(configkit.Keys("items"), "id"),
|
|
configkit.SequenceIdentityAt(configkit.Keys("unmatched"), "id"),
|
|
},
|
|
}
|
|
_, err := configkit.Load(configkit.NoSource(), configkit.Options[map[string]any]{
|
|
Definition: configkit.Definition[map[string]any]{
|
|
SequenceIdentities: definition.SequenceIdentities,
|
|
Defaults: func() map[string]any {
|
|
return map[string]any{"items": []any{
|
|
map[string]any{"id": int64(1)}, map[string]any{"id": float64(1)},
|
|
}}
|
|
},
|
|
},
|
|
})
|
|
loadErr := requireFacadeLoadError(t, err)
|
|
if diagnostics := loadErr.Diagnostics(); len(diagnostics) != 1 || diagnostics[0].Code != configkit.CodeSequenceConflict {
|
|
t.Fatalf("typed numeric identity diagnostics = %#v", diagnostics)
|
|
}
|
|
|
|
valid, err := configkit.Load(configkit.Mapping("valid", map[string]any{
|
|
"items": []any{
|
|
map[string]any{"id": nil}, map[string]any{"id": true},
|
|
map[string]any{"id": int64(1)}, map[string]any{"id": "one"},
|
|
},
|
|
"other": []any{map[string]any{"id": "same"}, map[string]any{"id": "same"}},
|
|
}), configkit.Options[map[string]any]{Definition: definition})
|
|
if err != nil || valid == nil {
|
|
t.Fatalf("valid scalar identities and unmatched duplicate sequence = %#v, %v", valid, err)
|
|
}
|
|
if _, err := configkit.Load(configkit.Mapping("other-shapes", map[string]any{"items": nil}), configkit.Options[map[string]any]{Definition: definition}); err != nil {
|
|
t.Fatalf("null at a registered path was validated as an identity sequence: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestFinalSequenceIdentityDiagnosticDoesNotRevealResolvedSecrets(t *testing.T) {
|
|
const canary = "FINAL-IDENTITY-CREDENTIAL-CANARY"
|
|
const secretName = "CONFIGKIT_FINAL_IDENTITY_SECRET"
|
|
t.Setenv(secretName, canary)
|
|
type item struct {
|
|
ID string `yaml:"id"`
|
|
Token configkit.Secret `yaml:"token"`
|
|
}
|
|
type config struct {
|
|
Items []item `yaml:"items"`
|
|
}
|
|
definition := configkit.Definition[config]{
|
|
SequenceIdentities: []configkit.SequenceIdentity{configkit.SequenceIdentityAt(configkit.Keys("items"), "id")},
|
|
Defaults: func() config {
|
|
return config{Items: []item{
|
|
{ID: "duplicate", Token: configkit.SecretEnv(secretName)},
|
|
{ID: "duplicate", Token: configkit.SecretEnv(secretName)},
|
|
}}
|
|
},
|
|
}
|
|
result, err := configkit.Load(configkit.NoSource(), configkit.Options[config]{Definition: definition})
|
|
if result != nil {
|
|
t.Fatalf("duplicate identity returned a result: %#v", result.Config)
|
|
}
|
|
if err == nil || strings.Contains(err.Error(), canary) {
|
|
t.Fatalf("identity diagnostic leaked a resolved secret: %v", err)
|
|
}
|
|
loadErr := requireFacadeLoadError(t, err)
|
|
if diagnostics := loadErr.Diagnostics(); len(diagnostics) != 1 || diagnostics[0].Code != configkit.CodeSequenceConflict {
|
|
t.Fatalf("secret-bearing identity diagnostics = %#v", diagnostics)
|
|
}
|
|
}
|
|
|
|
func finalIdentityRecords(shape string) []any {
|
|
switch shape {
|
|
case "duplicate":
|
|
return []any{map[string]any{"id": "same"}, map[string]any{"id": "same"}}
|
|
case "missing":
|
|
return []any{map[string]any{"other": "value"}}
|
|
default:
|
|
return []any{map[string]any{"id": map[string]any{"nested": "value"}}}
|
|
}
|
|
}
|