This commit is contained in:
2026-10-08 17:07:57 +08:00
parent 16d2d9111a
commit c1ddb658bd
111 changed files with 2914 additions and 941 deletions
@@ -123,31 +123,9 @@ class LastlegOrderControllerTest {
verify(service).containerProgressDetail("BOOKING-1");
}
@Test
void exportBuildsCsvBeforeReturningTheResponse() {
byte[] csv = "csv".getBytes(java.nio.charset.StandardCharsets.UTF_8);
when(service.exportCsv("200", query)).thenReturn(csv);
var response = controller.export(user(0L, "PLATFORM"), "200", query);
assertArrayEquals(csv, response.getBody());
verify(service).exportCsv("200", query);
}
@Test
void platformOperatorCanExportAllCustomersWithoutFilters() {
byte[] csv = "all customers".getBytes(java.nio.charset.StandardCharsets.UTF_8);
LastlegOrderRequests.Query emptyQuery = new LastlegOrderRequests.Query(
null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null);
when(service.exportCsv(null, emptyQuery)).thenReturn(csv);
var response = controller.export(user(0L, "PLATFORM"), null, emptyQuery);
assertEquals(Long.valueOf(1), emptyQuery.page());
assertEquals(Long.valueOf(20), emptyQuery.size());
assertArrayEquals(csv, response.getBody());
verify(service).exportCsv(isNull(), eq(emptyQuery));
}
@Test
void iamSecurityEventsAreRejectedBeforeLoadingCompletedConsumer() {
@@ -0,0 +1,101 @@
package com.baiyue.lastleg.export;
import com.baiyue.common.core.api.PageResult;
import com.baiyue.common.core.export.ExportContracts.ExportBatchRequest;
import com.baiyue.lastleg.dto.LastlegOrderRequests;
import com.baiyue.lastleg.service.LastlegOrderService;
import com.baiyue.lastleg.inventory.query.dto.InventoryQueryRequests;
import com.baiyue.lastleg.inventory.query.service.InventoryQueryService;
import com.baiyue.lastleg.masterdata.exchange.dto.MonthlyExchangeRateDtos;
import com.baiyue.lastleg.masterdata.exchange.service.MonthlyExchangeRateService;
import com.baiyue.lastleg.fleet.reporting.TripOperationsModels;
import com.baiyue.lastleg.fleet.reporting.TripOperationsService;
import com.baiyue.lastleg.security.InternalServiceTokenGuard;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import jakarta.validation.Validation;
import org.junit.jupiter.api.*;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.web.server.ResponseStatusException;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.*;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
class LastlegExportProviderServiceTest {
private LastlegExportProviderService service;
private LastlegOrderService orders;
private InventoryQueryService inventory;
private MonthlyExchangeRateService rates;
private TripOperationsService operations;
private jakarta.validation.ValidatorFactory validators;
@BeforeEach void setup() {
service = new LastlegExportProviderService();
orders = mock(LastlegOrderService.class); inventory = mock(InventoryQueryService.class);
rates = mock(MonthlyExchangeRateService.class); operations = mock(TripOperationsService.class);
ReflectionTestUtils.setField(service, "orders", orders);
ReflectionTestUtils.setField(service, "inventory", inventory);
ReflectionTestUtils.setField(service, "rates", rates);
ReflectionTestUtils.setField(service, "operations", operations);
ReflectionTestUtils.setField(service, "json", new ObjectMapper().findAndRegisterModules().disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS));
validators = Validation.buildDefaultValidatorFactory();
ReflectionTestUtils.setField(service, "validator", validators.getValidator());
ReflectionTestUtils.setField(service, "signingKey", "test-internal-token-with-at-least-32-bytes");
}
@AfterEach void close() { validators.close(); }
@Test void batchesAllRowsAndBindsSnapshotToFiltersRequesterAndAttempt() {
var row = new MonthlyExchangeRateDtos.Response("2026-10", "USD", "CNY", new BigDecimal("7.12"), "ACTIVE", 1, "operator", LocalDateTime.of(2026,10,8,12,0), null);
when(rates.page(any())).thenAnswer(call -> {
MonthlyExchangeRateDtos.Query q = call.getArgument(0);
return new PageResult<>(List.of(row), q.page(), q.size(), 2, 2);
});
var first = service.batch("MONTHLY_EXCHANGE_RATE_LIST", request(Map.of("status", "ACTIVE"), null, null));
assertTrue(first.hasMore()); assertEquals(2L, first.totalRows());
assertEquals(new BigDecimal("7.12"), first.rows().get(0).get("usdToCnyAverage"));
assertEquals("2026-10-08T12:00:00", first.rows().get(0).get("updatedAt"));
var second = service.batch("MONTHLY_EXCHANGE_RATE_LIST", request(Map.of("status", "ACTIVE"), first.nextCursor(), first.snapshotToken()));
assertFalse(second.hasMore()); assertTrue(second.columns().isEmpty()); assertEquals(first.schemaHash(), second.schemaHash());
verify(rates).page(new MonthlyExchangeRateDtos.Query(2,1,null,null,"ACTIVE"));
assertThrows(ResponseStatusException.class, () -> service.batch("MONTHLY_EXCHANGE_RATE_LIST", request(Map.of("status", "INACTIVE"), "1", first.snapshotToken())));
assertThrows(ResponseStatusException.class, () -> service.batch("MONTHLY_EXCHANGE_RATE_LIST", new ExportBatchRequest("t","0","PLATFORM","other",1,Map.of("status","ACTIVE"),"1",first.snapshotToken(),1)));
assertThrows(ResponseStatusException.class, () -> service.batch("MONTHLY_EXCHANGE_RATE_LIST", new ExportBatchRequest("t","0","PLATFORM","u",2,Map.of("status","ACTIVE"),"1",first.snapshotToken(),1)));
}
@Test void retainsOrderStageFalseAndRelabelFiltersWithoutExportingInternalIds() {
var order = new ObjectMapper().findAndRegisterModules().convertValue(Map.of("id", "9007199254740993", "orderNo", "LL-1", "currentStatus", "PENDING_ASSIGNMENT", "abnormal", false, "detained", true), com.baiyue.lastleg.dto.LastlegOrderSummary.class);
when(orders.page(isNull(), any(), eq("RL-1"))).thenReturn(new PageResult<>(List.of(order),1,1,1,1));
var result = service.batch("LAST_MILE_ORDER_LIST", request(Map.of("detained",false,"sailingStage","BEFORE_DEPARTURE","relabelNo","RL-1"),null,null));
verify(orders).page(isNull(), argThat(q -> Boolean.FALSE.equals(q.detained()) && q.sailingStage() == LastlegOrderRequests.SailingStage.BEFORE_DEPARTURE), eq("RL-1"));
assertEquals("否", result.rows().get(0).get("abnormal"));
assertEquals("是", result.rows().get(0).get("detained"));
assertEquals("待分配", result.rows().get(0).get("currentStatus"));
assertTrue(result.columns().stream().noneMatch(c -> c.key().equals("id") || c.key().equals("customerId")));
}
@Test void keepsCustomerScopeAndLedgerCustomerFilter() {
when(inventory.ledgers(eq("200"), any())).thenReturn(new PageResult<>(List.of(),1,1,0,0));
service.batch("OVERSEAS_INVENTORY_LEDGER", request(Map.of("customerId","200","customerName","客户"),null,null));
verify(inventory).ledgers(eq("200"), argThat(q -> "客户".equals(q.customerName())));
}
@Test void defaultsCostVisibilityClosed() {
when(operations.report(any(), eq(false))).thenReturn(new TripOperationsModels.Result(new PageResult<>(List.of(),1,1,0,0),null,null));
service.batch("TRIP_OPERATIONS_LIST",request(Map.of("from","2026-10-01T00:00:00Z","to","2026-10-08T00:00:00Z"),null,null));
verify(operations).report(any(), eq(false));
}
@Test void rejectsScopeInvalidParametersAndUntrustedInternalCalls() {
assertThrows(ResponseStatusException.class, () -> service.batch("LAST_MILE_ORDER_LIST",new ExportBatchRequest("t","1","MERCHANT","u",1,Map.of(),null,null,1)));
assertThrows(ResponseStatusException.class, () -> service.batch("LAST_MILE_ORDER_LIST",request(Map.of("unknown","x"),null,null)));
assertThrows(ResponseStatusException.class, () -> service.batch("LAST_MILE_ORDER_LIST",request(Map.of(),"1",null)));
assertThrows(ResponseStatusException.class, () -> new LastlegExportProviderController(new InternalServiceTokenGuard("expected"),service).batch("wrong","LAST_MILE_ORDER_LIST",request(Map.of(),null,null)));
verifyNoInteractions(orders);
}
private ExportBatchRequest request(Map<String,Object> p,String cursor,String snapshot) {
return new ExportBatchRequest("t","0","PLATFORM","u",1,p,cursor,snapshot,1);
}
}
@@ -59,9 +59,9 @@ class FleetQuotePersistenceTest {
service.importRows("1",new FleetQuoteModels.Import("batch-key",List.of(row)));
assertThatThrownBy(()->service.importRows("1",new FleetQuoteModels.Import("batch-key",List.of(row,draft("SALE","SF",BigDecimal.ONE))))).hasMessageContaining("不同报价");
}
@Test void deletePreservesHistoryAndExportEscapesSpreadsheetFormulas() {
@Test void deletePreservesHistoryAndBusinessText() {
var created=service.create("1",new FleetQuoteModels.Create("formula-key",draft("SALE","=2+2",BigDecimal.ZERO)));
assertThat(new String(service.exportCsv(new FleetQuoteModels.Query(1,20,null,null,null,null)),java.nio.charset.StandardCharsets.UTF_8)).contains("'=2+2");
assertThat(service.page(new FleetQuoteModels.Query(1,20,null,null,null,null)).records().get(0).details().originCity()).isEqualTo("=2+2");
service.delete(created.quoteNo(),"1",0);assertThat(service.page(new FleetQuoteModels.Query(1,20,null,null,null,null)).total()).isZero();
assertThat(service.history(created.quoteNo())).hasSize(2);
}
@@ -60,32 +60,9 @@ class InventoryQueryControllerTest {
verify(service).dashboard(eq(null), eq(query));
}
@Test
void balanceExportIsMaterializedBeforeReturningResponse() {
byte[] csv = "\uFEFFcustomerId\r\n10001\r\n".getBytes(java.nio.charset.StandardCharsets.UTF_8);
when(service.exportBalances("10001", query)).thenReturn(csv);
ResponseEntity<byte[]> response = controller.exportBalances(user(0L, "PLATFORM"), "10001", query);
org.junit.jupiter.api.Assertions.assertArrayEquals(csv, response.getBody());
org.junit.jupiter.api.Assertions.assertEquals("text/csv;charset=UTF-8",
response.getHeaders().getContentType().toString());
verify(service).exportBalances("10001", query);
}
@Test
void ledgerExportIsMaterializedBeforeReturningResponse() {
InventoryQueryRequests.LedgerQuery ledgerQuery = new InventoryQueryRequests.LedgerQuery(
1L, 20L, null, null, null, null, null, null, null, null, null, null, null, null, null);
byte[] csv = "\uFEFFoccurredAt\r\n".getBytes(java.nio.charset.StandardCharsets.UTF_8);
when(service.exportLedgers("10001", ledgerQuery)).thenReturn(csv);
ResponseEntity<byte[]> response = controller.exportLedgers(
user(0L, "PLATFORM"), "10001", ledgerQuery);
org.junit.jupiter.api.Assertions.assertArrayEquals(csv, response.getBody());
verify(service).exportLedgers("10001", ledgerQuery);
}
@Test
void unauthenticatedPositionDetailIsRejected() {
@@ -198,21 +198,21 @@ class InventoryQueryServiceTest {
InventoryQueryRequests.LedgerQuery query = new InventoryQueryRequests.LedgerQuery(
1L, 20L, null, null, null, null, null, null, null, null, null, null, null,
java.time.LocalDateTime.of(2026, 8, 18, 0, 0),
java.time.LocalDateTime.of(2026, 8, 17, 0, 0));
java.time.LocalDateTime.of(2026, 8, 17, 0, 0), null);
assertThrows(ResponseStatusException.class, () -> service.ledgers("100", query));
verify(mapper, never()).countLedgers(any(), any());
}
@Test
void balanceExportUsesInboundOrderPackageColumnsOnly() {
InventoryQueryRequests.BalanceQuery query = balanceQuery(null);
when(mapper.sourceBalances("100", query, 0, 10_000)).thenReturn(List.of());
String csv = new String(service.exportBalances("100", query), StandardCharsets.UTF_8);
assertTrue(csv.contains("inboundOrderNo,inboundCode,skuCode,productNameZh,productNameEn,totalPackages"));
assertFalse(csv.contains("Quantity"));
void balanceExportPagesSourceRowsInsteadOfGroupedBalances() {
var query = balanceQuery(null);
when(mapper.countSourceBalances("100", query)).thenReturn(201L);
when(mapper.sourceBalances("100", query, 0, 20)).thenReturn(List.of());
var page = service.exportBalancePage("100", query);
assertEquals(201L, page.total());
verify(mapper).sourceBalances("100", query, 0, 20);
verify(mapper, never()).countBalances(any(), any());
}
private static InventoryQueryRequests.BalanceQuery balanceQuery(String status) {
@@ -224,7 +224,7 @@ class InventoryQueryServiceTest {
InventoryQueryRequests.BalanceQuery balances = new InventoryQueryRequests.BalanceQuery(
null, null, null, null, null, null, null, null, null, null, null, null, null, null);
InventoryQueryRequests.LedgerQuery ledgers = new InventoryQueryRequests.LedgerQuery(
null, null, null, null, null, null, null, null, null, null, null, null, null, null, null);
null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null);
assertEquals(1L, balances.page());
assertEquals(20L, balances.size());
@@ -27,7 +27,7 @@ class PlatformOverseasQuoteControllerContractTest {
assertThat(authorities.get("create")).contains("platform:overseas-master-data:quote:create");
assertThat(authorities.get("update")).contains("platform:overseas-master-data:quote:edit");
assertThat(authorities.get("status")).contains("platform:overseas-master-data:quote:status");
assertThat(authorities.get("export")).contains("platform:overseas-master-data:quote:export");
assertThat(authorities).doesNotContainKey("export");
Method options = Stream.of(PlatformOverseasQuoteController.class.getDeclaredMethods())
.filter(method -> method.getName().equals("warehouseOptions"))
@@ -23,6 +23,6 @@ class PlatformVehicleControllerContractTest {
assertThat(authorities.get("create")).contains("platform:overseas-master-data:vehicle:create");
assertThat(authorities.get("update")).contains("platform:overseas-master-data:vehicle:edit");
assertThat(authorities.get("status")).contains("platform:overseas-master-data:vehicle:status");
assertThat(authorities.get("export")).contains("platform:overseas-master-data:vehicle:export");
assertThat(authorities).doesNotContainKey("export");
}
}
@@ -63,12 +63,7 @@ class VehicleApplicationServiceTest {
org.mockito.Mockito.verify(repository, org.mockito.Mockito.never()).create(org.mockito.ArgumentMatchers.any());
}
@Test void exportUsesPlateAsBusinessIdentifierAndIncludesAllCapacityDimensions() {
when(repository.list(org.mockito.ArgumentMatchers.any())).thenReturn(java.util.List.of(vehicle(3, 1000)));
String csv = new String(service.exportCsv(new VehicleModels.Query(1, 20, null, null, null, null)), java.nio.charset.StandardCharsets.UTF_8);
assertThat(csv).contains("最大托位,最大载重(kg),可用容积(m³)").contains("\"30\",\"18000\",\"90\"")
.doesNotContain("车辆ID");
}
private static VehicleModels.Update update(long version, long mileage) {
return new VehicleModels.Update(version, "ABC-001", "Box Truck", new BigDecimal("12.50"), mileage,
@@ -389,51 +389,9 @@ class LastlegOrderServiceTest {
assertEquals(25, page.getValue().getSize());
}
@Test
void exportUsesChineseBooleanAndMainStatusLabels() {
LastlegOrder order = order(11L, "100", "PENDING_ASSIGNMENT", null);
order.setAbnormal(false);
order.setDetained(true);
var resultPage = new com.baomidou.mybatisplus.extension.plugins.pagination.Page<LastlegOrder>(1, 1000);
resultPage.setRecords(List.of(order));
resultPage.setTotal(1);
when(orders.selectPage(any(), any())).thenReturn(resultPage);
String csv = new String(service.exportCsv(null, new LastlegOrderRequests.Query(
1L, 20L, null, null, null, null, null, null, null, null, null, null,
null, null, null, null, null)), StandardCharsets.UTF_8);
assertTrue(csv.contains("\"否\",\"是\""));
assertTrue(csv.contains("\"待分配\""));
assertFalse(csv.contains("\"false\""));
assertFalse(csv.contains("\"PENDING_ASSIGNMENT\""));
assertEquals("草稿", LastlegOrderStatus.labelOf("DRAFT"));
assertEquals("审核通过", LastlegOrderStatus.labelOf("APPROVED"));
assertEquals("已分配目的仓", LastlegOrderStatus.labelOf("DESTINATION_ASSIGNED"));
assertEquals("已生成入库计划", LastlegOrderStatus.labelOf("INBOUND_PLANNED"));
assertEquals("处理中", LastlegOrderStatus.labelOf("PROCESSING"));
}
@Test
void exportContinuesAfterThePaginationInterceptorCapsTheBatchSize() {
when(orders.selectPage(any(), any())).thenAnswer(invocation -> {
com.baomidou.mybatisplus.extension.plugins.pagination.Page<LastlegOrder> page = invocation.getArgument(0);
page.setSize(500);
page.setTotal(501);
long start = (page.getCurrent() - 1) * page.getSize() + 1;
page.setRecords(java.util.stream.LongStream.rangeClosed(start, Math.min(start + 499, 501))
.mapToObj(id -> order(id, "100", "PENDING_ASSIGNMENT", null)).toList());
return page;
});
var query = new LastlegOrderRequests.Query(1L, 20L, null, null, null, null, null, null,
null, null, null, null, null, null, null, null, LastlegOrderRequests.SailingStage.BEFORE_DEPARTURE);
String csv = new String(service.exportCsv("100", query), StandardCharsets.UTF_8);
assertEquals(502, csv.lines().count());
assertTrue(csv.contains("\"LM-501\""));
verify(orders, org.mockito.Mockito.times(2)).selectPage(any(), any());
}
@Test
void pageWarehouseFilterMatchesFrozenWarehouseName() {