暂存提交

This commit is contained in:
2026-07-31 16:53:07 +08:00
parent 45358ef40f
commit 704cb15ccf
98 changed files with 1944 additions and 134 deletions
@@ -1,6 +1,7 @@
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;
@@ -10,9 +11,10 @@ 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 当前权限接口不应依赖角色管理权限() throws Exception {
void currentPermissionsEndpointDoesNotRequireRoleManagementPermission() throws Exception {
Method method = AuthorizationController.class.getMethod("permissions", String.class,
AuthenticatedUser.class);
@@ -1,5 +1,6 @@
package com.baiyue.iam.controller;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.yaml.snakeyaml.Yaml;
@@ -9,9 +10,10 @@ import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
@DisplayName("IAM OpenAPI 契约")
class IamOpenApiContractTest {
@Test
void OpenApi应声明权限分页和内部用户的真实响应结构() throws IOException {
void openApiDeclaresPermissionPaginationAndInternalUserResponseSchema() throws IOException {
String document;
try (var stream = getClass().getResourceAsStream("/openapi/iam-v1.yaml")) {
assertThat(stream).isNotNull();
@@ -23,6 +25,8 @@ class IamOpenApiContractTest {
.contains("PermissionCodesApiResponse:")
.contains("TenantPageApiResponse:")
.contains("InternalUserListApiResponse:")
.contains("/api/platform/v1/iam/internal-users/{membershipId}/roles:")
.contains("AssignInternalUserRolesRequest:")
.contains("membershipId: {type: integer, format: int64}")
.contains("roleIds: {type: array, items: {type: integer, format: int64}}")
.contains("'422': {$ref: '#/components/responses/UnprocessableEntity'}");
@@ -1,5 +1,6 @@
package com.baiyue.iam.controller;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.web.util.pattern.PathPatternParser;
import org.springframework.http.server.PathContainer;
@@ -7,11 +8,12 @@ import org.springframework.http.server.PathContainer;
import static org.assertj.core.api.Assertions.assertThat;
/** 防止成员数字主键路由再次拦截固定的角色分配路径。 */
@DisplayName("商户成员路由")
class MerchantMemberRouteTest {
private final PathPatternParser parser = new PathPatternParser();
@Test
void 成员编辑路由只接受数字主键() {
void memberUpdateRouteAcceptsNumericPrimaryKeyOnly() {
var pattern = parser.parse("/api/merchant/v1/iam/members/{membershipId:\\d+}");
assertThat(pattern.matches(PathContainer.parsePath("/api/merchant/v1/iam/members/123"))).isTrue();
@@ -1,5 +1,6 @@
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;
@@ -9,10 +10,11 @@ import java.lang.reflect.Method;
import static org.junit.jupiter.api.Assertions.*;
@DisplayName("平台内部用户接口契约")
class PlatformInternalUserContractTest {
@Test
void 内部用户列表必须校验列表权限() throws Exception {
void internalUserListRequiresListPermission() throws Exception {
Method method = ProfileResourceController.class.getMethod("internalUsers",
com.baiyue.common.security.AuthenticatedUser.class);
@@ -23,16 +25,18 @@ class PlatformInternalUserContractTest {
}
@Test
void 平台用户写接口以成员关系主键寻址() {
void platformUserWriteEndpointsAddressMembershipId() {
assertMapping("updateInternalUser", "/api/platform/v1/iam/internal-users/{membershipId:\\d+}");
assertMapping("changeInternalUserStatus",
"/api/platform/v1/iam/internal-users/{membershipId:\\d+}/status");
assertMapping("resetInternalUserPassword",
"/api/platform/v1/iam/internal-users/{membershipId:\\d+}/password");
assertMapping("assignInternalUserRoles",
"/api/platform/v1/iam/internal-users/{membershipId:\\d+}/roles");
}
@Test
void 企业资料查看必须校验查看权限() throws Exception {
void tenantProfileReadRequiresViewPermission() throws Exception {
Method method = MerchantTenantController.class.getMethod("profile",
com.baiyue.common.security.AuthenticatedUser.class);
assertEquals("hasAuthority('merchant:profile:view')",
@@ -5,6 +5,7 @@ import io.swagger.v3.oas.annotations.responses.ApiResponses;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.Pattern;
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;
@@ -18,9 +19,10 @@ 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 客户分页接口声明权限分页约束和错误状态() throws Exception {
void merchantPageEndpointDeclaresPermissionPaginationConstraintsAndErrorStatuses() throws Exception {
Method method = PlatformTenantController.class.getMethod("page", long.class, long.class,
String.class, String.class, AuthenticatedUser.class);
@@ -39,6 +41,14 @@ class PlatformTenantControllerContractTest {
assertEquals(Set.of("200", "400", "401", "403", "422"), statuses);
}
@Test
void pendingMerchantListRequiresMerchantApprovalPermission() throws Exception {
Method method = PlatformTenantController.class.getMethod("reviews", AuthenticatedUser.class);
assertEquals("hasAuthority('platform:customer:review')",
method.getAnnotation(PreAuthorize.class).value());
}
private <T extends Annotation> T annotation(Method method, int parameter, Class<T> type) {
return method.getParameters()[parameter].getAnnotation(type);
}
@@ -1,5 +1,6 @@
package com.baiyue.iam.controller;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.web.bind.annotation.PostMapping;
@@ -10,6 +11,7 @@ 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
@@ -5,6 +5,7 @@ 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;
@@ -16,6 +17,7 @@ import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@DisplayName("权限快照服务")
class AuthorizationSnapshotServiceTest {
@Test
void loadsRolesAndPermissionsForActiveMerchantIdentity() {
@@ -82,7 +84,7 @@ class AuthorizationSnapshotServiceTest {
}
@Test
void 空角色和权限查询结果不应触发五百错误() {
void emptyRoleAndPermissionQueriesDoNotTriggerServerError() {
AuthorizationQueryMapper mapper = mock(AuthorizationQueryMapper.class);
when(mapper.countActiveIdentity(42L, 0L, 9L)).thenReturn(1);
when(mapper.findRoles(42L, 0L, 9L, "PLATFORM")).thenReturn(null);
@@ -10,6 +10,7 @@ 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;
@@ -18,6 +19,7 @@ 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);
@@ -35,7 +37,7 @@ class MemberServiceTest {
when(repository.findRoleIds(100L, 20L)).thenReturn(List.of(50L));
}
@Test void 成员详情返回联系方式和已有角色() {
@Test void memberDetailsReturnContactInformationAndExistingRoles() {
user.setPhone("13800138000"); user.setEmail("member@example.com");
MemberResponse result = service.detail(100L, 20L);
assertEquals("测试成员", result.displayName());
@@ -44,12 +46,12 @@ class MemberServiceTest {
verifyNoInteractions(auth);
}
@Test void 跨租户查询被拒绝() {
@Test void crossTenantLookupIsRejected() {
when(repository.findByTenantAndId(200L, 20L)).thenReturn(null);
assertThrows(BizException.class, () -> service.detail(200L, 20L));
}
@Test void 企业所有者不可停用() {
@Test void tenantOwnerCannotBeDisabled() {
member.setMemberType("OWNER");
BizException exception = assertThrows(BizException.class, () -> service.changeStatus(100L, 20L,
new ChangeMemberStatusRequest("DISABLED"), 20L));
@@ -57,7 +59,7 @@ class MemberServiceTest {
verify(auth, never()).status(anyString(), anyLong(), any());
}
@Test void 停用成员同步认证账号并保存本地状态() {
@Test void disablingMemberSynchronizesAuthAccountAndPersistsLocalStatus() {
when(auth.status(anyString(), eq(40L), any())).thenReturn(ApiResponse.success(null));
MemberResponse result = service.changeStatus(100L, 20L,
new ChangeMemberStatusRequest("DISABLED"), 99L);
@@ -66,14 +68,14 @@ class MemberServiceTest {
verify(repository).saveUser(user);
}
@Test void 认证服务失败时不得写入本地状态() {
@Test void authServiceFailureDoesNotPersistLocalStatus() {
when(auth.status(anyString(), eq(40L), any())).thenThrow(new RuntimeException("网络失败"));
assertThrows(RuntimeException.class, () -> service.changeStatus(100L, 20L,
new ChangeMemberStatusRequest("DISABLED"), 99L));
verify(repository, never()).save(member);
}
@Test void 企业所有者不能通过成员编辑接口降级或变更角色() {
@Test void tenantOwnerCannotBeDowngradedOrHaveRolesChangedThroughMemberUpdate() {
member.setMemberType("OWNER");
UpdateMemberRequest request = new UpdateMemberRequest("所有者", null, null,
"EMPLOYEE", null, List.of());
@@ -83,7 +85,7 @@ class MemberServiceTest {
verify(repository, never()).replaceRoles(anyLong(), anyLong(), anyList());
}
@Test void 企业所有者密码不能被成员管理员重置() {
@Test void tenantOwnerPasswordCannotBeResetByMemberAdministrator() {
member.setMemberType("OWNER");
assertThrows(BizException.class, () -> service.resetPassword(100L, 20L,
@@ -5,16 +5,18 @@ import com.baiyue.iam.dto.ProfileDtos.UpdatePersonalProfileRequest;
import com.baiyue.iam.entity.IamMembership;
import com.baiyue.iam.entity.IamUser;
import com.baiyue.iam.repository.MemberRepository;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
@DisplayName("个人资料服务")
class PersonalProfileServiceTest {
private final MemberRepository repository = mock(MemberRepository.class);
private final PersonalProfileService service = new PersonalProfileService(repository);
@Test void 只能修改当前租户中的本人资料() {
@Test void canOnlyModifyOwnProfileWithinCurrentTenant() {
IamMembership membership = new IamMembership(); membership.setUserId(3L); membership.setStatus("ACTIVE");
IamUser user = new IamUser(); user.setId(3L); user.setAuthUserId(2L);
when(repository.findByTenantAndAuthUser(1L, 2L)).thenReturn(membership);
@@ -25,7 +27,7 @@ class PersonalProfileServiceTest {
verify(repository).saveUser(user);
}
@Test void 跨租户身份被拒绝() {
@Test void crossTenantIdentityIsRejected() {
assertThrows(BizException.class, () -> service.get(9L, 2L, "tester"));
}
}
@@ -10,6 +10,7 @@ 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;
@@ -19,6 +20,7 @@ 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);
@@ -45,7 +47,7 @@ class PlatformInternalUserServiceTest {
}
@Test
void 列表返回成员主键联系方式和当前角色() {
void listReturnsMembershipIdContactInformationAndCurrentRoles() {
when(repository.findByTenant(0L)).thenReturn(List.of(membership));
InternalUserResponse result = service.list().get(0);
@@ -57,7 +59,7 @@ class PlatformInternalUserServiceTest {
}
@Test
void 新建平台用户在Auth创建平台账号并写入IAM成员() {
void createPlatformUserCreatesPlatformAccountInAuthAndMembershipInIam() {
IamRole role = new IamRole();
role.setId(40L); role.setTenantId(0L); role.setAppCode("PLATFORM"); role.setStatus("ACTIVE");
when(roleMapper.selectById(40L)).thenReturn(role);
@@ -81,7 +83,7 @@ class PlatformInternalUserServiceTest {
}
@Test
void 本地创建失败会补偿认证账号且不记录密码() {
void localCreationFailureCompensatesAuthAccountWithoutRecordingPassword() {
when(auth.create(anyString(), any())).thenReturn(ApiResponse.success(
new AuthAccountClient.AccountSummary(99L, "new.operator", "新运营", "ACTIVE")));
when(auth.compensate(anyString(), anyLong())).thenReturn(ApiResponse.success(null));
@@ -95,7 +97,7 @@ class PlatformInternalUserServiceTest {
}
@Test
void 不能停用当前登录的平台成员() {
void cannotDisableCurrentlyAuthenticatedPlatformMember() {
assertThrows(BizException.class, () -> service.changeStatus(30L,
new ChangeInternalUserStatusRequest("DISABLED"), 30L));
@@ -103,7 +105,7 @@ class PlatformInternalUserServiceTest {
}
@Test
void 状态本地写入失败时恢复Auth且成功后不再远程查询() {
void localStatusPersistenceFailureRestoresAuthAndAvoidsFurtherRemoteLookupsAfterSuccess() {
when(auth.status(anyString(), eq(10L), any())).thenReturn(ApiResponse.success(null));
doThrow(new RuntimeException("IAM 写入失败")).when(repository).saveUser(user);
@@ -118,7 +120,7 @@ class PlatformInternalUserServiceTest {
}
@Test
void 状态补偿应恢复Auth原状态而不是IAM旧状态() {
void statusCompensationRestoresOriginalAuthStatusInsteadOfOldIamStatus() {
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));
@@ -134,7 +136,7 @@ class PlatformInternalUserServiceTest {
}
@Test
void 重置密码只转交Auth不写入IAM() {
void resetPasswordDelegatesOnlyToAuthWithoutWritingIam() {
when(auth.reset(anyString(), eq(10L), any())).thenReturn(ApiResponse.success(null));
service.resetPassword(30L, new ResetInternalUserPasswordRequest("ResetPass1"));
@@ -143,4 +145,30 @@ class PlatformInternalUserServiceTest {
argThat(value -> "ResetPass1".equals(value.newPassword())));
verify(repository, never()).saveUser(any());
}
@Test
void roleAssignmentAcceptsEnabledPlatformRolesOnlyAndReturnsLatestView() {
IamRole role = new IamRole();
role.setId(41L); role.setTenantId(0L); role.setAppCode("PLATFORM"); role.setStatus("ACTIVE");
when(roleMapper.selectById(41L)).thenReturn(role);
InternalUserResponse result = service.assignRoles(30L,
new AssignInternalUserRolesRequest(List.of(41L, 41L)));
assertEquals(List.of(41L), result.roleIds());
verify(repository).replaceRoles(0L, 30L, List.of(41L));
}
@Test
void preventsAssigningMerchantRolesToPlatformUser() {
IamRole merchantRole = new IamRole();
merchantRole.setId(42L); merchantRole.setTenantId(10001L);
merchantRole.setAppCode("MERCHANT"); merchantRole.setStatus("ACTIVE");
when(roleMapper.selectById(42L)).thenReturn(merchantRole);
assertThrows(BizException.class, () -> service.assignRoles(30L,
new AssignInternalUserRolesRequest(List.of(42L))));
verify(repository, never()).replaceRoles(anyLong(), anyLong(), anyList());
}
}
@@ -8,12 +8,15 @@ import com.baiyue.iam.dto.RbacDtos.CreateRoleRequest;
import com.baiyue.iam.dto.RbacDtos.UpdateRoleRequest;
import com.baiyue.iam.dto.RbacDtos.ChangeRoleStatusRequest;
import com.baiyue.iam.dto.RbacDtos.CreatePermissionRequest;
import com.baiyue.iam.dto.RbacDtos.UpdatePermissionRequest;
import com.baiyue.iam.dto.RbacDtos.ChangePermissionStatusRequest;
import com.baiyue.iam.entity.IamMembership;
import com.baiyue.iam.entity.IamPermission;
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;
@@ -29,6 +32,7 @@ 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;
@@ -42,7 +46,7 @@ class RbacServiceTest {
}
@Test
void 角色列表返回当前权限用于安全回显() {
void roleListReturnsCurrentPermissionsForSafeView() {
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));
@@ -54,7 +58,7 @@ class RbacServiceTest {
}
@Test
void 权限列表同码时优先当前租户并去除全局模板重复项() {
void permissionListPrefersCurrentTenantAndRemovesDuplicateGlobalTemplatesWithSameCode() {
IamPermission global = permission(21L, 0L, "merchant:member:list");
IamPermission tenant = permission(22L, 100L, "merchant:member:list");
IamPermission fallback = permission(23L, 0L, "merchant:member:status");
@@ -69,7 +73,7 @@ class RbacServiceTest {
}
@Test
void 跨租户角色访问被拒绝() {
void crossTenantRoleAccessIsRejected() {
when(repository.findRole(11L, 200L, "MERCHANT")).thenReturn(null);
BizException exception = assertThrows(BizException.class,
@@ -79,7 +83,7 @@ class RbacServiceTest {
}
@Test
void 系统角色禁止删除() {
void systemRoleCannotBeDeleted() {
when(repository.findRole(11L, 0L, "PLATFORM"))
.thenReturn(role(11L, 0L, "PLATFORM", "SYSTEM"));
@@ -91,7 +95,7 @@ class RbacServiceTest {
}
@Test
void 已分配成员的自定义角色禁止删除() {
void customRoleAssignedToMembersCannotBeDeleted() {
when(repository.findRole(11L, 100L, "MERCHANT"))
.thenReturn(role(11L, 100L, "MERCHANT", "CUSTOM"));
when(repository.isRoleAssigned(100L, 11L)).thenReturn(true);
@@ -104,7 +108,7 @@ class RbacServiceTest {
}
@Test
void 空权限集合可以明确清空授权() {
void emptyPermissionSetExplicitlyClearsAuthorization() {
when(repository.findRole(11L, 100L, "MERCHANT"))
.thenReturn(role(11L, 100L, "MERCHANT", "CUSTOM"));
@@ -114,7 +118,7 @@ class RbacServiceTest {
}
@Test
void 空角色集合可以明确清空成员授权() {
void emptyRoleSetExplicitlyClearsMemberAuthorization() {
IamMembership membership = new IamMembership();
membership.setId(31L);
membership.setTenantId(100L);
@@ -126,7 +130,7 @@ class RbacServiceTest {
}
@Test
void 成员角色回显只返回当前应用域角色() {
void memberRoleViewReturnsRolesFromCurrentApplicationDomainOnly() {
IamMembership membership = new IamMembership();
membership.setId(31L);
membership.setTenantId(100L);
@@ -142,7 +146,7 @@ class RbacServiceTest {
}
@Test
void 同一租户同一应用角色编码重复时返回冲突() {
void duplicateRoleCodeWithinTenantAndApplicationReturnsConflict() {
when(repository.existsRoleCode(100L, "MERCHANT", "ORDER_ADMIN", null)).thenReturn(true);
BizException exception = assertThrows(BizException.class, () -> service.createRole(
@@ -153,7 +157,7 @@ class RbacServiceTest {
}
@Test
void 停用角色不得继续分配给成员() {
void disabledRoleCannotBeAssignedToMember() {
IamMembership membership = new IamMembership();
membership.setId(31L);
membership.setTenantId(100L);
@@ -170,7 +174,7 @@ class RbacServiceTest {
}
@Test
void 自定义角色可以编辑和停用并保持权限回显() {
void customRoleCanBeUpdatedAndDisabledWhileRetainingPermissionView() {
IamRole role = role(11L, 100L, "MERCHANT", "CUSTOM");
when(repository.findRole(11L, 100L, "MERCHANT")).thenReturn(role);
when(repository.findRolePermissionIds(100L, 11L)).thenReturn(Set.of(21L));
@@ -187,7 +191,7 @@ class RbacServiceTest {
}
@Test
void 未使用的自定义角色可以安全删除() {
void unreferencedCustomRoleCanBeDeletedSafely() {
when(repository.findRole(11L, 100L, "MERCHANT"))
.thenReturn(role(11L, 100L, "MERCHANT", "CUSTOM"));
when(repository.isRoleAssigned(100L, 11L)).thenReturn(false);
@@ -198,7 +202,7 @@ class RbacServiceTest {
}
@Test
void 企业所有者角色不能通过普通成员授权接口修改() {
void tenantOwnerRolesCannotBeChangedThroughRegularMemberAuthorizationEndpoint() {
IamMembership owner = new IamMembership();
owner.setId(31L);
owner.setTenantId(100L);
@@ -213,7 +217,7 @@ class RbacServiceTest {
}
@Test
void 系统角色权限不能通过普通授权接口修改() {
void systemRolePermissionsCannotBeChangedThroughRegularAuthorizationEndpoint() {
when(repository.findRole(11L, 0L, "PLATFORM"))
.thenReturn(role(11L, 0L, "PLATFORM", "SYSTEM"));
@@ -225,7 +229,7 @@ class RbacServiceTest {
}
@Test
void 企业所有者系统角色不能分配给普通成员() {
void tenantOwnerSystemRoleCannotBeAssignedToRegularMember() {
IamMembership member = new IamMembership();
member.setId(31L);
member.setTenantId(100L);
@@ -243,7 +247,7 @@ class RbacServiceTest {
}
@Test
void 权限编码重复时返回稳定冲突且不写数据库() {
void duplicatePermissionCodeReturnsStableConflictWithoutDatabaseWrite() {
when(repository.existsPermissionCode(100L, "MERCHANT", "merchant:role:test"))
.thenReturn(true);
CreatePermissionRequest request = new CreatePermissionRequest(
@@ -256,6 +260,67 @@ class RbacServiceTest {
verify(repository, never()).insertPermission(any());
}
@Test
void builtInSystemPermissionCannotBeUpdatedDisabledOrDeleted() {
IamPermission builtIn = permission(21L, 0L, "platform:permission:list");
builtIn.setAppCode("PLATFORM");
builtIn.setBuiltIn(true);
when(repository.findOwnedPermission(21L, 0L, "PLATFORM")).thenReturn(builtIn);
BizException exception = assertThrows(BizException.class, () -> service.changePermissionStatus(
0L, "PLATFORM", 21L, new ChangePermissionStatusRequest("DISABLED")));
assertEquals(ResultCode.CONFLICT, exception.getResultCode());
verify(repository, never()).updatePermission(any());
verify(repository, never()).deletePermission(any());
}
@Test
void customPermissionCanBeUpdatedAndToggledWithoutChangingCode() {
IamPermission custom = permission(21L, 100L, "merchant:custom:edit");
when(repository.findOwnedPermission(21L, 100L, "MERCHANT")).thenReturn(custom);
var updated = service.updatePermission(100L, "MERCHANT", 21L,
new UpdatePermissionRequest("新名称", 0L, "/custom", "PUT", 10));
var disabled = service.changePermissionStatus(100L, "MERCHANT", 21L,
new ChangePermissionStatusRequest("DISABLED"));
assertEquals("merchant:custom:edit", updated.getPermissionCode());
assertEquals("新名称", updated.getPermissionName());
assertEquals("DISABLED", disabled.getStatus());
verify(repository, times(2)).updatePermission(custom);
}
@Test
void permissionResourceWithChildrenOrRoleBindingsRejectsDeletion() {
IamPermission custom = permission(21L, 100L, "merchant:custom:edit");
when(repository.findOwnedPermission(21L, 100L, "MERCHANT")).thenReturn(custom);
when(repository.hasPermissionChildren(100L, "MERCHANT", 21L)).thenReturn(true);
BizException childFailure = assertThrows(BizException.class,
() -> service.deletePermission(100L, "MERCHANT", 21L));
assertEquals(ResultCode.CONFLICT, childFailure.getResultCode());
when(repository.hasPermissionChildren(100L, "MERCHANT", 21L)).thenReturn(false);
when(repository.isPermissionGranted(100L, 21L)).thenReturn(true);
BizException bindingFailure = assertThrows(BizException.class,
() -> service.deletePermission(100L, "MERCHANT", 21L));
assertEquals(ResultCode.CONFLICT, bindingFailure.getResultCode());
verify(repository, never()).deletePermission(any());
}
@Test
void unreferencedCustomPermissionCanBeDeleted() {
IamPermission custom = permission(21L, 100L, "merchant:custom:edit");
when(repository.findOwnedPermission(21L, 100L, "MERCHANT")).thenReturn(custom);
when(repository.hasPermissionChildren(100L, "MERCHANT", 21L)).thenReturn(false);
when(repository.isPermissionGranted(100L, 21L)).thenReturn(false);
service.deletePermission(100L, "MERCHANT", 21L);
verify(repository).deletePermission(custom);
}
private IamRole role(Long id, Long tenantId, String appCode, String roleType) {
IamRole role = new IamRole();
role.setId(id);
@@ -2,7 +2,11 @@ package com.baiyue.iam.service;
import com.baiyue.common.core.api.PageResult;
import com.baiyue.iam.entity.IamTenant;
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;
@@ -12,7 +16,11 @@ 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.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,4 +49,66 @@ class TenantServiceTest {
assertEquals("Alice", result.records().get(0).contactName());
verify(repository).pageMerchants(1, 10, "Example", "ACTIVE");
}
@Test
void platformCanMaintainAndDisableOnlyMerchantTenant() {
TenantRepository repository = mock(TenantRepository.class);
TenantService service = new TenantService();
ReflectionTestUtils.setField(service, "repository", repository);
IamTenant tenant = merchant(10001L, "ACTIVE");
when(repository.findTenant(10001L)).thenReturn(tenant);
var updated = service.updateMerchant(10001L,
new UpdateTenantRequest("New name", "New legal", "Alice", "13900139000", "alice@example.com"));
var disabled = service.changeMerchantStatus(10001L, new ChangeTenantStatusRequest("DISABLED"));
assertEquals("New name", updated.tenantName());
assertEquals("DISABLED", disabled.status());
verify(repository, org.mockito.Mockito.times(2)).saveTenant(tenant);
}
@Test
void platformCannotBypassReviewToActivatePendingTenant() {
TenantRepository repository = mock(TenantRepository.class);
TenantService service = new TenantService();
ReflectionTestUtils.setField(service, "repository", repository);
when(repository.findTenant(10001L)).thenReturn(merchant(10001L, "PENDING"));
BizException exception = assertThrows(BizException.class,
() -> service.changeMerchantStatus(10001L, new ChangeTenantStatusRequest("ACTIVE")));
assertEquals(ResultCode.CONFLICT, exception.getResultCode());
}
@Test
void activeTenantStillRequiresApprovedQualificationToPlaceOrder() {
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.findTenant(10001L)).thenReturn(merchant(10001L, "ACTIVE"));
when(profileRepository.countApprovedQualifications(10001L)).thenReturn(0L, 1L);
var withoutQualification = service.orderEligibility(10001L);
var withQualification = service.orderEligibility(10001L);
assertEquals(false, withoutQualification.eligible());
assertEquals("ACTIVE", withoutQualification.tenantStatus());
assertEquals(true, withQualification.eligible());
}
private IamTenant merchant(long id, String status) {
IamTenant tenant = new IamTenant();
tenant.setId(id);
tenant.setTenantCode("TEN" + id);
tenant.setTenantType("MERCHANT");
tenant.setStatus(status);
tenant.setTenantName("Example Merchant");
tenant.setLegalName("Example Merchant Ltd");
tenant.setUnifiedSocialCreditCode("91330100EXAMPLE001");
tenant.setContactName("Alice");
tenant.setContactPhone("13800138000");
return tenant;
}
}