package checker import ( "context" "encoding/json" "strings" "testing" sdk "git.happydns.org/checker-sdk-go/checker" ) // fakeObs round-trips through JSON like the production read path so tests // catch any tag drift between DNSSECData fields and rule expectations. type fakeObs struct{ data *DNSSECData } func (f fakeObs) Get(_ context.Context, _ sdk.ObservationKey, dest any) error { if f.data == nil { return nil } raw, err := json.Marshal(f.data) if err != nil { return err } return json.Unmarshal(raw, dest) } func (fakeObs) GetRelated(_ context.Context, _ sdk.ObservationKey) ([]sdk.RelatedObservation, error) { return nil, nil } func run(r sdk.CheckRule, data *DNSSECData, opts sdk.CheckerOptions) []sdk.CheckState { return r.Evaluate(context.Background(), fakeObs{data: data}, opts) } func signedZone(denial DenialKind, p *NSEC3ParamObservation) *DNSSECData { return &DNSSECData{ Domain: "example.com", Servers: map[string]PerServerView{ "ns1.example.com.:53": { Server: "ns1.example.com.:53", DNSKEYs: []DNSKEYRecord{{Flags: 257, Algorithm: 13, KeyTag: 12345, IsKSK: true}}, NSEC3PARAM: p, DenialKind: denial, }, }, } } func wantStatus(t *testing.T, states []sdk.CheckState, want sdk.Status) { t.Helper() if len(states) == 0 { t.Fatalf("no states returned") } if states[0].Status != want { t.Fatalf("status = %v, want %v: %+v", states[0].Status, want, states[0]) } } func TestDenialUsesNSEC3(t *testing.T) { cases := []struct { name string data *DNSSECData want sdk.Status }{ { name: "NSEC zone is walkable -> WARN", data: signedZone(DenialNSEC, nil), want: sdk.StatusWarn, }, { name: "NSEC3 zone -> OK", data: signedZone(DenialNSEC3, &NSEC3ParamObservation{Iterations: 0}), want: sdk.StatusOK, }, { name: "OPT-OUT zone -> OK", data: signedZone(DenialOptOut, &NSEC3ParamObservation{Iterations: 0, Flags: 1}), want: sdk.StatusOK, }, { name: "Unsigned zone -> INFO", data: &DNSSECData{Domain: "x", Servers: map[string]PerServerView{ "ns1:53": {Server: "ns1:53", DenialKind: DenialNone}, }}, want: sdk.StatusInfo, }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { wantStatus(t, run(denialUsesNSEC3Rule{}, tc.data, nil), tc.want) }) } } func TestNSEC3Iterations(t *testing.T) { cases := []struct { name string iter uint16 opts sdk.CheckerOptions want sdk.Status }{ {"iter=0 -> OK", 0, nil, sdk.StatusOK}, {"iter=1 default ceiling 0 -> WARN", 1, nil, sdk.StatusWarn}, {"iter=10 default ceiling 0 -> WARN", 10, nil, sdk.StatusWarn}, {"iter=10 ceiling 100 -> OK", 10, sdk.CheckerOptions{"nsec3IterationsMax": float64(100)}, sdk.StatusOK}, {"iter=10 severity=crit -> CRIT", 10, sdk.CheckerOptions{"nsec3IterationsSeverity": "crit"}, sdk.StatusCrit}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { data := signedZone(DenialNSEC3, &NSEC3ParamObservation{Iterations: tc.iter}) wantStatus(t, run(nsec3IterationsRule{}, data, tc.opts), tc.want) }) } } func TestNSEC3SaltEmpty(t *testing.T) { cases := []struct { name string saltLength uint8 want sdk.Status }{ {"empty salt -> OK", 0, sdk.StatusOK}, {"non-empty salt -> WARN", 8, sdk.StatusWarn}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { data := signedZone(DenialNSEC3, &NSEC3ParamObservation{ Iterations: 0, SaltLength: tc.saltLength, Salt: strings.Repeat("ab", int(tc.saltLength)), }) wantStatus(t, run(nsec3SaltEmptyRule{}, data, nil), tc.want) }) } } func TestNSEC3OptOut(t *testing.T) { cases := []struct { name string flags uint8 want sdk.Status }{ {"OPT-OUT off -> OK", 0, sdk.StatusOK}, {"OPT-OUT on -> INFO", 1, sdk.StatusInfo}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { data := signedZone(DenialNSEC3, &NSEC3ParamObservation{Iterations: 0, Flags: tc.flags}) wantStatus(t, run(nsec3OptOutRule{}, data, nil), tc.want) }) } } func TestDenialConsistent(t *testing.T) { consistent := &DNSSECData{ Domain: "x", Servers: map[string]PerServerView{ "ns1:53": {Server: "ns1:53", DenialKind: DenialNSEC3}, "ns2:53": {Server: "ns2:53", DenialKind: DenialNSEC3}, }, } wantStatus(t, run(denialConsistentRule{}, consistent, nil), sdk.StatusOK) drifting := &DNSSECData{ Domain: "x", Servers: map[string]PerServerView{ "ns1:53": {Server: "ns1:53", DenialKind: DenialNSEC}, "ns2:53": {Server: "ns2:53", DenialKind: DenialNSEC3}, }, } wantStatus(t, run(denialConsistentRule{}, drifting, nil), sdk.StatusWarn) } func TestRoundTripJSON(t *testing.T) { d := signedZone(DenialNSEC3, &NSEC3ParamObservation{Iterations: 0, SaltLength: 0}) raw, err := json.Marshal(d) if err != nil { t.Fatal(err) } var back DNSSECData if err := json.Unmarshal(raw, &back); err != nil { t.Fatal(err) } if back.Domain != d.Domain { t.Fatalf("domain round-trip lost: %q vs %q", back.Domain, d.Domain) } if got := back.Servers["ns1.example.com.:53"].DenialKind; got != DenialNSEC3 { t.Fatalf("denial round-trip lost: %v", got) } }