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}