refactor(api): 统一 HTTP 200 响应契约
This commit is contained in:
@@ -29,7 +29,8 @@ class IamOpenApiContractTest {
|
||||
.contains("AssignInternalUserRolesRequest:")
|
||||
.contains("membershipId: {type: integer, format: int64}")
|
||||
.contains("roleIds: {type: array, items: {type: integer, format: int64}}")
|
||||
.contains("'422': {$ref: '#/components/responses/UnprocessableEntity'}");
|
||||
.contains("example: '200'")
|
||||
.doesNotContain("'422': {$ref: '#/components/responses/UnprocessableEntity'}");
|
||||
|
||||
Map<String, Object> openApi = new Yaml().load(document);
|
||||
assertThat(openApi).containsEntry("openapi", "3.0.3");
|
||||
|
||||
+17
-2
@@ -5,6 +5,8 @@ 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;
|
||||
@@ -22,7 +24,7 @@ import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
@DisplayName("平台客户接口契约")
|
||||
class PlatformTenantControllerContractTest {
|
||||
@Test
|
||||
void merchantPageEndpointDeclaresPermissionPaginationConstraintsAndErrorStatuses() throws Exception {
|
||||
void merchantPageEndpointDeclaresPermissionPaginationConstraintsAndUnifiedBusinessStatus() throws Exception {
|
||||
Method method = PlatformTenantController.class.getMethod("page", long.class, long.class,
|
||||
String.class, String.class, AuthenticatedUser.class);
|
||||
|
||||
@@ -38,7 +40,7 @@ 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", "400", "401", "403", "422"), statuses);
|
||||
assertEquals(Set.of("200", "401", "403"), statuses);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -49,6 +51,19 @@ class PlatformTenantControllerContractTest {
|
||||
method.getAnnotation(PreAuthorize.class).value());
|
||||
}
|
||||
|
||||
@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");
|
||||
|
||||
assertNotNull(annotation(approve, 1, Valid.class));
|
||||
assertNotNull(annotation(reject, 1, Valid.class));
|
||||
assertEquals(500, comment.getAnnotation(Size.class).max());
|
||||
}
|
||||
|
||||
private <T extends Annotation> T annotation(Method method, int parameter, Class<T> type) {
|
||||
return method.getParameters()[parameter].getAnnotation(type);
|
||||
}
|
||||
|
||||
@@ -72,8 +72,22 @@ class MemberServiceTest {
|
||||
|
||||
@Test void authServiceFailureDoesNotPersistLocalStatus() {
|
||||
when(auth.status(anyString(), eq(40L), any())).thenThrow(new RuntimeException("网络失败"));
|
||||
assertThrows(RuntimeException.class, () -> service.changeStatus(100L, 20L,
|
||||
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,
|
||||
new ChangeMemberStatusRequest("DISABLED"), 99L));
|
||||
|
||||
assertEquals(ResultCode.NOT_FOUND, exception.getResultCode());
|
||||
assertEquals("认证账号不存在", exception.getMessage());
|
||||
verify(repository, never()).save(member);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
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.*;
|
||||
@@ -58,6 +59,29 @@ class PlatformInternalUserServiceTest {
|
||||
assertEquals(List.of(40L), result.roleIds());
|
||||
}
|
||||
|
||||
@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() {
|
||||
IamRole role = new IamRole();
|
||||
|
||||
@@ -2,6 +2,7 @@ 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;
|
||||
@@ -15,6 +16,7 @@ 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;
|
||||
@@ -98,6 +100,59 @@ 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);
|
||||
|
||||
Reference in New Issue
Block a user