712 lines
26 KiB
Go
712 lines
26 KiB
Go
package engine
|
|
|
|
import (
|
|
"errors"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestResolutionAuditsOrdinaryListFields(t *testing.T) {
|
|
type item struct {
|
|
Count int `yaml:"count"`
|
|
Enabled bool `yaml:"enabled"`
|
|
Nested []*int `yaml:"nested"`
|
|
}
|
|
type config struct {
|
|
Items []item `yaml:"items"`
|
|
}
|
|
for _, test := range []struct {
|
|
name string
|
|
other, resize bool
|
|
wantPath string
|
|
}{
|
|
{name: "covered field"},
|
|
{name: "uncovered field", other: true, wantPath: "/items/0/enabled"},
|
|
{name: "length change", resize: true, wantPath: "/items"},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
result, err := testLoad(Bytes("items", []byte("items: [{count: 3, enabled: true, nested: [null]}]\n")), Options[config]{Definition: Definition[config]{
|
|
Resolve: func(value *config, r *Resolution) error {
|
|
value.Items[0].Count = 0
|
|
r.RecordNormalization(Keys("items").Index(0).Key("count"), "clear count")
|
|
if test.other {
|
|
value.Items[0].Enabled = false
|
|
}
|
|
if test.resize {
|
|
value.Items = append(value.Items, item{})
|
|
}
|
|
return nil
|
|
},
|
|
}})
|
|
if test.wantPath == "" {
|
|
if err != nil || result.Config.Items[0].Count != 0 {
|
|
t.Fatalf("result = %#v, error = %v", result, err)
|
|
}
|
|
return
|
|
}
|
|
var failure *LoadError
|
|
if !errors.As(err, &failure) {
|
|
t.Fatalf("error = %v", err)
|
|
}
|
|
found := false
|
|
for _, diagnostic := range failure.Diagnostics() {
|
|
if diagnostic.Code == CodeResolveUnrecordedChange && diagnostic.Path.String() == test.wantPath {
|
|
found = true
|
|
}
|
|
}
|
|
if !found {
|
|
t.Fatalf("diagnostics = %#v", failure.Diagnostics())
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestResolutionComparisonKeepsDiffAtomic(t *testing.T) {
|
|
before := parseCompositionDocument(t, "before", "items: [{values: [true, null]}]")
|
|
after := parseCompositionDocument(t, "after", "items: [{values: [false, 0]}]")
|
|
deltas := compareResolutionValues(before, after)
|
|
if len(deltas) != 2 || deltas[0].path.String() != "/items/0/values/0" || deltas[1].path.String() != "/items/0/values/1" {
|
|
t.Fatalf("resolution deltas = %#v", deltas)
|
|
}
|
|
atomic := compareConfigurationValues(before, after)
|
|
if len(atomic) != 1 || atomic[0].path.String() != "/items" {
|
|
t.Fatalf("atomic deltas = %#v", atomic)
|
|
}
|
|
}
|
|
|
|
func TestResolutionAuditsRegisteredCollectionChanges(t *testing.T) {
|
|
type item struct {
|
|
ID string `yaml:"id"`
|
|
Value string `yaml:"value"`
|
|
}
|
|
type config struct {
|
|
Items []item `yaml:"items"`
|
|
}
|
|
for _, test := range []struct {
|
|
name string
|
|
move, container, rename bool
|
|
wantPath string
|
|
}{
|
|
{name: "field edit"},
|
|
{name: "uncovered move", move: true, wantPath: "/items"},
|
|
{name: "covered move", move: true, container: true},
|
|
{name: "identity replacement", rename: true, wantPath: "/items"},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
definition := Definition[config]{Resolve: func(value *config, r *Resolution) error {
|
|
value.Items[0].Value = "changed"
|
|
if test.move {
|
|
value.Items[0], value.Items[1] = value.Items[1], value.Items[0]
|
|
}
|
|
if test.rename {
|
|
value.Items[0].ID = "new"
|
|
}
|
|
index := 0
|
|
if test.move {
|
|
index = 1
|
|
}
|
|
r.RecordNormalization(Keys("items").Index(index).Key("value"), "change value")
|
|
if test.container {
|
|
r.RecordNormalization(Keys("items"), "reorder")
|
|
}
|
|
return nil
|
|
}}
|
|
definition = DefinitionWithSequenceIdentityRegistrations(definition, []SequenceIdentityRegistrationInput{{ExactPath: pathPointer(Keys("items")), IdentityFields: []string{"id"}}})
|
|
result, err := testLoad(Bytes("items", []byte("items: [{id: a, value: old}, {id: b, value: other}]\n")), Options[config]{Definition: definition})
|
|
if test.wantPath == "" {
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if test.move {
|
|
correspondence := result.history.resolutionPayload.correspondence
|
|
entry, ok := correspondence.entryPath(Keys("items").Index(1).Key("value"))
|
|
if !ok || entry.String() != "/items/0/value" {
|
|
t.Fatalf("retained entry = %s, %t", entry.String(), ok)
|
|
}
|
|
for _, snapshot := range result.history.resolutionPayload.snapshots {
|
|
if snapshot.change.Path.String() == "/items/1/value" && (snapshot.before == nil || snapshot.before.Text != "old" || !snapshot.hasEntry || snapshot.entryPath.String() != "/items/0/value") {
|
|
t.Fatalf("paired before = %#v", snapshot.before)
|
|
}
|
|
if snapshot.before != nil {
|
|
found := false
|
|
for _, root := range result.history.documentRoots() {
|
|
if root == snapshot.before {
|
|
found = true
|
|
}
|
|
}
|
|
if !found {
|
|
t.Fatal("paired snapshot omitted from retained roots")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return
|
|
}
|
|
var failure *LoadError
|
|
if !errors.As(err, &failure) {
|
|
t.Fatalf("error = %v", err)
|
|
}
|
|
found := false
|
|
for _, diagnostic := range failure.Diagnostics() {
|
|
if diagnostic.Code == CodeResolveUnrecordedChange && diagnostic.Path.String() == test.wantPath {
|
|
found = true
|
|
}
|
|
}
|
|
if !found {
|
|
t.Fatalf("diagnostics = %#v", failure.Diagnostics())
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRegisteredResolutionFallbackAndHelperTarget(t *testing.T) {
|
|
type item struct {
|
|
ID string `yaml:"id"`
|
|
Value string `yaml:"value"`
|
|
}
|
|
type config struct {
|
|
Items []item `yaml:"items"`
|
|
}
|
|
registration := []SequenceIdentityRegistrationInput{{ExactPath: pathPointer(Keys("items")), IdentityFields: []string{"id"}}}
|
|
makeDefinition := func(resolve func(*config, *Resolution) error) Definition[config] {
|
|
return DefinitionWithSequenceIdentityRegistrations(Definition[config]{Defaults: func() config { return config{Items: []item{{ID: "a"}, {ID: "a"}}} }, Resolve: resolve}, registration)
|
|
}
|
|
_, err := testLoad(NoSource(), Options[config]{Definition: makeDefinition(func(value *config, r *Resolution) error {
|
|
value.Items[1].ID = "b"
|
|
r.RecordNormalization(Keys("items"), "repair identities")
|
|
return nil
|
|
})})
|
|
if err != nil {
|
|
t.Fatalf("repair rejected: %v", err)
|
|
}
|
|
_, err = testLoad(NoSource(), Options[config]{Definition: makeDefinition(func(*config, *Resolution) error { return nil })})
|
|
if err == nil {
|
|
t.Fatal("invalid final identities accepted")
|
|
}
|
|
definition := DefinitionWithSequenceIdentityRegistrations(Definition[config]{Resolve: func(value *config, r *Resolution) error {
|
|
Normalize(r, Keys("items").Index(0).Key("value"), &value.Items[0].Value, "changed", "normalize")
|
|
value.Items[0], value.Items[1] = value.Items[1], value.Items[0]
|
|
r.RecordNormalization(Keys("items"), "reorder")
|
|
return nil
|
|
}}, registration)
|
|
_, err = testLoad(Bytes("items", []byte("items: [{id: a, value: first}, {id: b, value: second}]\n")), Options[config]{Definition: definition})
|
|
var failure *LoadError
|
|
if !errors.As(err, &failure) {
|
|
t.Fatalf("helper error = %v", err)
|
|
}
|
|
found := false
|
|
for _, diagnostic := range failure.Diagnostics() {
|
|
if diagnostic.Code == CodeResolveInvalidTarget && diagnostic.Path.String() == "/items/0/value" {
|
|
found = true
|
|
}
|
|
}
|
|
if !found {
|
|
t.Fatalf("diagnostics = %#v", failure.Diagnostics())
|
|
}
|
|
}
|
|
|
|
func TestRegisteredResolutionMetadataFollowsEntryIdentity(t *testing.T) {
|
|
type item struct {
|
|
ID string `yaml:"id"`
|
|
Enabled bool `yaml:"enabled"`
|
|
Count int `yaml:"count"`
|
|
}
|
|
type config struct {
|
|
Items []item `yaml:"items"`
|
|
}
|
|
entryFixed := false
|
|
definition := DefinitionWithSequenceIdentityRegistrations(Definition[config]{Resolve: func(value *config, r *Resolution) error {
|
|
entryFixed = r.IsExplicit(Keys("items").Index(0).Key("enabled")) && !r.IsExplicit(Keys("items").Index(1).Key("enabled"))
|
|
value.Items[0], value.Items[1] = value.Items[1], value.Items[0]
|
|
value.Items = append(value.Items, item{ID: "new"})
|
|
r.RecordNormalization(Keys("items"), "reorder and add")
|
|
return nil
|
|
}}, []SequenceIdentityRegistrationInput{{ExactPath: pathPointer(Keys("items")), IdentityFields: []string{"id"}}})
|
|
result, err := testLoad(Bytes("records", []byte("items: [{id: a, enabled: false}, {id: b, count: 0}]\n")), Options[config]{Definition: definition})
|
|
if err != nil || !entryFixed {
|
|
t.Fatalf("load = %#v, fixed entry = %t, error = %v", result, entryFixed, err)
|
|
}
|
|
for _, check := range []struct {
|
|
index int
|
|
key string
|
|
explicit bool
|
|
}{{0, "enabled", false}, {0, "count", true}, {1, "enabled", true}, {1, "count", false}, {2, "id", false}} {
|
|
info, ok := result.Metadata().Lookup(Keys("items").Index(check.index).Key(check.key))
|
|
if !ok || info.Explicit != check.explicit || info.Origin.Layer != LayerResolution {
|
|
t.Errorf("item %d %s = %#v, %t", check.index, check.key, info, ok)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRegisteredResolutionValidationLocationFollowsMovedRecord(t *testing.T) {
|
|
type item struct {
|
|
ID string `yaml:"id"`
|
|
Value string `yaml:"value"`
|
|
}
|
|
type config struct {
|
|
Items []item `yaml:"items"`
|
|
}
|
|
definition := DefinitionWithSequenceIdentityRegistrations(Definition[config]{
|
|
Resolve: func(value *config, r *Resolution) error {
|
|
value.Items[0], value.Items[1] = value.Items[1], value.Items[0]
|
|
r.RecordNormalization(Keys("items"), "reorder")
|
|
return nil
|
|
},
|
|
Validate: func(_ *config, v *Validation) { v.Error(Keys("items").Index(0).Key("value"), "invalid") },
|
|
}, []SequenceIdentityRegistrationInput{{ExactPath: pathPointer(Keys("items")), IdentityFields: []string{"id"}}})
|
|
_, err := testLoad(Bytes("records", []byte("items:\n - id: a\n value: first\n - id: b\n value: second\n")), Options[config]{Definition: definition})
|
|
var failure *LoadError
|
|
if !errors.As(err, &failure) {
|
|
t.Fatalf("error = %v", err)
|
|
}
|
|
for _, diagnostic := range failure.Diagnostics() {
|
|
if diagnostic.Path.String() == "/items/0/value" && diagnostic.Primary != nil && diagnostic.Primary.Line == 5 {
|
|
return
|
|
}
|
|
}
|
|
t.Fatalf("diagnostics = %#v", failure.Diagnostics())
|
|
}
|
|
|
|
func TestRegisteredResolutionHistoryFollowsMatchedSource(t *testing.T) {
|
|
type item struct {
|
|
ID string `yaml:"id"`
|
|
Value string `yaml:"value"`
|
|
}
|
|
type config struct {
|
|
Items []item `yaml:"items"`
|
|
}
|
|
definition := DefinitionWithSequenceIdentityRegistrations(Definition[config]{Resolve: func(value *config, r *Resolution) error {
|
|
value.Items[0], value.Items[1] = value.Items[1], value.Items[0]
|
|
value.Items = append(value.Items, item{ID: "new", Value: "added"})
|
|
r.RecordNormalization(Keys("items"), "reorder and add")
|
|
return nil
|
|
}}, []SequenceIdentityRegistrationInput{{ExactPath: pathPointer(Keys("items")), IdentityFields: []string{"id"}}})
|
|
result, err := testLoad(Bytes("records", []byte("items:\n - id: a\n value: first\n - id: b\n value: second\n")), Options[config]{Definition: definition})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, check := range []struct{ index, line int }{{0, 5}, {1, 3}} {
|
|
path := Keys("items").Index(check.index).Key("value")
|
|
if location, ok := result.history.latestHistoricalLocation(path); !ok || location.Line != check.line {
|
|
t.Errorf("historical location for %s = %#v, %t", path.String(), location, ok)
|
|
}
|
|
history := result.Metadata().History(path)
|
|
found := false
|
|
for _, change := range history {
|
|
for _, location := range change.Origin.Locations {
|
|
if location.Line == check.line {
|
|
found = true
|
|
} else if location.Line == 3 || location.Line == 5 {
|
|
t.Errorf("wrong record in %s: %#v", path.String(), history)
|
|
}
|
|
}
|
|
}
|
|
if !found {
|
|
t.Errorf("missing source line %d in %#v", check.line, history)
|
|
}
|
|
explanation, ok := result.Inspection.Explain(path)
|
|
if !ok {
|
|
t.Fatalf("missing explanation for %s", path.String())
|
|
}
|
|
want := "second\n"
|
|
if check.index == 1 {
|
|
want = "first\n"
|
|
}
|
|
foundValue := false
|
|
for _, change := range explanation.History {
|
|
if change.Origin.Layer == LayerMain && change.After.YAML == want {
|
|
foundValue = true
|
|
}
|
|
}
|
|
if !foundValue {
|
|
t.Errorf("missing matched input %q in %#v", want, explanation.History)
|
|
}
|
|
}
|
|
for _, change := range result.Metadata().History(Keys("items").Index(2).Key("value")) {
|
|
if change.Origin.Layer == LayerMain {
|
|
t.Fatalf("added record inherited input: %#v", change)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestMovedRecordExplanationIncludesInheritedValue(t *testing.T) {
|
|
type item struct {
|
|
ID string `yaml:"id"`
|
|
Value string `yaml:"value"`
|
|
}
|
|
type config struct {
|
|
Items []item `yaml:"items"`
|
|
}
|
|
definition := DefinitionWithSequenceIdentityRegistrations(Definition[config]{Resolve: func(value *config, r *Resolution) error {
|
|
value.Items[0], value.Items[1] = value.Items[1], value.Items[0]
|
|
r.RecordNormalization(Keys("items"), "reorder")
|
|
return nil
|
|
}}, []SequenceIdentityRegistrationInput{{ExactPath: pathPointer(Keys("items")), IdentityFields: []string{"id"}}})
|
|
result, err := testLoad(Bytes("records", []byte("items: [{id: parent, value: inherited}, {id: child, extends: {id: parent}}]\n")), Options[config]{Definition: definition})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
explanation, ok := result.Inspection.Explain(Keys("items").Index(0).Key("value"))
|
|
if !ok {
|
|
t.Fatal("missing explanation")
|
|
}
|
|
found := false
|
|
for _, change := range explanation.History {
|
|
if change.After.YAML == "inherited\n" {
|
|
found = true
|
|
}
|
|
}
|
|
if !found {
|
|
t.Fatalf("inherited history = %#v", explanation.History)
|
|
}
|
|
}
|
|
|
|
func TestMovedRecordExplanationIncludesCLIValue(t *testing.T) {
|
|
type item struct {
|
|
ID string `yaml:"id"`
|
|
Value string `yaml:"value"`
|
|
}
|
|
type config struct {
|
|
Items []item `yaml:"items"`
|
|
}
|
|
definition := DefinitionWithSequenceIdentityRegistrations(Definition[config]{Resolve: func(value *config, r *Resolution) error {
|
|
value.Items[0], value.Items[1] = value.Items[1], value.Items[0]
|
|
r.RecordNormalization(Keys("items"), "reorder")
|
|
return nil
|
|
}}, []SequenceIdentityRegistrationInput{{ExactPath: pathPointer(Keys("items")), IdentityFields: []string{"id"}}})
|
|
result, err := testLoad(NoSource(), Options[config]{Definition: definition, Overrides: []Override{{
|
|
Path: Keys("items"), Flag: "--items", Value: []any{map[string]any{"id": "a", "value": "first"}, map[string]any{"id": "b", "value": "second"}},
|
|
}}})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
explanation, ok := result.Inspection.Explain(Keys("items").Index(1).Key("value"))
|
|
if !ok {
|
|
t.Fatal("missing explanation")
|
|
}
|
|
for _, change := range explanation.History {
|
|
if change.Origin.Layer == LayerCLI && change.After.YAML == "first\n" {
|
|
return
|
|
}
|
|
}
|
|
t.Fatalf("CLI history = %#v", explanation.History)
|
|
}
|
|
|
|
func TestRegisteredResolutionExplainsRemovalAndReusedPositions(t *testing.T) {
|
|
type item struct {
|
|
ID string `yaml:"id"`
|
|
Value string `yaml:"value"`
|
|
}
|
|
type config struct {
|
|
Items []item `yaml:"items"`
|
|
}
|
|
definition := DefinitionWithSequenceIdentityRegistrations(Definition[config]{Resolve: func(value *config, r *Resolution) error {
|
|
value.Items = value.Items[1:]
|
|
r.RecordNormalization(Keys("items"), "remove first")
|
|
return nil
|
|
}}, []SequenceIdentityRegistrationInput{{ExactPath: pathPointer(Keys("items")), IdentityFields: []string{"id"}}})
|
|
result, err := testLoad(Bytes("records", []byte("items:\n - id: reports\n value: report\n - id: invoices\n value: invoice\n")), Options[config]{Definition: definition})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
path := Keys("items").Index(0).Key("value")
|
|
explanation, ok := result.Inspection.Explain(path)
|
|
if !ok {
|
|
t.Fatal("survivor explanation missing")
|
|
}
|
|
found := false
|
|
for _, change := range explanation.History {
|
|
if change.Origin.Layer == LayerResolution {
|
|
if change.Before.YAML != "invoice\n" || change.After.YAML != "invoice\n" {
|
|
t.Fatalf("survivor operation = %#v", change)
|
|
}
|
|
found = true
|
|
}
|
|
if change.Origin.Layer == LayerMain && change.After.YAML == "report\n" {
|
|
t.Fatalf("removed occupant leaked into survivor: %#v", change)
|
|
}
|
|
}
|
|
if !found {
|
|
t.Fatal("missing collection operation")
|
|
}
|
|
container, ok := result.Inspection.Explain(Keys("items"))
|
|
if !ok {
|
|
t.Fatal("collection explanation missing")
|
|
}
|
|
found = false
|
|
for _, change := range container.History {
|
|
if change.Origin.Layer == LayerResolution && strings.Contains(change.Before.YAML, "reports") && !strings.Contains(change.After.YAML, "reports") {
|
|
found = true
|
|
}
|
|
}
|
|
if !found {
|
|
t.Fatalf("missing removed collection evidence: %#v", container.History)
|
|
}
|
|
mainEvidence := false
|
|
for _, change := range result.Metadata().History(Keys("items")) {
|
|
if change.Origin.Layer == LayerMain && len(change.Origin.Locations) != 0 {
|
|
mainEvidence = true
|
|
}
|
|
}
|
|
if !mainEvidence {
|
|
t.Fatal("collection history lost source attribution")
|
|
}
|
|
if _, ok := result.Inspection.Explain(Keys("items").Index(1)); !ok {
|
|
t.Fatal("absent historical position missing")
|
|
}
|
|
if _, ok := result.Inspection.Explain(Keys("items").Index(8)); ok {
|
|
t.Fatal("unknown position reported")
|
|
}
|
|
}
|
|
|
|
func TestRegisteredResolutionExplainsAddedAndChangedIdentities(t *testing.T) {
|
|
type item struct {
|
|
ID string `yaml:"id"`
|
|
Value string `yaml:"value"`
|
|
}
|
|
type config struct {
|
|
Items []item `yaml:"items"`
|
|
}
|
|
for _, replace := range []bool{false, true} {
|
|
definition := DefinitionWithSequenceIdentityRegistrations(Definition[config]{Resolve: func(value *config, r *Resolution) error {
|
|
if replace {
|
|
value.Items[0] = item{ID: "new", Value: "added"}
|
|
} else {
|
|
value.Items = append([]item{{ID: "new", Value: "added"}}, value.Items...)
|
|
}
|
|
r.RecordNormalization(Keys("items"), "update collection")
|
|
return nil
|
|
}}, []SequenceIdentityRegistrationInput{{ExactPath: pathPointer(Keys("items")), IdentityFields: []string{"id"}}})
|
|
result, err := testLoad(Bytes("records", []byte("items: [{id: old, value: original}]\n")), Options[config]{Definition: definition})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
explanation, ok := result.Inspection.Explain(Keys("items").Index(0).Key("value"))
|
|
if !ok {
|
|
t.Fatal("added explanation missing")
|
|
}
|
|
for _, change := range explanation.History {
|
|
if change.Origin.Layer == LayerMain || (change.Origin.Layer == LayerResolution && change.Before.Present) {
|
|
t.Fatalf("added record inherited previous value: %#v", explanation.History)
|
|
}
|
|
}
|
|
if !replace {
|
|
moved, ok := result.Inspection.Explain(Keys("items").Index(1).Key("value"))
|
|
if !ok {
|
|
t.Fatal("shifted survivor missing")
|
|
}
|
|
found := false
|
|
for _, change := range moved.History {
|
|
if change.Origin.Layer == LayerResolution && change.Before.YAML == "original\n" && change.After.YAML == "original\n" {
|
|
found = true
|
|
}
|
|
}
|
|
if !found {
|
|
t.Fatalf("shifted survivor = %#v", moved.History)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRegisteredResolutionExplainsRemovedFieldOnMovedRecord(t *testing.T) {
|
|
type config struct {
|
|
Items []map[string]any `yaml:"items"`
|
|
}
|
|
definition := DefinitionWithSequenceIdentityRegistrations(Definition[config]{Resolve: func(value *config, r *Resolution) error {
|
|
delete(value.Items[0], "extra")
|
|
value.Items[0], value.Items[1] = value.Items[1], value.Items[0]
|
|
r.RecordNormalization(Keys("items"), "remove and reorder")
|
|
return nil
|
|
}}, []SequenceIdentityRegistrationInput{{ExactPath: pathPointer(Keys("items")), IdentityFields: []string{"id"}}})
|
|
result, err := testLoad(Bytes("records", []byte("items: [{id: a, extra: own}, {id: b, extra: other}]\n")), Options[config]{Definition: definition})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
explanation, ok := result.Inspection.Explain(Keys("items").Index(1).Key("extra"))
|
|
if !ok {
|
|
t.Fatal("removed field explanation missing")
|
|
}
|
|
for _, change := range explanation.History {
|
|
if change.Origin.Layer == LayerResolution && change.Before.YAML == "own\n" && !change.After.Present {
|
|
return
|
|
}
|
|
}
|
|
t.Fatalf("removed field history = %#v", explanation.History)
|
|
}
|
|
|
|
func TestRegisteredResolutionTransfersSensitivityFromEntry(t *testing.T) {
|
|
type item struct {
|
|
ID string `yaml:"id"`
|
|
Value string `yaml:"value"`
|
|
}
|
|
type config struct {
|
|
Items []item `yaml:"items"`
|
|
Derived string `yaml:"derived"`
|
|
}
|
|
definition := DefinitionWithSequenceIdentityRegistrations(Definition[config]{
|
|
Secret: func(path Path) bool { return path.String() == "/items/0/value" },
|
|
Resolve: func(value *config, r *Resolution) error {
|
|
value.Items[0], value.Items[1] = value.Items[1], value.Items[0]
|
|
value.Derived = value.Items[1].Value
|
|
r.RecordNormalization(Keys("items"), "reorder")
|
|
r.RecordNormalization(Keys("derived"), "derive", Keys("items").Index(1).Key("value"))
|
|
return nil
|
|
},
|
|
}, []SequenceIdentityRegistrationInput{{ExactPath: pathPointer(Keys("items")), IdentityFields: []string{"id"}}})
|
|
result, err := testLoad(Bytes("records", []byte("items: [{id: a, value: confidential}, {id: b, value: visible}]\n")), Options[config]{Definition: definition})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, path := range []Path{Keys("items").Index(0).Key("value"), Keys("items").Index(1).Key("value"), Keys("derived")} {
|
|
if !pathIsSecret(result.Inspection.secrets, path) {
|
|
t.Errorf("sensitivity lost at %s", path.String())
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRegisteredResolutionKeepsRemovedSensitivity(t *testing.T) {
|
|
type item struct {
|
|
ID string `yaml:"id"`
|
|
Value string `yaml:"value"`
|
|
}
|
|
type config struct {
|
|
Items []item `yaml:"items"`
|
|
}
|
|
definition := DefinitionWithSequenceIdentityRegistrations(Definition[config]{
|
|
Secret: func(path Path) bool { return path.String() == "/items/0/value" },
|
|
Resolve: func(value *config, r *Resolution) error {
|
|
value.Items = value.Items[1:]
|
|
r.RecordNormalization(Keys("items"), "remove record")
|
|
return nil
|
|
},
|
|
}, []SequenceIdentityRegistrationInput{{ExactPath: pathPointer(Keys("items")), IdentityFields: []string{"id"}}})
|
|
result, err := testLoad(Bytes("records", []byte("items: [{id: a, value: confidential}, {id: b, value: visible}]\n")), Options[config]{Definition: definition})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !pathIsSecret(result.Inspection.secrets, Keys("items").Index(0).Key("value")) {
|
|
t.Fatal("removed historical sensitivity lost")
|
|
}
|
|
}
|
|
|
|
func TestMovedAndRemovedSensitiveValuesStayHidden(t *testing.T) {
|
|
type item struct {
|
|
ID string `yaml:"id"`
|
|
Value string `yaml:"value"`
|
|
}
|
|
type config struct {
|
|
Items []item `yaml:"items"`
|
|
Derived string `yaml:"derived"`
|
|
}
|
|
for _, remove := range []bool{false, true} {
|
|
definition := DefinitionWithSequenceIdentityRegistrations(Definition[config]{
|
|
Secret: func(path Path) bool { return path.String() == "/items/1/value" },
|
|
Resolve: func(value *config, r *Resolution) error {
|
|
value.Items[0], value.Items[1] = value.Items[1], value.Items[0]
|
|
value.Derived = value.Items[1].Value
|
|
r.RecordNormalization(Keys("items").Index(1).Key("value"), "retain value", Keys("derived"))
|
|
if remove {
|
|
value.Items = value.Items[:1]
|
|
}
|
|
r.RecordNormalization(Keys("items"), "update records")
|
|
r.RecordNormalization(Keys("derived"), "derive", Keys("items").Index(1).Key("value"))
|
|
return nil
|
|
},
|
|
}, []SequenceIdentityRegistrationInput{{ExactPath: pathPointer(Keys("items")), IdentityFields: []string{"id"}}})
|
|
result, err := testLoad(Bytes("records", []byte("items: [{id: a, value: PRIVATE_CANARY}, {id: b, value: other}]\n")), Options[config]{Definition: definition})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, path := range []Path{Keys("items"), Keys("items").Index(0), Keys("items").Index(1), Keys("derived")} {
|
|
explanation, ok := result.Inspection.Explain(path)
|
|
if !ok {
|
|
continue
|
|
}
|
|
if strings.Contains(explanation.Effective.YAML, "PRIVATE_CANARY") {
|
|
t.Fatalf("effective leaked at %s", path.String())
|
|
}
|
|
for _, change := range explanation.History {
|
|
if strings.Contains(change.Before.YAML, "PRIVATE_CANARY") || strings.Contains(change.After.YAML, "PRIVATE_CANARY") {
|
|
t.Fatalf("history leaked at %s: %#v", path.String(), change)
|
|
}
|
|
}
|
|
}
|
|
var output strings.Builder
|
|
if err := result.Inspection.WriteEffective(&output); err != nil || strings.Contains(output.String(), "PRIVATE_CANARY") {
|
|
t.Fatalf("effective output leaked: %q, %v", output.String(), err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestMovedDynamicTaggedValueStaysHidden(t *testing.T) {
|
|
type sensitive struct {
|
|
ID string `yaml:"id"`
|
|
Token string `yaml:"token" configkit:"secret"`
|
|
}
|
|
type config struct {
|
|
Items []any `yaml:"items"`
|
|
}
|
|
definition := DefinitionWithSequenceIdentityRegistrations(Definition[config]{
|
|
Defaults: func() config {
|
|
return config{Items: []any{sensitive{ID: "a", Token: "DYNAMIC_PRIVATE_CANARY"}, sensitive{ID: "b", Token: "other"}}}
|
|
},
|
|
Resolve: func(value *config, r *Resolution) error {
|
|
value.Items[0], value.Items[1] = value.Items[1], value.Items[0]
|
|
r.RecordNormalization(Keys("items"), "reorder")
|
|
return nil
|
|
},
|
|
}, []SequenceIdentityRegistrationInput{{ExactPath: pathPointer(Keys("items")), IdentityFields: []string{"id"}}})
|
|
result, err := testLoad(NoSource(), Options[config]{Definition: definition})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, path := range []Path{Keys("items"), Keys("items").Index(1).Key("token")} {
|
|
explanation, ok := result.Inspection.Explain(path)
|
|
if !ok {
|
|
t.Fatalf("missing explanation %s", path.String())
|
|
}
|
|
if strings.Contains(explanation.Effective.YAML, "DYNAMIC_PRIVATE_CANARY") {
|
|
t.Fatal("effective value leaked")
|
|
}
|
|
for _, change := range explanation.History {
|
|
if strings.Contains(change.Before.YAML, "DYNAMIC_PRIVATE_CANARY") || strings.Contains(change.After.YAML, "DYNAMIC_PRIVATE_CANARY") {
|
|
t.Fatal("history leaked")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestMovedDependencyProtectsEarlierMatchedValue(t *testing.T) {
|
|
type item struct {
|
|
ID string `yaml:"id"`
|
|
Secret string `yaml:"secret"`
|
|
Value string `yaml:"value"`
|
|
}
|
|
type config struct {
|
|
Items []item `yaml:"items"`
|
|
}
|
|
definition := DefinitionWithSequenceIdentityRegistrations(Definition[config]{
|
|
Secret: func(path Path) bool { return path.String() == "/items/0/secret" },
|
|
Resolve: func(value *config, r *Resolution) error {
|
|
value.Items[0], value.Items[1] = value.Items[1], value.Items[0]
|
|
value.Items[0].Value = value.Items[1].Secret
|
|
r.RecordNormalization(Keys("items"), "reorder")
|
|
r.RecordNormalization(Keys("items").Index(0).Key("value"), "derive", Keys("items").Index(1).Key("secret"))
|
|
return nil
|
|
},
|
|
}, []SequenceIdentityRegistrationInput{{ExactPath: pathPointer(Keys("items")), IdentityFields: []string{"id"}}})
|
|
result, err := testLoad(Bytes("records", []byte("items: [{id: a, secret: HIDDEN_A, value: public}, {id: b, value: EARLIER_PRIVATE}]\n")), Options[config]{Definition: definition})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
explanation, ok := result.Inspection.Explain(Keys("items").Index(0).Key("value"))
|
|
if !ok {
|
|
t.Fatal("missing explanation")
|
|
}
|
|
for _, change := range explanation.History {
|
|
if strings.Contains(change.Before.YAML, "EARLIER_PRIVATE") || strings.Contains(change.After.YAML, "EARLIER_PRIVATE") || strings.Contains(change.After.YAML, "HIDDEN_A") {
|
|
t.Fatalf("sensitive lineage leaked: %#v", change)
|
|
}
|
|
}
|
|
}
|