This commit is contained in:
2026-08-03 13:24:06 +08:00
45 changed files with 1479 additions and 403 deletions
@@ -1,7 +1,6 @@
package com.baiyue.iam.controller;
import com.baiyue.common.security.AuthenticatedUser;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
@@ -11,10 +10,9 @@ import java.lang.reflect.Method;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
@DisplayName("权限快照接口契约")
class AuthorizationControllerContractTest {
@Test
void currentPermissionsEndpointDoesNotRequireRoleManagementPermission() throws Exception {
void 当前权限接口不应依赖角色管理权限() throws Exception {
Method method = AuthorizationController.class.getMethod("permissions", String.class,
AuthenticatedUser.class);
@@ -1,6 +1,5 @@
package com.baiyue.iam.controller;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.yaml.snakeyaml.Yaml;
@@ -10,10 +9,9 @@ import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
@DisplayName("IAM OpenAPI 契约")
class IamOpenApiContractTest {
@Test
void openApiDeclaresPermissionPaginationAndInternalUserResponseSchema() throws IOException {
void OpenApi应声明权限分页和内部用户的真实响应结构() throws IOException {
String document;
try (var stream = getClass().getResourceAsStream("/openapi/iam-v1.yaml")) {
assertThat(stream).isNotNull();
@@ -24,13 +22,29 @@ class IamOpenApiContractTest {
.contains("/api/merchant/v1/iam/authorization/permissions:")
.contains("PermissionCodesApiResponse:")
.contains("TenantPageApiResponse:")
.contains("TenantApiResponse:")
.contains("TenantSuccess:")
.contains("salesOwnerName:")
.contains("paymentTermDays:")
.contains("registeredCapitalCurrency:")
.contains("createdAt: {type: string, format: date-time, description: 入驻时间}")
.contains("id: {type: string, pattern: '^\\d+$'")
.contains("/api/platform/v1/iam/tenants/{tenantId}/addresses:")
.contains("/api/platform/v1/iam/tenants/{tenantId}/qualifications:")
.contains("platform:customer:view")
.contains("TenantAddressListApiResponse:")
.contains("TenantQualificationListApiResponse:")
.contains("InternalUserListApiResponse:")
.contains("/api/platform/v1/iam/internal-users/{membershipId}/roles:")
.contains("AssignInternalUserRolesRequest:")
.contains("/api/merchant/v1/iam/members/{membershipId}:")
.contains("仅当 roleIds 与当前角色集合实际不同时额外要求 merchant:member:grant-role")
.contains("传空数组清空角色也属于角色变更")
.contains("roleIds 为服务端实时查询的当前角色集合")
.contains("UpdateMemberRequest:")
.contains("membershipId: {type: integer, format: int64}")
.contains("roleIds: {type: array, items: {type: integer, format: int64}}")
.contains("example: '200'")
.doesNotContain("'422': {$ref: '#/components/responses/UnprocessableEntity'}");
.contains("'422': {$ref: '#/components/responses/UnprocessableEntity'}");
Map<String, Object> openApi = new Yaml().load(document);
assertThat(openApi).containsEntry("openapi", "3.0.3");
@@ -0,0 +1,29 @@
package com.baiyue.iam.controller;
import com.baiyue.common.security.AuthenticatedUser;
import org.junit.jupiter.api.Test;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import java.lang.reflect.Method;
import static org.junit.jupiter.api.Assertions.assertEquals;
class PlatformCustomerResourceContractTest {
@Test
void 平台地址和资质只读查询统一使用客户查看权限() throws Exception {
assertReadOnlyViewContract("platformAddresses",
"/api/platform/v1/iam/tenants/{tenantId}/addresses");
assertReadOnlyViewContract("platformQualifications",
"/api/platform/v1/iam/tenants/{tenantId}/qualifications");
}
private void assertReadOnlyViewContract(String methodName, String path) throws Exception {
Method method = ProfileResourceController.class.getMethod(
methodName, long.class, AuthenticatedUser.class);
assertEquals(path, method.getAnnotation(GetMapping.class).value()[0]);
assertEquals("hasAuthority('platform:customer:view')",
method.getAnnotation(PreAuthorize.class).value());
}
}
@@ -1,6 +1,5 @@
package com.baiyue.iam.controller;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
@@ -11,11 +10,10 @@ import java.lang.reflect.Method;
import static org.junit.jupiter.api.Assertions.*;
@DisplayName("平台内部用户接口契约")
class PlatformInternalUserContractTest {
@Test
void internalUserListRequiresListPermission() throws Exception {
void 内部用户列表必须校验列表权限() throws Exception {
Method method = ProfileResourceController.class.getMethod("internalUsers",
com.baiyue.common.security.AuthenticatedUser.class);
@@ -26,7 +24,7 @@ class PlatformInternalUserContractTest {
}
@Test
void platformUserWriteEndpointsAddressMembershipId() {
void 平台用户写接口以成员关系主键寻址() {
assertMapping("updateInternalUser", "/api/platform/v1/iam/internal-users/{membershipId:\\d+}");
assertMapping("changeInternalUserStatus",
"/api/platform/v1/iam/internal-users/{membershipId:\\d+}/status");
@@ -5,9 +5,6 @@ import io.swagger.v3.oas.annotations.responses.ApiResponses;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;
import jakarta.validation.Valid;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
@@ -21,10 +18,9 @@ import java.util.stream.Collectors;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
@DisplayName("平台客户接口契约")
class PlatformTenantControllerContractTest {
@Test
void merchantPageEndpointDeclaresPermissionPaginationConstraintsAndUnifiedBusinessStatus() throws Exception {
void 客户分页接口声明权限分页约束和错误状态() throws Exception {
Method method = PlatformTenantController.class.getMethod("page", long.class, long.class,
String.class, String.class, AuthenticatedUser.class);
@@ -40,11 +36,11 @@ class PlatformTenantControllerContractTest {
Set<String> statuses = Arrays.stream(method.getAnnotation(ApiResponses.class).value())
.map(io.swagger.v3.oas.annotations.responses.ApiResponse::responseCode)
.collect(Collectors.toSet());
assertEquals(Set.of("200", "401", "403"), statuses);
assertEquals(Set.of("200", "400", "401", "403", "422"), statuses);
}
@Test
void pendingMerchantListRequiresMerchantApprovalPermission() throws Exception {
void 待审列表必须校验客户审核权限() throws Exception {
Method method = PlatformTenantController.class.getMethod("reviews", AuthenticatedUser.class);
assertEquals("hasAuthority('platform:customer:review')",
@@ -52,16 +48,11 @@ class PlatformTenantControllerContractTest {
}
@Test
void reviewRequestsAreValidatedAndCommentMatchesDatabaseLimit() throws Exception {
Method approve = PlatformTenantController.class.getMethod("approve", long.class,
com.baiyue.iam.dto.TenantDtos.ReviewRequest.class, AuthenticatedUser.class);
Method reject = PlatformTenantController.class.getMethod("reject", long.class,
com.baiyue.iam.dto.TenantDtos.ReviewRequest.class, AuthenticatedUser.class);
Method comment = com.baiyue.iam.dto.TenantDtos.ReviewRequest.class.getMethod("comment");
void 客户详情必须使用独立查看权限() throws Exception {
Method method = PlatformTenantController.class.getMethod("tenant", long.class, AuthenticatedUser.class);
assertNotNull(annotation(approve, 1, Valid.class));
assertNotNull(annotation(reject, 1, Valid.class));
assertEquals(500, comment.getAnnotation(Size.class).max());
assertEquals("hasAuthority('platform:customer:view')",
method.getAnnotation(PreAuthorize.class).value());
}
private <T extends Annotation> T annotation(Method method, int parameter, Class<T> type) {
@@ -1,6 +1,5 @@
package com.baiyue.iam.controller;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.web.bind.annotation.PostMapping;
@@ -11,7 +10,6 @@ import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
@DisplayName("企业资质上传接口契约")
class QualificationUploadContractTest {
@Test
@@ -0,0 +1,60 @@
package com.baiyue.iam.repository;
import org.junit.jupiter.api.Test;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.assertj.core.api.Assertions.assertThat;
class CustomerProfileMigrationContractTest {
@Test
void 客户档案迁移覆盖全部字段并通过元数据判断保持可重复执行() throws IOException {
String sql;
try (var stream = getClass().getResourceAsStream("/db/phase1-customer-profile.sql")) {
assertThat(stream).isNotNull();
sql = new String(stream.readAllBytes(), StandardCharsets.UTF_8);
}
assertThat(sql)
.contains("USE baiyue_iam;")
.contains("SET NAMES utf8mb4;")
.contains("information_schema.columns")
.contains("contact_wechat")
.contains("sales_owner_name")
.contains("payment_term_days")
.contains("enterprise_type")
.contains("established_date")
.contains("registered_capital")
.contains("registered_capital_currency")
.contains("registered_address")
.contains("internal_remark");
}
@Test
void 统一测试环境入口包含客户字段查看权限和执行后核验() throws IOException {
Path repositoryRoot = Path.of(System.getProperty("maven.multiModuleProjectDirectory", ".."));
Path migration = repositoryRoot.resolve("deploy/sql/2026-07-31一期回归测试环境增量迁移.sql");
Path readme = repositoryRoot.resolve("deploy/sql/README.md");
assertThat(migration).exists();
assertThat(readme).exists();
String sql = Files.readString(migration, StandardCharsets.UTF_8);
String instructions = Files.readString(readme, StandardCharsets.UTF_8);
assertThat(sql)
.contains("CALL regression_assert_table('iam_tenant')")
.contains("CREATE PROCEDURE regression_add_iam_column")
.contains("platform:customer:view")
.contains("'IAM客户档案字段' AS check_item")
.contains("9 AS expected_count")
.contains("44 AS expected_count")
.contains("MIGRATION_COMPLETED");
assertThat(instructions)
.contains("phase1-platform-master-data-and-permission-management.sql")
.contains("phase1-customer-profile.sql")
.contains("测试环境只执行统一入口");
}
}
@@ -5,7 +5,6 @@ import com.baiyue.common.core.exception.BizException;
import com.baiyue.iam.dto.AuthorizationDtos.SnapshotRequest;
import com.baiyue.common.security.AuthenticatedUser;
import com.baiyue.iam.mapper.AuthorizationQueryMapper;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.test.util.ReflectionTestUtils;
@@ -17,7 +16,6 @@ import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@DisplayName("权限快照服务")
class AuthorizationSnapshotServiceTest {
@Test
void loadsRolesAndPermissionsForActiveMerchantIdentity() {
@@ -84,7 +82,7 @@ class AuthorizationSnapshotServiceTest {
}
@Test
void emptyRoleAndPermissionQueriesDoNotTriggerServerError() {
void 空角色和权限查询结果不应触发五百错误() {
AuthorizationQueryMapper mapper = mock(AuthorizationQueryMapper.class);
when(mapper.countActiveIdentity(42L, 0L, 9L)).thenReturn(1);
when(mapper.findRoles(42L, 0L, 9L, "PLATFORM")).thenReturn(null);
@@ -11,7 +11,6 @@ import com.baiyue.iam.entity.IamUser;
import com.baiyue.iam.mapper.IamRoleMapper;
import com.baiyue.iam.repository.MemberRepository;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.util.List;
@@ -21,7 +20,6 @@ import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
@DisplayName("商户成员服务")
class MemberServiceTest {
private final MemberRepository repository = mock(MemberRepository.class);
private final IamRoleMapper roleMapper = mock(IamRoleMapper.class);
@@ -39,7 +37,7 @@ class MemberServiceTest {
when(repository.findRoleIds(100L, 20L)).thenReturn(List.of(50L));
}
@Test void memberDetailsReturnContactInformationAndExistingRoles() {
@Test void 成员详情返回联系方式和已有角色() {
user.setPhone("13800138000"); user.setEmail("member@example.com");
MemberResponse result = service.detail(100L, 20L);
assertEquals("测试成员", result.displayName());
@@ -48,12 +46,12 @@ class MemberServiceTest {
verifyNoInteractions(auth);
}
@Test void crossTenantLookupIsRejected() {
@Test void 跨租户查询被拒绝() {
when(repository.findByTenantAndId(200L, 20L)).thenReturn(null);
assertThrows(BizException.class, () -> service.detail(200L, 20L));
}
@Test void tenantOwnerCannotBeDisabled() {
@Test void 企业所有者不可停用() {
member.setMemberType("OWNER");
BizException exception = assertThrows(BizException.class, () -> service.changeStatus(100L, 20L,
new ChangeMemberStatusRequest("DISABLED"), 20L));
@@ -61,7 +59,7 @@ class MemberServiceTest {
verify(auth, never()).status(anyString(), anyLong(), any());
}
@Test void disablingMemberSynchronizesAuthAccountAndPersistsLocalStatus() {
@Test void 停用成员同步认证账号并保存本地状态() {
when(auth.status(anyString(), eq(40L), any())).thenReturn(ApiResponse.success(null));
MemberResponse result = service.changeStatus(100L, 20L,
new ChangeMemberStatusRequest("DISABLED"), 99L);
@@ -70,28 +68,14 @@ class MemberServiceTest {
verify(repository).saveUser(user);
}
@Test void authServiceFailureDoesNotPersistLocalStatus() {
@Test void 认证服务失败时不得写入本地状态() {
when(auth.status(anyString(), eq(40L), any())).thenThrow(new RuntimeException("网络失败"));
BizException exception = assertThrows(BizException.class,
() -> service.changeStatus(100L, 20L,
new ChangeMemberStatusRequest("DISABLED"), 99L));
assertEquals(ResultCode.SERVICE_UNAVAILABLE, exception.getResultCode());
verify(repository, never()).save(member);
}
@Test void authHttp200BusinessFailureKeepsBusinessCodeAndDoesNotPersistLocalStatus() {
when(auth.status(anyString(), eq(40L), any())).thenReturn(
ApiResponse.<Void>failure(ResultCode.NOT_FOUND, "认证账号不存在", null, null));
BizException exception = assertThrows(BizException.class, () -> service.changeStatus(100L, 20L,
assertThrows(RuntimeException.class, () -> service.changeStatus(100L, 20L,
new ChangeMemberStatusRequest("DISABLED"), 99L));
assertEquals(ResultCode.NOT_FOUND, exception.getResultCode());
assertEquals("认证账号不存在", exception.getMessage());
verify(repository, never()).save(member);
}
@Test void tenantOwnerCannotBeDowngradedOrHaveRolesChangedThroughMemberUpdate() {
@Test void 企业所有者不能通过成员编辑接口降级或变更角色() {
member.setMemberType("OWNER");
UpdateMemberRequest request = new UpdateMemberRequest("所有者", null, null,
"EMPLOYEE", null, List.of());
@@ -101,7 +85,7 @@ class MemberServiceTest {
verify(repository, never()).replaceRoles(anyLong(), anyLong(), anyList());
}
@Test void createWithRolesRequiresGrantRolePermission() {
@Test void 新增成员携带非空角色但无授权权限时返回403且不创建账号() {
CreateMemberRequest request = new CreateMemberRequest("member01", "Password1", "成员",
null, null, "EMPLOYEE", null, List.of(50L));
@@ -113,7 +97,7 @@ class MemberServiceTest {
verify(repository, never()).insert(any());
}
@Test void profileOnlyUpdateAllowsUnchangedRolesWithoutGrantPermission() {
@Test void 编辑资料且角色集合未变化时不要求角色授权权限() {
IamRole role = activeMerchantRole(50L, 100L);
when(roleMapper.selectById(50L)).thenReturn(role);
UpdateMemberRequest request = new UpdateMemberRequest("新姓名", "13800138000", null,
@@ -126,7 +110,7 @@ class MemberServiceTest {
verify(repository).replaceRoles(100L, 20L, List.of(50L));
}
@Test void roleChangeRequiresGrantRolePermission() {
@Test void 编辑成员变更角色但无授权权限时返回403() {
UpdateMemberRequest request = new UpdateMemberRequest("测试成员", null, null,
"EMPLOYEE", null, List.of(60L));
@@ -138,7 +122,7 @@ class MemberServiceTest {
verify(repository, never()).replaceRoles(anyLong(), anyLong(), anyList());
}
@Test void clearingRolesAlsoRequiresGrantRolePermission() {
@Test void 空集合清空已有角色同样要求角色授权权限() {
UpdateMemberRequest request = new UpdateMemberRequest("测试成员", null, null,
"EMPLOYEE", null, List.of());
@@ -149,7 +133,7 @@ class MemberServiceTest {
verify(repository, never()).replaceRoles(anyLong(), anyLong(), anyList());
}
@Test void grantRolePermissionAllowsClearingRoles() {
@Test void 具有角色授权权限时允许清空已有角色() {
UpdateMemberRequest request = new UpdateMemberRequest("测试成员", null, null,
"EMPLOYEE", null, List.of());
@@ -159,7 +143,7 @@ class MemberServiceTest {
verify(repository).replaceRoles(100L, 20L, List.of());
}
@Test void crossTenantUpdateReturnsNotFoundBeforeReadingRoles() {
@Test void 跨租户编辑先按目标租户查成员并返回404() {
when(repository.findByTenantAndId(200L, 20L)).thenReturn(null);
UpdateMemberRequest request = new UpdateMemberRequest("测试成员", null, null,
"EMPLOYEE", null, List.of(50L));
@@ -178,7 +162,7 @@ class MemberServiceTest {
return role;
}
@Test void tenantOwnerPasswordCannotBeResetByMemberAdministrator() {
@Test void 企业所有者密码不能被成员管理员重置() {
member.setMemberType("OWNER");
assertThrows(BizException.class, () -> service.resetPassword(100L, 20L,
@@ -1,7 +1,6 @@
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.AuthAccountClient;
import com.baiyue.iam.dto.PlatformInternalUserDtos.*;
@@ -11,7 +10,6 @@ import com.baiyue.iam.entity.IamUser;
import com.baiyue.iam.mapper.IamRoleMapper;
import com.baiyue.iam.repository.MemberRepository;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
@@ -21,7 +19,6 @@ import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
@DisplayName("平台内部用户服务")
class PlatformInternalUserServiceTest {
private final MemberRepository repository = mock(MemberRepository.class);
private final IamRoleMapper roleMapper = mock(IamRoleMapper.class);
@@ -48,7 +45,7 @@ class PlatformInternalUserServiceTest {
}
@Test
void listReturnsMembershipIdContactInformationAndCurrentRoles() {
void 列表返回成员主键联系方式和当前角色() {
when(repository.findByTenant(0L)).thenReturn(List.of(membership));
InternalUserResponse result = service.list().get(0);
@@ -60,30 +57,7 @@ class PlatformInternalUserServiceTest {
}
@Test
void authHttp200BusinessFailureKeepsBusinessCode() {
when(repository.findByTenant(0L)).thenReturn(List.of(membership));
when(auth.summary("internal-token", 10L)).thenReturn(
ApiResponse.<AuthAccountClient.AccountSummary>failure(
ResultCode.NOT_FOUND, "认证账号不存在", null, null));
BizException exception = assertThrows(BizException.class, service::list);
assertEquals(ResultCode.NOT_FOUND, exception.getResultCode());
assertEquals("认证账号不存在", exception.getMessage());
}
@Test
void authDecodeOrTransportFailureMapsToServiceUnavailable() {
when(repository.findByTenant(0L)).thenReturn(List.of(membership));
when(auth.summary("internal-token", 10L)).thenThrow(new RuntimeException("decode failed"));
BizException exception = assertThrows(BizException.class, service::list);
assertEquals(ResultCode.SERVICE_UNAVAILABLE, exception.getResultCode());
}
@Test
void createPlatformUserCreatesPlatformAccountInAuthAndMembershipInIam() {
void 新建平台用户在Auth创建平台账号并写入IAM成员() {
IamRole role = new IamRole();
role.setId(40L); role.setTenantId(0L); role.setAppCode("PLATFORM"); role.setStatus("ACTIVE");
when(roleMapper.selectById(40L)).thenReturn(role);
@@ -107,7 +81,7 @@ class PlatformInternalUserServiceTest {
}
@Test
void localCreationFailureCompensatesAuthAccountWithoutRecordingPassword() {
void 本地创建失败会补偿认证账号且不记录密码() {
when(auth.create(anyString(), any())).thenReturn(ApiResponse.success(
new AuthAccountClient.AccountSummary(99L, "new.operator", "新运营", "ACTIVE")));
when(auth.compensate(anyString(), anyLong())).thenReturn(ApiResponse.success(null));
@@ -121,7 +95,7 @@ class PlatformInternalUserServiceTest {
}
@Test
void cannotDisableCurrentlyAuthenticatedPlatformMember() {
void 不能停用当前登录的平台成员() {
assertThrows(BizException.class, () -> service.changeStatus(30L,
new ChangeInternalUserStatusRequest("DISABLED"), 30L));
@@ -129,7 +103,7 @@ class PlatformInternalUserServiceTest {
}
@Test
void localStatusPersistenceFailureRestoresAuthAndAvoidsFurtherRemoteLookupsAfterSuccess() {
void 状态本地写入失败时恢复Auth且成功后不再远程查询() {
when(auth.status(anyString(), eq(10L), any())).thenReturn(ApiResponse.success(null));
doThrow(new RuntimeException("IAM 写入失败")).when(repository).saveUser(user);
@@ -144,7 +118,7 @@ class PlatformInternalUserServiceTest {
}
@Test
void statusCompensationRestoresOriginalAuthStatusInsteadOfOldIamStatus() {
void 状态补偿应恢复Auth原状态而不是IAM旧状态() {
when(auth.summary("internal-token", 10L)).thenReturn(ApiResponse.success(
new AuthAccountClient.AccountSummary(10L, "operator", "平台运营", "DISABLED")));
when(auth.status(anyString(), eq(10L), any())).thenReturn(ApiResponse.success(null));
@@ -160,7 +134,7 @@ class PlatformInternalUserServiceTest {
}
@Test
void resetPasswordDelegatesOnlyToAuthWithoutWritingIam() {
void 重置密码只转交Auth不写入IAM() {
when(auth.reset(anyString(), eq(10L), any())).thenReturn(ApiResponse.success(null));
service.resetPassword(30L, new ResetInternalUserPasswordRequest("ResetPass1"));
@@ -171,7 +145,7 @@ class PlatformInternalUserServiceTest {
}
@Test
void roleAssignmentAcceptsEnabledPlatformRolesOnlyAndReturnsLatestView() {
void 角色分配只接受平台已启用角色并返回最新回显() {
IamRole role = new IamRole();
role.setId(41L); role.setTenantId(0L); role.setAppCode("PLATFORM"); role.setStatus("ACTIVE");
when(roleMapper.selectById(41L)).thenReturn(role);
@@ -184,7 +158,7 @@ class PlatformInternalUserServiceTest {
}
@Test
void preventsAssigningMerchantRolesToPlatformUser() {
void 禁止给平台用户分配商户角色() {
IamRole merchantRole = new IamRole();
merchantRole.setId(42L); merchantRole.setTenantId(10001L);
merchantRole.setAppCode("MERCHANT"); merchantRole.setStatus("ACTIVE");
@@ -16,7 +16,6 @@ import com.baiyue.iam.entity.IamRole;
import com.baiyue.iam.repository.MemberRepository;
import com.baiyue.iam.repository.RbacRepository;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
@@ -32,7 +31,6 @@ import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
@DisplayName("角色与权限服务")
class RbacServiceTest {
@Mock private RbacRepository repository;
@Mock private MemberRepository memberRepository;
@@ -46,7 +44,7 @@ class RbacServiceTest {
}
@Test
void roleListReturnsCurrentPermissionsForSafeView() {
void 角色列表返回当前权限用于安全回显() {
IamRole role = role(11L, 100L, "MERCHANT", "CUSTOM");
when(repository.findRoles(100L, "MERCHANT")).thenReturn(List.of(role));
when(repository.findRolePermissionIds(100L, 11L)).thenReturn(Set.of(21L, 22L));
@@ -58,7 +56,7 @@ class RbacServiceTest {
}
@Test
void permissionListPrefersCurrentTenantAndRemovesDuplicateGlobalTemplatesWithSameCode() {
void 权限列表同码时优先当前租户并去除全局模板重复项() {
IamPermission global = permission(21L, 0L, "merchant:member:list");
IamPermission tenant = permission(22L, 100L, "merchant:member:list");
IamPermission fallback = permission(23L, 0L, "merchant:member:status");
@@ -73,7 +71,7 @@ class RbacServiceTest {
}
@Test
void crossTenantRoleAccessIsRejected() {
void 跨租户角色访问被拒绝() {
when(repository.findRole(11L, 200L, "MERCHANT")).thenReturn(null);
BizException exception = assertThrows(BizException.class,
@@ -83,7 +81,7 @@ class RbacServiceTest {
}
@Test
void systemRoleCannotBeDeleted() {
void 系统角色禁止删除() {
when(repository.findRole(11L, 0L, "PLATFORM"))
.thenReturn(role(11L, 0L, "PLATFORM", "SYSTEM"));
@@ -95,7 +93,7 @@ class RbacServiceTest {
}
@Test
void customRoleAssignedToMembersCannotBeDeleted() {
void 已分配成员的自定义角色禁止删除() {
when(repository.findRole(11L, 100L, "MERCHANT"))
.thenReturn(role(11L, 100L, "MERCHANT", "CUSTOM"));
when(repository.isRoleAssigned(100L, 11L)).thenReturn(true);
@@ -108,7 +106,7 @@ class RbacServiceTest {
}
@Test
void emptyPermissionSetExplicitlyClearsAuthorization() {
void 空权限集合可以明确清空授权() {
when(repository.findRole(11L, 100L, "MERCHANT"))
.thenReturn(role(11L, 100L, "MERCHANT", "CUSTOM"));
@@ -118,7 +116,7 @@ class RbacServiceTest {
}
@Test
void emptyRoleSetExplicitlyClearsMemberAuthorization() {
void 空角色集合可以明确清空成员授权() {
IamMembership membership = new IamMembership();
membership.setId(31L);
membership.setTenantId(100L);
@@ -130,7 +128,7 @@ class RbacServiceTest {
}
@Test
void memberRoleViewReturnsRolesFromCurrentApplicationDomainOnly() {
void 成员角色回显只返回当前应用域角色() {
IamMembership membership = new IamMembership();
membership.setId(31L);
membership.setTenantId(100L);
@@ -146,7 +144,7 @@ class RbacServiceTest {
}
@Test
void duplicateRoleCodeWithinTenantAndApplicationReturnsConflict() {
void 同一租户同一应用角色编码重复时返回冲突() {
when(repository.existsRoleCode(100L, "MERCHANT", "ORDER_ADMIN", null)).thenReturn(true);
BizException exception = assertThrows(BizException.class, () -> service.createRole(
@@ -157,7 +155,7 @@ class RbacServiceTest {
}
@Test
void disabledRoleCannotBeAssignedToMember() {
void 停用角色不得继续分配给成员() {
IamMembership membership = new IamMembership();
membership.setId(31L);
membership.setTenantId(100L);
@@ -174,7 +172,7 @@ class RbacServiceTest {
}
@Test
void customRoleCanBeUpdatedAndDisabledWhileRetainingPermissionView() {
void 自定义角色可以编辑和停用并保持权限回显() {
IamRole role = role(11L, 100L, "MERCHANT", "CUSTOM");
when(repository.findRole(11L, 100L, "MERCHANT")).thenReturn(role);
when(repository.findRolePermissionIds(100L, 11L)).thenReturn(Set.of(21L));
@@ -191,7 +189,7 @@ class RbacServiceTest {
}
@Test
void unreferencedCustomRoleCanBeDeletedSafely() {
void 未使用的自定义角色可以安全删除() {
when(repository.findRole(11L, 100L, "MERCHANT"))
.thenReturn(role(11L, 100L, "MERCHANT", "CUSTOM"));
when(repository.isRoleAssigned(100L, 11L)).thenReturn(false);
@@ -202,7 +200,7 @@ class RbacServiceTest {
}
@Test
void tenantOwnerRolesCannotBeChangedThroughRegularMemberAuthorizationEndpoint() {
void 企业所有者角色不能通过普通成员授权接口修改() {
IamMembership owner = new IamMembership();
owner.setId(31L);
owner.setTenantId(100L);
@@ -217,7 +215,7 @@ class RbacServiceTest {
}
@Test
void systemRolePermissionsCannotBeChangedThroughRegularAuthorizationEndpoint() {
void 系统角色权限不能通过普通授权接口修改() {
when(repository.findRole(11L, 0L, "PLATFORM"))
.thenReturn(role(11L, 0L, "PLATFORM", "SYSTEM"));
@@ -229,7 +227,7 @@ class RbacServiceTest {
}
@Test
void tenantOwnerSystemRoleCannotBeAssignedToRegularMember() {
void 企业所有者系统角色不能分配给普通成员() {
IamMembership member = new IamMembership();
member.setId(31L);
member.setTenantId(100L);
@@ -247,7 +245,7 @@ class RbacServiceTest {
}
@Test
void duplicatePermissionCodeReturnsStableConflictWithoutDatabaseWrite() {
void 权限编码重复时返回稳定冲突且不写数据库() {
when(repository.existsPermissionCode(100L, "MERCHANT", "merchant:role:test"))
.thenReturn(true);
CreatePermissionRequest request = new CreatePermissionRequest(
@@ -261,7 +259,7 @@ class RbacServiceTest {
}
@Test
void builtInSystemPermissionCannotBeUpdatedDisabledOrDeleted() {
void 系统预置权限不允许编辑启停或删除() {
IamPermission builtIn = permission(21L, 0L, "platform:permission:list");
builtIn.setAppCode("PLATFORM");
builtIn.setBuiltIn(true);
@@ -276,7 +274,7 @@ class RbacServiceTest {
}
@Test
void customPermissionCanBeUpdatedAndToggledWithoutChangingCode() {
void 自定义权限能编辑和启停但保持编码不变() {
IamPermission custom = permission(21L, 100L, "merchant:custom:edit");
when(repository.findOwnedPermission(21L, 100L, "MERCHANT")).thenReturn(custom);
@@ -292,7 +290,7 @@ class RbacServiceTest {
}
@Test
void permissionResourceWithChildrenOrRoleBindingsRejectsDeletion() {
void 带子节点或角色绑定的权限资源拒绝删除() {
IamPermission custom = permission(21L, 100L, "merchant:custom:edit");
when(repository.findOwnedPermission(21L, 100L, "MERCHANT")).thenReturn(custom);
when(repository.hasPermissionChildren(100L, "MERCHANT", 21L)).thenReturn(true);
@@ -310,7 +308,7 @@ class RbacServiceTest {
}
@Test
void unreferencedCustomPermissionCanBeDeleted() {
void 无引用的自定义权限可以删除() {
IamPermission custom = permission(21L, 100L, "merchant:custom:edit");
when(repository.findOwnedPermission(21L, 100L, "MERCHANT")).thenReturn(custom);
when(repository.hasPermissionChildren(100L, "MERCHANT", 21L)).thenReturn(false);
@@ -2,27 +2,26 @@ package com.baiyue.iam.service;
import com.baiyue.common.core.api.PageResult;
import com.baiyue.iam.entity.IamTenant;
import com.baiyue.iam.entity.IamTenantReview;
import com.baiyue.iam.dto.TenantDtos.ChangeTenantStatusRequest;
import com.baiyue.iam.dto.TenantDtos.UpdateTenantRequest;
import com.baiyue.iam.repository.TenantRepository;
import com.baiyue.iam.repository.ProfileResourceRepository;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.test.util.ReflectionTestUtils;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.when;
import static org.junit.jupiter.api.Assertions.assertThrows;
import com.baiyue.common.core.exception.BizException;
import com.baiyue.common.core.api.ResultCode;
@DisplayName("企业租户服务")
class TenantServiceTest {
@Test
@@ -41,6 +40,11 @@ class TenantServiceTest {
tenant.setContactName("Alice");
tenant.setContactPhone("13800138000");
tenant.setContactEmail("alice@example.com");
tenant.setSalesOwnerName("Sales Zhang");
tenant.setPaymentTermDays(30);
tenant.setEnterpriseType("LIMITED_COMPANY");
tenant.setCreatedAt(LocalDateTime.of(2026, 7, 1, 9, 30));
tenant.setApprovedAt(LocalDateTime.of(2026, 7, 2, 10, 0));
when(repository.pageMerchants(1, 10, "Example", "ACTIVE"))
.thenReturn(new PageResult<>(List.of(tenant), 1, 10, 1, 1));
@@ -49,6 +53,12 @@ class TenantServiceTest {
assertEquals(1, result.total());
assertEquals("TEN10001", result.records().get(0).tenantCode());
assertEquals("Alice", result.records().get(0).contactName());
assertEquals("Sales Zhang", result.records().get(0).salesOwnerName());
assertEquals(30, result.records().get(0).paymentTermDays());
assertEquals(LocalDateTime.of(2026, 7, 1, 9, 30), result.records().get(0).createdAt());
assertEquals(LocalDateTime.of(2026, 7, 2, 10, 0), result.records().get(0).approvedAt());
assertEquals("138****8000", result.records().get(0).contactPhone());
assertEquals("a***@example.com", result.records().get(0).contactEmail());
verify(repository).pageMerchants(1, 10, "Example", "ACTIVE");
}
@@ -61,10 +71,21 @@ class TenantServiceTest {
when(repository.findTenant(10001L)).thenReturn(tenant);
var updated = service.updateMerchant(10001L,
new UpdateTenantRequest("New name", "New legal", "Alice", "13900139000", "alice@example.com"));
new UpdateTenantRequest("New name", "New legal", "Alice", "13900139000", "alice@example.com",
"alice_wechat", "Sales Zhang", 45, "LIMITED_COMPANY", LocalDate.of(2020, 1, 2),
new BigDecimal("5000000.00"), "CNY", "深圳市南山区示例路 1 号", "一期重点客户"));
var disabled = service.changeMerchantStatus(10001L, new ChangeTenantStatusRequest("DISABLED"));
assertEquals("New name", updated.tenantName());
assertEquals("alice_wechat", updated.contactWechat());
assertEquals("Sales Zhang", updated.salesOwnerName());
assertEquals(45, updated.paymentTermDays());
assertEquals("LIMITED_COMPANY", updated.enterpriseType());
assertEquals(LocalDate.of(2020, 1, 2), updated.establishedDate());
assertEquals(new BigDecimal("5000000.00"), updated.registeredCapital());
assertEquals("CNY", updated.registeredCapitalCurrency());
assertEquals("深圳市南山区示例路 1 号", updated.registeredAddress());
assertEquals("一期重点客户", updated.internalRemark());
assertEquals("DISABLED", disabled.status());
verify(repository, org.mockito.Mockito.times(2)).saveTenant(tenant);
}
@@ -83,7 +104,7 @@ class TenantServiceTest {
}
@Test
void activeTenantStillRequiresApprovedQualificationToPlaceOrder() {
void 企业激活不等于具备下单资格仍需已审核资质() {
TenantRepository repository = mock(TenantRepository.class);
ProfileResourceRepository profileRepository = mock(ProfileResourceRepository.class);
TenantService service = new TenantService();
@@ -100,59 +121,6 @@ class TenantServiceTest {
assertEquals(true, withQualification.eligible());
}
@Test
void reviewCompletesPendingTicketWithConditionalUpdate() {
TenantRepository repository = mock(TenantRepository.class);
ProfileResourceRepository profileRepository = mock(ProfileResourceRepository.class);
TenantService service = new TenantService();
ReflectionTestUtils.setField(service, "repository", repository);
ReflectionTestUtils.setField(service, "profileRepository", profileRepository);
IamTenantReview review = pendingReview(90001L, 10001L);
IamTenant tenant = merchant(10001L, "PENDING");
when(repository.findReview(90001L)).thenReturn(review);
when(repository.completeReview(org.mockito.ArgumentMatchers.eq(90001L),
org.mockito.ArgumentMatchers.eq("APPROVED"), org.mockito.ArgumentMatchers.eq(70001L),
org.mockito.ArgumentMatchers.eq("ok"), org.mockito.ArgumentMatchers.any()))
.thenReturn(true);
when(repository.findTenant(10001L)).thenReturn(tenant);
when(profileRepository.findQualifications(10001L)).thenReturn(List.of());
service.review(90001L, 70001L, true, "ok");
assertEquals("ACTIVE", tenant.getStatus());
assertEquals(70001L, tenant.getApprovedBy());
verify(repository).saveTenant(tenant);
}
@Test
void duplicateOrConcurrentReviewIsRejectedBeforeTenantMutation() {
TenantRepository repository = mock(TenantRepository.class);
ProfileResourceRepository profileRepository = mock(ProfileResourceRepository.class);
TenantService service = new TenantService();
ReflectionTestUtils.setField(service, "repository", repository);
ReflectionTestUtils.setField(service, "profileRepository", profileRepository);
when(repository.findReview(90001L)).thenReturn(pendingReview(90001L, 10001L));
when(repository.completeReview(org.mockito.ArgumentMatchers.eq(90001L),
org.mockito.ArgumentMatchers.eq("REJECTED"), org.mockito.ArgumentMatchers.eq(70001L),
org.mockito.ArgumentMatchers.eq("no"), org.mockito.ArgumentMatchers.any()))
.thenReturn(false);
BizException exception = assertThrows(BizException.class,
() -> service.review(90001L, 70001L, false, "no"));
assertEquals(ResultCode.CONFLICT, exception.getResultCode());
verify(repository, never()).findTenant(org.mockito.ArgumentMatchers.anyLong());
verify(repository, never()).saveTenant(org.mockito.ArgumentMatchers.any());
}
private IamTenantReview pendingReview(long id, long tenantId) {
IamTenantReview review = new IamTenantReview();
review.setId(id);
review.setTenantId(tenantId);
review.setStatus("PENDING");
return review;
}
private IamTenant merchant(long id, String status) {
IamTenant tenant = new IamTenant();
tenant.setId(id);