package store_test import ( "context" "errors" "path/filepath" "testing" "kode.naiv.no/olemd/forgejo-mcp-broker/internal/store" ) func tempStorePath(t *testing.T) string { t.Helper() return filepath.Join(t.TempDir(), "broker.db") } // embeddedMigrationCount is the number of *.sql files shipped under // internal/store/migrations/. Bump it when adding a migration so the // idempotency tests stay accurate. const embeddedMigrationCount = 2 func TestOpen_FreshDB_AppliesMigrations(t *testing.T) { ctx := t.Context() s, err := store.Open(ctx, tempStorePath(t)) if err != nil { t.Fatalf("Open: %v", err) } defer s.Close() // broker_meta should exist and carry the schema_version row. var version string row := s.DB().QueryRowContext(ctx, `SELECT value FROM broker_meta WHERE key = ?`, "schema_version") if err := row.Scan(&version); err != nil { t.Fatalf("reading schema_version: %v", err) } if version != "1" { t.Errorf("schema_version = %q, want %q", version, "1") } // schema_migrations should have recorded every embedded migration. var count int row = s.DB().QueryRowContext(ctx, `SELECT COUNT(*) FROM schema_migrations`) if err := row.Scan(&count); err != nil { t.Fatalf("counting schema_migrations: %v", err) } if count != embeddedMigrationCount { t.Errorf("schema_migrations row count = %d, want %d", count, embeddedMigrationCount) } } func TestOpen_ReopenIsIdempotent(t *testing.T) { ctx := t.Context() path := tempStorePath(t) s1, err := store.Open(ctx, path) if err != nil { t.Fatalf("first Open: %v", err) } if err := s1.Close(); err != nil { t.Fatalf("first Close: %v", err) } s2, err := store.Open(ctx, path) if err != nil { t.Fatalf("second Open: %v", err) } defer s2.Close() // Still exactly one applied migration. var count int if err := s2.DB().QueryRowContext(ctx, `SELECT COUNT(*) FROM schema_migrations`).Scan(&count); err != nil { t.Fatalf("counting schema_migrations: %v", err) } if count != embeddedMigrationCount { t.Errorf("after reopen, schema_migrations count = %d, want %d", count, embeddedMigrationCount) } } func TestOpen_NonexistentParent(t *testing.T) { // Config.Validate normally catches this, but the store must still fail // clearly when called with an unreachable path. ctx := t.Context() path := filepath.Join(t.TempDir(), "does", "not", "exist", "broker.db") s, err := store.Open(ctx, path) if err == nil { _ = s.Close() t.Fatal("Open should fail for nonexistent parent directory") } } func TestPing_AfterOpen(t *testing.T) { ctx := t.Context() s, err := store.Open(ctx, tempStorePath(t)) if err != nil { t.Fatalf("Open: %v", err) } defer s.Close() if err := s.Ping(ctx); err != nil { t.Errorf("Ping after Open failed: %v", err) } } func TestPing_AfterClose(t *testing.T) { ctx := t.Context() s, err := store.Open(ctx, tempStorePath(t)) if err != nil { t.Fatalf("Open: %v", err) } if err := s.Close(); err != nil { t.Fatalf("Close: %v", err) } if err := s.Ping(ctx); err == nil { t.Error("Ping after Close should fail") } } func TestPing_CanceledContext(t *testing.T) { ctx := t.Context() s, err := store.Open(ctx, tempStorePath(t)) if err != nil { t.Fatalf("Open: %v", err) } defer s.Close() cctx, cancel := context.WithCancel(ctx) cancel() if err := s.Ping(cctx); err == nil { t.Error("Ping with canceled context should fail") } else if !errors.Is(err, context.Canceled) { // PingContext should surface the cancellation — not a hard requirement // since driver behavior varies slightly, so log rather than fail. t.Logf("Ping error not context.Canceled (ok): %v", err) } } func TestPath_ReturnsAbsolute(t *testing.T) { ctx := t.Context() // Use a relative path to confirm Store.Path returns an absolute one. dir := t.TempDir() t.Chdir(dir) s, err := store.Open(ctx, "broker.db") if err != nil { t.Fatalf("Open: %v", err) } defer s.Close() if !filepath.IsAbs(s.Path()) { t.Errorf("Path = %q, want absolute", s.Path()) } }