Files
Maven/internal/store/migrations_test.go
T
kami 047a813278 store: at-rest encryption + schema-migration runner
Two spine infra items (feature-ranking #1, part of the migration prereq):

- migrations.go: PRAGMA user_version runner, empty (no-op) migration slice,
  one tx per step, fail-closed. Mechanism in place before any real schema
  change needs it.
- crypt.go: file-level at-rest encryption. On-disk file is always AES-256-GCM
  ciphertext; decrypted to a tmpfs working copy modernc sqlite operates on;
  re-encrypted atomically on Close, plaintext wiped, key zeroed. Pure stdlib,
  CGO stays off. Fails closed on wrong key/tamper, never falls back to
  plaintext. Key is a 32-byte seam (config db_key_b64/db_key_env today; the
  passkey-derived L3 cold-start key plugs into the same seam later).

Chosen over cgo SQLCipher (would force libsqlcipher + CGO across the project)
and over the ncruces page-level VFS (swaps the driver project-wide); noted as
the upgrade path in a ponytail: comment. Threat model is disk-at-rest only.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 21:22:09 +04:00

48 lines
1.3 KiB
Go

package store
import (
"context"
"testing"
)
func userVersion(t *testing.T, s *Store) int {
t.Helper()
var v int
if err := s.db.QueryRowContext(context.Background(), "PRAGMA user_version").Scan(&v); err != nil {
t.Fatalf("read user_version: %v", err)
}
return v
}
func TestMigrateAppliesOnceAndIsIdempotent(t *testing.T) {
ctx := context.Background()
s := newTestStore(t)
// Empty baseline slice leaves the DB at version 0.
if v := userVersion(t, s); v != 0 {
t.Fatalf("fresh DB user_version = %d, want 0", v)
}
// Append a fake migration and run it: creates a throwaway table, bumps to 1.
migrations = append(migrations, `CREATE TABLE migrate_probe (id INTEGER PRIMARY KEY)`)
t.Cleanup(func() { migrations = migrations[:len(migrations)-1] })
if err := migrate(ctx, s.db); err != nil {
t.Fatalf("migrate: %v", err)
}
if v := userVersion(t, s); v != 1 {
t.Fatalf("after migrate user_version = %d, want 1", v)
}
if _, err := s.db.ExecContext(ctx, "INSERT INTO migrate_probe DEFAULT VALUES"); err != nil {
t.Fatalf("probe table not created: %v", err)
}
// Second run is a no-op — re-running the CREATE would error (no IF NOT EXISTS).
if err := migrate(ctx, s.db); err != nil {
t.Fatalf("migrate second run not idempotent: %v", err)
}
if v := userVersion(t, s); v != 1 {
t.Fatalf("after re-migrate user_version = %d, want 1", v)
}
}