This commit is contained in:
2026-08-05 16:26:10 +08:00
parent 06f7f9d2f2
commit 5ba9716e12
155 changed files with 2883 additions and 551 deletions
@@ -14,6 +14,8 @@ import java.util.Set;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
@SpringJUnitConfig(MerchantInboundBillingController.class)
class MerchantInboundBillingControllerContractTest {
@@ -38,6 +40,17 @@ class MerchantInboundBillingControllerContractTest {
"admin", "PLATFORM", "WEB", Set.of());
assertThatThrownBy(() -> controller.find(platform, 77L)).isInstanceOf(BizException.class);
}
@Test
void merchantTechnicalTenantCannotReadCustomerBilling() {
var invalidMerchant = new AuthenticatedUser(1L, 0L, 0L, 1L,
"merchant", "MERCHANT", "WEB", Set.of());
assertThatThrownBy(() -> controller.find(invalidMerchant, 77L))
.isInstanceOf(BizException.class);
verify(service, never()).find(0L, 77L);
}
@Test
void shouldHandleCase03() throws Exception {
var method = MerchantInboundBillingController.class.getDeclaredMethod(
@@ -1,9 +1,13 @@
package com.baiyue.firstleg.repository;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.AbstractWrapper;
import com.baomidou.mybatisplus.core.conditions.Wrapper;
import com.baomidou.mybatisplus.core.MybatisConfiguration;
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
import com.baiyue.firstleg.entity.CustomerChargeRuleEntity;
import com.baiyue.firstleg.entity.CarrierFbaRateEntity;
import com.baiyue.firstleg.entity.InboundPricingIdempotencyEntity;
import com.baiyue.firstleg.mapper.CarrierFbaRateMapper;
import com.baiyue.firstleg.mapper.CustomerChargeRuleMapper;
import com.baiyue.firstleg.mapper.InboundFeeItemMapper;
@@ -85,6 +89,26 @@ class FbaPricingRepositoryRuleListTest {
assertThat(result).isEqualTo(4);
}
@Test
void idempotencyCompletionKeepsTenantAndRecordFilters() {
TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new MybatisConfiguration(), "test"),
InboundPricingIdempotencyEntity.class);
InboundPricingIdempotencyEntity value = new InboundPricingIdempotencyEntity();
value.setTenantId(10001L);
value.setId(88L);
repository.completeIdempotency(value, 501L, java.time.LocalDateTime.now());
@SuppressWarnings("unchecked")
ArgumentCaptor<com.baomidou.mybatisplus.core.conditions.Wrapper<InboundPricingIdempotencyEntity>> wrapper =
ArgumentCaptor.forClass(com.baomidou.mybatisplus.core.conditions.Wrapper.class);
verify(idempotency).update(org.mockito.ArgumentMatchers.isNull(), wrapper.capture());
assertThat(wrapper.getValue().getCustomSqlSegment())
.contains("tenant_id")
.containsPattern("(?i)(^|\\W)id\\s*=");
assertThat(params(wrapper.getValue()).values()).contains(10001L, 88L);
}
private Collection<Object> queryValues(Long tenantId, String ruleCode) {
when(rules.selectList(org.mockito.ArgumentMatchers.any())).thenReturn(List.of());
@@ -111,4 +135,9 @@ class FbaPricingRepositoryRuleListTest {
return wrapper.getValue().getParamNameValuePairs().values();
}
@SuppressWarnings("rawtypes")
private static java.util.Map<String, Object> params(Wrapper<?> wrapper) {
return ((AbstractWrapper) wrapper).getParamNameValuePairs();
}
}
@@ -35,7 +35,7 @@ class InboundWorkflowSqlContractTest {
assertThat(migration)
.contains("phase2_fba_add_column('fl_inbound_pallet_allocation', 'warehouse_resource_tenant_id'")
.contains("SET warehouse_resource_tenant_id = tenant_id")
.contains("SET warehouse_resource_tenant_id = 0")
.contains("MODIFY COLUMN warehouse_resource_tenant_id BIGINT UNSIGNED NOT NULL");
}
}
@@ -0,0 +1,117 @@
package com.baiyue.firstleg.repository;
import com.baomidou.mybatisplus.core.MybatisConfiguration;
import com.baomidou.mybatisplus.core.conditions.AbstractWrapper;
import com.baomidou.mybatisplus.core.conditions.Wrapper;
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
import com.baiyue.firstleg.entity.FeeItemEntity;
import com.baiyue.firstleg.entity.FulfillmentTaskEntity;
import com.baiyue.firstleg.entity.InboundFeeDetailEntity;
import com.baiyue.firstleg.entity.InboundOrderEntity;
import com.baiyue.firstleg.entity.InboundWeightResultEntity;
import com.baiyue.firstleg.mapper.AssignmentHistoryMapper;
import com.baiyue.firstleg.mapper.FeeItemMapper;
import com.baiyue.firstleg.mapper.FulfillmentTaskMapper;
import com.baiyue.firstleg.mapper.InboundFeeDetailMapper;
import com.baiyue.firstleg.mapper.InboundMeasurementItemMapper;
import com.baiyue.firstleg.mapper.InboundMeasurementMapper;
import com.baiyue.firstleg.mapper.InboundOrderMapper;
import com.baiyue.firstleg.mapper.InboundWeightResultMapper;
import com.baiyue.firstleg.mapper.InboundWeightRevisionMapper;
import com.baiyue.firstleg.mapper.MeasureReviewRecordMapper;
import jakarta.annotation.Resource;
import org.apache.ibatis.builder.MapperBuilderAssistant;
import org.junit.jupiter.api.BeforeAll;
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 static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@SpringJUnitConfig({FulfillmentTaskRepository.class, InboundMeasurementRepository.class,
FeeItemRepository.class})
class TenantScopedMutationRepositoryTest {
private static final long TENANT = 10001L;
private static final long ID = 88L;
@MockBean private FulfillmentTaskMapper tasks;
@MockBean private AssignmentHistoryMapper histories;
@MockBean private InboundOrderMapper inboundOrders;
@MockBean private InboundMeasurementMapper measurements;
@MockBean private InboundMeasurementItemMapper measurementItems;
@MockBean private InboundWeightResultMapper weights;
@MockBean private InboundWeightRevisionMapper revisions;
@MockBean private MeasureReviewRecordMapper reviews;
@MockBean private FeeItemMapper feeItems;
@MockBean private InboundFeeDetailMapper feeDetails;
@Resource private FulfillmentTaskRepository fulfillmentRepository;
@Resource private InboundMeasurementRepository measurementRepository;
@Resource private FeeItemRepository feeRepository;
@BeforeAll
static void initializeMetadata() {
initialize(FulfillmentTaskEntity.class);
initialize(InboundWeightResultEntity.class);
initialize(InboundFeeDetailEntity.class);
}
@Test
@SuppressWarnings({"rawtypes", "unchecked"})
void fulfillmentUpdateMatchesTenantAndId() {
FulfillmentTaskEntity task = new FulfillmentTaskEntity();
task.setTenantId(TENANT);
task.setId(ID);
when(tasks.update(any(), any(Wrapper.class))).thenReturn(1);
assertThat(fulfillmentRepository.update(task)).isTrue();
ArgumentCaptor<Wrapper<FulfillmentTaskEntity>> wrapper = ArgumentCaptor.forClass(Wrapper.class);
verify(tasks).update(eq(task), wrapper.capture());
assertTenantAndId(wrapper.getValue());
}
@Test
@SuppressWarnings({"rawtypes", "unchecked"})
void measurementUpdateMatchesTenantAndId() {
InboundWeightResultEntity weight = new InboundWeightResultEntity();
weight.setTenantId(TENANT);
weight.setId(ID);
when(weights.update(any(), any(Wrapper.class))).thenReturn(1);
assertThat(measurementRepository.updateWeight(weight)).isTrue();
ArgumentCaptor<Wrapper<InboundWeightResultEntity>> wrapper = ArgumentCaptor.forClass(Wrapper.class);
verify(weights).update(eq(weight), wrapper.capture());
assertTenantAndId(wrapper.getValue());
}
@Test
@SuppressWarnings({"rawtypes", "unchecked"})
void feeDetailDeleteMatchesTenantAndId() {
when(feeDetails.delete(any(Wrapper.class))).thenReturn(1);
assertThat(feeRepository.deleteDetail(TENANT, ID)).isEqualTo(1);
ArgumentCaptor<Wrapper<InboundFeeDetailEntity>> wrapper = ArgumentCaptor.forClass(Wrapper.class);
verify(feeDetails).delete(wrapper.capture());
assertTenantAndId(wrapper.getValue());
}
private static void initialize(Class<?> entityType) {
TableInfoHelper.initTableInfo(
new MapperBuilderAssistant(new MybatisConfiguration(), "test"), entityType);
}
private static void assertTenantAndId(Wrapper<?> wrapper) {
assertThat(wrapper.getCustomSqlSegment())
.contains("tenant_id")
.containsPattern("(?i)(^|\\W)id\\s*=");
assertThat(((AbstractWrapper<?, ?, ?>) wrapper).getParamNameValuePairs().values())
.contains(TENANT, ID);
}
}
@@ -49,6 +49,18 @@ class FbaPricingServiceTest {
@MockBean private FbaPricingRepository repository;
@Resource private FbaPricingService service;
@Test
@DisplayName("客户B不能读取客户A的费用快照")
void rejectsCrossCustomerFeeLookup() {
assertThatThrownBy(() -> service.latest(10002L, INBOUND))
.isInstanceOf(BizException.class)
.extracting(error -> ((BizException) error).getResultCode())
.isEqualTo(ResultCode.NOT_FOUND);
verify(repository).findLatestFee(10002L, INBOUND);
verify(repository, never()).findLatestFee(TENANT, INBOUND);
}
@Test
@DisplayName("包税KG按配置系数取体积重较大值并按规则向上舍入")
void calculatesTaxIncludedKgWithConfiguredFactorAndPrecision() {
@@ -22,6 +22,7 @@ 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.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -35,6 +36,17 @@ class FeeManagementServiceTest {
@Resource
private InboundFeeDetailService details;
@Test
@DisplayName("客户B不能删除客户A的费用明细")
void rejectsCrossCustomerFeeDetailDelete() {
assertThatThrownBy(() -> details.delete(10002L, 21L, 31L))
.isInstanceOf(BizException.class)
.hasMessageContaining("费用明细不存在");
verify(repository).findDetail(10002L, 21L, 31L);
verify(repository, never()).deleteDetail(10002L, 31L);
}
@Test
@DisplayName("费用项目列表返回分页结果")
void pagesItems() {
@@ -0,0 +1,41 @@
package com.baiyue.firstleg.service;
import com.baiyue.common.core.api.ResultCode;
import com.baiyue.common.core.exception.BizException;
import com.baiyue.common.security.AuthenticatedUser;
import org.junit.jupiter.api.Test;
import java.util.Set;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
class FirstlegScopeGuardTest {
@Test
void platformRequiresZeroTenantAndPositiveTargetCustomer() {
AuthenticatedUser platform = user(0L, "PLATFORM");
assertThat(FirstlegScopeGuard.platform(platform, 10001L)).isEqualTo(10001L);
assertForbidden(() -> FirstlegScopeGuard.platform(user(10001L, "PLATFORM"), 10002L));
assertForbidden(() -> FirstlegScopeGuard.platform(user(0L, "MERCHANT"), 10002L));
}
@Test
void merchantRequiresPositiveAuthenticatedCustomer() {
assertThat(FirstlegScopeGuard.merchant(user(10001L, "MERCHANT"))).isEqualTo(10001L);
assertForbidden(() -> FirstlegScopeGuard.merchant(user(0L, "MERCHANT")));
assertForbidden(() -> FirstlegScopeGuard.merchant(user(10001L, "PLATFORM")));
}
private static AuthenticatedUser user(long tenantId, String appCode) {
return new AuthenticatedUser(9L, tenantId, tenantId, 1L,
"operator", appCode, "WEB", Set.of());
}
private static void assertForbidden(org.assertj.core.api.ThrowableAssert.ThrowingCallable call) {
assertThatThrownBy(call)
.isInstanceOf(BizException.class)
.extracting(error -> ((BizException) error).getResultCode())
.isEqualTo(ResultCode.FORBIDDEN);
}
}
@@ -115,7 +115,7 @@ class InboundCompletionOrchestratorTest {
private Fixture fixture(String reviewStatus, boolean feeRelease) {
when(idempotency.begin(anyLong(), anyLong(), anyLong(), anyString(), anyString(), anyString()))
.thenReturn(new InboundIdempotencyService.Context(1L, false));
.thenReturn(new InboundIdempotencyService.Context(10001L, 1L, false));
InboundOrderEntity order = new InboundOrderEntity();
order.setProcessStatus("FEE_CONFIRMING");
order.setFeeStatus("CONFIRMED");
@@ -37,7 +37,11 @@ class InboundIdempotencyServiceTest {
InboundIdempotencyService.Context context = service.begin(
10001L, 9L, 10L, "MEASURE_APPROVE", "key-1", "10|20|3|");
assertThat(context.replayed()).isTrue();
assertThat(context)
.extracting(InboundIdempotencyService.Context::tenantId,
InboundIdempotencyService.Context::id,
InboundIdempotencyService.Context::replayed)
.containsExactly(10001L, 1L, true);
}
@Test
@@ -59,11 +63,12 @@ class InboundIdempotencyServiceTest {
@Test
@DisplayName("新请求完成时只允许PROCESSING条件更新一次")
void shouldCompleteProcessingRecordOnce() {
when(repository.completeIdempotency(1L, "{\"inboundId\":\"10\"}")).thenReturn(1);
when(repository.completeIdempotency(10001L, 1L, "{\"inboundId\":\"10\"}"))
.thenReturn(1);
service.complete(new InboundIdempotencyService.Context(1L, false), 10L);
service.complete(new InboundIdempotencyService.Context(10001L, 1L, false), 10L);
verify(repository).completeIdempotency(1L, "{\"inboundId\":\"10\"}");
verify(repository).completeIdempotency(10001L, 1L, "{\"inboundId\":\"10\"}");
}
private InboundIdempotencyEntity existing(String status) {
@@ -69,6 +69,23 @@ class InboundMeasurementOpenApiServiceTest {
@Resource private ObjectMapper objectMapper;
@Resource private InboundMeasurementOpenApiService service;
@Test
@DisplayName("第三方客户A不能访问只存在于客户B的入库单")
void shouldRejectInboundOwnedByAnotherCustomer() {
Fixture fixture = fixture();
when(fixture.repository.findInbound(10002L, "RK202608010001")).thenReturn(inbound());
assertThatThrownBy(() -> fixture.service.receive(headers("nonce-cross"), BODY, image()))
.isInstanceOf(com.baiyue.common.core.exception.BizException.class)
.hasMessageContaining("入仓码不存在或不属于绑定仓库");
verify(fixture.repository).findInbound(10001L, "RK202608010001");
verify(fixture.repository, never()).findInbound(10002L, "RK202608010001");
verify(fixture.images, never()).upload(anyString(),
org.mockito.ArgumentMatchers.anyLong(), org.mockito.ArgumentMatchers.anyLong(),
org.mockito.ArgumentMatchers.anyLong(), any());
}
@Test
@DisplayName("回调只写批次明细和幂等结果,不存在入库主单update入口")
void shouldPersistMeasurementsWithoutUpdatingInboundOrder() {
@@ -23,6 +23,7 @@ import java.util.List;
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.ArgumentMatchers.anyLong;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
@@ -35,6 +36,17 @@ class InboundMeasurementServiceTest {
@MockBean private MeasurementBatchService batchService;
@Resource private InboundMeasurementService service;
@Test
void customerCannotReadAnotherCustomersMeasurement() {
assertThatThrownBy(() -> service.detail(2L, 10L))
.isInstanceOf(BizException.class)
.hasMessageContaining("入库单不存在");
verify(repository).findInbound(2L, 10L);
verify(repository, never()).findInbound(1L, 10L);
verify(repository, never()).listWeights(anyLong(), anyLong());
}
@Test
void correctingWeightKeepsOcrOriginalAndWritesImmutableRevision() {
InboundOrderEntity inbound = inbound("MEASURING");
@@ -30,7 +30,7 @@ import static org.mockito.Mockito.when;
@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;
private static final long PLATFORM_RESOURCE_TENANT = 0L;
private static final long WAREHOUSE = 200L;
private static final long INBOUND = 88L;
private static final long OPERATOR = 9L;
@@ -43,8 +43,8 @@ class MeasurementLocationOccupationServiceTest {
@Resource private MeasurementLocationOccupationService service;
@Test
void sharedWarehouseResourcesUseResourceTenantWhileSnapshotsRemainInBusinessTenant() {
properties.setWarehouseResourceTenantId(RESOURCE_TENANT);
void platformWarehouseResourcesUseZeroWhileSnapshotsRemainInBusinessTenant() {
properties.setWarehouseResourceTenantId(90001L);
when(workflow.findOwned(BUSINESS_TENANT, INBOUND)).thenReturn(inbound());
when(warehouse.reserve(org.mockito.ArgumentMatchers.eq("internal-token"),
org.mockito.ArgumentMatchers.any())).thenReturn(ApiResponse.success(batch("RESERVED")));
@@ -56,27 +56,27 @@ class MeasurementLocationOccupationServiceTest {
ArgumentCaptor<WarehousePalletLocationClient.ReserveRequest> reserve =
ArgumentCaptor.forClass(WarehousePalletLocationClient.ReserveRequest.class);
verify(warehouse).reserve(org.mockito.ArgumentMatchers.eq("internal-token"), reserve.capture());
assertThat(reserve.getValue().tenantId()).isEqualTo(RESOURCE_TENANT);
assertThat(reserve.getValue().tenantId()).isEqualTo(PLATFORM_RESOURCE_TENANT);
assertThat(reserve.getValue().businessId()).isEqualTo(INBOUND);
assertThat(reserve.getValue().idempotencyKey()).isEqualTo("measure-88-30-3");
ArgumentCaptor<WarehousePalletLocationClient.ConfirmRequest> confirm =
ArgumentCaptor.forClass(WarehousePalletLocationClient.ConfirmRequest.class);
verify(warehouse).confirm(org.mockito.ArgumentMatchers.eq("internal-token"), confirm.capture());
assertThat(confirm.getValue().tenantId()).isEqualTo(RESOURCE_TENANT);
assertThat(confirm.getValue().tenantId()).isEqualTo(PLATFORM_RESOURCE_TENANT);
assertThat(confirm.getValue().businessId()).isEqualTo(INBOUND);
verify(allocations, times(2)).replace(
org.mockito.ArgumentMatchers.eq(BUSINESS_TENANT),
org.mockito.ArgumentMatchers.eq(INBOUND),
org.mockito.ArgumentMatchers.eq(WAREHOUSE),
org.mockito.ArgumentMatchers.eq(RESOURCE_TENANT),
org.mockito.ArgumentMatchers.eq(PLATFORM_RESOURCE_TENANT),
org.mockito.ArgumentMatchers.eq(new BigDecimal("1.200000")),
org.mockito.ArgumentMatchers.any());
}
@Test
void zeroResourceTenantFallsBackToBusinessTenant() {
void defaultResourceTenantRemainsPlatformZero() {
properties.setWarehouseResourceTenantId(0L);
when(workflow.findOwned(BUSINESS_TENANT, INBOUND)).thenReturn(inbound());
when(warehouse.reserve(org.mockito.ArgumentMatchers.eq("internal-token"),
@@ -89,17 +89,25 @@ class MeasurementLocationOccupationServiceTest {
ArgumentCaptor<WarehousePalletLocationClient.ReserveRequest> reserve =
ArgumentCaptor.forClass(WarehousePalletLocationClient.ReserveRequest.class);
verify(warehouse).reserve(org.mockito.ArgumentMatchers.eq("internal-token"), reserve.capture());
assertThat(reserve.getValue().tenantId()).isEqualTo(BUSINESS_TENANT);
assertThat(reserve.getValue().tenantId()).isEqualTo(PLATFORM_RESOURCE_TENANT);
ArgumentCaptor<WarehousePalletLocationClient.ConfirmRequest> confirm =
ArgumentCaptor.forClass(WarehousePalletLocationClient.ConfirmRequest.class);
verify(warehouse).confirm(org.mockito.ArgumentMatchers.eq("internal-token"), confirm.capture());
assertThat(confirm.getValue().tenantId()).isEqualTo(BUSINESS_TENANT);
assertThat(confirm.getValue().tenantId()).isEqualTo(PLATFORM_RESOURCE_TENANT);
verify(allocations, times(2)).replace(
org.mockito.ArgumentMatchers.eq(BUSINESS_TENANT),
org.mockito.ArgumentMatchers.eq(INBOUND),
org.mockito.ArgumentMatchers.eq(WAREHOUSE),
org.mockito.ArgumentMatchers.eq(PLATFORM_RESOURCE_TENANT),
org.mockito.ArgumentMatchers.eq(new BigDecimal("1.200000")),
org.mockito.ArgumentMatchers.any());
}
@Test
void confirmFailureReleasesReservationInResourceTenantAndKeepsOriginalFailure() {
properties.setWarehouseResourceTenantId(RESOURCE_TENANT);
properties.setWarehouseResourceTenantId(90001L);
when(workflow.findOwned(BUSINESS_TENANT, INBOUND)).thenReturn(inbound());
when(warehouse.reserve(org.mockito.ArgumentMatchers.eq("internal-token"),
org.mockito.ArgumentMatchers.any())).thenReturn(ApiResponse.success(batch("RESERVED")));
@@ -115,9 +123,9 @@ class MeasurementLocationOccupationServiceTest {
ArgumentCaptor<WarehousePalletLocationClient.ReleaseRequest> release =
ArgumentCaptor.forClass(WarehousePalletLocationClient.ReleaseRequest.class);
verify(warehouse).release(org.mockito.ArgumentMatchers.eq("internal-token"), release.capture());
assertThat(release.getValue().tenantId()).isEqualTo(RESOURCE_TENANT);
assertThat(release.getValue().tenantId()).isEqualTo(PLATFORM_RESOURCE_TENANT);
assertThat(release.getValue().businessId()).isEqualTo(INBOUND);
verify(allocations).replace(BUSINESS_TENANT, INBOUND, WAREHOUSE, RESOURCE_TENANT,
verify(allocations).replace(BUSINESS_TENANT, INBOUND, WAREHOUSE, PLATFORM_RESOURCE_TENANT,
new BigDecimal("1.200000"), batch("RESERVED"));
}
@@ -40,6 +40,23 @@ class MeasurementReviewServiceTest {
@MockBean private InboundIdempotencyService idempotency;
@MockBean private InboundProperties properties;
@Resource private MeasurementReviewService service;
@Test
@DisplayName("客户B不能审核客户A的测量")
void shouldRejectCrossCustomerReview() {
when(idempotency.begin(anyLong(), anyLong(), anyLong(), anyString(), anyString(), anyString()))
.thenReturn(new InboundIdempotencyService.Context(10002L, 1L, false));
assertThatThrownBy(() -> service.approve(10002L, 9L, "审核员", 10L, "cross-customer",
new MeasurementReviewRequest(20L, 3, null)))
.isInstanceOf(BizException.class)
.extracting(error -> ((BizException) error).getResultCode())
.isEqualTo(ResultCode.NOT_FOUND);
verify(repository).findOwned(10002L, 10L);
verify(repository, never()).transitionReview(anyLong(), anyLong(), anyLong(), anyInt(), anyBoolean());
}
@Test
@DisplayName("审核通过是测量到计费的唯一正常推进关口")
void shouldApproveOnlyPendingSubmittedMeasurement() {
@@ -73,7 +90,7 @@ class MeasurementReviewServiceTest {
void shouldReplaySameReviewRequest() {
Fixture fixture = fixture(false);
when(fixture.idempotency.begin(anyLong(), anyLong(), anyLong(), anyString(), anyString(), anyString()))
.thenReturn(new InboundIdempotencyService.Context(1L, true));
.thenReturn(new InboundIdempotencyService.Context(10001L, 1L, true));
fixture.service.approve(10001L, 9L, "审核员", 10L, "approve-1",
new MeasurementReviewRequest(20L, 3, null));
@@ -147,7 +164,7 @@ class MeasurementReviewServiceTest {
private Fixture fixture(boolean allowRelease) {
when(idempotency.begin(anyLong(), anyLong(), anyLong(), anyString(), anyString(), anyString()))
.thenReturn(new InboundIdempotencyService.Context(1L, false));
.thenReturn(new InboundIdempotencyService.Context(10001L, 1L, false));
when(properties.isAllowRejectedMeasurement()).thenReturn(allowRelease);
InboundOrderEntity order = new InboundOrderEntity();
order.setProcessStatus("MEASURE_PENDING");