checker-dav/internal/dav/endpoints_test.go
Pierre-Olivier Mercier 7d5535fddf Initial commit
CalDAV and CardDAV checkers sharing a single Go module. Discovery follows
RFC 6764 (/.well-known + SRV/TXT), authenticated probes cover principal,
home-set, collections and a minimal REPORT query on top of go-webdav.
Common shape in internal/dav/; CalDAV adds a scheduling rule.

Surfaces its context URL (and each secure-SRV target) as TLS endpoints via
the EndpointDiscoverer interface, so the dedicated TLS checker can pick
them up without re-parsing observations.

HTML report foregrounds common misconfigs (well-known returning 200,
missing SRV, plaintext-only SRV, missing DAV capability, skipped auth
phase) as action-item callouts before the full phase breakdown.
2026-04-26 21:47:40 +07:00

98 lines
2.8 KiB
Go

package dav
import (
"testing"
tlsct "git.happydns.org/checker-tls/contract"
)
// parseAll decodes DiscoverEntries output via the TLS contract. Malformed
// entries fail the test so we notice drift quickly.
func parseAll(t *testing.T, obs *Observation) []tlsct.TLSEndpoint {
t.Helper()
entries := DiscoverEntries(obs)
eps, warnings := tlsct.ParseEntries(entries)
if len(warnings) != 0 {
t.Fatalf("unexpected decode warnings: %v", warnings)
}
out := make([]tlsct.TLSEndpoint, len(eps))
for i, e := range eps {
if e.Ref == "" {
t.Errorf("entry %d has empty Ref", i)
}
out[i] = e.Endpoint
}
return out
}
func TestDiscoverEntries_contextURLOnly(t *testing.T) {
obs := &Observation{
Discovery: DiscoveryResult{ContextURL: "https://dav.example.com/caldav/"},
}
got := parseAll(t, obs)
if len(got) != 1 {
t.Fatalf("got %d endpoints, want 1: %+v", len(got), got)
}
if got[0].Host != "dav.example.com" || got[0].Port != 443 {
t.Errorf("unexpected endpoint: %+v", got[0])
}
// Direct TLS; no STARTTLS upgrade.
if got[0].STARTTLS != "" {
t.Errorf("STARTTLS = %q, want empty (direct TLS)", got[0].STARTTLS)
}
// SNI must be set unconditionally, even when it is equal to Host.
if got[0].SNI != "dav.example.com" {
t.Errorf("SNI = %q, want dav.example.com", got[0].SNI)
}
}
func TestDiscoverEntries_nonDefaultPort(t *testing.T) {
obs := &Observation{
Discovery: DiscoveryResult{ContextURL: "https://dav.example.com:8443/caldav/"},
}
got := parseAll(t, obs)
if len(got) != 1 || got[0].Port != 8443 {
t.Fatalf("unexpected: %+v", got)
}
}
func TestDiscoverEntries_srvTargets(t *testing.T) {
// SRV pointing to a different name than the domain → we must surface
// the SRV target too, because that's the hostname the cert needs to
// cover.
obs := &Observation{
Discovery: DiscoveryResult{
ContextURL: "https://dav.example.com/caldav/",
SecureSRV: []SRVRecord{
{Target: "dav-backend-1.example.net", Port: 443},
{Target: "dav-backend-2.example.net", Port: 443},
{Target: "dav.example.com", Port: 443}, // duplicate of context → deduped
},
},
}
got := parseAll(t, obs)
if len(got) != 3 {
t.Fatalf("expected 3 unique endpoints, got %d: %+v", len(got), got)
}
hosts := map[string]bool{}
for _, e := range got {
hosts[e.Host] = true
if e.SNI != e.Host {
t.Errorf("endpoint %+v: SNI=%q, want %q (equal to Host)", e, e.SNI, e.Host)
}
}
for _, want := range []string{"dav.example.com", "dav-backend-1.example.net", "dav-backend-2.example.net"} {
if !hosts[want] {
t.Errorf("missing host %q in %+v", want, got)
}
}
}
func TestDiscoverEntries_emptyOnNoContextURL(t *testing.T) {
if got := DiscoverEntries(&Observation{}); got != nil {
t.Errorf("expected nil, got %+v", got)
}
if got := DiscoverEntries(nil); got != nil {
t.Errorf("expected nil for nil obs, got %+v", got)
}
}