package com.battery.device.neware;
|
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
|
import java.sql.Connection;
|
import java.sql.PreparedStatement;
|
import java.sql.Statement;
|
import java.time.Clock;
|
import java.time.Instant;
|
import java.time.ZoneOffset;
|
import java.util.List;
|
import java.util.Map;
|
import java.util.Set;
|
import javax.sql.DataSource;
|
import org.junit.jupiter.api.Test;
|
import com.battery.device.api.model.AdapterCapabilities;
|
import com.battery.device.api.model.ChannelIdentity;
|
import com.battery.device.api.registry.AdapterInstanceConfig;
|
|
/**
|
* 只读防线测试:非查询语句在到达驱动之前被拒绝,且该数据源在其生命周期内从未发出过非查询语句。
|
*/
|
class ReadOnlyVendorDataSourceTest
|
{
|
private static final String ADAPTER_CODE = "neware76";
|
private static final ChannelIdentity CHANNEL = new ChannelIdentity(ADAPTER_CODE, 21, 36, 1, 2);
|
|
private ReadOnlyVendorDataSource dataSourceOver(FakeVendorDatabase database)
|
{
|
return new ReadOnlyVendorDataSource(database.dataSource(), ADAPTER_CODE);
|
}
|
|
@Test
|
void queryStatementsAreForwardedAndRecorded() throws Exception
|
{
|
ReadOnlyVendorDataSource dataSource = new ReadOnlyVendorDataSource(new FakeVendorDatabase().dataSource(),
|
ADAPTER_CODE);
|
try (Connection connection = dataSource.getConnection();
|
PreparedStatement statement = connection.prepareStatement("select 1 from channel"))
|
{
|
statement.executeQuery();
|
}
|
try (Connection connection = dataSource.getConnection();
|
Statement statement = connection.createStatement())
|
{
|
statement.executeQuery("show tables");
|
statement.executeQuery("describe `channel`");
|
}
|
assertTrue(dataSource.audit().rejectedStatements().isEmpty());
|
assertEquals(3, dataSource.audit().forwardedStatements().size());
|
}
|
|
@Test
|
void nonQueryStatementsAreRejectedBeforeTheyReachTheDriver() throws Exception
|
{
|
FakeVendorDatabase database = new FakeVendorDatabase();
|
ReadOnlyVendorDataSource dataSource = dataSourceOver(database);
|
List<String> nonQueries = List.of("update channel set chl_status = 1",
|
"insert into channel(dev_uid) values(210036)", "delete from channel",
|
"drop table data_210036_2026_09_18", "create index idx on channel(chl_status)",
|
"truncate table channel", "set global read_only = 0", "grant select on *.* to x",
|
"alter table channel add column x int", "call some_procedure()");
|
for (String sql : nonQueries)
|
{
|
assertThrows(NonQueryStatementException.class, () -> {
|
try (Connection connection = dataSource.getConnection();
|
PreparedStatement statement = connection.prepareStatement(sql))
|
{
|
statement.execute();
|
}
|
}, "非查询语句必须被拒绝:" + sql);
|
}
|
assertEquals(nonQueries.size(), dataSource.audit().rejectedStatements().size());
|
assertTrue(database.statements().isEmpty(), "被拒绝的语句不得到达驱动");
|
// 语句级执行同样被拦下,并且没有任何非查询语句到达驱动。
|
assertThrows(NonQueryStatementException.class, () -> {
|
try (Connection connection = dataSource.getConnection(); Statement statement = connection.createStatement())
|
{
|
statement.executeUpdate("update channel set chl_status = 1");
|
}
|
});
|
assertThrows(NonQueryStatementException.class,
|
() -> dataSource.getConnection().prepareCall("{call anything()}"));
|
assertTrue(database.statements().isEmpty(), "被拒绝的语句不得到达驱动");
|
assertEquals(nonQueries.size() + 1, dataSource.audit().rejectedStatements().size());
|
}
|
|
/**
|
* 这条断言对应验收要求:整个适配器生命周期内,该数据源没有发出过任何非查询语句。
|
*/
|
@Test
|
void adapterLifecycleNeverSendsANonQueryStatement()
|
{
|
FakeVendorDatabase database = new FakeVendorDatabase()
|
.registeredChannel(210036L, 1, 2)
|
.test(new FakeVendorDatabase.TestRow(210036L, 1, 2, 440L, "2026-09-18 09:00:00",
|
"data_210036_2026_09_18", null))
|
.sample(new FakeVendorDatabase.SampleRow("data_210036_2026_09_18", 440L, 1, 2, 7435L, 3, 12, "2",
|
300000L, 13733, -11889, 0, 0, 50350024, 1103331, "2026-09-28 09:54:56"));
|
ReadOnlyVendorDataSource guarded = dataSourceOver(database);
|
AdapterInstanceConfig config = new AdapterInstanceConfig(ADAPTER_CODE, NewareMysqlAdapter.ADAPTER_TYPE,
|
"26.1.2.3", 3306, 60000, "Asia/Hong_Kong", true, AdapterCapabilities.observeOnly(),
|
Map.of("database", "bts63"));
|
NewareMysqlAdapter adapter = new NewareMysqlAdapter(config, guarded, () -> { },
|
Clock.fixed(Instant.parse("2026-09-28T02:00:00Z"), ZoneOffset.UTC), 0);
|
|
adapter.discoverChannels();
|
adapter.inquire(Set.of(CHANNEL));
|
adapter.latestCabinet(CHANNEL);
|
adapter.currentWatermark(CHANNEL);
|
adapter.samplesSince(CHANNEL,
|
new com.battery.device.api.TestDataObservationAdapter.SampleWatermark(
|
Instant.parse("2026-09-28T01:00:00Z"), 1L));
|
|
assertTrue(guarded.audit().rejectedStatements().isEmpty(),
|
"适配器不应尝试任何非查询语句:" + guarded.audit().rejectedStatements());
|
assertTrue(!database.statements().isEmpty(), "适配器应当实际读取来源");
|
for (String statement : database.statements())
|
{
|
assertTrue(ReadOnlyVendorDataSource.isQuery(statement), "到达驱动的语句必须是查询:" + statement);
|
}
|
}
|
|
@Test
|
void statementClassificationKeepsQueriesAndRejectsEverythingElse()
|
{
|
assertTrue(ReadOnlyVendorDataSource.isQuery("select dev_uid from channel"));
|
assertTrue(ReadOnlyVendorDataSource.isQuery("\n SELECT 1"));
|
assertTrue(ReadOnlyVendorDataSource.isQuery("/* probe */ show grants"));
|
assertTrue(ReadOnlyVendorDataSource.isQuery("-- probe\n describe channel"));
|
assertTrue(ReadOnlyVendorDataSource.isQuery("desc `channel`"));
|
assertTrue(!ReadOnlyVendorDataSource.isQuery("update channel set chl_status = 0"));
|
assertTrue(!ReadOnlyVendorDataSource.isQuery(""));
|
// 以 select 开头但紧跟字母的标识符不是查询语句。
|
assertTrue(!ReadOnlyVendorDataSource.isQuery("selection_helper()"));
|
}
|
|
@Test
|
void rejectedStatementsNeverReachTheDriverEvenWhenTheApplicationKeepsTrying()
|
{
|
// 连续尝试写操作后,来源侧一条语句都没收到,说明拦截发生在驱动之前。
|
FakeVendorDatabase database = new FakeVendorDatabase();
|
ReadOnlyVendorDataSource dataSource = dataSourceOver(database);
|
for (String sql : List.of("update channel set chl_status = 1", "delete from channel",
|
"insert into channel(dev_uid) values(210036)"))
|
{
|
try
|
{
|
dataSource.getConnection().prepareStatement(sql);
|
}
|
catch (Exception ignored)
|
{
|
// 每条都被白名单挡下。
|
}
|
}
|
assertTrue(database.statements().isEmpty());
|
assertEquals(3, dataSource.audit().rejectedStatements().size());
|
}
|
}
|