package rfc2136 import ( "testing" "github.com/miekg/dns" ) func mustRR(t *testing.T, s string) dns.RR { t.Helper() rr, err := dns.NewRR(s) if err != nil { t.Fatalf("failed to parse RR %q: %v", s, err) } return rr } func TestStore_AddLookup(t *testing.T) { s := newStore() s.Add(mustRR(t, `foo.example.com. 60 IN TXT "token-1"`)) got := s.Lookup("foo.example.com.", dns.TypeTXT) if len(got) != 1 { t.Fatalf("Lookup TXT: got %d records, want 1", len(got)) } if got[0].String() != `foo.example.com. 60 IN TXT "token-1"` { t.Errorf("unexpected RR: %s", got[0].String()) } } func TestStore_AddMultipleRRsetEntries(t *testing.T) { s := newStore() s.Add(mustRR(t, `foo.example.com. 60 IN TXT "token-1"`)) s.Add(mustRR(t, `foo.example.com. 60 IN TXT "token-2"`)) got := s.Lookup("foo.example.com.", dns.TypeTXT) if len(got) != 2 { t.Errorf("RRset size = %d, want 2 (both TXT values)", len(got)) } } func TestStore_AddDedupes(t *testing.T) { s := newStore() s.Add(mustRR(t, `foo.example.com. 60 IN TXT "same"`)) s.Add(mustRR(t, `foo.example.com. 60 IN TXT "same"`)) got := s.Lookup("foo.example.com.", dns.TypeTXT) if len(got) != 1 { t.Errorf("RRset size = %d, want 1 (duplicate ignored)", len(got)) } } func TestStore_LookupCaseInsensitive(t *testing.T) { s := newStore() s.Add(mustRR(t, `FOO.example.com. 60 IN TXT "token"`)) if got := s.Lookup("foo.EXAMPLE.com.", dns.TypeTXT); len(got) != 1 { t.Errorf("case-insensitive lookup failed: got %d", len(got)) } } func TestStore_LookupMissingNameReturnsNil(t *testing.T) { s := newStore() if got := s.Lookup("nope.example.com.", dns.TypeTXT); got != nil { t.Errorf("expected nil for missing name, got %v", got) } } func TestStore_LookupNameExistsWrongTypeReturnsNil(t *testing.T) { s := newStore() s.Add(mustRR(t, `foo.example.com. 60 IN A 192.0.2.1`)) if got := s.Lookup("foo.example.com.", dns.TypeTXT); got != nil { t.Errorf("expected nil for wrong type (A exists but not TXT), got %v", got) } if !s.NameExists("foo.example.com.") { t.Errorf("NameExists should return true (A record exists)") } } func TestStore_RemoveRRset(t *testing.T) { s := newStore() s.Add(mustRR(t, `foo.example.com. 60 IN TXT "a"`)) s.Add(mustRR(t, `foo.example.com. 60 IN TXT "b"`)) s.Add(mustRR(t, `foo.example.com. 60 IN A 192.0.2.1`)) s.RemoveRRset("foo.example.com.", dns.TypeTXT) if got := s.Lookup("foo.example.com.", dns.TypeTXT); got != nil { t.Errorf("TXT RRset should be gone, got %v", got) } if got := s.Lookup("foo.example.com.", dns.TypeA); len(got) != 1 { t.Errorf("A record should survive RRset deletion, got %v", got) } if !s.NameExists("foo.example.com.") { t.Errorf("name should still exist (A remains)") } } func TestStore_RemoveRRsetReapsEmptyName(t *testing.T) { s := newStore() s.Add(mustRR(t, `foo.example.com. 60 IN TXT "a"`)) s.RemoveRRset("foo.example.com.", dns.TypeTXT) if s.NameExists("foo.example.com.") { t.Errorf("name should have been reaped after last RRset removed") } } func TestStore_RemoveRR(t *testing.T) { s := newStore() s.Add(mustRR(t, `foo.example.com. 60 IN TXT "keep"`)) s.Add(mustRR(t, `foo.example.com. 60 IN TXT "drop"`)) s.RemoveRR(mustRR(t, `foo.example.com. 60 IN TXT "drop"`)) got := s.Lookup("foo.example.com.", dns.TypeTXT) if len(got) != 1 { t.Fatalf("RRset size after RemoveRR = %d, want 1", len(got)) } if got[0].(*dns.TXT).Txt[0] != "keep" { t.Errorf("wrong RR remained: %v", got[0]) } } func TestStore_RemoveName(t *testing.T) { s := newStore() s.Add(mustRR(t, `foo.example.com. 60 IN TXT "a"`)) s.Add(mustRR(t, `foo.example.com. 60 IN A 192.0.2.1`)) s.RemoveName("foo.example.com.") if s.NameExists("foo.example.com.") { t.Errorf("name should be gone after RemoveName") } } func TestStore_GenerationBumpsOnMutation(t *testing.T) { s := newStore() start := s.generation() s.Add(mustRR(t, `foo.example.com. 60 IN TXT "a"`)) if s.generation() != start+1 { t.Errorf("generation after Add: %d, want %d", s.generation(), start+1) } // Re-adding the same RR is a no-op → generation must NOT bump. s.Add(mustRR(t, `foo.example.com. 60 IN TXT "a"`)) if s.generation() != start+1 { t.Errorf("generation after duplicate Add: %d, want %d (no bump)", s.generation(), start+1) } s.RemoveRRset("foo.example.com.", dns.TypeTXT) if s.generation() != start+2 { t.Errorf("generation after RemoveRRset: %d, want %d", s.generation(), start+2) } // Removing again is a no-op → no bump. s.RemoveRRset("foo.example.com.", dns.TypeTXT) if s.generation() != start+2 { t.Errorf("generation after no-op RemoveRRset: %d, want %d", s.generation(), start+2) } } func TestStore_LookupReturnsCopy(t *testing.T) { // The returned slice must be a copy so external mutations don't // affect store state. s := newStore() s.Add(mustRR(t, `foo.example.com. 60 IN TXT "original"`)) got := s.Lookup("foo.example.com.", dns.TypeTXT) got[0] = mustRR(t, `foo.example.com. 60 IN TXT "tampered"`) // Re-lookup should still see the original. again := s.Lookup("foo.example.com.", dns.TypeTXT) if again[0].(*dns.TXT).Txt[0] != "original" { t.Errorf("Lookup returned shared slice — store corrupted by external mutation: %v", again[0]) } }