feat(ft546): enforce shadow database isolation

This commit is contained in:
zgw
2026-08-13 14:23:50 +08:00
parent 364668c939
commit b364831c4a
23 changed files with 2516 additions and 14 deletions

View File

@@ -1,6 +1,7 @@
package com.ruoyi.shadow;
import com.ruoyi.project.shadowtest.ForbiddenFactoryBean;
import com.ruoyi.shadow.startup.ShadowDatabaseGate;
import com.ruoyi.shadow.startup.ShadowStartupReport;
import com.ruoyi.shadow.web.ShadowStatusController;
import com.ruoyi.wcs.application.dispatch.DispatchCoordinator;
@@ -11,9 +12,13 @@ import org.springframework.boot.test.context.ConfigDataApplicationContextInitial
import org.springframework.boot.test.context.runner.WebApplicationContextRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import java.util.LinkedHashMap;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
@@ -26,9 +31,10 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
class Ft546ShadowApplicationTest {
private final WebApplicationContextRunner contextRunner =
new WebApplicationContextRunner()
.withInitializer(
new ConfigDataApplicationContextInitializer())
.withUserConfiguration(Ft546ShadowApplication.class);
.withInitializer(
new ConfigDataApplicationContextInitializer())
.withUserConfiguration(Ft546ShadowApplication.class,
TestShadowDatabaseGateConfiguration.class);
@Test
void explicitReadOnlyShadowConfigurationStartsTheNarrowContext() {
@@ -52,12 +58,17 @@ class Ft546ShadowApplicationTest {
.getStartupChecks().get("SHADOW_ENABLED"));
assertEquals("PASS", context.getBean(ShadowStartupReport.class)
.getStartupChecks().get("SIDE_EFFECT_BEANS_ABSENT"));
assertEquals("PENDING", context.getBean(ShadowStartupReport.class)
assertEquals("PASS", context.getBean(ShadowStartupReport.class)
.getStartupChecks().get("SOURCE_PERMISSIONS_VERIFIED"));
assertEquals("PENDING", context.getBean(ShadowStartupReport.class)
assertEquals("PASS", context.getBean(ShadowStartupReport.class)
.getStartupChecks().get("EVIDENCE_DATABASE_VERIFIED"));
assertNotNull(context.getBean(DispatchCoordinator.class));
assertNotNull(context.getBean(ShadowStatusController.class));
Map<String, String> persisted = context.getBean(
RecordingShadowDatabaseGate.class).getPersistedChecks();
assertNotNull(persisted);
assertEquals(context.getBean(ShadowStartupReport.class)
.getStartupChecks(), persisted);
assertTrue(context.getBeansOfType(
DeviceCommandGateway.class).isEmpty());
assertTrue(context.getBeansOfType(
@@ -98,18 +109,30 @@ class Ft546ShadowApplicationTest {
assertStartupFailure(contextRunner.withPropertyValues(
"spring.profiles.active=shadow",
"shadow.enabled=true",
"shadow.evidence.jdbc-url=jdbc:h2:mem:evidence"),
"shadow.evidence.jdbc-url=jdbc:h2:mem:evidence",
"shadow.source.username=source",
"shadow.evidence.username=evidence"),
"SHADOW_SOURCE_JDBC_URL_REQUIRED");
assertStartupFailure(contextRunner.withPropertyValues(
"spring.profiles.active=shadow",
"shadow.enabled=true",
"shadow.source.jdbc-url=jdbc:h2:mem:source"),
"shadow.source.jdbc-url=jdbc:h2:mem:source",
"shadow.source.username=source",
"shadow.evidence.username=evidence"),
"SHADOW_EVIDENCE_JDBC_URL_REQUIRED");
assertStartupFailure(configured(contextRunner).withPropertyValues(
"shadow.source.username="),
"SHADOW_SOURCE_USERNAME_REQUIRED");
assertStartupFailure(configured(contextRunner).withPropertyValues(
"shadow.evidence.username="),
"SHADOW_EVIDENCE_USERNAME_REQUIRED");
assertStartupFailure(contextRunner.withPropertyValues(
"spring.profiles.active=shadow",
"shadow.enabled=true",
"shadow.source.jdbc-url=jdbc:h2:mem:source",
"shadow.evidence.jdbc-url=jdbc:h2:mem:evidence",
"shadow.source.username=source",
"shadow.evidence.username=evidence",
"server.address=0.0.0.0"),
"SHADOW_STATUS_ADDRESS_MUST_BE_LOOPBACK");
}
@@ -145,7 +168,9 @@ class Ft546ShadowApplicationTest {
"spring.profiles.active=shadow",
"shadow.enabled=true",
"shadow.source.jdbc-url=jdbc:h2:mem:source",
"shadow.evidence.jdbc-url=jdbc:h2:mem:evidence");
"shadow.evidence.jdbc-url=jdbc:h2:mem:evidence",
"shadow.source.username=source",
"shadow.evidence.username=evidence");
}
private static void assertStartupFailure(
@@ -197,4 +222,36 @@ class Ft546ShadowApplicationTest {
return new ForbiddenFactoryBean();
}
}
@Configuration(proxyBeanMethods = false)
static class TestShadowDatabaseGateConfiguration {
@Bean
@Primary
RecordingShadowDatabaseGate testShadowDatabaseGate() {
return new RecordingShadowDatabaseGate();
}
}
static final class RecordingShadowDatabaseGate
implements ShadowDatabaseGate {
private Map<String, String> persistedChecks;
@Override
public Map<String, String> verify(
com.ruoyi.shadow.config.ShadowProperties properties) {
Map<String, String> checks =
new LinkedHashMap<String, String>();
checks.put("SOURCE_PERMISSIONS_VERIFIED", "PASS");
checks.put("EVIDENCE_DATABASE_VERIFIED", "PASS");
return checks;
}
public void persistChecks(Map<String, String> checks) {
persistedChecks = new LinkedHashMap<String, String>(checks);
}
Map<String, String> getPersistedChecks() {
return persistedChecks;
}
}
}