格式修改

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
@@ -3,7 +3,11 @@ package com.baiyue.firstleg.controller;
import com.baiyue.common.core.exception.BizException;
import com.baiyue.firstleg.dto.InboundOrderReference;
import com.baiyue.firstleg.service.InboundOrderCreationService;
import jakarta.annotation.Resource;
import org.junit.jupiter.api.Test;
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,13 +17,16 @@ import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@SpringJUnitConfig(InternalInboundOrderController.class)
@TestPropertySource(properties = "baiyue.security.internal-service-token=internal-token")
class InternalInboundOrderReferenceControllerTest {
private final InboundOrderCreationService service = mock(InboundOrderCreationService.class);
private final InternalInboundOrderController controller =
new InternalInboundOrderController(service, "internal-token");
@MockBean
private InboundOrderCreationService service;
@Resource
private InternalInboundOrderController controller;
@Test
void 使用内部令牌按租户批量查询订单入仓编码() {
void shouldHandleCase01() {
var request = new InboundOrderReference.BatchRequest(10001L, List.of(77L, 77L, 78L));
when(service.findReferences(10001L, List.of(77L, 78L))).thenReturn(List.of(
new InboundOrderReference.Item(77L, 88L, "RK-77", "WAIT_MEASURE")));
@@ -32,7 +39,7 @@ class InternalInboundOrderReferenceControllerTest {
}
@Test
void 错误内部令牌不能查询任意租户的入仓编码() {
void shouldHandleCase02() {
var request = new InboundOrderReference.BatchRequest(10001L, List.of(77L));
assertThatThrownBy(() -> controller.references("bad-token", request))
.isInstanceOf(BizException.class);
@@ -3,8 +3,11 @@ package com.baiyue.firstleg.controller;
import com.baiyue.common.core.exception.BizException;
import com.baiyue.common.security.AuthenticatedUser;
import com.baiyue.firstleg.service.MerchantInboundBillingService;
import jakarta.annotation.Resource;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import java.util.Set;
@@ -12,12 +15,15 @@ import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.Mockito.mock;
@SpringJUnitConfig(MerchantInboundBillingController.class)
class MerchantInboundBillingControllerContractTest {
private final MerchantInboundBillingController controller =
new MerchantInboundBillingController(mock(MerchantInboundBillingService.class));
@MockBean
private MerchantInboundBillingService service;
@Resource
private MerchantInboundBillingController controller;
@Test
void 客户费用查询复用订单查看权限且不提供确认接口() throws Exception {
void shouldHandleCase01() throws Exception {
var method = MerchantInboundBillingController.class.getDeclaredMethod(
"find", AuthenticatedUser.class, long.class);
assertThat(method.getAnnotation(PreAuthorize.class).value())
@@ -27,13 +33,13 @@ class MerchantInboundBillingControllerContractTest {
}
@Test
void 平台身份不能越权调用客户费用接口() {
void shouldHandleCase02() {
var platform = new AuthenticatedUser(1L, 0L, 0L, 1L,
"admin", "PLATFORM", "WEB", Set.of());
assertThatThrownBy(() -> controller.find(platform, 77L)).isInstanceOf(BizException.class);
}
@Test
void 商户测量图片复用订单查看权限() throws Exception {
void shouldHandleCase03() throws Exception {
var method = MerchantInboundBillingController.class.getDeclaredMethod(
"image", AuthenticatedUser.class, long.class, long.class);
assertThat(method.getAnnotation(PreAuthorize.class).value())
@@ -16,6 +16,9 @@ import org.apache.ibatis.session.Configuration;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import jakarta.annotation.Resource;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import java.util.Collection;
import java.util.List;
@@ -25,7 +28,16 @@ import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@SpringJUnitConfig(FbaPricingRepository.class)
class FbaPricingRepositoryRuleListTest {
@MockBean private CustomerChargeRuleMapper rules;
@MockBean private CarrierFbaRateMapper rates;
@MockBean private InboundPricingMapper pricing;
@MockBean private InboundFeeMapper fees;
@MockBean private InboundFeeItemMapper feeItems;
@MockBean private InboundPricingIdempotencyMapper idempotency;
@MockBean private InboundOutboxMapper outbox;
@Resource private FbaPricingRepository repository;
@BeforeAll
static void initializeEntityMetadata() {
TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new Configuration(), "test"),
@@ -57,11 +69,9 @@ class FbaPricingRepositoryRuleListTest {
@Test
void internalVersionCalculationKeepsTenantAndRuleCodeFilters() {
CustomerChargeRuleMapper rules = mock(CustomerChargeRuleMapper.class);
CustomerChargeRuleEntity current = new CustomerChargeRuleEntity();
current.setRuleVersion(3);
when(rules.selectList(org.mockito.ArgumentMatchers.any())).thenReturn(List.of(current));
FbaPricingRepository repository = repository(rules);
int result = repository.nextRuleVersion(10001L, "CR-1");
@@ -75,10 +85,8 @@ class FbaPricingRepositoryRuleListTest {
assertThat(result).isEqualTo(4);
}
private static Collection<Object> queryValues(Long tenantId, String ruleCode) {
CustomerChargeRuleMapper rules = mock(CustomerChargeRuleMapper.class);
private Collection<Object> queryValues(Long tenantId, String ruleCode) {
when(rules.selectList(org.mockito.ArgumentMatchers.any())).thenReturn(List.of());
FbaPricingRepository repository = repository(rules);
repository.listRules(tenantId, ruleCode);
@@ -90,10 +98,8 @@ class FbaPricingRepositoryRuleListTest {
return wrapper.getValue().getParamNameValuePairs().values();
}
private static Collection<Object> rateQueryValues(Long tenantId, String rateCode) {
CarrierFbaRateMapper rates = mock(CarrierFbaRateMapper.class);
private Collection<Object> rateQueryValues(Long tenantId, String rateCode) {
when(rates.selectList(org.mockito.ArgumentMatchers.any())).thenReturn(List.of());
FbaPricingRepository repository = repository(mock(CustomerChargeRuleMapper.class), rates);
repository.listRates(tenantId, rateCode);
@@ -105,14 +111,4 @@ class FbaPricingRepositoryRuleListTest {
return wrapper.getValue().getParamNameValuePairs().values();
}
private static FbaPricingRepository repository(CustomerChargeRuleMapper rules) {
return repository(rules, mock(CarrierFbaRateMapper.class));
}
private static FbaPricingRepository repository(
CustomerChargeRuleMapper rules, CarrierFbaRateMapper rates) {
return new FbaPricingRepository(rules, rates,
mock(InboundPricingMapper.class), mock(InboundFeeMapper.class), mock(InboundFeeItemMapper.class),
mock(InboundPricingIdempotencyMapper.class), mock(InboundOutboxMapper.class));
}
}
@@ -18,6 +18,9 @@ import com.baiyue.firstleg.mapper.InboundTransportSnapshotMapper;
import org.junit.jupiter.api.Test;
import org.apache.ibatis.builder.MapperBuilderAssistant;
import org.mockito.ArgumentCaptor;
import jakarta.annotation.Resource;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
@@ -26,22 +29,22 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.when;
@SpringJUnitConfig(InboundOrderRepository.class)
class InboundOrderRepositoryTest {
@MockBean private InboundOrderMapper orders;
@MockBean private InboundTransportSnapshotMapper transports;
@MockBean private InboundPalletAllocationMapper pallets;
@MockBean private InboundFileMapper files;
@MockBean private InboundInternalNoteMapper notes;
@MockBean private InboundStatusHistoryMapper histories;
@Resource private InboundOrderRepository repository;
@Test
@SuppressWarnings({"rawtypes", "unchecked"})
void shouldApplyTenantCompanyAndWarehouseFilters() {
TableInfoHelper.initTableInfo(
new MapperBuilderAssistant(new MybatisConfiguration(), "test"), InboundOrderEntity.class);
InboundOrderMapper orders = mock(InboundOrderMapper.class);
when(orders.selectPage(any(Page.class), any(Wrapper.class)))
.thenReturn(new Page<InboundOrderEntity>(1, 20));
InboundOrderRepository repository = new InboundOrderRepository(
orders,
mock(InboundTransportSnapshotMapper.class),
mock(InboundPalletAllocationMapper.class),
mock(InboundFileMapper.class),
mock(InboundInternalNoteMapper.class),
mock(InboundStatusHistoryMapper.class));
InboundOrderQuery query = new InboundOrderQuery(
null, null, null, null, null, null, null, null,
null, null, null, null, 20001L, 30001L, null, null, null, 1, 20);
@@ -69,8 +72,6 @@ class InboundOrderRepositoryTest {
TableInfoHelper.initTableInfo(
new MapperBuilderAssistant(new MybatisConfiguration(), "test"),
InboundPalletAllocationEntity.class);
InboundOrderMapper orders = mock(InboundOrderMapper.class);
InboundPalletAllocationMapper pallets = mock(InboundPalletAllocationMapper.class);
Page<InboundOrderEntity> page = new Page<>(1, 20);
InboundOrderEntity first = new InboundOrderEntity();
first.setId(1L);
@@ -84,13 +85,6 @@ class InboundOrderRepositoryTest {
allocation(1L, 1, "A0"),
allocation(1L, 2, "A2"),
allocation(2L, 1, "B1")));
InboundOrderRepository repository = new InboundOrderRepository(
orders,
mock(InboundTransportSnapshotMapper.class),
pallets,
mock(InboundFileMapper.class),
mock(InboundInternalNoteMapper.class),
mock(InboundStatusHistoryMapper.class));
var result = repository.page(10001L, new InboundOrderQuery(
null, null, null, null, null, null, null, null,
@@ -118,9 +112,6 @@ class InboundOrderRepositoryTest {
TableInfoHelper.initTableInfo(
new MapperBuilderAssistant(new MybatisConfiguration(), "test"),
InboundPalletAllocationEntity.class);
InboundOrderMapper orders = mock(InboundOrderMapper.class);
InboundTransportSnapshotMapper transports = mock(InboundTransportSnapshotMapper.class);
InboundPalletAllocationMapper pallets = mock(InboundPalletAllocationMapper.class);
Page<InboundOrderEntity> page = new Page<>(1, 20);
page.setRecords(java.util.List.of(order(1L, 10001L), order(2L, 10002L)));
when(orders.selectPage(any(Page.class), any(Wrapper.class))).thenReturn(page);
@@ -132,9 +123,6 @@ class InboundOrderRepositoryTest {
allocation(1L, 10001L, 1, "A1"),
allocation(2L, 10002L, 1, "B1"),
allocation(1L, 10002L, 2, "WRONG")));
InboundOrderRepository repository = new InboundOrderRepository(
orders, transports, pallets, mock(InboundFileMapper.class),
mock(InboundInternalNoteMapper.class), mock(InboundStatusHistoryMapper.class));
var result = repository.page(null, new InboundOrderQuery(
null, null, null, null, null, null, null, null,
@@ -3,8 +3,14 @@ package com.baiyue.firstleg.security;
import com.baiyue.common.core.api.ResultCode;
import com.baiyue.common.core.exception.BizException;
import com.baiyue.firstleg.config.MeasurementOpenApiProperties;
import jakarta.annotation.Resource;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
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.time.Clock;
import java.time.Instant;
@@ -13,19 +19,34 @@ import java.util.Map;
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.when;
@DisplayName("第三方测量请求签名校验")
@SpringJUnitConfig(classes = {MeasurementRequestAuthenticator.class,
MeasurementRequestAuthenticatorTest.TestConfig.class})
class MeasurementRequestAuthenticatorTest {
private static final Instant NOW = Instant.parse("2026-08-01T02:30:00Z");
private static final String SECRET = "test-secret-at-least-32-bytes-long";
@MockBean private NonceReplayGuard guard;
@Resource private MeasurementOpenApiProperties properties;
@Resource private MeasurementRequestAuthenticator authenticator;
@BeforeEach
void setUp() {
properties.setEnabled(true);
MeasurementOpenApiProperties.AppCredential credential = new MeasurementOpenApiProperties.AppCredential();
credential.setSecret(SECRET);
credential.setTenantId(10001L);
credential.setSubjectId(90001L);
credential.setWarehouseBindings(Map.of("CN-YW-01", 30001L));
properties.setApps(Map.of("device-provider", credential));
}
@Test
@DisplayName("合法签名按六行规范串校验并占用Nonce")
void shouldAcceptValidSignatureAndClaimNonce() {
MeasurementOpenApiProperties properties = properties();
NonceReplayGuard guard = (appKey, nonce, ttl) -> true;
MeasurementRequestAuthenticator authenticator = new MeasurementRequestAuthenticator(
properties, guard, Clock.fixed(NOW, ZoneOffset.UTC));
when(guard.claim(any(), any(), any())).thenReturn(true);
String body = "{\"eventNo\":\"M1\"}";
String timestamp = String.valueOf(NOW.toEpochMilli());
String canonical = MeasurementRequestAuthenticator.canonicalString("POST",
@@ -46,12 +67,10 @@ class MeasurementRequestAuthenticatorTest {
@DisplayName("非法签名返回统一未认证业务码且不占用Nonce")
void shouldRejectInvalidSignatureBeforeClaimingNonce() {
int[] claims = {0};
NonceReplayGuard guard = (appKey, nonce, ttl) -> {
when(guard.claim(any(), any(), any())).thenAnswer(invocation -> {
claims[0]++;
return true;
};
MeasurementRequestAuthenticator authenticator = new MeasurementRequestAuthenticator(
properties(), guard, Clock.fixed(NOW, ZoneOffset.UTC));
});
assertThatThrownBy(() -> authenticator.authenticate("POST", "/openapi/v1/inbound-measurements",
new MeasurementRequestHeaders("device-provider", String.valueOf(NOW.toEpochMilli()),
@@ -65,8 +84,7 @@ class MeasurementRequestAuthenticatorTest {
@Test
@DisplayName("相同应用重复Nonce被拒绝")
void shouldRejectReplayedNonce() {
MeasurementRequestAuthenticator authenticator = new MeasurementRequestAuthenticator(
properties(), (appKey, nonce, ttl) -> false, Clock.fixed(NOW, ZoneOffset.UTC));
when(guard.claim(any(), any(), any())).thenReturn(false);
String timestamp = String.valueOf(NOW.toEpochMilli());
String body = "{}";
String canonical = MeasurementRequestAuthenticator.canonicalString("POST",
@@ -81,15 +99,16 @@ class MeasurementRequestAuthenticatorTest {
.isEqualTo(ResultCode.CONFLICT));
}
private MeasurementOpenApiProperties properties() {
MeasurementOpenApiProperties properties = new MeasurementOpenApiProperties();
properties.setEnabled(true);
MeasurementOpenApiProperties.AppCredential credential = new MeasurementOpenApiProperties.AppCredential();
credential.setSecret(SECRET);
credential.setTenantId(10001L);
credential.setSubjectId(90001L);
credential.setWarehouseBindings(Map.of("CN-YW-01", 30001L));
properties.setApps(Map.of("device-provider", credential));
return properties;
@Configuration
static class TestConfig {
@Bean
MeasurementOpenApiProperties properties() {
return new MeasurementOpenApiProperties();
}
@Bean
Clock clock() {
return Clock.fixed(NOW, ZoneOffset.UTC);
}
}
}
@@ -3,7 +3,10 @@ package com.baiyue.firstleg.service;
import com.baiyue.firstleg.dto.FbaPricingDtos.FeeSnapshotResponse;
import com.baiyue.firstleg.entity.InboundOrderEntity;
import com.baiyue.firstleg.repository.InboundWorkflowRepository;
import jakarta.annotation.Resource;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.anyLong;
@@ -13,15 +16,15 @@ import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@SpringJUnitConfig(AutomaticInboundCompletionService.class)
class AutomaticInboundCompletionServiceTest {
private final FbaPricingService pricing = mock(FbaPricingService.class);
private final InboundWorkflowRepository workflow = mock(InboundWorkflowRepository.class);
private final InboundCompletionOrchestrator completion = mock(InboundCompletionOrchestrator.class);
private final AutomaticInboundCompletionService service =
new AutomaticInboundCompletionService(pricing, workflow, completion);
@MockBean private FbaPricingService pricing;
@MockBean private InboundWorkflowRepository workflow;
@MockBean private InboundCompletionOrchestrator completion;
@Resource private AutomaticInboundCompletionService service;
@Test
void 客服审核通过后自动计费发布并完成入库() {
void shouldHandleCase01() {
FeeSnapshotResponse fee = fee(501L);
when(pricing.latest(10001L, 88L)).thenReturn(fee);
when(workflow.findOwned(10001L, 88L)).thenReturn(
@@ -43,7 +46,7 @@ class AutomaticInboundCompletionServiceTest {
}
@Test
void 已完成入库的审核重放只返回最新费用() {
void shouldHandleCase02() {
FeeSnapshotResponse fee = fee(501L);
when(workflow.findOwned(10001L, 88L)).thenReturn(order("INBOUNDED", "SENT", 8));
when(pricing.latest(10001L, 88L)).thenReturn(fee);
@@ -58,7 +61,7 @@ class AutomaticInboundCompletionServiceTest {
}
@Test
void 中断在入库处理中时续跑完成而不重复计费() {
void shouldHandleCase03() {
FeeSnapshotResponse fee = fee(501L);
when(pricing.latest(10001L, 88L)).thenReturn(fee);
when(workflow.findOwned(10001L, 88L)).thenReturn(
@@ -4,8 +4,11 @@ import com.baiyue.common.core.exception.BizException;
import com.baiyue.firstleg.dto.FbaPricingDtos.CustomerRuleVersionRequest;
import com.baiyue.firstleg.entity.CustomerChargeRuleEntity;
import com.baiyue.firstleg.repository.FbaPricingRepository;
import jakarta.annotation.Resource;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@@ -20,18 +23,22 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@DisplayName("FBA计费基础资料版本")
@SpringJUnitConfig(FbaMasterDataService.class)
class FbaMasterDataServiceTest {
private static final LocalDateTime AUGUST = LocalDateTime.of(2026, 8, 1, 0, 0);
@MockBean
private FbaPricingRepository repository;
@Resource
private FbaMasterDataService service;
@Test
@DisplayName("平台列表不传企业时委托仓储查询全部企业规则")
void listsRulesAcrossTenantsWithoutTenantFilter() {
FbaPricingRepository repository = mock(FbaPricingRepository.class);
List<CustomerChargeRuleEntity> expected = List.of(activeRule(AUGUST, null));
when(repository.listRules(null, "CR-1")).thenReturn(expected);
List<CustomerChargeRuleEntity> result =
new FbaMasterDataService(repository).listRules(null, "CR-1");
List<CustomerChargeRuleEntity> result = service.listRules(null, "CR-1");
assertThat(result).isSameAs(expected);
verify(repository).listRules(null, "CR-1");
@@ -40,7 +47,6 @@ class FbaMasterDataServiceTest {
@Test
@DisplayName("半开有效期首尾相接不视为重叠")
void acceptsAdjacentHalfOpenPeriods() {
FbaPricingRepository repository = mock(FbaPricingRepository.class);
CustomerChargeRuleEntity old = activeRule(AUGUST.minusMonths(1), AUGUST);
when(repository.listRules(1L, null)).thenReturn(List.of(old));
when(repository.listRules(1L, "CR-1")).thenReturn(List.of(old));
@@ -48,8 +54,8 @@ class FbaMasterDataServiceTest {
doAnswer(invocation -> { ((CustomerChargeRuleEntity) invocation.getArgument(0)).setId(10L); return null; })
.when(repository).insertRule(any());
CustomerChargeRuleEntity created = new FbaMasterDataService(repository)
.createRuleVersion(1L, 2L, request(AUGUST, AUGUST.plusMonths(1)));
CustomerChargeRuleEntity created = service.createRuleVersion(
1L, 2L, request(AUGUST, AUGUST.plusMonths(1)));
assertThat(created.getId()).isEqualTo(10L);
assertThat(created.getRuleVersion()).isEqualTo(2);
@@ -58,12 +64,11 @@ class FbaMasterDataServiceTest {
@Test
@DisplayName("任意重叠有效期在保存阶段拒绝避免多规则匹配")
void rejectsOverlappingPeriods() {
FbaPricingRepository repository = mock(FbaPricingRepository.class);
when(repository.listRules(1L, null)).thenReturn(List.of(
activeRule(AUGUST, AUGUST.plusMonths(1))));
assertThatThrownBy(() -> new FbaMasterDataService(repository)
.createRuleVersion(1L, 2L, request(AUGUST.plusDays(1), AUGUST.plusMonths(2))))
assertThatThrownBy(() -> service.createRuleVersion(
1L, 2L, request(AUGUST.plusDays(1), AUGUST.plusMonths(2))))
.isInstanceOf(BizException.class)
.hasMessageContaining("有效期重叠");
}
@@ -10,9 +10,14 @@ import com.baiyue.firstleg.entity.InboundFeeItemEntity;
import com.baiyue.firstleg.entity.InboundPricingEntity;
import com.baiyue.firstleg.entity.InboundPricingIdempotencyEntity;
import com.baiyue.firstleg.repository.FbaPricingRepository;
import jakarta.annotation.Resource;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
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.math.BigDecimal;
import java.time.Clock;
@@ -35,16 +40,19 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@DisplayName("FBA费用状态机")
@SpringJUnitConfig(classes = {FbaPricingService.class, FbaPricingServiceTest.TestConfig.class})
class FbaPricingServiceTest {
private static final long TENANT = 10001L;
private static final long OPERATOR = 20001L;
private static final long INBOUND = 30001L;
private static final LocalDateTime NOW = LocalDateTime.of(2026, 8, 3, 10, 0);
@MockBean private FbaPricingRepository repository;
@Resource private FbaPricingService service;
@Test
@DisplayName("包税KG按配置系数取体积重较大值并按规则向上舍入")
void calculatesTaxIncludedKgWithConfiguredFactorAndPrecision() {
FbaPricingRepository repository = readyRepository();
readyRepository();
InboundPricingEntity inbound = inbound("100.000", "1.0001");
CustomerChargeRuleEntity rule = rule("KG", "167.000000", 3, "CEILING");
CarrierFbaRateEntity rate = rate("KG", "2.345678", "10.00");
@@ -52,7 +60,7 @@ class FbaPricingServiceTest {
when(repository.matchRules(inbound, NOW)).thenReturn(List.of(rule));
when(repository.matchRates(inbound, "KG", NOW)).thenReturn(List.of(rate));
FeeSnapshotResponse response = service(repository).calculate(TENANT, OPERATOR, INBOUND, NOW, "calc-1");
FeeSnapshotResponse response = service.calculate(TENANT, OPERATOR, INBOUND, NOW, "calc-1");
assertThat(response.convertedQuantity()).isEqualByComparingTo("167.017");
assertThat(response.chargeableQuantity()).isEqualByComparingTo("167.017");
@@ -64,7 +72,7 @@ class FbaPricingServiceTest {
@Test
@DisplayName("不包税CBM按配置系数折算且精度边界向上取整")
void calculatesTaxExcludedCbmAtRoundingBoundary() {
FbaPricingRepository repository = readyRepository();
readyRepository();
InboundPricingEntity inbound = inbound("363.001", "0.5000");
CustomerChargeRuleEntity rule = rule("CBM", "363.000000", 4, "CEILING");
CarrierFbaRateEntity rate = rate("CBM", "100.000000", "0.00");
@@ -72,7 +80,7 @@ class FbaPricingServiceTest {
when(repository.matchRules(inbound, NOW)).thenReturn(List.of(rule));
when(repository.matchRates(inbound, "CBM", NOW)).thenReturn(List.of(rate));
FeeSnapshotResponse response = service(repository).calculate(TENANT, OPERATOR, INBOUND, NOW, "calc-2");
FeeSnapshotResponse response = service.calculate(TENANT, OPERATOR, INBOUND, NOW, "calc-2");
assertThat(response.convertedQuantity()).isEqualByComparingTo("1.0001");
assertThat(response.chargeableQuantity()).isEqualByComparingTo("1.0001");
@@ -82,12 +90,12 @@ class FbaPricingServiceTest {
@Test
@DisplayName("审核未通过的测量快照禁止计费")
void rejectsUnapprovedMeasurementSnapshot() {
FbaPricingRepository repository = readyRepository();
readyRepository();
InboundPricingEntity inbound = inbound("10", "1");
inbound.setMeasureReviewStatus("REJECTED");
when(repository.findInbound(TENANT, INBOUND)).thenReturn(inbound);
assertThatThrownBy(() -> service(repository).calculate(TENANT, OPERATOR, INBOUND, NOW, "calc-3"))
assertThatThrownBy(() -> service.calculate(TENANT, OPERATOR, INBOUND, NOW, "calc-3"))
.isInstanceOf(BizException.class)
.hasMessageContaining("审核通过");
verify(repository, never()).matchRules(any(), any());
@@ -96,25 +104,25 @@ class FbaPricingServiceTest {
@Test
@DisplayName("无客户规则和多客户规则都明确拒绝")
void rejectsMissingAndAmbiguousRules() {
FbaPricingRepository missing = readyRepository();
readyRepository();
InboundPricingEntity inbound = inbound("10", "1");
when(missing.findInbound(TENANT, INBOUND)).thenReturn(inbound);
when(missing.matchRules(inbound, NOW)).thenReturn(List.of());
assertThatThrownBy(() -> service(missing).calculate(TENANT, OPERATOR, INBOUND, NOW, "none"))
when(repository.findInbound(TENANT, INBOUND)).thenReturn(inbound);
when(repository.matchRules(inbound, NOW)).thenReturn(List.of());
assertThatThrownBy(() -> service.calculate(TENANT, OPERATOR, INBOUND, NOW, "none"))
.isInstanceOf(BizException.class).hasMessageContaining("未匹配到有效客户计费规则");
FbaPricingRepository ambiguous = readyRepository();
when(ambiguous.findInbound(TENANT, INBOUND)).thenReturn(inbound);
when(ambiguous.matchRules(inbound, NOW)).thenReturn(List.of(
org.mockito.Mockito.reset(repository);
readyRepository();
when(repository.findInbound(TENANT, INBOUND)).thenReturn(inbound);
when(repository.matchRules(inbound, NOW)).thenReturn(List.of(
rule("KG", "167", 3, "CEILING"), rule("KG", "168", 3, "CEILING")));
assertThatThrownBy(() -> service(ambiguous).calculate(TENANT, OPERATOR, INBOUND, NOW, "many"))
assertThatThrownBy(() -> service.calculate(TENANT, OPERATOR, INBOUND, NOW, "many"))
.isInstanceOf(BizException.class).hasMessageContaining("多条有效客户计费规则");
}
@Test
@DisplayName("同一幂等键重复试算直接返回原费用快照")
void returnsOriginalSnapshotForRepeatedCalculation() {
FbaPricingRepository repository = mock(FbaPricingRepository.class);
InboundPricingIdempotencyEntity completed = completed("FEE_CALCULATE", 501L);
when(repository.findIdempotency(TENANT, OPERATOR, "FEE_CALCULATE", "same")).thenReturn(completed);
when(repository.findFee(TENANT, 501L)).thenReturn(savedFee("CALCULATED"));
@@ -122,7 +130,7 @@ class FbaPricingServiceTest {
InboundFeeItemEntity item = new InboundFeeItemEntity(); item.setUnitPrice(new BigDecimal("2.00"));
when(repository.findBaseItem(TENANT, 501L)).thenReturn(item);
FeeSnapshotResponse response = service(repository).calculate(TENANT, OPERATOR, INBOUND, NOW, "same");
FeeSnapshotResponse response = service.calculate(TENANT, OPERATOR, INBOUND, NOW, "same");
assertThat(response.feeId()).isEqualTo(501L);
verify(repository, never()).matchRules(any(), any());
@@ -132,7 +140,6 @@ class FbaPricingServiceTest {
@Test
@DisplayName("重复发送只返回首次结果且不会重复写Outbox")
void sendIsIdempotentAndDoesNotDuplicateOutbox() {
FbaPricingRepository repository = mock(FbaPricingRepository.class);
InboundPricingEntity inbound = inbound("10", "1");
InboundFeeEntity confirmed = savedFee("CONFIRMED");
when(repository.findInbound(TENANT, INBOUND)).thenReturn(inbound);
@@ -148,7 +155,6 @@ class FbaPricingServiceTest {
doAnswer(invocation -> { ((InboundPricingIdempotencyEntity) invocation.getArgument(0)).setId(900L); return null; })
.when(repository).insertIdempotency(any());
FbaPricingService service = service(repository);
service.send(TENANT, OPERATOR, INBOUND, "send-once");
service.send(TENANT, OPERATOR, INBOUND, "send-once");
@@ -159,12 +165,11 @@ class FbaPricingServiceTest {
@Test
@DisplayName("同一幂等键用于不同试算时点会冲突")
void rejectsReusedIdempotencyKeyWithDifferentRequest() {
FbaPricingRepository repository = mock(FbaPricingRepository.class);
InboundPricingIdempotencyEntity completed = completed("FEE_CALCULATE", 501L);
completed.setRequestHash("different");
when(repository.findIdempotency(TENANT, OPERATOR, "FEE_CALCULATE", "same")).thenReturn(completed);
assertThatThrownBy(() -> service(repository).calculate(TENANT, OPERATOR, INBOUND, NOW, "same"))
assertThatThrownBy(() -> service.calculate(TENANT, OPERATOR, INBOUND, NOW, "same"))
.isInstanceOf(BizException.class)
.extracting(exception -> ((BizException) exception).getResultCode())
.isEqualTo(ResultCode.CONFLICT);
@@ -173,7 +178,6 @@ class FbaPricingServiceTest {
@Test
@DisplayName("已发布费用可生成新版本且不回退已入库状态")
void adjustsPublishedFeeWithoutRollingBackInbound() {
FbaPricingRepository repository = mock(FbaPricingRepository.class);
InboundPricingEntity inbound = inbound("10", "1");
inbound.setProcessStatus("INBOUNDED");
inbound.setFeeStatus("SENT");
@@ -195,7 +199,7 @@ class FbaPricingServiceTest {
when(repository.completeIdempotency(any(), eq(502L), eq(NOW))).thenReturn(1);
when(repository.findFee(TENANT, 502L)).thenAnswer(invocation -> inserted.get());
FeeSnapshotResponse response = service(repository).adjustPublished(TENANT, OPERATOR, INBOUND,
FeeSnapshotResponse response = service.adjustPublished(TENANT, OPERATOR, INBOUND,
new com.baiyue.firstleg.dto.FbaPricingDtos.AdjustFeeRequest(
new BigDecimal("100.00"), new BigDecimal("20.00"), "客服修正"), "adjust-1");
@@ -208,8 +212,7 @@ class FbaPricingServiceTest {
verify(repository).insertOutbox(any());
}
private FbaPricingRepository readyRepository() {
FbaPricingRepository repository = mock(FbaPricingRepository.class);
private void readyRepository() {
when(repository.findIdempotency(anyLong(), anyLong(), any(), any())).thenReturn(null);
doAnswer(invocation -> { ((InboundPricingIdempotencyEntity) invocation.getArgument(0)).setId(900L); return null; })
.when(repository).insertIdempotency(any());
@@ -218,12 +221,14 @@ class FbaPricingServiceTest {
when(repository.nextFeeVersion(TENANT, INBOUND)).thenReturn(1);
when(repository.markInboundCalculated(eq(TENANT), eq(INBOUND), any(), any())).thenReturn(1);
when(repository.completeIdempotency(any(), eq(501L), eq(NOW))).thenReturn(1);
return repository;
}
private FbaPricingService service(FbaPricingRepository repository) {
return new FbaPricingService(repository,
Clock.fixed(Instant.parse("2026-08-03T02:00:00Z"), ZoneId.of("Asia/Shanghai")));
@Configuration
static class TestConfig {
@Bean
Clock clock() {
return Clock.fixed(Instant.parse("2026-08-03T02:00:00Z"), ZoneId.of("Asia/Shanghai"));
}
}
private InboundPricingEntity inbound(String weight, String volume) {
@@ -6,9 +6,12 @@ import com.baiyue.common.core.exception.BizException;
import com.baiyue.firstleg.dto.FulfillmentTaskQuery;
import com.baiyue.firstleg.entity.FulfillmentTaskEntity;
import com.baiyue.firstleg.repository.FulfillmentTaskRepository;
import jakarta.annotation.Resource;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import java.util.List;
@@ -19,24 +22,24 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@DisplayName("履约任务服务")
@SpringJUnitConfig(FulfillmentTaskService.class)
class FulfillmentTaskServiceTest {
@MockBean private FulfillmentTaskRepository repository;
@Resource private FulfillmentTaskService service;
@Test
@DisplayName("列表查询保留租户和分页条件")
void shouldPreserveTenantAndPaginationInPageQuery() {
FulfillmentTaskRepository repository = mock(FulfillmentTaskRepository.class);
FulfillmentTaskQuery query = new FulfillmentTaskQuery(null, null, null, 1, 20);
PageResult<FulfillmentTaskEntity> expected = new PageResult<>(List.of(), 1, 20, 0, 0);
when(repository.page(10001L, query)).thenReturn(expected);
assertThat(service(repository).page(10001L, query)).isSameAs(expected);
assertThat(service.page(10001L, query)).isSameAs(expected);
verify(repository).page(10001L, query);
}
@Test
@DisplayName("详情和按订单查询不存在时返回404")
void shouldReturnNotFoundForMissingTaskAndOrder() {
FulfillmentTaskRepository repository = mock(FulfillmentTaskRepository.class);
FulfillmentTaskService service = service(repository);
assertThatThrownBy(() -> service.detail(10001L, 1L))
.isInstanceOf(BizException.class)
@@ -51,20 +54,14 @@ class FulfillmentTaskServiceTest {
@Test
@DisplayName("查询分配历史前先校验任务归属租户")
void shouldVerifyTaskTenantBeforeLoadingAssignmentHistory() {
FulfillmentTaskRepository repository = mock(FulfillmentTaskRepository.class);
FulfillmentTaskEntity task = new FulfillmentTaskEntity();
when(repository.findOwned(10001L, 1L)).thenReturn(task);
PageResult<com.baiyue.firstleg.entity.AssignmentHistoryEntity> expected =
new PageResult<>(List.of(), 1, 20, 0, 0);
when(repository.pageHistory(10001L, 1L, 1, 20)).thenReturn(expected);
assertThat(service(repository).assignmentHistory(10001L, 1L, 1, 20)).isSameAs(expected);
assertThat(service.assignmentHistory(10001L, 1L, 1, 20)).isSameAs(expected);
verify(repository).findOwned(10001L, 1L);
}
private FulfillmentTaskService service(FulfillmentTaskRepository repository) {
FulfillmentTaskService service = new FulfillmentTaskService();
ReflectionTestUtils.setField(service, "repository", repository);
return service;
}
}
@@ -5,8 +5,11 @@ import com.baiyue.common.core.exception.BizException;
import com.baiyue.firstleg.entity.InboundFeeEntity;
import com.baiyue.firstleg.entity.InboundOrderEntity;
import com.baiyue.firstleg.repository.InboundWorkflowRepository;
import jakarta.annotation.Resource;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
@@ -19,7 +22,14 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@DisplayName("发送费用并入库状态编排")
@SpringJUnitConfig(InboundCompletionOrchestrator.class)
class InboundCompletionOrchestratorTest {
@MockBean
private InboundWorkflowRepository repository;
@MockBean
private InboundIdempotencyService idempotency;
@Resource
private InboundCompletionOrchestrator service;
@Test
@DisplayName("审核通过且费用和库位引用已准备时进入INBOUNDING")
void shouldBeginInboundAfterAllGuardsPass() {
@@ -104,8 +114,6 @@ class InboundCompletionOrchestratorTest {
}
private Fixture fixture(String reviewStatus, boolean feeRelease) {
InboundWorkflowRepository repository = mock(InboundWorkflowRepository.class);
InboundIdempotencyService idempotency = mock(InboundIdempotencyService.class);
when(idempotency.begin(anyLong(), anyLong(), anyLong(), anyString(), anyString(), anyString()))
.thenReturn(new InboundIdempotencyService.Context(1L, false));
InboundOrderEntity order = new InboundOrderEntity();
@@ -117,7 +125,7 @@ class InboundCompletionOrchestratorTest {
when(repository.findOwned(10001L, 10L)).thenReturn(order);
when(repository.findConfirmedFee(10001L, 10L, 30L)).thenReturn(fee);
when(repository.countPreparedAllocations(10001L, 10L)).thenReturn(1L);
return new Fixture(new InboundCompletionOrchestrator(repository, idempotency), repository, order);
return new Fixture(service, repository, order);
}
private record Fixture(InboundCompletionOrchestrator service,
@@ -4,9 +4,12 @@ import com.baiyue.common.core.api.ResultCode;
import com.baiyue.common.core.exception.BizException;
import com.baiyue.firstleg.entity.InboundIdempotencyEntity;
import com.baiyue.firstleg.repository.InboundWorkflowRepository;
import jakarta.annotation.Resource;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
@@ -17,16 +20,19 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@DisplayName("入库写操作幂等边界")
@SpringJUnitConfig(InboundIdempotencyService.class)
class InboundIdempotencyServiceTest {
@MockBean
private InboundWorkflowRepository repository;
@Resource
private InboundIdempotencyService service;
@Test
@DisplayName("同键同请求成功记录可重放")
void shouldReplayCompletedRequest() {
InboundWorkflowRepository repository = mock(InboundWorkflowRepository.class);
doThrow(new DuplicateKeyException("duplicate")).when(repository).insertIdempotency(any());
InboundIdempotencyEntity existing = existing("SUCCESS");
when(repository.findIdempotency(10001L, "MEASURE_APPROVE", 9L, "key-1"))
.thenReturn(existing);
InboundIdempotencyService service = new InboundIdempotencyService(repository);
InboundIdempotencyService.Context context = service.begin(
10001L, 9L, 10L, "MEASURE_APPROVE", "key-1", "10|20|3|");
@@ -37,13 +43,11 @@ class InboundIdempotencyServiceTest {
@Test
@DisplayName("同键不同请求返回冲突")
void shouldRejectDifferentRequestForSameKey() {
InboundWorkflowRepository repository = mock(InboundWorkflowRepository.class);
doThrow(new DuplicateKeyException("duplicate")).when(repository).insertIdempotency(any());
InboundIdempotencyEntity existing = existing("SUCCESS");
existing.setRequestHash("different-hash");
when(repository.findIdempotency(10001L, "MEASURE_APPROVE", 9L, "key-1"))
.thenReturn(existing);
InboundIdempotencyService service = new InboundIdempotencyService(repository);
assertThatThrownBy(() -> service.begin(
10001L, 9L, 10L, "MEASURE_APPROVE", "key-1", "10|20|4|"))
@@ -55,9 +59,7 @@ class InboundIdempotencyServiceTest {
@Test
@DisplayName("新请求完成时只允许PROCESSING条件更新一次")
void shouldCompleteProcessingRecordOnce() {
InboundWorkflowRepository repository = mock(InboundWorkflowRepository.class);
when(repository.completeIdempotency(1L, "{\"inboundId\":\"10\"}")).thenReturn(1);
InboundIdempotencyService service = new InboundIdempotencyService(repository);
service.complete(new InboundIdempotencyService.Context(1L, false), 10L);
@@ -13,6 +13,8 @@ import com.baiyue.firstleg.security.AuthenticatedMeasurementApp;
import com.baiyue.firstleg.security.MeasurementRequestAuthenticator;
import com.baiyue.firstleg.security.MeasurementRequestHeaders;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.annotation.Resource;
import jakarta.validation.Validator;
import jakarta.validation.Validation;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
@@ -20,6 +22,11 @@ import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.TransactionCallback;
import org.springframework.transaction.support.TransactionTemplate;
import org.springframework.mock.web.MockMultipartFile;
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.time.OffsetDateTime;
import java.math.BigDecimal;
@@ -41,6 +48,9 @@ import static org.mockito.Mockito.doThrow;
import org.mockito.ArgumentCaptor;
@DisplayName("第三方测量开放接口服务")
@SpringJUnitConfig(classes = {InboundMeasurementOpenApiService.class,
InboundMeasurementOpenApiServiceTest.TestConfig.class})
@TestPropertySource(properties = "baiyue.security.internal-service-token=internal-token")
class InboundMeasurementOpenApiServiceTest {
private static final String BODY = """
{"eventNo":"M202608010001","warehouseCode":"CN-YW-01","inboundCode":"RK202608010001",
@@ -50,6 +60,14 @@ class InboundMeasurementOpenApiServiceTest {
"boxSpecification":"50.2x40.1x30cm"}
""";
private static final byte[] IMAGE = "image-content".getBytes(java.nio.charset.StandardCharsets.UTF_8);
@MockBean private InboundMeasurementOpenApiRepository repository;
@MockBean private MeasurementRequestAuthenticator authenticator;
@MockBean private ThirdPartyCallLogService logs;
@MockBean private TransactionTemplate transactions;
@MockBean private OrderCargoClient orderCargo;
@MockBean private MeasurementImageClient images;
@Resource private ObjectMapper objectMapper;
@Resource private InboundMeasurementOpenApiService service;
@Test
@DisplayName("回调只写批次明细和幂等结果,不存在入库主单update入口")
@@ -85,12 +103,11 @@ class InboundMeasurementOpenApiServiceTest {
@DisplayName("重复Idempotency-Key且报文一致时返回首次结果,不重复写测量")
void shouldReturnFirstResultForDuplicateCallback() throws Exception {
Fixture fixture = fixture();
ObjectMapper mapper = objectMapper();
EventResponse first = new EventResponse("M202608010001", "RK202608010001", true,
"RECEIVED", OffsetDateTime.parse("2026-08-01T02:30:00Z"));
ThirdPartyCallLogEntity existing = new ThirdPartyCallLogEntity();
existing.setRequestHash(requestHash());
existing.setResultJson(mapper.writeValueAsString(first));
existing.setResultJson(objectMapper.writeValueAsString(first));
existing.setProcessingStatus("ACCEPTED");
when(fixture.logs.claimIdempotency(1L)).thenReturn(false);
when(fixture.logs.findIdempotencyOwner(10001L, "device-provider", "idem-1")).thenReturn(existing);
@@ -137,12 +154,6 @@ class InboundMeasurementOpenApiServiceTest {
}
private Fixture fixture() {
InboundMeasurementOpenApiRepository repository = mock(InboundMeasurementOpenApiRepository.class);
MeasurementRequestAuthenticator authenticator = mock(MeasurementRequestAuthenticator.class);
ThirdPartyCallLogService logs = mock(ThirdPartyCallLogService.class);
TransactionTemplate transactions = mock(TransactionTemplate.class);
OrderCargoClient orderCargo = mock(OrderCargoClient.class);
MeasurementImageClient images = mock(MeasurementImageClient.class);
when(logs.start(anyString(), anyString(), any(), anyString(), anyString())).thenReturn(1L);
when(logs.claimIdempotency(1L)).thenReturn(true);
when(authenticator.authenticate(anyString(), anyString(), any(), anyString(), anyBoolean()))
@@ -161,14 +172,20 @@ class InboundMeasurementOpenApiServiceTest {
TransactionCallback<EventResponse> callback = invocation.getArgument(0);
return callback.doInTransaction(mock(TransactionStatus.class));
});
InboundMeasurementOpenApiService service = new InboundMeasurementOpenApiService(repository,
authenticator, logs, orderCargo, images, "internal-token", objectMapper(),
Validation.buildDefaultValidatorFactory().getValidator(), transactions);
return new Fixture(repository, logs, orderCargo, images, service);
}
private ObjectMapper objectMapper() {
return new ObjectMapper().findAndRegisterModules();
@Configuration
static class TestConfig {
@Bean
ObjectMapper objectMapper() {
return new ObjectMapper().findAndRegisterModules();
}
@Bean
Validator validator() {
return Validation.buildDefaultValidatorFactory().getValidator();
}
}
private InboundOrderLookupEntity inbound() {
@@ -9,10 +9,13 @@ import com.baiyue.firstleg.entity.InboundOrderEntity;
import com.baiyue.firstleg.entity.InboundWeightResultEntity;
import com.baiyue.firstleg.entity.InboundWeightRevisionEntity;
import com.baiyue.firstleg.repository.InboundMeasurementRepository;
import jakarta.annotation.Resource;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import java.math.BigDecimal;
import java.util.List;
@@ -26,15 +29,11 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@DisplayName("平台入库测量应用服务")
@SpringJUnitConfig(InboundMeasurementService.class)
class InboundMeasurementServiceTest {
private InboundMeasurementRepository repository;
private InboundMeasurementService service;
@BeforeEach
void setUp() {
repository = mock(InboundMeasurementRepository.class);
service = new InboundMeasurementService(repository, mock(MeasurementBatchService.class));
}
@MockBean private InboundMeasurementRepository repository;
@MockBean private MeasurementBatchService batchService;
@Resource private InboundMeasurementService service;
@Test
void correctingWeightKeepsOcrOriginalAndWritesImmutableRevision() {
@@ -9,7 +9,10 @@ import com.baiyue.firstleg.dto.InboundOrderQuery;
import com.baiyue.firstleg.dto.InboundOrderSummary;
import com.baiyue.firstleg.entity.InboundOrderEntity;
import com.baiyue.firstleg.repository.InboundOrderRepository;
import jakarta.annotation.Resource;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import java.time.LocalDateTime;
import java.util.List;
@@ -20,15 +23,20 @@ import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@SpringJUnitConfig(InboundOrderQueryService.class)
class InboundOrderQueryServiceTest {
@MockBean
private InboundOrderRepository repository;
@Resource
private InboundOrderQueryService service;
@Test
void shouldQueryAllPlatformTenantsWhenTenantFilterIsMissing() {
InboundOrderRepository repository = mock(InboundOrderRepository.class);
InboundOrderQuery query = query(null, null, null, null);
PageResult<InboundOrderSummary> expected = new PageResult<>(List.of(), 1, 20, 0, 0);
when(repository.page(null, query)).thenReturn(expected);
assertThat(new InboundOrderQueryService(repository).page(null, query)).isSameAs(expected);
assertThat(service.page(null, query)).isSameAs(expected);
verify(repository).page(null, query);
}
@@ -46,9 +54,8 @@ class InboundOrderQueryServiceTest {
@Test
void shouldRejectConflictingTabAndStatus() {
InboundOrderRepository repository = mock(InboundOrderRepository.class);
InboundOrderQuery query = query("WAIT_MEASURE", InboundListTab.WAIT_MEASURE, null, null);
assertThatThrownBy(() -> new InboundOrderQueryService(repository).page(10001L, query))
assertThatThrownBy(() -> service.page(10001L, query))
.isInstanceOf(BizException.class)
.extracting(exception -> ((BizException) exception).getResultCode())
.isEqualTo(ResultCode.BAD_REQUEST);
@@ -56,31 +63,28 @@ class InboundOrderQueryServiceTest {
@Test
void shouldRejectReversedArrivalRange() {
InboundOrderRepository repository = mock(InboundOrderRepository.class);
LocalDateTime now = LocalDateTime.now();
assertThatThrownBy(() -> new InboundOrderQueryService(repository)
.page(10001L, query(null, null, now, now.minusDays(1))))
assertThatThrownBy(() -> service.page(
10001L, query(null, null, now, now.minusDays(1))))
.isInstanceOf(BizException.class);
}
@Test
void shouldLoadDetailOnlyAfterTenantOwnedOrderExists() {
InboundOrderRepository repository = mock(InboundOrderRepository.class);
InboundOrderEntity order = new InboundOrderEntity();
order.setId(9L);
InboundOrderDetail detail = new InboundOrderDetail(null, null, List.of(), List.of(), List.of(), List.of());
when(repository.findOwned(10001L, 9L)).thenReturn(order);
when(repository.detail(10001L, order)).thenReturn(detail);
assertThat(new InboundOrderQueryService(repository).detail(10001L, 9L)).isSameAs(detail);
assertThat(service.detail(10001L, 9L)).isSameAs(detail);
verify(repository).findOwned(10001L, 9L);
verify(repository).detail(10001L, order);
}
@Test
void shouldReturnBusinessNotFoundForCrossTenantOrMissingOrder() {
InboundOrderRepository repository = mock(InboundOrderRepository.class);
assertThatThrownBy(() -> new InboundOrderQueryService(repository).detail(10002L, 9L))
assertThatThrownBy(() -> service.detail(10002L, 9L))
.isInstanceOf(BizException.class)
.extracting(exception -> ((BizException) exception).getResultCode())
.isEqualTo(ResultCode.NOT_FOUND);
@@ -6,10 +6,13 @@ import com.baiyue.firstleg.entity.InboundMeasurementEntity;
import com.baiyue.firstleg.entity.InboundMeasurementItemEntity;
import com.baiyue.firstleg.entity.InboundOrderEntity;
import com.baiyue.firstleg.repository.InboundMeasurementRepository;
import jakarta.annotation.Resource;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@@ -24,14 +27,13 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@DisplayName("测量批次共享领域服务")
@SpringJUnitConfig(MeasurementBatchService.class)
class MeasurementBatchServiceTest {
private InboundMeasurementRepository repository;
private MeasurementBatchService service;
@MockBean private InboundMeasurementRepository repository;
@Resource private MeasurementBatchService service;
@BeforeEach
void setUp() {
repository = mock(InboundMeasurementRepository.class);
service = new MeasurementBatchService(repository);
when(repository.findInbound(1L, 10L)).thenReturn(inbound());
org.mockito.Mockito.doAnswer(invocation -> {
InboundMeasurementEntity entity = invocation.getArgument(0);
@@ -6,8 +6,14 @@ import com.baiyue.firstleg.config.InboundProperties;
import com.baiyue.firstleg.entity.InboundOrderEntity;
import com.baiyue.firstleg.repository.InboundPalletAllocationRepository;
import com.baiyue.firstleg.repository.InboundWorkflowRepository;
import jakarta.annotation.Resource;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
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.math.BigDecimal;
import java.util.List;
@@ -19,6 +25,9 @@ import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@SpringJUnitConfig(classes = {MeasurementLocationOccupationService.class,
MeasurementLocationOccupationServiceTest.TestConfig.class})
@TestPropertySource(properties = "baiyue.security.internal-service-token=internal-token")
class MeasurementLocationOccupationServiceTest {
private static final long BUSINESS_TENANT = 10001L;
private static final long RESOURCE_TENANT = 90001L;
@@ -27,12 +36,11 @@ class MeasurementLocationOccupationServiceTest {
private static final long OPERATOR = 9L;
private static final long MEASUREMENT = 30L;
private final WarehousePalletLocationClient warehouse = mock(WarehousePalletLocationClient.class);
private final InboundPalletAllocationRepository allocations = mock(InboundPalletAllocationRepository.class);
private final InboundWorkflowRepository workflow = mock(InboundWorkflowRepository.class);
private final InboundProperties properties = properties();
private final MeasurementLocationOccupationService service =
new MeasurementLocationOccupationService(warehouse, allocations, workflow, properties, "internal-token");
@MockBean private WarehousePalletLocationClient warehouse;
@MockBean private InboundPalletAllocationRepository allocations;
@MockBean private InboundWorkflowRepository workflow;
@Resource private InboundProperties properties;
@Resource private MeasurementLocationOccupationService service;
@Test
void sharedWarehouseResourcesUseResourceTenantWhileSnapshotsRemainInBusinessTenant() {
@@ -119,6 +127,14 @@ class MeasurementLocationOccupationServiceTest {
return value;
}
@Configuration
static class TestConfig {
@Bean
InboundProperties inboundProperties() {
return properties();
}
}
private static InboundOrderEntity inbound() {
InboundOrderEntity value = new InboundOrderEntity();
value.setTenantId(BUSINESS_TENANT);
@@ -8,8 +8,11 @@ import com.baiyue.firstleg.dto.MeasurementReviewRequest;
import com.baiyue.firstleg.entity.InboundMeasurementEntity;
import com.baiyue.firstleg.entity.InboundOrderEntity;
import com.baiyue.firstleg.repository.InboundWorkflowRepository;
import jakarta.annotation.Resource;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import java.util.List;
import java.util.concurrent.Callable;
@@ -31,7 +34,12 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@DisplayName("入库测量审核服务")
@SpringJUnitConfig(MeasurementReviewService.class)
class MeasurementReviewServiceTest {
@MockBean private InboundWorkflowRepository repository;
@MockBean private InboundIdempotencyService idempotency;
@MockBean private InboundProperties properties;
@Resource private MeasurementReviewService service;
@Test
@DisplayName("审核通过是测量到计费的唯一正常推进关口")
void shouldApproveOnlyPendingSubmittedMeasurement() {
@@ -138,12 +146,9 @@ class MeasurementReviewServiceTest {
}
private Fixture fixture(boolean allowRelease) {
InboundWorkflowRepository repository = mock(InboundWorkflowRepository.class);
InboundIdempotencyService idempotency = mock(InboundIdempotencyService.class);
when(idempotency.begin(anyLong(), anyLong(), anyLong(), anyString(), anyString(), anyString()))
.thenReturn(new InboundIdempotencyService.Context(1L, false));
InboundProperties properties = new InboundProperties();
properties.setAllowRejectedMeasurement(allowRelease);
when(properties.isAllowRejectedMeasurement()).thenReturn(allowRelease);
InboundOrderEntity order = new InboundOrderEntity();
order.setProcessStatus("MEASURE_PENDING");
order.setMeasureReviewStatus("PENDING");
@@ -151,8 +156,7 @@ class MeasurementReviewServiceTest {
measurement.setStatus("SUBMITTED");
when(repository.findOwned(10001L, 10L)).thenReturn(order);
when(repository.findMeasurement(10001L, 10L, 20L)).thenReturn(measurement);
return new Fixture(new MeasurementReviewService(repository, idempotency, properties),
repository, idempotency, order, measurement);
return new Fixture(service, repository, idempotency, order, measurement);
}
private record Fixture(MeasurementReviewService service, InboundWorkflowRepository repository,
@@ -2,7 +2,10 @@ package com.baiyue.firstleg.service;
import com.baiyue.firstleg.dto.InboundMeasurementRequests;
import com.baiyue.firstleg.dto.InboundMeasurementResponses;
import jakarta.annotation.Resource;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import org.springframework.transaction.annotation.Transactional;
import static org.assertj.core.api.Assertions.assertThat;
@@ -12,13 +15,14 @@ import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@SpringJUnitConfig(MeasurementSubmissionService.class)
class MeasurementSubmissionServiceTest {
private final InboundMeasurementService measurements = mock(InboundMeasurementService.class);
private final MeasurementLocationOccupationService locations = mock(MeasurementLocationOccupationService.class);
private final MeasurementSubmissionService service = new MeasurementSubmissionService(measurements, locations);
@MockBean private InboundMeasurementService measurements;
@MockBean private MeasurementLocationOccupationService locations;
@Resource private MeasurementSubmissionService service;
@Test
void 测量提交与库位占用处于同一事务边界() throws Exception {
void shouldHandleCase01() throws Exception {
assertThat(MeasurementSubmissionService.class
.getDeclaredMethod("submit", long.class, long.class, long.class,
InboundMeasurementRequests.SubmitRequest.class)
@@ -26,7 +30,7 @@ class MeasurementSubmissionServiceTest {
}
@Test
void 测量完成后立即占用库位再返回待客服审核() {
void shouldHandleCase02() {
var request = new InboundMeasurementRequests.SubmitRequest(30L, 2);
var submitted = new InboundMeasurementResponses.MeasurementSubmitted(
30L, 3, "MEASURE_PENDING", "PENDING", "STANDARD", null, null, null);
@@ -38,7 +42,7 @@ class MeasurementSubmissionServiceTest {
}
@Test
void 库位占用失败向上抛出以触发测量事务回滚() {
void shouldHandleCase03() {
var request = new InboundMeasurementRequests.SubmitRequest(30L, 2);
var submitted = new InboundMeasurementResponses.MeasurementSubmitted(
30L, 3, "MEASURE_PENDING", "PENDING", "STANDARD", null, null, null);
@@ -7,7 +7,11 @@ import com.baiyue.firstleg.dto.InboundMeasurementResponses;
import com.baiyue.firstleg.client.MeasurementImageClient;
import com.baiyue.firstleg.entity.InboundOrderEntity;
import com.baiyue.firstleg.repository.InboundOrderRepository;
import jakarta.annotation.Resource;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import java.math.BigDecimal;
import java.util.List;
@@ -19,16 +23,17 @@ import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@SpringJUnitConfig(MerchantInboundBillingService.class)
@TestPropertySource(properties = "baiyue.internal.token=internal-token")
class MerchantInboundBillingServiceTest {
private final InboundOrderRepository orders = mock(InboundOrderRepository.class);
private final FbaPricingService pricing = mock(FbaPricingService.class);
private final InboundMeasurementService measurements = mock(InboundMeasurementService.class);
private final MeasurementImageClient imageClient = mock(MeasurementImageClient.class);
private final MerchantInboundBillingService service = new MerchantInboundBillingService(
orders, pricing, measurements, imageClient, "internal-token");
@MockBean private InboundOrderRepository orders;
@MockBean private FbaPricingService pricing;
@MockBean private InboundMeasurementService measurements;
@MockBean private MeasurementImageClient imageClient;
@Resource private MerchantInboundBillingService service;
@Test
void 客户只能看到本租户已完成入库且已发布的费用() {
void shouldHandleCase01() {
InboundOrderEntity inbound = inbound("INBOUNDED", "SENT");
InboundOrderDetail detail = new InboundOrderDetail(mock(com.baiyue.firstleg.dto.InboundOrderSummary.class),
"客户备注", List.of(), List.of(
@@ -64,7 +69,7 @@ class MerchantInboundBillingServiceTest {
}
@Test
void 客服尚未完成发布时客户不可见中间费用() {
void shouldHandleCase02() {
InboundOrderEntity inbound = inbound("FEE_CONFIRMING", "CONFIRMED");
when(orders.findByOrderId(10001L, 77L)).thenReturn(inbound);
@@ -75,7 +80,7 @@ class MerchantInboundBillingServiceTest {
}
@Test
void 使用租户条件查询防止跨租户读取订单费用() {
void shouldHandleCase03() {
when(orders.findByOrderId(20002L, 77L)).thenReturn(null);
assertThatThrownBy(() -> service.find(20002L, 77L)).isInstanceOf(BizException.class);
@@ -85,7 +90,7 @@ class MerchantInboundBillingServiceTest {
}
@Test
void 只能下载已发布计费明细实际引用的图片() {
void shouldHandleCase04() {
InboundOrderEntity inbound = inbound("INBOUNDED", "SENT");
InboundMeasurementResponses.Detail measurement = mock(InboundMeasurementResponses.Detail.class);
InboundMeasurementResponses.Item item = mock(InboundMeasurementResponses.Item.class);
@@ -94,7 +99,9 @@ class MerchantInboundBillingServiceTest {
when(measurement.weightResults()).thenReturn(List.of());
when(orders.findByOrderId(10001L, 77L)).thenReturn(inbound);
when(measurements.detail(10001L, 88L)).thenReturn(measurement);
when(imageClient.download("internal-token", 10001L, 88L, 41L)).thenReturn(
when(imageClient.download(org.mockito.ArgumentMatchers.anyString(),
org.mockito.ArgumentMatchers.eq(10001L), org.mockito.ArgumentMatchers.eq(88L),
org.mockito.ArgumentMatchers.eq(41L))).thenReturn(
org.springframework.http.ResponseEntity.ok()
.contentType(org.springframework.http.MediaType.IMAGE_PNG)
.body(new byte[]{1, 2, 3}));
@@ -106,7 +113,7 @@ class MerchantInboundBillingServiceTest {
}
@Test
void 拒绝下载未被当前计费明细引用的图片() {
void shouldHandleCase05() {
InboundOrderEntity inbound = inbound("INBOUNDED", "SENT");
InboundMeasurementResponses.Detail measurement = mock(InboundMeasurementResponses.Detail.class);
when(measurement.items()).thenReturn(List.of());