Skip to content
quarry Artifact [58883dc006]
Login

Artifact [58883dc006]

Artifact 58883dc006227b619b27a63f273c02bf88a65551eef8cf3c29d7cdef8a2ab1f0:


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)
	}
}