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 main
2
3import (
4 "context"
5 "database/sql"
6 "path/filepath"
7 "testing"
8 "time"
9
10 "github.com/google/uuid"
11)
12
13func retentionTestDB(t *testing.T) (*sql.DB, uuid.UUID) {
14 t.Helper()
15
16 db, err := openDB(filepath.Join(t.TempDir(), "db.sqlite3"))
17 if err != nil {
18 t.Fatal(err)
19 }
20 t.Cleanup(func() { db.Close() })
21
22 id := uuid.New()
23 if _, err := db.Exec(
24 `INSERT INTO properties (id, name, created_at, updated_at) VALUES (?,?,?,?)`,
25 id[:], "test", time.Now().UnixMilli(), time.Now().UnixMilli()); err != nil {
26 t.Fatal(err)
27 }
28 return db, id
29}
30
31func seedEvent(t *testing.T, db *sql.DB, table string, id uuid.UUID, at int64) {
32 t.Helper()
33 if _, err := db.Exec(
34 `INSERT INTO `+table+` (property_id, event, created_at) VALUES (?,?,?)`,
35 id[:], "page_view", at); err != nil {
36 t.Fatal(err)
37 }
38}
39
40func count(t *testing.T, db *sql.DB, table string) int {
41 t.Helper()
42 var n int
43 if err := db.QueryRow(`SELECT COUNT(*) FROM ` + table).Scan(&n); err != nil {
44 t.Fatal(err)
45 }
46 return n
47}
48
49func TestSweepDropsEventsPastRetention(t *testing.T) {
50 db, id := retentionTestDB(t)
51 now := time.Now().UTC()
52
53 // A day either side of the boundary, so an off-by-a-window error shows up
54 // rather than being hidden by a generous margin.
55 stale := now.Add(-eventRetention - 24*time.Hour).UnixMilli()
56 keep := now.Add(-eventRetention + 24*time.Hour).UnixMilli()
57
58 for _, table := range sweptTables {
59 seedEvent(t, db, table, id, stale)
60 seedEvent(t, db, table, id, stale)
61 seedEvent(t, db, table, id, keep)
62 }
63
64 NewSweeper(db).sweep(context.Background())
65
66 for _, table := range sweptTables {
67 if got := count(t, db, table); got != 1 {
68 t.Errorf("%s = %d rows, want 1: only the row inside the window survives", table, got)
69 }
70 }
71
72 // The sweep is about traffic tables, and deleting the property row instead
73 // would take every dashboard with it.
74 if got := count(t, db, "properties"); got != 1 {
75 t.Errorf("properties = %d, want 1: properties are never swept", got)
76 }
77}
78
79func TestSweepKeepsEverythingInsideRetention(t *testing.T) {
80 db, id := retentionTestDB(t)
81
82 for _, table := range sweptTables {
83 seedEvent(t, db, table, id, time.Now().Add(-time.Hour).UnixMilli())
84 }
85
86 NewSweeper(db).sweep(context.Background())
87
88 for _, table := range sweptTables {
89 if got := count(t, db, table); got != 1 {
90 t.Errorf("%s = %d rows, want 1", table, got)
91 }
92 }
93}
94
95// A fresh database is created with auto_vacuum INCREMENTAL, which is what lets
96// the sweep hand pages back rather than only stopping the file from growing.
97func TestFreshDatabaseCanReclaim(t *testing.T) {
98 db, _ := retentionTestDB(t)
99
100 var mode int
101 if err := db.QueryRow("PRAGMA auto_vacuum").Scan(&mode); err != nil {
102 t.Fatal(err)
103 }
104 if mode != 2 {
105 t.Fatalf("auto_vacuum = %d, want 2 (INCREMENTAL)", mode)
106 }
107 if got := NewSweeper(db).reclaim(context.Background()); got != "reclaimed" {
108 t.Errorf("reclaim() = %q, want %q", got, "reclaimed")
109 }
110}