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
@@ -8,6 +8,7 @@ import com.baiyue.lastleg.inboundplan.dto.ManualInboundPlanRequest;
import com.baiyue.lastleg.inboundplan.dto.UpdateFbaAppointmentRequest;
import com.baiyue.lastleg.inboundplan.service.InboundFbaAppointmentService;
import com.baiyue.lastleg.inboundplan.service.InboundPlanService;
import com.baiyue.lastleg.inboundplan.service.ContainerInboundPlanConversionCoordinator;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@@ -30,6 +31,7 @@ import static org.mockito.Mockito.when;
class InboundPlanControllerTest {
@Mock InboundPlanService service;
@Mock InboundFbaAppointmentService appointmentService;
@Mock ContainerInboundPlanConversionCoordinator containerConversion;
private InboundPlanController controller;
@BeforeEach
@@ -37,6 +39,7 @@ class InboundPlanControllerTest {
controller = new InboundPlanController();
ReflectionTestUtils.setField(controller, "service", service);
ReflectionTestUtils.setField(controller, "appointmentService", appointmentService);
ReflectionTestUtils.setField(controller, "containerConversion", containerConversion);
}
@Test
@@ -79,19 +82,21 @@ class InboundPlanControllerTest {
@Test
void platformContainerConversionUsesStableBookingIdentity() {
controller.convertContainer(platform(), new ConvertContainerInboundPlanRequest(" BOOKING-1 "));
var request = new ConvertContainerInboundPlanRequest(" BOOKING-1 ", 3, "a".repeat(64));
controller.convertContainer(platform(), "convert-key", request);
verify(service).convertContainer(" BOOKING-1 ");
verify(containerConversion).confirm(request, "convert-key");
}
@Test
void merchantCannotConvertAWholeContainer() {
AuthenticatedUser customer = new AuthenticatedUser(8L, 100L, 100L, 9L, "customer", "MERCHANT", "WEB", Set.of());
assertThatThrownBy(() -> controller.convertContainer(customer,
new ConvertContainerInboundPlanRequest("BOOKING-1")))
assertThatThrownBy(() -> controller.convertContainer(customer, "convert-key",
new ConvertContainerInboundPlanRequest("BOOKING-1", 3, "a".repeat(64))))
.isInstanceOf(ResponseStatusException.class);
verify(service, never()).convertContainer(org.mockito.ArgumentMatchers.any());
verify(containerConversion, never()).confirm(org.mockito.ArgumentMatchers.any(),
org.mockito.ArgumentMatchers.any());
}
private static AuthenticatedUser platform() {
@@ -8,6 +8,8 @@ import org.springframework.test.web.client.MockRestServiceServer;
import org.springframework.web.client.RestClient;
import org.springframework.web.server.ResponseStatusException;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo;
@@ -159,4 +161,38 @@ class HttpWarehouseResourceGatewayTest {
server.verify();
}
@Test
void ensurePalletBatchAcceptsJacksonStringEncodedLongs() {
server.expect(requestTo("http://warehouse/internal/v1/warehouse-pallet-batches/ensure-reserved"))
.andRespond(withSuccess("""
{"code":"200","data":{"batchId":"41","batchKey":"BATCH-1","status":"RESERVED",
"attemptNo":1,"replayed":false,"items":[{"customerId":"100","businessKey":"CIP:1:1",
"inboundPlanNo":"IP-21","sequenceNo":1,"palletId":"51","palletCode":"PLT-51",
"locationId":"61","resourceLockId":"71","status":"RESERVED"}]}}
""", MediaType.APPLICATION_JSON));
var result = gateway.ensurePalletBatch(new WarehouseResourceGateway.PalletBatchEnsure(31L, "BATCH-1",
List.of(new WarehouseResourceGateway.PalletBatchItemRequest(
100L, "CIP:1:1", "IP-21", 1, "STANDARD", "ONT8"))));
assertThat(result.batchId()).isEqualTo(41L);
assertThat(result.items().get(0).resourceLockId()).isEqualTo(71L);
server.verify();
}
@Test
void palletBatchMapsBusinessConflictEnvelopeEvenWithHttp200() {
server.expect(requestTo("http://warehouse/internal/v1/warehouse-pallet-batches/confirm"))
.andRespond(withSuccess("""
{"code":"A0409","message":"Batch request hash conflict","data":null}
""", MediaType.APPLICATION_JSON));
assertThatThrownBy(() -> gateway.confirmPalletBatch(
new WarehouseResourceGateway.PalletBatchCommand(31L, "BATCH-1", "CONFIRM:1")))
.isInstanceOf(ResponseStatusException.class)
.hasMessageContaining("409")
.hasMessageContaining("Batch request hash conflict");
server.verify();
}
}
@@ -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")));