This commit is contained in:
2026-08-07 19:23:40 +08:00
parent 37fec1e012
commit c698290106
39 changed files with 1897 additions and 13 deletions
@@ -0,0 +1,27 @@
package com.baiyue.iam.client;
import org.junit.jupiter.api.Test;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.PostMapping;
import static org.assertj.core.api.Assertions.assertThat;
class CustomerDimensionRuleClientContractTest {
@Test
void usesTheFirstlegInternalDimensionConfigContract() throws Exception {
FeignClient feign = CustomerDimensionRuleClient.class.getAnnotation(FeignClient.class);
PostMapping initialize = CustomerDimensionRuleClient.class
.getMethod("initialize", String.class,
CustomerDimensionRuleClient.InitializationRequest.class)
.getAnnotation(PostMapping.class);
DeleteMapping compensate = CustomerDimensionRuleClient.class
.getMethod("compensate", String.class, long.class, String.class)
.getAnnotation(DeleteMapping.class);
assertThat(feign.name()).isEqualTo("firstleg-service");
assertThat(feign.path()).isEqualTo("/internal/firstleg/v1/customer-dimension-configs");
assertThat(initialize.value()).containsExactly("/defaults");
assertThat(compensate.value()).containsExactly("/defaults");
}
}
@@ -0,0 +1,40 @@
package com.baiyue.iam.repository;
import org.junit.jupiter.api.Test;
import org.springframework.core.io.ClassPathResource;
import java.nio.charset.StandardCharsets;
import static org.assertj.core.api.Assertions.assertThat;
class CustomerDimensionConfigPermissionSqlContractTest {
@Test
void declaresIndependentPlatformViewAndEditPermissions() throws Exception {
String sql = new ClassPathResource("db/customer-dimension-config-permissions.sql")
.getContentAsString(StandardCharsets.UTF_8);
assertThat(sql)
.contains("(5102111, 0, 'PLATFORM', 'BUTTON', 5100010")
.contains("(5102112, 0, 'PLATFORM', 'BUTTON', 5100010")
.contains("'firstleg:dimension-config:view'")
.contains("'firstleg:dimension-config:edit'")
.contains("SIGNAL SQLSTATE '45000'")
.contains("id = 5102111 AND permission_code <> 'firstleg:dimension-config:view'")
.contains("permission_code = 'firstleg:dimension-config:edit' AND id <> 5102112");
}
@Test
void resolvesRoleGrantsByBusinessKeys() throws Exception {
String sql = new ClassPathResource("db/customer-dimension-config-permissions.sql")
.getContentAsString(StandardCharsets.UTF_8);
assertThat(sql)
.contains("SELECT role.id, permission.id, role.tenant_id")
.contains("role.role_code = 'PLATFORM_ADMIN'")
.contains("permission.permission_code IN (")
.contains("role.role_code = 'ORDER_OPERATOR'")
.contains("permission.permission_code = 'firstleg:dimension-config:view'")
.doesNotContain("SELECT 4000001")
.doesNotContain("SELECT 4000003");
}
}
@@ -0,0 +1,138 @@
package com.baiyue.iam.service;
import com.baiyue.common.core.api.ApiResponse;
import com.baiyue.common.core.api.ResultCode;
import com.baiyue.common.core.exception.BizException;
import com.baiyue.iam.client.CustomerDimensionRuleClient;
import jakarta.annotation.Resource;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import org.springframework.transaction.support.TransactionSynchronization;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import java.util.List;
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 static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@SpringJUnitConfig(CustomerDimensionRuleInitializationService.class)
@TestPropertySource(properties = "baiyue.security.internal-service-token=internal-token")
class CustomerDimensionRuleInitializationServiceTest {
@MockBean private CustomerDimensionRuleClient client;
@Resource private CustomerDimensionRuleInitializationService service;
@AfterEach
void clearSynchronization() {
if (TransactionSynchronizationManager.isSynchronizationActive()) {
TransactionSynchronizationManager.clearSynchronization();
}
}
@Test
void retriesWithOneStableKeyAndReturnsTheIdempotentResult() {
when(client.initialize(anyString(), any()))
.thenThrow(new RuntimeException("timeout"))
.thenReturn(ApiResponse.failure(ResultCode.SERVICE_UNAVAILABLE,
"temporarily unavailable", "request-1", "trace-1"))
.thenReturn(ApiResponse.success(new CustomerDimensionRuleClient.InitializationResponse(91L, false)));
var result = service.initialize(10001L, 9001L);
assertFalse(result.created());
ArgumentCaptor<CustomerDimensionRuleClient.InitializationRequest> requests =
ArgumentCaptor.forClass(CustomerDimensionRuleClient.InitializationRequest.class);
verify(client, times(3)).initialize(anyString(), requests.capture());
assertEquals(List.of("iam-customer-create:10001", "iam-customer-create:10001",
"iam-customer-create:10001"),
requests.getAllValues().stream().map(
CustomerDimensionRuleClient.InitializationRequest::initializationKey).toList());
assertEquals(List.of(9001L, 9001L, 9001L), requests.getAllValues().stream()
.map(CustomerDimensionRuleClient.InitializationRequest::operatorId).toList());
assertEquals(List.of(10001L, 10001L, 10001L), requests.getAllValues().stream()
.map(CustomerDimensionRuleClient.InitializationRequest::tenantId).toList());
}
@Test
void reportsServiceUnavailableAfterThreeFailedAttemptsAndTriesCompensation() {
when(client.initialize(anyString(), any())).thenThrow(new RuntimeException("timeout"));
when(client.compensate(anyString(), anyLong(), anyString())).thenReturn(ApiResponse.success(null));
BizException failure = assertThrows(BizException.class, () -> service.initialize(10001L, 9001L));
assertEquals(ResultCode.SERVICE_UNAVAILABLE, failure.getResultCode());
verify(client, times(3)).initialize(anyString(), any());
verify(client).compensate("internal-token", 10001L, "iam-customer-create:10001");
}
@Test
void localRollbackCompensatesOnlyAConfigurationCreatedByThisRegistration() {
when(client.initialize(anyString(), any()))
.thenReturn(ApiResponse.success(new CustomerDimensionRuleClient.InitializationResponse(91L, true)));
when(client.compensate(anyString(), anyLong(), anyString())).thenReturn(ApiResponse.success(null));
TransactionSynchronizationManager.initSynchronization();
var result = service.initialize(10001L, 9001L);
List<TransactionSynchronization> synchronizations =
TransactionSynchronizationManager.getSynchronizations();
TransactionSynchronizationManager.clearSynchronization();
synchronizations.forEach(value -> value.afterCompletion(TransactionSynchronization.STATUS_ROLLED_BACK));
assertTrue(result.created());
verify(client).compensate("internal-token", 10001L, "iam-customer-create:10001");
}
@Test
void localRollbackDoesNotDeleteAConfigurationThatAlreadyExisted() {
when(client.initialize(anyString(), any()))
.thenReturn(ApiResponse.success(new CustomerDimensionRuleClient.InitializationResponse(91L, false)));
TransactionSynchronizationManager.initSynchronization();
service.initialize(10001L, 9001L);
List<TransactionSynchronization> synchronizations =
TransactionSynchronizationManager.getSynchronizations();
TransactionSynchronizationManager.clearSynchronization();
synchronizations.forEach(value -> value.afterCompletion(TransactionSynchronization.STATUS_ROLLED_BACK));
verify(client, never()).compensate(anyString(), anyLong(), anyString());
}
@Test
void responseTimeoutThenIdempotentSuccessStillCompensatesOnLocalRollback() {
when(client.initialize(anyString(), any()))
.thenThrow(new RuntimeException("response timeout"))
.thenReturn(ApiResponse.success(new CustomerDimensionRuleClient.InitializationResponse(91L, true)));
when(client.compensate(anyString(), anyLong(), anyString())).thenReturn(ApiResponse.success(null));
TransactionSynchronizationManager.initSynchronization();
service.initialize(10001L, 9001L);
List<TransactionSynchronization> synchronizations =
TransactionSynchronizationManager.getSynchronizations();
TransactionSynchronizationManager.clearSynchronization();
synchronizations.forEach(value -> value.afterCompletion(TransactionSynchronization.STATUS_ROLLED_BACK));
verify(client, times(2)).initialize(anyString(), any());
verify(client).compensate("internal-token", 10001L, "iam-customer-create:10001");
}
@Test
void committedClientRegistrationCanBeCompensatedWithItsDeterministicKey() {
when(client.compensate(anyString(), anyLong(), anyString())).thenReturn(ApiResponse.success(null));
service.compensateRegistration(10001L);
verify(client).compensate("internal-token", 10001L, "iam-customer-create:10001");
}
}
@@ -39,6 +39,7 @@ class MerchantOnboardingServiceTest {
@MockBean private IamTenantAddressMapper addresses;
@MockBean private IamTenantQualificationMapper qualifications;
@MockBean private IamTenantReviewMapper reviews;
@MockBean private CustomerDimensionRuleInitializationService dimensionRuleInitialization;
@Resource private MerchantOnboardingService service;
@Test
void clientRegistrationCreatesOwnerAndAssignsGlobalMerchantOwnerRole() {
@@ -63,6 +64,7 @@ class MerchantOnboardingServiceTest {
verify(users).insert(user.capture());
verify(memberships).insert(membership.capture());
verify(membershipRoles).insert(roleGrant.capture());
verify(dimensionRuleInitialization).initialize(result.tenantId(), 9001L);
assertEquals("PROFILE_REQUIRED", result.tenantStatus());
assertEquals(result.tenantId(), tenant.getValue().getId());
@@ -104,6 +106,7 @@ class MerchantOnboardingServiceTest {
service.compensateRegistration(9001L);
verify(dimensionRuleInitialization).compensateRegistration(10001L);
verify(membershipRoles).delete(any());
verify(memberships).deleteById(30001L);
verify(addresses).delete(any());
@@ -111,6 +114,28 @@ class MerchantOnboardingServiceTest {
verify(tenants).deleteById((java.io.Serializable) Long.valueOf(10001L));
}
@Test
void clientRegistrationFailsWhenDefaultDimensionRuleCannotBeInitialized() {
IamRole ownerRole = new IamRole();
ownerRole.setId(4000100L);
ownerRole.setTenantId(0L);
ownerRole.setAppCode("MERCHANT");
ownerRole.setRoleCode("MERCHANT_OWNER");
ownerRole.setStatus("ACTIVE");
when(roles.selectOne(any())).thenReturn(ownerRole);
org.mockito.Mockito.doThrow(new com.baiyue.common.core.exception.BizException(
com.baiyue.common.core.api.ResultCode.SERVICE_UNAVAILABLE, "初始化失败"))
.when(dimensionRuleInitialization).initialize(
org.mockito.ArgumentMatchers.anyLong(), org.mockito.ArgumentMatchers.anyLong());
var failure = assertThrows(com.baiyue.common.core.exception.BizException.class,
() -> service.register(new MerchantRegistrationRequest(
9001L, "注册人", "测试企业", "张三", "13800138000",
"owner@example.com", "CN", "深圳市宝安区")));
assertEquals(com.baiyue.common.core.api.ResultCode.SERVICE_UNAVAILABLE, failure.getResultCode());
}
@Test
void compensationRefusesAfterQualificationUpload() {
@@ -136,4 +161,34 @@ class MerchantOnboardingServiceTest {
verify(users, never()).deleteById((java.io.Serializable) any());
verify(tenants, never()).deleteById((java.io.Serializable) any());
}
@Test
void compensationKeepsIamRegistrationWhenDimensionRuleCompensationFails() {
IamUser user = new IamUser();
user.setId(20001L);
user.setAuthUserId(9001L);
IamMembership membership = new IamMembership();
membership.setId(30001L);
membership.setTenantId(10001L);
membership.setUserId(20001L);
membership.setMemberType("OWNER");
IamTenant tenant = new IamTenant();
tenant.setId(10001L);
tenant.setStatus("PROFILE_REQUIRED");
when(users.selectOne(any())).thenReturn(user);
when(memberships.selectList(any())).thenReturn(java.util.List.of(membership));
when(tenants.selectOne(any())).thenReturn(tenant);
when(qualifications.selectCount(any())).thenReturn(0L);
when(reviews.selectCount(any())).thenReturn(0L);
org.mockito.Mockito.doThrow(new com.baiyue.common.core.exception.BizException(
com.baiyue.common.core.api.ResultCode.SERVICE_UNAVAILABLE, "补偿失败"))
.when(dimensionRuleInitialization).compensateRegistration(10001L);
assertThrows(com.baiyue.common.core.exception.BizException.class,
() -> service.compensateRegistration(9001L));
verify(membershipRoles, never()).delete(any());
verify(users, never()).deleteById((java.io.Serializable) any());
verify(tenants, never()).deleteById((java.io.Serializable) any());
}
}
@@ -23,6 +23,7 @@ class TenantCustomerExportTest {
@MockBean private ProfileResourceRepository profileRepository;
@MockBean private AuthAccountClient authAccountClient;
@MockBean private TenantApprovalInitializationCoordinator approvalInitialization;
@MockBean private CustomerDimensionRuleInitializationService dimensionRuleInitialization;
@Resource private TenantService service;
@Test
void returnsUnmaskedCustomerRowsAndAdvancingCursor() {
@@ -48,6 +48,7 @@ class TenantServiceTest {
@MockBean private MemberRepository members;
@MockBean private IamRoleMapper roles;
@MockBean private AuthAccountClient authClient;
@MockBean private CustomerDimensionRuleInitializationService dimensionRuleInitialization;
@SpyBean private TenantAdministratorProvisioningService provisioning;
@Resource private TenantService service;
@@ -177,9 +178,23 @@ class TenantServiceTest {
assertEquals("基础版", created.servicePackage());
verify(repository).insertTenant(argThat(tenant -> Long.valueOf(9001L).equals(tenant.getCreatedBy())
&& Long.valueOf(9001L).equals(tenant.getUpdatedBy())));
verify(dimensionRuleInitialization).initialize(created.id(), 9001L);
verify(repository, never()).insertReview(any());
}
@Test
void platformRegistrationFailsWhenDefaultDimensionRuleCannotBeInitialized() {
doThrow(new BizException(ResultCode.SERVICE_UNAVAILABLE, "初始化失败"))
.when(dimensionRuleInitialization).initialize(any(Long.class), any(Long.class));
BizException failure = assertThrows(BizException.class, () -> service.register(new RegisterRequest(
"Example Merchant", "Example Merchant Ltd", "Bob", "Alice", "13800138000",
"alice@example.com", "基础版", null, null, 30, null,
null, null, "CNY", null, null), 9001L));
assertEquals(ResultCode.SERVICE_UNAVAILABLE, failure.getResultCode());
}
@Test
void platformRegistrationAlwaysCreatesTheDraftWithoutACreditCode() {