import { beforeEach, describe, expect, it, vi } from 'vitest' import { readFileSync } from 'node:fs' import { resolve } from 'node:path' import { http } from '@/shared/api/http' import { adjustSmartLoadingItem, assignPendingReviewSmartLoadingOrder, assignSmartLoadingGroup, assignSmartLoadingItem, assignSmartLoadingPallet, getContainer, listContainers, listSmartLoadingCandidates, listSmartLoadingWorkbenchContainers, removeAllUnpickedSmartLoadingItems, removeSmartLoadingGroup, removeSmartLoadingItem, removeSmartLoadingPallet, recommendSmartLoading, getContainerClearance, downloadContainerClearance, uploadContainerClearance, completeContainerClearance, getContainerOverview, updateContainerPickupCode, markContainerException, cancelContainerException, uploadContainerDocument, downloadContainerDocument, downloadContainerPdaPhoto, getCustomerContainerTransitSummary, createContainerSimulation, linkContainerOceanBooking, unlinkContainerOceanBooking, updateContainer, } from './platformContainerLoading' vi.mock('@/shared/api/http', () => ({ http: { get: vi.fn(), post: vi.fn(), put: vi.fn(), patch: vi.fn(), delete: vi.fn() }, unwrapApiResponse: (response: { data: T }) => response.data, })) const container = { id: 9007199254740993, planId: 9007199254740994, planNo: 'LP-001', warehouseId: 9007199254740995, shippingCompanyId: 9007199254740996, terminalTruckingCompanyId: '9007199254740998', terminalTruckingCompany: '测试拖车供应商', sourceType: 'SIMULATION', recordStatus: 'ACTIVE', mergedIntoContainerBookingId: null, channelGroupCode: 'EXX', channelCode: 'NY_EXX', channelName: '纽约EXX海铁', exceptionActive: true, destinationClearanceDocumentStatus: 'COMPLETED', customsDeclarationDocumentStatus: 'UPLOADED', deliveryOrderDocumentStatus: 'UPLOADED', pickupCode: '00123456', pickupCodeUpdatedBy: 9007199254740999, unloadingCompletedAt: '2026-08-20T10:00:00', unloadingPhotoUploadedAt: '2026-08-20T10:15:00', estimatedEmptyReportedAt: '2026-08-21T09:00:00', actualEmptyReportedAt: null, emptyReportSourceVersion: '9007199254740997', version: '2', capacityCbm: '68', maxWeightKg: '19500' } const plan = { id: 9007199254740993, bookingId: 9007199254740994, version: '3', status: 'LOADING', canAssign: true, canEdit: true, blockedReason: null, finalCompletionStatus: 'NOT_STARTED', pdaTaskId: 9007199254740996, pdaTaskStatus: 'LOADING', pdaTaskVersion: '4', items: [{ id: 9007199254740995, loadingPlanId: 9007199254740993, accountId: 1, inboundId: 2, inboundDetailId: 3, orderId: 4, destinationWarehouseId: 5, packageCount: '6', version: '1', pickedPackageCount: '2', minPackageCount: '2', canAdjust: true, canRemove: false }], } describe('platform container loading API', () => { beforeEach(() => vi.clearAllMocks()) it('uses the customer transit summary and preserves fractional TEU', async () => { vi.mocked(http.get).mockResolvedValueOnce({ code: '200', data: { customerId: '9007199254740993', containerCount: 2, totalTeu: '2.25', unknownContainerCount: 0, asOf: '2026-08-13T12:00:00', definition: '已开船未到仓' } }) const result = await getCustomerContainerTransitSummary('9007199254740993') expect(http.get).toHaveBeenCalledWith('/platform/v1/customers/9007199254740993/container-transit-summary') expect(result).toMatchObject({ customerId: '9007199254740993', containerCount: 2, totalTeu: '2.25', unknownContainerCount: 0 }) }) it('normalizes paged container ids and numeric metadata', async () => { vi.mocked(http.get).mockResolvedValueOnce({ code: '200', data: { records: [{ container, workbenchStage: 'PRE_LOADING', statusSummary: { loadingStatus: 'WAIT_LOADING' }, allowedActions: ['EDIT_CONTAINER'], actualFacts: {}, terminalDrayage: null }], page: '1', size: '20', total: '1', pages: '1' } }) const result = await listContainers({ page: 1, size: 20 }) expect(result.records[0]).toMatchObject({ id: String(container.id), planId: String(container.planId), planNo: 'LP-001', warehouseId: String(container.warehouseId), terminalTruckingCompanyId: String(container.terminalTruckingCompanyId), terminalTruckingCompany: '测试拖车供应商', channelCode: 'NY_EXX', channelName: '纽约EXX海铁', exceptionActive: true, destinationClearanceDocumentStatus: 'COMPLETED', customsDeclarationDocumentStatus: 'UPLOADED', deliveryOrderDocumentStatus: 'UPLOADED', pickupCode: '00123456', pickupCodeUpdatedBy: String(container.pickupCodeUpdatedBy), unloadingCompletedAt: '2026-08-20T10:00:00', unloadingPhotoUploadedAt: '2026-08-20T10:15:00', estimatedEmptyReportedAt: '2026-08-21T09:00:00', actualEmptyReportedAt: null, emptyReportSourceVersion: '9007199254740997', version: 2, capacityCbm: 68 }) expect(http.get).toHaveBeenCalledWith('/platform/v1/containers/domestic', { params: { page: 1, size: 20, scope: 'ACTIVE' } }) }) it('uses the full-edit request shape without retaining the retired container transport endpoint', async () => { vi.mocked(http.put).mockResolvedValue({ code: '200', data: container }) const data = { warehouseId: '8', warehouseName: '义乌仓', containerType: '40HQ', containerNo: null, billOfLadingNo: null, vesselName: null, voyageNo: null, shippingCompanyId: null, shippingCompanyName: null, departurePortCode: null, departurePortName: null, destinationPortCode: null, destinationPortName: null, destinationWarehouseCode: null, destinationWarehouseName: null, capacityCbm: 68, maxWeightKg: 19500, terminalTruckingCompanyId: null, terminalTruckingCompany: null, loadingAt: null, estimatedSailingAt: null, actualSailingAt: null, estimatedPortArrivalAt: null, actualPortArrivalAt: null, estimatedWarehouseArrivalAt: null, actualWarehouseArrivalAt: null } await updateContainer('7', 2, data) expect(http.put).toHaveBeenCalledWith('/platform/v1/containers/7', { version: 2, data }) expect(readFileSync(resolve(process.cwd(), 'src/modules/container-loading/api/platformContainerLoading.ts'), 'utf8')).not.toContain('/transport') }) it.each(['DOMESTIC', 'OVERSEAS'] as const)('reads the %s detail envelope and exact long identifiers in one request', async (workbench) => { vi.mocked(http.get).mockResolvedValueOnce({ code: '200', data: { container: { ...container, id: '9007199254740993', planId: '9007199254740995' }, workbenchStage: 'PRE_LOADING', allowedActions: ['EDIT_CONTAINER'], statusSummary: {}, actualFacts: null, terminalDrayage: null, } }) expect(await getContainer('9007199254740993', workbench)).toMatchObject({ id: '9007199254740993', planId: '9007199254740995', allowedActions: ['EDIT_CONTAINER'] }) expect(http.get).toHaveBeenCalledExactlyOnceWith(`/platform/v1/containers/${workbench.toLowerCase()}/9007199254740993`) }) it('keeps empty overseas history pages and query filters without fetching individual details', async () => { vi.mocked(http.get).mockResolvedValueOnce({ code: '200', data: { records: [], page: 2, size: 20, total: 0, pages: 0 } }) const query = { page: 2, size: 20, warehouseId: '9007199254740993', cabinetNo: 'CB-TEST', scope: 'HISTORY' as const } expect(await listContainers(query, 'OVERSEAS')).toEqual({ records: [], page: 2, size: 20, total: 0, pages: 0 }) expect(http.get).toHaveBeenCalledExactlyOnceWith('/platform/v1/containers/overseas', { params: query }) }) it('propagates a failed workbench request without retrying retired routes', async () => { const failure = new Error('workbench unavailable') vi.mocked(http.get).mockRejectedValueOnce(failure) await expect(listContainers({ page: 1, size: 20 })).rejects.toBe(failure) expect(http.get).toHaveBeenCalledTimes(1) }) it('rejects a retired flat response instead of rendering incorrect container rows', async () => { vi.mocked(http.get).mockResolvedValueOnce({ code: '200', data: { records: [container], page: 1, size: 20, total: 1, pages: 1 } }) await expect(listContainers({ page: 1, size: 20 })).rejects.toThrow() expect(http.get).toHaveBeenCalledTimes(1) }) it('uses the frozen simulation create and ocean booking link contracts', async () => { vi.mocked(http.post).mockResolvedValue({ code: '200', data: { ...container, oceanBookingId: 101, oceanBookingRequirementId: 202 } }) vi.mocked(http.delete).mockResolvedValue({ code: '200', data: { ...container, oceanBookingId: null, oceanBookingRequirementId: null } }) await createContainerSimulation({ channelGroupCode: 'EXX', warehouseId: '8', containerType: '40HQ', capacityCbm: 68, maxWeightKg: 19500 }) await linkContainerOceanBooking('7', { oceanBookingId: '101', requirementId: '202', containerNo: 'MSCU1234567', expectedVersion: 3 }) await unlinkContainerOceanBooking('7', 4, '空柜改配') expect(http.post).toHaveBeenNthCalledWith(1, '/platform/v1/containers', { channelGroupCode: 'EXX', warehouseId: '8', containerType: '40HQ', capacityCbm: 68, maxWeightKg: 19500 }) expect(http.post).toHaveBeenNthCalledWith(2, '/platform/v1/containers/7/ocean-booking-link', { oceanBookingId: '101', requirementId: '202', containerNo: 'MSCU1234567', expectedVersion: 3 }) expect(http.delete).toHaveBeenCalledWith('/platform/v1/containers/7/ocean-booking-link', { data: { expectedVersion: 4, reason: '空柜改配' } }) }) it('normalizes every long id in candidates and plans', async () => { vi.mocked(http.get) .mockResolvedValueOnce({ code: '200', data: { container, workbenchStage: 'PRE_LOADING', statusSummary: {}, allowedActions: [], actualFacts: {}, terminalDrayage: null } }) .mockResolvedValueOnce({ code: '200', data: { records: [{ accountId: 1, inboundId: 2, orderId: 4, orderNo: 'ORD-1', inboundCode: 'IN-1', orderStatusSnapshot: 'WAIT_SPLIT', expectedArrivalAt: '2026-09-10T09:30:00', warehouseId: 5, destinationWarehouseId: 6, channelCode: 'NY_EXX', channelName: '纽约EXX海铁', customerName: '示例企业', goodsSummary: '服装、鞋类', availablePackageCount: '7', unitGrossWeightKg: '12.5', unitNetWeightKg: '11.8', averageUnitVolumeCbm: '0.187', availableVolumeCbm: '1.309', estimatedVolumeCbm: '2.5', coLoadGroupId: 9007199254740999n, groupNo: 'CLG-9007199254740999', memberCount: '3', groupTotalBoxes: '21', locked: false, blockReason: null, groupMembers: [] }], page: '1', size: '20', total: '1', pages: '1' } }) expect((await getContainer('9')).id).toBe(String(container.id)) expect(await listSmartLoadingCandidates({ channelGroupCodes: ['EXX', 'HEDE'], warehouseId: '5', orderStatuses: ['PENDING_REVIEW', 'WAIT_MEASURE'], page: 1, size: 20 })).toMatchObject({ records: [{ accountId: '1', customerId: '1', inboundId: '2', inboundCode: 'IN-1', orderStatusSnapshot: 'WAIT_SPLIT', expectedArrivalAt: '2026-09-10T09:30:00', destinationWarehouseId: '6', channelCode: 'NY_EXX', channelName: '纽约EXX海铁', goodsSummary: '服装、鞋类', availablePackageCount: 7, unitGrossWeightKg: 12.5, unitNetWeightKg: 11.8, averageUnitVolumeCbm: 0.187, availableVolumeCbm: 1.309, estimatedVolumeCbm: 2.5, coLoadGroupId: '9007199254740999', groupNo: 'CLG-9007199254740999', memberCount: 3, groupTotalBoxes: 21, locked: false, blockReason: null }], total: 1 }) const candidateParams = vi.mocked(http.get).mock.calls.at(-1)?.[1]?.params as URLSearchParams expect(candidateParams.getAll('orderStatuses')).toEqual(['PENDING_REVIEW', 'WAIT_MEASURE']) expect(candidateParams.has('orderStatus')).toBe(false) const params = vi.mocked(http.get).mock.calls.at(-1)?.[1]?.params as URLSearchParams expect(params.getAll('channelGroupCodes')).toEqual(['EXX', 'HEDE']) expect(params.get('warehouseId')).toBe('5') expect(params.has('bookingId')).toBe(false) }) it('sends plan and item versions plus operation identifiers', async () => { vi.mocked(http.post).mockResolvedValue({ code: '200', data: plan }) vi.mocked(http.patch).mockResolvedValue({ code: '200', data: plan }) vi.mocked(http.delete).mockResolvedValue({ code: '200', data: plan }) await assignSmartLoadingItem('10', { inboundId: '2', packageCount: 4, expectedVersion: 5 }) await adjustSmartLoadingItem('10', '11', 6, 7, 8) await removeSmartLoadingItem('10', '11', 9, 10) expect(vi.mocked(http.post).mock.calls[0]?.[0]).toBe('/platform/v1/first-leg/smart-loading/plans/10/inbound-assignments') expect(vi.mocked(http.post).mock.calls[0]?.[1]).toMatchObject({ expectedVersion: 5, operationId: expect.stringContaining('assign-inbound-') }) expect(vi.mocked(http.patch).mock.calls[0]?.[1]).toMatchObject({ expectedPlanVersion: 7, expectedItemVersion: 8, operationId: expect.stringContaining('adjust-') }) expect(vi.mocked(http.delete).mock.calls[0]?.[1]).toMatchObject({ data: { expectedPlanVersion: 9, expectedItemVersion: 10, operationId: expect.stringContaining('remove-') } }) }) it('assigns a sealed pallet through the whole-pallet command', async () => { vi.mocked(http.post).mockResolvedValue({ code: '200', data: plan }) await assignSmartLoadingPallet('10', { palletId: '9007199254740998', expectedVersion: 5 }) expect(http.post).toHaveBeenCalledWith( '/platform/v1/first-leg/smart-loading/plans/10/pallet-assignments', { palletId: '9007199254740998', expectedVersion: 5, operationId: expect.stringContaining('assign-pallet-') }, ) }) it('limits smart recommendation to the current customer and inbound filters', async () => { vi.mocked(http.post).mockResolvedValue({ code: '200', data: plan }) await recommendSmartLoading('10', 5, { customerId: '9007199254740998', inboundCode: ' EXXHWK2609140162 ', }) expect(http.post).toHaveBeenCalledWith( '/platform/v1/first-leg/smart-loading/plans/10/recommend', expect.objectContaining({ expectedVersion: 5, customerId: '9007199254740998', inboundCode: 'EXXHWK2609140162', operationId: expect.stringContaining('recommend-'), }), ) }) it('removes a sealed pallet through the whole-pallet command', async () => { vi.mocked(http.delete).mockResolvedValue({ code: '200', data: plan }) await removeSmartLoadingPallet('10', '9007199254740998', 6) expect(http.delete).toHaveBeenCalledWith( '/platform/v1/first-leg/smart-loading/plans/10/pallet-assignments/9007199254740998', { data: { expectedPlanVersion: 6, operationId: expect.stringContaining('remove-pallet-') } }, ) }) it('uses the dedicated provisional assignment command for a pending-review order', async () => { vi.mocked(http.post).mockResolvedValue({ code: '200', data: plan }) await assignPendingReviewSmartLoadingOrder('10', { customerId: '21', orderId: '31', inboundCode: 'EXXHK2609020361', packageCount: 100, expectedVersion: 5, }) expect(http.post).toHaveBeenCalledWith( '/platform/v1/first-leg/smart-loading/plans/10/pending-review-assignments', expect.objectContaining({ customerId: '21', orderId: '31', inboundCode: 'EXXHK2609020361', packageCount: 100, expectedVersion: 5, operationId: expect.stringContaining('assign-pending-review-'), }), ) }) it('queries workbench containers with the same global filters and excludes the retired booking parameter', async () => { vi.mocked(http.get).mockResolvedValueOnce({ code: '200', data: { records: [{ bookingId: 9007199254740993n, cabinetNo: 'CAB-1', containerNo: null, warehouseId: 8, warehouseName: '义乌仓', channelCode: 'MATSON_CLX_TRUCK', channelName: '美森正班海卡', containerType: '40HQ', planId: 9007199254740994n, planNo: 'LP-1', planStatus: 'EDITING', version: '2' }], page: '1', size: '200', total: '1', pages: '1', } }) const result = await listSmartLoadingWorkbenchContainers({ channelGroupCodes: ['MATSON_CLX', 'EXX'], customerId: '12', inboundCode: 'IN-1', createdStart: '2026-09-01', createdEnd: '2026-09-03', expectedArrivalStart: '2026-09-04', expectedArrivalEnd: '2026-09-06', orderStatuses: ['WAIT_SPLIT'], page: 1, size: 200 }) expect(result).toMatchObject({ records: [{ bookingId: '9007199254740993', warehouseId: '8', planId: '9007199254740994', version: 2 }], total: 1 }) const params = vi.mocked(http.get).mock.calls.at(-1)?.[1]?.params as URLSearchParams expect(params.getAll('channelGroupCodes')).toEqual(['MATSON_CLX', 'EXX']) expect(params.get('customerId')).toBe('12') expect(params.get('inboundCode')).toBe('IN-1') expect(params.get('createdStart')).toBe('2026-09-01') expect(params.get('createdEnd')).toBe('2026-09-03') expect(params.get('expectedArrivalStart')).toBe('2026-09-04') expect(params.get('expectedArrivalEnd')).toBe('2026-09-06') expect(params.has('orderStatus')).toBe(false) expect(params.has('orderStatuses')).toBe(false) expect(params.has('bookingId')).toBe(false) }) it('uses only the atomic group assign and whole-group remove endpoints for co-load groups', async () => { vi.mocked(http.post).mockResolvedValue({ code: '200', data: plan }) vi.mocked(http.delete).mockResolvedValue({ code: '200', data: plan }) await assignSmartLoadingGroup('10', '2093000000000000006', 5) await removeSmartLoadingGroup('10', '2093000000000000001', 6) expect(http.post).toHaveBeenCalledWith( '/platform/v1/first-leg/smart-loading/plans/10/group-assignments', { inboundId: '2093000000000000006', expectedVersion: 5, operationId: expect.stringContaining('assign-coload-group-') }, ) expect(http.delete).toHaveBeenCalledWith( '/platform/v1/first-leg/smart-loading/plans/10/group-assignments/2093000000000000001', { data: { expectedPlanVersion: 6, operationId: expect.stringContaining('remove-coload-group-') } }, ) }) it('uses one endpoint to remove all unpicked plan items', async () => { vi.mocked(http.delete).mockResolvedValue({ code: '200', data: plan }) await removeAllUnpickedSmartLoadingItems('10', 6) expect(http.delete).toHaveBeenCalledWith( '/platform/v1/first-leg/smart-loading/plans/10/unpicked-items', { data: { expectedPlanVersion: 6, operationId: expect.stringContaining('remove-unpicked-') } }, ) }) it('uses stable container id and versioned clearance document actions', async () => { vi.spyOn(http, 'get').mockResolvedValue({ code: '200', data: { id: 4, documentType: 'CUSTOMS_CLEARANCE', fileId: 8, fileName: 'clearance.pdf', version: 2 } }) vi.spyOn(http, 'post').mockResolvedValue({ code: '200', data: { id: 4, documentType: 'CUSTOMS_CLEARANCE', fileId: 8, fileName: 'clearance.pdf', version: 3 } }) expect((await getContainerClearance('7'))?.fileId).toBe('8') const file = new File(['clearance'], 'clearance.pdf', { type: 'application/pdf' }) await uploadContainerClearance('7', file, 2, '上传资料') await completeContainerClearance('7', 3, '完成清关') expect(vi.mocked(http.get)).toHaveBeenCalledWith('/platform/v1/containers/7/clearance') expect(vi.mocked(http.post).mock.calls[0]?.[0]).toBe('/platform/v1/containers/7/clearance/document/upload') const body = vi.mocked(http.post).mock.calls[0]?.[1] as FormData expect(body.get('file')).toBe(file) expect(body.get('expectedVersion')).toBe('2') expect(body.get('operationReason')).toBe('上传资料') expect(vi.mocked(http.post).mock.calls[0]?.[2]).toMatchObject({ headers: { 'Content-Type': 'multipart/form-data', 'Idempotency-Key': expect.any(String) } }) expect(vi.mocked(http.post).mock.calls[1]?.[0]).toBe('/platform/v1/containers/7/clearance/complete') }) it('normalizes aggregate ids and uses the frozen overview and exception contracts', async () => { vi.mocked(http.get).mockResolvedValueOnce({ code: '200', data: { container: { id: 9007199254740993, warehouseId: 9007199254740994, version: '4' }, loadingPlanId: 9007199254740995, pda: { taskId: 9007199254740996, loadingPlanId: 9007199254740995, containerBookingId: 9007199254740993, taskVersion: '2', itemCount: '4', completedItemCount: '3', unresolvedItemCount: '1', photoCounts: [{ evidenceType: 'EMPTY_CONTAINER', count: '2' }], photos: [{ evidenceId: 20, tenantId: 21, inboundId: 22, fileId: 23, downloadPath: '/api/platform/v1/containers/9007199254740993/pda/photos/23/download' }], exceptionReasons: [] }, documents: [{ id: 9007199254740997, documentType: 'DELIVERY_ORDER', fileId: 9007199254740998, uploadedBy: 9, version: '2' }], exception: { active: false, markedBy: null }, exceptionHistory: [{ id: 10, operatorId: 11 }], timeline: [{ id: '2092438843630397001', eventId: '2092438843630397002', idempotencyKey: '2092438843630397003', operatorId: 12, eventType: 'PUSH_PRELOAD', detail: '推送预装清单' }], } }) const result = await getContainerOverview('7') expect(result).toMatchObject({ container: { id: expect.any(String), warehouseId: expect.any(String), version: 4 }, loadingPlanId: expect.any(String), pda: { taskId: expect.any(String), itemCount: 4, photoCounts: [{ count: 2 }] }, documents: [{ fileId: expect.any(String), version: 2 }], timeline: [{ id: '2092438843630397001', eventId: '2092438843630397002', idempotencyKey: '2092438843630397003', operatorId: '12' }] }) expect(http.get).toHaveBeenCalledWith('/platform/v1/containers/7/overview') vi.mocked(http.post).mockResolvedValueOnce({ code: '200', data: null }) vi.mocked(http.delete).mockResolvedValueOnce({ code: '200', data: null }) await markContainerException('7', 2, '查验') await cancelContainerException('7', 3, '已解除') expect(http.post).toHaveBeenCalledWith('/platform/v1/containers/7/exception', { expectedVersion: 2, reason: '查验' }, { headers: { 'Idempotency-Key': expect.stringContaining('container-exception-mark-') } }) expect(http.delete).toHaveBeenCalledWith('/platform/v1/containers/7/exception', { data: { expectedVersion: 3, reason: '已解除' }, headers: { 'Idempotency-Key': expect.stringContaining('container-exception-cancel-') } }) }) it('uploads only DO or PU through the logistics endpoint and downloads through the protected proxy', async () => { vi.mocked(http.post).mockResolvedValueOnce({ code: '200', data: { id: 1, documentType: 'DELIVERY_ORDER', fileId: 2, fileName: 'do.pdf', version: 1 } }) vi.mocked(http.get).mockResolvedValueOnce(new Blob(['do'])) const file = new File(['do'], 'do.pdf') await uploadContainerDocument('7', 'DELIVERY_ORDER', file, 0, '上传DO') await downloadContainerDocument('7', 'DELIVERY_ORDER', '2') expect(vi.mocked(http.post).mock.calls[0]?.[0]).toBe('/platform/v1/containers/7/documents/DELIVERY_ORDER/upload') expect(http.get).toHaveBeenCalledWith('/platform/v1/containers/7/documents/DELIVERY_ORDER/2/download', { responseType: 'blob' }) }) it('saves the PU numeric code as a string with the container version', async () => { vi.mocked(http.put).mockResolvedValueOnce({ code: '200', data: { container: { id: 7, warehouseId: 8, pickupCode: '00123456', pickupCodeUpdatedBy: 9, version: 3 }, documents: [], exception: {}, exceptionHistory: [], timeline: [] } }) const result = await updateContainerPickupCode('7', '00123456', 2) expect(http.put).toHaveBeenCalledWith('/platform/v1/containers/7/pickup-code', { pickupCode: '00123456', expectedVersion: 2 }) expect(result.container).toMatchObject({ pickupCode: '00123456', pickupCodeUpdatedBy: '9', version: 3 }) }) it('normalizes the gateway /api prefix once and accepts only exact protected container photo paths', async () => { vi.mocked(http.get).mockResolvedValue(new Blob(['photo'])) const longPath = '/api/platform/v1/containers/2092437461590446082/pda/photos/2092439032600797185/download' await downloadContainerPdaPhoto(longPath) await downloadContainerPdaPhoto('/platform/v1/containers/2092437461590446082/pda/photos/2092439032600797185/download') expect(http.get).toHaveBeenNthCalledWith(1, '/platform/v1/containers/2092437461590446082/pda/photos/2092439032600797185/download', { responseType: 'blob' }) expect(http.get).toHaveBeenNthCalledWith(2, '/platform/v1/containers/2092437461590446082/pda/photos/2092439032600797185/download', { responseType: 'blob' }) expect(vi.mocked(http.get).mock.calls.every((call) => !String(call[0]).includes('/api/api/'))).toBe(true) for (const path of [ '/api/platform/v1/files/loading-evidence/21/22/23', 'https://example.com/photo.jpg', '//example.com/platform/v1/containers/7/pda/photos/23/download', '/platform/v1/containers/7/pda/photos/23/download?token=x', '/platform/v1/containers/7/pda/photos/../23/download', '/api/api/platform/v1/containers/7/pda/photos/23/download', ]) expect(() => downloadContainerPdaPhoto(path)).toThrow('invalid container photo path') }) it('downloads destination clearance through its exact protected endpoint with string ids', async () => { const blob = new Blob(['clearance']) vi.mocked(http.get).mockResolvedValueOnce(blob) const result = await downloadContainerClearance('2092438843630391297', '2092438843630393502') expect(http.get).toHaveBeenCalledWith('/platform/v1/containers/2092438843630391297/clearance/document/2092438843630393502/download', { responseType: 'blob' }) expect(result).toBe(blob) }) })