Files
gochat/backend/cmd/migrate/migrate_test.go
T
rogee aeddedf2a3 Reorganize repo: backend/, deploy/, docs/ layout + AGENTS.md
Restructure the monorepo into clear top-level directories:
- backend/: Go module root (cmd, internal, pkg, configs, migrations,
  docs/swagger, scripts, tests, go.mod, Makefile, .air.toml)
- deploy/: Docker (Dockerfile, docker-compose*), quickstart, fluentd
- docs/: project documentation + reports/ (moved from repo root)
- AGENTS.md: new AI coding-agent guide at repo root

Update all references to the new layout:
- Dockerfile: COPY backend/go.mod, COPY backend/ (context = repo root)
- docker-compose files: context ../.., dockerfile deploy/docker/Dockerfile,
  env_file ../../.env, volume mounts ../../backend:/app
- deploy/quickstart/compose.yaml: dockerfile deploy/docker/Dockerfile
- CI: working-directory: backend for go commands, file deploy/docker/Dockerfile,
  coverage path backend/coverage.out, health_check backend/scripts/
- backend/Makefile: docker target uses -f ../deploy/docker/Dockerfile ../
- README: architecture tree, quickstart, config paths updated

Move root stray scripts (rename_models.*, run_m11_tests.sh, verify_build.sh,
gorm_bool_main.go) to backend/scripts/legacy/. All moves via git mv to
preserve history. Build, vet, SQLite tests, and docker compose config verified.
2026-07-07 14:44:12 +08:00

220 lines
6.9 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)
version, dirty, 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)
}