| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430 |
- package com.emoon.okr.config;
- import com.emoon.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 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() {
- 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 migrationAddsPeriodStageDatesAndParticipantReportingSnapshot() {
- createBaseTables();
- DatabaseMigrationRunner runner = new DatabaseMigrationRunner(jdbcTemplate, transactionTemplate, backupService);
- runner.run();
- assertColumnExists(jdbcTemplate, "assessment_period", "okr_due_date");
- assertColumnExists(jdbcTemplate, "assessment_period", "self_review_due_date");
- assertColumnExists(jdbcTemplate, "period_participant", "department_id_snapshot");
- assertColumnExists(jdbcTemplate, "period_participant", "superior_id_snapshot");
- assertColumnExists(jdbcTemplate, "period_participant", "position_snapshot");
- assertColumnExists(jdbcTemplate, "period_participant", "role_snapshot");
- assertColumnExists(jdbcTemplate, "period_participant", "evaluator_id");
- assertEquals(1, jdbcTemplate.queryForObject(
- "SELECT COUNT(*) FROM schema_migration WHERE version = 5", Integer.class));
- }
- @Test
- void migrationAddsPeerReviewTablesAndDeadlines() {
- createBaseTables();
- DatabaseMigrationRunner runner = new DatabaseMigrationRunner(jdbcTemplate, transactionTemplate, backupService);
- runner.run();
- runner.run();
- assertColumnExists(jdbcTemplate, "assessment_period", "peer_invitation_deadline");
- assertColumnExists(jdbcTemplate, "assessment_period", "peer_review_deadline");
- assertTableExists(jdbcTemplate, "peer_review_request");
- assertTableExists(jdbcTemplate, "peer_review_invitation");
- assertEquals(1, jdbcTemplate.queryForObject(
- "SELECT COUNT(*) FROM schema_migration WHERE version = 13", Integer.class));
- }
- @Test
- void migrationAddsPeriodDimensionSnapshotAndExplicitTemplateAssignments() {
- createBaseTables();
- DatabaseMigrationRunner runner = new DatabaseMigrationRunner(jdbcTemplate, transactionTemplate, backupService);
- runner.run();
- assertTableExists(jdbcTemplate, "period_dimension");
- assertTableExists(jdbcTemplate, "kpi_template_assignment");
- assertColumnExists(jdbcTemplate, "assessment_period", "company_owner_user_id");
- assertEquals(1, jdbcTemplate.queryForObject(
- "SELECT COUNT(*) FROM schema_migration WHERE version = 8", Integer.class));
- }
- @Test
- void migrationNormalizesLegacyPerformanceTemplatesToFortyPoints() throws Exception {
- createBaseTables();
- jdbcTemplate.execute("""
- CREATE TABLE kpi_template (
- id INTEGER PRIMARY KEY AUTOINCREMENT,
- name VARCHAR(128) NOT NULL,
- category VARCHAR(64),
- items_json TEXT,
- deleted INTEGER NOT NULL DEFAULT 0
- )
- """);
- jdbcTemplate.update("INSERT INTO kpi_template(name, items_json) VALUES (?, ?)",
- "研发工程师", "[{\"name\":\"交付\",\"maxScore\":20},{\"name\":\"质量\",\"maxScore\":15},{\"name\":\"协作\",\"maxScore\":10},{\"name\":\"成长\",\"maxScore\":10},{\"name\":\"创新\",\"maxScore\":5}]");
- new DatabaseMigrationRunner(jdbcTemplate, transactionTemplate, backupService).run();
- String json = jdbcTemplate.queryForObject("SELECT items_json FROM kpi_template WHERE id = 1", String.class);
- var items = new com.fasterxml.jackson.databind.ObjectMapper().readTree(json);
- int total = 0;
- for (var item : items) total += item.get("maxScore").asInt();
- assertEquals(40, total);
- assertEquals(1, jdbcTemplate.queryForObject(
- "SELECT COUNT(*) FROM schema_migration WHERE version = 9", Integer.class));
- }
- @Test
- void migrationBackfillsLegacyParticipantEvaluatorAndExemptsUsersWithoutSuperior() {
- createBaseTables();
- jdbcTemplate.execute("ALTER TABLE sys_user ADD COLUMN superior_id INTEGER");
- jdbcTemplate.execute("ALTER TABLE sys_user ADD COLUMN department_id INTEGER");
- jdbcTemplate.execute("ALTER TABLE sys_user ADD COLUMN position VARCHAR(128)");
- jdbcTemplate.update("INSERT INTO sys_user(id,username,password_hash,real_name,role,superior_id,created_at,updated_at) " +
- "VALUES (1,'manager','x','上级','EMPLOYEE',NULL,datetime('now'),datetime('now'))");
- jdbcTemplate.update("INSERT INTO sys_user(id,username,password_hash,real_name,role,superior_id,department_id,position,created_at,updated_at) " +
- "VALUES (2,'employee','x','员工','EMPLOYEE',1,3,'后端开发工程师',datetime('now'),datetime('now'))");
- jdbcTemplate.execute("""
- CREATE TABLE period_participant (
- id INTEGER PRIMARY KEY AUTOINCREMENT,
- period_id INTEGER NOT NULL,
- user_id INTEGER NOT NULL,
- status VARCHAR(32) NOT NULL DEFAULT 'ACTIVE',
- reason TEXT,
- operated_by INTEGER,
- exempted_at TEXT,
- created_at TEXT NOT NULL,
- updated_at TEXT NOT NULL
- )
- """);
- jdbcTemplate.update("INSERT INTO period_participant(period_id,user_id,status,created_at,updated_at) " +
- "VALUES (1,1,'ACTIVE',datetime('now'),datetime('now'))");
- jdbcTemplate.update("INSERT INTO period_participant(period_id,user_id,status,created_at,updated_at) " +
- "VALUES (1,2,'ACTIVE',datetime('now'),datetime('now'))");
- new DatabaseMigrationRunner(jdbcTemplate, transactionTemplate, backupService).run();
- assertEquals(1L, jdbcTemplate.queryForObject(
- "SELECT evaluator_id FROM period_participant WHERE user_id = 2", Long.class));
- assertEquals("ACTIVE", jdbcTemplate.queryForObject(
- "SELECT status FROM period_participant WHERE user_id = 2", String.class));
- assertEquals("后端开发工程师", jdbcTemplate.queryForObject(
- "SELECT position_snapshot FROM period_participant WHERE user_id = 2", String.class));
- assertEquals(3L, jdbcTemplate.queryForObject(
- "SELECT department_id_snapshot FROM period_participant WHERE user_id = 2", Long.class));
- assertEquals("EMPLOYEE", jdbcTemplate.queryForObject(
- "SELECT role_snapshot FROM period_participant WHERE user_id = 2", String.class));
- assertEquals("EXEMPT_NO_EVALUATOR", jdbcTemplate.queryForObject(
- "SELECT status FROM period_participant WHERE user_id = 1", String.class));
- assertEquals(1, jdbcTemplate.queryForObject(
- "SELECT COUNT(*) FROM schema_migration WHERE version = 10", Integer.class));
- assertEquals(1, jdbcTemplate.queryForObject(
- "SELECT COUNT(*) FROM schema_migration WHERE version = 11", Integer.class));
- }
- @Test
- void migrationAssignsWangkangAsTechnicalDirectorWithoutRewritingExecutingSnapshot() {
- createBaseTables();
- jdbcTemplate.execute("ALTER TABLE sys_user ADD COLUMN position VARCHAR(128)");
- jdbcTemplate.update("INSERT INTO sys_user(id,username,password_hash,real_name,role,position,created_at,updated_at) " +
- "VALUES (3,'wangkang','x','王康','SUPER_ADMIN','后端开发工程师',datetime('now'),datetime('now'))");
- jdbcTemplate.execute("""
- CREATE TABLE period_participant (
- id INTEGER PRIMARY KEY AUTOINCREMENT,
- period_id INTEGER NOT NULL,
- user_id INTEGER NOT NULL,
- status VARCHAR(32) NOT NULL DEFAULT 'ACTIVE',
- position_snapshot VARCHAR(128),
- created_at TEXT NOT NULL,
- updated_at TEXT NOT NULL
- )
- """);
- jdbcTemplate.update("INSERT INTO assessment_period(id,name,start_date,end_date,status,created_at,updated_at) " +
- "VALUES (1,'执行周期','2026-07-01','2026-07-31','EXECUTING',datetime('now'),datetime('now'))");
- jdbcTemplate.update("INSERT INTO period_participant(period_id,user_id,status,position_snapshot,created_at,updated_at) " +
- "VALUES (1,3,'ACTIVE','后端开发工程师',datetime('now'),datetime('now'))");
- new DatabaseMigrationRunner(jdbcTemplate, transactionTemplate, backupService).run();
- assertEquals("技术总监", jdbcTemplate.queryForObject(
- "SELECT position FROM sys_user WHERE username = 'wangkang'", String.class));
- assertEquals("后端开发工程师", jdbcTemplate.queryForObject(
- "SELECT position_snapshot FROM period_participant WHERE user_id = 3", String.class));
- assertEquals(1, jdbcTemplate.queryForObject(
- "SELECT COUNT(*) FROM schema_migration WHERE version = 12", 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");
- }
- @Test
- void shouldBackfillCommentsJsonIntoScoreItems() {
- createBaseTables();
- // Insert a score with comments_json
- jdbcTemplate.update(
- "INSERT INTO performance_score(id, period_id, user_id, evaluator_id, type, comments_json, created_at, updated_at) " +
- "VALUES (1, 1, 1, 2, 'SELF', ?, datetime('now'), datetime('now'))",
- "{\"krScores\":[{\"krId\":1,\"title\":\"KR1\",\"score\":0.8},{\"krId\":2,\"title\":\"KR2\",\"score\":0.6}]," +
- "\"dimensions\":[{\"id\":1,\"name\":\"质量\",\"maxScore\":10,\"score\":8},{\"id\":2,\"name\":\"效率\",\"maxScore\":10,\"score\":7}]}");
- DatabaseMigrationRunner runner = new DatabaseMigrationRunner(jdbcTemplate, transactionTemplate, backupService);
- runner.run();
- // Verify performance_score_item was populated
- int krCount = jdbcTemplate.queryForObject(
- "SELECT COUNT(*) FROM performance_score_item WHERE score_id = 1 AND item_type = 'KR'", Integer.class);
- assertEquals(2, krCount, "Should have 2 KR items");
- int dimCount = jdbcTemplate.queryForObject(
- "SELECT COUNT(*) FROM performance_score_item WHERE score_id = 1 AND item_type = 'DIMENSION'", Integer.class);
- assertEquals(2, dimCount, "Should have 2 dimension items");
- // Verify original comments_json is preserved
- String json = jdbcTemplate.queryForObject(
- "SELECT comments_json FROM performance_score WHERE id = 1", String.class);
- assertNotNull(json);
- assertTrue(json.contains("krScores"));
- // Verify migrations recorded
- assertEquals(1, jdbcTemplate.queryForObject(
- "SELECT COUNT(*) FROM schema_migration WHERE version = 3", Integer.class));
- }
- private void createBaseTables() {
- jdbcTemplate.execute("""
- CREATE TABLE sys_user (
- id INTEGER PRIMARY KEY AUTOINCREMENT,
- username VARCHAR(64) NOT NULL UNIQUE,
- password_hash VARCHAR(256) NOT NULL,
- real_name VARCHAR(64) NOT NULL,
- role VARCHAR(20) NOT NULL DEFAULT 'EMPLOYEE',
- deleted INTEGER NOT NULL DEFAULT 0,
- created_at TEXT NOT NULL,
- updated_at TEXT NOT NULL
- )
- """);
- jdbcTemplate.execute("""
- CREATE TABLE assessment_period (
- id INTEGER PRIMARY KEY AUTOINCREMENT,
- name VARCHAR(128) NOT NULL,
- period_type VARCHAR(20) NOT NULL DEFAULT 'MONTHLY',
- start_date TEXT NOT NULL,
- end_date TEXT NOT NULL,
- status VARCHAR(20) NOT NULL DEFAULT 'DRAFT',
- created_at TEXT NOT NULL,
- updated_at TEXT NOT NULL
- )
- """);
- jdbcTemplate.execute("""
- CREATE TABLE performance_score (
- id INTEGER PRIMARY KEY AUTOINCREMENT,
- period_id INTEGER NOT NULL,
- user_id INTEGER NOT NULL,
- evaluator_id INTEGER NOT NULL,
- type VARCHAR(20) NOT NULL,
- comments_json TEXT,
- deleted INTEGER NOT NULL DEFAULT 0,
- created_at TEXT NOT NULL,
- updated_at TEXT NOT NULL
- )
- """);
- jdbcTemplate.execute("""
- CREATE TABLE performance_feedback (
- id INTEGER PRIMARY KEY AUTOINCREMENT,
- period_id INTEGER NOT NULL,
- user_id INTEGER NOT NULL,
- superior_id INTEGER NOT NULL,
- created_at TEXT NOT NULL,
- updated_at TEXT NOT NULL
- )
- """);
- jdbcTemplate.execute("""
- CREATE TABLE okr_objective (
- id INTEGER PRIMARY KEY AUTOINCREMENT,
- period_id INTEGER NOT NULL,
- user_id INTEGER,
- dept_id INTEGER,
- title VARCHAR(256) NOT NULL,
- created_at TEXT NOT NULL,
- updated_at TEXT NOT NULL
- )
- """);
- jdbcTemplate.execute("""
- CREATE TABLE kr_key_result (
- id INTEGER PRIMARY KEY AUTOINCREMENT,
- objective_id INTEGER NOT NULL,
- title VARCHAR(256) NOT NULL,
- source_kr_id INTEGER,
- created_at TEXT NOT NULL,
- updated_at TEXT NOT NULL
- )
- """);
- jdbcTemplate.execute("""
- CREATE TABLE notification (
- id INTEGER PRIMARY KEY AUTOINCREMENT,
- user_id INTEGER NOT NULL,
- type VARCHAR(32) NOT NULL,
- title VARCHAR(256) NOT NULL,
- is_read INTEGER DEFAULT 0,
- created_at TEXT NOT NULL
- )
- """);
- }
- private void assertColumnExists(JdbcTemplate jdbcTemplate, String tableName, String columnName) {
- boolean exists = jdbcTemplate.queryForList("PRAGMA table_info(" + tableName + ")").stream()
- .anyMatch(row -> columnName.equalsIgnoreCase(String.valueOf(row.get("name"))));
- assertTrue(exists, tableName + "." + columnName + " should exist");
- }
- @Test
- void shouldMigrateLegacy4x15DimensionsTo4x10() {
- createBaseTables();
- jdbcTemplate.execute("""
- CREATE TABLE assessment_dimension (
- id INTEGER PRIMARY KEY AUTOINCREMENT,
- name VARCHAR(64) NOT NULL,
- max_score INTEGER NOT NULL DEFAULT 10,
- sort_order INTEGER DEFAULT 0,
- deleted INTEGER NOT NULL DEFAULT 0,
- created_at TEXT NOT NULL,
- updated_at TEXT NOT NULL
- )
- """);
- jdbcTemplate.update("INSERT INTO assessment_dimension(id,name,max_score,sort_order,created_at,updated_at) VALUES (1,\"交付质量\",15,1,datetime(\"now\"),datetime(\"now\"))");
- jdbcTemplate.update("INSERT INTO assessment_dimension(id,name,max_score,sort_order,created_at,updated_at) VALUES (2,\"协作沟通\",15,2,datetime(\"now\"),datetime(\"now\"))");
- jdbcTemplate.update("INSERT INTO assessment_dimension(id,name,max_score,sort_order,created_at,updated_at) VALUES (3,\"学习成长\",15,3,datetime(\"now\"),datetime(\"now\"))");
- jdbcTemplate.update("INSERT INTO assessment_dimension(id,name,max_score,sort_order,created_at,updated_at) VALUES (4,\"创新贡献\",15,4,datetime(\"now\"),datetime(\"now\"))");
- DatabaseMigrationRunner runner = new DatabaseMigrationRunner(jdbcTemplate, transactionTemplate, backupService);
- runner.run();
- for (int i = 1; i <= 4; i++) {
- int maxScore = jdbcTemplate.queryForObject(
- "SELECT max_score FROM assessment_dimension WHERE id = " + i, Integer.class);
- assertEquals(10, maxScore, "Dim " + i + " should be 10");
- }
- assertEquals(1, jdbcTemplate.queryForObject(
- "SELECT COUNT(*) FROM schema_migration WHERE version = 4", Integer.class));
- }
- private void assertTableExists(JdbcTemplate jdbcTemplate, String tableName) {
- Integer count = jdbcTemplate.queryForObject("""
- SELECT COUNT(*) FROM sqlite_master
- WHERE type = 'table' AND name = ?
- """, Integer.class, tableName);
- assertEquals(1, count);
- }
- }
|