Files
Redflag/aggregator-server/internal/database/migrations/idempotency_test.go
jpetree331 ab676c3b83 test(database): B-1 pre-fix tests for migration and schema bugs
Pre-fix test suite documenting 9 database migration and schema
integrity bugs. Tests FAIL where they assert correct post-fix
behavior, PASS where they document current buggy state.

Tests added:
- F-B1-11 P0: main.go swallows migration errors (3 tests)
- F-B1-13: Duplicate migration numbers 009/012 (2 tests)
- F-B1-1: Migration 024 self-insert into schema_migrations (2 tests)
- F-B1-2: Migration 024 references non-existent column (2 tests)
- F-B1-3: Migration 018 wrong file suffix (2 tests)
- F-B1-4: Migration 018 GRANT to wrong role (1 test)
- F-B1-15: 7+ migrations not idempotent (2 tests)
- F-B1-5: Missing agent_commands sent_at index (2 tests)
- F-B1-6: N+1 query in GetDashboardStats (2 tests)
- F-B1-10: No background refresh token cleanup (2 tests)

Current state: 10 PASS, 10 FAIL, 0 SKIP.
All A-series tests continue to pass (no regressions).
See docs/B1_PreFix_Tests.md for full inventory.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 06:42:19 -04:00

128 lines
3.9 KiB
Go

package migrations_test
// idempotency_test.go — Pre-fix tests for migration idempotency (ETHOS #4).
//
// F-B1-15 LOW: 7+ migrations lack IF NOT EXISTS on CREATE/ALTER statements.
// ETHOS #4 requires all schema changes to be idempotent.
// Running migrations twice on an existing DB will fail.
//
// Run: cd aggregator-server && go test ./internal/database/migrations/... -v -run TestMigrations
import (
"fmt"
"os"
"strings"
"testing"
)
// checkIdempotency counts non-idempotent SQL statements in a migration file.
// Go's regexp doesn't support negative lookahead, so we use string matching.
func checkIdempotency(src string) (violations int, details []string) {
lines := strings.Split(src, "\n")
for i, line := range lines {
lower := strings.ToLower(strings.TrimSpace(line))
// Skip comments
if strings.HasPrefix(lower, "--") {
continue
}
// CREATE TABLE without IF NOT EXISTS
if strings.Contains(lower, "create table") && !strings.Contains(lower, "if not exists") {
violations++
details = append(details, fmt.Sprintf(" line %d: CREATE TABLE without IF NOT EXISTS", i+1))
}
// CREATE INDEX without IF NOT EXISTS
if (strings.Contains(lower, "create index") || strings.Contains(lower, "create unique index")) &&
!strings.Contains(lower, "if not exists") {
violations++
details = append(details, fmt.Sprintf(" line %d: CREATE INDEX without IF NOT EXISTS", i+1))
}
// ADD COLUMN without IF NOT EXISTS
if strings.Contains(lower, "add column") && !strings.Contains(lower, "if not exists") {
violations++
details = append(details, fmt.Sprintf(" line %d: ADD COLUMN without IF NOT EXISTS", i+1))
}
}
return
}
// ---------------------------------------------------------------------------
// Test 4.1 — Documents that idempotency violations exist (F-B1-15)
//
// Category: PASS-NOW (documents violations)
// ---------------------------------------------------------------------------
func TestMigrationsHaveIdempotencyViolations(t *testing.T) {
files, err := os.ReadDir(".")
if err != nil {
t.Fatalf("failed to read migrations directory: %v", err)
}
totalViolations := 0
for _, f := range files {
if !strings.HasSuffix(f.Name(), ".up.sql") {
continue
}
// Skip A-series migrations (025, 026) which are already idempotent
if strings.HasPrefix(f.Name(), "025_") || strings.HasPrefix(f.Name(), "026_") {
continue
}
content, err := os.ReadFile(f.Name())
if err != nil {
continue
}
violations, details := checkIdempotency(string(content))
if violations > 0 {
totalViolations += violations
t.Logf("[WARNING] [server] [database] %s: %d violations", f.Name(), violations)
for _, d := range details {
t.Logf(" %s", d)
}
}
}
if totalViolations == 0 {
t.Error("[ERROR] [server] [database] F-B1-15 already fixed: no idempotency violations found")
}
t.Logf("[INFO] [server] [database] F-B1-15 confirmed: %d idempotency violations in pre-A-series migrations", totalViolations)
}
// ---------------------------------------------------------------------------
// Test 4.2 — All migrations MUST be idempotent (assert fix)
//
// Category: FAIL-NOW / PASS-AFTER-FIX
// ---------------------------------------------------------------------------
func TestAllMigrationsAreIdempotent(t *testing.T) {
files, err := os.ReadDir(".")
if err != nil {
t.Fatalf("failed to read migrations directory: %v", err)
}
for _, f := range files {
if !strings.HasSuffix(f.Name(), ".up.sql") {
continue
}
// Skip A-series migrations (already idempotent)
if strings.HasPrefix(f.Name(), "025_") || strings.HasPrefix(f.Name(), "026_") {
continue
}
content, err := os.ReadFile(f.Name())
if err != nil {
continue
}
violations, details := checkIdempotency(string(content))
if violations > 0 {
t.Errorf("[ERROR] [server] [database] %s: %d idempotency violations\n"+
"F-B1-15: all schema changes must use IF NOT EXISTS per ETHOS #4.\n%s",
f.Name(), violations, strings.Join(details, "\n"))
}
}
}