feat(lastleg): harden container receiving contract
This commit is contained in:
+82
@@ -0,0 +1,82 @@
|
||||
package com.baiyue.lastleg.inboundplan.pda;
|
||||
|
||||
import com.baiyue.lastleg.inboundplan.pda.controller.ReceivingContainerController;
|
||||
import com.baiyue.lastleg.inboundplan.pda.dto.ReceivingContainerRequests;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.RecordComponent;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Arrays;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class ReceivingContainerContractTest {
|
||||
private static final Set<String> FORBIDDEN_CALLER_SCOPE = Set.of(
|
||||
"customerId", "planNo", "allocationId", "warehouseId", "locationId");
|
||||
|
||||
@Test
|
||||
void pdaRequestsNeverAcceptServerOwnedScopeFields() {
|
||||
for (Class<?> type : ReceivingContainerRequests.class.getDeclaredClasses()) {
|
||||
if (!type.isRecord()) continue;
|
||||
Set<String> fields = Arrays.stream(type.getRecordComponents())
|
||||
.map(RecordComponent::getName).collect(Collectors.toSet());
|
||||
assertTrue(java.util.Collections.disjoint(fields, FORBIDDEN_CALLER_SCOPE), type.getSimpleName());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void everyBrowserReachableReceivingMethodUsesOnlyPdaOperatePermission() {
|
||||
PreAuthorize permission = ReceivingContainerController.class.getAnnotation(PreAuthorize.class);
|
||||
assertTrue(permission != null);
|
||||
assertTrue("hasAuthority('lastleg:pda:operate')".equals(permission.value()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void schemaContainsCustomerScopedPlanChildrenAndContainerSessionLocks() throws IOException {
|
||||
String schema = resource("db/lastleg.sql").toLowerCase();
|
||||
String unified = java.nio.file.Files.readString(
|
||||
java.nio.file.Path.of("../deploy/sql/海外区域统一增量-MySQL8.sql"), StandardCharsets.UTF_8).toLowerCase();
|
||||
for (String table : Set.of("ll_inbound_plan_line", "ll_inbound_plan_pallet",
|
||||
"ll_inbound_plan_pallet_allocation")) {
|
||||
assertTrue(tableBlock(schema, table).contains("customer_id varchar(128) not null"), table);
|
||||
assertTrue(unified.contains("table_name='" + table + "' and column_name='customer_id'"), table);
|
||||
assertTrue(unified.contains("update " + table), table);
|
||||
}
|
||||
assertFalse(schema.contains("tenant_id varchar(64)"));
|
||||
assertFalse(unified.contains("column_name='tenant_id'"));
|
||||
assertTrue(schema.contains("create table if not exists ll_receiving_container_session"));
|
||||
assertTrue(schema.contains("unique key uk_ll_receiving_active_operator(active_operator_key)"));
|
||||
assertTrue(schema.contains("unique key uk_ll_receiving_active_container(active_container_key)"));
|
||||
assertTrue(schema.contains("create table if not exists ll_receiving_container_operation"));
|
||||
assertTrue(schema.contains("wait_sign、signed、receiving、completed"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void openApiPublishesOnlyStableContainerPaths() throws IOException {
|
||||
String yaml = resource("openapi/overseas-business-v1.yaml");
|
||||
String prefix = "/api/pda/v1/last-leg/receiving-containers";
|
||||
assertTrue(yaml.contains(prefix + "/candidates:"));
|
||||
assertTrue(yaml.contains(prefix + "/current/receipts/bulk:"));
|
||||
assertTrue(yaml.contains(prefix + "/current/receipts/carton:"));
|
||||
assertTrue(yaml.contains(prefix + "/current/complete:"));
|
||||
assertFalse(yaml.contains("/api/pda/v1/last-leg/receiving-containers/{planNo}"));
|
||||
}
|
||||
|
||||
private String resource(String name) throws IOException {
|
||||
try (var input = getClass().getClassLoader().getResourceAsStream(name)) {
|
||||
assertTrue(input != null, name);
|
||||
return new String(input.readAllBytes(), StandardCharsets.UTF_8);
|
||||
}
|
||||
}
|
||||
|
||||
private static String tableBlock(String schema, String table) {
|
||||
int start = schema.indexOf("create table if not exists " + table + " (");
|
||||
int end = schema.indexOf(") engine=innodb", start);
|
||||
return start < 0 || end < 0 ? "" : schema.substring(start, end);
|
||||
}
|
||||
}
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
package com.baiyue.lastleg.inboundplan.pda.controller;
|
||||
|
||||
import com.baiyue.common.security.AuthenticatedUser;
|
||||
import com.baiyue.lastleg.inboundplan.pda.dto.ReceivingContainerResponses;
|
||||
import com.baiyue.lastleg.inboundplan.pda.service.ReceivingContainerService;
|
||||
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 java.util.Set;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class ReceivingContainerControllerTest {
|
||||
@Mock ReceivingContainerService service;
|
||||
private ReceivingContainerController controller;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
controller = new ReceivingContainerController();
|
||||
ReflectionTestUtils.setField(controller, "service", service);
|
||||
}
|
||||
|
||||
@Test
|
||||
void candidatesDerivesCustomerAndOperatorOnlyFromPdaIdentity() {
|
||||
when(service.candidates("100", "42")).thenReturn(List.of(new ReceivingContainerResponses.Candidate(
|
||||
"CONT-1", "WAIT_SIGN", 2, 2, 20, 0, false)));
|
||||
|
||||
controller.candidates(pda());
|
||||
|
||||
verify(service).candidates("100", "42");
|
||||
}
|
||||
|
||||
@Test
|
||||
void platformIdentityCannotEnterPdaReceivingApi() {
|
||||
AuthenticatedUser platform = new AuthenticatedUser(7L, 0L, 0L, 8L, "operator", "PLATFORM", "WEB",
|
||||
Set.of("lastleg:pda:operate"));
|
||||
assertThatThrownBy(() -> controller.candidates(platform)).isInstanceOf(ResponseStatusException.class)
|
||||
.hasMessageContaining("401");
|
||||
}
|
||||
|
||||
private static AuthenticatedUser pda() {
|
||||
return new AuthenticatedUser(42L, 100L, 100L, 8L, "receiver", "PDA", "PDA",
|
||||
Set.of("lastleg:pda:operate"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user