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.security.MessageDigest;
|
import java.util.HexFormat;
|
|
import org.junit.jupiter.api.Test;
|
|
class FlywayMigrationContractTest
|
{
|
/**
|
* 基线文件的"内容"指纹。**行尾不参与判定**:仓库按 autocrlf 检出,同一份内容在不同机器上
|
* 可能是 LF 也可能是 CRLF;而 Flyway 不读 `db/migration`(那是不可变历史,只有本测试读它),
|
* 所以把行尾当噪声剔除,守卫只拦"有人改写基线内容"。
|
*
|
* V1 与 V3 的指纹按提交 `545e467 补提过往数据库改动` 之后的实际内容重新钉过:那次提交改动了
|
* 这两份历史脚本,而这里的指纹没跟着更新,之后一直靠 target/classes 里的旧副本"看起来是绿的",
|
* `mvn clean` 之后就会红。本次修订没有再次改写基线内容。
|
*/
|
private static final String V1_SHA256 = "dfcef29b236a0e0629accd2de1fd7fa079486c31ec769b7d20406c0e46b24186";
|
private static final String V2_SHA256 = "698023958beee6245b099cf4505d35874cd06823e4dbbc3ddcb11b8f76609b89";
|
private static final String V3_SHA256 = "51f675a1a28eee2da0ea83379a429cb8337138000e83e29f4a2f4e868dee9680";
|
private static final String V6_SHA256 = "2f20752fcb9a318435d27ba4a0c3e0ba9443ba2f61a884f82bace457270d7db8";
|
|
@Test
|
void keepsImmutableBaselineAndAddsObservationIncrementOnly()
|
{
|
assertEquals(V1_SHA256, sha256("db/migration/V1__ruoyi_platform_baseline.sql"));
|
assertEquals(V2_SHA256, sha256("db/migration/V2__battery_detection_domain.sql"));
|
assertEquals(V3_SHA256, sha256("db/migration/V3__battery_detection_permissions.sql"));
|
|
String v4 = resource("db/migration/V4__observation_persistence_constraints.sql");
|
assertTrue(v4.contains("ADD COLUMN creator_id"));
|
assertTrue(v4.contains("uk_exception_open_dedupe"));
|
assertTrue(v4.contains("adapter_timezone_id"));
|
assertTrue(v4.contains("fk_sample_channel"));
|
assertFalse(v4.matches("(?s).*\\bINSERT\\s+INTO\\b.*"));
|
|
String v5 = resource("db/migration/V5__adapter_discovery_runtime.sql");
|
assertTrue(v5.contains("connection_status"));
|
assertTrue(v5.contains("last_observed_at_utc"));
|
assertTrue(v5.contains("chk_adapter_connection_status"));
|
assertTrue(v5.contains("system:deviceAdapter:list"));
|
assertTrue(v5.contains("system:deviceAdapter:query"));
|
assertTrue(v5.contains("system:deviceAdapter:add"));
|
assertTrue(v5.contains("system:deviceAdapter:edit"));
|
assertTrue(v5.contains("system:deviceAdapter:changeStatus"));
|
assertTrue(v5.contains("system:deviceAdapter:discover"));
|
assertTrue(v5.contains("system:detectionCabinet:list"));
|
assertFalse(v5.matches("(?s).*\\bDELETE\\s+FROM\\b.*"));
|
|
String v6 = resource("db/migration/V6__separate_discovery_from_registration.sql");
|
assertEquals(V6_SHA256, sha256("db/migration/V6__separate_discovery_from_registration.sql"));
|
assertTrue(v6.contains("registration_status"));
|
assertTrue(v6.contains("DISCOVERED"));
|
assertTrue(v6.contains("REGISTERED"));
|
assertTrue(v6.contains("chk_cabinet_registration_status"));
|
assertTrue(v6.contains("DEFAULT 'DISCOVERED'"));
|
assertFalse(v6.matches("(?s).*\\bDELETE\\s+FROM\\b.*"));
|
|
String v7 = resource("db/migration/V7__complete_device_registration_boundary.sql");
|
assertTrue(v7.contains("WHEN create_by = 'system' THEN 'DISCOVERED'"));
|
assertTrue(v7.contains("ELSE 'REGISTERED'"));
|
assertTrue(v7.contains("uk_cabinet_mac_del"));
|
assertFalse(v7.matches("(?s).*\\bDELETE\\s+FROM\\b.*"));
|
|
String v8 = resource("db/migration/V8__complete_collector_page_contract.sql");
|
assertTrue(v8.contains("ALTER TABLE t_collector"));
|
assertTrue(v8.contains("test_status"));
|
assertTrue(v8.contains("current_test"));
|
assertTrue(v8.contains("current_sample_name"));
|
assertTrue(v8.contains("chk_collector_test_status"));
|
assertFalse(v8.matches("(?s).*\\bDELETE\\s+FROM\\b.*"));
|
|
String v9 = resource("db/migration/V9__move_discovery_candidates_to_redis.sql");
|
assertTrue(v9.contains("adapter_code VARCHAR(64)"));
|
assertTrue(v9.contains("adapter.adapter_code = 'dev-sim'"));
|
assertTrue(v9.contains("cabinet.registration_status = 'DISCOVERED'"));
|
assertTrue(v9.contains("t_test_channel_binding"));
|
assertTrue(v9.contains("t_test_sample"));
|
assertTrue(v9.contains("t_device_run_event"));
|
assertTrue(v9.contains("SELECT protected_cabinet.cabinet_id"));
|
assertTrue(v9.contains("SELECT DISTINCT sibling.cabinet_id"));
|
assertTrue(v9.contains("uk_channel_stable_identity"));
|
assertTrue(v9.contains("ADD KEY idx_cabinet_adapter (adapter_id)"));
|
assertTrue(v9.contains("SET adapter_id = NULL WHERE adapter_code = 'dev-sim'"));
|
assertFalse(v9.contains("DROP FOREIGN KEY fk_channel_adapter"));
|
assertFalse(v9.contains("DROP FOREIGN KEY fk_cabinet_adapter"));
|
assertFalse(v9.matches("(?s).*t_test_battery\\s+\\w+\\s+WHERE\\s+\\w+\\.cabinet_id.*"));
|
|
String v10 = resource("db/migration/V10__test_configuration_collector.sql");
|
assertTrue(v10.contains("ALTER TABLE t_test_battery"));
|
assertTrue(v10.contains("collector_id"));
|
assertTrue(v10.contains("fk_test_battery_collector"));
|
assertFalse(v10.matches("(?s).*\\bINSERT\\s+INTO\\b.*"));
|
|
String v11 = resource("db/migration/V11__persist_battery_group_field_values.sql");
|
assertTrue(v11.contains("ALTER TABLE t_battery_group_relation_field"));
|
assertTrue(v11.contains("field_value VARCHAR(500)"));
|
assertFalse(v11.matches("(?s).*\\bINSERT\\s+INTO\\b.*"));
|
|
String v12 = resource("db/migration/V12__custom_field_type_constraints.sql");
|
assertTrue(v12.contains("chk_battery_group_field_type"));
|
assertTrue(v12.contains("'text', 'number', 'time', 'remark'"));
|
assertFalse(v12.matches("(?s).*\\bINSERT\\s+INTO\\b.*"));
|
|
String v13 = resource("db/migration/V13__test_required_device_bindings.sql");
|
assertTrue(v13.contains("reception_status"));
|
assertTrue(v13.contains("DEFAULT 'NOT_STARTED'"));
|
assertTrue(v13.contains("CREATE TABLE t_test_collector_channel"));
|
assertTrue(v13.contains("uk_test_collector_channel"));
|
assertTrue(v13.contains("CREATE TABLE t_test_field_snapshot"));
|
assertFalse(v13.matches("(?s).*\\bINSERT\\s+INTO\\b.*"));
|
assertFalse(v13.matches("(?s).*\\bUPDATE\\s+t_test\\b.*"));
|
|
String v14 = resource("db/migration/V14__test_reception_intervals.sql");
|
assertTrue(v14.contains("CREATE TABLE t_test_reception_interval"));
|
assertTrue(v14.contains("start_boundary_at_utc"));
|
assertTrue(v14.contains("CREATE TABLE t_collector_channel_lease"));
|
assertTrue(v14.contains("idx_test_reception_status"));
|
assertTrue(v14.contains("system:test:start"));
|
|
String v16 = resource("db/migration/V16__simplify_test_reception_lifecycle.sql");
|
assertTrue(v16.contains("system:test:stop"));
|
assertTrue(v16.contains("旧区间表保留为历史数据"));
|
assertFalse(v16.matches("(?s).*\\bDROP\\s+TABLE\\b.*"));
|
|
String v17 = resource("db/migration/V17__persist_runtime_attach_flag.sql");
|
assertTrue(v17.contains("ALTER TABLE t_test"));
|
assertTrue(v17.contains("ADD COLUMN runtime_attach"));
|
assertTrue(v17.contains("TINYINT(1) NOT NULL DEFAULT 0"));
|
assertTrue(v17.contains("AFTER ended_at_utc"));
|
assertFalse(v17.matches("(?s).*\\bDROP\\s+TABLE\\b.*"));
|
|
String v18 = resource("db/migration/V18__collector_disconnect_recovery.sql");
|
assertTrue(v18.contains("CREATE TABLE t_test_collector_exception_interval"));
|
assertTrue(v18.contains("missing_from_utc"));
|
assertTrue(v18.contains("recovered_at_utc"));
|
assertTrue(v18.contains("uk_collector_exception_open"));
|
assertFalse(v18.matches("(?s).*\\bDROP\\s+TABLE\\b.*"));
|
|
String v19 = resource("db/migration/V19__collector_disconnect_open_dedupe.sql");
|
assertTrue(v19.contains("DROP INDEX uk_collector_exception_open"));
|
assertTrue(v19.contains("open_dedupe_key VARCHAR(320)"));
|
assertTrue(v19.contains("interval_status = 'OPEN'"));
|
assertTrue(v19.contains("ELSE NULL"));
|
assertTrue(v19.contains("ADD UNIQUE KEY uk_collector_exception_open (open_dedupe_key)"));
|
assertFalse(v19.matches("(?s).*\\bDROP\\s+TABLE\\b.*"));
|
|
String v20 = resource("db/migration/V20__manual_end_reception_intervals.sql");
|
assertTrue(v20.contains("chk_reception_interval_boundary_order"));
|
assertTrue(v20.contains("idx_reception_interval_source_order"));
|
assertTrue(v20.contains("system:test:stop"));
|
assertFalse(v20.matches("(?s).*\\bDROP\\s+TABLE\\b.*"));
|
|
String v31 = resource("db/migration/V31__unified_realtime_snapshots.sql");
|
assertTrue(v31.contains("CREATE TABLE t_test_realtime_snapshot"));
|
assertTrue(v31.contains("fk_realtime_snapshot_test"));
|
assertTrue(v31.contains("fk_realtime_snapshot_binding"));
|
assertTrue(v31.contains("fk_realtime_snapshot_channel"));
|
assertTrue(v31.contains("fk_realtime_snapshot_collector"));
|
assertFalse(v31.contains("fk_snapshot_test"));
|
assertFalse(v31.contains("fk_snapshot_binding"));
|
assertFalse(v31.contains("fk_snapshot_channel"));
|
|
String testMapper = resource("mapper/battery/TTestMapper.xml");
|
assertTrue(testMapper.contains("selectReceivingChannelsForAdapter"));
|
assertTrue(testMapper.contains("updateReceptionResumed"));
|
assertTrue(testMapper.contains("updateReceptionStopped"));
|
assertFalse(testMapper.contains("#{testId}</if><if"));
|
}
|
|
private static String sha256(String path)
|
{
|
try
|
{
|
// 行尾是检出环境的产物,不是内容:换行统一成 LF 之后再算指纹。
|
String content = resource(path).replace("\r\n", "\n");
|
return HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256")
|
.digest(content.getBytes(StandardCharsets.UTF_8)));
|
}
|
catch (Exception exception)
|
{
|
throw new AssertionError("无法读取迁移资源 " + path, exception);
|
}
|
}
|
|
private static String resource(String path)
|
{
|
try (InputStream stream = FlywayMigrationContractTest.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);
|
}
|
}
|
}
|