fix(phase3): close loading and dispatch flows
This commit is contained in:
+16
@@ -13,9 +13,12 @@ import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.RecordComponent;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Map;
|
||||
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@DisplayName("预装与装柜清单控制器契约")
|
||||
@@ -70,6 +73,12 @@ class LoadingListControllerContractTest {
|
||||
.noneMatch(method -> method.getName().equals("complete"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void listWarehouseFilterIsOptional() {
|
||||
assertThat(component(PreloadQuery.class, "warehouseId").getAnnotation(NotNull.class)).isNull();
|
||||
assertThat(component(ConfirmedQuery.class, "warehouseId").getAnnotation(NotNull.class)).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void yamlParses() throws Exception {
|
||||
String yaml = new ClassPathResource("openapi/loading-lists-v1.yaml")
|
||||
@@ -108,6 +117,13 @@ class LoadingListControllerContractTest {
|
||||
return stringMap(stringMap(schemas.get(schemaName)).get("properties"));
|
||||
}
|
||||
|
||||
private static RecordComponent component(Class<?> type, String name) {
|
||||
return java.util.Arrays.stream(type.getRecordComponents())
|
||||
.filter(component -> component.getName().equals(name))
|
||||
.findFirst()
|
||||
.orElseThrow();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static Map<String, Object> stringMap(Object value) {
|
||||
return (Map<String, Object>) value;
|
||||
|
||||
+3
-3
@@ -16,9 +16,9 @@ class CustomsDispatchGateContributorTest {
|
||||
|
||||
@BeforeEach void setUp() { MockitoAnnotations.openMocks(this); }
|
||||
|
||||
@Test void failsClosedWhenCaseMissingOrScopeMismatches() {
|
||||
@Test void failsClosedWhenCaseMissingOrScopeIsInvalid() {
|
||||
assertThat(contributor.assess(context()).passed()).isFalse();
|
||||
assertThat(contributor.assess(new Context(10001L, 10002L, 21L, 31L, 81L, 91L, null)).actual())
|
||||
assertThat(contributor.assess(new Context(0L, 10002L, 21L, 31L, 81L, 91L, null)).actual())
|
||||
.isEqualTo("INVALID_CUSTOMER_SCOPE");
|
||||
}
|
||||
|
||||
@@ -30,5 +30,5 @@ class CustomsDispatchGateContributorTest {
|
||||
assertThat(contributor.assess(context()).passed()).isTrue();
|
||||
}
|
||||
|
||||
private static Context context() { return new Context(10001L, 10001L, 21L, 31L, 81L, 91L, null); }
|
||||
private static Context context() { return new Context(10001L, 10002L, 21L, 31L, 81L, 91L, null); }
|
||||
}
|
||||
|
||||
+21
@@ -16,4 +16,25 @@ class OutboundDispatchReadMapperContractTest {
|
||||
.contains("ORDER BY started_at DESC,id DESC")
|
||||
.doesNotContain("created_at");
|
||||
}
|
||||
|
||||
@Test
|
||||
void evidenceQueryUsesNativeSqlComparisonInsteadOfXmlEntities() throws Exception {
|
||||
String sql = OutboundDispatchReadMapper.class.getMethod("evidence", long.class, long.class)
|
||||
.getAnnotation(Select.class).value()[0];
|
||||
|
||||
assertThat(sql)
|
||||
.contains("seal_no<>''")
|
||||
.doesNotContain("<", ">");
|
||||
}
|
||||
|
||||
@Test
|
||||
void oceanAndTruckQueriesUseTenantIsolationKey() throws Exception {
|
||||
String oceanSql = OutboundDispatchReadMapper.class.getMethod("ocean", long.class, long.class)
|
||||
.getAnnotation(Select.class).value()[0];
|
||||
String truckSql = OutboundDispatchReadMapper.class.getMethod("truck", long.class, long.class, long.class)
|
||||
.getAnnotation(Select.class).value()[0];
|
||||
|
||||
assertThat(oceanSql).contains("booking.customer_id=#{tenantId}").doesNotContain("#{customerId}");
|
||||
assertThat(truckSql).contains("customer_id=#{tenantId}").doesNotContain("#{customerId}");
|
||||
}
|
||||
}
|
||||
|
||||
+15
-2
@@ -32,6 +32,19 @@ class OutboundDispatchServiceTest {
|
||||
.extracting(GateItem::code).containsExactlyInAnyOrder("CUSTOMS_CLEARANCE", "SAILING_SCHEDULE");
|
||||
}
|
||||
|
||||
@Test
|
||||
void aggregateUsesTenantKeyWhenCustomerEntityIdDiffers() {
|
||||
OutboundDispatchService service = new OutboundDispatchService(repository, List.of());
|
||||
stubReadyChain();
|
||||
|
||||
service.checkGate(new Query(null, 11L, 22L));
|
||||
|
||||
verify(repository).ocean(22L, 7L);
|
||||
verify(repository).truck(22L, 44L, 7L);
|
||||
verify(repository, never()).ocean(22L, 8L);
|
||||
verify(repository, never()).truck(22L, 44L, 8L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void confirmAdvancesDispatchWithoutMutatingLoadedInbound() {
|
||||
OutboundDispatchService service = new OutboundDispatchService(repository,
|
||||
@@ -109,8 +122,8 @@ class OutboundDispatchServiceTest {
|
||||
private void stubReadyChain() {
|
||||
when(repository.bindings(any())).thenReturn(List.of(binding()));
|
||||
when(repository.pick(44L)).thenReturn(new OutboundDispatchReadMapper.IdStatus(55L, "COMPLETED"));
|
||||
when(repository.ocean(22L, 8L)).thenReturn(new OutboundDispatchReadMapper.IdStatus(66L, "RELEASED"));
|
||||
when(repository.truck(22L, 44L, 8L)).thenReturn(new OutboundDispatchReadMapper.IdStatus(77L, "DEPARTED"));
|
||||
when(repository.ocean(22L, 7L)).thenReturn(new OutboundDispatchReadMapper.IdStatus(66L, "RELEASED"));
|
||||
when(repository.truck(22L, 44L, 7L)).thenReturn(new OutboundDispatchReadMapper.IdStatus(77L, "DEPARTED"));
|
||||
when(repository.evidence(7L, 11L)).thenReturn(new OutboundDispatchReadMapper.Evidence(5, 1));
|
||||
}
|
||||
|
||||
|
||||
+4
-4
@@ -25,11 +25,11 @@ class PortScheduleDispatchGateContributorTest {
|
||||
@Test
|
||||
void usesFixedCodeAndFailsClosedBeforeQueryForInvalidContext() {
|
||||
assertThat(contributor.code()).isEqualTo("SAILING_SCHEDULE");
|
||||
assertThat(contributor.assess(new Context(10001L, 10002L, 1L, 2L, 3L, 4L, 5L)).actual())
|
||||
assertThat(contributor.assess(new Context(0L, 10002L, 1L, 2L, 3L, 4L, 5L)).actual())
|
||||
.isEqualTo("INVALID_CUSTOMER_SCOPE");
|
||||
assertThat(contributor.assess(new Context(10001L, 10001L, 1L, 2L, 3L, 4L, null)).actual())
|
||||
assertThat(contributor.assess(new Context(10001L, 10002L, 1L, 2L, 3L, 4L, null)).actual())
|
||||
.isEqualTo("MISSING_OCEAN_BOOKING");
|
||||
assertThat(contributor.assess(new Context(10001L, 10001L, 1L, 2L, 0L, 4L, 5L)).actual())
|
||||
assertThat(contributor.assess(new Context(10001L, 10002L, 1L, 2L, 0L, 4L, 5L)).actual())
|
||||
.isEqualTo("MISSING_CONTAINER_BOOKING");
|
||||
verify(repository, never()).latestForDispatch(10001L, 5L, 3L);
|
||||
}
|
||||
@@ -90,7 +90,7 @@ class PortScheduleDispatchGateContributorTest {
|
||||
}
|
||||
|
||||
private static Context context() {
|
||||
return new Context(10001L, 10001L, 1L, 2L, 3L, 4L, 5L);
|
||||
return new Context(10001L, 10002L, 1L, 2L, 3L, 4L, 5L);
|
||||
}
|
||||
|
||||
private static PortScheduleEntity ready() {
|
||||
|
||||
@@ -166,6 +166,20 @@ class LoadingListServiceTest {
|
||||
"plan".equals(value.keyword())));
|
||||
}
|
||||
|
||||
@Test
|
||||
void listQueriesAllowAllWarehouses() {
|
||||
when(repository.preloadPage(org.mockito.ArgumentMatchers.any())).thenReturn(
|
||||
new PageResult<>(List.of(), 1, 20, 0, 0));
|
||||
when(repository.confirmedPage(org.mockito.ArgumentMatchers.any())).thenReturn(
|
||||
new PageResult<>(List.of(), 1, 20, 0, 0));
|
||||
|
||||
service.preloadPage(new PreloadQuery(null, "LOADING", null, null, null, 1, 20));
|
||||
service.confirmedPage(new ConfirmedQuery(null, null, null, null, 1, 20));
|
||||
|
||||
verify(repository).preloadPage(org.mockito.ArgumentMatchers.argThat(value -> value.warehouseId() == null));
|
||||
verify(repository).confirmedPage(org.mockito.ArgumentMatchers.argThat(value -> value.warehouseId() == null));
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsPlanOutsideRequestedWarehouse() {
|
||||
when(repository.aggregate(10L)).thenReturn(row("LOADING", 1));
|
||||
|
||||
Reference in New Issue
Block a user