暂存提交

This commit is contained in:
2026-07-31 14:57:09 +08:00
parent 7a19e2f096
commit f8804fbc52
53 changed files with 3771 additions and 0 deletions
@@ -0,0 +1,23 @@
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;
import static org.junit.jupiter.api.Assertions.assertNull;
class AuthorizationControllerContractTest {
@Test
void 当前权限接口不应依赖角色管理权限() throws Exception {
Method method = AuthorizationController.class.getMethod("permissions", String.class,
AuthenticatedUser.class);
assertEquals("/api/{app}/v1/iam/authorization/permissions",
method.getAnnotation(GetMapping.class).value()[0]);
assertNull(method.getAnnotation(PreAuthorize.class));
}
}
@@ -0,0 +1,34 @@
package com.baiyue.iam.controller;
import org.junit.jupiter.api.Test;
import org.yaml.snakeyaml.Yaml;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
class IamOpenApiContractTest {
@Test
void OpenApi应声明权限分页和内部用户的真实响应结构() throws IOException {
String document;
try (var stream = getClass().getResourceAsStream("/openapi/iam-v1.yaml")) {
assertThat(stream).isNotNull();
document = new String(stream.readAllBytes(), StandardCharsets.UTF_8);
}
assertThat(document).contains("/api/platform/v1/iam/authorization/permissions:")
.contains("/api/merchant/v1/iam/authorization/permissions:")
.contains("PermissionCodesApiResponse:")
.contains("TenantPageApiResponse:")
.contains("InternalUserListApiResponse:")
.contains("membershipId: {type: integer, format: int64}")
.contains("roleIds: {type: array, items: {type: integer, format: int64}}")
.contains("'422': {$ref: '#/components/responses/UnprocessableEntity'}");
Map<String, Object> openApi = new Yaml().load(document);
assertThat(openApi).containsEntry("openapi", "3.0.3");
assertThat(openApi.get("paths")).isInstanceOf(Map.class);
}
}
@@ -0,0 +1,48 @@
package com.baiyue.iam.controller;
import org.junit.jupiter.api.Test;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PutMapping;
import java.lang.reflect.Method;
import static org.junit.jupiter.api.Assertions.*;
class PlatformInternalUserContractTest {
@Test
void 内部用户列表必须校验列表权限() throws Exception {
Method method = ProfileResourceController.class.getMethod("internalUsers",
com.baiyue.common.security.AuthenticatedUser.class);
assertEquals("hasAuthority('platform:user:list')",
method.getAnnotation(PreAuthorize.class).value());
assertEquals("/api/platform/v1/iam/internal-users",
method.getAnnotation(GetMapping.class).value()[0]);
}
@Test
void 平台用户写接口以成员关系主键寻址() {
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");
}
@Test
void 企业资料查看必须校验查看权限() throws Exception {
Method method = MerchantTenantController.class.getMethod("profile",
com.baiyue.common.security.AuthenticatedUser.class);
assertEquals("hasAuthority('merchant:profile:view')",
method.getAnnotation(PreAuthorize.class).value());
}
private void assertMapping(String methodName, String path) {
Method method = java.util.Arrays.stream(ProfileResourceController.class.getMethods())
.filter(candidate -> candidate.getName().equals(methodName))
.findFirst().orElseThrow();
assertEquals(path, method.getAnnotation(PutMapping.class).value()[0]);
}
}
@@ -0,0 +1,45 @@
package com.baiyue.iam.controller;
import com.baiyue.common.security.AuthenticatedUser;
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.Test;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Set;
import java.util.stream.Collectors;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
class PlatformTenantControllerContractTest {
@Test
void 客户分页接口声明权限分页约束和错误状态() throws Exception {
Method method = PlatformTenantController.class.getMethod("page", long.class, long.class,
String.class, String.class, AuthenticatedUser.class);
assertEquals("/tenants", method.getAnnotation(GetMapping.class).value()[0]);
assertEquals("hasAuthority('platform:customer:list')",
method.getAnnotation(PreAuthorize.class).value());
assertNotNull(annotation(method, 0, Min.class));
assertNotNull(annotation(method, 1, Min.class));
assertEquals(200, annotation(method, 1, Max.class).value());
assertEquals("PROFILE_REQUIRED|PENDING|ACTIVE|REJECTED|DISABLED",
annotation(method, 3, Pattern.class).regexp());
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", "400", "401", "403", "422"), statuses);
}
private <T extends Annotation> T annotation(Method method, int parameter, Class<T> type) {
return method.getParameters()[parameter].getAnnotation(type);
}
}
@@ -0,0 +1,40 @@
package com.baiyue.iam.controller;
import org.junit.jupiter.api.Test;
import org.springframework.web.bind.annotation.PostMapping;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
class QualificationUploadContractTest {
@Test
void iamControllerDoesNotExposeQualificationUpload() {
boolean exposesLegacyPost = java.util.Arrays.stream(ProfileResourceController.class.getDeclaredMethods())
.map(method -> method.getAnnotation(PostMapping.class))
.filter(java.util.Objects::nonNull)
.flatMap(mapping -> java.util.Arrays.stream(mapping.value()))
.anyMatch("/api/merchant/v1/iam/tenant/qualifications"::equals);
assertFalse(exposesLegacyPost, "IAM 商户资质接口只读,上传必须由 File 服务处理");
}
@Test
void staticOpenApiDoesNotAdvertiseLegacyQualificationPost() throws IOException {
try (var input = getClass().getClassLoader().getResourceAsStream("openapi/iam-v1.yaml")) {
assertNotNull(input);
String yaml = new String(input.readAllBytes(), StandardCharsets.UTF_8);
int pathStart = yaml.indexOf(" /api/merchant/v1/iam/tenant/qualifications:");
int nextPath = yaml.indexOf("\n /api/", pathStart + 1);
assertTrue(pathStart >= 0 && nextPath > pathStart);
String pathContract = yaml.substring(pathStart, nextPath);
assertFalse(pathContract.contains("\n post:"));
assertFalse(yaml.contains(" QualificationRequest:"));
}
}
}
@@ -0,0 +1,146 @@
package com.baiyue.iam.service;
import com.baiyue.common.core.api.ApiResponse;
import com.baiyue.common.core.exception.BizException;
import com.baiyue.iam.client.AuthAccountClient;
import com.baiyue.iam.dto.PlatformInternalUserDtos.*;
import com.baiyue.iam.entity.IamMembership;
import com.baiyue.iam.entity.IamRole;
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.Test;
import org.mockito.ArgumentCaptor;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
class PlatformInternalUserServiceTest {
private final MemberRepository repository = mock(MemberRepository.class);
private final IamRoleMapper roleMapper = mock(IamRoleMapper.class);
private final AuthAccountClient auth = mock(AuthAccountClient.class);
private final PlatformInternalUserService service =
new PlatformInternalUserService(repository, roleMapper, auth, "internal-token");
private IamMembership membership;
private IamUser user;
@BeforeEach
void setUp() {
membership = new IamMembership();
membership.setId(30L); membership.setTenantId(0L); membership.setUserId(20L);
membership.setMemberType("EMPLOYEE"); membership.setMemberNo("PLATFORM-30");
membership.setStatus("ACTIVE");
user = new IamUser();
user.setId(20L); user.setAuthUserId(10L); user.setUserType("INTERNAL");
user.setDisplayName("平台运营"); user.setPhone("13800138000"); user.setStatus("ACTIVE");
when(repository.findByTenantAndId(0L, 30L)).thenReturn(membership);
when(repository.findUser(20L)).thenReturn(user);
when(repository.findRoleIds(0L, 30L)).thenReturn(List.of(40L));
when(auth.summary("internal-token", 10L)).thenReturn(ApiResponse.success(
new AuthAccountClient.AccountSummary(10L, "operator", "平台运营", "ACTIVE")));
}
@Test
void 列表返回成员主键联系方式和当前角色() {
when(repository.findByTenant(0L)).thenReturn(List.of(membership));
InternalUserResponse result = service.list().get(0);
assertEquals(30L, result.membershipId());
assertEquals("operator", result.username());
assertEquals("13800138000", result.phone());
assertEquals(List.of(40L), result.roleIds());
}
@Test
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);
when(auth.create(anyString(), any())).thenReturn(ApiResponse.success(
new AuthAccountClient.AccountSummary(99L, "new.operator", "新运营", "ACTIVE")));
CreateInternalUserRequest request = new CreateInternalUserRequest(null, null, "新运营",
"13900139000", "operator@example.com", "new.operator", "Password1",
"PLATFORM-NEW", List.of(40L));
InternalUserResponse result = service.create(request);
assertEquals("new.operator", result.username());
ArgumentCaptor<AuthAccountClient.CreateAccountRequest> captor =
ArgumentCaptor.forClass(AuthAccountClient.CreateAccountRequest.class);
verify(auth).create(eq("internal-token"), captor.capture());
assertEquals("PLATFORM", captor.getValue().appCode());
assertEquals(0L, captor.getValue().tenantId());
verify(repository).insertUser(any(IamUser.class));
verify(repository).insert(any(IamMembership.class));
verify(repository).replaceRoles(eq(0L), anyLong(), eq(List.of(40L)));
}
@Test
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));
doThrow(new RuntimeException("IAM 写入失败")).when(repository).insertUser(any());
CreateInternalUserRequest request = new CreateInternalUserRequest(null, null, "新运营",
null, null, "new.operator", "Password1", null, List.of());
assertThrows(RuntimeException.class, () -> service.create(request));
verify(auth).compensate(eq("internal-token"), anyLong());
}
@Test
void 不能停用当前登录的平台成员() {
assertThrows(BizException.class, () -> service.changeStatus(30L,
new ChangeInternalUserStatusRequest("DISABLED"), 30L));
verify(auth, never()).status(anyString(), anyLong(), any());
}
@Test
void 状态本地写入失败时恢复Auth且成功后不再远程查询() {
when(auth.status(anyString(), eq(10L), any())).thenReturn(ApiResponse.success(null));
doThrow(new RuntimeException("IAM 写入失败")).when(repository).saveUser(user);
assertThrows(RuntimeException.class, () -> service.changeStatus(30L,
new ChangeInternalUserStatusRequest("DISABLED"), 99L));
verify(auth).status(eq("internal-token"), eq(10L),
argThat(value -> "DISABLED".equals(value.status())));
verify(auth).status(eq("internal-token"), eq(10L),
argThat(value -> "ACTIVE".equals(value.status())));
verify(auth, times(1)).summary("internal-token", 10L);
}
@Test
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));
doThrow(new RuntimeException("IAM 写入失败")).when(repository).saveUser(user);
assertThrows(RuntimeException.class, () -> service.changeStatus(30L,
new ChangeInternalUserStatusRequest("ACTIVE"), 99L));
verify(auth).status(eq("internal-token"), eq(10L),
argThat(value -> "ACTIVE".equals(value.status())));
verify(auth).status(eq("internal-token"), eq(10L),
argThat(value -> "DISABLED".equals(value.status())));
}
@Test
void 重置密码只转交Auth不写入IAM() {
when(auth.reset(anyString(), eq(10L), any())).thenReturn(ApiResponse.success(null));
service.resetPassword(30L, new ResetInternalUserPasswordRequest("ResetPass1"));
verify(auth).reset(eq("internal-token"), eq(10L),
argThat(value -> "ResetPass1".equals(value.newPassword())));
verify(repository, never()).saveUser(any());
}
}
@@ -0,0 +1,44 @@
package com.baiyue.iam.service;
import com.baiyue.common.core.api.PageResult;
import com.baiyue.iam.entity.IamTenant;
import com.baiyue.iam.repository.TenantRepository;
import org.junit.jupiter.api.Test;
import org.springframework.test.util.ReflectionTestUtils;
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.when;
class TenantServiceTest {
@Test
void pageMerchantsMapsTenantEntitiesToApiResponses() {
TenantRepository repository = mock(TenantRepository.class);
TenantService service = new TenantService();
ReflectionTestUtils.setField(service, "repository", repository);
IamTenant tenant = new IamTenant();
tenant.setId(10001L);
tenant.setTenantCode("TEN10001");
tenant.setTenantName("Example Merchant");
tenant.setStatus("ACTIVE");
tenant.setLegalName("Example Merchant Ltd");
tenant.setUnifiedSocialCreditCode("91330100EXAMPLE001");
tenant.setContactName("Alice");
tenant.setContactPhone("13800138000");
tenant.setContactEmail("alice@example.com");
when(repository.pageMerchants(1, 10, "Example", "ACTIVE"))
.thenReturn(new PageResult<>(List.of(tenant), 1, 10, 1, 1));
var result = service.pageMerchants(1, 10, "Example", "ACTIVE");
assertEquals(1, result.total());
assertEquals("TEN10001", result.records().get(0).tenantCode());
assertEquals("Alice", result.records().get(0).contactName());
verify(repository).pageMerchants(1, 10, "Example", "ACTIVE");
}
}