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

2.9 KB · 111 lines · Go Raw History
  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}