checker-xmpp/checker/tls_related_test.go

220 lines
6.3 KiB
Go

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, "<title>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])
}
}