package health
import (
"context"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
)
func hostsIn(t *testing.T, page string) []string {
t.Helper()
got, err := ForeignResources("https://forge.example/login", strings.NewReader(page))
if err != nil {
t.Fatal(err)
}
return got
}
// The exact shape Cloudflare appended to every page on zeitforge: a
// module script from another host, after the footer, carrying an
// integrity hash and no nonce.
func TestAnInjectedBeaconIsFound(t *testing.T) {
got := hostsIn(t, `<html><body><p>hi</p>
<script type="module" src="https://static.cloudflareinsights.com/beacon.min.js/v31edd" integrity="sha512-abc"></script>
</body></html>`)
if len(got) != 1 || got[0] != "static.cloudflareinsights.com" {
t.Errorf("FAIL: found %v, want the beacon's host", got)
}
}
// A page that keeps the promise reports nothing — including the parts
// that LOOK external. The footer links to fossil-scm.org, and an
// anchor is an address a reader may follow, not a resource the page
// pulls in. Calling that a violation would make the check cry wolf on
// every page it ever ran against.
func TestAPageThatKeepsThePromiseIsSilent(t *testing.T) {
got := hostsIn(t, `<html><head>
<link rel="stylesheet" href="/assets/quarry.abc.css">
<script src="/assets/onerror.js" nonce="x"></script>
<script type="module" src="https://forge.example/assets/datastar.js"></script>
</head><body>
<img src="data:image/png;base64,AAAA" alt="">
<img src="/r/alpha/avatar.png" alt="">
<p>powered by <a href="https://fossil-scm.org/">fossil</a> · <a href="https://example.org/x">docs</a></p>
<svg><use href="#i-file"/></svg>
</body></html>`)
if len(got) != 0 {
t.Errorf("FAIL: a clean page reported %v", got)
}
}
// Every element that makes the browser fetch counts, not just scripts:
// a font, a tracking pixel and an iframe are the same promise broken.
func TestEveryFetchingElementCounts(t *testing.T) {
got := hostsIn(t, `<html><head>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=X">
</head><body>
<img src="https://pixel.example/track.gif">
<iframe src="https://embed.example/x"></iframe>
<video><source src="https://media.example/v.mp4"></video>
<object data="https://obj.example/o.swf"></object>
<img srcset="https://cdn.example/a.png 1x, /local/b.png 2x">
</body></html>`)
want := []string{"cdn.example", "embed.example", "fonts.googleapis.com", "media.example", "obj.example", "pixel.example"}
if len(got) != len(want) {
t.Fatalf("FAIL: found %v, want %v", got, want)
}
for i := range want {
if got[i] != want[i] {
t.Fatalf("FAIL: found %v, want %v", got, want)
}
}
}
// Reported once per host however many times it appears: the panel
// should say "cloudflareinsights", not say it forty times.
func TestAHostIsReportedOnce(t *testing.T) {
got := hostsIn(t, `<html><body>
<img src="https://cdn.example/a.png"><img src="https://cdn.example/b.png"><img src="https://CDN.example/c.png">
</body></html>`)
if len(got) != 1 {
t.Errorf("FAIL: found %v, want one host", got)
}
}
// The probe reports a foreign host as an error, and a clean page as nil.
func TestTheProbeAnswersOnWhatItRead(t *testing.T) {
var page string
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html")
w.Write([]byte(page))
}))
defer srv.Close()
// No throttle in the test: each call must read the page, because the
// two halves of this test are two different pages.
probe := ForeignProbe(srv.Client(), time.Nanosecond)
page = `<html><body><script src="/assets/x.js"></script></body></html>`
if err := probe(context.Background(), Dep{URL: srv.URL}); err != nil {
t.Errorf("FAIL: a clean page probed as %v", err)
}
page = `<html><body><script src="https://tracker.example/b.js"></script></body></html>`
err := probe(context.Background(), Dep{URL: srv.URL})
if err == nil {
t.Fatal("FAIL: an injected script probed clean")
}
if !strings.Contains(err.Error(), "tracker.example") {
t.Errorf("FAIL: the fault does not name the host: %v", err)
}
}
// A probe that could not read the page says so. Treating an unanswered
// question as a good answer is how a check becomes decoration — and
// this one has a real way of failing, since a front that filters robots
// can refuse the probe while serving readers normally.
func TestAnUnreadablePageIsNotCleanAndNotSilent(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.Error(w, "blocked", http.StatusForbidden)
}))
defer srv.Close()
err := ForeignProbe(srv.Client(), time.Nanosecond)(context.Background(), Dep{URL: srv.URL})
if err == nil {
t.Fatal("FAIL: a page the probe could not read reported clean")
}
if !strings.Contains(err.Error(), "could not run") {
t.Errorf("FAIL: the fault reads as a violation rather than an unanswered question: %v", err)
}
// An unreachable host is the same kind of answer.
err = ForeignProbe(&http.Client{}, time.Nanosecond)(context.Background(), Dep{URL: "http://127.0.0.1:1/nope"})
if err == nil || !strings.Contains(err.Error(), "could not read") {
t.Errorf("FAIL: an unreachable page gave %v", err)
}
}
// A custom probe replaces the default "did it answer" test, and the
// watcher reports its verdict on transition like any other.
func TestACustomProbeDrivesTheWatcher(t *testing.T) {
var reported []string
failing := true
w := New([]Dep{{
Name: "origin", URL: "https://forge.example/login", Why: "readers may be served a page this forge did not send",
Probe: func(context.Context, Dep) error {
if failing {
return context.DeadlineExceeded
}
return nil
},
}}, func(d Dep, up bool, err error) {
reported = append(reported, d.Name+":"+map[bool]string{true: "up", false: "down"}[up])
})
w.Check(context.Background())
failing = false
w.Check(context.Background())
w.Check(context.Background())
if len(reported) != 2 || reported[0] != "origin:down" || reported[1] != "origin:up" {
t.Errorf("FAIL: reported %v, want one down then one up — transitions, not states", reported)
}
}
// The page is read on a slower beat than the watcher ticks. An injected
// script is a configuration, not a process that can die between two
// ticks, and pulling the forge's own page through its own front every
// thirty seconds would be two thousand requests a day to learn nothing.
func TestThePageIsNotRefetchedOnEveryTick(t *testing.T) {
reads := 0
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
reads++
w.Write([]byte(`<html><body><script src="/x.js"></script></body></html>`))
}))
defer srv.Close()
probe := ForeignProbe(srv.Client(), time.Hour)
for i := 0; i < 5; i++ {
if err := probe(context.Background(), Dep{URL: srv.URL}); err != nil {
t.Fatalf("FAIL: %v", err)
}
}
if reads != 1 {
t.Errorf("FAIL: read the page %d times across five ticks, want 1", reads)
}
// And the held verdict is the real one, not a default: a violation
// keeps reporting between fetches rather than going quiet.
bad := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(`<html><body><script src="https://tracker.example/b.js"></script></body></html>`))
}))
defer bad.Close()
held := ForeignProbe(bad.Client(), time.Hour)
first := held(context.Background(), Dep{URL: bad.URL})
if first == nil {
t.Fatal("FAIL: setup — the bad page probed clean")
}
if second := held(context.Background(), Dep{URL: bad.URL}); second == nil {
t.Error("FAIL: a held verdict went quiet between fetches")
}
}