forgejo-mcp-broker/internal/store/store_internal_test.go

170 lines
4.6 KiB
Go
Raw Normal View History

package store
import (
"context"
"database/sql"
"path/filepath"
"strings"
"testing"
"testing/fstest"
_ "modernc.org/sqlite"
)
// openRawDB opens a raw *sql.DB to a fresh temp SQLite file and wraps it in
// a Store — without applying the real embedded migrations. Tests use it to
// exercise migrate() with synthetic migration sets.
func openRawDB(t *testing.T) *Store {
t.Helper()
path := filepath.Join(t.TempDir(), "broker.db")
db, err := sql.Open("sqlite", buildDSN(path))
if err != nil {
t.Fatalf("sql.Open: %v", err)
}
t.Cleanup(func() { _ = db.Close() })
return &Store{db: db, path: path}
}
func TestMigrate_AppliesOnlyPendingVersions(t *testing.T) {
ctx := t.Context()
s := openRawDB(t)
fsys := fstest.MapFS{
"m/0001_a.sql": {Data: []byte("CREATE TABLE t1 (x INTEGER);")},
"m/0002_b.sql": {Data: []byte("CREATE TABLE t2 (y INTEGER);")},
}
if err := s.migrate(ctx, fsys, "m"); err != nil {
t.Fatalf("first migrate: %v", err)
}
// Re-running is a no-op; both migrations already recorded.
if err := s.migrate(ctx, fsys, "m"); err != nil {
t.Fatalf("second migrate: %v", err)
}
var count int
if err := s.db.QueryRowContext(ctx,
`SELECT COUNT(*) FROM schema_migrations`).Scan(&count); err != nil {
t.Fatalf("count: %v", err)
}
if count != 2 {
t.Errorf("applied count = %d, want 2", count)
}
}
func TestMigrate_BadSQLRollsBack(t *testing.T) {
ctx := t.Context()
s := openRawDB(t)
fsys := fstest.MapFS{
"m/0001_good.sql": {Data: []byte("CREATE TABLE ok (x INTEGER);")},
"m/0002_bad.sql": {Data: []byte("THIS IS NOT SQL")},
}
err := s.migrate(ctx, fsys, "m")
if err == nil {
t.Fatal("expected migrate to fail on bad SQL")
}
if !strings.Contains(err.Error(), "0002_bad.sql") {
t.Errorf("error should mention failing migration: %v", err)
}
// 0001 should have been applied and committed; 0002 should not.
var count int
if err := s.db.QueryRowContext(ctx,
`SELECT COUNT(*) FROM schema_migrations`).Scan(&count); err != nil {
t.Fatalf("count: %v", err)
}
if count != 1 {
t.Errorf("after failed migrate, applied count = %d, want 1", count)
}
}
func TestMigrate_MalformedFilenameFails(t *testing.T) {
ctx := t.Context()
s := openRawDB(t)
fsys := fstest.MapFS{
"m/no_number.sql": {Data: []byte("SELECT 1;")},
}
err := s.migrate(ctx, fsys, "m")
if err == nil || !strings.Contains(err.Error(), "non-numeric") {
t.Errorf("want malformed-filename error, got %v", err)
}
}
func TestMigrate_LoadAppliedVersionsOnClosedDBFails(t *testing.T) {
// Exercise the error path in loadAppliedVersions by closing the DB
// before migrate reads schema_migrations.
ctx := t.Context()
s := openRawDB(t)
if _, err := s.db.ExecContext(ctx, `
CREATE TABLE schema_migrations (
version INTEGER PRIMARY KEY,
name TEXT NOT NULL,
applied_at TEXT NOT NULL
)`); err != nil {
t.Fatalf("seed: %v", err)
}
// Close so the subsequent query fails.
if err := s.db.Close(); err != nil {
t.Fatalf("close: %v", err)
}
_, err := loadAppliedVersions(ctx, s.db)
if err == nil {
t.Error("expected error from loadAppliedVersions on closed DB")
}
}
func TestMigrate_RecordStepFails_OnPKConflict(t *testing.T) {
// A migration that manually inserts its own version row forces
// applyMigration's own INSERT into schema_migrations to fail with a
// primary-key conflict. Exercises the record-step error branch.
ctx := t.Context()
s := openRawDB(t)
fsys := fstest.MapFS{
"m/0001_hijack.sql": {Data: []byte(
`INSERT INTO schema_migrations (version, name) VALUES (1, 'hijack');`)},
}
err := s.migrate(ctx, fsys, "m")
if err == nil || !strings.Contains(err.Error(), "record") {
t.Errorf("want record-fail error, got %v", err)
}
}
func TestLoadAppliedVersions_ScanError(t *testing.T) {
// Seed a schema_migrations table with a non-integer version so Scan
// into *int fails. Exercises loadAppliedVersions' scan-error branch.
ctx := t.Context()
s := openRawDB(t)
if _, err := s.db.ExecContext(ctx, `CREATE TABLE schema_migrations (
version TEXT PRIMARY KEY,
name TEXT NOT NULL,
applied_at TEXT NOT NULL
)`); err != nil {
t.Fatalf("create table: %v", err)
}
if _, err := s.db.ExecContext(ctx,
`INSERT INTO schema_migrations VALUES ('not-a-number', 'x', 'now')`); err != nil {
t.Fatalf("seed row: %v", err)
}
if _, err := loadAppliedVersions(ctx, s.db); err == nil {
t.Error("expected scan error on non-int version")
}
}
func TestMigrate_CanceledContext(t *testing.T) {
s := openRawDB(t)
ctx, cancel := context.WithCancel(t.Context())
cancel()
err := s.migrate(ctx, fstest.MapFS{"m/0001_a.sql": {Data: []byte("SELECT 1;")}}, "m")
if err == nil {
t.Error("expected error on canceled context")
}
}