|
|
@@ -1,17 +1,119 @@
|
|
|
package com.yimeng.okr.config;
|
|
|
|
|
|
+import com.yimeng.okr.service.DatabaseBackupService;
|
|
|
+import org.junit.jupiter.api.BeforeEach;
|
|
|
import org.junit.jupiter.api.Test;
|
|
|
+import org.junit.jupiter.api.io.TempDir;
|
|
|
import org.springframework.jdbc.core.JdbcTemplate;
|
|
|
+import org.springframework.jdbc.datasource.DataSourceTransactionManager;
|
|
|
import org.springframework.jdbc.datasource.SingleConnectionDataSource;
|
|
|
+import org.springframework.transaction.support.TransactionTemplate;
|
|
|
|
|
|
-import static org.junit.jupiter.api.Assertions.assertEquals;
|
|
|
-import static org.junit.jupiter.api.Assertions.assertTrue;
|
|
|
+import java.nio.file.Files;
|
|
|
+import java.nio.file.Path;
|
|
|
+
|
|
|
+import static org.junit.jupiter.api.Assertions.*;
|
|
|
|
|
|
class DatabaseMigrationRunnerTest {
|
|
|
|
|
|
+ private JdbcTemplate jdbcTemplate;
|
|
|
+ private TransactionTemplate transactionTemplate;
|
|
|
+ private DatabaseBackupService backupService;
|
|
|
+ private Path tempDir;
|
|
|
+
|
|
|
+ @BeforeEach
|
|
|
+ void setUp(@TempDir Path tempDir) throws Exception {
|
|
|
+ this.tempDir = tempDir;
|
|
|
+ // Create temp db file for backup service to find
|
|
|
+ Path dbFile = tempDir.resolve("test.db");
|
|
|
+ Files.createFile(dbFile);
|
|
|
+
|
|
|
+ SingleConnectionDataSource dataSource = new SingleConnectionDataSource();
|
|
|
+ dataSource.setDriverClassName("org.sqlite.JDBC");
|
|
|
+ dataSource.setUrl("jdbc:sqlite::memory:");
|
|
|
+ dataSource.setSuppressClose(true);
|
|
|
+ jdbcTemplate = new JdbcTemplate(dataSource);
|
|
|
+ DataSourceTransactionManager txManager = new DataSourceTransactionManager(dataSource);
|
|
|
+ transactionTemplate = new TransactionTemplate(txManager);
|
|
|
+ backupService = new DatabaseBackupService(
|
|
|
+ "jdbc:sqlite:" + dbFile.toString(),
|
|
|
+ tempDir.resolve("backups").toString());
|
|
|
+ }
|
|
|
+
|
|
|
@Test
|
|
|
void runAppliesVersionedMigrationsOnlyOnce() {
|
|
|
- JdbcTemplate jdbcTemplate = jdbcTemplate();
|
|
|
+ createBaseTables();
|
|
|
+
|
|
|
+ DatabaseMigrationRunner runner = new DatabaseMigrationRunner(jdbcTemplate, transactionTemplate, backupService);
|
|
|
+ runner.run();
|
|
|
+ runner.run();
|
|
|
+
|
|
|
+ assertColumnExists(jdbcTemplate, "sys_user", "status");
|
|
|
+ assertColumnExists(jdbcTemplate, "performance_score", "published_at");
|
|
|
+ assertTableExists(jdbcTemplate, "period_participant");
|
|
|
+ assertEquals(1, jdbcTemplate.queryForObject(
|
|
|
+ "SELECT COUNT(*) FROM schema_migration WHERE version = 1", Integer.class));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void shouldRollbackMigrationAndNotRecordVersionWhenMigrationFails() {
|
|
|
+ createBaseTables();
|
|
|
+
|
|
|
+ // Create a custom runner with a failing migration
|
|
|
+ DatabaseMigrationRunner runner = new DatabaseMigrationRunner(jdbcTemplate, transactionTemplate, backupService) {
|
|
|
+ @Override
|
|
|
+ public void run(String... args) {
|
|
|
+ jdbcTemplate.execute("""
|
|
|
+ CREATE TABLE IF NOT EXISTS schema_migration (
|
|
|
+ version INTEGER PRIMARY KEY,
|
|
|
+ description TEXT NOT NULL,
|
|
|
+ applied_at TEXT NOT NULL
|
|
|
+ )
|
|
|
+ """);
|
|
|
+
|
|
|
+ // Apply migration 1 first (succeeds)
|
|
|
+ MigrationContext ctx = new MigrationContext(jdbcTemplate);
|
|
|
+ transactionTemplate.executeWithoutResult(status -> {
|
|
|
+ jdbcTemplate.execute("ALTER TABLE sys_user ADD COLUMN status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE'");
|
|
|
+ jdbcTemplate.update(
|
|
|
+ "INSERT INTO schema_migration(version, description, applied_at) VALUES (?, ?, datetime('now'))",
|
|
|
+ 1, "test migration 1");
|
|
|
+ });
|
|
|
+
|
|
|
+ // Now try a failing migration 2
|
|
|
+ try {
|
|
|
+ transactionTemplate.executeWithoutResult(status -> {
|
|
|
+ jdbcTemplate.execute("ALTER TABLE sys_user ADD COLUMN status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE'");
|
|
|
+ // Force failure
|
|
|
+ jdbcTemplate.execute("THIS IS INVALID SQL");
|
|
|
+ jdbcTemplate.update(
|
|
|
+ "INSERT INTO schema_migration(version, description, applied_at) VALUES (?, ?, datetime('now'))",
|
|
|
+ 2, "should not be recorded");
|
|
|
+ });
|
|
|
+ fail("Expected exception was not thrown");
|
|
|
+ } catch (Exception e) {
|
|
|
+ // Expected — migration failed
|
|
|
+ }
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ try {
|
|
|
+ runner.run();
|
|
|
+ } catch (Exception e) {
|
|
|
+ // May throw RuntimeException wrapping the migration failure
|
|
|
+ }
|
|
|
+
|
|
|
+ // Migration 1 should be recorded
|
|
|
+ assertEquals(1, jdbcTemplate.queryForObject(
|
|
|
+ "SELECT COUNT(*) FROM schema_migration WHERE version = 1", Integer.class));
|
|
|
+ // Migration 2 should NOT be recorded
|
|
|
+ assertEquals(0, jdbcTemplate.queryForObject(
|
|
|
+ "SELECT COUNT(*) FROM schema_migration WHERE version = 2", Integer.class));
|
|
|
+ // Column should exist (from migration 1, not rolled back)
|
|
|
+ assertColumnExists(jdbcTemplate, "sys_user", "status");
|
|
|
+ }
|
|
|
+
|
|
|
+ private void createBaseTables() {
|
|
|
jdbcTemplate.execute("""
|
|
|
CREATE TABLE sys_user (
|
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
|
@@ -55,24 +157,6 @@ class DatabaseMigrationRunnerTest {
|
|
|
updated_at TEXT NOT NULL
|
|
|
)
|
|
|
""");
|
|
|
-
|
|
|
- DatabaseMigrationRunner runner = new DatabaseMigrationRunner(jdbcTemplate);
|
|
|
- runner.run();
|
|
|
- runner.run();
|
|
|
-
|
|
|
- assertColumnExists(jdbcTemplate, "sys_user", "status");
|
|
|
- assertColumnExists(jdbcTemplate, "performance_score", "published_at");
|
|
|
- assertTableExists(jdbcTemplate, "period_participant");
|
|
|
- assertEquals(1, jdbcTemplate.queryForObject(
|
|
|
- "SELECT COUNT(*) FROM schema_migration WHERE version = 1", Integer.class));
|
|
|
- }
|
|
|
-
|
|
|
- private JdbcTemplate jdbcTemplate() {
|
|
|
- SingleConnectionDataSource dataSource = new SingleConnectionDataSource();
|
|
|
- dataSource.setDriverClassName("org.sqlite.JDBC");
|
|
|
- dataSource.setUrl("jdbc:sqlite::memory:");
|
|
|
- dataSource.setSuppressClose(true);
|
|
|
- return new JdbcTemplate(dataSource);
|
|
|
}
|
|
|
|
|
|
private void assertColumnExists(JdbcTemplate jdbcTemplate, String tableName, String columnName) {
|