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.assertThrows;
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
|
import java.io.InputStream;
|
import java.nio.charset.StandardCharsets;
|
import java.util.List;
|
import java.util.Map;
|
import java.util.UUID;
|
import org.apache.ibatis.datasource.unpooled.UnpooledDataSource;
|
import org.junit.jupiter.api.Test;
|
import org.springframework.core.io.ClassPathResource;
|
import org.springframework.jdbc.core.JdbcTemplate;
|
import org.springframework.jdbc.datasource.init.ScriptUtils;
|
import com.battery.device.api.model.AdapterCapabilities;
|
import com.battery.device.api.registry.AdapterInstanceConfig;
|
import com.battery.device.api.registry.AdapterRegistryException;
|
import com.battery.device.api.registry.DeviceAdapterRegistry;
|
|
/**
|
* 适配器实例按存储形态放宽与来源形态配置列的迁移契约:
|
* 迁移把 {@code source_config} 加在能力列之后,运行库上该列确实存在且可空;
|
* 类型白名单放宽到直连厂商库,未知类型仍然拒绝。
|
*/
|
class VendorSourceInstanceMigrationContractTest
|
{
|
@Test
|
void migrationAddsNullableSourceConfigColumnWithoutTouchingExistingRows() throws Exception
|
{
|
String path = "db/migration-final/V22__vendor_source_instance_config.sql";
|
String migration = resource(path);
|
assertTrue(migration.contains("ALTER TABLE t_device_adapter ADD COLUMN source_config JSON NULL"));
|
assertTrue(migration.contains("AFTER capabilities"));
|
// 迁移只加列:不改既有列、不写入任何数据,更不写入凭据。
|
assertFalse(migration.matches("(?s).*\\b(INSERT|UPDATE|DELETE|DROP)\\b.*"));
|
assertFalse(migration.toLowerCase().contains("password"));
|
|
var source = new UnpooledDataSource("org.h2.Driver",
|
"jdbc:h2:mem:" + UUID.randomUUID() + ";MODE=MySQL;DATABASE_TO_LOWER=TRUE", "sa", "");
|
try (var connection = source.getConnection())
|
{
|
JdbcTemplate jdbc = new JdbcTemplate(source);
|
jdbc.execute("create table t_device_adapter(adapter_id bigint auto_increment primary key,"
|
+ "adapter_code varchar(64), capabilities json)");
|
ScriptUtils.executeSqlScript(connection, new ClassPathResource(path));
|
|
// 来源形态配置只承载非敏感项;连接口令由环境变量提供,不入库。
|
jdbc.update("insert into t_device_adapter(adapter_code, capabilities, source_config) values(?,?,?)",
|
"neware76", "{\"control\":false}", "{\"database\":\"bts63\",\"user\":\"visitor\"}");
|
assertEquals(1, jdbc.queryForObject(
|
"select count(*) from t_device_adapter where source_config is not null", Integer.class));
|
// 代码装配实例(例如开发模拟器)保持为空。
|
jdbc.update("insert into t_device_adapter(adapter_code, capabilities) values(?,?)", "dev-sim",
|
"{\"control\":false}");
|
assertEquals(1, jdbc.queryForObject(
|
"select count(*) from t_device_adapter where source_config is null", Integer.class));
|
}
|
}
|
|
@Test
|
void sourceConfigCarriesOnlyNonSensitiveOptionsAndUnknownTypesStayRejected()
|
{
|
AdapterInstanceConfig config = new AdapterInstanceConfig("neware76", "NEWARE_MYSQL", "26.1.2.3", 3306,
|
60000, "Asia/Hong_Kong", true, AdapterCapabilities.observeOnly(),
|
Map.of("database", "bts63", "user", "visitor", "passwordEnv", "BATTERY_VENDOR_DB_PASSWORD"));
|
|
assertEquals("bts63", config.sourceOption("database"));
|
assertTrue(config.capabilities().discovery());
|
assertFalse(config.capabilities().control());
|
|
try (DeviceAdapterRegistry registry = new DeviceAdapterRegistry(List.of()))
|
{
|
// 放宽不等于接受任何类型:未注册的存储形态仍然明确失败。
|
assertThrows(AdapterRegistryException.class,
|
() -> registry.synchronize(new AdapterInstanceConfig("mystery", "SOME_OTHER_VENDOR",
|
"127.0.0.1", 3306, 60000, "Asia/Hong_Kong", true,
|
AdapterCapabilities.observeOnly(), Map.of())));
|
}
|
}
|
|
private static String resource(String path)
|
{
|
try (InputStream stream = VendorSourceInstanceMigrationContractTest.class.getClassLoader()
|
.getResourceAsStream(path))
|
{
|
if (stream == null) throw new IllegalStateException("缺少迁移文件:" + path);
|
return new String(stream.readAllBytes(), StandardCharsets.UTF_8);
|
}
|
catch (java.io.IOException exception)
|
{
|
throw new IllegalStateException("无法读取迁移文件:" + path, exception);
|
}
|
}
|
}
|