diff --git a/internal/plan/view/helpers_test.go b/internal/plan/view/helpers_test.go index a1b1556..7b6fa65 100644 --- a/internal/plan/view/helpers_test.go +++ b/internal/plan/view/helpers_test.go @@ -381,17 +381,20 @@ func assertJSONPaths(tb testing.TB, raw []byte, wants []jsonPathWant) { } // assertGolden is the primary golden contract: got must equal -// testdata/golden/.golden in full. A mismatch stops the test. +// testdata/golden/.golden in full. A mismatch stops the test +// with "golden file %s is stale; run with -update to regenerate". // A normal run never skips the comparison. -update rewrites the file // from got, then the same equality still runs. func assertGolden(t *testing.T, name, got string) { t.Helper() - require.Equal(t, string(readGolden(t, name, got)), got) + path, want := readGolden(t, name, got) + require.Equal(t, string(want), got, "golden file %s is stale; run with -update to regenerate", path) } func assertJSONGolden(t *testing.T, name, got string) { t.Helper() - require.JSONEq(t, string(readGolden(t, name, got)), got) + path, want := readGolden(t, name, got) + require.JSONEq(t, string(want), got, "golden file %s is stale; run with -update to regenerate", path) } func compilePlanSchema(t *testing.T) *jsonschema.Schema { @@ -412,7 +415,7 @@ func validatePlanSchema(t *testing.T, raw []byte) { require.NoError(t, compilePlanSchema(t).Validate(v)) } -func readGolden(t *testing.T, name, got string) []byte { +func readGolden(t *testing.T, name, got string) (string, []byte) { t.Helper() path := filepath.Clean(filepath.Join("testdata", "golden", name+".golden")) if *update { @@ -423,5 +426,5 @@ func readGolden(t *testing.T, name, got string) []byte { if err != nil { t.Fatalf("golden %s missing or unreadable: %v\n--- got ---\n%s", path, err, got) } - return want + return path, want } diff --git a/internal/plan/view/json_test.go b/internal/plan/view/json_test.go index bb509c3..bd1d2d1 100644 --- a/internal/plan/view/json_test.go +++ b/internal/plan/view/json_test.go @@ -444,6 +444,9 @@ func TestWriteJSON_DriftSecretRedaction(t *testing.T) { assertJSONAt(t, buf.Bytes(), tt.before, "drift", 0, "fields", 0, "before") validatePlanSchema(t, buf.Bytes()) require.Equal(t, "b2xk", p.Drift[0].Fields[0].Before) + require.Equal(t, "old", objectString(t, p.Drift[0].Previous.Object, "stringData", "password")) + require.Equal(t, "bmV3", objectString(t, p.Drift[0].Live.Object, "data", "password")) + require.Equal(t, "c2VjcmV0", objectString(t, p.Drift[1].Live.Object, "data", "password")) }) } } diff --git a/internal/plan/view/prepare_test.go b/internal/plan/view/prepare_test.go index 9174419..166a40f 100644 --- a/internal/plan/view/prepare_test.go +++ b/internal/plan/view/prepare_test.go @@ -56,6 +56,93 @@ func TestPrepareRender_InvalidEnums(t *testing.T) { }(), wantErr: "invalid change 0 field 0 op", }, + { + name: "invalid drift action", + plan: semantic.Plan{ + HelmAction: semantic.HelmNone, + Drift: []semantic.DriftChange{{ + Resource: ref("ConfigMap", "app"), + }}, + }, + wantErr: "invalid drift 0 action", + }, + { + name: "invalid drift field op", + plan: semantic.Plan{ + HelmAction: semantic.HelmNone, + Drift: []semantic.DriftChange{{ + Resource: ref("ConfigMap", "app"), + Action: semantic.DriftModified, + Fields: []semantic.FieldChange{{Path: "/data/key"}}, + }}, + }, + wantErr: "invalid drift 0 field 0 op", + }, + { + name: "invalid task phase", + plan: semantic.Plan{ + HelmAction: semantic.HelmUpgrade, + Tasks: []semantic.TaskPlan{{ + Name: "migrate", + }}, + }, + wantErr: "invalid task 0 phase", + }, + { + name: "invalid task action", + plan: semantic.Plan{ + HelmAction: semantic.HelmUpgrade, + Tasks: []semantic.TaskPlan{{ + Name: "migrate", + Phase: semantic.TaskPreDeploy, + }}, + }, + wantErr: "invalid task 0 action", + }, + { + name: "invalid definition action", + plan: semantic.Plan{ + HelmAction: semantic.HelmUpgrade, + Tasks: []semantic.TaskPlan{{ + Name: "migrate", + Phase: semantic.TaskPreDeploy, + Action: semantic.TaskUpdate, + Definitions: []semantic.HookDefinition{{ + Resource: ref("ConfigMap", "app"), + }}, + }}, + }, + wantErr: "invalid task 0 definition 0 action", + }, + { + name: "invalid definition field op", + plan: semantic.Plan{ + HelmAction: semantic.HelmUpgrade, + Tasks: []semantic.TaskPlan{{ + Name: "migrate", + Phase: semantic.TaskPreDeploy, + Action: semantic.TaskUpdate, + Definitions: []semantic.HookDefinition{{ + Resource: ref("ConfigMap", "app"), + Action: semantic.Update, + Fields: []semantic.FieldChange{{Path: "/data/key"}}, + }}, + }}, + }, + wantErr: "invalid task 0 definition 0 field 0 op", + }, + { + name: "invalid chart crd lifecycle", + plan: semantic.Plan{ + HelmAction: semantic.HelmUpgrade, + ChartCRDs: []semantic.ChartCRD{{ + Source: ".deployah/crds/widgets.yaml", + Kind: "CustomResourceDefinition", + Name: "widgets.example.com", + }}, + }, + wantErr: "invalid chart crd 0 lifecycle", + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/internal/plan/view/redact_test.go b/internal/plan/view/redact_test.go index d7d4d00..433fbe2 100644 --- a/internal/plan/view/redact_test.go +++ b/internal/plan/view/redact_test.go @@ -383,6 +383,49 @@ func TestSecretRedaction_OmitsPlaintext(t *testing.T) { } } +func TestSecretRedaction_HookDefinitionsUnchanged(t *testing.T) { + t.Parallel() + p := mustPlanWithTasks(t, semantic.HelmUpgrade, nil, []semantic.TaskPlan{{ + Name: "migrate", + Phase: semantic.TaskPreDeploy, + Action: semantic.TaskUpdate, + WillRun: true, + Definitions: []semantic.HookDefinition{ + { + Resource: ref("Secret", "hook-secret"), + Action: semantic.Update, + Before: snap(secretObj("hook-secret", "old-pass", "old-tok")), + After: snap(secretObj("hook-secret", "new-pass", "new-tok")), + }, + { + Resource: semantic.ResourceRef{APIVersion: "v1", Kind: "Secret", Namespace: "prod", Name: "created"}, + Action: semantic.Create, + After: snap(secretObj("created", "c-pass", "c-tok")), + }, + { + Resource: semantic.ResourceRef{APIVersion: "v1", Kind: "Secret", Namespace: "prod", Name: "removed"}, + Action: semantic.Delete, + Before: snap(secretObj("removed", "d-pass", "d-tok")), + }, + }, + }}) + require.NoError(t, view.WriteHuman(&bytes.Buffer{}, p, view.Options{})) + require.NoError(t, view.WriteJSON(&bytes.Buffer{}, p, view.Options{})) + defs := p.Tasks[0].Definitions + require.Len(t, defs, 3) + require.Equal(t, "created", objectString(t, defs[0].After.Object, "metadata", "name")) + require.Equal(t, "c-pass", objectString(t, defs[0].After.Object, "stringData", "password")) + require.Equal(t, "c-tok", objectString(t, defs[0].After.Object, "data", "token")) + require.Equal(t, "hook-secret", objectString(t, defs[1].Before.Object, "metadata", "name")) + require.Equal(t, "old-pass", objectString(t, defs[1].Before.Object, "stringData", "password")) + require.Equal(t, "old-tok", objectString(t, defs[1].Before.Object, "data", "token")) + require.Equal(t, "new-pass", objectString(t, defs[1].After.Object, "stringData", "password")) + require.Equal(t, "new-tok", objectString(t, defs[1].After.Object, "data", "token")) + require.Equal(t, "removed", objectString(t, defs[2].Before.Object, "metadata", "name")) + require.Equal(t, "d-pass", objectString(t, defs[2].Before.Object, "stringData", "password")) + require.Equal(t, "d-tok", objectString(t, defs[2].Before.Object, "data", "token")) +} + func TestSecretRedaction_DriftDefaultAndShowSecrets(t *testing.T) { t.Parallel() p := secretDriftPlan(t)