package checker import ( "context" "encoding/json" "strings" "testing" "time" sdk "git.happydns.org/checker-sdk-go/checker" ) // stubObsGetter is a minimal ObservationGetter that returns canned XMPPData // and a canned list of related observations. type stubObsGetter struct { xmpp XMPPData related []sdk.RelatedObservation relErr error } func (s *stubObsGetter) Get(_ context.Context, key sdk.ObservationKey, dest any) error { if key != ObservationKeyXMPP { return nil } b, _ := json.Marshal(s.xmpp) return json.Unmarshal(b, dest) } func (s *stubObsGetter) GetRelated(_ context.Context, _ sdk.ObservationKey) ([]sdk.RelatedObservation, error) { return s.related, s.relErr } func mkTLSObs(t *testing.T, payload any) sdk.RelatedObservation { t.Helper() b, err := json.Marshal(payload) if err != nil { t.Fatalf("marshal tls payload: %v", err) } return sdk.RelatedObservation{ CheckerID: "tls", Key: TLSRelatedKey, Data: b, CollectedAt: time.Now(), Ref: "ep-1", } } func TestRule_FoldsTLSCritIntoAggregate(t *testing.T) { obs := &stubObsGetter{ xmpp: healthyXMPPData(), related: []sdk.RelatedObservation{ mkTLSObs(t, map[string]any{ "host": "xmpp.example.com", "port": 5222, "chain_valid": false, "hostname_match": true, }), }, } states := (&xmppRule{}).Evaluate(context.Background(), obs, sdk.CheckerOptions{"domain": "example.com", "mode": "both"}) state := states[0] if state.Status != sdk.StatusCrit { t.Fatalf("expected StatusCrit due to TLS chain invalid, got %s (%s)", state.Status, state.Message) } if !strings.Contains(state.Message, "xmpp.example.com:5222") && !strings.Contains(state.Message, "Invalid certificate") { t.Fatalf("expected TLS message in state, got %q", state.Message) } } func TestRule_IgnoresUnrelatedTLSObs(t *testing.T) { obs := &stubObsGetter{ xmpp: healthyXMPPData(), related: nil, } states := (&xmppRule{}).Evaluate(context.Background(), obs, sdk.CheckerOptions{"domain": "example.com", "mode": "both"}) state := states[0] if state.Status != sdk.StatusOK { t.Fatalf("expected StatusOK without related TLS issues, got %s (%s)", state.Status, state.Message) } } func TestHTMLReportCtx_IncludesTLSPosture(t *testing.T) { data := healthyXMPPData() p := &xmppProvider{} related := []sdk.RelatedObservation{ mkTLSObs(t, map[string]any{ "host": "xmpp.example.com", "port": 5222, "chain_valid": true, "hostname_match": true, "not_after": time.Now().Add(60 * 24 * time.Hour).Format(time.RFC3339), "tls_version": "TLS 1.3", }), } rctx := &stubReportCtx{data: mustJSON(t, data), related: related} html, err := p.GetHTMLReport(rctx) if err != nil { t.Fatalf("GetHTMLReport: %v", err) } if !strings.Contains(html, "chain valid") { t.Fatal("expected 'chain valid' in HTML, not found") } if !strings.Contains(html, "hostname match") { t.Fatal("expected 'hostname match' in HTML, not found") } if !strings.Contains(html, "TLS checker") { t.Fatal("expected TLS checker footer mention, not found") } } func TestHTMLReport_BackCompatNoRelated(t *testing.T) { data := healthyXMPPData() p := &xmppProvider{} // StaticReportContext mimics the host-side "no related observations" path // (e.g. /report HTTP handler on the remote checker). html, err := p.GetHTMLReport(sdk.StaticReportContext(mustJSON(t, data))) if err != nil { t.Fatalf("GetHTMLReport: %v", err) } // Renderer must still produce a valid document and must not include TLS // posture rows when no related observations were passed. if !strings.Contains(html, "XMPP Report") { t.Fatal("expected report title in HTML") } if strings.Contains(html, "TLS cert") { t.Fatal("did not expect 'TLS cert' row without related observations") } } type stubReportCtx struct { data json.RawMessage related []sdk.RelatedObservation } func (s *stubReportCtx) Data() json.RawMessage { return s.data } func (s *stubReportCtx) Related(_ sdk.ObservationKey) []sdk.RelatedObservation { return s.related } func (s *stubReportCtx) States() []sdk.CheckState { return nil } func mustJSON(t *testing.T, v any) json.RawMessage { t.Helper() b, err := json.Marshal(v) if err != nil { t.Fatalf("marshal: %v", err) } return b } func healthyXMPPData() XMPPData { return XMPPData{ Domain: "example.com", SRV: SRVLookup{ Client: []SRVRecord{{Target: "xmpp.example.com", Port: 5222}}, Server: []SRVRecord{{Target: "xmpp.example.com", Port: 5269}}, }, Endpoints: []EndpointProbe{ { Mode: ModeClient, Target: "xmpp.example.com", Port: 5222, Address: "xmpp.example.com:5222", TCPConnected: true, StreamOpened: true, STARTTLSOffered: true, STARTTLSRequired: true, STARTTLSUpgraded: true, FeaturesRead: true, SASLMechanisms: []string{"SCRAM-SHA-256", "SCRAM-SHA-256-PLUS"}, }, { Mode: ModeServer, Target: "xmpp.example.com", Port: 5269, Address: "xmpp.example.com:5269", TCPConnected: true, StreamOpened: true, STARTTLSOffered: true, STARTTLSRequired: true, STARTTLSUpgraded: true, FeaturesRead: true, DialbackOffered: true, }, }, Coverage: ReachabilitySpan{HasIPv4: true, WorkingC2S: true, WorkingS2S: true}, } } func TestTLSIssuesFromRelated_StructuredIssues(t *testing.T) { related := []sdk.RelatedObservation{ mkTLSObs(t, map[string]any{ "host": "xmpp.example.com", "port": 5222, "issues": []map[string]any{ {"code": "tls.self_signed", "severity": "crit", "message": "self-signed cert"}, {"code": "tls.weak_cipher", "severity": "warn", "message": "weak cipher"}, }, }), } out := tlsIssuesFromRelated(related) if len(out) != 2 { t.Fatalf("expected 2 issues, got %d", len(out)) } if out[0].Code != "xmpp.tls.self_signed" || out[0].Severity != SeverityCrit { t.Fatalf("unexpected first issue: %+v", out[0]) } } func TestTLSIssuesFromRelated_FlagsOnly(t *testing.T) { related := []sdk.RelatedObservation{ mkTLSObs(t, map[string]any{ "host": "xmpp.example.com", "port": 5222, "hostname_match": false, }), } out := tlsIssuesFromRelated(related) if len(out) != 1 { t.Fatalf("expected 1 synthesized issue, got %d", len(out)) } if out[0].Severity != SeverityCrit || !strings.Contains(out[0].Message, "does not cover") { t.Fatalf("unexpected synthesized issue: %+v", out[0]) } }