orchard
mirrorEvery 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
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}