* HH-441: harden durable storage and recovery * HH-441: clear recovery review blockers * HH-441: enforce offsite backup failure domain --------- Co-authored-by: Rogee <rogee@ipao.vip>
226 lines
7.1 KiB
Go
226 lines
7.1 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"github.com/gochat/gochat/internal/database"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// setupTestMigrations creates a temporary directory with test migration files
|
|
// that are compatible with SQLite (no PostgreSQL-specific features).
|
|
func setupTestMigrations(t *testing.T) string {
|
|
t.Helper()
|
|
dir := t.TempDir()
|
|
|
|
// Migration 1: Create a simple users table (SQLite-compatible)
|
|
up1 := `
|
|
CREATE TABLE IF NOT EXISTS test_users (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
name TEXT NOT NULL,
|
|
email TEXT NOT NULL UNIQUE,
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
`
|
|
down1 := `DROP TABLE IF EXISTS test_users;`
|
|
require.NoError(t, os.WriteFile(filepath.Join(dir, "000001_create_test_users.up.sql"), []byte(up1), 0644))
|
|
require.NoError(t, os.WriteFile(filepath.Join(dir, "000001_create_test_users.down.sql"), []byte(down1), 0644))
|
|
|
|
// Migration 2: Add a phone column
|
|
up2 := `
|
|
ALTER TABLE test_users ADD COLUMN phone TEXT;
|
|
`
|
|
down2 := `
|
|
-- SQLite doesn't support DROP COLUMN before 3.35.0, so we recreate the table
|
|
CREATE TABLE test_users_backup AS SELECT id, name, email, created_at, updated_at FROM test_users;
|
|
DROP TABLE test_users;
|
|
CREATE TABLE test_users (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
name TEXT NOT NULL,
|
|
email TEXT NOT NULL UNIQUE,
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
INSERT INTO test_users SELECT id, name, email, created_at, updated_at FROM test_users_backup;
|
|
DROP TABLE test_users_backup;
|
|
`
|
|
require.NoError(t, os.WriteFile(filepath.Join(dir, "000002_add_test_user_phone.up.sql"), []byte(up2), 0644))
|
|
require.NoError(t, os.WriteFile(filepath.Join(dir, "000002_add_test_user_phone.down.sql"), []byte(down2), 0644))
|
|
|
|
// Migration 3: Create a posts table
|
|
up3 := `
|
|
CREATE TABLE IF NOT EXISTS test_posts (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
user_id INTEGER NOT NULL REFERENCES test_users(id),
|
|
title TEXT NOT NULL,
|
|
content TEXT,
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
`
|
|
down3 := `DROP TABLE IF EXISTS test_posts;`
|
|
require.NoError(t, os.WriteFile(filepath.Join(dir, "000003_create_test_posts.up.sql"), []byte(up3), 0644))
|
|
require.NoError(t, os.WriteFile(filepath.Join(dir, "000003_create_test_posts.down.sql"), []byte(down3), 0644))
|
|
|
|
return dir
|
|
}
|
|
|
|
// sqliteDBURL creates a SQLite database URL for golang-migrate.
|
|
func sqliteDBURL(t *testing.T) string {
|
|
t.Helper()
|
|
dbPath := filepath.Join(t.TempDir(), "test.db")
|
|
return fmt.Sprintf("sqlite3://%s", dbPath)
|
|
}
|
|
|
|
func TestRunMigrationsUp(t *testing.T) {
|
|
migrationsDir := setupTestMigrations(t)
|
|
dbURL := sqliteDBURL(t)
|
|
|
|
err := database.RunMigrations(dbURL, migrationsDir)
|
|
assert.NoError(t, err, "RunMigrations should apply all pending migrations")
|
|
|
|
// Verify current version is 3
|
|
version, dirty, err := database.CurrentVersion(dbURL, migrationsDir)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, uint(3), version)
|
|
assert.False(t, dirty)
|
|
}
|
|
|
|
func TestRunMigrationsNoChange(t *testing.T) {
|
|
migrationsDir := setupTestMigrations(t)
|
|
dbURL := sqliteDBURL(t)
|
|
|
|
// First run: apply all migrations
|
|
err := database.RunMigrations(dbURL, migrationsDir)
|
|
require.NoError(t, err)
|
|
|
|
// Second run: should return nil (ErrNoChange is handled)
|
|
err = database.RunMigrations(dbURL, migrationsDir)
|
|
assert.NoError(t, err, "Running migrations with no pending changes should succeed")
|
|
}
|
|
|
|
func TestMigrateStepsUp(t *testing.T) {
|
|
migrationsDir := setupTestMigrations(t)
|
|
dbURL := sqliteDBURL(t)
|
|
|
|
// Apply 1 step at a time
|
|
err := database.MigrateSteps(dbURL, migrationsDir, 1)
|
|
require.NoError(t, err)
|
|
|
|
version, dirty, err := database.CurrentVersion(dbURL, migrationsDir)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, uint(1), version)
|
|
assert.False(t, dirty)
|
|
|
|
// Apply 2 more steps
|
|
err = database.MigrateSteps(dbURL, migrationsDir, 2)
|
|
require.NoError(t, err)
|
|
|
|
version, dirty, err = database.CurrentVersion(dbURL, migrationsDir)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, uint(3), version)
|
|
assert.False(t, dirty)
|
|
}
|
|
|
|
func TestMigrateStepsDown(t *testing.T) {
|
|
migrationsDir := setupTestMigrations(t)
|
|
dbURL := sqliteDBURL(t)
|
|
|
|
// First, apply all migrations
|
|
err := database.RunMigrations(dbURL, migrationsDir)
|
|
require.NoError(t, err)
|
|
|
|
// Rollback 1 step
|
|
err = database.MigrateSteps(dbURL, migrationsDir, -1)
|
|
require.NoError(t, err)
|
|
|
|
version, _, err := database.CurrentVersion(dbURL, migrationsDir)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, uint(2), version)
|
|
|
|
// Rollback 2 more steps
|
|
err = database.MigrateSteps(dbURL, migrationsDir, -2)
|
|
require.NoError(t, err)
|
|
|
|
version, _, err = database.CurrentVersion(dbURL, migrationsDir)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, uint(0), version)
|
|
}
|
|
|
|
func TestRollbackMigrations(t *testing.T) {
|
|
migrationsDir := setupTestMigrations(t)
|
|
dbURL := sqliteDBURL(t)
|
|
|
|
// Apply all migrations first
|
|
err := database.RunMigrations(dbURL, migrationsDir)
|
|
require.NoError(t, err)
|
|
|
|
// Rollback all
|
|
err = database.RollbackMigrations(dbURL, migrationsDir)
|
|
assert.NoError(t, err, "RollbackMigrations should succeed")
|
|
|
|
version, _, err := database.CurrentVersion(dbURL, migrationsDir)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, uint(0), version, "All migrations should be rolled back")
|
|
}
|
|
|
|
func TestRollbackNoChange(t *testing.T) {
|
|
migrationsDir := setupTestMigrations(t)
|
|
dbURL := sqliteDBURL(t)
|
|
|
|
// Try to rollback when no migrations have been applied
|
|
err := database.RollbackMigrations(dbURL, migrationsDir)
|
|
assert.NoError(t, err, "Rollback with no applied migrations should succeed (ErrNoChange)")
|
|
}
|
|
|
|
func TestForceVersion(t *testing.T) {
|
|
migrationsDir := setupTestMigrations(t)
|
|
dbURL := sqliteDBURL(t)
|
|
|
|
// Apply all migrations first
|
|
err := database.RunMigrations(dbURL, migrationsDir)
|
|
require.NoError(t, err)
|
|
|
|
// Force version to 1
|
|
err = database.ForceVersion(dbURL, migrationsDir, 1)
|
|
require.NoError(t, err)
|
|
|
|
version, dirty, err := database.CurrentVersion(dbURL, migrationsDir)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, uint(1), version)
|
|
// After Force, dirty state should be cleared
|
|
assert.False(t, dirty)
|
|
}
|
|
|
|
func TestCurrentVersion(t *testing.T) {
|
|
migrationsDir := setupTestMigrations(t)
|
|
dbURL := sqliteDBURL(t)
|
|
|
|
// Before any migrations, version should be 0 (with ErrNoChange handled internally)
|
|
_, _, err := database.CurrentVersion(dbURL, migrationsDir)
|
|
// When no migrations have been applied, Version returns an error
|
|
// This is expected — golang-migrate returns ErrNoChange for version 0
|
|
if err != nil {
|
|
t.Logf("Expected: version 0 with no migrations applied may return error: %v", err)
|
|
}
|
|
|
|
// Apply migrations
|
|
err = database.RunMigrations(dbURL, migrationsDir)
|
|
require.NoError(t, err)
|
|
|
|
version, dirty, err := database.CurrentVersion(dbURL, migrationsDir)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, uint(3), version)
|
|
assert.False(t, dirty)
|
|
}
|
|
func TestRejectDestructiveProductionMigration(t *testing.T) {
|
|
require.Error(t, rejectDestructiveProductionMigration("production", -1))
|
|
require.NoError(t, rejectDestructiveProductionMigration("production", 1))
|
|
require.NoError(t, rejectDestructiveProductionMigration("development", -1))
|
|
}
|