优化
This commit is contained in:
+84
@@ -0,0 +1,84 @@
|
||||
package com.baiyue.lastleg.config;
|
||||
|
||||
import com.sun.net.httpserver.HttpServer;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.boot.context.properties.bind.Bindable;
|
||||
import org.springframework.boot.context.properties.bind.Binder;
|
||||
import org.springframework.boot.context.properties.source.MapConfigurationPropertySource;
|
||||
import org.springframework.web.client.ResourceAccessException;
|
||||
import org.springframework.web.client.RestClient;
|
||||
|
||||
import java.net.InetSocketAddress;
|
||||
import java.time.Duration;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
class LastlegRestClientFactoryTest {
|
||||
private final LastlegRestClientFactory factory = new LastlegRestClientFactory();
|
||||
|
||||
@Test
|
||||
void defaultsArePositiveAndSeparateFileUploadsFromWarehouseCalls() {
|
||||
LastlegRestClientProperties properties = new LastlegRestClientProperties();
|
||||
|
||||
assertThat(properties.getUnloadPhotoFile().getConnectTimeout()).isEqualTo(Duration.ofSeconds(3));
|
||||
assertThat(properties.getUnloadPhotoFile().getReadTimeout()).isEqualTo(Duration.ofSeconds(30));
|
||||
assertThat(properties.getRelabelPhotoFile().getReadTimeout()).isEqualTo(Duration.ofSeconds(30));
|
||||
assertThat(properties.getWarehouseResource().getReadTimeout()).isEqualTo(Duration.ofSeconds(5));
|
||||
assertThat(properties.getPalletPhysical().getReadTimeout()).isEqualTo(Duration.ofSeconds(5));
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsUnboundedOrNonPositiveTimeouts() {
|
||||
assertThatThrownBy(() -> new LastlegRestClientProperties.ClientTimeouts(null, Duration.ofSeconds(1)))
|
||||
.isInstanceOf(IllegalArgumentException.class);
|
||||
assertThatThrownBy(() -> new LastlegRestClientProperties.ClientTimeouts(Duration.ZERO, Duration.ofSeconds(1)))
|
||||
.isInstanceOf(IllegalArgumentException.class);
|
||||
assertThatThrownBy(() -> new LastlegRestClientProperties.ClientTimeouts(Duration.ofSeconds(1), Duration.ZERO))
|
||||
.isInstanceOf(IllegalArgumentException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void bindsPerClientTimeoutOverridesFromConfiguration() {
|
||||
var source = new MapConfigurationPropertySource(Map.of(
|
||||
"baiyue.http.clients.unload-photo-file.connect-timeout", "125ms",
|
||||
"baiyue.http.clients.unload-photo-file.read-timeout", "9s",
|
||||
"baiyue.http.clients.pallet-physical.connect-timeout", "250ms",
|
||||
"baiyue.http.clients.pallet-physical.read-timeout", "2s"));
|
||||
|
||||
LastlegRestClientProperties properties = new Binder(source)
|
||||
.bind("baiyue.http.clients", Bindable.of(LastlegRestClientProperties.class))
|
||||
.orElseThrow(() -> new AssertionError("Lastleg HTTP client properties were not bound"));
|
||||
|
||||
assertThat(properties.getUnloadPhotoFile().getConnectTimeout()).isEqualTo(Duration.ofMillis(125));
|
||||
assertThat(properties.getUnloadPhotoFile().getReadTimeout()).isEqualTo(Duration.ofSeconds(9));
|
||||
assertThat(properties.getPalletPhysical().getConnectTimeout()).isEqualTo(Duration.ofMillis(250));
|
||||
assertThat(properties.getPalletPhysical().getReadTimeout()).isEqualTo(Duration.ofSeconds(2));
|
||||
assertThat(properties.getWarehouseResource().getReadTimeout()).isEqualTo(Duration.ofSeconds(5));
|
||||
}
|
||||
|
||||
@Test
|
||||
void appliesConfiguredConnectAndReadTimeoutsWithoutRetryOrFallback() throws Exception {
|
||||
var timeouts = new LastlegRestClientProperties.ClientTimeouts(
|
||||
Duration.ofMillis(250), Duration.ofMillis(75));
|
||||
assertThat(factory.httpClient(timeouts).connectTimeout()).contains(Duration.ofMillis(250));
|
||||
|
||||
HttpServer server = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0);
|
||||
server.createContext("/slow", exchange -> {
|
||||
try { Thread.sleep(500); }
|
||||
catch (InterruptedException interrupted) { Thread.currentThread().interrupt(); }
|
||||
exchange.sendResponseHeaders(200, 0);
|
||||
exchange.close();
|
||||
});
|
||||
server.start();
|
||||
try {
|
||||
RestClient client = factory.build(RestClient.builder(),
|
||||
"http://127.0.0.1:" + server.getAddress().getPort(), timeouts);
|
||||
assertThatThrownBy(() -> client.get().uri("/slow").retrieve().toBodilessEntity())
|
||||
.isInstanceOf(ResourceAccessException.class);
|
||||
} finally {
|
||||
server.stop(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
+15
@@ -3,6 +3,8 @@ package com.baiyue.lastleg.controller;
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.baiyue.common.core.api.PageResult;
|
||||
import com.baiyue.common.security.AuthenticatedUser;
|
||||
import com.baiyue.common.event.EventEnvelope;
|
||||
import com.baiyue.common.event.EventTypes;
|
||||
import com.baiyue.lastleg.dto.LastlegOrderRequests;
|
||||
import com.baiyue.lastleg.dto.LastlegOrderSummary;
|
||||
import com.baiyue.lastleg.security.InternalServiceTokenGuard;
|
||||
@@ -21,6 +23,8 @@ import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.ArgumentMatchers.isNull;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
class LastlegOrderControllerTest {
|
||||
private final LastlegOrderService service = mock(LastlegOrderService.class);
|
||||
@@ -46,6 +50,17 @@ class LastlegOrderControllerTest {
|
||||
verify(service).page(eq("200"), eq(query));
|
||||
}
|
||||
|
||||
@Test
|
||||
void iamSecurityEventsAreRejectedBeforeLoadingCompletedConsumer() {
|
||||
EventEnvelope event = mock(EventEnvelope.class);
|
||||
when(event.eventType()).thenReturn(EventTypes.IAM_USER_STATUS_CHANGED);
|
||||
LoadingCompletedConsumer loading = (LoadingCompletedConsumer) ReflectionTestUtils.getField(controller, "consumer");
|
||||
|
||||
assertThrows(ResponseStatusException.class, () -> controller.receive("internal-token", event));
|
||||
|
||||
verify(loading, never()).consume(event);
|
||||
}
|
||||
|
||||
private static AuthenticatedUser user(long customerId, String appCode) {
|
||||
return new AuthenticatedUser(1L, customerId, customerId, 1L, "tester", appCode, "WEB", Set.of());
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package com.baiyue.lastleg.masterdata.vehicle.controller;
|
||||
package com.baiyue.lastleg.masterdata.vehicle.adapter.in.web;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
package com.baiyue.lastleg.masterdata.vehicle.application;
|
||||
|
||||
import com.baiyue.lastleg.masterdata.vehicle.application.VehicleApplicationService.FailureKind;
|
||||
import com.baiyue.lastleg.masterdata.vehicle.application.VehicleApplicationService.VehicleFailure;
|
||||
import com.baiyue.lastleg.masterdata.vehicle.application.model.VehicleModels;
|
||||
import com.baiyue.lastleg.masterdata.vehicle.application.port.out.VehicleRepository;
|
||||
import com.baiyue.lastleg.masterdata.vehicle.application.port.out.VehicleRepository.VehicleSnapshot;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Optional;
|
||||
|
||||
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.when;
|
||||
|
||||
class VehicleApplicationServiceTest {
|
||||
private VehicleRepository repository;
|
||||
private VehicleApplicationService service;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
repository = mock(VehicleRepository.class);
|
||||
service = new VehicleApplicationService(repository);
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsStaleVersionBeforeUpdating() {
|
||||
when(repository.lockById(1L)).thenReturn(Optional.of(vehicle(3L, 1000L)));
|
||||
VehicleModels.Update request = update(2L, 1000L);
|
||||
assertThatThrownBy(() -> service.update(1L, "9", request))
|
||||
.isInstanceOfSatisfying(VehicleFailure.class,
|
||||
failure -> assertThat(failure.kind()).isEqualTo(FailureKind.CONFLICT));
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsDecreasingOdometer() {
|
||||
when(repository.lockById(1L)).thenReturn(Optional.of(vehicle(3L, 1000L)));
|
||||
VehicleModels.Update request = update(3L, 999L);
|
||||
assertThatThrownBy(() -> service.update(1L, "9", request))
|
||||
.isInstanceOfSatisfying(VehicleFailure.class,
|
||||
failure -> assertThat(failure.kind()).isEqualTo(FailureKind.BAD_REQUEST));
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsUnknownStatus() {
|
||||
when(repository.lockById(1L)).thenReturn(Optional.of(vehicle(3L, 1000L)));
|
||||
assertThatThrownBy(() -> service.changeStatus(1L, "9", new VehicleModels.ChangeStatus("BUSY", 3L)))
|
||||
.isInstanceOfSatisfying(VehicleFailure.class,
|
||||
failure -> assertThat(failure.kind()).isEqualTo(FailureKind.BAD_REQUEST));
|
||||
}
|
||||
|
||||
private static VehicleModels.Update update(long version, long mileage) {
|
||||
return new VehicleModels.Update(version, "ABC-001", "Box Truck", new BigDecimal("12.50"), mileage,
|
||||
"Driver", "123", "OVERSEAS", "OWNED", null);
|
||||
}
|
||||
|
||||
private static VehicleSnapshot vehicle(long version, long mileage) {
|
||||
return new VehicleSnapshot(1L, "ABC-001", "Box Truck", new BigDecimal("12.50"), mileage,
|
||||
"Driver", "123", "OVERSEAS", "OWNED", "IN_SERVICE", null, version, "9", null);
|
||||
}
|
||||
}
|
||||
-63
@@ -1,63 +0,0 @@
|
||||
package com.baiyue.lastleg.masterdata.vehicle.service;
|
||||
|
||||
import com.baiyue.lastleg.masterdata.vehicle.dto.VehicleDtos;
|
||||
import com.baiyue.lastleg.masterdata.vehicle.entity.Vehicle;
|
||||
import com.baiyue.lastleg.masterdata.vehicle.mapper.VehicleMapper;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
class VehicleServiceTest {
|
||||
private VehicleMapper mapper;
|
||||
private VehicleService service;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
mapper = mock(VehicleMapper.class);
|
||||
service = new VehicleService();
|
||||
ReflectionTestUtils.setField(service, "vehicles", mapper);
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsStaleVersionBeforeUpdating() {
|
||||
when(mapper.lockById(1L)).thenReturn(vehicle(3L, 1000L));
|
||||
VehicleDtos.Update request = update(2L, 1000L);
|
||||
assertThatThrownBy(() -> service.update(1L, "9", request))
|
||||
.isInstanceOf(ResponseStatusException.class)
|
||||
.hasMessageContaining("409 CONFLICT");
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsDecreasingOdometer() {
|
||||
when(mapper.lockById(1L)).thenReturn(vehicle(3L, 1000L));
|
||||
VehicleDtos.Update request = update(3L, 999L);
|
||||
assertThatThrownBy(() -> service.update(1L, "9", request))
|
||||
.isInstanceOf(ResponseStatusException.class)
|
||||
.hasMessageContaining("400 BAD_REQUEST");
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsUnknownStatus() {
|
||||
when(mapper.lockById(1L)).thenReturn(vehicle(3L, 1000L));
|
||||
assertThatThrownBy(() -> service.changeStatus(1L, "9", new VehicleDtos.ChangeStatus("BUSY", 3L)))
|
||||
.isInstanceOf(ResponseStatusException.class)
|
||||
.hasMessageContaining("400 BAD_REQUEST");
|
||||
}
|
||||
|
||||
private static VehicleDtos.Update update(long version, long mileage) {
|
||||
return new VehicleDtos.Update(version, "ABC-001", "Box Truck", new BigDecimal("12.50"), mileage,
|
||||
"Driver", "123", "OVERSEAS", "OWNED", null);
|
||||
}
|
||||
|
||||
private static Vehicle vehicle(long version, long mileage) {
|
||||
Vehicle value = new Vehicle(); value.setId(1L); value.setRecordVersion(version); value.setMileageKm(mileage);
|
||||
value.setStatus("IN_SERVICE"); return value;
|
||||
}
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
package com.baiyue.lastleg.repository;
|
||||
|
||||
import com.baomidou.mybatisplus.core.MybatisConfiguration;
|
||||
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
|
||||
import com.baiyue.lastleg.entity.LastlegEventOutbox;
|
||||
import com.baiyue.lastleg.mapper.LastlegEventOutboxMapper;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.apache.ibatis.builder.MapperBuilderAssistant;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.isNull;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
class UnloadCompletedOutboxRepositoryTest {
|
||||
@Test
|
||||
@SuppressWarnings({"rawtypes", "unchecked"})
|
||||
void claimRechecksRetryWindowAfterCandidateSelection() {
|
||||
TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new MybatisConfiguration(), "test"),
|
||||
LastlegEventOutbox.class);
|
||||
LastlegEventOutboxMapper mapper = mock(LastlegEventOutboxMapper.class);
|
||||
UnloadCompletedOutboxRepository repository = new UnloadCompletedOutboxRepository();
|
||||
ReflectionTestUtils.setField(repository, "mapper", mapper);
|
||||
ArgumentCaptor<LambdaUpdateWrapper> wrapper = ArgumentCaptor.forClass(LambdaUpdateWrapper.class);
|
||||
when(mapper.update(isNull(LastlegEventOutbox.class), wrapper.capture())).thenReturn(1);
|
||||
LocalDateTime now = LocalDateTime.of(2026, 8, 25, 12, 0);
|
||||
|
||||
assertThat(repository.claim(1L, "worker", now, now.minusMinutes(1))).isTrue();
|
||||
|
||||
assertThat(wrapper.getValue().getSqlSegment())
|
||||
.contains("next_retry_at", "<=", "status", "locked_at");
|
||||
}
|
||||
}
|
||||
+2
@@ -8,6 +8,8 @@ class LoadingCompletedConsumerTest {
|
||||
@Test void loadingCreatesAwaitingPickupReadyGateWithoutPollutingShippingVersion(){ service.consume(event()); verify(shippingGates).initialize("book-1","100","","MSCU1",0L,1L); }
|
||||
@Test void repeatedEventReturnsExistingEffect(){ EventEnvelope event=event(); LastlegEventInbox row=new LastlegEventInbox(); row.setPayloadHash(event.payloadHash()); when(inbox.selectById(event.eventId())).thenReturn(row); when(orders.countContainer("100","MSCU1")).thenReturn(2L); assertEquals(2,service.consume(event)); verify(orders).countContainer("100","MSCU1"); verify(orders,never()).insert(any(LastlegOrder.class)); verify(details,never()).insert(any(LastlegOrderDetail.class)); }
|
||||
@Test void sameEventWithDifferentHashIsRejected(){ EventEnvelope event=event(); LastlegEventInbox row=new LastlegEventInbox(); row.setPayloadHash("sha256:"+"0".repeat(64)); when(inbox.selectById(event.eventId())).thenReturn(row); assertThrows(IllegalArgumentException.class,()->service.consume(event)); }
|
||||
@Test void tamperedPayloadHashIsRejectedBeforeInboxMutation(){ EventEnvelope source=event(); EventEnvelope tampered=new EventEnvelope(source.eventId(),source.eventType(),source.eventVersion(),source.occurredAt(),source.sourceRegion(),source.targetRegion(),source.tenantId(),source.aggregateType(),source.aggregateId(),source.aggregateVersion(),source.handoverVersion(),source.traceId(),source.correlationId(),source.causationId(),source.payload(),"sha256:"+"0".repeat(64)); IllegalArgumentException error=assertThrows(IllegalArgumentException.class,()->service.consume(tampered)); assertTrue(error.getMessage().contains("payloadHash")); verify(inbox,never()).insert(any(LastlegEventInbox.class)); }
|
||||
@Test void concurrentDuplicateKeyWithDifferentHashIsRejected(){ EventEnvelope event=event(); LastlegEventInbox raced=new LastlegEventInbox(); raced.setPayloadHash("sha256:"+"0".repeat(64)); when(inbox.selectById(event.eventId())).thenReturn(null,raced); doThrow(new org.springframework.dao.DuplicateKeyException("race")).when(inbox).insert(any(LastlegEventInbox.class)); IllegalArgumentException error=assertThrows(IllegalArgumentException.class,()->service.consume(event)); assertTrue(error.getMessage().contains("摘要冲突")); verify(orders,never()).countContainer(anyString(),anyString()); }
|
||||
@Test void inboundSnapshotUsesCustomerIdAndHasNoPhantomTenantColumn() throws Exception {
|
||||
assertThrows(NoSuchFieldException.class,()->LastlegInboundOrder.class.getDeclaredField("tenantId"));
|
||||
String schema=java.nio.file.Files.readString(java.nio.file.Path.of("src/main/resources/db/lastleg.sql"));
|
||||
|
||||
+15
@@ -96,6 +96,21 @@ class ContainerShippingNodeConsumerTest {
|
||||
.hasMessageContaining("摘要冲突");
|
||||
}
|
||||
|
||||
@Test
|
||||
void tamperedPayloadHashIsRejectedBeforeInboxMutation() {
|
||||
EventEnvelope source = event("123e4567-e89b-12d3-a456-426614174105", "PICKUP_READY", 80, 5,
|
||||
Instant.parse("2026-08-21T10:00:00Z"));
|
||||
EventEnvelope tampered = new EventEnvelope(source.eventId(), source.eventType(), source.eventVersion(),
|
||||
source.occurredAt(), source.sourceRegion(), source.targetRegion(), source.tenantId(),
|
||||
source.aggregateType(), source.aggregateId(), source.aggregateVersion(), source.handoverVersion(),
|
||||
source.traceId(), source.correlationId(), source.causationId(), source.payload(),
|
||||
"sha256:" + "0".repeat(64));
|
||||
|
||||
assertThatThrownBy(() -> consumer.consume(tampered)).isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessageContaining("payloadHash");
|
||||
verify(inboxes, never()).insert(any(LastlegEventInbox.class));
|
||||
}
|
||||
|
||||
private ContainerShippingGate gate(String status, int sequence, long version, Instant actualAt) {
|
||||
ContainerShippingGate gate = new ContainerShippingGate(); gate.setContainerBookingId("CB-1");
|
||||
gate.setCustomerId("100"); gate.setOceanBookingId("OB-1"); gate.setContainerNo("CONT-1");
|
||||
|
||||
Reference in New Issue
Block a user