From cb2f826dc25e4be99203aed776998a1d52802502 Mon Sep 17 00:00:00 2001 From: Vassiliy Yegorov Date: Fri, 3 Jul 2026 12:57:36 +0700 Subject: [PATCH] =?UTF-8?q?test(diff):=20=D0=BF=D1=83=D1=81=D1=82=D0=BE?= =?UTF-8?q?=D0=B9=20=D1=88=D0=B0=D0=B1=D0=BB=D0=BE=D0=BD=20=E2=80=94=20?= =?UTF-8?q?=D0=BC=D0=B0=D1=81=D1=81=D0=BE=D0=B2=D1=8B=D0=B9=20Delete=20?= =?UTF-8?q?=D1=83=D0=BF=D1=80=D0=B0=D0=B2=D0=BB=D1=8F=D0=B5=D0=BC=D1=8B?= =?UTF-8?q?=D1=85,=20NS=20=D0=BE=D1=81=D1=82=D0=B0=D1=91=D1=82=D1=81=D1=8F?= =?UTF-8?q?=20ReadOnly?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/diff/diff_test.go | 40 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/internal/diff/diff_test.go b/internal/diff/diff_test.go index 9771acb..eac6b43 100644 --- a/internal/diff/diff_test.go +++ b/internal/diff/diff_test.go @@ -59,6 +59,46 @@ func TestDiffMarksReadOnlyForNSSOA(t *testing.T) { } } +// Global Constraint: an empty/nil template must not silently no-op — every managed +// record in the zone must surface as a Delete, while read-only records (NS/SOA) +// stay ReadOnly and excluded from Actionable(). This guards against mass deletion +// bugs where a missing template accidentally wipes the zone unattended. +func TestDiffEmptyTemplateDeletesAllManagedKeepsNSReadOnly(t *testing.T) { + actual := []model.Record{ + {Type: model.A, Name: "a.example.com.", TTL: 300, Values: []string{"1.1.1.1"}}, + {Type: model.A, Name: "b.example.com.", TTL: 300, Values: []string{"2.2.2.2"}}, + {Type: model.NS, Name: "example.com.", TTL: 3600, Values: []string{"ns1.example.com."}}, + } + cs := Diff(nil, actual) + + if len(cs.Diffs) != 3 { + t.Fatalf("expected 3 diffs (2 A deletes + 1 NS), got %d: %+v", len(cs.Diffs), cs.Diffs) + } + + da := find(cs, "A a.example.com.") + if da == nil || da.Kind != Delete || da.ReadOnly { + t.Fatalf("A a.example.com. must be a non-read-only Delete, got %+v", da) + } + db := find(cs, "A b.example.com.") + if db == nil || db.Kind != Delete || db.ReadOnly { + t.Fatalf("A b.example.com. must be a non-read-only Delete, got %+v", db) + } + dns := find(cs, "NS example.com.") + if dns == nil || dns.Kind != Delete || !dns.ReadOnly { + t.Fatalf("NS example.com. must be a ReadOnly Delete, got %+v", dns) + } + + act := cs.Actionable() + if len(act) != 2 { + t.Fatalf("expected 2 actionable deletes (A records only), got %d: %+v", len(act), act) + } + for _, d := range act { + if d.Type == model.NS { + t.Fatalf("NS must be excluded from Actionable(), got %+v", d) + } + } +} + func TestActionableExcludesInSyncAndReadOnly(t *testing.T) { tmpl := []model.Record{ {Type: model.A, Name: "a.example.com.", TTL: 300, Values: []string{"1.1.1.1"}}, // in sync