2026-08-25 16:40:48 +08:00
|
|
|
package com.baiyue.lastleg.security;
|
|
|
|
|
|
|
|
|
|
import org.junit.jupiter.api.Test;
|
|
|
|
|
|
|
|
|
|
import java.nio.charset.StandardCharsets;
|
|
|
|
|
import java.nio.file.Files;
|
|
|
|
|
import java.nio.file.Path;
|
|
|
|
|
|
|
|
|
|
import static org.assertj.core.api.Assertions.assertThat;
|
|
|
|
|
|
|
|
|
|
class LastlegSecurityContractTest {
|
|
|
|
|
@Test void securityProjectionLivesInLastlegDatabaseWithReliableInbox() throws Exception {
|
|
|
|
|
String baseline = read("lastleg-service/src/main/resources/db/lastleg.sql");
|
|
|
|
|
String migration = read("deploy/sql/海外区域统一增量-MySQL8.sql");
|
|
|
|
|
String authSeed = read("auth/src/main/resources/db/phase1-admin-accounts.sql");
|
|
|
|
|
String service = read("lastleg-service/src/main/java/com/baiyue/lastleg/security/service/LastlegSecurityProjectionService.java");
|
|
|
|
|
String validation = read("lastleg-service/src/main/java/com/baiyue/lastleg/security/service/LastlegSecurityValidationService.java");
|
|
|
|
|
String validationDto = read("lastleg-service/src/main/java/com/baiyue/lastleg/security/dto/LastlegSecurityDtos.java");
|
|
|
|
|
String mirror = read("lastleg-service/src/main/java/com/baiyue/lastleg/security/service/LastlegSecurityProjectionRedisMirror.java");
|
|
|
|
|
assertThat(baseline).contains("CREATE TABLE IF NOT EXISTS ll_security_event_inbox",
|
|
|
|
|
"PRIMARY KEY (event_id)", "payload_hash CHAR(64)",
|
|
|
|
|
"CREATE TABLE IF NOT EXISTS ll_security_projection",
|
|
|
|
|
"PRIMARY KEY (aggregate_type,aggregate_id)", "aggregate_version BIGINT",
|
2026-09-07 11:37:06 +08:00
|
|
|
"permission_version BIGINT", "roles_json JSON", "authorities_json JSON");
|
2026-09-08 17:58:42 +08:00
|
|
|
assertThat(baseline).contains("table_name='ll_security_projection' AND column_name='roles_json'",
|
|
|
|
|
"table_name='ll_security_projection' AND column_name='authorities_json'",
|
|
|
|
|
"UPDATE ll_security_projection SET roles_json=JSON_ARRAY() WHERE roles_json IS NULL",
|
|
|
|
|
"UPDATE ll_security_projection SET authorities_json=JSON_ARRAY() WHERE authorities_json IS NULL");
|
2026-09-17 15:10:06 +08:00
|
|
|
assertThat(baseline).contains("SET @platform_admin_overseas_roles = JSON_ARRAY('PLATFORM_ADMIN')",
|
2026-09-09 23:46:21 +08:00
|
|
|
"SET @platform_admin_overseas_authorities = JSON_ARRAY(",
|
|
|
|
|
"('USER','1000001',0,'ACTIVE',0,JSON_ARRAY(),JSON_ARRAY(),0",
|
|
|
|
|
"('AUTHORIZATION','1000001',0,NULL,0,@platform_admin_overseas_roles,@platform_admin_overseas_authorities,0",
|
2026-08-25 16:40:48 +08:00
|
|
|
"baseline-platform-admin-user-v0",
|
2026-09-09 23:46:21 +08:00
|
|
|
"baseline-platform-admin-authorization-v0",
|
|
|
|
|
"platform:overseas-warehouse:last-mile-order:list",
|
|
|
|
|
"platform:overseas-warehouse:inbound-order:list",
|
|
|
|
|
"platform:overseas-warehouse:dispatch:list",
|
|
|
|
|
"platform:overseas-business:purchase:list",
|
|
|
|
|
"platform:overseas-master-data:quote:list",
|
|
|
|
|
"platform:finance:fee-source:review",
|
|
|
|
|
"AND aggregate_version=0 AND last_event_id='baseline-platform-admin-authorization-v0'")
|
|
|
|
|
.doesNotContain("('AUTHORIZATION','1000001',0,NULL,0,JSON_ARRAY(),JSON_ARRAY(),0",
|
|
|
|
|
"baseline-merchant-admin");
|
2026-09-17 15:10:06 +08:00
|
|
|
assertThat(migration).contains(
|
|
|
|
|
"CREATE TABLE ll_security_event_inbox (",
|
|
|
|
|
"CREATE TABLE ll_security_projection (",
|
|
|
|
|
"VALUES ('AUTHORIZATION','1000001',0,NULL,0,",
|
|
|
|
|
"VALUES ('USER','1000001',0,'ACTIVE',0,'[]','[]',0",
|
|
|
|
|
"PLATFORM_ADMIN",
|
|
|
|
|
"baseline-platform-admin-user-v0",
|
|
|
|
|
"baseline-platform-admin-authorization-v0",
|
|
|
|
|
"platform:overseas-warehouse:last-mile-order:list",
|
|
|
|
|
"platform:overseas-warehouse:inbound-order:list",
|
|
|
|
|
"platform:overseas-warehouse:dispatch:list",
|
|
|
|
|
"platform:overseas-business:purchase:list",
|
|
|
|
|
"platform:overseas-master-data:quote:list",
|
|
|
|
|
"platform:finance:fee-source:review")
|
|
|
|
|
.doesNotContain("SET @platform_admin_overseas_roles", "UPDATE ll_security_projection",
|
|
|
|
|
"baseline-merchant-admin");
|
2026-08-25 16:40:48 +08:00
|
|
|
assertThat(authSeed).contains("(1000001, 'platform_admin'", "default_tenant_id",
|
|
|
|
|
"default_app_code");
|
2026-09-17 15:10:06 +08:00
|
|
|
assertThat(migration).contains("USE baiyue_lastleg;")
|
2026-08-25 16:40:48 +08:00
|
|
|
.doesNotContain("CREATE DATABASE IF NOT EXISTS baiyue_overseas_security",
|
|
|
|
|
"USE baiyue_overseas_security;", "CREATE TABLE IF NOT EXISTS overseas_security_event_inbox",
|
|
|
|
|
"CREATE TABLE IF NOT EXISTS overseas_security_projection");
|
|
|
|
|
assertThat(service).contains("SecurityProjectionMonotonicity.decide", "Decision.APPLY");
|
|
|
|
|
assertThat(service).contains("EventPayloadHash.matches", "validatedStorageHash")
|
|
|
|
|
.doesNotContain("LoadingCompletedConsumer");
|
|
|
|
|
assertThat(mirror).contains("TransactionPhase.AFTER_COMMIT", "Redis 镜像写入失败");
|
2026-09-07 11:37:06 +08:00
|
|
|
assertThat(validation).contains("SELECT permission_version", "authorities_json", "tenant_id=?",
|
2026-08-25 16:40:48 +08:00
|
|
|
"SECURITY_PROJECTION_MISSING", "SECURITY_PROJECTION_NOT_READY", "USER_DISABLED",
|
|
|
|
|
"TENANT_DISABLED", "PERMISSION_VERSION_STALE");
|
|
|
|
|
assertThat(validationDto).contains("Long userId", "Long tenantId", "Long permissionVersion")
|
|
|
|
|
.doesNotContain("Long tokenVersion");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Test void genericLastlegEventEndpointWhitelistsBusinessEventsOnly() throws Exception {
|
|
|
|
|
String controller = read("lastleg-service/src/main/java/com/baiyue/lastleg/controller/LastlegOrderController.java");
|
|
|
|
|
assertThat(controller).contains("FIRST_LEG_CONTAINER_SHIPPING_NODE_RECORDED",
|
|
|
|
|
"FIRST_LEG_LOADING_PLAN_COMPLETED", "Lastleg 不消费该事件类型")
|
|
|
|
|
.doesNotContain("IAM_USER_STATUS_CHANGED", "IAM_TENANT_STATUS_CHANGED",
|
|
|
|
|
"IAM_PERMISSION_VERSION_CHANGED");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Test void internalControllerUsesLastlegOwnedPathsAndExistingTokenGuard() throws Exception {
|
|
|
|
|
String controller = read("lastleg-service/src/main/java/com/baiyue/lastleg/security/controller/LastlegSecurityController.java");
|
|
|
|
|
String guard = read("lastleg-service/src/main/java/com/baiyue/lastleg/security/InternalServiceTokenGuard.java");
|
|
|
|
|
String application = read("lastleg-service/src/main/resources/application.yml");
|
|
|
|
|
assertThat(controller).contains("@RequestMapping(\"/internal/lastleg/security/v1\")",
|
|
|
|
|
"@PostMapping(\"/events\")", "@PostMapping(\"/validate\")",
|
|
|
|
|
"com.baiyue.lastleg.security.InternalServiceTokenGuard")
|
|
|
|
|
.doesNotContain("/internal/overseas-security/v1");
|
|
|
|
|
assertThat(guard).contains("MessageDigest.isEqual", "SERVICE_UNAVAILABLE", "UNAUTHORIZED");
|
|
|
|
|
assertThat(application).contains("internal-service-token: ${INTERNAL_SERVICE_TOKEN:}")
|
|
|
|
|
.doesNotContain("phase1-local-internal-token");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static String read(String relative) throws Exception {
|
|
|
|
|
Path path = Path.of(relative);
|
|
|
|
|
if (!Files.exists(path)) path = Path.of("..").resolve(relative);
|
|
|
|
|
return Files.readString(path, StandardCharsets.UTF_8);
|
|
|
|
|
}
|
|
|
|
|
}
|