格式修改
This commit is contained in:
@@ -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);
|
||||
|
||||
+13
-6
@@ -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}));
|
||||
|
||||
+11
-6
@@ -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());
|
||||
}
|
||||
|
||||
+13
-11
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user