// This file is part of the happyDomain (R) project. // Copyright (c) 2020-2026 happyDomain // Authors: Pierre-Olivier Mercier, et al. package checker import ( "encoding/json" "strings" "testing" sdk "git.happydns.org/checker-sdk-go/checker" ) func wellKnownData(t *testing.T, probes map[string]WellKnownProbe) map[string]json.RawMessage { t.Helper() raw, err := json.Marshal(WellKnownData{URIs: probes}) if err != nil { t.Fatalf("marshal: %v", err) } return map[string]json.RawMessage{ObservationKeyWellKnown: raw} } func TestSecurityTxtRule_OK(t *testing.T) { data := &HTTPData{ Domain: "example.test", Probes: []HTTPProbe{httpsProbe("a:443")}, Extensions: wellKnownData(t, map[string]WellKnownProbe{ "/.well-known/security.txt": { PathProbe: PathProbe{StatusCode: 200, Bytes: 128, ContentType: "text/plain; charset=utf-8"}, ContactCount: 1, ExpiresCount: 1, }, "/robots.txt": {PathProbe: PathProbe{StatusCode: 200, Bytes: 42}}, }), } states := runRule(t, &securityTxtRule{}, data, nil) mustStatus(t, states, sdk.StatusOK) if !hasCode(states, "http.security_txt.ok") { t.Errorf("expected ok, got %+v", states) } } func TestSecurityTxtRule_Empty(t *testing.T) { data := &HTTPData{ Domain: "example.test", Probes: []HTTPProbe{httpsProbe("a:443")}, Extensions: wellKnownData(t, map[string]WellKnownProbe{ "/.well-known/security.txt": {PathProbe: PathProbe{StatusCode: 200, Bytes: 0}}, }), } states := runRule(t, &securityTxtRule{}, data, nil) mustStatus(t, states, sdk.StatusWarn) if !hasCode(states, "http.security_txt.empty") { t.Errorf("expected empty, got %+v", states) } } func TestSecurityTxtRule_ReadError(t *testing.T) { data := &HTTPData{ Domain: "example.test", Probes: []HTTPProbe{httpsProbe("a:443")}, Extensions: wellKnownData(t, map[string]WellKnownProbe{ // 200 with a partial body and a read error: counts are unreliable. "/.well-known/security.txt": { PathProbe: PathProbe{StatusCode: 200, Bytes: 12, ContentType: "text/plain", Error: "unexpected EOF"}, ContactCount: 1, }, }), } states := runRule(t, &securityTxtRule{}, data, nil) mustStatus(t, states, sdk.StatusWarn) if !hasCode(states, "http.security_txt.read_error") { t.Errorf("expected read_error, got %+v", states) } } func TestSecurityTxtRule_Invalid(t *testing.T) { cases := []struct { name string probe WellKnownProbe }{ { name: "soft-404 html", probe: WellKnownProbe{ PathProbe: PathProbe{StatusCode: 200, Bytes: 6320, ContentType: "text/html; charset=utf-8"}, }, }, { name: "no contact", probe: WellKnownProbe{ PathProbe: PathProbe{StatusCode: 200, Bytes: 64, ContentType: "text/plain"}, ContactCount: 0, ExpiresCount: 1, }, }, { name: "no expires", probe: WellKnownProbe{ PathProbe: PathProbe{StatusCode: 200, Bytes: 64, ContentType: "text/plain"}, ContactCount: 1, ExpiresCount: 0, }, }, { name: "two expires", probe: WellKnownProbe{ PathProbe: PathProbe{StatusCode: 200, Bytes: 64, ContentType: "text/plain"}, ContactCount: 1, ExpiresCount: 2, }, }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { data := &HTTPData{ Domain: "example.test", Probes: []HTTPProbe{httpsProbe("a:443")}, Extensions: wellKnownData(t, map[string]WellKnownProbe{ "/.well-known/security.txt": tc.probe, }), } states := runRule(t, &securityTxtRule{}, data, nil) mustStatus(t, states, sdk.StatusWarn) if !hasCode(states, "http.security_txt.invalid") { t.Errorf("expected invalid, got %+v", states) } if states[0].Meta["fix"] == nil { t.Errorf("expected fix hint in meta, got %+v", states[0].Meta) } }) } } func TestSecurityTxtRule_InvalidWording(t *testing.T) { // A genuine file (Contact + Expires present) served with the wrong // Content-Type is invalid, but must not be mislabelled a soft-404. data := &HTTPData{ Domain: "example.test", Probes: []HTTPProbe{httpsProbe("a:443")}, Extensions: wellKnownData(t, map[string]WellKnownProbe{ "/.well-known/security.txt": { PathProbe: PathProbe{StatusCode: 200, Bytes: 64, ContentType: "application/octet-stream"}, ContactCount: 1, ExpiresCount: 1, }, }), } states := runRule(t, &securityTxtRule{}, data, nil) mustStatus(t, states, sdk.StatusWarn) if !hasCode(states, "http.security_txt.invalid") { t.Fatalf("expected invalid, got %+v", states) } if strings.Contains(states[0].Message, "soft-404") { t.Errorf("genuine file should not be labelled soft-404, got %q", states[0].Message) } // A bodyless/placeholder page with no fields keeps the soft-404 hint. data.Extensions = wellKnownData(t, map[string]WellKnownProbe{ "/.well-known/security.txt": { PathProbe: PathProbe{StatusCode: 200, Bytes: 6320, ContentType: "text/html; charset=utf-8"}, }, }) states = runRule(t, &securityTxtRule{}, data, nil) if !strings.Contains(states[0].Message, "soft-404") { t.Errorf("placeholder page should mention soft-404, got %q", states[0].Message) } } func TestSecurityTxtRule_Missing(t *testing.T) { data := &HTTPData{ Domain: "example.test", Probes: []HTTPProbe{httpsProbe("a:443")}, Extensions: wellKnownData(t, map[string]WellKnownProbe{ "/.well-known/security.txt": {PathProbe: PathProbe{StatusCode: 404}}, }), } states := runRule(t, &securityTxtRule{}, data, nil) mustStatus(t, states, sdk.StatusInfo) if !hasCode(states, "http.security_txt.missing") { t.Errorf("expected missing, got %+v", states) } if states[0].Meta["fix"] == nil { t.Errorf("expected fix hint in meta, got %+v", states[0].Meta) } } func TestCountSecurityTxtFields(t *testing.T) { body := "# comment: not a Contact\n" + "\n" + "Contact: mailto:security@example.test\n" + "contact: https://example.test/security\n" + " CONTACT : tel:+1-201-555-0123\n" + "Expires: 2026-12-31T23:59:59z\n" + "Preferred-Languages: en\n" contacts, expires := countSecurityTxtFields([]byte(body)) if contacts != 3 { t.Errorf("contacts = %d, want 3", contacts) } if expires != 1 { t.Errorf("expires = %d, want 1", expires) } } func TestSecurityTxtRule_NoCollectorData(t *testing.T) { data := &HTTPData{ Domain: "example.test", Probes: []HTTPProbe{httpsProbe("a:443")}, } states := runRule(t, &securityTxtRule{}, data, nil) mustStatus(t, states, sdk.StatusUnknown) if !hasCode(states, "http.security_txt.no_data") { t.Errorf("expected no_data, got %+v", states) } } func TestSecurityTxtRule_DecodeError(t *testing.T) { data := &HTTPData{ Domain: "example.test", Probes: []HTTPProbe{httpsProbe("a:443")}, Extensions: map[string]json.RawMessage{ ObservationKeyWellKnown: json.RawMessage(`"not an object"`), }, } states := runRule(t, &securityTxtRule{}, data, nil) if states[0].Status != sdk.StatusError || states[0].Code != "http.security_txt.decode_error" { t.Errorf("expected decode_error, got %+v", states) } }