package com.battery.business.runtime; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.InputStream; import java.nio.charset.StandardCharsets; import java.util.Set; import org.flywaydb.core.api.Location; import org.flywaydb.core.internal.resolver.ChecksumCalculator; import org.flywaydb.core.internal.resource.classpath.ClassPathResource; import org.junit.jupiter.api.Test; /** Ticket03 最终数据库基线的结构契约。 */ class FinalDatabaseBaselineContractTest { private static final Set REQUIRED_TABLES = Set.of( "t_battery_group", "t_battery_group_field", "t_battery_group_relation_field", "t_device_adapter", "t_detection_cabinet", "t_detection_channel", "t_collector", "t_test", "t_test_battery", "t_test_channel_binding", "t_test_collector_channel", "t_test_warning_threshold", "t_test_realtime_record", "t_test_step_record", "t_test_cycle_record", "t_test_data_revision_action", "t_test_warning_exception"); private static final Set RETIRED_TABLES = Set.of( "t_test_field_snapshot", "t_test_battery_field", "t_test_plan_step", "t_test_audit", "t_protocol_message", "t_test_reception_interval", "t_history_sync_cursor", "t_data_gap", "t_channel_lease", "t_collector_channel_lease", "t_test_data_override", "t_test_console_import_record"); @Test void finalBaselineKeepsTheChecksumAlreadyAppliedToDevelopmentDatabase() { // 已执行的基线必须保持 Flyway 校验值,新增字段只能追加迁移。 ClassPathResource baseline = new ClassPathResource(new Location("classpath:db/migration-final"), "db/migration-final/V2__final_business_baseline.sql", getClass().getClassLoader(), StandardCharsets.UTF_8); assertEquals(-1864711325, ChecksumCalculator.calculate(baseline)); } @Test void finalBaselineContainsOnlyTheAcceptedBusinessModel() { String sql = resource("db/migration-final/V2__final_business_baseline.sql"); String platformSql = resource("db/migration-final/V1__ruoyi_platform_baseline.sql"); for (String table : REQUIRED_TABLES) { assertTrue(sql.matches("(?s).*CREATE TABLE\\s+" + table + "\\s*\\(.*"), "缺少最终业务表 " + table); } for (String table : RETIRED_TABLES) { assertFalse(sql.matches("(?s).*CREATE TABLE\\s+" + table + "\\s*\\(.*"), "不应创建废止表 " + table); } assertFalse(sql.matches("(?is).*\\bFOREIGN\\s+KEY\\b.*"), "最终基线不得建立物理外键"); assertFalse(platformSql.matches("(?is).*\\bFOREIGN\\s+KEY\\b.*"), "平台基线不得建立物理外键"); assertFalse(platformSql.matches("(?is).*\\bINT\\s+NOT\\s+NULL\\s+AUTO_INCREMENT\\b.*"), "最终基线自增主键统一使用 BIGINT"); assertTrue(sql.contains("test_battery_id BIGINT NOT NULL AUTO_INCREMENT")); assertTrue(sql.contains("DATETIME(3)")); assertTrue(sql.contains("before_content_json JSON NULL")); assertTrue(sql.contains("source_identity VARCHAR(256) NOT NULL")); assertTrue(sql.contains("adapter_timezone_id VARCHAR(64) NOT NULL")); assertTrue(sql.contains("connection_status IN ('ONLINE', 'OFFLINE')")); assertTrue(sql.contains("reception_status IN ('NOT_STARTED', 'RECEIVING', 'PAUSED', 'ENDED')")); assertTrue(sql.contains("threshold_id BIGINT NOT NULL")); assertTrue(sql.contains("open_dedupe_key VARCHAR(512) GENERATED ALWAYS AS")); } @Test void flywayPointsAtTheFinalBaselineLocation() { String config = resource("application.yml"); assertTrue(config.contains("locations: classpath:db/migration-final")); } private static String resource(String path) { try (InputStream stream = FinalDatabaseBaselineContractTest.class.getClassLoader() .getResourceAsStream(path)) { if (stream == null) { throw new AssertionError("缺少资源 " + path); } return new String(stream.readAllBytes(), StandardCharsets.UTF_8); } catch (Exception exception) { throw new AssertionError("无法读取资源 " + path, exception); } } }