package checker import ( "context" "encoding/json" "strings" "testing" "time" "github.com/miekg/dns" sdk "git.happydns.org/checker-sdk-go/checker" ) // fakeObs is a tiny ObservationGetter that serves a single pre-built // DelegationData payload for the delegation key. type fakeObs struct { data *DelegationData err error } func (f *fakeObs) Get(_ context.Context, key sdk.ObservationKey, dest any) error { if f.err != nil { return f.err } if key != ObservationKeyDelegation { return &errString{"unexpected key " + string(key)} } raw, err := json.Marshal(f.data) if err != nil { return err } return json.Unmarshal(raw, dest) } func (f *fakeObs) GetRelated(_ context.Context, _ sdk.ObservationKey) ([]sdk.RelatedObservation, error) { return nil, nil } type errString struct{ s string } func (e *errString) Error() string { return e.s } // statusByCode indexes states by Code, asserting one state per code. func statusByCode(t *testing.T, states []sdk.CheckState) map[string]sdk.CheckState { t.Helper() out := map[string]sdk.CheckState{} for _, s := range states { out[s.Code+"|"+s.Subject] = s } return out } func evalRule(t *testing.T, r sdk.CheckRule, data *DelegationData, opts sdk.CheckerOptions) []sdk.CheckState { t.Helper() if opts == nil { opts = sdk.CheckerOptions{} } return r.Evaluate(context.Background(), &fakeObs{data: data}, opts) } func TestMinNameServersRule(t *testing.T) { r := &minNameServersRule{} t.Run("warn when below default minimum", func(t *testing.T) { states := evalRule(t, r, &DelegationData{DeclaredNS: []string{"a."}}, nil) if len(states) != 1 || states[0].Status != sdk.StatusWarn { t.Fatalf("want one Warn state, got %+v", states) } }) t.Run("ok when at minimum", func(t *testing.T) { states := evalRule(t, r, &DelegationData{DeclaredNS: []string{"a.", "b."}}, nil) if len(states) != 1 || states[0].Status != sdk.StatusOK { t.Fatalf("want one OK state, got %+v", states) } }) t.Run("respects custom minimum", func(t *testing.T) { opts := sdk.CheckerOptions{"minNameServers": float64(3)} states := evalRule(t, r, &DelegationData{DeclaredNS: []string{"a.", "b."}}, opts) if states[0].Status != sdk.StatusWarn { t.Fatalf("want Warn with min=3 and 2 NS, got %+v", states) } }) } func TestParentDiscoveredRule(t *testing.T) { r := &parentDiscoveredRule{} cases := []struct { name string data *DelegationData want sdk.Status }{ {"discovery error", &DelegationData{ParentDiscoveryError: "boom"}, sdk.StatusCrit}, {"no parent ns", &DelegationData{}, sdk.StatusCrit}, {"ok", &DelegationData{ParentNS: []string{"1.2.3.4:53"}}, sdk.StatusOK}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { states := evalRule(t, r, tc.data, nil) if len(states) != 1 || states[0].Status != tc.want { t.Fatalf("want %v, got %+v", tc.want, states) } }) } } func TestNSMatchesDeclaredRule(t *testing.T) { r := &nsMatchesDeclaredRule{} data := &DelegationData{ DelegatedFQDN: "www.example.com.", DeclaredNS: []string{"ns1.example.net.", "ns2.example.net."}, ParentViews: []ParentView{ {Server: "p1:53", NS: []string{"ns1.example.net.", "ns2.example.net."}}, // match {Server: "p2:53", NS: []string{"ns1.example.net.", "ns3.example.net."}}, // mismatch {Server: "p3:53", UDPNSError: "timeout"}, // skipped }, } states := evalRule(t, r, data, nil) idx := statusByCode(t, states) if s := idx["delegation_ns_mismatch|p1:53"]; s.Status != sdk.StatusOK { t.Errorf("p1: want OK, got %+v", s) } if s := idx["delegation_ns_mismatch|p2:53"]; s.Status != sdk.StatusCrit { t.Errorf("p2: want Crit, got %+v", s) } if _, ok := idx["delegation_ns_mismatch|p3:53"]; ok { t.Errorf("p3 should be skipped, got %+v", idx) } } func TestInBailiwickGlueRule(t *testing.T) { r := &inBailiwickGlueRule{} data := &DelegationData{ DelegatedFQDN: "example.com.", ParentViews: []ParentView{{ Server: "p:53", NS: []string{"ns1.example.com.", "ns2.elsewhere.net."}, Glue: map[string][]string{ "ns1.example.com.": {"192.0.2.1"}, }, }}, } states := evalRule(t, r, data, nil) var sawOK, sawMissing, sawOOB bool for _, s := range states { switch { case strings.HasPrefix(s.Subject, "ns1.example.com."): if s.Status == sdk.StatusOK { sawOK = true } case strings.HasPrefix(s.Subject, "ns2.elsewhere.net."): sawOOB = true // out-of-bailiwick: rule must not emit a state for it } if s.Status == sdk.StatusCrit { sawMissing = true } } if !sawOK { t.Error("expected OK state for in-bailiwick NS with glue") } if sawMissing { t.Error("did not expect Crit (no in-bailiwick NS is missing glue)") } if sawOOB { t.Error("out-of-bailiwick NS must be ignored by inBailiwickGlueRule") } } func TestUnnecessaryGlueRule(t *testing.T) { r := &unnecessaryGlueRule{} data := &DelegationData{ DelegatedFQDN: "example.com.", ParentViews: []ParentView{{ Server: "p:53", NS: []string{"ns1.elsewhere.net."}, Glue: map[string][]string{"ns1.elsewhere.net.": {"192.0.2.5"}}, }}, } states := evalRule(t, r, data, nil) if len(states) != 1 || states[0].Status != sdk.StatusWarn { t.Fatalf("want single Warn, got %+v", states) } } func TestDSPresentAtParentRule_RequireDS(t *testing.T) { r := &dsPresentAtParentRule{} data := &DelegationData{ DeclaredDS: []DSRecord{{KeyTag: 1, Algorithm: 8, DigestType: 2, Digest: "AAAA"}}, ParentViews: []ParentView{{Server: "p:53"}}, // no DS at parent } t.Run("default is informational", func(t *testing.T) { states := evalRule(t, r, data, nil) if states[0].Status != sdk.StatusInfo { t.Fatalf("want Info, got %+v", states) } }) t.Run("requireDS escalates to Crit", func(t *testing.T) { states := evalRule(t, r, data, sdk.CheckerOptions{"requireDS": true}) if states[0].Status != sdk.StatusCrit { t.Fatalf("want Crit with requireDS, got %+v", states) } }) } func TestChildAuthoritativeRule(t *testing.T) { r := &childAuthoritativeRule{} data := &DelegationData{ Children: []ChildNSView{{ NSName: "ns1.example.com.", Addresses: []ChildAddressView{ {Address: "192.0.2.1", Authoritative: true}, {Address: "192.0.2.2", Authoritative: false}, {Address: "192.0.2.3", UDPError: "timeout"}, // skipped }, }}, } states := evalRule(t, r, data, nil) if len(states) != 2 { t.Fatalf("want 2 states (skip the UDP failure), got %d: %+v", len(states), states) } var foundCrit bool for _, s := range states { if s.Status == sdk.StatusCrit { foundCrit = true } } if !foundCrit { t.Error("expected at least one Crit (the lame address)") } } func TestChildSOASerialDriftRule(t *testing.T) { r := &childSOASerialDriftRule{} data := &DelegationData{ Children: []ChildNSView{{ NSName: "ns1.example.com.", Addresses: []ChildAddressView{ {Address: "192.0.2.1", SOASerial: 1, SOASerialKnown: true}, {Address: "192.0.2.2", SOASerial: 2, SOASerialKnown: true}, }, }, { NSName: "ns2.example.com.", Addresses: []ChildAddressView{ {Address: "192.0.2.3", SOASerial: 7, SOASerialKnown: true}, {Address: "192.0.2.4", SOASerial: 7, SOASerialKnown: true}, }, }}, } states := evalRule(t, r, data, nil) if len(states) != 2 { t.Fatalf("want 2 states, got %d", len(states)) } bySubject := map[string]sdk.Status{} for _, s := range states { bySubject[s.Subject] = s.Status } if bySubject["ns1.example.com."] != sdk.StatusWarn { t.Errorf("ns1 drift: want Warn, got %v", bySubject["ns1.example.com."]) } if bySubject["ns2.example.com."] != sdk.StatusOK { t.Errorf("ns2 agreement: want OK, got %v", bySubject["ns2.example.com."]) } } func TestChildTCPRule_OptionToggle(t *testing.T) { r := &childTCPRule{} data := &DelegationData{ Children: []ChildNSView{{ NSName: "ns1.example.com.", Addresses: []ChildAddressView{ {Address: "192.0.2.1", TCPError: "connection refused"}, }, }}, } t.Run("default requireTCP=true → Crit", func(t *testing.T) { states := evalRule(t, r, data, nil) if states[0].Status != sdk.StatusCrit { t.Fatalf("want Crit, got %+v", states) } }) t.Run("requireTCP=false → Warn", func(t *testing.T) { states := evalRule(t, r, data, sdk.CheckerOptions{"requireTCP": false}) if states[0].Status != sdk.StatusWarn { t.Fatalf("want Warn, got %+v", states) } }) } func TestChildGlueMatchesParentRule(t *testing.T) { r := &childGlueMatchesParentRule{} data := &DelegationData{ DelegatedFQDN: "example.com.", ParentViews: []ParentView{{ Server: "p:53", NS: []string{"ns1.example.com."}, Glue: map[string][]string{"ns1.example.com.": {"192.0.2.1", "192.0.2.2"}}, }}, Children: []ChildNSView{{ NSName: "ns1.example.com.", Addresses: []ChildAddressView{ {Address: "192.0.2.1", ChildGlueAddrs: []string{"192.0.2.1"}}, // missing .2 → mismatch }, }}, } t.Run("default → Crit", func(t *testing.T) { states := evalRule(t, r, data, nil) if states[0].Status != sdk.StatusCrit { t.Fatalf("want Crit, got %+v", states) } }) t.Run("allowGlueMismatch → Warn", func(t *testing.T) { states := evalRule(t, r, data, sdk.CheckerOptions{"allowGlueMismatch": true}) if states[0].Status != sdk.StatusWarn { t.Fatalf("want Warn, got %+v", states) } }) } func TestNSHasAuthoritativeAnswerRule(t *testing.T) { r := &nsHasAuthoritativeAnswerRule{} data := &DelegationData{ Children: []ChildNSView{ { NSName: "ok.example.com.", Addresses: []ChildAddressView{ {Address: "192.0.2.1", Authoritative: false}, {Address: "192.0.2.2", Authoritative: true}, }, }, { NSName: "lame.example.com.", Addresses: []ChildAddressView{ {Address: "192.0.2.3", Authoritative: false}, }, }, }, } states := evalRule(t, r, data, nil) bySubject := map[string]sdk.Status{} for _, s := range states { bySubject[s.Subject] = s.Status } if bySubject["ok.example.com."] != sdk.StatusOK { t.Errorf("ok.example.com.: want OK, got %v", bySubject["ok.example.com."]) } if bySubject["lame.example.com."] != sdk.StatusCrit { t.Errorf("lame.example.com.: want Crit, got %v", bySubject["lame.example.com."]) } } func TestDNSKEYMatchesDSRule_Match(t *testing.T) { // Build a key, derive its DS, and verify the rule passes when child serves // that key and parent serves that DS. key := &dns.DNSKEY{ Hdr: dns.RR_Header{Name: "example.com.", Rrtype: dns.TypeDNSKEY, Class: dns.ClassINET}, Flags: 257, Protocol: 3, Algorithm: dns.RSASHA256, PublicKey: "AwEAAcMnWBKLuvG/LwnPVykcmpvnntwxfshHlHRhlY0F3oz8AMcuF8gw" + "2Ge56vG9oqVxTzHl4Ss2dEqCQOjFlOVo+pa3JwIO1lUzbQ==", } ds := key.ToDS(dns.SHA256) if ds == nil { t.Fatal("derive DS") } data := &DelegationData{ ParentViews: []ParentView{{Server: "p:53", DS: []DSRecord{NewDSRecord(ds)}}}, Children: []ChildNSView{{ NSName: "ns1.example.com.", Addresses: []ChildAddressView{ {Address: "192.0.2.1", DNSKEYs: []DNSKEYRecord{NewDNSKEYRecord(key)}}, }, }}, } r := &dnskeyMatchesDSRule{} states := evalRule(t, r, data, nil) if len(states) != 1 || states[0].Status != sdk.StatusOK { t.Fatalf("want OK match, got %+v", states) } } func TestDNSKEYMatchesDSRule_NoMatch(t *testing.T) { key := &dns.DNSKEY{ Hdr: dns.RR_Header{Name: "example.com.", Rrtype: dns.TypeDNSKEY, Class: dns.ClassINET}, Flags: 257, Protocol: 3, Algorithm: dns.RSASHA256, PublicKey: "AwEAAcMnWBKLuvG/LwnPVykcmpvnntwxfshHlHRhlY0F3oz8AMcuF8gw" + "2Ge56vG9oqVxTzHl4Ss2dEqCQOjFlOVo+pa3JwIO1lUzbQ==", } bogus := &dns.DS{KeyTag: 1, Algorithm: 8, DigestType: 2, Digest: "00"} data := &DelegationData{ ParentViews: []ParentView{{Server: "p:53", DS: []DSRecord{NewDSRecord(bogus)}}}, Children: []ChildNSView{{ NSName: "ns1.example.com.", Addresses: []ChildAddressView{ {Address: "192.0.2.1", DNSKEYs: []DNSKEYRecord{NewDNSKEYRecord(key)}}, }, }}, } states := evalRule(t, (&dnskeyMatchesDSRule{}), data, nil) if len(states) != 1 || states[0].Status != sdk.StatusCrit { t.Fatalf("want Crit, got %+v", states) } } func TestRulesReturnsAllRules(t *testing.T) { rules := Rules() if len(rules) == 0 { t.Fatal("expected at least one rule") } // Every rule must have a non-empty name and description, and must be // safely evaluable against an empty DelegationData (no panics). seen := map[string]bool{} for _, r := range rules { if r.Name() == "" { t.Errorf("rule %T has empty name", r) } if r.Description() == "" { t.Errorf("rule %s has empty description", r.Name()) } if seen[r.Name()] { t.Errorf("duplicate rule name: %s", r.Name()) } seen[r.Name()] = true states := r.Evaluate(context.Background(), &fakeObs{data: &DelegationData{}}, sdk.CheckerOptions{}) if len(states) == 0 { t.Errorf("rule %s returned no states for empty data", r.Name()) } } } func TestLoadDataPropagatesError(t *testing.T) { r := &minNameServersRule{} states := r.Evaluate(context.Background(), &fakeObs{err: &errString{"boom"}}, sdk.CheckerOptions{}) if len(states) != 1 || states[0].Status != sdk.StatusError { t.Fatalf("want single Error state, got %+v", states) } } // ------------------------- parentNSQueryRule ------------------------- func TestParentNSQueryRule(t *testing.T) { r := &parentNSQueryRule{} t.Run("no parent views → Unknown", func(t *testing.T) { states := evalRule(t, r, &DelegationData{}, nil) if len(states) != 1 || states[0].Status != sdk.StatusUnknown { t.Fatalf("want Unknown, got %+v", states) } }) t.Run("UDP error → Crit", func(t *testing.T) { data := &DelegationData{ ParentViews: []ParentView{{Server: "p:53", UDPNSError: "timeout"}}, } states := evalRule(t, r, data, nil) if len(states) != 1 || states[0].Status != sdk.StatusCrit { t.Fatalf("want Crit, got %+v", states) } }) t.Run("empty NS RRset → Crit", func(t *testing.T) { data := &DelegationData{ ParentViews: []ParentView{{Server: "p:53", NS: []string{}}}, } states := evalRule(t, r, data, nil) if len(states) != 1 || states[0].Status != sdk.StatusCrit { t.Fatalf("want Crit, got %+v", states) } }) t.Run("OK when NS records returned", func(t *testing.T) { data := &DelegationData{ ParentViews: []ParentView{{Server: "p:53", NS: []string{"ns1.example.com."}}}, } states := evalRule(t, r, data, nil) if len(states) != 1 || states[0].Status != sdk.StatusOK { t.Fatalf("want OK, got %+v", states) } }) t.Run("per-server subjects", func(t *testing.T) { data := &DelegationData{ ParentViews: []ParentView{ {Server: "p1:53", NS: []string{"ns1.example.com."}}, {Server: "p2:53", UDPNSError: "refused"}, }, } states := evalRule(t, r, data, nil) if len(states) != 2 { t.Fatalf("want 2 states, got %d", len(states)) } idx := statusByCode(t, states) if idx["delegation_parent_query_failed|p1:53"].Status != sdk.StatusOK { t.Errorf("p1: want OK") } if idx["delegation_parent_query_failed|p2:53"].Status != sdk.StatusCrit { t.Errorf("p2: want Crit") } }) } // ------------------------- parentTCPRule ------------------------- func TestParentTCPRule(t *testing.T) { r := &parentTCPRule{} t.Run("no parent views → Unknown", func(t *testing.T) { states := evalRule(t, r, &DelegationData{}, nil) if states[0].Status != sdk.StatusUnknown { t.Fatalf("want Unknown, got %+v", states) } }) t.Run("TCP error with requireTCP=true → Crit", func(t *testing.T) { data := &DelegationData{ ParentViews: []ParentView{{Server: "p:53", TCPNSError: "refused"}}, } states := evalRule(t, r, data, nil) if states[0].Status != sdk.StatusCrit { t.Fatalf("want Crit, got %+v", states) } }) t.Run("TCP error with requireTCP=false → Warn", func(t *testing.T) { data := &DelegationData{ ParentViews: []ParentView{{Server: "p:53", TCPNSError: "refused"}}, } states := evalRule(t, r, data, sdk.CheckerOptions{"requireTCP": false}) if states[0].Status != sdk.StatusWarn { t.Fatalf("want Warn, got %+v", states) } }) t.Run("no TCP error → OK", func(t *testing.T) { data := &DelegationData{ ParentViews: []ParentView{{Server: "p:53"}}, } states := evalRule(t, r, data, nil) if states[0].Status != sdk.StatusOK { t.Fatalf("want OK, got %+v", states) } }) } // ------------------------- dsQueryRule ------------------------- func TestDSQueryRule(t *testing.T) { r := &dsQueryRule{} t.Run("no parent views → Unknown", func(t *testing.T) { states := evalRule(t, r, &DelegationData{}, nil) if states[0].Status != sdk.StatusUnknown { t.Fatalf("want Unknown, got %+v", states) } }) t.Run("DS query error → Warn", func(t *testing.T) { data := &DelegationData{ ParentViews: []ParentView{{Server: "p:53", DSQueryError: "timeout"}}, } states := evalRule(t, r, data, nil) if states[0].Status != sdk.StatusWarn { t.Fatalf("want Warn, got %+v", states) } }) t.Run("DS query OK with records", func(t *testing.T) { data := &DelegationData{ ParentViews: []ParentView{{Server: "p:53", DS: []DSRecord{{KeyTag: 1, Algorithm: 8, DigestType: 2, Digest: "AAAA"}}}}, } states := evalRule(t, r, data, nil) if states[0].Status != sdk.StatusOK { t.Fatalf("want OK, got %+v", states) } }) t.Run("DS query OK with empty answer", func(t *testing.T) { data := &DelegationData{ ParentViews: []ParentView{{Server: "p:53"}}, } states := evalRule(t, r, data, nil) if states[0].Status != sdk.StatusOK { t.Fatalf("want OK for NOERROR+empty, got %+v", states) } }) } // ------------------------- dsMatchesDeclaredRule ------------------------- func TestDSMatchesDeclaredRule(t *testing.T) { r := &dsMatchesDeclaredRule{} ds1 := DSRecord{KeyTag: 1, Algorithm: 8, DigestType: 2, Digest: "AAAA"} ds2 := DSRecord{KeyTag: 2, Algorithm: 8, DigestType: 2, Digest: "BBBB"} t.Run("no DS data to compare → Info", func(t *testing.T) { states := evalRule(t, r, &DelegationData{}, nil) if states[0].Status != sdk.StatusInfo { t.Fatalf("want Info, got %+v", states) } }) t.Run("DS RRset matches declared → OK", func(t *testing.T) { data := &DelegationData{ DeclaredDS: []DSRecord{ds1}, ParentViews: []ParentView{{Server: "p:53", DS: []DSRecord{ds1}}}, } states := evalRule(t, r, data, nil) if states[0].Status != sdk.StatusOK { t.Fatalf("want OK, got %+v", states) } }) t.Run("DS missing from parent → Crit when declared non-empty", func(t *testing.T) { data := &DelegationData{ DeclaredDS: []DSRecord{ds1, ds2}, ParentViews: []ParentView{{Server: "p:53", DS: []DSRecord{ds1}}}, } states := evalRule(t, r, data, nil) if states[0].Status != sdk.StatusCrit { t.Fatalf("want Crit, got %+v", states) } }) t.Run("extra DS at parent with no declared → Warn", func(t *testing.T) { data := &DelegationData{ DeclaredDS: []DSRecord{}, ParentViews: []ParentView{{Server: "p:53", DS: []DSRecord{ds1}}}, } states := evalRule(t, r, data, nil) if states[0].Status != sdk.StatusWarn { t.Fatalf("want Warn, got %+v", states) } }) t.Run("DS query error → view skipped", func(t *testing.T) { data := &DelegationData{ DeclaredDS: []DSRecord{ds1}, ParentViews: []ParentView{ {Server: "p1:53", DSQueryError: "timeout"}, {Server: "p2:53", DS: []DSRecord{ds1}}, }, } states := evalRule(t, r, data, nil) idx := statusByCode(t, states) if _, ok := idx["delegation_ds_mismatch|p1:53"]; ok { t.Error("p1 with DS query error must be skipped") } if idx["delegation_ds_mismatch|p2:53"].Status != sdk.StatusOK { t.Error("p2: want OK") } }) } // ------------------------- dsPresentAtParentRule ------------------------- func TestDSPresentAtParentRule(t *testing.T) { r := &dsPresentAtParentRule{} t.Run("no declared DS → Info", func(t *testing.T) { states := evalRule(t, r, &DelegationData{}, nil) if states[0].Status != sdk.StatusInfo { t.Fatalf("want Info, got %+v", states) } }) t.Run("declared DS and parent serves DS → OK", func(t *testing.T) { data := &DelegationData{ DeclaredDS: []DSRecord{{KeyTag: 1, Algorithm: 8, DigestType: 2, Digest: "AAAA"}}, ParentViews: []ParentView{{Server: "p:53", DS: []DSRecord{{KeyTag: 1, Algorithm: 8, DigestType: 2, Digest: "AAAA"}}}}, } states := evalRule(t, r, data, nil) if states[0].Status != sdk.StatusOK { t.Fatalf("want OK, got %+v", states) } }) } // ------------------------- dsRRSIGValidityRule ------------------------- func nowUint32() uint32 { return uint32(time.Now().UTC().Unix()) } func TestDSRRSIGValidityRule(t *testing.T) { r := &dsRRSIGValidityRule{} t.Run("no DS RRSIGs → Info", func(t *testing.T) { data := &DelegationData{ ParentViews: []ParentView{{Server: "p:53", DS: []DSRecord{{KeyTag: 1}}}}, } states := evalRule(t, r, data, nil) if states[0].Status != sdk.StatusInfo { t.Fatalf("want Info, got %+v", states) } }) t.Run("DS query error → view skipped", func(t *testing.T) { data := &DelegationData{ ParentViews: []ParentView{{Server: "p:53", DSQueryError: "timeout", DSRRSIGs: []DSRRSIGObservation{{ Inception: nowUint32() - 3600, Expiration: nowUint32() + 86400, }}}}, } states := evalRule(t, r, data, nil) if len(states) != 1 && states[0].Status != sdk.StatusInfo { t.Fatalf("view with DS query error must be skipped, got %+v", states) } }) t.Run("valid RRSIG → OK", func(t *testing.T) { now := nowUint32() data := &DelegationData{ ParentViews: []ParentView{{ Server: "p:53", DSRRSIGs: []DSRRSIGObservation{{ Inception: now - 3600, Expiration: now + 86400, }}, }}, } states := evalRule(t, r, data, nil) if states[0].Status != sdk.StatusOK { t.Fatalf("want OK, got %+v", states) } }) t.Run("expired RRSIG → Crit", func(t *testing.T) { now := nowUint32() data := &DelegationData{ ParentViews: []ParentView{{ Server: "p:53", DSRRSIGs: []DSRRSIGObservation{{ Inception: now - 172800, Expiration: now - 3600, }}, }}, } states := evalRule(t, r, data, nil) if states[0].Status != sdk.StatusCrit { t.Fatalf("want Crit for expired, got %+v", states) } if !strings.Contains(states[0].Message, "expired") { t.Errorf("message should mention 'expired', got %q", states[0].Message) } }) t.Run("not-yet-valid RRSIG → Crit", func(t *testing.T) { now := nowUint32() data := &DelegationData{ ParentViews: []ParentView{{ Server: "p:53", DSRRSIGs: []DSRRSIGObservation{{ Inception: now + 3600, Expiration: now + 172800, }}, }}, } states := evalRule(t, r, data, nil) if states[0].Status != sdk.StatusCrit { t.Fatalf("want Crit for not-yet-valid, got %+v", states) } if !strings.Contains(states[0].Message, "not yet valid") { t.Errorf("message should mention 'not yet valid', got %q", states[0].Message) } }) } // ------------------------- nsResolvableRule ------------------------- func TestNSResolvableRule(t *testing.T) { r := &nsResolvableRule{} t.Run("no out-of-bailiwick NS → Info", func(t *testing.T) { data := &DelegationData{ DelegatedFQDN: "example.com.", Children: []ChildNSView{{ NSName: "ns1.example.com.", // in-bailiwick, must be skipped }}, } states := evalRule(t, r, data, nil) if states[0].Status != sdk.StatusInfo { t.Fatalf("want Info, got %+v", states) } }) t.Run("resolve error → Crit", func(t *testing.T) { data := &DelegationData{ DelegatedFQDN: "example.com.", Children: []ChildNSView{{ NSName: "ns1.elsewhere.net.", ResolveError: "NXDOMAIN", }}, } states := evalRule(t, r, data, nil) if states[0].Status != sdk.StatusCrit { t.Fatalf("want Crit, got %+v", states) } }) t.Run("resolved → OK", func(t *testing.T) { data := &DelegationData{ DelegatedFQDN: "example.com.", Children: []ChildNSView{{ NSName: "ns1.elsewhere.net.", Addresses: []ChildAddressView{{Address: "192.0.2.1"}}, }}, } states := evalRule(t, r, data, nil) if states[0].Status != sdk.StatusOK { t.Fatalf("want OK, got %+v", states) } }) t.Run("in-bailiwick skipped even if resolve error set", func(t *testing.T) { data := &DelegationData{ DelegatedFQDN: "example.com.", Children: []ChildNSView{{ NSName: "ns1.example.com.", ResolveError: "NXDOMAIN", }}, } states := evalRule(t, r, data, nil) // In-bailiwick NS must be ignored; only Info state for "no out-of-bailiwick NS". if states[0].Status != sdk.StatusInfo { t.Fatalf("want Info (in-bailiwick skipped), got %+v", states) } }) } // ------------------------- childReachableRule ------------------------- func TestChildReachableRule(t *testing.T) { r := &childReachableRule{} t.Run("no child addresses → Unknown", func(t *testing.T) { states := evalRule(t, r, &DelegationData{}, nil) if states[0].Status != sdk.StatusUnknown { t.Fatalf("want Unknown, got %+v", states) } }) t.Run("UDP error → Crit", func(t *testing.T) { data := &DelegationData{ Children: []ChildNSView{{ NSName: "ns1.example.com.", Addresses: []ChildAddressView{{Address: "192.0.2.1", UDPError: "timeout"}}, }}, } states := evalRule(t, r, data, nil) if states[0].Status != sdk.StatusCrit { t.Fatalf("want Crit, got %+v", states) } }) t.Run("reachable → OK", func(t *testing.T) { data := &DelegationData{ Children: []ChildNSView{{ NSName: "ns1.example.com.", Addresses: []ChildAddressView{{Address: "192.0.2.1", Authoritative: true}}, }}, } states := evalRule(t, r, data, nil) if states[0].Status != sdk.StatusOK { t.Fatalf("want OK, got %+v", states) } }) } // ------------------------- childNSMatchesParentRule ------------------------- func TestChildNSMatchesParentRule(t *testing.T) { r := &childNSMatchesParentRule{} t.Run("no primary parent view → Unknown", func(t *testing.T) { data := &DelegationData{ ParentViews: []ParentView{{Server: "p:53", UDPNSError: "timeout"}}, } states := evalRule(t, r, data, nil) if states[0].Status != sdk.StatusUnknown { t.Fatalf("want Unknown, got %+v", states) } }) t.Run("child NS matches parent → OK", func(t *testing.T) { data := &DelegationData{ ParentViews: []ParentView{{ Server: "p:53", NS: []string{"ns1.example.com.", "ns2.example.com."}, }}, Children: []ChildNSView{{ NSName: "ns1.example.com.", Addresses: []ChildAddressView{{ Address: "192.0.2.1", ChildNS: []string{"ns1.example.com.", "ns2.example.com."}, }}, }}, } states := evalRule(t, r, data, nil) if states[0].Status != sdk.StatusOK { t.Fatalf("want OK, got %+v", states) } }) t.Run("child NS drift → Warn", func(t *testing.T) { data := &DelegationData{ ParentViews: []ParentView{{ Server: "p:53", NS: []string{"ns1.example.com.", "ns2.example.com."}, }}, Children: []ChildNSView{{ NSName: "ns1.example.com.", Addresses: []ChildAddressView{{ Address: "192.0.2.1", ChildNS: []string{"ns1.example.com.", "ns3.example.com."}, }}, }}, } states := evalRule(t, r, data, nil) if states[0].Status != sdk.StatusWarn { t.Fatalf("want Warn, got %+v", states) } }) t.Run("UDP error or NS query error → view skipped", func(t *testing.T) { data := &DelegationData{ ParentViews: []ParentView{{ Server: "p:53", NS: []string{"ns1.example.com."}, }}, Children: []ChildNSView{{ NSName: "ns1.example.com.", Addresses: []ChildAddressView{ {Address: "192.0.2.1", UDPError: "timeout"}, {Address: "192.0.2.2", ChildNSError: "refused"}, }, }}, } states := evalRule(t, r, data, nil) // Both addresses must be skipped → Unknown if states[0].Status != sdk.StatusUnknown { t.Fatalf("want Unknown (all addresses skipped), got %+v", states) } }) } // ------------------------- dnskeyQueryRule ------------------------- func TestDNSKEYQueryRule(t *testing.T) { r := &dnskeyQueryRule{} t.Run("no parent DS → Unknown", func(t *testing.T) { data := &DelegationData{ ParentViews: []ParentView{{Server: "p:53"}}, Children: []ChildNSView{{NSName: "ns1.example.com.", Addresses: []ChildAddressView{{Address: "192.0.2.1"}}}}, } states := evalRule(t, r, data, nil) if states[0].Status != sdk.StatusUnknown { t.Fatalf("want Unknown when no parent DS, got %+v", states) } }) t.Run("DNSKEY query error → Warn", func(t *testing.T) { data := &DelegationData{ ParentViews: []ParentView{{Server: "p:53", DS: []DSRecord{{KeyTag: 1}}}}, Children: []ChildNSView{{ NSName: "ns1.example.com.", Addresses: []ChildAddressView{{Address: "192.0.2.1", DNSKEYError: "timeout"}}, }}, } states := evalRule(t, r, data, nil) if states[0].Status != sdk.StatusWarn { t.Fatalf("want Warn, got %+v", states) } }) t.Run("DNSKEY query succeeds → OK", func(t *testing.T) { key := &dns.DNSKEY{ Hdr: dns.RR_Header{Name: "example.com.", Rrtype: dns.TypeDNSKEY, Class: dns.ClassINET}, Flags: 257, Protocol: 3, Algorithm: dns.RSASHA256, PublicKey: "AwEAAcMnWBKLuvG/LwnPVykcmpvnntwxfshHlHRhlY0F3oz8AMcuF8gw2Ge56vG9oqVxTzHl4Ss2dEqCQOjFlOVo+pa3JwIO1lUzbQ==", } data := &DelegationData{ ParentViews: []ParentView{{Server: "p:53", DS: []DSRecord{{KeyTag: 1}}}}, Children: []ChildNSView{{ NSName: "ns1.example.com.", Addresses: []ChildAddressView{{Address: "192.0.2.1", DNSKEYs: []DNSKEYRecord{NewDNSKEYRecord(key)}}}, }}, } states := evalRule(t, r, data, nil) if states[0].Status != sdk.StatusOK { t.Fatalf("want OK, got %+v", states) } }) t.Run("UDP error → address skipped, Unknown if no usable address", func(t *testing.T) { data := &DelegationData{ ParentViews: []ParentView{{Server: "p:53", DS: []DSRecord{{KeyTag: 1}}}}, Children: []ChildNSView{{ NSName: "ns1.example.com.", Addresses: []ChildAddressView{{Address: "192.0.2.1", UDPError: "timeout"}}, }}, } states := evalRule(t, r, data, nil) if states[0].Status != sdk.StatusUnknown { t.Fatalf("want Unknown (UDP error skips address), got %+v", states) } }) } // ------------------------- dnskeyMatchesDSRule edge cases ------------------------- func TestDNSKEYMatchesDSRule_NoDSAtParent(t *testing.T) { data := &DelegationData{ ParentViews: []ParentView{{Server: "p:53"}}, // no DS Children: []ChildNSView{{ NSName: "ns1.example.com.", Addresses: []ChildAddressView{{Address: "192.0.2.1"}}, }}, } states := evalRule(t, (&dnskeyMatchesDSRule{}), data, nil) if states[0].Status != sdk.StatusUnknown { t.Fatalf("want Unknown when parent has no DS, got %+v", states) } } func TestDNSKEYMatchesDSRule_NoKeysObserved(t *testing.T) { data := &DelegationData{ ParentViews: []ParentView{{Server: "p:53", DS: []DSRecord{{KeyTag: 1}}}}, Children: []ChildNSView{{ NSName: "ns1.example.com.", Addresses: []ChildAddressView{{Address: "192.0.2.1", DNSKEYError: "timeout"}}, }}, } states := evalRule(t, (&dnskeyMatchesDSRule{}), data, nil) if states[0].Status != sdk.StatusUnknown { t.Fatalf("want Unknown when no DNSKEY observed, got %+v", states) } } // ------------------------- inBailiwickGlueRule missing-glue branch ------------------------- func TestInBailiwickGlueRule_MissingGlue(t *testing.T) { r := &inBailiwickGlueRule{} data := &DelegationData{ DelegatedFQDN: "example.com.", ParentViews: []ParentView{{ Server: "p:53", NS: []string{"ns1.example.com."}, Glue: map[string][]string{}, // no glue for in-bailiwick NS }}, } states := evalRule(t, r, data, nil) var sawCrit bool for _, s := range states { if s.Status == sdk.StatusCrit { sawCrit = true } } if !sawCrit { t.Error("want Crit when in-bailiwick NS has no glue") } } // ------------------------- unnecessaryGlueRule OK/Info branches ------------------------- func TestUnnecessaryGlueRule_NoExtraGlue(t *testing.T) { r := &unnecessaryGlueRule{} data := &DelegationData{ DelegatedFQDN: "example.com.", ParentViews: []ParentView{{ Server: "p:53", NS: []string{"ns1.elsewhere.net."}, Glue: map[string][]string{}, // out-of-bailiwick with no glue }}, } states := evalRule(t, r, data, nil) if len(states) != 1 || states[0].Status != sdk.StatusOK { t.Fatalf("want OK for out-of-bailiwick NS without glue, got %+v", states) } } func TestUnnecessaryGlueRule_NoOutOfBailiwickNS(t *testing.T) { r := &unnecessaryGlueRule{} data := &DelegationData{ DelegatedFQDN: "example.com.", ParentViews: []ParentView{{ Server: "p:53", NS: []string{"ns1.example.com."}, Glue: map[string][]string{"ns1.example.com.": {"192.0.2.1"}}, }}, } states := evalRule(t, r, data, nil) if len(states) != 1 || states[0].Status != sdk.StatusInfo { t.Fatalf("want Info when all NS are in-bailiwick, got %+v", states) } } // ------------------------- childGlueMatchesParentRule edge cases ------------------------- func TestChildGlueMatchesParentRule_NoPrimary(t *testing.T) { r := &childGlueMatchesParentRule{} data := &DelegationData{ DelegatedFQDN: "example.com.", ParentViews: []ParentView{{Server: "p:53", UDPNSError: "timeout"}}, } states := evalRule(t, r, data, nil) if states[0].Status != sdk.StatusUnknown { t.Fatalf("want Unknown when no primary parent view, got %+v", states) } } func TestChildGlueMatchesParentRule_OutOfBailiwickSkipped(t *testing.T) { r := &childGlueMatchesParentRule{} data := &DelegationData{ DelegatedFQDN: "example.com.", ParentViews: []ParentView{{ Server: "p:53", NS: []string{"ns1.elsewhere.net."}, Glue: map[string][]string{}, }}, Children: []ChildNSView{{ NSName: "ns1.elsewhere.net.", // out-of-bailiwick Addresses: []ChildAddressView{{Address: "192.0.2.1", ChildGlueAddrs: []string{"192.0.2.99"}}}, }}, } states := evalRule(t, r, data, nil) // No in-bailiwick NS → no states at all (rule stays silent). if len(states) != 0 { t.Fatalf("want no states for out-of-bailiwick NS, got %+v", states) } } // ------------------------- nsHasAuthoritativeAnswerRule no-children branch ------------------------- func TestNSHasAuthoritativeAnswerRule_NoChildren(t *testing.T) { r := &nsHasAuthoritativeAnswerRule{} states := evalRule(t, r, &DelegationData{}, nil) if states[0].Status != sdk.StatusUnknown { t.Fatalf("want Unknown when no children, got %+v", states) } } func TestNSHasAuthoritativeAnswerRule_NoAddresses(t *testing.T) { r := &nsHasAuthoritativeAnswerRule{} data := &DelegationData{ Children: []ChildNSView{{NSName: "ns1.example.com.", Addresses: nil}}, } // NS with no addresses is skipped by the rule. states := evalRule(t, r, data, nil) if states[0].Status != sdk.StatusUnknown { t.Fatalf("want Unknown when NS has no addresses, got %+v", states) } } // ------------------------- primaryParentView ------------------------- func TestPrimaryParentView(t *testing.T) { t.Run("nil when all views failed", func(t *testing.T) { views := []ParentView{ {Server: "p1:53", UDPNSError: "timeout"}, {Server: "p2:53", UDPNSError: "refused"}, } if primaryParentView(views) != nil { t.Error("want nil") } }) t.Run("nil when NS RRsets are empty", func(t *testing.T) { views := []ParentView{{Server: "p:53", NS: []string{}}} if primaryParentView(views) != nil { t.Error("want nil when NS slice is empty") } }) t.Run("returns first successful view", func(t *testing.T) { views := []ParentView{ {Server: "p1:53", UDPNSError: "timeout"}, {Server: "p2:53", NS: []string{"ns1.example.com."}}, {Server: "p3:53", NS: []string{"ns2.example.com."}}, } got := primaryParentView(views) if got == nil || got.Server != "p2:53" { t.Errorf("want p2:53, got %v", got) } }) t.Run("nil for empty slice", func(t *testing.T) { if primaryParentView(nil) != nil { t.Error("want nil for empty slice") } }) }