package com.ruoyi.config; 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 { private static final String V1_SHA256 = "37de76546e5aa0aa0ce7e976b73383a2d80ef97fd2db923d9e23dbc2b34534c9"; private static final String V2_SHA256 = "698023958beee6245b099cf4505d35874cd06823e4dbbc3ddcb11b8f76609b89"; private static final String V3_SHA256 = "763de1dfaa96e6f07f7ed3b3975e6f281dbc0c2e7f139da3bed6a2f1bfc39727"; 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.*")); } private static String sha256(String path) { try { return HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256") .digest(resource(path).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); } } }