格式修改

This commit is contained in:
2026-08-05 10:35:07 +08:00
parent 81c9c92f5d
commit 496fc58a90
241 changed files with 5600 additions and 2088 deletions
@@ -5,26 +5,42 @@ import com.baiyue.common.core.export.ExportContracts.ExportBatchRequest;
import com.baiyue.common.core.export.ExportContracts.ExportBatchResponse;
import com.baiyue.file.config.ExportDefinitionRegistry;
import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
import io.micrometer.core.instrument.MeterRegistry;
import jakarta.annotation.Resource;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.mockito.Mockito.*;
@SpringJUnitConfig(classes = {ExportProviderClient.class, ExportProviderClientTest.TestConfig.class})
class ExportProviderClientTest {
@MockBean private IamExportProviderClient iam;
@MockBean private OrderExportProviderClient order;
@MockBean private WarehouseExportProviderClient warehouse;
@MockBean private FirstlegExportProviderClient firstleg;
@Resource private ExportProviderClient client;
@Configuration(proxyBeanMethods = false)
static class TestConfig {
@Bean
MeterRegistry meterRegistry() {
return new SimpleMeterRegistry();
}
}
@Test void routesTheRequestToTheAllowlistedServiceSelectedByTheDefinition() {
IamExportProviderClient iam=mock(IamExportProviderClient.class);
OrderExportProviderClient order=mock(OrderExportProviderClient.class);
WarehouseExportProviderClient warehouse=mock(WarehouseExportProviderClient.class);
FirstlegExportProviderClient firstleg=mock(FirstlegExportProviderClient.class);
var response=ApiResponse.success(new ExportBatchResponse(1,"hash","snapshot",java.util.List.of(),java.util.List.of(),null,false,0L));
when(order.batch(anyString(),anyString(),any())).thenReturn(response);
var definition=new ExportDefinitionRegistry.Definition();definition.setProviderService("order-service");
ExportBatchRequest request=new ExportBatchRequest("9","0","PLATFORM","7",1, Map.of(),null,null,500);
var actual=new ExportProviderClient(iam,order,warehouse,firstleg,new SimpleMeterRegistry())
.batch("token","ORDER_LIST",definition,request);
var actual=client.batch("token","ORDER_LIST",definition,request);
assertSame(response,actual);
verify(order).batch("token","ORDER_LIST",request);
@@ -2,9 +2,13 @@ package com.baiyue.file.controller;
import com.baiyue.common.core.exception.BizException;
import com.baiyue.file.service.MeasurementImageService;
import jakarta.annotation.Resource;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
@@ -12,13 +16,16 @@ import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@SpringJUnitConfig(InternalMeasurementImageController.class)
@TestPropertySource(properties = "baiyue.security.internal-service-token=internal-token")
class InternalMeasurementImageControllerTest {
private final MeasurementImageService images = mock(MeasurementImageService.class);
private final InternalMeasurementImageController controller =
new InternalMeasurementImageController(images, "internal-token");
@MockBean
private MeasurementImageService images;
@Resource
private InternalMeasurementImageController controller;
@Test
void 保存第三方尺寸测量图片但不触发重量识别契约() {
void shouldHandleCase01() {
var file = new MockMultipartFile("file", "measure.png", MediaType.IMAGE_PNG_VALUE,
new byte[]{1, 2, 3});
var result = new MeasurementImageService.UploadResult(41L, "measure.png",
@@ -33,7 +40,7 @@ class InternalMeasurementImageControllerTest {
}
@Test
void 内部令牌错误时拒绝保存() {
void shouldHandleCase02() {
var file = new MockMultipartFile("file", "measure.png", MediaType.IMAGE_PNG_VALUE,
new byte[]{1});
assertThatThrownBy(() -> controller.upload("bad-token", 31L, 11L, 21L, file))
@@ -41,7 +48,7 @@ class InternalMeasurementImageControllerTest {
.hasMessageContaining("凭证无效");
}
@Test
void 内部下载同时校验租户入库单和文件ID() {
void shouldHandleCase03() {
when(images.download(11L, 21L, 41L)).thenReturn(
new MeasurementImageService.Download("measure.png", MediaType.IMAGE_PNG_VALUE,
new byte[]{1, 2, 3}));
@@ -3,12 +3,15 @@ package com.baiyue.file.controller;
import com.baiyue.common.core.exception.BizException;
import com.baiyue.common.security.AuthenticatedUser;
import com.baiyue.file.service.MeasurementImageService;
import jakarta.annotation.Resource;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import java.lang.reflect.Method;
import java.util.Set;
@@ -18,13 +21,15 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
@SpringJUnitConfig(PlatformMeasurementImageController.class)
class PlatformMeasurementImageControllerContractTest {
private final MeasurementImageService images = mock(MeasurementImageService.class);
private final PlatformMeasurementImageController controller =
new PlatformMeasurementImageController(images);
@MockBean
private MeasurementImageService images;
@Resource
private PlatformMeasurementImageController controller;
@Test
void 暴露平台测量图片multipart入口并校验重量维护权限() throws Exception {
void shouldHandleCase01() throws Exception {
RequestMapping base = PlatformMeasurementImageController.class.getAnnotation(RequestMapping.class);
assertThat(base.value()).containsExactly("/api/platform/v1/files/measurements");
Method method = PlatformMeasurementImageController.class.getDeclaredMethod("upload",
@@ -36,7 +41,7 @@ class PlatformMeasurementImageControllerContractTest {
}
@Test
void 商户身份不能调用平台上传接口() {
void shouldHandleCase02() {
var file = new MockMultipartFile("file", "weight.png", MediaType.IMAGE_PNG_VALUE, new byte[]{1});
var merchant = new AuthenticatedUser(2L, 10001L, 10001L, 2L,
"merchant", "MERCHANT", "WEB", Set.of());
@@ -46,7 +51,7 @@ class PlatformMeasurementImageControllerContractTest {
}
@Test
void 下载始终同时使用租户入库单和文件ID隔离() {
void shouldHandleCase03() {
var platform = new AuthenticatedUser(1L, 0L, 0L, 1L,
"admin", "PLATFORM", "WEB", Set.of());
org.mockito.Mockito.when(images.download(10001L, 88L, 99L)).thenReturn(
@@ -6,12 +6,16 @@ import com.baiyue.common.core.exception.BizException;
import com.baiyue.common.security.AuthenticatedUser;
import com.baiyue.file.client.IamQualificationClient;
import com.baiyue.file.service.LocalQualificationStorage;
import com.baiyue.file.service.QualificationPackageService;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
@@ -20,6 +24,8 @@ import java.time.LocalDate;
import java.util.Arrays;
import java.util.Set;
import jakarta.annotation.Resource;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -32,7 +38,14 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@DisplayName("企业资质文件接口契约功能")
@SpringJUnitConfig(classes = {QualificationFileController.class, PlatformQualificationFileController.class})
@TestPropertySource(properties = "baiyue.security.internal-service-token=internal-token")
class QualificationFileContractTest {
@MockBean private LocalQualificationStorage defaultStorage;
@MockBean private IamQualificationClient defaultIamClient;
@MockBean private QualificationPackageService packageService;
@Resource private PlatformQualificationFileController platformController;
@Resource private QualificationFileController merchantController;
@Test
void fileServiceExposesTheOnlyPublicMultipartUploadEntry() {
@@ -246,12 +259,11 @@ class QualificationFileContractTest {
}
}
private static PlatformQualificationFileController platformController(
private PlatformQualificationFileController platformController(
LocalQualificationStorage storage, IamQualificationClient iamClient) {
var controller = new PlatformQualificationFileController();
var controller = platformController;
ReflectionTestUtils.setField(controller, "storage", storage);
ReflectionTestUtils.setField(controller, "iamClient", iamClient);
ReflectionTestUtils.setField(controller, "internalToken", "internal-token");
return controller;
}
@@ -263,12 +275,11 @@ class QualificationFileContractTest {
&& Arrays.asList(mapping.consumes()).contains(MediaType.MULTIPART_FORM_DATA_VALUE));
}
private static QualificationFileController merchantController(
private QualificationFileController merchantController(
LocalQualificationStorage storage, IamQualificationClient iamClient) {
var controller = new QualificationFileController();
var controller = merchantController;
ReflectionTestUtils.setField(controller, "storage", storage);
ReflectionTestUtils.setField(controller, "iamClient", iamClient);
ReflectionTestUtils.setField(controller, "internalToken", "internal-token");
return controller;
}
@@ -2,9 +2,15 @@ package com.baiyue.file.mq;
import com.baiyue.file.service.ExportTaskExecutor;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.annotation.Resource;
import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
import org.apache.rocketmq.common.message.MessageExt;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import java.nio.charset.StandardCharsets;
@@ -13,10 +19,24 @@ import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
@SpringJUnitConfig(classes = {ExportCommandConsumer.class, ExportCommandConsumerTest.TestConfig.class})
@TestPropertySource(properties = {
"baiyue.file.export.consumer.enabled=true",
"baiyue.file.export.instance-id=file-1",
"baiyue.file.export.consumer.concurrency=4",
"baiyue.file.export.consumer.consume-timeout-minutes=90",
"baiyue.file.export.consumer.max-reconsume-times=2"
})
class ExportCommandConsumerTest {
@MockBean private ExportTaskExecutor executor;
@Resource private ExportCommandConsumer consumer;
@Configuration(proxyBeanMethods = false)
static class TestConfig {
@Bean ObjectMapper objectMapper() { return new ObjectMapper(); }
}
@Test void appliesConfiguredConcurrencyAndDispatchesAValidCommand() {
ExportTaskExecutor executor=mock(ExportTaskExecutor.class);
ExportCommandConsumer consumer=new ExportCommandConsumer(new ObjectMapper(),executor,"file-1",4,90,2);
DefaultMQPushConsumer pushConsumer=new DefaultMQPushConsumer();
consumer.prepareStart(pushConsumer);
assertEquals(4,pushConsumer.getConsumeThreadMin());
@@ -31,7 +51,6 @@ class ExportCommandConsumerTest {
}
@Test void rejectsMalformedMessagesSoRocketMqCanRedeliverThem() {
ExportCommandConsumer consumer=new ExportCommandConsumer(new ObjectMapper(),mock(ExportTaskExecutor.class),"file-1",4,90,2);
MessageExt message=new MessageExt();message.setBody("not-json".getBytes(StandardCharsets.UTF_8));
assertThrows(IllegalArgumentException.class,() -> consumer.onMessage(message));
}
@@ -1,29 +1,46 @@
package com.baiyue.file.mq;
import com.baiyue.file.service.ExportStateService;
import io.micrometer.core.instrument.MeterRegistry;
import jakarta.annotation.Resource;
import org.apache.rocketmq.common.message.MessageExt;
import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
import org.junit.jupiter.api.Test;
import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import static org.mockito.Mockito.*;
@SpringJUnitConfig(classes = {ExportDlqHandler.class, ExportDlqHandlerTest.TestConfig.class})
@TestPropertySource(properties = "baiyue.file.export.consumer.enabled=true")
class ExportDlqHandlerTest {
@MockBean private ExportStateService states;
@Resource private ExportDlqHandler handler;
@Resource private MeterRegistry meters;
@Configuration(proxyBeanMethods = false)
static class TestConfig {
@Bean MeterRegistry meterRegistry() { return new SimpleMeterRegistry(); }
}
@Test void databaseFailuresCannotFallIntoAnUnconsumedSecondaryDlq() {
RocketMQMessageListener listener=ExportDlqHandler.class.getAnnotation(RocketMQMessageListener.class);
org.junit.jupiter.api.Assertions.assertEquals(Integer.MAX_VALUE,listener.maxReconsumeTimes());
}
@Test void quarantinesUsingUserPropertiesWithoutParsingTheBody() {
ExportStateService states=mock(ExportStateService.class); MessageExt message=new MessageExt();
SimpleMeterRegistry meters=new SimpleMeterRegistry();
MessageExt message=new MessageExt();
message.putUserProperty("eventId","event-1");message.putUserProperty("taskId","9");message.putUserProperty("dispatchVersion","3");message.putUserProperty("schemaVersion","1");
new ExportDlqHandler(states,meters).onMessage(message);
handler.onMessage(message);
verify(states).quarantine(eq("event-1"),eq(9L),eq(3),contains("自动隔离"));
org.junit.jupiter.api.Assertions.assertEquals(1,meters.counter("export_dlq_messages").count());
}
@Test void malformedPropertiesAreAcknowledgedWithoutTouchingTasks() {
ExportStateService states=mock(ExportStateService.class); SimpleMeterRegistry meters=new SimpleMeterRegistry();
new ExportDlqHandler(states,meters).onMessage(new MessageExt());
verifyNoInteractions(states); org.junit.jupiter.api.Assertions.assertEquals(1,meters.counter("export_dlq_messages").count());
double before = meters.counter("export_dlq_messages").count();
handler.onMessage(new MessageExt());
verifyNoInteractions(states);
org.junit.jupiter.api.Assertions.assertEquals(before + 1, meters.counter("export_dlq_messages").count());
}
}
@@ -2,10 +2,14 @@ package com.baiyue.file.mq;
import com.baiyue.file.entity.FileExportOutbox;
import com.baiyue.file.repository.ExportOutboxRepository;
import jakarta.annotation.Resource;
import org.apache.rocketmq.spring.core.RocketMQTemplate;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.springframework.messaging.Message;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import java.util.List;
@@ -13,14 +17,23 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
@SpringJUnitConfig(ExportOutboxPublisher.class)
@TestPropertySource(properties = {
"baiyue.file.export.publisher.enabled=true",
"baiyue.file.export.instance-id=file-1",
"baiyue.file.export.publisher.batch-size=50",
"baiyue.file.export.publisher.lease-seconds=60"
})
class ExportOutboxPublisherTest {
@MockBean private ExportOutboxRepository outbox;
@MockBean private RocketMQTemplate rocket;
@Resource private ExportOutboxPublisher publisher;
@Test void publishesAllDlqLocatorFieldsAsRocketMqUserProperties() {
ExportOutboxRepository outbox=mock(ExportOutboxRepository.class);
RocketMQTemplate rocket=mock(RocketMQTemplate.class);
FileExportOutbox row=row();
when(outbox.publishableIds(50)).thenReturn(List.of(1L));when(outbox.claim(1L,"file-1",60)).thenReturn(true);when(outbox.find(1L)).thenReturn(row);
new ExportOutboxPublisher(outbox,rocket,"file-1",50,60).publish();
publisher.publish();
@SuppressWarnings("rawtypes") ArgumentCaptor<Message> message=ArgumentCaptor.forClass(Message.class);
verify(rocket).syncSend(eq("test-export:EXPORT_TASK"),message.capture(),eq(3000L));
@@ -32,28 +45,22 @@ class ExportOutboxPublisherTest {
}
@Test void returnsFailedSendsToPendingWithTheFirstBackoffInsteadOfGivingUp() {
ExportOutboxRepository outbox=mock(ExportOutboxRepository.class);
RocketMQTemplate rocket=mock(RocketMQTemplate.class);
FileExportOutbox row=row();row.setAttemptCount(1);
when(outbox.publishableIds(50)).thenReturn(List.of(1L));when(outbox.claim(1L,"file-1",60)).thenReturn(true);when(outbox.find(1L)).thenReturn(row);
doThrow(new RuntimeException("broker down")).when(rocket).syncSend(anyString(),any(Message.class),anyLong());
new ExportOutboxPublisher(outbox,rocket,"file-1",50,60).publish();
publisher.publish();
verify(outbox).sendFailed(1L,"file-1",5,"broker down");
verify(outbox,never()).sent(anyLong(),anyString());
}
@Test void stillRetriesAfterThirtyConsecutiveBrokerFailuresWithoutADeadState() {
ExportOutboxRepository outbox=mock(ExportOutboxRepository.class);
RocketMQTemplate rocket=mock(RocketMQTemplate.class);
FileExportOutbox row=row();row.setAttemptCount(30);
when(outbox.publishableIds(50)).thenReturn(List.of(1L));
when(outbox.claim(1L,"file-1",60)).thenReturn(true);
when(outbox.find(1L)).thenReturn(row);
doThrow(new RuntimeException("broker down")).when(rocket).syncSend(anyString(),any(Message.class),anyLong());
ExportOutboxPublisher publisher=new ExportOutboxPublisher(outbox,rocket,"file-1",50,60);
for(int i=0;i<30;i++) publisher.publish();
verify(outbox,times(30)).sendFailed(1L,"file-1",900,"broker down");
@@ -5,6 +5,13 @@ import com.baiyue.file.repository.ExportOutboxRepository;
import com.baiyue.file.repository.ExportTaskRepository;
import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import jakarta.annotation.Resource;
import java.time.Duration;
import java.util.List;
@@ -12,19 +19,26 @@ import java.util.List;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
@SpringJUnitConfig(classes = {ExportReconciler.class, ExportReconcilerTest.TestConfig.class})
@TestPropertySource(properties = {
"baiyue.file.export.reconciler.enabled=true",
"baiyue.file.export.reconciler.redispatch-grace-seconds=60",
"baiyue.file.export.reconciler.sent-stale-seconds=600",
"baiyue.file.export.retention-days=7"
})
class ExportReconcilerTest {
@MockBean private ExportTaskRepository tasks;
@MockBean private ExportOutboxRepository outbox;
@MockBean private ExportStateService states;
@MockBean private ExportStorage storage;
@Resource private ExportReconciler reconciler;
@Test void performsEveryRecoveryResponsibilityWithoutExecutingBusinessExports() {
ExportTaskRepository tasks=mock(ExportTaskRepository.class);
ExportOutboxRepository outbox=mock(ExportOutboxRepository.class);
ExportStateService states=mock(ExportStateService.class);
ExportStorage storage=mock(ExportStorage.class);
FileExportTask missing=task(1L,"PENDING"), stale=task(2L,"RUNNING"), expired=task(3L,"SUCCEEDED");expired.setObjectKey("exports/3/file.xlsx");
when(outbox.releaseExpiredClaims()).thenReturn(2);when(outbox.redispatchSent(600,60)).thenReturn(3);
when(tasks.missingOutbox(60)).thenReturn(List.of(missing));when(states.ensureOutbox(missing)).thenReturn(true);
when(tasks.staleRunning()).thenReturn(List.of(stale));when(states.recoverStale(stale)).thenReturn(true);
when(tasks.expiredArtifactsDb()).thenReturn(List.of(expired));when(storage.delete(expired.getObjectKey())).thenReturn(true);
when(storage.cleanupOrphans(eq(Duration.ofHours(24)),any())).thenReturn(1);
ExportReconciler reconciler=new ExportReconciler(tasks,outbox,states,storage,new SimpleMeterRegistry(),60,600,7);
reconciler.reconcile();
@@ -34,11 +48,14 @@ class ExportReconcilerTest {
}
@Test void deletesOnlyTheConfiguredDailyBatchOfRetainedSentOutboxRows() {
ExportOutboxRepository outbox=mock(ExportOutboxRepository.class);
ExportReconciler reconciler=new ExportReconciler(mock(ExportTaskRepository.class),outbox,mock(ExportStateService.class),mock(ExportStorage.class),new SimpleMeterRegistry(),60,600,7);
reconciler.cleanupRetainedOutbox();
verify(outbox).deleteRetainedSent(7,5000);
}
private FileExportTask task(long id,String status){FileExportTask task=new FileExportTask();task.setId(id);task.setStatus(status);return task;}
@Configuration
static class TestConfig {
@Bean SimpleMeterRegistry meterRegistry() { return new SimpleMeterRegistry(); }
}
}
@@ -3,23 +3,29 @@ package com.baiyue.file.service;
import com.baiyue.file.repository.ExportOutboxRepository;
import com.baiyue.file.repository.ExportTaskRepository;
import com.baiyue.file.entity.FileExportTask;
import jakarta.annotation.Resource;
import org.junit.jupiter.api.Test;
import org.mockito.InOrder;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Mockito.*;
@SpringJUnitConfig(ExportStateService.class)
class ExportStateServiceTest {
@MockBean private ExportTaskRepository tasks;
@MockBean private ExportOutboxRepository outbox;
@MockBean private ExportCommandFactory commands;
@MockBean private ExportMetrics metrics;
@Resource private ExportStateService service;
@Test void creationLimitsAndDuplicateDetectionRunUnderTheDatabaseGuard() {
ExportTaskRepository tasks=mock(ExportTaskRepository.class);
ExportOutboxRepository outbox=mock(ExportOutboxRepository.class);
ExportCommandFactory commands=mock(ExportCommandFactory.class);
FileExportTask task=task(9L,1,"PENDING");
task.setTenantId(0L);task.setAppCode("PLATFORM");task.setRequestedBy(7L);task.setExportType("CUSTOMER_LIST");task.setParametersJson("{}");
when(commands.create(task,1)).thenReturn(mock(com.baiyue.file.entity.FileExportOutbox.class));
when(outbox.insert(any())).thenReturn(true);
ExportStateService service=new ExportStateService(tasks,outbox,commands,mock(ExportMetrics.class));
ExportStateService.CreationResult result=service.createConstrained(task);
@@ -35,12 +41,9 @@ class ExportStateServiceTest {
}
@Test void duplicateCreationReturnsTheExistingTaskWithoutWritingAnotherOutbox() {
ExportTaskRepository tasks=mock(ExportTaskRepository.class);
ExportOutboxRepository outbox=mock(ExportOutboxRepository.class);
FileExportTask request=task(9L,1,"PENDING"), existing=task(8L,1,"PENDING");
request.setTenantId(0L);request.setAppCode("PLATFORM");request.setRequestedBy(7L);request.setExportType("CUSTOMER_LIST");request.setParametersJson("{}");
when(tasks.recentDuplicate(0L,"PLATFORM",7L,"CUSTOMER_LIST","{}")).thenReturn(existing);
ExportStateService service=new ExportStateService(tasks,outbox,mock(ExportCommandFactory.class),mock(ExportMetrics.class));
ExportStateService.CreationResult result=service.createConstrained(request);
@@ -52,12 +55,9 @@ class ExportStateServiceTest {
}
@Test void activeTaskLimitsAreEnforcedAfterTakingTheCreationGuard() {
ExportTaskRepository tasks=mock(ExportTaskRepository.class);
ExportOutboxRepository outbox=mock(ExportOutboxRepository.class);
FileExportTask request=task(9L,1,"PENDING");
request.setTenantId(0L);request.setAppCode("PLATFORM");request.setRequestedBy(7L);request.setExportType("CUSTOMER_LIST");request.setParametersJson("{}");
when(tasks.activeForUser(0L,"PLATFORM",7L)).thenReturn(3L);
ExportStateService service=new ExportStateService(tasks,outbox,mock(ExportCommandFactory.class),mock(ExportMetrics.class));
assertThrows(com.baiyue.common.core.exception.BizException.class,() -> service.createConstrained(request));
@@ -70,23 +70,17 @@ class ExportStateServiceTest {
}
@Test void quarantineRequiresOutboxAndTaskToChangeAtomically() {
ExportTaskRepository tasks=mock(ExportTaskRepository.class);
ExportOutboxRepository outbox=mock(ExportOutboxRepository.class);
when(tasks.find(9L)).thenReturn(task(9L,3,"PENDING"));
when(outbox.quarantine("event-1",9L,3,"broken")).thenReturn(true);
when(tasks.failQuarantined(9L,3,"broken")).thenReturn(false);
ExportStateService service=new ExportStateService(tasks,outbox,mock(ExportCommandFactory.class),mock(ExportMetrics.class));
assertThrows(IllegalStateException.class,() -> service.quarantine("event-1",9L,3,"broken"));
verify(tasks).failQuarantined(9L,3,"broken");
}
@Test void oldDlqMessageThatMatchesNoOutboxDoesNotTouchTheCurrentTask() {
ExportTaskRepository tasks=mock(ExportTaskRepository.class);
ExportOutboxRepository outbox=mock(ExportOutboxRepository.class);
when(tasks.find(9L)).thenReturn(task(9L,3,"PENDING"));
when(outbox.quarantine(anyString(),anyLong(),anyInt(),anyString())).thenReturn(false);
ExportStateService service=new ExportStateService(tasks,outbox,mock(ExportCommandFactory.class),mock(ExportMetrics.class));
assertDoesNotThrow(() -> service.quarantine("old-event",9L,2,"broken"));
verify(tasks).find(9L);
@@ -94,10 +88,7 @@ class ExportStateServiceTest {
}
@Test void matchingDlqMessageDoesNotDamageAnAlreadyRunningTask() {
ExportTaskRepository tasks=mock(ExportTaskRepository.class);
ExportOutboxRepository outbox=mock(ExportOutboxRepository.class);
when(tasks.find(9L)).thenReturn(task(9L,3,"RUNNING"));
ExportStateService service=new ExportStateService(tasks,outbox,mock(ExportCommandFactory.class),mock(ExportMetrics.class));
assertDoesNotThrow(() -> service.quarantine("event-1",9L,3,"broken"));
verifyNoInteractions(outbox);
@@ -4,20 +4,44 @@ import com.baiyue.file.config.ExportDefinitionRegistry;
import com.baiyue.file.entity.FileExportTask;
import com.baiyue.file.repository.ExportTaskRepository;
import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
import io.micrometer.core.instrument.MeterRegistry;
import jakarta.annotation.Resource;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import org.springframework.test.util.ReflectionTestUtils;
import java.util.concurrent.atomic.AtomicBoolean;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
@SpringJUnitConfig(classes = {ExportTaskExecutor.class, ExportTaskExecutorTest.TestConfig.class})
@TestPropertySource(properties = {
"baiyue.file.export.task.lease-seconds=300",
"baiyue.file.export.task.heartbeat-seconds=60",
"baiyue.file.export.task.max-duration-minutes=30",
"baiyue.file.export.retention-days=7"
})
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD)
class ExportTaskExecutorTest {
@MockBean private ExportTaskRepository tasks;
@MockBean private ExportDefinitionRegistry definitions;
@MockBean private GenericXlsxExportRunner runner;
@MockBean private ExportStateService states;
@MockBean private ExportStorage storage;
@Resource private ExportTaskExecutor executor;
@Configuration(proxyBeanMethods = false)
static class TestConfig {
@Bean MeterRegistry meterRegistry() { return new SimpleMeterRegistry(); }
}
@Test void aCommandThatCannotClaimTheTaskIsAcknowledgedWithoutExecutingOrChangingState() {
ExportTaskRepository tasks=mock(ExportTaskRepository.class);
ExportDefinitionRegistry definitions=mock(ExportDefinitionRegistry.class);
GenericXlsxExportRunner runner=mock(GenericXlsxExportRunner.class);
ExportStateService states=mock(ExportStateService.class);
when(tasks.claim(9L,1,"file-1",300,30)).thenReturn(false);
ExportTaskExecutor executor=new ExportTaskExecutor(tasks,definitions,runner,states,mock(ExportStorage.class),new SimpleMeterRegistry(),300,60,30,7);
executor.execute(9L,1,"file-1");
@@ -26,10 +50,8 @@ class ExportTaskExecutorTest {
}
@Test void watchdogRenewsTheLeaseWhileTheRunnerIsStillWriting() {
ExportTaskRepository tasks=mock(ExportTaskRepository.class);
ExportDefinitionRegistry definitions=mock(ExportDefinitionRegistry.class);
GenericXlsxExportRunner runner=mock(GenericXlsxExportRunner.class);
ExportStateService states=mock(ExportStateService.class);
ReflectionTestUtils.setField(executor, "leaseSeconds", 5);
ReflectionTestUtils.setField(executor, "heartbeatSeconds", 1);
FileExportTask running=task(1,"file-1");
when(tasks.claim(9L,1,"file-1",5,30)).thenReturn(true);when(tasks.find(9L)).thenReturn(running);
when(tasks.heartbeat(9L,1,"file-1",5)).thenReturn(true);when(tasks.succeed(anyLong(),anyInt(),anyString(),anyString(),anyString(),anyString(),anyLong(),anyLong(),anyInt())).thenReturn(true);
@@ -38,7 +60,6 @@ class ExportTaskExecutorTest {
Thread.sleep(1200);
return new GenericXlsxExportRunner.Result("file.xlsx","exports/9/test.xlsx","application/xlsx",100,1);
});
ExportTaskExecutor executor=new ExportTaskExecutor(tasks,definitions,runner,states,mock(ExportStorage.class),new SimpleMeterRegistry(),5,1,30,7);
executor.execute(9L,1,"file-1");
@@ -47,17 +68,11 @@ class ExportTaskExecutorTest {
}
@Test void aLateFailureFromAnOldExecutionCannotRetryOrFailANewerVersionOwnedByTheSameInstance() {
ExportTaskRepository tasks=mock(ExportTaskRepository.class);
ExportDefinitionRegistry definitions=mock(ExportDefinitionRegistry.class);
GenericXlsxExportRunner runner=mock(GenericXlsxExportRunner.class);
ExportStateService states=mock(ExportStateService.class);
ExportStorage storage=mock(ExportStorage.class);
when(tasks.claim(9L,1,"file-1",300,30)).thenReturn(true);
FileExportTask first=task(1,"file-1"), newer=task(2,"file-1");
when(tasks.find(9L)).thenReturn(first,newer);
when(definitions.require("CUSTOMER_LIST")).thenReturn(new ExportDefinitionRegistry.Definition());
when(runner.run(eq(first),any(),any(),any())).thenThrow(new RuntimeException("late failure"));
ExportTaskExecutor executor=new ExportTaskExecutor(tasks,definitions,runner,states,storage,new SimpleMeterRegistry(),300,60,30,7);
executor.execute(9L,1,"file-1");
@@ -66,11 +81,6 @@ class ExportTaskExecutorTest {
}
@Test void deletesTheGeneratedFileWhenTheFinalDatabaseCommitThrows() {
ExportTaskRepository tasks=mock(ExportTaskRepository.class);
ExportDefinitionRegistry definitions=mock(ExportDefinitionRegistry.class);
GenericXlsxExportRunner runner=mock(GenericXlsxExportRunner.class);
ExportStateService states=mock(ExportStateService.class);
ExportStorage storage=mock(ExportStorage.class);
FileExportTask running=task(1,"file-1");
when(tasks.claim(9L,1,"file-1",300,30)).thenReturn(true);
when(tasks.find(9L)).thenReturn(running);
@@ -80,7 +90,6 @@ class ExportTaskExecutorTest {
when(tasks.succeed(anyLong(),anyInt(),anyString(),anyString(),anyString(),anyString(),anyLong(),anyLong(),anyInt()))
.thenThrow(new RuntimeException("database unavailable"));
when(states.retryAfterFailure(eq(running),eq("file-1"),intThat(value -> value >= 48 && value <= 72),anyString())).thenReturn(true);
ExportTaskExecutor executor=new ExportTaskExecutor(tasks,definitions,runner,states,storage,new SimpleMeterRegistry(),300,60,30,7);
executor.execute(9L,1,"file-1");
@@ -8,6 +8,12 @@ import com.baiyue.file.entity.FileExportTask;
import com.baiyue.file.repository.ExportTaskRepository;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import jakarta.annotation.Resource;
import com.baiyue.file.config.ExportDefinitionRegistry;
import java.util.Map;
@@ -18,19 +24,22 @@ 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.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@SpringJUnitConfig(classes = {ExportTaskService.class, ExportTaskServiceTest.TestConfig.class})
class ExportTaskServiceTest {
@MockBean private ExportTaskRepository repository;
@MockBean private ExportStorage storage;
@MockBean private ExportDefinitionRegistry registry;
@MockBean private ExportStateService states;
@MockBean private ExportMetrics metrics;
@Resource private ExportTaskService service;
@Test
void createsPersistedTaskOnlyWhenSourceExportPermissionIsPresent() {
ExportTaskRepository repository = mock(ExportTaskRepository.class);
ExportStateService states=mock(ExportStateService.class);
when(registry.require("CUSTOMER_LIST")).thenReturn(definition());
when(states.createConstrained(any(FileExportTask.class))).thenAnswer(invocation ->
new ExportStateService.CreationResult(invocation.getArgument(0), true));
ExportTaskService service = new ExportTaskService(repository, mock(ExportStorage.class),
new ObjectMapper(), registry(), states, mock(ExportMetrics.class));
var response = service.create(user(1L, Set.of("platform:customer:export")),
new CreateExportTaskRequest("CUSTOMER_LIST", "XLSX", "客户列表", Map.of("status", "ACTIVE")));
@@ -45,9 +54,7 @@ class ExportTaskServiceTest {
@Test
void refusesAnotherUsersTaskUnlessDownloadAllIsGranted() {
ExportTaskRepository repository = mock(ExportTaskRepository.class);
ExportTaskService service = new ExportTaskService(repository, mock(ExportStorage.class),
new ObjectMapper(), registry(), mock(ExportStateService.class), mock(ExportMetrics.class));
when(registry.require("CUSTOMER_LIST")).thenReturn(definition());
FileExportTask task = new FileExportTask();
task.setId(9L); task.setTenantId(0L); task.setAppCode("PLATFORM"); task.setRequestedBy(2L);
when(repository.find(9L)).thenReturn(task);
@@ -59,8 +66,7 @@ class ExportTaskServiceTest {
@Test
void rejectsParametersDeeperThanTwoLevels() {
ExportTaskService service = new ExportTaskService(mock(ExportTaskRepository.class), mock(ExportStorage.class),
new ObjectMapper(), registry(), mock(ExportStateService.class), mock(ExportMetrics.class));
when(registry.require("CUSTOMER_LIST")).thenReturn(definition());
var request = new CreateExportTaskRequest("CUSTOMER_LIST", "XLSX", "客户列表",
Map.of("level1", Map.of("level2", List.of(Map.of("level3", "value")))));
@@ -71,8 +77,13 @@ class ExportTaskServiceTest {
private AuthenticatedUser user(long id, Set<String> permissions) {
return new AuthenticatedUser(id, 0L, 0L, id, "user" + id, "PLATFORM", "WEB", permissions);
}
private ExportDefinitionRegistry registry(){
private ExportDefinitionRegistry.Definition definition(){
var definition=new ExportDefinitionRegistry.Definition();definition.setProviderService("iam-service");definition.setProviderPath("/internal/exports/v1/CUSTOMER_LIST/batches");definition.setRequiredPermission("platform:customer:export");definition.setAppScope("PLATFORM");definition.setMaxRows(200000);definition.setMaxAttempts(3);definition.setBatchSize(500);
ExportDefinitionRegistry registry=mock(ExportDefinitionRegistry.class);when(registry.require("CUSTOMER_LIST")).thenReturn(definition);return registry;
return definition;
}
@Configuration
static class TestConfig {
@Bean ObjectMapper objectMapper() { return new ObjectMapper(); }
}
}
@@ -11,6 +11,15 @@ import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.mockito.ArgumentCaptor;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.boot.test.mock.mockito.SpyBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import jakarta.annotation.Resource;
import java.nio.file.Files;
import java.nio.file.Path;
@@ -23,10 +32,21 @@ import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
@SpringJUnitConfig(classes = {GenericXlsxExportRunner.class, LocalExportStorage.class,
GenericXlsxExportRunnerTest.TestConfig.class})
class GenericXlsxExportRunnerTest {
@TempDir Path temp;
@TempDir static Path temp;
@MockBean private ExportProviderClient provider;
@SpyBean private LocalExportStorage storage;
@Resource private GenericXlsxExportRunner runner;
@DynamicPropertySource
static void properties(DynamicPropertyRegistry registry) {
registry.add("baiyue.file.storage-root", () -> temp.toString());
registry.add("baiyue.security.internal-service-token", () -> "internal-token");
registry.add("baiyue.file.export.task.max-file-size-mb", () -> 50);
}
@Test void writesRealTypedWorkbookAndCarriesSnapshotAcrossBatches() throws Exception {
ExportProviderClient provider=mock(ExportProviderClient.class); LocalExportStorage storage=new LocalExportStorage(temp.toString());storage.initialize();
List<ExportColumn> columns=List.of(new ExportColumn("text","文本",ExportColumnType.STRING,18),new ExportColumn("integer","整数",ExportColumnType.INTEGER,12),new ExportColumn("decimal","金额",ExportColumnType.DECIMAL,12),new ExportColumn("flag","布尔",ExportColumnType.BOOLEAN,10),new ExportColumn("date","日期",ExportColumnType.DATE,12),new ExportColumn("time","时间",ExportColumnType.DATETIME,20));
List<Map<String,Object>> firstRows=IntStream.range(0,100).mapToObj(index -> Map.<String,Object>of("text",index==0?"=SUM(1,2)":"row-"+index,"integer",12,"decimal","3.50","flag",true,"date","2026-08-03","time","2026-08-03T21:00:00")).toList();
var first=new ExportBatchResponse(1,"sha256:test","snapshot-9",columns,firstRows,"10",true,101L);
@@ -34,7 +54,7 @@ class GenericXlsxExportRunnerTest {
when(provider.batch(anyString(),eq("CUSTOMER_LIST"),any(),any())).thenReturn(ApiResponse.success(first),ApiResponse.success(second));
FileExportTask task=new FileExportTask();task.setId(9L);task.setTenantId(0L);task.setAppCode("PLATFORM");task.setRequestedBy(7L);task.setDispatchVersion(1);task.setExportType("CUSTOMER_LIST");task.setTaskName("客户列表");task.setParametersJson("{}");
var definition=new ExportDefinitionRegistry.Definition();definition.setMaxRows(200000);definition.setBatchSize(500);
var result=new GenericXlsxExportRunner(provider,storage,new ObjectMapper(),"internal-token",50).run(task,definition,new AtomicBoolean(),ignored->{ });
var result=runner.run(task,definition,new AtomicBoolean(),ignored->{ });
assertEquals(101,result.rows());
try(var workbook=new XSSFWorkbook(storage.load(result.objectKey()).getInputStream())){
var row=workbook.getSheetAt(0).getRow(1);assertEquals("'=SUM(1,2)",row.getCell(0).getStringCellValue());
@@ -47,8 +67,6 @@ class GenericXlsxExportRunnerTest {
}
@Test void deletesTheCompletedFileWhenReadingItsFinalSizeFails() throws Exception {
ExportProviderClient provider=mock(ExportProviderClient.class);
LocalExportStorage storage=spy(new LocalExportStorage(temp.toString()));storage.initialize();
var batch=new ExportBatchResponse(1,"sha256:test","snapshot-9",
List.of(new ExportColumn("text","文本",ExportColumnType.STRING,18)),
List.of(Map.of("text","row")),null,false,1L);
@@ -56,12 +74,20 @@ class GenericXlsxExportRunnerTest {
FileExportTask task=new FileExportTask();task.setId(9L);task.setTenantId(0L);task.setAppCode("PLATFORM");task.setRequestedBy(7L);task.setDispatchVersion(1);task.setExportType("CUSTOMER_LIST");task.setTaskName("客户列表");task.setParametersJson("{}");
var definition=new ExportDefinitionRegistry.Definition();definition.setMaxRows(200000);definition.setBatchSize(500);
doThrow(new RuntimeException("size unavailable")).when(storage).size(anyString());
long filesBefore;
try (var paths=Files.walk(temp.resolve("exports"))) {
filesBefore = paths.filter(Files::isRegularFile).count();
}
assertThrows(ExportFailure.class,() -> new GenericXlsxExportRunner(provider,storage,new ObjectMapper(),"internal-token",50)
.run(task,definition,new AtomicBoolean(),ignored->{ }));
assertThrows(ExportFailure.class,() -> runner.run(task,definition,new AtomicBoolean(),ignored->{ }));
try(var paths=Files.walk(temp.resolve("exports"))) {
assertEquals(0,paths.filter(Files::isRegularFile).count());
assertEquals(filesBefore, paths.filter(Files::isRegularFile).count());
}
}
@Configuration
static class TestConfig {
@Bean ObjectMapper objectMapper() { return new ObjectMapper(); }
}
}
@@ -6,6 +6,13 @@ import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import org.springframework.test.util.ReflectionTestUtils;
import jakarta.annotation.Resource;
import java.nio.file.Files;
import java.nio.file.Path;
@@ -16,14 +23,20 @@ import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
@DisplayName("本地企业资质文件存储功能")
@SpringJUnitConfig(LocalQualificationStorage.class)
class LocalQualificationStorageTest {
@TempDir
Path storageRoot;
static Path storageRoot;
@Resource private LocalQualificationStorage storage;
@DynamicPropertySource
static void properties(DynamicPropertyRegistry registry) {
registry.add("baiyue.file.storage-root", () -> storageRoot.toString());
}
@Test
void initializeVerifiesTheActualQualificationMountAndCleansUpItsProbe() throws Exception {
var storage = new LocalQualificationStorage(storageRoot.toString());
storage.initialize();
@@ -36,7 +49,6 @@ class LocalQualificationStorageTest {
@Test
void storeReturnsFileServiceUrlBackedByARealTenantFile() throws Exception {
var storage = new LocalQualificationStorage(storageRoot.toString());
storage.initialize();
byte[] png = {(byte) 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x01};
var upload = new MockMultipartFile("file", "license.png", "image/png", png);
@@ -53,7 +65,6 @@ class LocalQualificationStorageTest {
@Test
void storesPdfQualificationAfterVerifyingItsSignature() throws Exception {
var storage = new LocalQualificationStorage(storageRoot.toString());
storage.initialize();
var upload = new MockMultipartFile("file", "license.pdf", "application/pdf", "%PDF-1.4\ncontent".getBytes());
@@ -66,7 +77,6 @@ class LocalQualificationStorageTest {
@Test
void rejectsQualificationFilesLargerThanTheClientFiveMegabyteLimit() {
var storage = new LocalQualificationStorage(storageRoot.toString());
storage.initialize();
var upload = new MockMultipartFile("file", "oversized.pdf", "application/pdf",
new byte[5 * 1024 * 1024 + 1]);
@@ -79,7 +89,6 @@ class LocalQualificationStorageTest {
@Test
void deletesTheReplacedQualificationByItsValidatedDocumentUrl() {
var storage = new LocalQualificationStorage(storageRoot.toString());
storage.initialize();
var upload = new MockMultipartFile("file", "license.pdf", "application/pdf", "%PDF-1.4\ncontent".getBytes());
var stored = storage.store(10001L, upload);
@@ -91,7 +100,6 @@ class LocalQualificationStorageTest {
@Test
void doesNotDeleteAQualificationForAnUntrustedDocumentUrl() {
var storage = new LocalQualificationStorage(storageRoot.toString());
storage.initialize();
var upload = new MockMultipartFile("file", "license.pdf", "application/pdf", "%PDF-1.4\ncontent".getBytes());
var stored = storage.store(10001L, upload);
@@ -103,17 +111,17 @@ class LocalQualificationStorageTest {
@Test
void legacySeedPathCannotBePretendedToBeAStoredFile() {
var storage = new LocalQualificationStorage(storageRoot.toString());
assertThrows(BizException.class,
() -> storage.load(10001L, "demo-business-license.jpg"));
}
@Test
@DirtiesContext(methodMode = DirtiesContext.MethodMode.AFTER_METHOD)
void invalidStorageFailsFastDuringStartup() throws Exception {
Path invalidRoot = storageRoot.resolve("root-is-file");
Files.writeString(invalidRoot, "not-a-directory");
var storage = new LocalQualificationStorage(invalidRoot.toString());
ReflectionTestUtils.setField(storage, "configuredRoot", invalidRoot.toString());
IllegalStateException exception = assertThrows(IllegalStateException.class, storage::initialize);
@@ -122,14 +130,15 @@ class LocalQualificationStorageTest {
@Test
void writeFailureReturnsServiceUnavailableInsteadOfBusinessError() throws Exception {
var storage = new LocalQualificationStorage(storageRoot.toString());
storage.initialize();
Files.writeString(storageRoot.resolve("qualifications").resolve("10001"), "阻止创建租户目录");
long isolatedTenantId = 19999L;
Files.writeString(storageRoot.resolve("qualifications").resolve(String.valueOf(isolatedTenantId)),
"阻止创建租户目录");
byte[] png = {(byte) 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x01};
var upload = new MockMultipartFile("file", "license.png", "image/png", png);
BizException exception = assertThrows(BizException.class,
() -> storage.store(10001L, upload));
() -> storage.store(isolatedTenantId, upload));
assertEquals(ResultCode.SERVICE_UNAVAILABLE, exception.getResultCode());
}
@@ -4,8 +4,13 @@ import com.baiyue.file.client.WeightOcrClient;
import com.baiyue.file.entity.FileObjectEntity;
import com.baiyue.file.mapper.FileObjectMapper;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import jakarta.annotation.Resource;
import java.nio.file.Path;
@@ -13,17 +18,18 @@ import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;
@SpringJUnitConfig(MeasurementImageService.class)
@TestPropertySource(properties = "baiyue.security.internal-service-token=internal-token")
class MeasurementImageServiceEvidenceTest {
@MockBean private MeasurementImageStorage storage;
@MockBean private FileObjectMapper files;
@MockBean private WeightOcrClient ocr;
@Resource private MeasurementImageService service;
@Test
void 尺寸测量证据图片落库但不调用重量Ocr() {
MeasurementImageStorage storage = mock(MeasurementImageStorage.class);
FileObjectMapper files = mock(FileObjectMapper.class);
WeightOcrClient ocr = mock(WeightOcrClient.class);
MeasurementImageService service = new MeasurementImageService(storage, files, ocr, "internal-token");
void shouldHandleCase01() {
var image = new MockMultipartFile("file", "measure.png", MediaType.IMAGE_PNG_VALUE,
new byte[]{1, 2, 3});
when(storage.store(11L, 21L, image)).thenReturn(new MeasurementImageStorage.Stored(
@@ -43,11 +49,7 @@ class MeasurementImageServiceEvidenceTest {
}
@Test
void 尺寸测量证据图片不能通过重试入口触发重量Ocr() {
MeasurementImageStorage storage = mock(MeasurementImageStorage.class);
FileObjectMapper files = mock(FileObjectMapper.class);
WeightOcrClient ocr = mock(WeightOcrClient.class);
MeasurementImageService service = new MeasurementImageService(storage, files, ocr, "internal-token");
void shouldHandleCase02() {
FileObjectEntity object = new FileObjectEntity();
object.setId(41L);
object.setTenantId(11L);
@@ -6,8 +6,12 @@ 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.boot.test.mock.mockito.MockBean;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import jakarta.annotation.Resource;
import java.nio.file.Path;
@@ -17,11 +21,17 @@ 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.reset;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@SpringJUnitConfig(QualificationPackageService.class)
@TestPropertySource(properties = "baiyue.security.internal-service-token=internal-token")
class QualificationPackageServiceTest {
@MockBean private LocalQualificationStorage storage;
@MockBean private IamQualificationClient iamClient;
@Resource private QualificationPackageService service;
private static final long TENANT_ID = 10001L;
private static final String BUSINESS_URL =
"/api/merchant/v1/files/qualifications/11111111-1111-1111-1111-111111111111.pdf";
@@ -111,20 +121,15 @@ class QualificationPackageServiceTest {
ordered.verify(fixture.storage).deleteByDocumentUrl(TENANT_ID, oldLegalPersonUrl);
}
private static Fixture storedFixture() {
private 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");
private Fixture fixture() {
reset(storage, iamClient);
var businessLicense = new MockMultipartFile(
"businessLicense", "license.pdf", "application/pdf", "%PDF-1.4".getBytes());
var legalPersonId = new MockMultipartFile(