This commit is contained in:
2026-08-05 09:11:26 +08:00
parent d544bbd0c5
commit 3791fee285
45 changed files with 1187 additions and 126 deletions
@@ -61,6 +61,13 @@ class QualificationFileContractTest {
assertTrue(hasMultipartPost);
}
@Test
void merchantAndPlatformExposeDedicatedRequiredPackageRoutes() {
assertTrue(hasMultipartRoute(QualificationFileController.class, "/required-package"));
assertTrue(hasMultipartRoute(PlatformQualificationFileController.class,
"/{tenantId}/required-package"));
}
@Test
void platformQualificationPreviewUsesTheCustomerViewPermission() throws Exception {
var method = PlatformQualificationFileController.class.getDeclaredMethod(
@@ -82,7 +89,7 @@ class QualificationFileContractTest {
when(iamClient.submitPlatform(eq("internal-token"), any())).thenReturn(ApiResponse.success(
new IamQualificationClient.PlatformQualificationResponse(31L, null, "PROFILE_REQUIRED", oldUrl)));
var response = controller.upload(platformUser(), 10001L, file, "LEGAL_PERSON_ID", null,
var response = controller.upload(platformUser(), 10001L, file, "BANK_ACCOUNT_LICENSE", null,
LocalDate.of(2030, 12, 31), true);
var ordered = inOrder(storage, iamClient);
@@ -108,7 +115,7 @@ class QualificationFileContractTest {
when(iamClient.submitPlatform(eq("internal-token"), any())).thenReturn(ApiResponse.success(
new IamQualificationClient.PlatformQualificationResponse(31L, null, "PROFILE_REQUIRED", currentUrl)));
controller.upload(platformUser(), 10001L, file, "LEGAL_PERSON_ID", null, null, true);
controller.upload(platformUser(), 10001L, file, "BANK_ACCOUNT_LICENSE", null, null, true);
verify(storage, never()).deleteByDocumentUrl(10001L, currentUrl);
}
@@ -124,7 +131,7 @@ class QualificationFileContractTest {
when(iamClient.submitPlatform(eq("internal-token"), any())).thenThrow(new RuntimeException("timeout"));
var exception = assertThrows(BizException.class,
() -> controller.upload(platformUser(), 10001L, file, "LEGAL_PERSON_ID", null, null, true));
() -> controller.upload(platformUser(), 10001L, file, "BANK_ACCOUNT_LICENSE", null, null, true));
assertEquals(ResultCode.SERVICE_UNAVAILABLE, exception.getResultCode());
verify(storage, never()).delete(stored);
@@ -142,7 +149,7 @@ class QualificationFileContractTest {
ApiResponse.failure(ResultCode.CONFLICT, "duplicate", null, null));
var exception = assertThrows(BizException.class,
() -> controller.upload(platformUser(), 10001L, file, "LEGAL_PERSON_ID", null, null, false));
() -> controller.upload(platformUser(), 10001L, file, "BANK_ACCOUNT_LICENSE", null, null, false));
assertEquals(ResultCode.CONFLICT, exception.getResultCode());
verify(storage).delete(stored);
@@ -159,7 +166,7 @@ class QualificationFileContractTest {
when(iamClient.submit(eq("internal-token"), any())).thenThrow(new RuntimeException("timeout"));
var exception = assertThrows(BizException.class,
() -> controller.upload(merchantUser(), file, "BUSINESS_LICENSE", null, null, false));
() -> controller.upload(merchantUser(), file, "FREIGHT_FORWARDER_CERTIFICATE", null, null, false));
assertEquals(ResultCode.SERVICE_UNAVAILABLE, exception.getResultCode());
verify(storage, never()).delete(stored);
@@ -177,12 +184,12 @@ class QualificationFileContractTest {
when(iamClient.submit(eq("internal-token"), any())).thenReturn(ApiResponse.success(
new IamQualificationClient.QualificationResponse(31L, null, "PROFILE_REQUIRED", oldUrl)));
controller.upload(merchantUser(), file, "LEGAL_PERSON_ID", null, LocalDate.of(2030, 1, 1), true);
controller.upload(merchantUser(), file, "FREIGHT_FORWARDER_CERTIFICATE", null, LocalDate.of(2030, 1, 1), true);
var requestCaptor = org.mockito.ArgumentCaptor.forClass(IamQualificationClient.QualificationRequest.class);
verify(iamClient).submit(eq("internal-token"), requestCaptor.capture());
verify(storage).deleteByDocumentUrl(10001L, oldUrl);
assertEquals("LEGAL_PERSON_ID", requestCaptor.getValue().qualificationType());
assertEquals("FREIGHT_FORWARDER_CERTIFICATE", requestCaptor.getValue().qualificationType());
assertTrue(requestCaptor.getValue().replace());
}
@@ -203,9 +210,8 @@ class QualificationFileContractTest {
}
@Test
void everyRequiredQualificationTypeIsAcceptedAndForwarded() {
void everyOptionalQualificationTypeIsAcceptedAndForwarded() {
for (String qualificationType : Set.of(
"BUSINESS_LICENSE", "LEGAL_PERSON_ID",
"FREIGHT_FORWARDER_CERTIFICATE", "BANK_ACCOUNT_LICENSE")) {
var storage = mock(LocalQualificationStorage.class);
var iamClient = mock(IamQualificationClient.class);
@@ -224,6 +230,22 @@ class QualificationFileContractTest {
}
}
@Test
void legacySingleUploadRejectsBothRequiredTypesBeforeWritingTheFile() {
for (String qualificationType : Set.of("BUSINESS_LICENSE", "LEGAL_PERSON_ID")) {
var storage = mock(LocalQualificationStorage.class);
var iamClient = mock(IamQualificationClient.class);
var controller = merchantController(storage, iamClient);
var file = new MockMultipartFile("file", "qualification.pdf", "application/pdf", "%PDF-1.4".getBytes());
assertThrows(BizException.class,
() -> controller.upload(merchantUser(), file, qualificationType, null, null, false));
verify(storage, never()).store(any(Long.class), any());
verify(iamClient, never()).submit(any(), any());
}
}
private static PlatformQualificationFileController platformController(
LocalQualificationStorage storage, IamQualificationClient iamClient) {
var controller = new PlatformQualificationFileController();
@@ -233,6 +255,14 @@ class QualificationFileContractTest {
return controller;
}
private static boolean hasMultipartRoute(Class<?> controllerType, String route) {
return Arrays.stream(controllerType.getDeclaredMethods())
.map(method -> method.getAnnotation(PostMapping.class))
.filter(java.util.Objects::nonNull)
.anyMatch(mapping -> Arrays.asList(mapping.value()).contains(route)
&& Arrays.asList(mapping.consumes()).contains(MediaType.MULTIPART_FORM_DATA_VALUE));
}
private static QualificationFileController merchantController(
LocalQualificationStorage storage, IamQualificationClient iamClient) {
var controller = new QualificationFileController();
@@ -0,0 +1,147 @@
package com.baiyue.file.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.file.client.IamQualificationClient;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.test.util.ReflectionTestUtils;
import java.nio.file.Path;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
class QualificationPackageServiceTest {
private static final long TENANT_ID = 10001L;
private static final String BUSINESS_URL =
"/api/merchant/v1/files/qualifications/11111111-1111-1111-1111-111111111111.pdf";
private static final String LEGAL_PERSON_URL =
"/api/merchant/v1/files/qualifications/22222222-2222-2222-2222-222222222222.png";
@Test
void storesBothFilesBeforeRegisteringOneIamPackage() {
var fixture = fixture();
when(fixture.storage.store(TENANT_ID, fixture.businessLicense)).thenReturn(fixture.businessStored);
when(fixture.storage.store(TENANT_ID, fixture.legalPersonId)).thenReturn(fixture.legalPersonStored);
when(fixture.iamClient.submitRequiredPackage(eq("internal-token"), any())).thenReturn(ApiResponse.success(
new IamQualificationClient.RequiredPackageResponse(31L, 32L, "PROFILE_REQUIRED", null, null)));
var result = fixture.service.upload(TENANT_ID, fixture.businessLicense, fixture.legalPersonId,
"91310101abcde12345", null, null, false, false, false);
var ordered = inOrder(fixture.storage, fixture.iamClient);
ordered.verify(fixture.storage).store(TENANT_ID, fixture.businessLicense);
ordered.verify(fixture.storage).store(TENANT_ID, fixture.legalPersonId);
var captor = ArgumentCaptor.forClass(IamQualificationClient.RequiredPackageRequest.class);
ordered.verify(fixture.iamClient).submitRequiredPackage(eq("internal-token"), captor.capture());
assertEquals("91310101ABCDE12345", captor.getValue().unifiedSocialCreditCode());
assertEquals(BUSINESS_URL, result.businessLicenseDocumentUrl());
assertEquals(LEGAL_PERSON_URL, result.legalPersonIdDocumentUrl());
}
@Test
void secondStorageFailureDeletesTheFirstFileAndNeverCallsIam() {
var fixture = fixture();
when(fixture.storage.store(TENANT_ID, fixture.businessLicense)).thenReturn(fixture.businessStored);
when(fixture.storage.store(TENANT_ID, fixture.legalPersonId)).thenThrow(new BizException("disk full"));
assertThrows(BizException.class, () -> fixture.service.upload(TENANT_ID,
fixture.businessLicense, fixture.legalPersonId, "91310101ABCDE12345",
null, null, false, false, false));
verify(fixture.storage).delete(fixture.businessStored);
verify(fixture.iamClient, never()).submitRequiredPackage(any(), any());
}
@Test
void explicitIamRejectionDeletesBothUnreferencedFiles() {
var fixture = storedFixture();
when(fixture.iamClient.submitRequiredPackage(eq("internal-token"), any())).thenReturn(
ApiResponse.failure(ResultCode.CONFLICT, "duplicate", null, null));
var exception = assertThrows(BizException.class, () -> fixture.service.upload(TENANT_ID,
fixture.businessLicense, fixture.legalPersonId, "91310101ABCDE12345",
null, null, false, false, false));
assertEquals(ResultCode.CONFLICT, exception.getResultCode());
verify(fixture.storage).delete(fixture.businessStored);
verify(fixture.storage).delete(fixture.legalPersonStored);
}
@Test
void unknownIamOutcomePreservesBothFilesForReconciliation() {
var fixture = storedFixture();
when(fixture.iamClient.submitRequiredPackage(eq("internal-token"), any()))
.thenThrow(new RuntimeException("timeout"));
var exception = assertThrows(BizException.class, () -> fixture.service.upload(TENANT_ID,
fixture.businessLicense, fixture.legalPersonId, "91310101ABCDE12345",
null, null, false, false, false));
assertEquals(ResultCode.SERVICE_UNAVAILABLE, exception.getResultCode());
verify(fixture.storage, never()).delete(fixture.businessStored);
verify(fixture.storage, never()).delete(fixture.legalPersonStored);
}
@Test
void platformSuccessCleansReplacedFilesOnlyAfterIamCommit() {
var fixture = storedFixture();
var oldBusinessUrl = "/api/merchant/v1/files/qualifications/33333333-3333-3333-3333-333333333333.pdf";
var oldLegalPersonUrl = "/api/merchant/v1/files/qualifications/44444444-4444-4444-4444-444444444444.png";
when(fixture.iamClient.submitPlatformRequiredPackage(eq("internal-token"), any())).thenReturn(
ApiResponse.success(new IamQualificationClient.RequiredPackageResponse(
31L, 32L, "PROFILE_REQUIRED", oldBusinessUrl, oldLegalPersonUrl)));
fixture.service.upload(TENANT_ID, fixture.businessLicense, fixture.legalPersonId,
"91310101ABCDE12345", null, null, true, true, true);
var ordered = inOrder(fixture.iamClient, fixture.storage);
ordered.verify(fixture.iamClient).submitPlatformRequiredPackage(eq("internal-token"), any());
ordered.verify(fixture.storage).deleteByDocumentUrl(TENANT_ID, oldBusinessUrl);
ordered.verify(fixture.storage).deleteByDocumentUrl(TENANT_ID, oldLegalPersonUrl);
}
private static Fixture storedFixture() {
var fixture = fixture();
when(fixture.storage.store(TENANT_ID, fixture.businessLicense)).thenReturn(fixture.businessStored);
when(fixture.storage.store(TENANT_ID, fixture.legalPersonId)).thenReturn(fixture.legalPersonStored);
return fixture;
}
private static Fixture fixture() {
var storage = mock(LocalQualificationStorage.class);
var iamClient = mock(IamQualificationClient.class);
var service = new QualificationPackageService();
ReflectionTestUtils.setField(service, "storage", storage);
ReflectionTestUtils.setField(service, "iamClient", iamClient);
ReflectionTestUtils.setField(service, "internalToken", "internal-token");
var businessLicense = new MockMultipartFile(
"businessLicense", "license.pdf", "application/pdf", "%PDF-1.4".getBytes());
var legalPersonId = new MockMultipartFile(
"legalPersonId", "legal-person.png", "image/png", new byte[] {1, 2, 3});
var businessStored = new LocalQualificationStorage.StoredFile(
Path.of("business.pdf"), "business.pdf", BUSINESS_URL);
var legalPersonStored = new LocalQualificationStorage.StoredFile(
Path.of("legal-person.png"), "legal-person.png", LEGAL_PERSON_URL);
return new Fixture(service, storage, iamClient, businessLicense, legalPersonId,
businessStored, legalPersonStored);
}
private record Fixture(QualificationPackageService service,
LocalQualificationStorage storage,
IamQualificationClient iamClient,
MockMultipartFile businessLicense,
MockMultipartFile legalPersonId,
LocalQualificationStorage.StoredFile businessStored,
LocalQualificationStorage.StoredFile legalPersonStored) { }
}