Files
restic-manager/internal/store/store_test.go
T
steve c275f4ff4c phase 1 foundations: api types, store, crypto, auth
Lands the bottom three layers of Phase 1:

P1-08 internal/api: protocol_version + envelope + every WS message
  shape from spec.md §6.2 (Hello, Heartbeat, Job*, Schedule*, etc).
  Wire-format tests pin the JSON shape so a rename here breaks
  tests instead of silently breaking the agent.

P1-02 + P1-03 internal/store: SQLite via modernc.org/sqlite,
  embed.FS + a tiny version table for hand-rolled migrations.
  0001_initial.sql covers every table from spec.md §5 plus
  enrollment_tokens and host_schedule_version. Typed accessors
  for users / sessions / enrollment / audit. WAL + foreign_keys
  + busy_timeout on by default.

P1-06 internal/crypto: XChaCha20-Poly1305 AEAD wrapper with
  per-message random nonce. Key file lifecycle (generate +
  refuse-to-overwrite, load with size validation). Optional
  additionalData binds ciphertext to the row that owns it.

P1-04 internal/auth (partial — passwords + tokens; sessions
  middleware lands with the HTTP handlers): argon2id following
  RFC 9106 (64 MiB / t=3 / p=4 / 32B), constant-time verify.
  HashToken stores SHA-256 of session/agent/enrollment tokens
  so a stolen DB doesn't hand over credentials.

Build floor moves to Go 1.25 (modernc.org/sqlite v1.50+ requires
it); CI + Dockerfile + README updated. Markdown lint diagnostics
on tasks.md cleared.

All packages tested. ~70 new tests pass in <1s.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 00:24:40 +01:00

94 lines
2.3 KiB
Go

package store
import (
"context"
"path/filepath"
"testing"
)
// openTestStore opens an isolated file-backed db in a t.TempDir.
// In-memory + shared-cache works too but file makes failures easier
// to inspect when a test panics.
func openTestStore(t *testing.T) *Store {
t.Helper()
dir := t.TempDir()
s, err := Open(context.Background(), filepath.Join(dir, "rm.db"))
if err != nil {
t.Fatalf("open: %v", err)
}
t.Cleanup(func() { _ = s.Close() })
return s
}
func TestOpenAppliesMigrations(t *testing.T) {
t.Parallel()
s := openTestStore(t)
row := s.DB().QueryRow(`SELECT MAX(version) FROM schema_version`)
var v int
if err := row.Scan(&v); err != nil {
t.Fatalf("scan: %v", err)
}
if v < 1 {
t.Fatalf("expected at least migration 1 applied, got %d", v)
}
// Spot-check a few tables exist with expected columns.
tables := []string{"users", "sessions", "hosts", "repos",
"credentials", "schedules", "jobs", "job_logs",
"snapshots", "alerts", "audit_log",
"enrollment_tokens", "host_schedule_version"}
for _, tbl := range tables {
row := s.DB().QueryRow(
`SELECT name FROM sqlite_master WHERE type='table' AND name = ?`, tbl)
var got string
if err := row.Scan(&got); err != nil {
t.Errorf("table %q missing: %v", tbl, err)
}
}
}
func TestMigrateIsIdempotent(t *testing.T) {
t.Parallel()
dir := t.TempDir()
path := filepath.Join(dir, "rm.db")
for i := 0; i < 3; i++ {
s, err := Open(context.Background(), path)
if err != nil {
t.Fatalf("open #%d: %v", i, err)
}
_ = s.Close()
}
s, err := Open(context.Background(), path)
if err != nil {
t.Fatalf("final open: %v", err)
}
defer s.Close()
row := s.DB().QueryRow(`SELECT COUNT(*) FROM schema_version`)
var n int
if err := row.Scan(&n); err != nil {
t.Fatalf("scan: %v", err)
}
if n != 1 {
t.Errorf("re-running migrations should not insert duplicate rows; got %d", n)
}
}
func TestForeignKeysEnforced(t *testing.T) {
t.Parallel()
s := openTestStore(t)
// Inserting a session with a non-existent user should fail because
// FKs are on. Without the pragma, SQLite silently accepts this.
_, err := s.DB().Exec(
`INSERT INTO sessions (id, user_id, created_at, expires_at)
VALUES (?, ?, datetime('now'), datetime('now','+1 hour'))`,
"sess1", "no-such-user")
if err == nil {
t.Fatal("expected FK violation, got nil")
}
}