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) } migs, err := loadMigrations() if err != nil { t.Fatalf("load migrations: %v", err) } if n != len(migs) { t.Errorf("re-running migrations should not insert duplicate rows; want %d, got %d", len(migs), n) } } func TestMigration0009Schema(t *testing.T) { t.Parallel() s := openTestStore(t) ctx := context.Background() // host_credentials must have a composite PK (host_id, kind). // We verify this by inserting two rows for the same host_id (different kinds) // and confirming a duplicate (host_id, kind) fails. _, err := s.DB().ExecContext(ctx, `INSERT INTO hosts (id, name, os, arch, enrolled_at) VALUES (?,?,?,?,?)`, "h-0009", "test-host", "linux", "amd64", "2026-01-01T00:00:00Z") if err != nil { t.Fatalf("insert host: %v", err) } now := "2026-01-01T00:00:00Z" if _, err := s.DB().ExecContext(ctx, `INSERT INTO host_credentials (host_id, kind, enc_repo_creds, updated_at) VALUES (?,?,?,?)`, "h-0009", "repo", "enc-repo", now); err != nil { t.Fatalf("insert repo creds: %v", err) } if _, err := s.DB().ExecContext(ctx, `INSERT INTO host_credentials (host_id, kind, enc_repo_creds, updated_at) VALUES (?,?,?,?)`, "h-0009", "admin", "enc-admin", now); err != nil { t.Fatalf("insert admin creds: %v", err) } // Duplicate (host_id, kind) must fail. if _, err := s.DB().ExecContext(ctx, `INSERT INTO host_credentials (host_id, kind, enc_repo_creds, updated_at) VALUES (?,?,?,?)`, "h-0009", "repo", "enc-repo-2", now); err == nil { t.Fatal("expected unique constraint violation on (host_id, kind), got nil") } // CHECK (kind IN ('repo','admin')) must reject an invalid kind. if _, err := s.DB().ExecContext(ctx, `INSERT INTO host_credentials (host_id, kind, enc_repo_creds, updated_at) VALUES (?,?,?,?)`, "h-0009", "other", "enc-other", now); err == nil { t.Fatal("expected CHECK constraint violation on kind='other', got nil") } // host_repo_stats table must exist with expected columns. if _, err := s.DB().ExecContext(ctx, `INSERT INTO host_repo_stats (host_id, lock_present, updated_at) VALUES (?,?,?)`, "h-0009", 0, now); err != nil { t.Fatalf("insert host_repo_stats: %v", err) } var lockPresent int if err := s.DB().QueryRowContext(ctx, `SELECT lock_present FROM host_repo_stats WHERE host_id = ?`, "h-0009", ).Scan(&lockPresent); err != nil { t.Fatalf("select host_repo_stats: %v", err) } if lockPresent != 0 { t.Errorf("expected lock_present=0, got %d", lockPresent) } // CHECK (last_check_status IN ('ok','errors_found','failed')) must reject // an invalid value. if _, err := s.DB().ExecContext(ctx, `UPDATE host_repo_stats SET last_check_status = ? WHERE host_id = ?`, "wat", "h-0009"); err == nil { t.Fatal("expected CHECK constraint violation on last_check_status='wat', got nil") } } 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") } }