116 lines
4.1 KiB
Go
116 lines
4.1 KiB
Go
package diff
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/vasyakrg/dns-autoresolver/internal/model"
|
|
)
|
|
|
|
func find(cs Changeset, key string) *RecordDiff {
|
|
for i := range cs.Diffs {
|
|
d := cs.Diffs[i]
|
|
var r *model.Record
|
|
if d.Desired != nil {
|
|
r = d.Desired
|
|
} else {
|
|
r = d.Actual
|
|
}
|
|
if r.Key() == key {
|
|
return &cs.Diffs[i]
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func TestDiffAddUpdateDeleteInSync(t *testing.T) {
|
|
tmpl := []model.Record{
|
|
{Type: model.A, Name: "a.example.com.", TTL: 300, Values: []string{"1.1.1.1"}}, // in sync
|
|
{Type: model.A, Name: "b.example.com.", TTL: 300, Values: []string{"2.2.2.2"}}, // update
|
|
{Type: model.A, Name: "c.example.com.", TTL: 300, Values: []string{"3.3.3.3"}}, // add
|
|
}
|
|
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{"9.9.9.9"}},
|
|
{Type: model.A, Name: "d.example.com.", TTL: 300, Values: []string{"4.4.4.4"}}, // delete (extra)
|
|
}
|
|
cs := Diff(tmpl, actual)
|
|
|
|
if d := find(cs, "A a.example.com."); d == nil || d.Kind != InSync {
|
|
t.Fatalf("a should be InSync, got %+v", d)
|
|
}
|
|
if d := find(cs, "A b.example.com."); d == nil || d.Kind != Update {
|
|
t.Fatalf("b should be Update, got %+v", d)
|
|
}
|
|
if d := find(cs, "A c.example.com."); d == nil || d.Kind != Add {
|
|
t.Fatalf("c should be Add, got %+v", d)
|
|
}
|
|
if d := find(cs, "A d.example.com."); d == nil || d.Kind != Delete {
|
|
t.Fatalf("d should be Delete, got %+v", d)
|
|
}
|
|
}
|
|
|
|
func TestDiffMarksReadOnlyForNSSOA(t *testing.T) {
|
|
tmpl := []model.Record{{Type: model.NS, Name: "example.com.", TTL: 3600, Values: []string{"ns1.example.com."}}}
|
|
actual := []model.Record{{Type: model.NS, Name: "example.com.", TTL: 3600, Values: []string{"ns9.other.com."}}}
|
|
cs := Diff(tmpl, actual)
|
|
d := find(cs, "NS example.com.")
|
|
if d == nil || d.Kind != Update || !d.ReadOnly {
|
|
t.Fatalf("NS diff must be Update and ReadOnly, got %+v", d)
|
|
}
|
|
}
|
|
|
|
// 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
|
|
{Type: model.A, Name: "b.example.com.", TTL: 300, Values: []string{"2.2.2.2"}}, // add
|
|
{Type: model.NS, Name: "example.com.", TTL: 3600, Values: []string{"ns1.example.com."}}, // read-only add
|
|
}
|
|
actual := []model.Record{
|
|
{Type: model.A, Name: "a.example.com.", TTL: 300, Values: []string{"1.1.1.1"}},
|
|
}
|
|
act := Diff(tmpl, actual).Actionable()
|
|
if len(act) != 1 || act[0].Name != "b.example.com." {
|
|
t.Fatalf("only b.example.com. is actionable, got %+v", act)
|
|
}
|
|
}
|