fix(lastleg): complete relabel document contract and regression gates

This commit is contained in:
2026-09-09 12:54:14 +08:00
parent ac3ce3703e
commit 655237e37f
19 changed files with 474 additions and 23 deletions
@@ -34,6 +34,33 @@ class RelabelApplicationControllerTest {
assertThrows(ResponseStatusException.class, () -> controller.create(platform(), "0", request()));
}
@Test
void uploadRejectsMerchantAndUnknownScopeBeforeResolvingCustomer() {
var merchant = new AuthenticatedUser(7L, 101L, 0L, 8L, "customer", "MERCHANT", "WEB", Set.of());
assertThrows(ResponseStatusException.class, () -> controller.uploadFile(merchant, "RL-1", "TOTAL", null, null));
assertThrows(ResponseStatusException.class, () -> controller.uploadFile(platform(), "RL-1", "PHOTO", null, null));
assertThrows(ResponseStatusException.class, () -> controller.uploadFile(platform(), "RL-1", "TOTAL", 22L, null));
org.mockito.Mockito.verifyNoInteractions(service);
}
@Test
void uploadUsesCustomerResolvedFromApplication() {
org.mockito.Mockito.when(service.customerId("RL-1")).thenReturn("101");
var file = new org.springframework.mock.web.MockMultipartFile("file", "labels.pdf", "application/pdf", "%PDF-1.7".getBytes());
controller.uploadFile(platform(), "RL-1", "ITEM", 22L, file);
verify(service).uploadItemFile("101", "RL-1", 22L, 7L, file);
}
@Test
void downloadUsesOwnedBindingAndForbidsCaching() {
org.mockito.Mockito.when(service.customerId("RL-1")).thenReturn("101");
org.mockito.Mockito.when(service.downloadFile("101", "RL-1", "TOTAL", null, 99L))
.thenReturn(new com.baiyue.lastleg.relabel.gateway.RelabelDocumentFileGateway.DownloadedFile("%PDF-1.7".getBytes(), "labels.pdf", "application/pdf"));
var response = controller.downloadFile(platform(), "RL-1", 99L, "TOTAL", null);
org.junit.jupiter.api.Assertions.assertEquals("no-store", response.getHeaders().getCacheControl());
org.junit.jupiter.api.Assertions.assertEquals("application/pdf", response.getHeaders().getContentType().toString());
}
private RelabelRequests.Create request() {
return new RelabelRequests.Create("CUSTOMER-1", "WH-1",
LocalDateTime.of(2026, 8, 20, 10, 0), "Li", "13800000000", null, null, "key-1",
@@ -230,7 +230,9 @@ class RelabelApplicationServiceTest {
service.create("10001", "operator-1", request);
verify(inventoryCommands, times(2)).hold(eq("10001"), eq("operator-1"), any());
ArgumentCaptor<InventoryCommandRequests.Move> holds = ArgumentCaptor.forClass(InventoryCommandRequests.Move.class);
verify(inventoryCommands, times(2)).hold(eq("10001"), eq("operator-1"), holds.capture());
assertEquals(2, holds.getAllValues().stream().map(InventoryCommandRequests.Move::idempotencyKey).distinct().count());
verify(items, times(2)).insert(any(RelabelItem.class));
}
@@ -0,0 +1,75 @@
package com.baiyue.lastleg.relabel.service;
import com.baiyue.lastleg.relabel.entity.*;
import com.baiyue.lastleg.relabel.mapper.*;
import com.baiyue.lastleg.relabel.gateway.RelabelDocumentFileGateway;
import com.baiyue.lastleg.mapper.LastlegOrderMapper;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.web.server.ResponseStatusException;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.Mockito.*;
import static org.mockito.ArgumentMatchers.*;
class RelabelDocumentServiceTest {
final RelabelApplicationMapper applications=mock(RelabelApplicationMapper.class);
final RelabelItemMapper items=mock(RelabelItemMapper.class);
final RelabelDocumentFileGateway files=mock(RelabelDocumentFileGateway.class);
final RelabelApplicationService service=new RelabelApplicationService();
final RelabelApplication application=new RelabelApplication();
final MockMultipartFile pdf=new MockMultipartFile("file","labels.pdf","application/pdf","%PDF-1.7\nfixture".getBytes());
RelabelDocumentServiceTest(){
application.setId(11L);application.setCustomerId("101");application.setRelabelNo("RL-1");application.setStatus("PENDING_PROCESSING");
ReflectionTestUtils.setField(service,"applications",applications);ReflectionTestUtils.setField(service,"items",items);
ReflectionTestUtils.setField(service,"documentFiles",files);ReflectionTestUtils.setField(service,"orders",mock(LastlegOrderMapper.class));
ReflectionTestUtils.setField(service,"histories",mock(RelabelStatusHistoryMapper.class));ReflectionTestUtils.setField(service,"objectMapper",new ObjectMapper());
when(applications.lockNo("101","RL-1")).thenReturn(application);when(applications.findNo("101","RL-1")).thenReturn(application);
}
@Test void totalUploadPersistsReturnedMetadataAndExposesSummary(){
when(files.upload("101","OVERSEAS_RELABEL_TOTAL_DOCUMENT","11",7L,pdf)).thenReturn(new RelabelDocumentFileGateway.UploadedFile(99L,"labels.pdf","application/pdf",15));
var result=service.uploadTotalFile("101","RL-1",7L,pdf);
assertThat(result.application().totalFileId()).isEqualTo("99");
assertThat(result.application().totalFileName()).isEqualTo("labels.pdf");
assertThat(result.application().totalFileUrl()).endsWith("/files/99/download?scope=TOTAL");
verify(applications).updateById(application);
}
@Test void itemUploadBindsOwnCustomerApplicationAndDistinctCategory(){
RelabelItem item=item();when(items.selectById(22L)).thenReturn(item);
when(files.upload("101","OVERSEAS_RELABEL_ITEM_DOCUMENT","22",7L,pdf)).thenReturn(new RelabelDocumentFileGateway.UploadedFile(98L,"item.pdf","application/pdf",15));
service.uploadItemFile("101","RL-1",22L,7L,pdf);
assertThat(item.getRelabelFileId()).isEqualTo("98");assertThat(item.getRelabelFileUrl()).endsWith("scope=ITEM&itemId=22");
verify(items).updateById(item);
}
@Test void rejectsCrossCustomerAndCrossApplicationItemsBeforeFileUpload(){
assertThatThrownBy(()->service.uploadTotalFile("102","RL-1",7L,pdf)).isInstanceOf(ResponseStatusException.class);
RelabelItem item=item();item.setApplicationId(12L);when(items.selectById(22L)).thenReturn(item);
assertThatThrownBy(()->service.uploadItemFile("101","RL-1",22L,7L,pdf)).hasMessageContaining("不属于");
item.setApplicationId(11L);item.setCustomerId("102");
assertThatThrownBy(()->service.uploadItemFile("101","RL-1",22L,7L,pdf)).hasMessageContaining("不属于");
verifyNoInteractions(files);
}
@Test void rejectsFakePdfAndTerminalChangesBeforeUpload(){
var fake=new MockMultipartFile("file","labels.pdf","application/pdf","not pdf".getBytes());
assertThatThrownBy(()->service.uploadTotalFile("101","RL-1",7L,fake)).hasMessageContaining("不是PDF");
application.setStatus("COMPLETED");
assertThatThrownBy(()->service.uploadTotalFile("101","RL-1",7L,pdf)).hasMessageContaining("不允许修改资料");
verifyNoInteractions(files);
}
@Test void downloadRequiresCurrentBindingAndExactScope(){
application.setTotalFileId("99");
service.downloadFile("101","RL-1","TOTAL",null,99L);
verify(files).download("101","OVERSEAS_RELABEL_TOTAL_DOCUMENT","11",99L);
assertThatThrownBy(()->service.downloadFile("101","RL-1","TOTAL",null,98L)).hasMessageContaining("未绑定");
assertThatThrownBy(()->service.downloadFile("101","RL-1","other",null,99L)).hasMessageContaining("资料范围");
assertThatThrownBy(()->service.downloadFile("101","RL-1","ITEM",null,99L)).hasMessageContaining("指定明细");
verifyNoMoreInteractions(files);
}
@Test void fileFailureDoesNotPersistBindings(){
when(files.upload(anyString(),anyString(),anyString(),anyLong(),any())).thenThrow(new IllegalStateException("offline"));
assertThatThrownBy(()->service.uploadTotalFile("101","RL-1",7L,pdf)).hasMessage("offline");
verify(applications,never()).updateById(any(RelabelApplication.class));assertThat(application.getTotalFileId()).isNull();
}
private RelabelItem item(){RelabelItem item=new RelabelItem();item.setId(22L);item.setApplicationId(11L);item.setCustomerId("101");return item;}
}