package configkit_test import ( "fmt" "testing" "testing/fstest" configkit "gitea.maximumdirect.net/eric/ConfigKit" ) type movedVersionItem struct { ID string `yaml:"id"` Version string `yaml:"version"` } type movedVersionCatalog struct { Items []movedVersionItem `yaml:"items"` } func TestMainVersionDecodingFollowsSequenceMovement(t *testing.T) { t.Parallel() for _, mounted := range []bool{false, true} { for _, matching := range []bool{false, true} { for _, version := range []string{"4", "'4'"} { t.Run(fmt.Sprintf("mounted=%t/matched=%t/%s", mounted, matching, version), func(t *testing.T) { body := "items: [{id: main, version: " + version + "}]\n" path := configkit.Keys("items").Index(0).Key("version") main := configkit.Input{} if mounted { body, path = "id: main\nversion: "+version+"\n", configkit.Keys("version") main.Destination, main.PlacementMode = configkit.Keys("items"), configkit.PlacementModeSequenceItem } main.Source = configkit.Bytes("main", []byte(body)) keep := "{id: keep}" if mounted { keep = "{id: keep, version: ordinary}" } lower := "items: [" + keep + "]\n" if matching { lower = "items: [" + keep + ", {id: main}]\n" } calls := 0 loaded, err := configkit.LoadLayered(main, configkit.LayeredOptions[movedVersionCatalog]{ SourceVersionMode: configkit.SourceVersionModeIfPresent, Underlays: []configkit.Underlay{{Inputs: []configkit.Input{{Source: configkit.Bytes("lower", []byte(lower))}}}}, Definition: configkit.Definition[movedVersionCatalog]{ SequenceIdentities: []configkit.SequenceIdentity{configkit.SequenceIdentityAt(configkit.Keys("items"), "id")}, Version: &configkit.VersionRule{Path: path, Check: func(version configkit.Version, _ *configkit.Validation) error { calls++ if !version.Present || version.Text != "4" { t.Errorf("checked version = %#v", version) } return nil }}, }, }) if err != nil { t.Fatal(err) } if calls != 1 || len(loaded.Config.Items) != 2 || loaded.Config.Items[1].ID != "main" || loaded.Config.Items[1].Version != "4" { t.Fatalf("moved main = %#v, checker calls = %d", loaded.Config, calls) } if mounted && loaded.Config.Items[0].Version != "ordinary" { t.Fatal("unrelated underlay payload was treated as the main declaration") } for _, source := range loaded.Sources() { if source.Layer == configkit.LayerBase && source.VersionCheck.Path.String() != path.String() { t.Fatal("effective movement changed source-local version evidence") } } }) } } } } func TestOverlayVersionProtectionUsesAssignedRecordPositions(t *testing.T) { t.Parallel() for _, test := range []struct { name, payload string code configkit.Code }{ {name: "change main header", payload: "[{id: main, version: changed}]", code: configkit.CodeOverlayVersion}, {name: "remove main header", payload: "[{id: main}]", code: configkit.CodeOverlayVersion}, {name: "other record version", payload: "[{id: keep, version: ordinary}]"}, {name: "new record version", payload: "[{id: new, version: ordinary}]"}, {name: "only main header gets integer conversion", payload: "[{id: new, version: 5}]", code: configkit.CodeDecodeType}, } { t.Run(test.name, func(t *testing.T) { files := configkit.NewFilesystem("catalog", fstest.MapFS{ "overlays/test.yml": {Data: []byte("items: " + test.payload + "\n")}, }) calls := 0 loaded, err := configkit.LoadLayered(configkit.Input{ Source: configkit.Bytes("main", []byte("id: main\nversion: 4\n")), Destination: configkit.Keys("items"), PlacementMode: configkit.PlacementModeSequenceItem, }, configkit.LayeredOptions[movedVersionCatalog]{ SourceVersionMode: configkit.SourceVersionModeIfPresent, Overlay: "test", OverlayFilesystem: &configkit.FilesystemContext{FS: &files, BaseDir: ".", Root: "."}, Underlays: []configkit.Underlay{{Inputs: []configkit.Input{{Source: configkit.Bytes("lower", []byte("items: [{id: keep}]\n"))}}}}, Definition: configkit.Definition[movedVersionCatalog]{ SequenceIdentities: []configkit.SequenceIdentity{configkit.SequenceIdentityAt(configkit.Keys("items"), "id")}, Version: &configkit.VersionRule{Path: configkit.Keys("version"), Check: func(configkit.Version, *configkit.Validation) error { calls++; return nil }}, }, }) if calls != 1 { t.Fatalf("checker calls = %d", calls) } if test.code != "" { loadErr := requireFacadeLoadError(t, err) if loaded != nil || len(loadErr.Diagnostics()) != 1 || loadErr.Diagnostics()[0].Code != test.code { t.Fatalf("version diagnostics = %#v", loadErr.Diagnostics()) } return } if err != nil { t.Fatal(err) } if loaded.Config.Items[1].Version != "4" { t.Fatal("unrelated overlay changed main header") } }) } } func TestUnderlayVersionProtectionUsesMainRecordPosition(t *testing.T) { t.Parallel() for _, imported := range []bool{false, true} { t.Run(fmt.Sprintf("imported=%t", imported), func(t *testing.T) { payload := "items: [{id: keep}, {id: main, version: forbidden}]\n" lower := configkit.Input{Source: configkit.Bytes("lower", []byte(payload))} want := configkit.CodeUnderlayVersion if imported { files := configkit.NewFilesystem("lower", fstest.MapFS{ "main.yml": {Data: []byte("configkit: {imports: [{path: fragment.yml}]}\n")}, "fragment.yml": {Data: []byte(payload)}, }) lower.Source, lower.Imports = files.File("main.yml"), true want = configkit.CodeImportVersion } calls := 0 result, err := configkit.LoadLayered(configkit.Input{ Source: configkit.Bytes("main", []byte("id: main\nversion: 4\n")), Destination: configkit.Keys("items"), PlacementMode: configkit.PlacementModeSequenceItem, }, configkit.LayeredOptions[movedVersionCatalog]{ SourceVersionMode: configkit.SourceVersionModeIfPresent, Underlays: []configkit.Underlay{{Inputs: []configkit.Input{lower}}}, Definition: configkit.Definition[movedVersionCatalog]{ SequenceIdentities: []configkit.SequenceIdentity{configkit.SequenceIdentityAt(configkit.Keys("items"), "id")}, Version: &configkit.VersionRule{Path: configkit.Keys("version"), Check: func(configkit.Version, *configkit.Validation) error { calls++; return nil }}, }, }) failure := requireFacadeLoadError(t, err) if result != nil || calls != 1 || len(failure.Diagnostics()) != 1 || failure.Diagnostics()[0].Code != want || failure.Diagnostics()[0].Path.String() != "/items/1/version" { t.Fatalf("result=%#v calls=%d diagnostics=%#v", result, calls, failure.Diagnostics()) } }) } } func TestMainVersionFollowsInheritedSequenceMovement(t *testing.T) { t.Parallel() for _, parentID := range []string{"keep", "main"} { t.Run(parentID, func(t *testing.T) { body := "base: {items: [{id: " + parentID + ", version: ordinary}]}\nchild:\n extends: base\n items: [{id: main, version: 4}]\n" type config struct { Base, Child movedVersionCatalog } calls := 0 result, err := configkit.Load(configkit.Bytes("main", []byte(body)), configkit.Options[config]{ SourceVersionMode: configkit.SourceVersionModeIfPresent, Definition: configkit.Definition[config]{ SequenceIdentities: []configkit.SequenceIdentity{ configkit.SequenceIdentityAt(configkit.Keys("base", "items"), "id"), configkit.SequenceIdentityAt(configkit.Keys("child", "items"), "id"), }, Version: &configkit.VersionRule{Path: configkit.Keys("child", "items").Index(0).Key("version"), Check: func(configkit.Version, *configkit.Validation) error { calls++; return nil }}, }, }) if calls != 1 { t.Fatalf("checker calls = %d", calls) } if parentID == "main" { failure := requireFacadeLoadError(t, err) if result != nil || len(failure.Diagnostics()) != 1 || failure.Diagnostics()[0].Code != configkit.CodeInheritanceVersion { t.Fatalf("diagnostics = %#v", failure.Diagnostics()) } return } if err != nil { t.Fatal(err) } if len(result.Config.Child.Items) != 2 || result.Config.Child.Items[1].Version != "4" || result.Config.Child.Items[0].Version != "ordinary" { t.Fatalf("inherited items = %#v", result.Config.Child.Items) } }) } }