优化
This commit is contained in:
@@ -1,35 +0,0 @@
|
||||
package com.baiyue.iam.client;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.cloud.openfeign.FeignClient;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class SecurityEventBridgeClientContractTest {
|
||||
private static final Path REPOSITORY_ROOT = Path.of("..").toAbsolutePath().normalize();
|
||||
|
||||
@Test
|
||||
void feignUsesTheExplicitOverseasBridgePropertyInsteadOfSameNameDiscovery() {
|
||||
FeignClient annotation = SecurityEventBridgeClient.class.getAnnotation(FeignClient.class);
|
||||
|
||||
assertThat(annotation.name()).isEqualTo("event-bridge-service");
|
||||
assertThat(annotation.url()).isEqualTo("${baiyue.security-event.event-bridge-url:}");
|
||||
}
|
||||
|
||||
@Test
|
||||
void cnIamConfigurationTargetsTheOverseasBridgeNetworkAlias() throws Exception {
|
||||
String application = Files.readString(REPOSITORY_ROOT.resolve("iam/src/main/resources/application.yml"));
|
||||
String nacos = Files.readString(REPOSITORY_ROOT.resolve("config/nacos/cn/iam-service.yml"));
|
||||
String compose = Files.readString(REPOSITORY_ROOT.resolve("deploy/docker-compose.yml"));
|
||||
|
||||
assertThat(application).contains(
|
||||
"event-bridge-url: ${SECURITY_EVENT_BRIDGE_URL:http://event-bridge-overseas:9312}");
|
||||
assertThat(nacos).contains(
|
||||
"event-bridge-url: ${SECURITY_EVENT_BRIDGE_URL:http://event-bridge-overseas:9312}");
|
||||
assertThat(compose).contains(
|
||||
"SECURITY_EVENT_BRIDGE_URL: ${SECURITY_EVENT_BRIDGE_URL:-http://event-bridge-overseas:9312}");
|
||||
}
|
||||
}
|
||||
@@ -12,26 +12,32 @@ class AuthorizationIdentitySqlTest {
|
||||
void disabledArchivedDeletedOrMissingCustomerCannotGetNewAuthorizationSnapshot() throws Exception {
|
||||
var jdbc = new JdbcTemplate(new DriverManagerDataSource("jdbc:h2:mem:" + UUID.randomUUID() + ";DB_CLOSE_DELAY=-1", "sa", ""));
|
||||
jdbc.execute("CREATE TABLE iam_user(id BIGINT, auth_user_id BIGINT, status VARCHAR(30), deleted INT)");
|
||||
jdbc.execute("CREATE TABLE iam_membership(id BIGINT, user_id BIGINT, tenant_id BIGINT, status VARCHAR(30), deleted INT)");
|
||||
jdbc.execute("CREATE TABLE iam_membership(id BIGINT, user_id BIGINT, tenant_id BIGINT, status VARCHAR(30), deleted INT, overseas_warehouse_id BIGINT, domestic_warehouse_id BIGINT)");
|
||||
jdbc.execute("CREATE TABLE iam_tenant(id BIGINT, status VARCHAR(30), deleted INT)");
|
||||
jdbc.execute("CREATE TABLE iam_membership_role(membership_id BIGINT,tenant_id BIGINT,role_id BIGINT)");
|
||||
jdbc.execute("CREATE TABLE iam_role(id BIGINT,tenant_id BIGINT,app_code VARCHAR(20),role_code VARCHAR(20),status VARCHAR(20),deleted INT)");
|
||||
jdbc.execute("CREATE TABLE iam_role_permission(role_id BIGINT,permission_id BIGINT)");
|
||||
jdbc.execute("CREATE TABLE iam_permission(id BIGINT,tenant_id BIGINT,app_code VARCHAR(20),permission_code VARCHAR(20),status VARCHAR(20),deleted INT)");
|
||||
jdbc.execute("CREATE TABLE iam_security_subject_version(auth_user_id BIGINT,permission_version BIGINT)");
|
||||
jdbc.execute("CREATE TABLE iam_security_tenant_version(tenant_id BIGINT,status_version BIGINT)");
|
||||
jdbc.update("INSERT INTO iam_user VALUES(1,42,'ACTIVE',0)");
|
||||
jdbc.update("INSERT INTO iam_membership VALUES(9,1,7,'ACTIVE',0)");
|
||||
jdbc.update("INSERT INTO iam_membership VALUES(9,1,7,'ACTIVE',0,NULL,NULL)");
|
||||
jdbc.update("INSERT INTO iam_tenant VALUES(7,'ACTIVE',0)");
|
||||
String sql = String.join("\n", AuthorizationQueryMapper.class.getMethod("countActiveIdentity", long.class, long.class, long.class)
|
||||
.getAnnotation(Select.class).value()).replace("#{userId}", "42").replace("#{membershipId}", "9").replace("#{tenantId}", "7");
|
||||
String sql = String.join("\n", AuthorizationQueryMapper.class.getMethod("findSnapshot", long.class, long.class, long.class, String.class)
|
||||
.getAnnotation(Select.class).value()).replace("#{userId}", "42").replace("#{membershipId}", "9").replace("#{tenantId}", "7").replace("#{appCode}", "'MERCHANT'");
|
||||
for (String status : new String[]{"ACTIVE", "PROFILE_REQUIRED", "PENDING", "REJECTED"}) {
|
||||
jdbc.update("UPDATE iam_tenant SET status=?", status);
|
||||
assertThat(jdbc.queryForObject(sql, Integer.class)).as(status).isEqualTo(1);
|
||||
assertThat(jdbc.queryForList(sql).size()).as(status).isEqualTo(1);
|
||||
}
|
||||
for (String status : new String[]{"DISABLED", "ARCHIVED", "UNKNOWN"}) {
|
||||
jdbc.update("UPDATE iam_tenant SET status=?", status);
|
||||
assertThat(jdbc.queryForObject(sql, Integer.class)).as(status).isZero();
|
||||
assertThat(jdbc.queryForList(sql).size()).as(status).isZero();
|
||||
}
|
||||
jdbc.update("UPDATE iam_tenant SET status='ACTIVE', deleted=1");
|
||||
assertThat(jdbc.queryForObject(sql, Integer.class)).isZero();
|
||||
assertThat(jdbc.queryForList(sql).size()).isZero();
|
||||
jdbc.update("DELETE FROM iam_tenant");
|
||||
assertThat(jdbc.queryForObject(sql, Integer.class)).isZero();
|
||||
assertThat(jdbc.queryForList(sql).size()).isZero();
|
||||
jdbc.update("UPDATE iam_membership SET tenant_id=0");
|
||||
assertThat(jdbc.queryForObject(sql.replace("m.tenant_id = 7", "m.tenant_id = 0"), Integer.class)).isEqualTo(1);
|
||||
assertThat(jdbc.queryForList(sql.replace("m.tenant_id=7", "m.tenant_id=0")).size()).isEqualTo(1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
package com.baiyue.iam.repository;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class SecurityEventSqlContractTest {
|
||||
@Test
|
||||
void securityOutboxHasVersionsAndReliableDeliveryColumns() throws Exception {
|
||||
String sql = new ClassPathResource("db/security-event-outbox.sql")
|
||||
.getContentAsString(StandardCharsets.UTF_8);
|
||||
assertThat(sql)
|
||||
.contains("iam_security_subject_version")
|
||||
.contains("permission_version BIGINT NOT NULL DEFAULT 0")
|
||||
.contains("iam_security_tenant_version")
|
||||
.contains("status_version BIGINT NOT NULL DEFAULT 0")
|
||||
.contains("iam_security_event_outbox")
|
||||
.contains("payload_json JSON NOT NULL")
|
||||
.contains("idx_security_outbox_publish")
|
||||
.contains("PENDING", "SENDING", "SENT", "DEAD");
|
||||
}
|
||||
|
||||
@Test
|
||||
void cleanInitializationIncludesSecurityEventSchema() throws Exception {
|
||||
Path direct = Path.of("deploy", "sql", "头程数据库统一增量-MySQL8.sql");
|
||||
Path migration = Files.exists(direct)
|
||||
? direct : Path.of("..", "deploy", "sql", "头程数据库统一增量-MySQL8.sql").normalize();
|
||||
String sql = Files.readString(migration, StandardCharsets.UTF_8);
|
||||
assertThat(sql)
|
||||
.contains("CREATE TABLE iam_security_subject_version")
|
||||
.contains("CREATE TABLE iam_security_tenant_version")
|
||||
.contains("CREATE TABLE iam_security_event_outbox")
|
||||
.contains("idx_security_outbox_publish", "idx_security_outbox_aggregate")
|
||||
.contains("SELECT 'PASS' AS initialization_status")
|
||||
.doesNotContain("assert_iam_security_event_schema", "CREATE TABLE IF NOT EXISTS iam_security_event");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.baiyue.iam.repository;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class SecurityVersionSqlContractTest {
|
||||
@Test void versionsRemainDurableWithoutProjectionOutbox() throws Exception {
|
||||
String sql = new ClassPathResource("db/security-version.sql").getContentAsString(StandardCharsets.UTF_8);
|
||||
assertThat(sql).contains("iam_security_subject_version", "permission_version BIGINT NOT NULL DEFAULT 0",
|
||||
"iam_security_tenant_version", "status_version BIGINT NOT NULL DEFAULT 0")
|
||||
.doesNotContain("iam_security_event_outbox");
|
||||
}
|
||||
|
||||
@Test void cleanInitializationIncludesVersionsWithoutRetiredProjectionTables() throws Exception {
|
||||
Path direct = Path.of("deploy", "sql", "头程数据库统一增量-MySQL8.sql");
|
||||
Path baseline = Files.exists(direct) ? direct : Path.of("..", "deploy", "sql", "头程数据库统一增量-MySQL8.sql");
|
||||
assertThat(Files.readString(baseline, StandardCharsets.UTF_8))
|
||||
.contains("CREATE TABLE iam_security_subject_version", "CREATE TABLE iam_security_tenant_version",
|
||||
"SELECT 'PASS' AS initialization_status")
|
||||
.doesNotContain("CREATE TABLE iam_security_event_outbox", "CREATE TABLE ll_security_event_inbox", "CREATE TABLE ll_security_projection");
|
||||
}
|
||||
}
|
||||
@@ -24,12 +24,7 @@ class AuthorizationSnapshotServiceTest {
|
||||
@Resource private AuthorizationSnapshotService service;
|
||||
@Test
|
||||
void loadsRolesAndPermissionsForActiveMerchantIdentity() {
|
||||
when(mapper.countActiveIdentity(42L, 7L, 9L)).thenReturn(1);
|
||||
when(mapper.findRoles(42L, 7L, 9L, "MERCHANT"))
|
||||
.thenReturn(List.of("ADMIN", "OPERATOR"));
|
||||
when(mapper.findPermissions(42L, 7L, 9L, "MERCHANT"))
|
||||
.thenReturn(List.of("order:read", "order:create"));
|
||||
when(mapper.findOverseasWarehouseId(7L, 9L)).thenReturn(900L);
|
||||
when(mapper.findSnapshot(42L, 7L, 9L, "MERCHANT")).thenReturn(rows(List.of("ADMIN", "OPERATOR"), List.of("order:read", "order:create")));
|
||||
|
||||
var result = service
|
||||
.load(new SnapshotRequest(42L, 7L, 9L, "merchant"));
|
||||
@@ -51,36 +46,29 @@ class AuthorizationSnapshotServiceTest {
|
||||
|
||||
@Test
|
||||
void rejectsInactiveMembership() {
|
||||
when(mapper.countActiveIdentity(42L, 7L, 9L)).thenReturn(0);
|
||||
|
||||
assertThatThrownBy(() -> service
|
||||
.load(new SnapshotRequest(42L, 7L, 9L, "MERCHANT")))
|
||||
.isInstanceOfSatisfying(BizException.class,
|
||||
exception -> assertThat(exception.getResultCode()).isEqualTo(ResultCode.FORBIDDEN));
|
||||
verify(mapper).countActiveIdentity(42L, 7L, 9L);
|
||||
when(mapper.findSnapshot(42L, 7L, 9L, "MERCHANT")).thenReturn(List.of());
|
||||
assertThatThrownBy(() -> service.load(new SnapshotRequest(42L, 7L, 9L, "MERCHANT")))
|
||||
.isInstanceOfSatisfying(BizException.class,
|
||||
exception -> assertThat(exception.getResultCode()).isEqualTo(ResultCode.FORBIDDEN));
|
||||
verify(mapper).findSnapshot(42L, 7L, 9L, "MERCHANT");
|
||||
}
|
||||
|
||||
@Test
|
||||
void loadsCurrentPlatformPermissionsWithoutRoleManagementPermission() {
|
||||
when(mapper.countActiveIdentity(42L, 0L, 9L)).thenReturn(1);
|
||||
when(mapper.findRoles(42L, 0L, 9L, "PLATFORM")).thenReturn(List.of("ORDER_OPERATOR"));
|
||||
when(mapper.findPermissions(42L, 0L, 9L, "PLATFORM"))
|
||||
.thenReturn(List.of("platform:order:list"));
|
||||
when(mapper.findSnapshot(42L, 0L, 9L, "PLATFORM")).thenReturn(rows(List.of("ORDER_OPERATOR"), List.of("platform:order:list")));
|
||||
|
||||
var result = service.currentPermissions(new AuthenticatedUser(42L, 0L, 0L, 9L,
|
||||
"operator", "PLATFORM", "PLATFORM", java.util.Set.of()), "PLATFORM");
|
||||
|
||||
assertThat(result).containsExactly("platform:order:list");
|
||||
verify(mapper).findPermissions(42L, 0L, 9L, "PLATFORM");
|
||||
verify(mapper).findSnapshot(42L, 0L, 9L, "PLATFORM");
|
||||
}
|
||||
|
||||
@Test
|
||||
void exposesPhase5DeliveryPermissionsFromCurrentRoleAssignments() {
|
||||
when(mapper.countActiveIdentity(42L, 7L, 9L)).thenReturn(1);
|
||||
when(mapper.findRoles(42L, 7L, 9L, "MERCHANT")).thenReturn(List.of("MERCHANT_OWNER"));
|
||||
when(mapper.findPermissions(42L, 7L, 9L, "MERCHANT")).thenReturn(List.of(
|
||||
when(mapper.findSnapshot(42L, 7L, 9L, "MERCHANT")).thenReturn(rows(List.of("MERCHANT_OWNER"), List.of(
|
||||
"merchant:warehouse:delivery:list",
|
||||
"merchant:warehouse:delivery:create"));
|
||||
"merchant:warehouse:delivery:create")));
|
||||
|
||||
var result = service.currentPermissions(new AuthenticatedUser(42L, 7L, 7L, 9L,
|
||||
"owner", "MERCHANT", "MERCHANT", java.util.Set.of()), "MERCHANT");
|
||||
@@ -92,14 +80,12 @@ class AuthorizationSnapshotServiceTest {
|
||||
|
||||
@Test
|
||||
void exposesCustomerOverseasBusinessPermissionsFromCurrentRoleAssignments() {
|
||||
when(mapper.countActiveIdentity(42L, 7L, 9L)).thenReturn(1);
|
||||
when(mapper.findRoles(42L, 7L, 9L, "MERCHANT")).thenReturn(List.of("MERCHANT_OWNER"));
|
||||
when(mapper.findPermissions(42L, 7L, 9L, "MERCHANT")).thenReturn(List.of(
|
||||
when(mapper.findSnapshot(42L, 7L, 9L, "MERCHANT")).thenReturn(rows(List.of("MERCHANT_OWNER"), List.of(
|
||||
"merchant:cross-border:procurement:list",
|
||||
"merchant:cross-border:procurement:create",
|
||||
"merchant:cross-border:domestic-trucking:list",
|
||||
"merchant:cross-border:pallet-shipping:list",
|
||||
"merchant:warehouse:inbound:list"));
|
||||
"merchant:warehouse:inbound:list")));
|
||||
|
||||
var result = service.currentPermissions(new AuthenticatedUser(42L, 7L, 7L, 9L,
|
||||
"owner", "MERCHANT", "MERCHANT", java.util.Set.of()), "MERCHANT");
|
||||
@@ -114,11 +100,9 @@ class AuthorizationSnapshotServiceTest {
|
||||
|
||||
@Test
|
||||
void exposesWalletPermissionsFromCurrentRoleAssignments() {
|
||||
when(mapper.countActiveIdentity(42L, 7L, 9L)).thenReturn(1);
|
||||
when(mapper.findRoles(42L, 7L, 9L, "MERCHANT")).thenReturn(List.of("MERCHANT_OWNER"));
|
||||
when(mapper.findPermissions(42L, 7L, 9L, "MERCHANT")).thenReturn(List.of(
|
||||
when(mapper.findSnapshot(42L, 7L, 9L, "MERCHANT")).thenReturn(rows(List.of("MERCHANT_OWNER"), List.of(
|
||||
"merchant:finance:account:view",
|
||||
"merchant:finance:recharge:create"));
|
||||
"merchant:finance:recharge:create")));
|
||||
|
||||
var merchantPermissions = service.currentPermissions(new AuthenticatedUser(42L, 7L, 7L, 9L,
|
||||
"owner", "MERCHANT", "MERCHANT", java.util.Set.of()), "MERCHANT");
|
||||
@@ -127,11 +111,9 @@ class AuthorizationSnapshotServiceTest {
|
||||
"merchant:finance:account:view",
|
||||
"merchant:finance:recharge:create");
|
||||
|
||||
when(mapper.countActiveIdentity(51L, 0L, 19L)).thenReturn(1);
|
||||
when(mapper.findRoles(51L, 0L, 19L, "PLATFORM")).thenReturn(List.of("PLATFORM_ADMIN"));
|
||||
when(mapper.findPermissions(51L, 0L, 19L, "PLATFORM")).thenReturn(List.of(
|
||||
when(mapper.findSnapshot(51L, 0L, 19L, "PLATFORM")).thenReturn(rows(List.of("PLATFORM_ADMIN"), List.of(
|
||||
"platform:finance:recharge:review",
|
||||
"platform:finance:customer-account:adjust"));
|
||||
"platform:finance:customer-account:adjust")));
|
||||
|
||||
var platformPermissions = service.currentPermissions(new AuthenticatedUser(51L, 0L, 0L, 19L,
|
||||
"platform-admin", "PLATFORM", "PLATFORM", java.util.Set.of()), "PLATFORM");
|
||||
@@ -143,11 +125,9 @@ class AuthorizationSnapshotServiceTest {
|
||||
|
||||
@Test
|
||||
void exposesInboundHoldIndependentlyFromReadPermissions() {
|
||||
when(mapper.countActiveIdentity(51L, 0L, 19L)).thenReturn(1);
|
||||
when(mapper.findRoles(51L, 0L, 19L, "PLATFORM")).thenReturn(List.of("PLATFORM_ADMIN"));
|
||||
when(mapper.findPermissions(51L, 0L, 19L, "PLATFORM")).thenReturn(List.of(
|
||||
when(mapper.findSnapshot(51L, 0L, 19L, "PLATFORM")).thenReturn(rows(List.of("PLATFORM_ADMIN"), List.of(
|
||||
"platform:firstleg:inbound:list",
|
||||
"platform:firstleg:inbound:hold"));
|
||||
"platform:firstleg:inbound:hold")));
|
||||
|
||||
var permissions = service.currentPermissions(new AuthenticatedUser(51L, 0L, 0L, 19L,
|
||||
"platform-admin", "PLATFORM", "PLATFORM", java.util.Set.of()), "PLATFORM");
|
||||
@@ -168,9 +148,7 @@ class AuthorizationSnapshotServiceTest {
|
||||
|
||||
@Test
|
||||
void shouldHandleCase01() {
|
||||
when(mapper.countActiveIdentity(42L, 0L, 9L)).thenReturn(1);
|
||||
when(mapper.findRoles(42L, 0L, 9L, "PLATFORM")).thenReturn(null);
|
||||
when(mapper.findPermissions(42L, 0L, 9L, "PLATFORM")).thenReturn(null);
|
||||
when(mapper.findSnapshot(42L, 0L, 9L, "PLATFORM")).thenReturn(rows(null, null));
|
||||
|
||||
var result = service.currentPermissions(new AuthenticatedUser(42L, 0L, 0L, 9L,
|
||||
"operator", "PLATFORM", "PLATFORM", java.util.Set.of()), "PLATFORM");
|
||||
@@ -180,12 +158,10 @@ class AuthorizationSnapshotServiceTest {
|
||||
|
||||
@Test
|
||||
void newLoginPermissionsDisappearWhileRoleDisabledAndReturnAfterReenable() {
|
||||
when(mapper.countActiveIdentity(42L, 7L, 9L)).thenReturn(1);
|
||||
when(mapper.findRoles(42L, 7L, 9L, "MERCHANT"))
|
||||
.thenReturn(List.of("ORDER_VIEWER"), List.of(), List.of("ORDER_VIEWER"));
|
||||
when(mapper.findPermissions(42L, 7L, 9L, "MERCHANT"))
|
||||
.thenReturn(List.of("merchant:order:list", "merchant:order:view"), List.of(),
|
||||
List.of("merchant:order:list", "merchant:order:view"));
|
||||
when(mapper.findSnapshot(42L, 7L, 9L, "MERCHANT"))
|
||||
.thenReturn(rows(List.of("ORDER_VIEWER"), List.of("merchant:order:list", "merchant:order:view")),
|
||||
rows(List.of(), List.of()),
|
||||
rows(List.of("ORDER_VIEWER"), List.of("merchant:order:list", "merchant:order:view")));
|
||||
SnapshotRequest request = new SnapshotRequest(42L, 7L, 9L, "MERCHANT");
|
||||
|
||||
assertThat(service.load(request).getPermissions())
|
||||
@@ -195,4 +171,16 @@ class AuthorizationSnapshotServiceTest {
|
||||
.containsExactlyInAnyOrder("merchant:order:list", "merchant:order:view");
|
||||
}
|
||||
|
||||
private List<com.baiyue.iam.dto.AuthorizationSnapshotRow> rows(List<String> roles, List<String> permissions) {
|
||||
var result = new java.util.ArrayList<com.baiyue.iam.dto.AuthorizationSnapshotRow>();
|
||||
List<String> roleValues = roles == null || roles.isEmpty() ? java.util.Collections.singletonList(null) : roles;
|
||||
List<String> permissionValues = permissions == null || permissions.isEmpty() ? java.util.Collections.singletonList(null) : permissions;
|
||||
for (String role : roleValues) for (String permission : permissionValues) {
|
||||
var row = new com.baiyue.iam.dto.AuthorizationSnapshotRow();
|
||||
row.setRoleCode(role); row.setPermissionCode(permission);
|
||||
row.setPermissionVersion(1L); row.setTenantVersion(1L); row.setOverseasWarehouseId(900L);
|
||||
result.add(row);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
package com.baiyue.iam.service;
|
||||
|
||||
import com.baiyue.iam.dto.AuthorizationDtos.SnapshotRequest;
|
||||
import com.baiyue.iam.mapper.AuthorizationQueryMapper;
|
||||
import org.h2.jdbcx.JdbcDataSource;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mybatis.spring.SqlSessionFactoryBean;
|
||||
import org.mybatis.spring.SqlSessionTemplate;
|
||||
import org.springframework.aop.framework.ProxyFactory;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
import org.springframework.transaction.annotation.AnnotationTransactionAttributeSource;
|
||||
import org.springframework.transaction.interceptor.TransactionInterceptor;
|
||||
import org.springframework.transaction.support.TransactionTemplate;
|
||||
import java.lang.reflect.Proxy;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.*;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
|
||||
/** Production SQL and transaction annotation, with a commit between the atomic SQL read and response assembly. */
|
||||
class AuthorizationSnapshotTransactionTest {
|
||||
@Test void singleStatementSnapshotCannotMixIdentityPermissionsAndVersion() throws Exception {
|
||||
var ds = new JdbcDataSource(); ds.setURL("jdbc:h2:mem:" + UUID.randomUUID() + ";MODE=MySQL;DB_CLOSE_DELAY=-1");
|
||||
var jdbc = new JdbcTemplate(ds);
|
||||
jdbc.execute("CREATE TABLE iam_user(id BIGINT,auth_user_id BIGINT,status VARCHAR(20),deleted INT)");
|
||||
jdbc.execute("CREATE TABLE iam_membership(id BIGINT,user_id BIGINT,tenant_id BIGINT,status VARCHAR(20),deleted INT,overseas_warehouse_id BIGINT,domestic_warehouse_id BIGINT)");
|
||||
jdbc.execute("CREATE TABLE iam_tenant(id BIGINT,status VARCHAR(20),deleted INT)");
|
||||
jdbc.execute("CREATE TABLE iam_role(id BIGINT,tenant_id BIGINT,app_code VARCHAR(20),role_code VARCHAR(20),status VARCHAR(20),deleted INT)");
|
||||
jdbc.execute("CREATE TABLE iam_membership_role(membership_id BIGINT,tenant_id BIGINT,role_id BIGINT)");
|
||||
jdbc.execute("CREATE TABLE iam_permission(id BIGINT,tenant_id BIGINT,app_code VARCHAR(20),permission_code VARCHAR(20),status VARCHAR(20),deleted INT)");
|
||||
jdbc.execute("CREATE TABLE iam_role_permission(role_id BIGINT,permission_id BIGINT)");
|
||||
jdbc.execute("CREATE TABLE iam_security_subject_version(auth_user_id BIGINT PRIMARY KEY,permission_version BIGINT)");
|
||||
jdbc.execute("CREATE TABLE iam_security_tenant_version(tenant_id BIGINT PRIMARY KEY,status_version BIGINT)");
|
||||
jdbc.update("INSERT INTO iam_user VALUES(1,42,'ACTIVE',0)");
|
||||
jdbc.update("INSERT INTO iam_membership VALUES(9,1,7,'ACTIVE',0,100,NULL)");
|
||||
jdbc.update("INSERT INTO iam_tenant VALUES(7,'ACTIVE',0)");
|
||||
jdbc.update("INSERT INTO iam_role VALUES(2,7,'MERCHANT','OPERATOR','ACTIVE',0)");
|
||||
jdbc.update("INSERT INTO iam_membership_role VALUES(9,7,2)");
|
||||
jdbc.update("INSERT INTO iam_permission VALUES(3,7,'MERCHANT','write','ACTIVE',0)");
|
||||
jdbc.update("INSERT INTO iam_role_permission VALUES(2,3)");
|
||||
jdbc.update("INSERT INTO iam_security_subject_version VALUES(42,1)");
|
||||
jdbc.update("INSERT INTO iam_security_tenant_version VALUES(7,1)");
|
||||
var factory = new SqlSessionFactoryBean(); factory.setDataSource(ds);
|
||||
var configuration = new org.apache.ibatis.session.Configuration(); configuration.addMapper(AuthorizationQueryMapper.class);
|
||||
configuration.addMapper(com.baiyue.iam.mapper.IamMembershipRoleMapper.class);
|
||||
factory.setConfiguration(configuration);
|
||||
var mapper = new SqlSessionTemplate(factory.getObject()).getMapper(AuthorizationQueryMapper.class);
|
||||
var read = new CountDownLatch(1); var committed = new CountDownLatch(1); var pause = new AtomicBoolean(true);
|
||||
AuthorizationQueryMapper controlled = (AuthorizationQueryMapper) Proxy.newProxyInstance(getClass().getClassLoader(),
|
||||
new Class<?>[]{AuthorizationQueryMapper.class}, (proxy, method, args) -> {
|
||||
Object result = method.invoke(mapper, args);
|
||||
if (method.getName().equals("findSnapshot") && pause.compareAndSet(true, false)) {
|
||||
read.countDown(); if (!committed.await(5, TimeUnit.SECONDS)) throw new AssertionError("Commit barrier timed out");
|
||||
}
|
||||
return result;
|
||||
});
|
||||
var target = new AuthorizationSnapshotService(); ReflectionTestUtils.setField(target, "mapper", controlled);
|
||||
var tx = new DataSourceTransactionManager(ds); var proxy = new ProxyFactory(target); proxy.setProxyTargetClass(true);
|
||||
proxy.addAdvice(new TransactionInterceptor(tx, new AnnotationTransactionAttributeSource()));
|
||||
var service = (AuthorizationSnapshotService) proxy.getProxy();
|
||||
var executor = Executors.newSingleThreadExecutor();
|
||||
try {
|
||||
var previous = executor.submit(() -> service.load(new SnapshotRequest(42L,7L,9L,"MERCHANT")));
|
||||
assertThat(read.await(5, TimeUnit.SECONDS)).isTrue();
|
||||
new TransactionTemplate(tx).executeWithoutResult(status -> {
|
||||
jdbc.update("UPDATE iam_permission SET permission_code='read' WHERE id=3");
|
||||
jdbc.update("UPDATE iam_security_subject_version SET permission_version=2 WHERE auth_user_id=42");
|
||||
jdbc.update("UPDATE iam_membership SET overseas_warehouse_id=200 WHERE id=9");
|
||||
});
|
||||
committed.countDown();
|
||||
var old = previous.get(5, TimeUnit.SECONDS);
|
||||
assertThat(old.getPermissions()).containsExactly("write");
|
||||
assertThat(old.getPermissionVersion()).isEqualTo(1); assertThat(old.getOverseasWarehouseId()).isEqualTo(100);
|
||||
var fresh = service.load(new SnapshotRequest(42L,7L,9L,"MERCHANT"));
|
||||
assertThat(fresh.getPermissions()).containsExactly("read");
|
||||
assertThat(fresh.getPermissionVersion()).isEqualTo(2); assertThat(fresh.getOverseasWarehouseId()).isEqualTo(200);
|
||||
// Shared platform role: enumerate both customers, not just membership tenant 0.
|
||||
jdbc.update("INSERT INTO iam_user VALUES(2,43,'ACTIVE',0)");
|
||||
jdbc.update("INSERT INTO iam_membership VALUES(10,2,8,'ACTIVE',0,NULL,NULL)");
|
||||
jdbc.update("INSERT INTO iam_role VALUES(5,0,'MERCHANT','SHARED','ACTIVE',0)");
|
||||
jdbc.update("INSERT INTO iam_membership_role VALUES(9,7,5),(10,8,5)");
|
||||
var assignments = new SqlSessionTemplate(factory.getObject())
|
||||
.getMapper(com.baiyue.iam.mapper.IamMembershipRoleMapper.class);
|
||||
assertThat(assignments.findAssignedAuthUserIds(0, 5L)).containsExactly(42L, 43L);
|
||||
assertThat(assignments.findAssignedAuthUserIds(7, 5L)).isEmpty();
|
||||
} finally { committed.countDown(); executor.shutdownNow(); }
|
||||
}
|
||||
}
|
||||
@@ -35,7 +35,7 @@ class MemberServiceTest {
|
||||
@MockBean private TenantRepository tenantRepository;
|
||||
@MockBean private IamRoleMapper roleMapper;
|
||||
@MockBean private AuthAccountClient auth;
|
||||
@MockBean private SecurityEventOutboxService securityEvents;
|
||||
@MockBean private SecurityVersionService securityVersions;
|
||||
@Resource private MemberService service;
|
||||
private IamMembership member;
|
||||
private IamUser user;
|
||||
@@ -106,8 +106,7 @@ class MemberServiceTest {
|
||||
assertEquals("DISABLED", result.status());
|
||||
verify(repository).save(member);
|
||||
verify(repository).saveUser(user);
|
||||
verify(securityEvents).userStatus(40L, 100L, "MERCHANT", "DISABLED");
|
||||
verify(securityEvents).permissionChanged(40L, 100L, "MERCHANT", "MEMBER_STATUS_CHANGED");
|
||||
verify(securityVersions).subjectChanged(40L);
|
||||
}
|
||||
|
||||
@Test void shouldHandleCase05() {
|
||||
@@ -156,8 +155,7 @@ class MemberServiceTest {
|
||||
verify(auth).create(eq("internal-token"), account.capture());
|
||||
long authUserId = account.getValue().accountId();
|
||||
verify(repository).replaceRoles(eq(100L), anyLong(), eq(List.of(50L)));
|
||||
verify(securityEvents).userStatus(authUserId, 100L, "MERCHANT", "ACTIVE");
|
||||
verify(securityEvents).permissionChanged(authUserId, 100L, "MERCHANT", "INITIAL_AUTHORIZATION");
|
||||
verify(securityVersions).subjectChanged(authUserId);
|
||||
}
|
||||
|
||||
@Test void shouldHandleCase08() {
|
||||
|
||||
@@ -44,7 +44,7 @@ class MerchantOnboardingServiceTest {
|
||||
@MockBean private IamTenantReviewMapper reviews;
|
||||
@MockBean private CustomerDimensionRuleInitializationService dimensionRuleInitialization;
|
||||
@MockBean private OverseasStorageFeeInitializationService overseasStorageFeeInitialization;
|
||||
@MockBean private SecurityEventOutboxService securityEvents;
|
||||
@MockBean private SecurityVersionService securityVersions;
|
||||
@MockBean private CustomerCodeRepository customerCodes;
|
||||
@Resource private MerchantOnboardingService service;
|
||||
|
||||
@@ -78,10 +78,8 @@ class MerchantOnboardingServiceTest {
|
||||
verify(membershipRoles).insert(roleGrant.capture());
|
||||
verify(dimensionRuleInitialization).initialize(result.tenantId(), 9001L);
|
||||
verify(overseasStorageFeeInitialization).initialize(result.tenantId(), 9001L, "测试企业");
|
||||
verify(securityEvents).tenantStatus(result.tenantId(), "PROFILE_REQUIRED");
|
||||
verify(securityEvents).userStatus(9001L, result.tenantId(), "MERCHANT", "ACTIVE");
|
||||
verify(securityEvents).permissionChanged(9001L, result.tenantId(), "MERCHANT",
|
||||
"INITIAL_AUTHORIZATION");
|
||||
verify(securityVersions).tenantChanged(result.tenantId());
|
||||
verify(securityVersions).subjectChanged(9001L);
|
||||
|
||||
assertEquals("PROFILE_REQUIRED", result.tenantStatus());
|
||||
assertEquals("PENDING", result.dimensionInitializationStatus());
|
||||
|
||||
@@ -30,7 +30,7 @@ class PlatformInternalUserServiceTest {
|
||||
@MockBean private MemberRepository repository;
|
||||
@MockBean private IamRoleMapper roleMapper;
|
||||
@MockBean private AuthAccountClient auth;
|
||||
@MockBean private SecurityEventOutboxService securityEvents;
|
||||
@MockBean private SecurityVersionService securityVersions;
|
||||
@Resource private PlatformInternalUserService service;
|
||||
private IamMembership membership;
|
||||
private IamUser user;
|
||||
@@ -86,8 +86,7 @@ class PlatformInternalUserServiceTest {
|
||||
verify(repository).insert(any(IamMembership.class));
|
||||
verify(repository).replaceRoles(eq(0L), anyLong(), eq(List.of(40L)));
|
||||
long authUserId = captor.getValue().accountId();
|
||||
verify(securityEvents).userStatus(authUserId, 0L, "PLATFORM", "ACTIVE");
|
||||
verify(securityEvents).permissionChanged(authUserId, 0L, "PLATFORM", "INITIAL_AUTHORIZATION");
|
||||
verify(securityVersions).subjectChanged(authUserId);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -103,8 +102,7 @@ class PlatformInternalUserServiceTest {
|
||||
service.create(request);
|
||||
|
||||
verify(repository).replaceRoles(eq(0L), anyLong(), eq(List.of(40L)));
|
||||
verify(securityEvents).userStatus(10L, 0L, "PLATFORM", "ACTIVE");
|
||||
verify(securityEvents).permissionChanged(10L, 0L, "PLATFORM", "INITIAL_AUTHORIZATION");
|
||||
verify(securityVersions).subjectChanged(10L);
|
||||
verify(auth, never()).create(anyString(), any());
|
||||
}
|
||||
|
||||
@@ -116,7 +114,7 @@ class PlatformInternalUserServiceTest {
|
||||
|
||||
service.create(request);
|
||||
|
||||
verifyNoInteractions(securityEvents);
|
||||
verifyNoInteractions(securityVersions);
|
||||
verify(repository, never()).replaceRoles(anyLong(), anyLong(), anyList());
|
||||
}
|
||||
|
||||
@@ -150,9 +148,7 @@ class PlatformInternalUserServiceTest {
|
||||
new ChangeInternalUserStatusRequest("DISABLED"), 99L);
|
||||
|
||||
assertEquals("DISABLED", result.status());
|
||||
verify(securityEvents).userStatus(10L, 0L, "PLATFORM", "DISABLED");
|
||||
verify(securityEvents).permissionChanged(10L, 0L, "PLATFORM",
|
||||
"PLATFORM_INTERNAL_USER_STATUS_CHANGED");
|
||||
verify(securityVersions).subjectChanged(10L);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -33,7 +33,7 @@ class PlatformUserManagementServiceTest {
|
||||
@MockBean private MemberRepository members;
|
||||
@MockBean private IamRoleMapper roles;
|
||||
@MockBean private AuthAccountClient auth;
|
||||
@MockBean private SecurityEventOutboxService securityEvents;
|
||||
@MockBean private SecurityVersionService securityVersions;
|
||||
@Resource private PlatformUserManagementService service;
|
||||
private IamRole platformRole;
|
||||
private IamMembership membership;
|
||||
@@ -107,8 +107,7 @@ class PlatformUserManagementServiceTest {
|
||||
verify(auth).create(eq("internal-token"), account.capture());
|
||||
long authUserId = account.getValue().accountId();
|
||||
verify(members).replaceRoles(eq(0L), anyLong(), eq(List.of(40001L)));
|
||||
verify(securityEvents).userStatus(authUserId, 0L, "PLATFORM", "DISABLED");
|
||||
verify(securityEvents).permissionChanged(authUserId, 0L, "PLATFORM", "INITIAL_AUTHORIZATION");
|
||||
verify(securityVersions).subjectChanged(authUserId);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -159,8 +158,7 @@ class PlatformUserManagementServiceTest {
|
||||
assertEquals(900L, membership.getOverseasWarehouseId());
|
||||
assertEquals(900L, response.overseasWarehouseId());
|
||||
verify(auth).revokeSessions(eq("internal-token"), any());
|
||||
verify(securityEvents).permissionChanged(10L, 0L, "PLATFORM",
|
||||
"PLATFORM_USER_AUTHORIZATION_CHANGED");
|
||||
verify(securityVersions).subjectChanged(10L);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -197,8 +195,7 @@ class PlatformUserManagementServiceTest {
|
||||
assertEquals(901L, response.overseasWarehouseId());
|
||||
verify(auth).revokeSessions(eq("internal-token"),
|
||||
eq(new AuthAccountClient.RevokeAccountSessionsRequest(Set.of(10L))));
|
||||
verify(securityEvents).permissionChanged(10L, 0L, "PLATFORM",
|
||||
"PLATFORM_USER_AUTHORIZATION_CHANGED");
|
||||
verify(securityVersions).subjectChanged(10L);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -225,8 +222,7 @@ class PlatformUserManagementServiceTest {
|
||||
assertNull(response.overseasWarehouseId());
|
||||
verify(auth).revokeSessions(eq("internal-token"),
|
||||
eq(new AuthAccountClient.RevokeAccountSessionsRequest(Set.of(10L))));
|
||||
verify(securityEvents).permissionChanged(10L, 0L, "PLATFORM",
|
||||
"PLATFORM_USER_AUTHORIZATION_CHANGED");
|
||||
verify(securityVersions).subjectChanged(10L);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -246,8 +242,7 @@ class PlatformUserManagementServiceTest {
|
||||
UserResponse result = service.changeStatus(30L, new ChangeStatusRequest("ACTIVE"), 99L);
|
||||
|
||||
assertEquals("ACTIVE", result.status());
|
||||
verify(securityEvents).userStatus(10L, 0L, "PLATFORM", "ACTIVE");
|
||||
verify(securityEvents).permissionChanged(10L, 0L, "PLATFORM", "PLATFORM_USER_STATUS_CHANGED");
|
||||
verify(securityVersions).subjectChanged(10L);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -40,7 +40,7 @@ class RbacServiceTest {
|
||||
@MockBean private RbacRepository repository;
|
||||
@MockBean private MemberRepository memberRepository;
|
||||
@MockBean private AuthAccountClient authClient;
|
||||
@MockBean private SecurityEventOutboxService securityEvents;
|
||||
@MockBean private SecurityVersionService securityVersions;
|
||||
@Resource private RbacService service;
|
||||
|
||||
@Test
|
||||
@@ -338,6 +338,8 @@ class RbacServiceTest {
|
||||
verify(authClient, times(2)).revokeSessions(eq("test-internal-token"), captor.capture());
|
||||
assertEquals(Set.of(501L, 502L), captor.getAllValues().get(0).accountIds());
|
||||
assertEquals(Set.of(501L, 502L), captor.getAllValues().get(1).accountIds());
|
||||
verify(securityVersions, times(2)).subjectChanged(501L);
|
||||
verify(securityVersions, times(2)).subjectChanged(502L);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -368,6 +370,7 @@ class RbacServiceTest {
|
||||
|
||||
verify(authClient).revokeSessions(eq("test-internal-token"),
|
||||
eq(new AuthAccountClient.RevokeAccountSessionsRequest(Set.of(501L, 502L, 503L))));
|
||||
for (long id : new long[]{501, 502, 503}) verify(securityVersions).subjectChanged(id);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -1,93 +0,0 @@
|
||||
package com.baiyue.iam.service;
|
||||
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.baiyue.common.event.EventEnvelope;
|
||||
import com.baiyue.common.event.EventPayloadCanonicalizer;
|
||||
import com.baiyue.common.event.EventPayloadHash;
|
||||
import com.baiyue.common.event.EventTypes;
|
||||
import com.baiyue.common.event.IamSecurityEventsV1;
|
||||
import com.baiyue.common.event.RegionCode;
|
||||
import com.baiyue.iam.client.SecurityEventBridgeClient;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class SecurityEventOutboxRelayTest {
|
||||
@Mock private JdbcTemplate jdbc;
|
||||
@Mock private SecurityEventBridgeClient client;
|
||||
private final ObjectMapper objectMapper = new ObjectMapper().findAndRegisterModules();
|
||||
private SecurityEventOutboxRelay relay;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
relay = new SecurityEventOutboxRelay();
|
||||
ReflectionTestUtils.setField(relay, "jdbc", jdbc);
|
||||
ReflectionTestUtils.setField(relay, "client", client);
|
||||
ReflectionTestUtils.setField(relay, "objectMapper", objectMapper);
|
||||
ReflectionTestUtils.setField(relay, "internalToken", "internal-token");
|
||||
ReflectionTestUtils.setField(relay, "maxAttempts", 12);
|
||||
}
|
||||
|
||||
@Test
|
||||
void cnToOverseasEventIsSentOnlyAfterMatchingBridgeAcknowledgement() throws Exception {
|
||||
EventEnvelope envelope = envelope(RegionCode.OVERSEAS);
|
||||
when(jdbc.update(anyString(), any(Object[].class))).thenReturn(1);
|
||||
when(jdbc.queryForObject(anyString(), eq(String.class), any(), any()))
|
||||
.thenReturn(objectMapper.writeValueAsString(envelope));
|
||||
when(client.receive(eq("internal-token"), any(EventEnvelope.class))).thenReturn(ApiResponse.success(
|
||||
new SecurityEventBridgeClient.Acknowledgement(envelope.eventId(), "ACCEPTED", "2026-08-25T07:00:00Z")));
|
||||
|
||||
relay.deliver(envelope.eventId());
|
||||
|
||||
ArgumentCaptor<EventEnvelope> sent = ArgumentCaptor.forClass(EventEnvelope.class);
|
||||
verify(client).receive(eq("internal-token"), sent.capture());
|
||||
assertThat(sent.getValue().sourceRegion()).isEqualTo(RegionCode.CN);
|
||||
assertThat(sent.getValue().targetRegion()).isEqualTo(RegionCode.OVERSEAS);
|
||||
verify(jdbc).update(anyString(), any(), any(), eq(envelope.eventId()), anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
void nonOverseasTargetNeverReachesTheBridge() throws Exception {
|
||||
EventEnvelope envelope = envelope(RegionCode.CN);
|
||||
when(jdbc.update(anyString(), any(Object[].class))).thenReturn(1);
|
||||
when(jdbc.queryForObject(anyString(), eq(String.class), any(), any()))
|
||||
.thenReturn(objectMapper.writeValueAsString(envelope));
|
||||
when(jdbc.queryForObject(anyString(), eq(Integer.class), eq(envelope.eventId()))).thenReturn(0);
|
||||
|
||||
relay.deliver(envelope.eventId());
|
||||
|
||||
verify(client, never()).receive(anyString(), any(EventEnvelope.class));
|
||||
verify(jdbc).update(anyString(), eq("PENDING"), eq(1), any(), anyString(), any(),
|
||||
eq(envelope.eventId()), anyString());
|
||||
}
|
||||
|
||||
private EventEnvelope envelope(RegionCode target) {
|
||||
var value = new IamSecurityEventsV1.PermissionVersionChanged(
|
||||
101L, 0L, "PLATFORM", 3L, "contract", Instant.parse("2026-08-25T07:00:00Z"),
|
||||
java.util.Set.of("PLATFORM_ADMIN"),
|
||||
java.util.Set.of("platform:overseas-warehouse:last-mile-order:list"));
|
||||
var payload = objectMapper.valueToTree(value);
|
||||
return new EventEnvelope("00000000-0000-4000-8000-000000000081",
|
||||
EventTypes.IAM_PERMISSION_VERSION_CHANGED, IamSecurityEventsV1.VERSION,
|
||||
Instant.parse("2026-08-25T07:00:00Z"), RegionCode.CN, target, "0",
|
||||
"AUTHORIZATION", "101", 3L, null, "trace-12345678", "correlation-12345678",
|
||||
null, payload, EventPayloadHash.sha256(EventPayloadCanonicalizer.canonicalize(payload)));
|
||||
}
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
package com.baiyue.iam.service;
|
||||
|
||||
import com.baiyue.common.event.EventEnvelope;
|
||||
import com.baiyue.common.event.IamSecurityEventsV1;
|
||||
import com.baiyue.iam.mapper.AuthorizationQueryMapper;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.CsvSource;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class SecurityEventOutboxServiceTest {
|
||||
@Mock JdbcTemplate jdbc;
|
||||
@Mock AuthorizationQueryMapper authorizationQueries;
|
||||
private final ObjectMapper objectMapper = new ObjectMapper().findAndRegisterModules();
|
||||
private SecurityEventOutboxService service;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
service = new SecurityEventOutboxService();
|
||||
ReflectionTestUtils.setField(service, "jdbc", jdbc);
|
||||
ReflectionTestUtils.setField(service, "objectMapper", objectMapper);
|
||||
ReflectionTestUtils.setField(service, "authorizationQueries", authorizationQueries);
|
||||
}
|
||||
|
||||
@Test
|
||||
void userStatusWritesAValidEnvelopeWithCorrelationId() throws Exception {
|
||||
when(jdbc.queryForObject(anyString(), eq(Long.class), any())).thenReturn(1L);
|
||||
|
||||
service.userStatus(101L, 0L, "PLATFORM", "DISABLED");
|
||||
|
||||
ArgumentCaptor<Object[]> parameters = ArgumentCaptor.forClass(Object[].class);
|
||||
verify(jdbc).update(eq("INSERT INTO iam_security_event_outbox(event_id,event_type,aggregate_type,aggregate_id,payload_json) "
|
||||
+ "VALUES (?,?,?,?,CAST(? AS JSON))"), parameters.capture());
|
||||
EventEnvelope envelope = objectMapper.readValue(String.valueOf(parameters.getValue()[4]), EventEnvelope.class);
|
||||
assertThat(envelope.requireValid().correlationId()).isNotBlank().isEqualTo(envelope.traceId());
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@CsvSource({
|
||||
"ACTIVE,ACTIVE",
|
||||
"PROFILE_REQUIRED,DISABLED",
|
||||
"PENDING,DISABLED",
|
||||
"REJECTED,DISABLED",
|
||||
"DISABLED,DISABLED",
|
||||
"ARCHIVED,DISABLED"
|
||||
})
|
||||
void tenantWorkflowStatusIsNormalizedToTheSecurityContract(String businessStatus,
|
||||
String expectedSecurityStatus) throws Exception {
|
||||
when(jdbc.queryForObject(anyString(), eq(Long.class), any())).thenReturn(3L);
|
||||
|
||||
service.tenantStatus(10001L, businessStatus);
|
||||
|
||||
ArgumentCaptor<Object[]> parameters = ArgumentCaptor.forClass(Object[].class);
|
||||
verify(jdbc).update(eq("INSERT INTO iam_security_event_outbox(event_id,event_type,aggregate_type,aggregate_id,payload_json) "
|
||||
+ "VALUES (?,?,?,?,CAST(? AS JSON))"), parameters.capture());
|
||||
EventEnvelope envelope = objectMapper.readValue(String.valueOf(parameters.getValue()[4]), EventEnvelope.class);
|
||||
IamSecurityEventsV1.TenantStatusChanged payload = objectMapper.treeToValue(
|
||||
envelope.payload(), IamSecurityEventsV1.TenantStatusChanged.class);
|
||||
assertThat(payload.status()).isEqualTo(expectedSecurityStatus);
|
||||
assertThat(envelope.aggregateVersion()).isEqualTo(3L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void permissionEventCarriesOnlyTheOverseasAuthorizationSnapshot() throws Exception {
|
||||
when(jdbc.queryForObject(anyString(), eq(Long.class), any())).thenReturn(7L);
|
||||
when(authorizationQueries.findRolesByIdentity(101L, 0L, "ADMIN"))
|
||||
.thenReturn(List.of("PLATFORM_ADMIN"));
|
||||
when(authorizationQueries.findPermissionsByIdentity(101L, 0L, "ADMIN"))
|
||||
.thenReturn(List.of("platform:overseas-warehouse:orders:view", "platform:iam:users:view",
|
||||
"merchant:overseas-warehouse:return-inventory:list", "lastleg:orders:view"));
|
||||
|
||||
service.permissionChanged(101L, 0L, "ADMIN", "ROLE_CHANGED");
|
||||
|
||||
ArgumentCaptor<Object[]> parameters = ArgumentCaptor.forClass(Object[].class);
|
||||
verify(jdbc).update(eq("INSERT INTO iam_security_event_outbox(event_id,event_type,aggregate_type,aggregate_id,payload_json) "
|
||||
+ "VALUES (?,?,?,?,CAST(? AS JSON))"), parameters.capture());
|
||||
EventEnvelope envelope = objectMapper.readValue(String.valueOf(parameters.getValue()[4]), EventEnvelope.class);
|
||||
IamSecurityEventsV1.PermissionVersionChanged payload = objectMapper.treeToValue(
|
||||
envelope.payload(), IamSecurityEventsV1.PermissionVersionChanged.class);
|
||||
assertThat(payload.roles()).containsExactly("PLATFORM_ADMIN");
|
||||
assertThat(payload.authorities()).containsExactlyInAnyOrder(
|
||||
"platform:overseas-warehouse:orders:view",
|
||||
"merchant:overseas-warehouse:return-inventory:list", "lastleg:orders:view");
|
||||
assertThat(payload.authorities()).doesNotContain("platform:iam:users:view");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package com.baiyue.iam.service;
|
||||
|
||||
import org.h2.jdbcx.JdbcDataSource;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.aop.framework.ProxyFactory;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
import org.springframework.transaction.annotation.AnnotationTransactionAttributeSource;
|
||||
import org.springframework.transaction.interceptor.TransactionInterceptor;
|
||||
import org.springframework.transaction.support.TransactionTemplate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.*;
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
|
||||
class SecurityVersionTransactionTest {
|
||||
@Test void concurrentVersionsAreDurableAndRollbackWithMutation() throws Exception {
|
||||
var ds = new JdbcDataSource(); ds.setURL("jdbc:h2:mem:" + UUID.randomUUID() + ";MODE=MySQL;DB_CLOSE_DELAY=-1;LOCK_TIMEOUT=5000");
|
||||
var jdbc = new JdbcTemplate(ds);
|
||||
jdbc.execute("CREATE TABLE iam_security_subject_version(auth_user_id BIGINT PRIMARY KEY,permission_version BIGINT)");
|
||||
jdbc.execute("CREATE TABLE iam_security_tenant_version(tenant_id BIGINT PRIMARY KEY,status_version BIGINT)");
|
||||
var manager = new DataSourceTransactionManager(ds);
|
||||
var target = new SecurityVersionService(); ReflectionTestUtils.setField(target, "jdbc", jdbc);
|
||||
var proxy = new ProxyFactory(target); proxy.setProxyTargetClass(true);
|
||||
proxy.addAdvice(new TransactionInterceptor(manager, new AnnotationTransactionAttributeSource()));
|
||||
var service = (SecurityVersionService) proxy.getProxy();
|
||||
assertThatThrownBy(() -> service.subjectChanged(42)).isInstanceOf(org.springframework.transaction.IllegalTransactionStateException.class);
|
||||
new TransactionTemplate(manager).executeWithoutResult(tx -> { service.subjectChanged(42); service.tenantChanged(7); });
|
||||
var start = new CountDownLatch(1); var executor = Executors.newFixedThreadPool(4);
|
||||
try {
|
||||
var futures = new ArrayList<Future<?>>();
|
||||
for (int i=0; i<8; i++) futures.add(executor.submit(() -> {
|
||||
try { if (!start.await(5, TimeUnit.SECONDS)) throw new AssertionError("Start barrier timed out"); }
|
||||
catch (InterruptedException failure) { Thread.currentThread().interrupt(); throw new AssertionError(failure); }
|
||||
new TransactionTemplate(manager).executeWithoutResult(tx -> service.subjectChanged(42));
|
||||
}));
|
||||
start.countDown(); for (var future : futures) future.get(10, TimeUnit.SECONDS);
|
||||
} finally { executor.shutdownNow(); }
|
||||
new TransactionTemplate(manager).executeWithoutResult(tx -> {
|
||||
service.subjectChanged(42); service.tenantChanged(7); tx.setRollbackOnly();
|
||||
});
|
||||
assertThat(jdbc.queryForObject("SELECT permission_version FROM iam_security_subject_version WHERE auth_user_id=42", Long.class)).isEqualTo(9);
|
||||
assertThat(jdbc.queryForObject("SELECT status_version FROM iam_security_tenant_version WHERE tenant_id=7", Long.class)).isEqualTo(1);
|
||||
}
|
||||
}
|
||||
+5
-10
@@ -44,7 +44,7 @@ class TenantAdministratorProvisioningServiceTest {
|
||||
@MockBean private MemberRepository members;
|
||||
@MockBean private IamRoleMapper roles;
|
||||
@MockBean private AuthAccountClient auth;
|
||||
@MockBean private SecurityEventOutboxService securityEvents;
|
||||
@MockBean private SecurityVersionService securityVersions;
|
||||
@Resource private TenantAdministratorProvisioningService service;
|
||||
private IamTenant tenant;
|
||||
|
||||
@@ -104,9 +104,7 @@ class TenantAdministratorProvisioningServiceTest {
|
||||
verify(members).insertUser(any(IamUser.class));
|
||||
verify(members).insert(any(IamMembership.class));
|
||||
verify(members).replaceRoles(eq(tenant.getId()), anyLong(), eq(List.of(4000100L)));
|
||||
verify(securityEvents).userStatus(account.getValue().accountId(), tenant.getId(), "MERCHANT", "ACTIVE");
|
||||
verify(securityEvents).permissionChanged(account.getValue().accountId(), tenant.getId(), "MERCHANT",
|
||||
"INITIAL_AUTHORIZATION");
|
||||
verify(securityVersions).subjectChanged(account.getValue().accountId());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -141,7 +139,7 @@ class TenantAdministratorProvisioningServiceTest {
|
||||
assertEquals(null, result.temporaryPassword());
|
||||
verify(auth, never()).create(anyString(), any());
|
||||
verify(members, never()).replaceRoles(anyLong(), anyLong(), any());
|
||||
verifyNoInteractions(securityEvents);
|
||||
verifyNoInteractions(securityVersions);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -160,8 +158,7 @@ class TenantAdministratorProvisioningServiceTest {
|
||||
assertFalse(result.created());
|
||||
verify(auth, never()).create(anyString(), any());
|
||||
verify(members).replaceRoles(tenant.getId(), owner.getId(), List.of(4000200L, 4000100L));
|
||||
verify(securityEvents).permissionChanged(user.getAuthUserId(), tenant.getId(), "MERCHANT",
|
||||
"MEMBER_ROLES_CHANGED");
|
||||
verify(securityVersions).subjectChanged(user.getAuthUserId());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -199,9 +196,7 @@ class TenantAdministratorProvisioningServiceTest {
|
||||
verify(members, never()).insertUser(any());
|
||||
verify(members, never()).insert(any());
|
||||
verify(members, never()).replaceRoles(anyLong(), anyLong(), any());
|
||||
verify(securityEvents).userStatus(user.getAuthUserId(), tenant.getId(), "MERCHANT", "ACTIVE");
|
||||
verify(securityEvents).permissionChanged(user.getAuthUserId(), tenant.getId(), "MERCHANT",
|
||||
"INITIAL_AUTHORIZATION");
|
||||
verify(securityVersions).subjectChanged(user.getAuthUserId());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -30,7 +30,7 @@ class TenantCustomerExportTest {
|
||||
@MockBean private CustomerDimensionRuleInitializationService dimensionRuleInitialization;
|
||||
@MockBean private OverseasStorageFeeInitializationService overseasStorageFeeInitialization;
|
||||
@MockBean private TenantAdministratorProvisioningService administratorProvisioning;
|
||||
@MockBean private SecurityEventOutboxService securityEvents;
|
||||
@MockBean private SecurityVersionService securityVersions;
|
||||
@Resource private TenantService service;
|
||||
@Test
|
||||
void returnsUnmaskedCustomerRowsAndAdvancingCursor() {
|
||||
|
||||
@@ -57,7 +57,7 @@ class TenantServiceTest {
|
||||
@MockBean private MemberRepository members;
|
||||
@MockBean private IamRoleMapper roles;
|
||||
@MockBean private AuthAccountClient authClient;
|
||||
@MockBean private SecurityEventOutboxService securityEvents;
|
||||
@MockBean private SecurityVersionService securityVersions;
|
||||
@MockBean private CustomerDimensionRuleInitializationService dimensionRuleInitialization;
|
||||
@MockBean private OverseasStorageFeeInitializationService overseasStorageFeeInitialization;
|
||||
@SpyBean private TenantAdministratorProvisioningService provisioning;
|
||||
@@ -196,13 +196,13 @@ class TenantServiceTest {
|
||||
when(authClient.revokeSessions("internal-token", request)).thenReturn(null);
|
||||
assertThrows(BizException.class,
|
||||
() -> service.changeMerchantStatus(10001L, new ChangeTenantStatusRequest("DISABLED")));
|
||||
verify(securityEvents, never()).tenantStatus(org.mockito.ArgumentMatchers.anyLong(), any());
|
||||
verify(securityVersions, never()).tenantChanged(org.mockito.ArgumentMatchers.anyLong());
|
||||
|
||||
when(authClient.revokeSessions("internal-token", request))
|
||||
.thenReturn(com.baiyue.common.core.api.ApiResponse.success(null));
|
||||
assertEquals("DISABLED", service.changeMerchantStatus(10001L,
|
||||
new ChangeTenantStatusRequest("DISABLED")).status());
|
||||
verify(securityEvents).tenantStatus(10001L, "DISABLED");
|
||||
verify(securityVersions).tenantChanged(10001L);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -357,7 +357,7 @@ class TenantServiceTest {
|
||||
verify(repository).insertTenant(argThat(tenant -> Long.valueOf(9001L).equals(tenant.getCreatedBy())
|
||||
&& Long.valueOf(9001L).equals(tenant.getUpdatedBy())));
|
||||
verify(dimensionRuleInitialization).initialize(created.tenant().id(), 9001L);
|
||||
verify(securityEvents).tenantStatus(created.tenant().id(), "PROFILE_REQUIRED");
|
||||
verify(securityVersions).tenantChanged(created.tenant().id());
|
||||
verify(provisioning).provisionPlatformCreatedTenant(any(), eq(9001L));
|
||||
verify(repository, never()).insertReview(any());
|
||||
}
|
||||
@@ -474,7 +474,7 @@ class TenantServiceTest {
|
||||
assertEquals("ACTIVE", tenant.getStatus());
|
||||
verify(repository).saveReview(review);
|
||||
verify(repository).saveTenant(tenant);
|
||||
verify(securityEvents).tenantStatus(10001L, "ACTIVE");
|
||||
verify(securityVersions).tenantChanged(10001L);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
Reference in New Issue
Block a user