package check import ( "os" "path/filepath" "regexp" "strings" "testing" "time" "git.coffeylabs.org/coffey-labs/hotdog-cms/internal/site" ) func mkSite(t *testing.T, files map[string]string, extra string) (string, string, *site.Config) { t.Helper() dir := t.TempDir() out := filepath.Join(dir, "public") for name, body := range files { p := filepath.Join(out, filepath.FromSlash(name)) os.MkdirAll(filepath.Dir(p), 0o755) os.WriteFile(p, []byte(body), 0o644) } os.WriteFile(filepath.Join(dir, "site.yaml"), []byte("name: T\nurl: https://example.org\n"+extra), 0o644) cfg, err := site.LoadConfig(dir) if err != nil { t.Fatal(err) } return dir, out, cfg } const head = `Home` func rules(r *Report) map[string]int { m := map[string]int{} for _, p := range r.Problems { m[string(p.Level)+" "+p.Rule]++ } return m } func TestRules(t *testing.T) { expires := time.Now().AddDate(0, 0, 10).UTC().Format(time.RFC3339) dir, out, cfg := mkSite(t, map[string]string{ "index.html": `` + head + ` xy

Write to hello@example.org or @me@social.example

safe@example.org

`, "card.png": "png", "about/index.html": `` + head + ``, "thin/index.html": ``, "sitemap.xml": `https://example.org/thin/`, "robots.txt": "User-agent: *\n", ".well-known/security.txt": "Contact: mailto:security@example.org\nExpires: " + expires + "\n", }, "check:\n cloudflare: true\n allow_third_party: [video.example.com]\n") r, err := Run(dir, out, cfg) if err != nil { t.Fatal(err) } got := rules(r) want := map[string]int{ "error tracker-before-consent": 2, // the gtag script, and the inline dataLayer code "error broken-link": 2, // /missing.png and /nowhere/ "error inline-handler": 1, "error mixed-content": 1, "error noindex-in-sitemap": 1, "error privacy-page": 1, "warning inline-script": 1, "warning inline-style": 1, "warning img-alt": 1, "warning third-party": 1, // cdn.example.net; video.example.com is allowed "warning cloudflare-email": 2, // hello@ and the handle; safe@ is inside markers "warning robots-sitemap": 1, "warning security-txt": 1, // expires within 30 days "warning duplicate-title": 1, "warning duplicate-description": 1, } for k, n := range want { if got[k] != n { t.Errorf("%s: got %d, want %d", k, got[k], n) } } for k := range got { if _, ok := want[k]; !ok { t.Errorf("unexpected finding %s (%d)", k, got[k]) } } if !strings.Contains(r.CSP, "script-src 'self' 'unsafe-inline' http://cdn.example.net https://www.googletagmanager.com") || !strings.Contains(r.CSP, "frame-src https://video.example.com") { t.Errorf("csp: %s", r.CSP) } // Ignoring a rule switches it off; strict CSP makes inline code an error. cfg.Check.Ignore = []string{"img-alt"} cfg.Check.CSP = "strict" r, _ = Run(dir, out, cfg) got = rules(r) if got["warning img-alt"] != 0 || got["error inline-script"] != 1 || got["error inline-style"] != 1 { t.Errorf("ignore/strict not applied: %v", got) } } func TestCleanSitePasses(t *testing.T) { dir, out, cfg := mkSite(t, map[string]string{ "index.html": `` + head + ``, "card.png": "png", "sitemap.xml": `https://example.org/`, "robots.txt": "Sitemap: https://example.org/sitemap.xml\n", ".well-known/security.txt": "Expires: " + time.Now().AddDate(1, 0, 0).UTC().Format(time.RFC3339) + "\n", }, "") r, err := Run(dir, out, cfg) if err != nil { t.Fatal(err) } if len(r.Problems) != 0 { t.Errorf("clean site has findings: %v", r.Problems) } if !strings.HasPrefix(r.CSP, "default-src 'self'; script-src 'self'; style-src 'self';") { t.Errorf("clean site's CSP isn't strict: %s", r.CSP) } } // gpsJPEG is the start of a JPEG whose EXIF block has a GPS entry: enough // for the rule, which reads only the header. func gpsJPEG() string { tiff := []byte{'I', 'I', 42, 0, 8, 0, 0, 0, 1, 0, // IFD0: one entry, the GPS pointer 0x25, 0x88, 4, 0, 1, 0, 0, 0, 26, 0, 0, 0, 0, 0, 0, 0, 1, 0, // GPS IFD: one entry 1, 0, 2, 0, 2, 0, 0, 0, 'N', 0, 0, 0, 0, 0, 0, 0} seg := append([]byte("Exif\x00\x00"), tiff...) b := []byte{0xFF, 0xD8, 0xFF, 0xE1, byte((len(seg) + 2) >> 8), byte(len(seg) + 2)} b = append(b, seg...) return string(append(b, 0xFF, 0xDA, 0, 2, 0xFF, 0xD9)) } func TestImageRules(t *testing.T) { dir, out, cfg := mkSite(t, map[string]string{ "index.html": `` + head + ``, "card.png": "png", "img/home.jpg": gpsJPEG(), "img/plain.jpg": "\xFF\xD8\xFF\xDA\x00\x02\xFF\xD9", "img/big.png": strings.Repeat("x", 600<<10), }, "") r, err := Run(dir, out, cfg) if err != nil { t.Fatal(err) } got := rules(r) if got["error image-location"] != 1 || got["warning image-weight"] != 1 { t.Fatalf("got %v", got) } for _, p := range r.Problems { if p.Rule == "image-location" && p.File != "public/img/home.jpg" { t.Errorf("flagged %s", p.File) } } } func TestLookContrast(t *testing.T) { dir, out, cfg := mkSite(t, map[string]string{"index.html": `` + head + ``, "card.png": "png"}, "look:\n accent: \"#dddddd\"\n") os.WriteFile(filepath.Join(dir, "look.yaml"), []byte("tokens:\n - { name: accent, type: color, default: \"#ba5019\", dark: \"#f2a65a\" }\n - { name: bg, type: color, default: \"#ffffff\", dark: \"#000000\" }\ncontrast:\n - [accent, bg]\n"), 0o644) r, err := Run(dir, out, cfg) if err != nil { t.Fatal(err) } var found []string for _, p := range r.Problems { if p.Rule == "look-contrast" { found = append(found, p.What) } } if len(found) != 1 || !strings.Contains(found[0], "accent on bg is 1.36:1 in light mode") { t.Errorf("look-contrast: %v", found) } } func TestEveryRuleHasAFix(t *testing.T) { src, _ := os.ReadFile("rules.go") more, _ := os.ReadFile("check.go") pages, _ := os.ReadFile("pages.go") all := string(src) + string(more) + string(pages) for _, m := range regexp.MustCompile(`c\.add\(\w+, "([a-z-]+)"`).FindAllStringSubmatch(all, -1) { if Fixes[m[1]] == "" { t.Errorf("rule %s has no fix", m[1]) } } for _, r := range []string{"duplicate-title", "duplicate-description", "forbidden-string"} { if Fixes[r] == "" { t.Errorf("rule %s has no fix", r) } } }