package obs
import (
"net/http"
"net/http/httptest"
"testing"
"time"
)
func find(rows []HourCount, pattern, agent string, signed bool) (HourCount, bool) {
for _, r := range rows {
if r.Pattern == pattern && r.Agent == agent && r.SignedIn == signed {
return r, true
}
}
return HourCount{}, false
}
func TestRollupCountsPerHourRouteAndSource(t *testing.T) {
var r Rollup
h0 := time.Date(2026, 11, 4, 10, 5, 0, 0, time.UTC)
for i := 0; i < 3; i++ {
r.Add(Hit{At: h0.Add(time.Duration(i) * time.Minute), Pattern: "GET /r/{repo}", UA: "Mozilla/5.0 Firefox/131.0", Elapsed: 10 * time.Millisecond})
}
r.Add(Hit{At: h0, Pattern: "GET /r/{repo}", UA: "Mozilla/5.0 Firefox/131.0", Account: "dmestas", Elapsed: 4 * time.Millisecond})
r.Add(Hit{At: h0, Pattern: "GET /r/{repo}", UA: "Googlebot/2.1", Elapsed: time.Millisecond})
r.Add(Hit{At: h0, Pattern: "GET /r/{repo}", UA: "curl/8", Elapsed: time.Millisecond})
r.Add(Hit{At: h0.Add(time.Hour), Pattern: "GET /r/{repo}", UA: "Mozilla/5.0 Firefox/131.0"})
// A stream's time is a tab's lifetime: counted, never timed.
r.Add(Hit{At: h0, Pattern: "GET /events", UA: "Mozilla/5.0 Firefox/131.0", Stream: true, Elapsed: time.Hour})
rows := r.Take(h0.Add(time.Hour).Truncate(time.Hour))
if len(rows) != 5 {
t.Fatalf("FAIL: %d rows, want 5: %+v", len(rows), rows)
}
if c, ok := find(rows, "GET /r/{repo}", AgentBrowser, false); !ok || c.Requests != 3 || c.Elapsed != 30*time.Millisecond {
t.Errorf("FAIL: anonymous browser row %+v ok=%v", c, ok)
}
if c, ok := find(rows, "GET /r/{repo}", AgentBrowser, true); !ok || c.Requests != 1 {
t.Errorf("FAIL: signed-in row %+v ok=%v", c, ok)
}
if _, ok := find(rows, "GET /r/{repo}", AgentCrawler, false); !ok {
t.Error("FAIL: no crawler row")
}
if _, ok := find(rows, "GET /r/{repo}", AgentAutomated, false); !ok {
t.Error("FAIL: no automated row")
}
if c, ok := find(rows, "GET /events", AgentBrowser, false); !ok || c.Requests != 1 || c.Elapsed != 0 {
t.Errorf("FAIL: stream row %+v", c)
}
for _, c := range rows {
if !c.Hour.Equal(h0.Truncate(time.Hour)) {
t.Errorf("FAIL: taken an open hour: %v", c.Hour)
}
}
// The open hour stays until it closes; Take(zero) drains it.
if rest := r.Take(time.Time{}); len(rest) != 1 || rest[0].Requests != 1 {
t.Errorf("FAIL: remaining %+v", rest)
}
if rest := r.Take(time.Time{}); len(rest) != 0 {
t.Errorf("FAIL: drained twice: %+v", rest)
}
}
func TestRollupKeepsNetworkAndCountryAndNoAddress(t *testing.T) {
var r Rollup
at := time.Date(2026, 11, 4, 10, 0, 0, 0, time.UTC)
r.Add(Hit{At: at, Pattern: "GET /", Addr: "203.0.113.9", Network: "Hetzner", Country: "DE", UA: "x"})
rows := r.Take(time.Time{})
if len(rows) != 1 || rows[0].Network != "Hetzner" || rows[0].Country != "DE" {
t.Fatalf("FAIL: %+v", rows)
}
}
func TestRollupBoundsItsKeys(t *testing.T) {
r := Rollup{Max: 4}
at := time.Date(2026, 11, 4, 10, 0, 0, 0, time.UTC)
for i := 0; i < 50; i++ {
r.Add(Hit{At: at, Pattern: "GET /", Network: string(rune('A' + i)), UA: "x"})
}
rows := r.Take(time.Time{})
if len(rows) > 6 {
t.Errorf("FAIL: %d keys for one route, want a bound near 4", len(rows))
}
var n int64
for _, c := range rows {
n += c.Requests
}
if n != 50 {
t.Errorf("FAIL: %d requests kept, want 50", n)
}
}
func TestRestoreReturnsCountsAfterAFailedWrite(t *testing.T) {
var r Rollup
at := time.Date(2026, 11, 4, 10, 0, 0, 0, time.UTC)
r.Add(Hit{At: at, Pattern: "GET /", UA: "x"})
rows := r.Take(time.Time{})
r.Add(Hit{At: at, Pattern: "GET /", UA: "x"})
r.Restore(rows)
got := r.Take(time.Time{})
if len(got) != 1 || got[0].Requests != 2 {
t.Errorf("FAIL: %+v", got)
}
}
func TestAHitCarriesTheRoutePatternTheHandlerNoted(t *testing.T) {
req := httptest.NewRequest("GET", "/r/quarry/file/a/b", nil)
h := serve(t, req, socket, func(w http.ResponseWriter, r *http.Request) {
NoteOf(r.Context()).Pattern("GET /r/{repo}/file/{...}")
})
if h.Pattern != "GET /r/{repo}/file/{...}" {
t.Errorf("FAIL: pattern %q", h.Pattern)
}
}
func TestAHitFallsBackToTheRouteResolver(t *testing.T) {
sink := &hitSink{}
rec := NewRecorder(10, sink)
rec.SetRoutes(func(r *http.Request) string { return "POST /r/{repo}" })
rec.Hits(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {}), socket).
ServeHTTP(httptest.NewRecorder(), httptest.NewRequest("POST", "/r/quarry", nil))
if sink.hits[0].Pattern != "POST /r/{repo}" {
t.Errorf("FAIL: pattern %q", sink.hits[0].Pattern)
}
}