This commit is contained in:
2026-08-26 15:58:54 +08:00
parent 57a2f6ef28
commit fd1a81594a
21 changed files with 406 additions and 20 deletions
@@ -128,6 +128,7 @@ class ReceivingContainerContractTest {
assertTrue(yaml.contains(prefix + "/current/skus/resolve:"));
assertTrue(yaml.contains(prefix + "/current/receipts/carton:"));
assertTrue(yaml.contains(prefix + "/current/complete:"));
assertTrue(yaml.contains(prefix + "/current/unload-photos/files/{fileId}/download:"));
assertFalse(yaml.contains("/api/pda/v1/last-leg/receiving-containers/{planNo}"));
String receivingPaths = yaml.substring(yaml.indexOf(prefix), yaml.indexOf("/api/platform/v1/last-leg/orders:"));
List<String> successResponses = receivingPaths.lines().map(String::trim)
@@ -139,6 +140,8 @@ class ReceivingContainerContractTest {
assertTrue(receivingPaths.contains("仅 PLATFORM WAREHOUSE_PICKER 可用"));
assertFalse(receivingPaths.contains("客户与操作账号均从 PDA Token 获取"));
assertTrue(yaml.contains("enum: [LICENSE_PLATE, CONTAINER_SEAL, DOOR_OPEN]"));
assertTrue(yaml.contains("photos: { type: array, minItems: 3, maxItems: 3"));
assertTrue(yaml.contains("required: [role, fileId, downloadPath]"));
assertTrue(yaml.contains("overseasWarehouseId: { type: string"));
assertTrue(yaml.contains("overseasWarehouseName: { type: string"));
assertFalse(receivingPaths.contains("photoFileIds"));
@@ -2,6 +2,7 @@ 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.gateway.UnloadPhotoFileGateway;
import com.baiyue.lastleg.inboundplan.pda.service.ReceivingContainerService;
import com.baiyue.lastleg.pda.security.PdaWarehouseIdentity;
import org.junit.jupiter.api.BeforeEach;
@@ -9,12 +10,16 @@ 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.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
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.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -55,6 +60,23 @@ class ReceivingContainerControllerTest {
.hasMessageContaining("403");
}
@Test
void downloadCurrentUnloadPhotoDelegatesToService() {
var user = picker();
when(service.downloadCurrentPhoto("42", "9999")).thenReturn(
new UnloadPhotoFileGateway.DownloadedFile("abc".getBytes(), "photo.jpg", MediaType.IMAGE_JPEG_VALUE));
ResponseEntity<byte[]> response = controller.downloadCurrentPhoto(user, "9999");
assertThat(response.getStatusCodeValue()).isEqualTo(200);
assertThat(response.getHeaders().getContentType()).isEqualTo(MediaType.IMAGE_JPEG);
assertThat(response.getHeaders().getFirst(HttpHeaders.CONTENT_DISPOSITION))
.startsWith("inline").contains("photo.jpg");
assertThat(response.getHeaders().getCacheControl()).isEqualTo("no-store");
assertThat(response.getBody()).containsExactly("abc".getBytes());
verify(service).downloadCurrentPhoto("42", "9999");
}
@Test
void pickerAuthorityWithoutWarehouseBindingIsForbidden() {
AuthenticatedUser unbound = new AuthenticatedUser(42L, 0L, 0L, 8L, "receiver", "PLATFORM", "PDA",
@@ -2,6 +2,8 @@ package com.baiyue.lastleg.inboundplan.pda.gateway;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.test.util.ReflectionTestUtils;
@@ -11,8 +13,10 @@ import org.springframework.web.server.ResponseStatusException;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.springframework.test.web.client.match.MockRestRequestMatchers.header;
import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo;
import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess;
import static org.springframework.test.web.client.response.MockRestResponseCreators.withStatus;
class HttpUnloadPhotoFileGatewayTest {
private HttpUnloadPhotoFileGateway gateway;
@@ -51,4 +55,32 @@ class HttpUnloadPhotoFileGatewayTest {
error -> assertThat(error.getStatusCode().value()).isEqualTo(400));
server.verify();
}
@Test
void downloadUsesBusinessOwnershipPathAndPreservesBlobMetadata() {
server.expect(requestTo(
"http://file-service/internal/file/v1/business-files/100/OVERSEAS_UNLOAD_PHOTO/8/9/content"))
.andExpect(header("X-Internal-Service-Token", "internal-token"))
.andRespond(withSuccess("photo-bytes".getBytes(), MediaType.IMAGE_JPEG)
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=door.jpg"));
UnloadPhotoFileGateway.DownloadedFile result = gateway.download("100", 8L, 9L);
assertThat(result.content()).containsExactly("photo-bytes".getBytes());
assertThat(result.fileName()).isEqualTo("door.jpg");
assertThat(result.contentType()).isEqualTo(MediaType.IMAGE_JPEG_VALUE);
server.verify();
}
@Test
void missingOwnedPhotoMapsToNotFound() {
server.expect(requestTo(
"http://file-service/internal/file/v1/business-files/100/OVERSEAS_UNLOAD_PHOTO/8/9/content"))
.andRespond(withStatus(HttpStatus.NOT_FOUND));
assertThatThrownBy(() -> gateway.download("100", 8L, 9L))
.isInstanceOfSatisfying(ResponseStatusException.class,
error -> assertThat(error.getStatusCode().value()).isEqualTo(404));
server.verify();
}
}
@@ -315,6 +315,11 @@ class ReceivingContainerServiceBehaviorTest {
ReceivingPhotoRole.DOOR_OPEN);
org.assertj.core.api.Assertions.assertThat(response.photos()).extracting("fileId")
.containsExactly("2092476334483931138", "2", "3");
org.assertj.core.api.Assertions.assertThat(response.photos()).extracting("downloadPath")
.containsExactly(
"/api/pda/v1/last-leg/receiving-containers/current/unload-photos/files/2092476334483931138/download",
"/api/pda/v1/last-leg/receiving-containers/current/unload-photos/files/2/download",
"/api/pda/v1/last-leg/receiving-containers/current/unload-photos/files/3/download");
verify(unloadPhotos).validate("100", 8L, 2092476334483931138L);
verify(unloadPhotos).validate("100", 8L, 2L);
verify(unloadPhotos).validate("100", 8L, 3L);
@@ -356,6 +361,35 @@ class ReceivingContainerServiceBehaviorTest {
verifyNoInteractions(unloadPhotos);
}
@Test
void currentSignedPhotoCanBeDownloadedOnlyThroughItsOperatorCustomerAndSessionScope() {
ReceivingContainerSession current = session("CONT-A", "SIGNED");
current.setSignPhotos("[{\"role\":\"LICENSE_PLATE\",\"fileId\":\"9\"},"
+ "{\"role\":\"CONTAINER_SEAL\",\"fileId\":\"10\"},"
+ "{\"role\":\"DOOR_OPEN\",\"fileId\":\"11\"}]");
when(sessions.findActiveByOperator("OPERATOR:7")).thenReturn(current);
UnloadPhotoFileGateway.DownloadedFile expected =
new UnloadPhotoFileGateway.DownloadedFile("image".getBytes(), "plate.jpg", "image/jpeg");
when(unloadPhotos.download("100", 8L, 9L)).thenReturn(expected);
assertThat(service.downloadCurrentPhoto("7", "9")).isSameAs(expected);
verify(unloadPhotos).download("100", 8L, 9L);
}
@Test
void photoOutsideCurrentSignedRoleBindingsIsForbiddenBeforeFileServiceCall() {
ReceivingContainerSession current = session("CONT-A", "SIGNED");
current.setSignPhotos("[{\"role\":\"LICENSE_PLATE\",\"fileId\":\"9\"},"
+ "{\"role\":\"CONTAINER_SEAL\",\"fileId\":\"10\"},"
+ "{\"role\":\"DOOR_OPEN\",\"fileId\":\"11\"}]");
when(sessions.findActiveByOperator("OPERATOR:7")).thenReturn(current);
assertThatThrownBy(() -> service.downloadCurrentPhoto("7", "12"))
.isInstanceOfSatisfying(ResponseStatusException.class,
error -> assertThat(error.getStatusCode().value()).isEqualTo(403));
verifyNoInteractions(unloadPhotos);
}
@Test
void bulkRejectsSkuOverReceiptBeforeWarehouseLookup() {
stubCurrent("FBA", "CONT-A");