package checker import ( "encoding/xml" "strings" "testing" tlsct "git.happydns.org/checker-tls/contract" ) func TestReadFeatures_WithStartTLSAndSCRAM(t *testing.T) { doc := ` SCRAM-SHA-256 SCRAM-SHA-1 PLAIN ` feats := decodeFeaturesForTest(t, doc) if feats.StartTLS == nil { t.Fatal("expected starttls present") } if feats.StartTLS.Required == nil { t.Fatal("expected starttls ") } if feats.Mechanisms == nil || len(feats.Mechanisms.Mechanism) != 3 { t.Fatalf("expected 3 mechanisms, got %+v", feats.Mechanisms) } } func TestReadFeatures_NoSTARTTLS(t *testing.T) { doc := ` PLAIN ` feats := decodeFeaturesForTest(t, doc) if feats.StartTLS != nil { t.Fatal("expected no starttls") } } func TestReadFeatures_S2SDialback(t *testing.T) { doc := ` ` feats := decodeFeaturesForTest(t, doc) if feats.Dialback == nil { t.Fatal("expected dialback feature") } } func decodeFeaturesForTest(t *testing.T, doc string) *streamFeatures { t.Helper() dec := xml.NewDecoder(strings.NewReader(doc)) feats, err := readFeatures(dec) if err != nil { t.Fatalf("readFeatures: %v", err) } return feats } func TestApplyFeatures_SASLExternal(t *testing.T) { ep := EndpointProbe{Mode: ModeServer} applyFeatures(&ep, &streamFeatures{ Mechanisms: &mechanismsEl{Mechanism: []string{"EXTERNAL"}}, }) if !ep.SASLExternal { t.Fatal("expected SASLExternal to be set") } } func TestDeriveIssues_NoSRV(t *testing.T) { d := &XMPPData{ Domain: "example.com", SRV: SRVLookup{FallbackProbed: true}, } is := deriveIssues(d, true, true) if !containsCode(is, CodeNoSRV) { t.Fatalf("expected %s in %+v", CodeNoSRV, is) } } func TestDeriveIssues_LegacyJabber(t *testing.T) { d := &XMPPData{ SRV: SRVLookup{ Client: []SRVRecord{{Target: "xmpp.example.com", Port: 5222}}, Jabber: []SRVRecord{{Target: "xmpp.example.com", Port: 5222}}, }, } is := deriveIssues(d, true, false) if !containsCode(is, CodeLegacyJabber) { t.Fatalf("expected %s in %+v", CodeLegacyJabber, is) } } func TestDeriveIssues_StartTLSMissing(t *testing.T) { d := &XMPPData{ SRV: SRVLookup{Client: []SRVRecord{{Target: "x", Port: 5222}}}, Endpoints: []EndpointProbe{{ Mode: ModeClient, Address: "1.2.3.4:5222", TCPConnected: true, StreamOpened: true, STARTTLSOffered: false, }}, } is := deriveIssues(d, true, false) if !containsCode(is, CodeStartTLSMissing) { t.Fatalf("expected %s in %+v", CodeStartTLSMissing, is) } } func TestDeriveIssues_PlainOnlyAndNoSCRAM(t *testing.T) { d := &XMPPData{ SRV: SRVLookup{Client: []SRVRecord{{Target: "x", Port: 5222}}}, Endpoints: []EndpointProbe{{ Mode: ModeClient, Address: "1.2.3.4:5222", TCPConnected: true, StreamOpened: true, STARTTLSOffered: true, STARTTLSRequired: true, STARTTLSUpgraded: true, FeaturesRead: true, SASLMechanisms: []string{"PLAIN"}, }}, } is := deriveIssues(d, true, false) if !containsCode(is, CodeSASLPlainOnly) { t.Fatalf("expected %s in %+v", CodeSASLPlainOnly, is) } if !containsCode(is, CodeSASLNoSCRAM) { t.Fatalf("expected %s in %+v", CodeSASLNoSCRAM, is) } } func TestDeriveIssues_S2SNoAuth(t *testing.T) { d := &XMPPData{ SRV: SRVLookup{Server: []SRVRecord{{Target: "x", Port: 5269}}}, Endpoints: []EndpointProbe{{ Mode: ModeServer, Address: "1.2.3.4:5269", TCPConnected: true, StreamOpened: true, STARTTLSOffered: true, STARTTLSRequired: true, STARTTLSUpgraded: true, FeaturesRead: true, DialbackOffered: false, SASLExternal: false, }}, } is := deriveIssues(d, false, true) if !containsCode(is, CodeS2SNoAuth) { t.Fatalf("expected %s in %+v", CodeS2SNoAuth, is) } } func TestDeriveIssues_HappyPath(t *testing.T) { d := &XMPPData{ SRV: SRVLookup{ Client: []SRVRecord{{Target: "x", Port: 5222}}, Server: []SRVRecord{{Target: "x", Port: 5269}}, ClientSecure: []SRVRecord{{Target: "x", Port: 5223}}, }, Endpoints: []EndpointProbe{ { Mode: ModeClient, Address: "1.2.3.4:5222", TCPConnected: true, StreamOpened: true, STARTTLSOffered: true, STARTTLSRequired: true, STARTTLSUpgraded: true, FeaturesRead: true, SASLMechanisms: []string{"SCRAM-SHA-256", "SCRAM-SHA-256-PLUS"}, }, { Mode: ModeServer, Address: "1.2.3.4:5269", TCPConnected: true, StreamOpened: true, STARTTLSOffered: true, STARTTLSRequired: true, STARTTLSUpgraded: true, FeaturesRead: true, DialbackOffered: true, }, }, } computeCoverage(d) is := deriveIssues(d, true, true) for _, i := range is { if i.Severity == SeverityCrit { t.Fatalf("unexpected crit issue %s: %s", i.Code, i.Message) } } } func TestComputeCoverage_Mixed(t *testing.T) { d := &XMPPData{ Endpoints: []EndpointProbe{ {Mode: ModeClient, TCPConnected: true, IsIPv6: false, STARTTLSUpgraded: true, SASLMechanisms: []string{"PLAIN"}}, {Mode: ModeClient, TCPConnected: true, IsIPv6: true}, }, } computeCoverage(d) if !d.Coverage.HasIPv4 { t.Fatal("expected IPv4 true") } if !d.Coverage.HasIPv6 { t.Fatal("expected IPv6 true") } if !d.Coverage.WorkingC2S { t.Fatal("expected WorkingC2S") } } func TestDiscoverEntries_AllSets(t *testing.T) { d := &XMPPData{ Domain: "example.com", SRV: SRVLookup{ Client: []SRVRecord{{Target: "xmpp.example.com", Port: 5222}}, Server: []SRVRecord{{Target: "xmpp.example.com", Port: 5269}}, ClientSecure: []SRVRecord{{Target: "xmpp.example.com", Port: 5223}}, ServerSecure: []SRVRecord{{Target: "xmpp.example.com", Port: 5270}}, Jabber: []SRVRecord{{Target: "xmpp.example.com", Port: 5222}}, // legacy, not emitted }, Endpoints: []EndpointProbe{ {Mode: ModeClient, Target: "xmpp.example.com", Port: 5222, STARTTLSRequired: true}, {Mode: ModeServer, Target: "xmpp.example.com", Port: 5269, STARTTLSRequired: false}, }, } p := &xmppProvider{} raw, err := p.DiscoverEntries(d) if err != nil { t.Fatalf("DiscoverEntries: %v", err) } if len(raw) != 4 { t.Fatalf("expected 4 entries (legacy jabber excluded), got %d: %+v", len(raw), raw) } type signature struct { starttls string host string port uint16 } by := map[signature]tlsct.TLSEndpoint{} for i, e := range raw { if e.Type != tlsct.Type { t.Errorf("entry %d: Type=%q, want %q", i, e.Type, tlsct.Type) } ep, err := tlsct.ParseEntry(e) if err != nil { t.Fatalf("entry %d: ParseEntry: %v", i, err) } if ep.SNI != "example.com" { t.Errorf("entry %d: SNI=%q, want example.com", i, ep.SNI) } by[signature{ep.STARTTLS, ep.Host, ep.Port}] = ep } c2s, ok := by[signature{"xmpp-client", "xmpp.example.com", 5222}] if !ok { t.Fatal("missing c2s entry") } if !c2s.RequireSTARTTLS { t.Errorf("c2s RequireSTARTTLS = false, want true") } s2s, ok := by[signature{"xmpp-server", "xmpp.example.com", 5269}] if !ok { t.Fatal("missing s2s entry") } if s2s.RequireSTARTTLS { t.Errorf("s2s RequireSTARTTLS = true, want false (opportunistic)") } directClient, ok := by[signature{"", "xmpp.example.com", 5223}] if !ok { t.Fatal("missing direct-TLS client entry") } if directClient.STARTTLS != "" || directClient.RequireSTARTTLS { t.Errorf("direct-TLS entry should carry no STARTTLS posture, got %+v", directClient) } } func TestDiscoverEntries_WrongType(t *testing.T) { p := &xmppProvider{} eps, err := p.DiscoverEntries("not an XMPPData") if err != nil { t.Fatalf("expected nil error for wrong type, got %v", err) } if eps != nil { t.Fatalf("expected nil entries for wrong type, got %v", eps) } } func containsCode(is []Issue, code string) bool { for _, i := range is { if i.Code == code { return true } } return false }