优化
This commit is contained in:
+6
@@ -0,0 +1,6 @@
|
||||
package com.baiyue.firstleg.controller;
|
||||
import org.junit.jupiter.api.Test; import org.springframework.core.io.ClassPathResource; import org.springframework.security.access.prepost.PreAuthorize; import java.lang.reflect.Method; import java.nio.charset.StandardCharsets; import static org.assertj.core.api.Assertions.assertThat;
|
||||
class TruckAppointmentControllerContractTest {
|
||||
@Test void exposesDedicatedEndpointAndPermissions() {String p=PlatformTruckAppointmentController.class.getAnnotation(org.springframework.web.bind.annotation.RequestMapping.class).value()[0];assertThat(p).isEqualTo("/api/platform/v1/truck-appointments"); for(String n:new String[]{"page","detail","create","edit","action","reschedule"}) assertThat(java.util.Arrays.stream(PlatformTruckAppointmentController.class.getDeclaredMethods()).anyMatch(m->m.getName().equals(n))).isTrue();}
|
||||
@Test void schemaHasAggregateTablesAndStates() throws Exception {String s=new ClassPathResource("db/phase3-truck-appointment-schema.sql").getContentAsString(StandardCharsets.UTF_8);assertThat(s).contains("fl_truck_appointment","fl_truck_appointment_history","fl_truck_appointment_attachment","fl_truck_appointment_operation","PENDING_DISPATCH","PICKUP_IN_PROGRESS","ARRIVED_WAREHOUSE","RESCHEDULED");}
|
||||
}
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
package com.baiyue.firstleg.oceanbooking.controller;
|
||||
|
||||
import com.baiyue.common.audit.BusinessAudit;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Arrays;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class PlatformOceanBookingControllerContractTest {
|
||||
@Test
|
||||
void usesDedicatedResourceAndPermissions() {
|
||||
assertThat(PlatformOceanBookingController.class.getAnnotation(RequestMapping.class).value())
|
||||
.containsExactly("/api/platform/v1/ocean-bookings");
|
||||
Map<String, String> permissions = Map.ofEntries(
|
||||
Map.entry("page", "platform:firstleg:ocean-booking:list"),
|
||||
Map.entry("detail", "platform:firstleg:ocean-booking:view"),
|
||||
Map.entry("create", "platform:firstleg:ocean-booking:create"),
|
||||
Map.entry("updateDraft", "platform:firstleg:ocean-booking:edit"),
|
||||
Map.entry("submit", "platform:firstleg:ocean-booking:submit"),
|
||||
Map.entry("confirm", "platform:firstleg:ocean-booking:confirm"),
|
||||
Map.entry("fail", "platform:firstleg:ocean-booking:confirm"),
|
||||
Map.entry("release", "platform:firstleg:ocean-booking:release"),
|
||||
Map.entry("cancel", "platform:firstleg:ocean-booking:cancel"),
|
||||
Map.entry("requestChange", "platform:firstleg:ocean-booking:change"),
|
||||
Map.entry("confirmChange", "platform:firstleg:ocean-booking:change"),
|
||||
Map.entry("failChange", "platform:firstleg:ocean-booking:change"),
|
||||
Map.entry("allocate", "platform:firstleg:ocean-booking:allocate"));
|
||||
permissions.forEach((name, permission) -> {
|
||||
Method method = method(name);
|
||||
assertThat(method.getAnnotation(PreAuthorize.class).value())
|
||||
.isEqualTo("hasAuthority('" + permission + "')");
|
||||
if (!name.equals("page") && !name.equals("detail")) {
|
||||
assertThat(method.getAnnotation(BusinessAudit.class)).isNotNull();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void pathVariablesUseExplicitAggregateNames() {
|
||||
assertThat(Arrays.stream(PlatformOceanBookingController.class.getDeclaredMethods())
|
||||
.filter(method -> !method.getName().equals("page") && !method.getName().equals("create"))
|
||||
.flatMap(method -> Arrays.stream(method.getParameters()))
|
||||
.filter(parameter -> parameter.isAnnotationPresent(org.springframework.web.bind.annotation.PathVariable.class))
|
||||
.allMatch(parameter -> parameter.getName().equals("oceanBookingId"))).isTrue();
|
||||
}
|
||||
|
||||
private static Method method(String name) {
|
||||
return Arrays.stream(PlatformOceanBookingController.class.getDeclaredMethods())
|
||||
.filter(method -> method.getName().equals(name)).findFirst().orElseThrow();
|
||||
}
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
package com.baiyue.firstleg.oceanbooking.repository;
|
||||
|
||||
import com.baiyue.firstleg.oceanbooking.mapper.OceanBookingMapper;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
import org.apache.ibatis.annotations.Update;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class OceanBookingMapperContractTest {
|
||||
@Test
|
||||
void listUsesPlatformAggregateFilters() throws Exception {
|
||||
String sql = String.join("\n", OceanBookingMapper.class.getDeclaredMethod("selectPage",
|
||||
com.baomidou.mybatisplus.extension.plugins.pagination.Page.class,
|
||||
com.baiyue.firstleg.oceanbooking.dto.OceanBookingDtos.Query.class)
|
||||
.getAnnotation(Select.class).value());
|
||||
assertThat(sql).contains("customer_id=#{query.customerId}")
|
||||
.contains("status=#{query.status}")
|
||||
.contains("booking_no LIKE")
|
||||
.contains("ORDER BY updated_at DESC,id DESC");
|
||||
}
|
||||
|
||||
@Test
|
||||
void everyMutationUsesExpectedVersionAndStateGuard() {
|
||||
String sql = Arrays.stream(OceanBookingMapper.class.getDeclaredMethods())
|
||||
.map(method -> method.getAnnotation(Update.class))
|
||||
.filter(java.util.Objects::nonNull)
|
||||
.flatMap(annotation -> Arrays.stream(annotation.value()))
|
||||
.reduce("", (left, right) -> left + '\n' + right);
|
||||
assertThat(sql)
|
||||
.contains("status='DRAFT' AND version=#{expectedVersion}")
|
||||
.contains("status=#{fromStatus} AND version=#{expectedVersion}")
|
||||
.contains("status='CONFIRMED' AND version=#{expectedVersion}")
|
||||
.contains("status='CHANGE_PENDING' AND version=#{expectedVersion}")
|
||||
.contains("status IN ('SUBMITTED','CONFIRMED','CHANGE_PENDING')");
|
||||
}
|
||||
}
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
package com.baiyue.firstleg.oceanbooking.repository;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class OceanBookingSqlContractTest {
|
||||
@Test
|
||||
void definesDedicatedAggregateWithoutChangingContainerSemantics() throws Exception {
|
||||
String sql = sql();
|
||||
assertThat(sql)
|
||||
.contains("CREATE TABLE IF NOT EXISTS fl_ocean_booking (")
|
||||
.contains("CREATE TABLE IF NOT EXISTS fl_ocean_booking_party")
|
||||
.contains("CREATE TABLE IF NOT EXISTS fl_ocean_booking_container_requirement")
|
||||
.contains("CREATE TABLE IF NOT EXISTS fl_ocean_booking_container_allocation")
|
||||
.contains("CREATE TABLE IF NOT EXISTS fl_ocean_booking_status_history")
|
||||
.contains("CREATE TABLE IF NOT EXISTS fl_ocean_booking_operation")
|
||||
.contains("container_booking_id BIGINT UNSIGNED NOT NULL COMMENT '明确引用现有柜次ID'")
|
||||
.contains("REFERENCES fl_container_booking(id)")
|
||||
.doesNotContain("ALTER TABLE fl_container_booking");
|
||||
}
|
||||
|
||||
@Test
|
||||
void enforcesStateIdempotencyAndAuditContracts() throws Exception {
|
||||
assertThat(sql())
|
||||
.contains("OLD.status='DRAFT' AND NEW.status IN ('SUBMITTED','CANCELLED')")
|
||||
.contains("OLD.status='CONFIRMED' AND NEW.status IN ('RELEASED','CHANGE_PENDING','CANCELLED')")
|
||||
.contains("OLD.status='CHANGE_PENDING' AND NEW.status IN ('CONFIRMED','FAILED','CANCELLED')")
|
||||
.contains("OLD.status IN ('RELEASED','CANCELLED')")
|
||||
.contains("UNIQUE KEY uk_ocean_operation (operator_id,operation,idempotency_key)")
|
||||
.contains("request_hash CHAR(64) NOT NULL")
|
||||
.contains("订舱状态历史不可修改")
|
||||
.contains("UNIQUE KEY uk_ocean_allocation_container (container_booking_id)");
|
||||
}
|
||||
|
||||
@Test
|
||||
void declaresDedicatedPlatformPermissions() throws Exception {
|
||||
assertThat(sql())
|
||||
.contains("platform:firstleg:ocean-booking:list")
|
||||
.contains("platform:firstleg:ocean-booking:view")
|
||||
.contains("platform:firstleg:ocean-booking:create")
|
||||
.contains("platform:firstleg:ocean-booking:edit")
|
||||
.contains("platform:firstleg:ocean-booking:submit")
|
||||
.contains("platform:firstleg:ocean-booking:confirm")
|
||||
.contains("platform:firstleg:ocean-booking:release")
|
||||
.contains("platform:firstleg:ocean-booking:cancel")
|
||||
.contains("platform:firstleg:ocean-booking:change")
|
||||
.contains("platform:firstleg:ocean-booking:allocate");
|
||||
}
|
||||
|
||||
private static String sql() throws Exception {
|
||||
return new ClassPathResource("db/phase3-ocean-booking-schema.sql")
|
||||
.getContentAsString(StandardCharsets.UTF_8);
|
||||
}
|
||||
}
|
||||
+121
@@ -0,0 +1,121 @@
|
||||
package com.baiyue.firstleg.oceanbooking.service;
|
||||
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.firstleg.oceanbooking.dto.OceanBookingDtos.ActionRequest;
|
||||
import com.baiyue.firstleg.oceanbooking.dto.OceanBookingDtos.ChangeRequest;
|
||||
import com.baiyue.firstleg.oceanbooking.dto.OceanBookingDtos.Detail;
|
||||
import com.baiyue.firstleg.oceanbooking.entity.OceanBookingEntity;
|
||||
import com.baiyue.firstleg.oceanbooking.entity.OceanBookingOperationEntity;
|
||||
import com.baiyue.firstleg.oceanbooking.entity.OceanBookingStatusHistoryEntity;
|
||||
import com.baiyue.firstleg.oceanbooking.repository.OceanBookingRepository;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
class OceanBookingServiceTest {
|
||||
@Mock private OceanBookingRepository repository;
|
||||
@InjectMocks private OceanBookingService service;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() throws Exception {
|
||||
MockitoAnnotations.openMocks(this);
|
||||
var field = OceanBookingService.class.getDeclaredField("objectMapper");
|
||||
field.setAccessible(true); field.set(service, new ObjectMapper().findAndRegisterModules());
|
||||
when(repository.parties(anyLong())).thenReturn(List.of());
|
||||
when(repository.requirements(anyLong())).thenReturn(List.of());
|
||||
when(repository.allocations(anyLong())).thenReturn(List.of());
|
||||
when(repository.histories(anyLong())).thenReturn(List.of());
|
||||
when(repository.claimOperation(any(), anyLong(), anyString(), anyString(), anyString())).thenReturn(true);
|
||||
when(repository.completeOperation(anyLong(), anyLong(), anyString(), anyString(), anyString())).thenReturn(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void advancesDraftToSubmittedWithVersionAndHistory() {
|
||||
when(repository.find(1L)).thenReturn(booking("DRAFT", 2), booking("SUBMITTED", 3));
|
||||
var shipper = new com.baiyue.firstleg.oceanbooking.entity.OceanBookingPartyEntity(); shipper.setPartyType("SHIPPER");
|
||||
var consignee = new com.baiyue.firstleg.oceanbooking.entity.OceanBookingPartyEntity(); consignee.setPartyType("CONSIGNEE");
|
||||
var notify = new com.baiyue.firstleg.oceanbooking.entity.OceanBookingPartyEntity(); notify.setPartyType("NOTIFY_PARTY");
|
||||
when(repository.parties(1L)).thenReturn(List.of(shipper, consignee, notify));
|
||||
when(repository.requirements(1L)).thenReturn(List.of(new com.baiyue.firstleg.oceanbooking.entity.OceanBookingContainerRequirementEntity()));
|
||||
when(repository.transition(1L, "DRAFT", "SUBMITTED", null, null, 2, 9L, "admin")).thenReturn(1);
|
||||
|
||||
Detail result = service.submit(1L, 9L, "admin", "submit-1", new ActionRequest(2, "提交"));
|
||||
|
||||
assertThat(result.oceanBooking().getStatus()).isEqualTo("SUBMITTED");
|
||||
ArgumentCaptor<OceanBookingStatusHistoryEntity> history =
|
||||
ArgumentCaptor.forClass(OceanBookingStatusHistoryEntity.class);
|
||||
verify(repository).insert(history.capture());
|
||||
assertThat(history.getValue().getFromStatus()).isEqualTo("DRAFT");
|
||||
assertThat(history.getValue().getToStatus()).isEqualTo("SUBMITTED");
|
||||
assertThat(history.getValue().getAction()).isEqualTo("SUBMIT");
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsCancellationAfterRelease() {
|
||||
when(repository.find(1L)).thenReturn(booking("RELEASED", 4));
|
||||
|
||||
assertThatThrownBy(() -> service.cancel(1L, 9L, "admin", "cancel-1",
|
||||
new ActionRequest(4, "错误"))).isInstanceOf(BizException.class)
|
||||
.hasMessageContaining("不可逆");
|
||||
|
||||
verify(repository, never()).transition(anyLong(), anyString(), anyString(), any(), any(), anyInt(), anyLong(), anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
void keepsCurrentVoyagePendingUntilChangeConfirmation() {
|
||||
OceanBookingEntity current = booking("CONFIRMED", 5);
|
||||
current.setVesselName("OLD"); current.setVoyageNo("V1");
|
||||
OceanBookingEntity pending = booking("CHANGE_PENDING", 6);
|
||||
pending.setVesselName("OLD"); pending.setVoyageNo("V1"); pending.setPendingVesselName("NEW"); pending.setPendingVoyageNo("V2");
|
||||
when(repository.find(1L)).thenReturn(current, pending);
|
||||
when(repository.requestChange(eq(1L), any(), eq("改船"), eq(5), eq(9L), eq("admin"))).thenReturn(1);
|
||||
|
||||
Detail result = service.requestChange(1L, 9L, "admin", "change-1",
|
||||
new ChangeRequest(5, "NEW", "V2", LocalDateTime.of(2026, 9, 1, 0, 0),
|
||||
LocalDateTime.of(2026, 9, 20, 0, 0), "改船"));
|
||||
|
||||
assertThat(result.oceanBooking().getStatus()).isEqualTo("CHANGE_PENDING");
|
||||
assertThat(result.oceanBooking().getVesselName()).isEqualTo("OLD");
|
||||
assertThat(result.oceanBooking().getPendingVesselName()).isEqualTo("NEW");
|
||||
}
|
||||
|
||||
@Test
|
||||
void replaysCompletedRequestBeforeVersionValidation() throws Exception {
|
||||
OceanBookingEntity submitted = booking("SUBMITTED", 3);
|
||||
Detail replay = new Detail(submitted, List.of(), List.of(), List.of(), List.of());
|
||||
OceanBookingOperationEntity operation = new OceanBookingOperationEntity();
|
||||
operation.setRequestHash(hashForSubmit()); operation.setStatus("SUCCESS");
|
||||
operation.setResultJson(new ObjectMapper().findAndRegisterModules().writeValueAsString(replay));
|
||||
when(repository.claimOperation(eq(1L), eq(9L), eq("SUBMIT"), eq("submit-replay"), anyString())).thenReturn(false);
|
||||
when(repository.operation(9L, "SUBMIT", "submit-replay")).thenReturn(operation);
|
||||
|
||||
Detail result = service.submit(1L, 9L, "admin", "submit-replay", new ActionRequest(2, "提交"));
|
||||
|
||||
assertThat(result.oceanBooking().getStatus()).isEqualTo("SUBMITTED");
|
||||
verify(repository, never()).find(anyLong());
|
||||
}
|
||||
|
||||
private String hashForSubmit() throws Exception {
|
||||
var method = OceanBookingService.class.getDeclaredMethod("hash", String.class, Long.class, Object.class);
|
||||
method.setAccessible(true);
|
||||
return (String) method.invoke(service, "SUBMIT", 1L, new ActionRequest(2, "提交"));
|
||||
}
|
||||
|
||||
private static OceanBookingEntity booking(String status, int version) {
|
||||
OceanBookingEntity value = new OceanBookingEntity(); value.setId(1L); value.setBookingNo("OB1");
|
||||
value.setStatus(status); value.setCustomerId(100L); value.setCustomerName("客户"); value.setVersion(version);
|
||||
return value;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user