This commit is contained in:
iceBear67
2026-08-15 07:13:00 +00:00
commit dd50674fdc
114 changed files with 26865 additions and 0 deletions
+301
View File
@@ -0,0 +1,301 @@
package store
import (
"context"
"database/sql"
"io"
"log/slog"
"path/filepath"
"strings"
"testing"
)
func testDB(t *testing.T) *DB {
t.Helper()
log := slog.New(slog.NewTextHandler(io.Discard, nil))
db, err := Open(context.Background(), filepath.Join(t.TempDir(), "pages.db"), log)
if err != nil {
t.Fatalf("Open: %v", err)
}
t.Cleanup(func() { db.Close() })
return db
}
func TestOpenAppliesMigrations(t *testing.T) {
db := testDB(t)
v, err := db.SchemaVersion(context.Background())
if err != nil {
t.Fatal(err)
}
if v != 1 {
t.Errorf("schema version = %d, want 1", v)
}
for _, table := range []string{"projects", "api_keys", "deployments", "blobs", "deployment_files"} {
var n int
if err := db.Reader().QueryRow(
`SELECT count(*) FROM sqlite_master WHERE type='table' AND name=?`, table).Scan(&n); err != nil {
t.Fatal(err)
}
if n != 1 {
t.Errorf("table %s missing", table)
}
}
}
func TestMigrateIsIdempotent(t *testing.T) {
ctx := context.Background()
path := filepath.Join(t.TempDir(), "pages.db")
log := slog.New(slog.NewTextHandler(io.Discard, nil))
db, err := Open(ctx, path, log)
if err != nil {
t.Fatal(err)
}
if _, err := db.w.ExecContext(ctx,
`INSERT INTO projects (name, created_at, updated_at) VALUES ('demo', 1, 1)`); err != nil {
t.Fatal(err)
}
if err := db.Close(); err != nil {
t.Fatal(err)
}
// Reopening must not re-run migrations, and must not lose data.
db2, err := Open(ctx, path, log)
if err != nil {
t.Fatalf("reopen: %v", err)
}
defer db2.Close()
var name string
if err := db2.Reader().QueryRow(`SELECT name FROM projects`).Scan(&name); err != nil {
t.Fatal(err)
}
if name != "demo" {
t.Errorf("project name = %q", name)
}
}
func TestMigrateDetectsChecksumDrift(t *testing.T) {
ctx := context.Background()
db := testDB(t)
if _, err := db.w.ExecContext(ctx,
`UPDATE schema_version SET checksum = 'tampered' WHERE version = 1`); err != nil {
t.Fatal(err)
}
err := db.migrate(ctx)
if err == nil {
t.Fatal("editing an applied migration must be detected")
}
if !strings.Contains(err.Error(), "modified after it was applied") {
t.Errorf("error = %v", err)
}
}
func TestMigrateRejectsUnknownFutureVersion(t *testing.T) {
ctx := context.Background()
db := testDB(t)
if _, err := db.w.ExecContext(ctx,
`INSERT INTO schema_version (version, checksum, applied_at) VALUES (99, 'x', 1)`); err != nil {
t.Fatal(err)
}
if err := db.migrate(ctx); err == nil {
t.Fatal("a database from a newer build must be refused")
}
}
// The partial unique index is what actually guarantees "one active deployment
// per project"; the application layer only has to avoid fighting it.
func TestOneActiveDeploymentPerProject(t *testing.T) {
ctx := context.Background()
db := testDB(t)
if err := db.Tx(ctx, func(tx *sql.Tx) error {
if _, err := tx.Exec(`INSERT INTO projects (id, name, created_at, updated_at) VALUES (1, 'demo', 1, 1)`); err != nil {
return err
}
_, err := tx.Exec(`INSERT INTO deployments (public_id, project_id, state, active, created_at)
VALUES ('dpl_a', 1, 'ready', 1, 1)`)
return err
}); err != nil {
t.Fatal(err)
}
err := db.Tx(ctx, func(tx *sql.Tx) error {
_, err := tx.Exec(`INSERT INTO deployments (public_id, project_id, state, active, created_at)
VALUES ('dpl_b', 1, 'ready', 1, 2)`)
return err
})
if err == nil {
t.Fatal("a second active deployment must be rejected by the database")
}
if !IsConstraint(err) {
t.Errorf("want a constraint violation, got %v", err)
}
// Demoting the old one first is the supported path.
if err := db.Tx(ctx, func(tx *sql.Tx) error {
if _, err := tx.Exec(`UPDATE deployments SET active = 0 WHERE project_id = 1 AND active = 1`); err != nil {
return err
}
_, err := tx.Exec(`INSERT INTO deployments (public_id, project_id, state, active, created_at)
VALUES ('dpl_b', 1, 'ready', 1, 2)`)
return err
}); err != nil {
t.Fatalf("demote-then-promote must be allowed: %v", err)
}
}
func TestRefcountTriggers(t *testing.T) {
ctx := context.Background()
db := testDB(t)
digest := make([]byte, 32)
digest[0] = 0xab
setup := func(tx *sql.Tx) error {
if _, err := tx.Exec(`INSERT INTO projects (id, name, created_at, updated_at) VALUES (1, 'demo', 1, 1)`); err != nil {
return err
}
if _, err := tx.Exec(`INSERT INTO deployments (id, public_id, project_id, state, created_at)
VALUES (1, 'dpl_a', 1, 'uploading', 1), (2, 'dpl_b', 1, 'uploading', 2)`); err != nil {
return err
}
if _, err := tx.Exec(`INSERT INTO blobs (digest, size, present, created_at, last_ref_at)
VALUES (?, 10, 0, 1, 1)`, digest); err != nil {
return err
}
_, err := tx.Exec(`INSERT INTO deployment_files (deployment_id, path, digest, size)
VALUES (1, 'index.html', ?, 10), (2, 'index.html', ?, 10)`, digest, digest)
return err
}
if err := db.Tx(ctx, setup); err != nil {
t.Fatal(err)
}
refcount := func() int {
t.Helper()
var n int
if err := db.Reader().QueryRow(`SELECT refcount FROM blobs WHERE digest = ?`, digest).Scan(&n); err != nil {
t.Fatal(err)
}
return n
}
if got := refcount(); got != 2 {
t.Fatalf("refcount after 2 inserts = %d, want 2", got)
}
if err := db.Tx(ctx, func(tx *sql.Tx) error {
_, err := tx.Exec(`DELETE FROM deployment_files WHERE deployment_id = 1`)
return err
}); err != nil {
t.Fatal(err)
}
if got := refcount(); got != 1 {
t.Errorf("refcount after explicit delete = %d, want 1", got)
}
// The cascade path: deleting the deployment row must also decrement, which
// only holds because recursive_triggers is ON.
if err := db.Tx(ctx, func(tx *sql.Tx) error {
_, err := tx.Exec(`DELETE FROM deployments WHERE id = 2`)
return err
}); err != nil {
t.Fatal(err)
}
if got := refcount(); got != 0 {
t.Errorf("refcount after cascade = %d, want 0 (recursive_triggers not in effect?)", got)
}
}
// A blob may not be dropped while a manifest still points at it.
func TestBlobDeleteRestricted(t *testing.T) {
ctx := context.Background()
db := testDB(t)
digest := make([]byte, 32)
if err := db.Tx(ctx, func(tx *sql.Tx) error {
if _, err := tx.Exec(`INSERT INTO projects (id, name, created_at, updated_at) VALUES (1, 'demo', 1, 1)`); err != nil {
return err
}
if _, err := tx.Exec(`INSERT INTO deployments (id, public_id, project_id, state, created_at)
VALUES (1, 'dpl_a', 1, 'ready', 1)`); err != nil {
return err
}
if _, err := tx.Exec(`INSERT INTO blobs (digest, size, present, created_at, last_ref_at) VALUES (?, 1, 1, 1, 1)`, digest); err != nil {
return err
}
_, err := tx.Exec(`INSERT INTO deployment_files (deployment_id, path, digest, size) VALUES (1, 'a', ?, 1)`, digest)
return err
}); err != nil {
t.Fatal(err)
}
err := db.Tx(ctx, func(tx *sql.Tx) error {
_, err := tx.Exec(`DELETE FROM blobs WHERE digest = ?`, digest)
return err
})
if err == nil {
t.Fatal("deleting a referenced blob must fail")
}
}
func TestScopeCheckConstraint(t *testing.T) {
ctx := context.Background()
db := testDB(t)
cases := []struct {
name string
scope string
proj any
ok bool
}{
{"admin without project", "admin", nil, true},
{"admin with project", "admin", int64(1), false},
{"project without project", "project", nil, false},
{"project with project", "project", int64(1), true},
{"unknown scope", "root", nil, false},
}
if err := db.Tx(ctx, func(tx *sql.Tx) error {
_, err := tx.Exec(`INSERT INTO projects (id, name, created_at, updated_at) VALUES (1, 'demo', 1, 1)`)
return err
}); err != nil {
t.Fatal(err)
}
for i, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
err := db.Tx(ctx, func(tx *sql.Tx) error {
_, err := tx.Exec(
`INSERT INTO api_keys (id, secret_hash, scope, project_id, created_at) VALUES (?, ?, ?, ?, 1)`,
"key"+string(rune('a'+i)), make([]byte, 32), tc.scope, tc.proj)
return err
})
if tc.ok && err != nil {
t.Errorf("insert should have been accepted: %v", err)
}
if !tc.ok && err == nil {
t.Error("insert should have been rejected")
}
})
}
}
// The write pool is capped at one connection, so a transaction that never
// returns would deadlock the server. This asserts that a plain read does not
// need the write pool.
func TestReadsDoNotBlockOnWriter(t *testing.T) {
ctx := context.Background()
db := testDB(t)
done := make(chan struct{})
go func() {
defer close(done)
_ = db.Tx(ctx, func(tx *sql.Tx) error {
if _, err := tx.Exec(`INSERT INTO projects (name, created_at, updated_at) VALUES ('slow', 1, 1)`); err != nil {
return err
}
var n int
// While this transaction is open, a reader must still make progress.
if err := db.Reader().QueryRow(`SELECT count(*) FROM projects`).Scan(&n); err != nil {
return err
}
return nil
})
}()
<-done
}