Merge remote-tracking branch 'origin/test' into dev

This commit is contained in:
2026-09-09 00:19:29 +08:00
19 changed files with 752 additions and 33 deletions
@@ -0,0 +1,126 @@
package com.baiyue.lastleg.inboundplan.service;
import com.baiyue.lastleg.inboundplan.dto.ConvertContainerInboundPlanRequest;
import com.baiyue.lastleg.inboundplan.entity.ContainerPlanConversionCommand;
import com.baiyue.lastleg.inboundplan.mapper.ContainerPlanConversionCommandMapper;
import com.baiyue.lastleg.inboundplan.pda.gateway.WarehouseResourceGateway;
import com.fasterxml.jackson.databind.json.JsonMapper;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.web.server.ResponseStatusException;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class ContainerInboundPlanConversionCoordinatorTest {
private static final String BOOKING_ID = "BOOKING-1";
private static final String SNAPSHOT_HASH = "a".repeat(64);
@Mock InboundPlanService plans;
@Mock WarehouseResourceGateway warehouse;
@Mock ContainerPlanConversionCommandMapper commands;
private ContainerInboundPlanConversionCoordinator coordinator;
private JsonMapper objectMapper;
@BeforeEach
void setUp() {
coordinator = new ContainerInboundPlanConversionCoordinator();
objectMapper = JsonMapper.builder().findAndAddModules().build();
ReflectionTestUtils.setField(coordinator, "plans", plans);
ReflectionTestUtils.setField(coordinator, "warehouse", warehouse);
ReflectionTestUtils.setField(coordinator, "commands", commands);
ReflectionTestUtils.setField(coordinator, "objectMapper", objectMapper);
}
@Test
void anotherIdempotencyKeyForCompletedCabinetReplaysWithoutCreatingAnotherBatch() throws Exception {
ConvertContainerInboundPlanRequest request = request();
ContainerPlanConversionCommand existing = command("FIRST-KEY", "COMPLETED", request);
existing.setResultJson("[]");
when(commands.find(BOOKING_ID, "SECOND-KEY")).thenReturn(null);
when(commands.findByBooking(BOOKING_ID)).thenReturn(existing);
assertEquals(List.of(), coordinator.confirm(request, "SECOND-KEY"));
verify(warehouse, never()).ensurePalletBatch(org.mockito.ArgumentMatchers.any());
verify(commands, never()).insert(org.mockito.ArgumentMatchers.any(ContainerPlanConversionCommand.class));
}
@Test
void concurrentCabinetClaimRejectsFollowerBeforeWarehouseReservation() throws Exception {
ConvertContainerInboundPlanRequest request = request();
ContainerPlanConversionCommand applying = command("FIRST-KEY", "APPLYING", request);
when(commands.find(BOOKING_ID, "SECOND-KEY")).thenReturn(null);
when(commands.findByBooking(BOOKING_ID)).thenReturn(applying);
ResponseStatusException error = assertThrows(ResponseStatusException.class,
() -> coordinator.confirm(request, "SECOND-KEY"));
assertEquals(409, error.getStatusCode().value());
verify(warehouse, never()).ensurePalletBatch(org.mockito.ArgumentMatchers.any());
}
@Test
void failedConditionalClaimStopsBeforeWarehouseReservation() throws Exception {
ConvertContainerInboundPlanRequest request = request();
ContainerPlanConversionCommand prepared = command("FIRST-KEY", "PREPARED", request);
when(commands.find(BOOKING_ID, "SECOND-KEY")).thenReturn(null);
when(commands.findByBooking(BOOKING_ID)).thenReturn(prepared);
when(commands.claim(org.mockito.ArgumentMatchers.eq(1L), org.mockito.ArgumentMatchers.eq("PREPARED"),
org.mockito.ArgumentMatchers.eq("APPLYING"), org.mockito.ArgumentMatchers.any())).thenReturn(0);
ResponseStatusException error = assertThrows(ResponseStatusException.class,
() -> coordinator.confirm(request, "SECOND-KEY"));
assertEquals(409, error.getStatusCode().value());
verify(warehouse, never()).ensurePalletBatch(org.mockito.ArgumentMatchers.any());
}
@Test
void reservationMustMatchEveryStableItemIdentity() throws Exception {
var expected = new ContainerInboundPlanConversionCoordinator.PreparedItem(
"100", 11L, 21L, "IP-21", 1, 1, "CIP:1:1", "ONT8");
var preparation = new ContainerInboundPlanConversionCoordinator.Preparation(31L, List.of(expected));
var response = new WarehouseResourceGateway.PalletBatchResponse(41L, "BATCH-1", "RESERVED", 1,
false, List.of(new WarehouseResourceGateway.PalletBatchItem(
100L, "CIP:1:1", "WRONG-PLAN", 1, 51L, "PLT-51", 61L, 71L, "RESERVED")));
ResponseStatusException error = assertThrows(ResponseStatusException.class,
() -> ReflectionTestUtils.invokeMethod(coordinator, "bind", preparation, response));
assertEquals(409, error.getStatusCode().value());
}
private ConvertContainerInboundPlanRequest request() {
return new ConvertContainerInboundPlanRequest(BOOKING_ID, 7L, SNAPSHOT_HASH);
}
private ContainerPlanConversionCommand command(String key, String status,
ConvertContainerInboundPlanRequest request) throws Exception {
ContainerPlanConversionCommand command = new ContainerPlanConversionCommand();
command.setId(1L);
command.setSourceContainerBookingId(BOOKING_ID);
command.setIdempotencyKey(key);
command.setRequestHash(ReflectionTestUtils.invokeMethod(coordinator, "hash",
List.of(BOOKING_ID, request.snapshotVersion(), request.snapshotHash())));
command.setSnapshotVersion(request.snapshotVersion());
command.setSnapshotHash(request.snapshotHash());
command.setWarehouseId(31L);
command.setBatchKey("BATCH-1");
command.setStatus(status);
command.setPreparationJson(objectMapper.writeValueAsString(
new ContainerInboundPlanConversionCoordinator.Preparation(31L, List.of())));
return command;
}
}
@@ -424,6 +424,8 @@ class InboundPlanServiceTest {
void containerConversionPrevalidatesEveryOrderBeforeWritingIndependentPlans() throws Exception {
assertNotNull(InboundPlanService.class.getMethod("convertContainer", String.class)
.getAnnotation(Transactional.class));
assertNotNull(InboundPlanService.class.getMethod("convertContainerReserved", String.class,
long.class, String.class, List.class).getAnnotation(Transactional.class));
when(orderMapper.lockContainerOrders("BOOKING-1")).thenReturn(List.of(
new InboundPlanOrderMapper.ContainerOrderRef("100", 1L, "PENDING_ASSIGNMENT", "MSCU6639870", "STANDARD"),
new InboundPlanOrderMapper.ContainerOrderRef("200", 2L, "DESTINATION_ASSIGNED", "MSCU6639870", "STANDARD")));