repos
/ orchard main

orchard

mirror

Every site I host, in one repo, along with the Cloudflare Tunnel and Caddy that front them. It's all Go, Vite, and SQLite, and it runs on a desktop at home with nothing listening on an inbound port.

blogbuncaddycloudflare-tunneldockergogolanghomelabhtml-templatemonorepoself-hostedseosqlitestatic-sitetypstuptime-monitoringviteweb-analytics

4.0 KB · 128 lines · Go Raw History
  1package tools
  2
  3import (
  4	"strings"
  5	"testing"
  6	"time"
  7)
  8
  9// A clock the test drives, since the whole thing is about windows and sleeping
 10// through an hour is not a test.
 11func atTime(b *Budgets, t *time.Time) { b.now = func() time.Time { return *t } }
 12
 13// The gap was never what got this address blocked. Six seconds of spacing was
 14// already in place and the total was unbounded, so a long turn could spend an
 15// afternoon's searches on one question and honour every gap doing it.
 16func TestBudgetStopsAtTheMinutePool(t *testing.T) {
 17	now := time.Now()
 18	b := NewBudgets()
 19	atTime(b, &now)
 20	want := budgetFor(SearchHost).minute
 21
 22	for i := 0; i < want; i++ {
 23		if err := b.Take(SearchHost); err != nil {
 24			t.Fatalf("call %d of %d refused early: %v", i+1, want, err)
 25		}
 26	}
 27	err := b.Take(SearchHost)
 28	if err == nil {
 29		t.Fatal("the pool did not stop the next call")
 30	}
 31	if !strings.Contains(err.Error(), "frees up in") {
 32		t.Errorf("the refusal does not say when it clears: %v", err)
 33	}
 34
 35	// It frees as the window slides, without anything being poked to find out.
 36	now = now.Add(61 * time.Second)
 37	if err := b.Take(SearchHost); err != nil {
 38		t.Errorf("the minute pool did not free up: %v", err)
 39	}
 40}
 41
 42func TestBudgetStopsAtTheHourAndDayPools(t *testing.T) {
 43	now := time.Now()
 44	b := NewBudgets()
 45	atTime(b, &now)
 46	bud := budgetFor(SearchHost)
 47
 48	// Spend the hour, a minute's worth at a time so the tighter pool never bites.
 49	for i := 0; i < bud.hour; i++ {
 50		if err := b.Take(SearchHost); err != nil {
 51			t.Fatalf("call %d refused early: %v", i+1, err)
 52		}
 53		if (i+1)%bud.minute == 0 {
 54			now = now.Add(61 * time.Second)
 55		}
 56	}
 57	if err := b.Take(SearchHost); err == nil || !strings.Contains(err.Error(), "hourly") {
 58		t.Fatalf("the hour pool did not bite, got %v", err)
 59	}
 60
 61	// An hour on, the hour pool is clear and the day pool is still counting.
 62	now = now.Add(61 * time.Minute)
 63	if err := b.Take(SearchHost); err != nil {
 64		t.Errorf("the hour pool did not free up: %v", err)
 65	}
 66	if _, _, day, _ := b.Left(SearchHost); day >= bud.day {
 67		t.Errorf("the day pool reset with the hour, %d left of %d", day, bud.day)
 68	}
 69}
 70
 71// A deploy must not hand the model a fresh allowance, which is the same mistake
 72// the penalty box made before it was persisted.
 73func TestBudgetSurvivesARestart(t *testing.T) {
 74	now := time.Now()
 75	b := NewBudgets()
 76	atTime(b, &now)
 77	for i := 0; i < 5; i++ {
 78		_ = b.Take(SearchHost)
 79	}
 80	spent := b.Spent(SearchHost)
 81	if len(spent) != 5 {
 82		t.Fatalf("recorded %d calls, want 5", len(spent))
 83	}
 84
 85	fresh := NewBudgets()
 86	atTime(fresh, &now)
 87	fresh.Restore(SearchHost, spent)
 88	_, _, _, _ = fresh.Left(SearchHost)
 89	if m, _, _, _ := fresh.Left(SearchHost); m != budgetFor(SearchHost).minute-5 {
 90		t.Errorf("after a restart the minute pool shows %d left, want %d",
 91			m, budgetFor(SearchHost).minute-5)
 92	}
 93}
 94
 95// Anything a day old stops counting and stops being held.
 96func TestBudgetForgetsYesterday(t *testing.T) {
 97	now := time.Now()
 98	b := NewBudgets()
 99	atTime(b, &now)
100	for i := 0; i < 5; i++ {
101		_ = b.Take(SearchHost)
102	}
103	now = now.Add(25 * time.Hour)
104	if _, _, day, _ := b.Left(SearchHost); day != budgetFor(SearchHost).day {
105		t.Errorf("yesterday's calls are still counted, %d left", day)
106	}
107	if got := len(b.Spent(SearchHost)); got != 0 {
108		t.Errorf("yesterday's timestamps are still held, %d of them", got)
109	}
110}
111
112// The search host has to be the strictest thing here, since it is the one that
113// has actually banned this address.
114func TestSearchIsTheStrictestBudget(t *testing.T) {
115	s, d := budgetFor(SearchHost), defaultBudget
116	if s.minute >= d.minute || s.hour >= d.hour || s.day >= d.day {
117		t.Errorf("search budget %+v is not stricter than the default %+v", s, d)
118	}
119	// Under the figure people repeat for an address, not at it.
120	if s.minute > 30 {
121		t.Errorf("minute pool of %d is at or past the 30 a minute people warn about", s.minute)
122	}
123	// At or above what the duckduckgo_search library asks between calls.
124	if s.gap < 2*time.Second {
125		t.Errorf("gap of %s is under the 2s that library recommends", s.gap)
126	}
127}