Skip to content
quarry Artifact [f52a5bf2da]
Login

Artifact [f52a5bf2da]

Artifact f52a5bf2daa6bb963e166fa01753b1b95a2222f230b7a8c1d2a531ec180c9da3:


package obs

import (
	"io"
	"net/http"
	"net/http/httptest"
	"testing"
	"time"
)

type hitSink struct {
	nopSink
	hits []Hit
}

func (s *hitSink) Hit(h Hit) { s.hits = append(s.hits, h) }

// serve runs one request through Hits and returns the hit it filed.
func serve(t *testing.T, req *http.Request, addr func(*http.Request) string, h http.HandlerFunc) Hit {
	t.Helper()
	sink := &hitSink{}
	rec := NewRecorder(10, sink)
	rec.Hits(h, addr).ServeHTTP(httptest.NewRecorder(), req)
	if len(sink.hits) != 1 {
		t.Fatalf("FAIL: filed %d hits, want 1", len(sink.hits))
	}
	return sink.hits[0]
}

func socket(req *http.Request) string { return remoteHost(req.RemoteAddr) }

// A hit carries what the outermost handler sees — status, bytes, the
// path without its query, the referring site — and what the handlers
// inside noted about who it was.
func TestAHitCarriesTheResponseAndWhatTheHandlerNoted(t *testing.T) {
	req := httptest.NewRequest("GET", "/r/quarry/timeline?n=500", nil)
	req.Header.Set("User-Agent", "Mozilla/5.0 Firefox/131.0")
	req.Header.Set("Referer", "https://news.ycombinator.com/item?id=1")
	h := serve(t, req, socket, func(w http.ResponseWriter, r *http.Request) {
		NoteOf(r.Context()).Account("dmestas")
		w.WriteHeader(http.StatusTeapot)
		io.WriteString(w, "twelve bytes")
	})
	if h.Status != http.StatusTeapot || h.Bytes != 12 {
		t.Errorf("FAIL: status %d bytes %d", h.Status, h.Bytes)
	}
	if h.URL != "/r/quarry/timeline" || h.Repo != "quarry" || h.Door != DoorWeb {
		t.Errorf("FAIL: url %q repo %q door %q", h.URL, h.Repo, h.Door)
	}
	if h.Account != "dmestas" || h.Referrer != "news.ycombinator.com" {
		t.Errorf("FAIL: account %q referrer %q", h.Account, h.Referrer)
	}
}

// A sync round notes its verb and client, and that decides the door
// whatever the path looks like.
func TestASyncRoundIsFiledWithItsVerb(t *testing.T) {
	req := httptest.NewRequest("POST", "/r/quarry", nil)
	h := serve(t, req, socket, func(w http.ResponseWriter, r *http.Request) {
		NoteOf(r.Context()).Sync("clone", "", "alice", "fossil", "22700")
	})
	if h.Door != DoorSync || h.Verb != "clone" || h.Claimed != "alice" || h.Account != "" || h.Client != "fossil" {
		t.Errorf("FAIL: %+v", h)
	}
}

// The country is the edge's, and believed only when the edge is who
// answered: a client writing CF-IPCountry to a forge it reaches
// directly is not evidence of where it is.
func TestTheCountryIsBelievedOnlyBehindTheProxy(t *testing.T) {
	behind := func(r *http.Request) string { return r.Header.Get("CF-Connecting-IP") }
	req := httptest.NewRequest("GET", "/", nil)
	req.Header.Set("CF-Connecting-IP", "203.0.113.9")
	req.Header.Set("CF-IPCountry", "de")
	if h := serve(t, req, behind, func(http.ResponseWriter, *http.Request) {}); h.Country != "DE" || h.Addr != "203.0.113.9" {
		t.Errorf("FAIL: behind the proxy: country %q addr %q", h.Country, h.Addr)
	}
	direct := httptest.NewRequest("GET", "/", nil)
	direct.Header.Set("CF-IPCountry", "DE")
	if h := serve(t, direct, socket, func(http.ResponseWriter, *http.Request) {}); h.Country != "" {
		t.Errorf("FAIL: a direct request was believed about its country: %q", h.Country)
	}
}

// A stream is marked, so its open-tab lifetime never reads as a
// response time.
func TestAStreamIsMarked(t *testing.T) {
	req := httptest.NewRequest("GET", "/r/quarry/chat.sse", nil)
	h := serve(t, req, socket, func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "text/event-stream")
		w.WriteHeader(200)
	})
	if !h.Stream {
		t.Error("FAIL: a stream was filed as a response")
	}
}

// The forge's own pages are not a referrer.
func TestTheForgeIsNotItsOwnReferrer(t *testing.T) {
	if got := referrerHost("https://zeitforge.app/r/quarry", "zeitforge.app"); got != "" {
		t.Errorf("FAIL: self-referral reported as %q", got)
	}
	if got := referrerHost("not a url", "zeitforge.app"); got != "" {
		t.Errorf("FAIL: an unparseable referrer reported as %q", got)
	}
}

// One reader is one visitor for the day, two readers are two, and the
// key is not the address.
func TestVisitorsAreCountedWithoutTheAddress(t *testing.T) {
	rec := NewRecorder(1, nil)
	day := time.Date(2026, 10, 23, 9, 0, 0, 0, time.UTC)
	a := rec.visitor(day, "203.0.113.9", "Firefox")
	if b := rec.visitor(day.Add(time.Hour), "203.0.113.9", "Firefox"); a != b {
		t.Error("FAIL: one reader read as two visitors in a day")
	}
	if c := rec.visitor(day, "203.0.113.10", "Firefox"); a == c {
		t.Error("FAIL: two readers read as one visitor")
	}
	if d := rec.visitor(day.Add(24*time.Hour), "203.0.113.9", "Firefox"); a == d {
		t.Error("FAIL: a visitor key outlived its day")
	}
}

// A handler that rewrites the path in place, as a proxy does, does not
// change what is filed; and a long poll is filed as a stream.
func TestAProxiedLongPollKeepsItsPathAndIsNoResponseTime(t *testing.T) {
	req := httptest.NewRequest("POST", "/r/quarry/ci/bridge/v1/tasks/poll", nil)
	h := serve(t, req, socket, func(w http.ResponseWriter, r *http.Request) {
		NoteOf(r.Context()).LongPoll()
		r.URL.Path = "/v1/tasks/poll"
	})
	if h.URL != "/r/quarry/ci/bridge/v1/tasks/poll" || h.Repo != "quarry" || h.Door != DoorBridge || !h.Stream {
		t.Errorf("FAIL: filed %q repo %q door %q stream %v", h.URL, h.Repo, h.Door, h.Stream)
	}
}