package checker import ( "context" "encoding/json" "strings" "testing" "time" sdk "git.happydns.org/checker-sdk-go/checker" ) func boolPtr(b bool) *bool { return &b } // fakeObs implements sdk.ObservationGetter against an in-memory map. type fakeObs struct { store map[string]any } func (f *fakeObs) Get(_ context.Context, key sdk.ObservationKey, dest any) error { v, ok := f.store[key] if !ok { return errFake("missing observation: " + key) } raw, err := json.Marshal(v) 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 errFake string func (e errFake) Error() string { return string(e) } // ── DNS rules ──────────────────────────────────────────────────────────────── func TestCheckDNSQueryFailed(t *testing.T) { if got := checkDNSQueryFailed(&EmailKeyData{}, nil); got != nil { t.Errorf("expected no issue, got %+v", got) } got := checkDNSQueryFailed(&EmailKeyData{DNSQueryError: "timeout"}, nil) if len(got) != 1 || got[0].Severity != sdk.StatusCrit { t.Errorf("expected one crit issue, got %+v", got) } } func TestCheckDNSNoRecord(t *testing.T) { // nil DNSAnswerPresent ⇒ no judgement. if got := checkDNSNoRecord(&EmailKeyData{}, nil); got != nil { t.Errorf("expected no issue when present is nil, got %+v", got) } // Present=true ⇒ no issue. if got := checkDNSNoRecord(&EmailKeyData{DNSAnswerPresent: boolPtr(true)}, nil); got != nil { t.Errorf("expected no issue when present, got %+v", got) } // Present=false ⇒ crit. got := checkDNSNoRecord(&EmailKeyData{Kind: KindSMIMEA, QueriedOwner: "x", DNSAnswerPresent: boolPtr(false)}, nil) if len(got) != 1 || got[0].Severity != sdk.StatusCrit || !strings.Contains(got[0].Message, "SMIMEA") { t.Errorf("unexpected: %+v", got) } } func TestCheckDNSSECNotValidated_Severity(t *testing.T) { d := &EmailKeyData{DNSSECSecure: boolPtr(false)} // Default: requireDNSSEC=true ⇒ crit. got := checkDNSSECNotValidated(d, sdk.CheckerOptions{}) if len(got) != 1 || got[0].Severity != sdk.StatusCrit { t.Errorf("default should be crit, got %+v", got) } // Override to false ⇒ warn. got = checkDNSSECNotValidated(d, sdk.CheckerOptions{OptionRequireDNSSEC: false}) if len(got) != 1 || got[0].Severity != sdk.StatusWarn { t.Errorf("opt-off should be warn, got %+v", got) } // Secure ⇒ no issue. got = checkDNSSECNotValidated(&EmailKeyData{DNSSECSecure: boolPtr(true)}, nil) if got != nil { t.Errorf("expected no issue, got %+v", got) } } func TestCheckOwnerHashMismatch(t *testing.T) { d := &EmailKeyData{Username: "alice", ExpectedOwnerPrefix: "abc", ObservedOwnerPrefix: "abc"} if got := checkOwnerHashMismatch(d, nil); got != nil { t.Errorf("matching prefixes should not issue, got %+v", got) } d.ObservedOwnerPrefix = "ABC" // case-insensitive if got := checkOwnerHashMismatch(d, nil); got != nil { t.Errorf("case-insensitive match should not issue, got %+v", got) } d.ObservedOwnerPrefix = "xyz" got := checkOwnerHashMismatch(d, nil) if len(got) != 1 || got[0].Severity != sdk.StatusCrit { t.Errorf("mismatch should crit, got %+v", got) } // Either prefix empty ⇒ skip silently. d.ObservedOwnerPrefix = "" if got := checkOwnerHashMismatch(d, nil); got != nil { t.Errorf("empty observed should skip, got %+v", got) } } // ── PGP rules ──────────────────────────────────────────────────────────────── func TestCheckPGPParseError(t *testing.T) { got := checkPGPParseError(&EmailKeyData{}, nil) if len(got) != 1 || !strings.Contains(got[0].Message, "no OPENPGPKEY") { t.Errorf("expected no-record issue, got %+v", got) } got = checkPGPParseError(&EmailKeyData{OpenPGP: &OpenPGPInfo{ParseError: "boom"}}, nil) if len(got) != 1 || got[0].Message != "boom" { t.Errorf("expected parse-error issue, got %+v", got) } if got := checkPGPParseError(&EmailKeyData{OpenPGP: &OpenPGPInfo{}}, nil); got != nil { t.Errorf("expected no issue, got %+v", got) } } func TestCheckPGPPrimaryExpired(t *testing.T) { past := time.Now().Add(-1 * time.Hour) d := &EmailKeyData{OpenPGP: &OpenPGPInfo{ExpiresAt: past}} got := checkPGPPrimaryExpired(d, nil) if len(got) != 1 || got[0].Severity != sdk.StatusCrit { t.Errorf("expected crit, got %+v", got) } d.OpenPGP.ExpiresAt = time.Now().Add(time.Hour) if got := checkPGPPrimaryExpired(d, nil); got != nil { t.Errorf("future expiry should not issue, got %+v", got) } } func TestCheckPGPPrimaryExpiring(t *testing.T) { soon := time.Now().Add(10 * 24 * time.Hour) d := &EmailKeyData{OpenPGP: &OpenPGPInfo{ExpiresAt: soon}} // Default 30-day window ⇒ warn. got := checkPGPPrimaryExpiring(d, sdk.CheckerOptions{}) if len(got) != 1 || got[0].Severity != sdk.StatusWarn { t.Errorf("expected warn, got %+v", got) } // Already expired ⇒ this rule does not fire (the expired rule does). d.OpenPGP.ExpiresAt = time.Now().Add(-time.Hour) if got := checkPGPPrimaryExpiring(d, sdk.CheckerOptions{}); got != nil { t.Errorf("expired key should not trigger expiring rule, got %+v", got) } // Disable via warnDays=0 ⇒ no issue. d.OpenPGP.ExpiresAt = soon if got := checkPGPPrimaryExpiring(d, sdk.CheckerOptions{OptionCertExpiryWarnDays: float64(0)}); got != nil { t.Errorf("warnDays=0 should disable, got %+v", got) } } func TestCheckPGPWeakKeySize(t *testing.T) { d := &EmailKeyData{OpenPGP: &OpenPGPInfo{PrimaryAlgorithm: "RSA", PrimaryBits: 1024}} got := checkPGPWeakKeySize(d, nil) if len(got) != 1 || got[0].Severity != sdk.StatusCrit { t.Errorf("1024-bit RSA should be crit, got %+v", got) } d.OpenPGP.PrimaryBits = 2048 got = checkPGPWeakKeySize(d, nil) if len(got) != 1 || got[0].Severity != sdk.StatusWarn { t.Errorf("2048-bit RSA should be warn, got %+v", got) } d.OpenPGP.PrimaryBits = 4096 if got := checkPGPWeakKeySize(d, nil); got != nil { t.Errorf("4096-bit RSA should pass, got %+v", got) } // Non-RSA ⇒ skip. d.OpenPGP.PrimaryAlgorithm = "Ed25519" d.OpenPGP.PrimaryBits = 256 if got := checkPGPWeakKeySize(d, nil); got != nil { t.Errorf("Ed25519 should skip, got %+v", got) } } func TestCheckPGPRecordTooLarge(t *testing.T) { d := &EmailKeyData{OpenPGP: &OpenPGPInfo{RawSize: pgpMaxRecordBytes + 1}} got := checkPGPRecordTooLarge(d, nil) if len(got) != 1 { t.Errorf("expected one issue, got %+v", got) } d.OpenPGP.RawSize = pgpMaxRecordBytes if got := checkPGPRecordTooLarge(d, nil); got != nil { t.Errorf("at-limit should pass, got %+v", got) } } func TestCheckPGPUIDMismatch(t *testing.T) { d := &EmailKeyData{Username: "alice", OpenPGP: &OpenPGPInfo{MatchesUsername: boolPtr(false)}} got := checkPGPUIDMismatch(d, nil) if len(got) != 1 || got[0].Severity != sdk.StatusInfo { t.Errorf("expected info issue, got %+v", got) } d.OpenPGP.MatchesUsername = boolPtr(true) if got := checkPGPUIDMismatch(d, nil); got != nil { t.Errorf("matching should pass, got %+v", got) } d.OpenPGP.MatchesUsername = nil if got := checkPGPUIDMismatch(d, nil); got != nil { t.Errorf("nil should skip, got %+v", got) } } // ── SMIMEA rules ───────────────────────────────────────────────────────────── func TestCheckSMIMEAFieldRanges(t *testing.T) { if got := checkSMIMEABadUsage(&EmailKeyData{SMIMEA: &SMIMEAInfo{Usage: 4}}, nil); len(got) != 1 { t.Errorf("usage=4 should issue, got %+v", got) } if got := checkSMIMEABadUsage(&EmailKeyData{SMIMEA: &SMIMEAInfo{Usage: 3}}, nil); got != nil { t.Errorf("usage=3 should pass, got %+v", got) } if got := checkSMIMEABadSelector(&EmailKeyData{SMIMEA: &SMIMEAInfo{Selector: 2}}, nil); len(got) != 1 { t.Errorf("selector=2 should issue") } if got := checkSMIMEABadMatchType(&EmailKeyData{SMIMEA: &SMIMEAInfo{MatchingType: 3}}, nil); len(got) != 1 { t.Errorf("matching=3 should issue") } } func TestCheckSMIMEACertExpired(t *testing.T) { past := time.Now().Add(-time.Hour) d := &EmailKeyData{SMIMEA: &SMIMEAInfo{Certificate: &CertInfo{NotAfter: past}}} got := checkSMIMEACertExpired(d, nil) if len(got) != 1 || got[0].Severity != sdk.StatusCrit { t.Errorf("expected crit, got %+v", got) } } func TestCheckSMIMEANoEmailProtect_Severity(t *testing.T) { d := &EmailKeyData{SMIMEA: &SMIMEAInfo{Certificate: &CertInfo{}}} // Default true ⇒ crit. if got := checkSMIMEANoEmailProtect(d, sdk.CheckerOptions{}); len(got) != 1 || got[0].Severity != sdk.StatusCrit { t.Errorf("default crit, got %+v", got) } // Off ⇒ warn. if got := checkSMIMEANoEmailProtect(d, sdk.CheckerOptions{OptionRequireEmailProtection: false}); got[0].Severity != sdk.StatusWarn { t.Errorf("opt-off should warn, got %+v", got) } // Has EKU ⇒ no issue. d.SMIMEA.Certificate.HasEmailProtectionEKU = true if got := checkSMIMEANoEmailProtect(d, sdk.CheckerOptions{}); got != nil { t.Errorf("EKU present should pass, got %+v", got) } } func TestCheckSMIMEAWeakSigAlgorithm(t *testing.T) { for _, algo := range []string{"MD5-RSA", "SHA1-RSA"} { d := &EmailKeyData{SMIMEA: &SMIMEAInfo{Certificate: &CertInfo{SignatureAlgorithm: algo}}} if got := checkSMIMEAWeakSigAlgorithm(d, nil); len(got) != 1 { t.Errorf("%s should issue", algo) } } d := &EmailKeyData{SMIMEA: &SMIMEAInfo{Certificate: &CertInfo{SignatureAlgorithm: "SHA256-RSA"}}} if got := checkSMIMEAWeakSigAlgorithm(d, nil); got != nil { t.Errorf("SHA256-RSA should pass, got %+v", got) } } func TestCheckSMIMEAEmailMismatch(t *testing.T) { d := &EmailKeyData{Username: "alice", SMIMEA: &SMIMEAInfo{Certificate: &CertInfo{ EmailAddresses: []string{"bob@example.com"}, EmailMatchesUsername: boolPtr(false), }}} got := checkSMIMEAEmailMismatch(d, nil) if len(got) != 1 || got[0].Severity != sdk.StatusInfo { t.Errorf("expected info, got %+v", got) } } // ── Rule.Evaluate plumbing ─────────────────────────────────────────────────── func TestRuleEvaluate_OKPath(t *testing.T) { obs := &fakeObs{store: map[string]any{ ObservationKey: &EmailKeyData{Kind: KindOpenPGPKey, QueriedOwner: "x.example.com.", DNSSECSecure: boolPtr(true)}, }} for _, r := range allRules { if r.name != RuleDNSSECNotValidated { continue } states := r.Evaluate(context.Background(), obs, sdk.CheckerOptions{}) if len(states) != 1 || states[0].Status != sdk.StatusOK || states[0].Code != RuleDNSSECNotValidated { t.Fatalf("expected single OK state, got %+v", states) } } } func TestRuleEvaluate_KindFiltering(t *testing.T) { obs := &fakeObs{store: map[string]any{ ObservationKey: &EmailKeyData{Kind: KindSMIMEA}, }} for _, r := range allRules { if r.name != RulePGPParseError { continue } states := r.Evaluate(context.Background(), obs, sdk.CheckerOptions{}) if len(states) != 1 || states[0].Status != sdk.StatusUnknown { t.Fatalf("PGP rule on SMIMEA kind should yield single Unknown state, got %+v", states) } } } func TestRuleEvaluate_MissingObservation(t *testing.T) { obs := &fakeObs{store: map[string]any{}} r := allRules[0] states := r.Evaluate(context.Background(), obs, sdk.CheckerOptions{}) if len(states) != 1 || states[0].Status != sdk.StatusError { t.Fatalf("expected single Error state, got %+v", states) } } func TestRulesUniqueNames(t *testing.T) { seen := map[string]bool{} for _, r := range allRules { if seen[r.name] { t.Errorf("duplicate rule name: %s", r.name) } seen[r.name] = true if r.check == nil { t.Errorf("rule %s has nil check func", r.name) } if r.okMessage == "" { t.Errorf("rule %s has empty okMessage", r.name) } } }