From 4991536065f14618d9f507131f016fc2c2ed949d Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Tue, 1 Sep 2026 11:03:40 +0800 Subject: [PATCH] test(firstleg): harden pallet option page contract --- .../main/resources/openapi/firstleg-v1.yaml | 8 +++---- .../InboundMeasurementServiceTest.java | 24 +++++++++++++++++++ ...weight-pallet-options-p0-contract.test.mjs | 14 +++++++++++ ...ght-pallet-page-response-contract.test.mjs | 7 ++++++ .../src/main/resources/openapi/pallet-v1.yaml | 8 +++---- .../controller/PalletOpenApiContractTest.java | 4 ++++ 6 files changed, 57 insertions(+), 8 deletions(-) diff --git a/firstleg/src/main/resources/openapi/firstleg-v1.yaml b/firstleg/src/main/resources/openapi/firstleg-v1.yaml index 4b8b6c640..ffef274fd 100644 --- a/firstleg/src/main/resources/openapi/firstleg-v1.yaml +++ b/firstleg/src/main/resources/openapi/firstleg-v1.yaml @@ -1328,10 +1328,10 @@ components: type: array maxItems: 100 items: {$ref: '#/components/schemas/WeightPalletSnapshot'} - page: {type: integer, format: int64, minimum: 1} - size: {type: integer, format: int64, minimum: 1, maximum: 100} - total: {type: integer, format: int64, minimum: 0} - pages: {type: integer, format: int64, minimum: 0} + page: {oneOf: [{type: integer, format: int64, minimum: 1}, {type: string, pattern: '^[1-9][0-9]*$'}]} + size: {oneOf: [{type: integer, format: int64, minimum: 1, maximum: 100}, {type: string, pattern: '^(?:[1-9]|[1-9][0-9]|100)$'}]} + total: {oneOf: [{type: integer, format: int64, minimum: 0}, {type: string, pattern: '^(?:0|[1-9][0-9]*)$'}]} + pages: {oneOf: [{type: integer, format: int64, minimum: 0}, {type: string, pattern: '^(?:0|[1-9][0-9]*)$'}]} CreateWeightResultRequest: type: object required: [fileId, palletId] diff --git a/firstleg/src/test/java/com/baiyue/firstleg/service/InboundMeasurementServiceTest.java b/firstleg/src/test/java/com/baiyue/firstleg/service/InboundMeasurementServiceTest.java index 22c81459b..f411552d7 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/service/InboundMeasurementServiceTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/service/InboundMeasurementServiceTest.java @@ -254,6 +254,30 @@ class InboundMeasurementServiceTest { verify(warehousePallets).palletSnapshots("internal-token", 100L, 1L, 20L, "TP-5"); } + @Test + void weightPalletOptionsFailsClosedOnWarehouseNon2xxResponse() { + InboundOrderEntity inbound = inbound("WAIT_MEASURE"); + when(repository.findInbound(1L, 10L)).thenReturn(inbound); + when(warehousePallets.palletSnapshots("internal-token", 100L, 1L, 20L, "TP-5")) + .thenThrow(mock(feign.FeignException.class)); + + assertThatThrownBy(() -> service.weightPalletOptions(1L, 10L, 1L, 20L, "TP-5")) + .isInstanceOf(BizException.class) + .hasMessageContaining("仓库服务暂不可用"); + } + + @Test + void weightPalletOptionsFailsClosedOnWarehouseNetworkError() { + InboundOrderEntity inbound = inbound("WAIT_MEASURE"); + when(repository.findInbound(1L, 10L)).thenReturn(inbound); + when(warehousePallets.palletSnapshots("internal-token", 100L, 1L, 20L, null)) + .thenThrow(mock(feign.RetryableException.class)); + + assertThatThrownBy(() -> service.weightPalletOptions(1L, 10L, 1L, 20L, null)) + .isInstanceOf(BizException.class) + .hasMessageContaining("仓库服务暂不可用"); + } + @Test void creatingWeightResultRejectsPalletOutsideTheInboundWarehouse() { InboundOrderEntity inbound = inbound("WAIT_MEASURE"); diff --git a/scripts/admin-test-env-modules/weight-pallet-options-p0-contract.test.mjs b/scripts/admin-test-env-modules/weight-pallet-options-p0-contract.test.mjs index 8398dca52..91868df68 100644 --- a/scripts/admin-test-env-modules/weight-pallet-options-p0-contract.test.mjs +++ b/scripts/admin-test-env-modules/weight-pallet-options-p0-contract.test.mjs @@ -7,6 +7,7 @@ const read = (relativeUrl) => readFileSync(new URL(relativeUrl, import.meta.url) const controller = read('../../firstleg/src/main/java/com/baiyue/firstleg/controller/PlatformInboundMeasurementController.java') const service = read('../../firstleg/src/main/java/com/baiyue/firstleg/service/InboundMeasurementService.java') const warehouseClient = read('../../firstleg/src/main/java/com/baiyue/firstleg/client/WarehousePalletLocationClient.java') +const serviceTests = read('../../firstleg/src/test/java/com/baiyue/firstleg/service/InboundMeasurementServiceTest.java') const openapi = read('../../firstleg/src/main/resources/openapi/firstleg-v1.yaml') const adminApi = read('../../../apps/admin-web/src/modules/inbounds/weight/api/weightOcr.ts') const adminPanel = read('../../../apps/admin-web/src/modules/inbounds/weight/components/WeightOcrPanel.vue') @@ -112,3 +113,16 @@ test('historical pallet snapshot remains read-only visible when it is no longer assert.match(cards, /row\.palletTareWeightKg/) assert.match(adminBehavior, /(?:historical|history|历史|失去候选|no longer eligible).{0,220}(?:palletCode|托盘编码|TP-)/is) }) + +test('backend detail reads the stored historical pallet snapshot without revalidating current eligibility', () => { + assert.match(serviceTests, /detailUsesStoredPalletSnapshotAfterCandidateBecomesUnavailable/) + assert.match(serviceTests, /verify\(warehousePallets, never\(\)\)\.palletSnapshot/) + assert.match(serviceTests, /verify\(warehousePallets, never\(\)\)\.palletSnapshots/) +}) + +test('backend evidence fails closed for Warehouse non-2xx and network errors', () => { + assert.match(serviceTests, /weightPalletOptionsFailsClosedOnWarehouseNon2xxResponse/) + assert.match(serviceTests, /weightPalletOptionsFailsClosedOnWarehouseNetworkError/) + assert.match(serviceTests, /FeignException/) + assert.match(serviceTests, /RetryableException/) +}) diff --git a/scripts/admin-test-env-modules/weight-pallet-page-response-contract.test.mjs b/scripts/admin-test-env-modules/weight-pallet-page-response-contract.test.mjs index 9c5aec34e..d91ffbb58 100644 --- a/scripts/admin-test-env-modules/weight-pallet-page-response-contract.test.mjs +++ b/scripts/admin-test-env-modules/weight-pallet-page-response-contract.test.mjs @@ -8,6 +8,7 @@ const read = (relativeUrl) => readFileSync(new URL(relativeUrl, import.meta.url) const controller = read('../../firstleg/src/main/java/com/baiyue/firstleg/controller/PlatformInboundMeasurementController.java') const pageResult = read('../../common-core/src/main/java/com/baiyue/common/core/api/PageResult.java') const longJackson = read('../../common-web/src/main/java/com/baiyue/common/web/LongIdJacksonConfiguration.java') +const serviceTests = read('../../firstleg/src/test/java/com/baiyue/firstleg/service/InboundMeasurementServiceTest.java') const openapi = read('../../firstleg/src/main/resources/openapi/firstleg-v1.yaml') const adminApi = read('../../../apps/admin-web/src/modules/inbounds/weight/api/weightOcr.ts') const adminApiTests = read('../../../apps/admin-web/src/modules/inbounds/weight/api/weightOcr.test.ts') @@ -84,3 +85,9 @@ test('Admin behavior separately covers a non-2xx HTTP or network error', () => { test('Admin aborts and ignores an expired candidate response', () => { assert.match(adminBehavior, /in-flight pallet request[\s\S]{0,900}signal\?\.aborted[\s\S]{0,300}过期托盘/) }) + +test('backend preserves failure evidence for Warehouse non-2xx and network errors', () => { + assert.match(serviceTests, /weightPalletOptionsFailsClosedOnWarehouseNon2xxResponse/) + assert.match(serviceTests, /weightPalletOptionsFailsClosedOnWarehouseNetworkError/) + assert.match(serviceTests, /仓库服务暂不可用/) +}) diff --git a/warehouse/src/main/resources/openapi/pallet-v1.yaml b/warehouse/src/main/resources/openapi/pallet-v1.yaml index a58ee80a1..22197caa9 100644 --- a/warehouse/src/main/resources/openapi/pallet-v1.yaml +++ b/warehouse/src/main/resources/openapi/pallet-v1.yaml @@ -78,7 +78,7 @@ components: type: array maxItems: 100 items: { $ref: '#/components/schemas/PalletSnapshot' } - page: { type: integer, format: int64, minimum: 1 } - size: { type: integer, format: int64, minimum: 1, maximum: 100 } - total: { type: integer, format: int64, minimum: 0 } - pages: { type: integer, format: int64, minimum: 0 } + page: { oneOf: [{ type: integer, format: int64, minimum: 1 }, { type: string, pattern: '^[1-9][0-9]*$' }] } + size: { oneOf: [{ type: integer, format: int64, minimum: 1, maximum: 100 }, { type: string, pattern: '^(?:[1-9]|[1-9][0-9]|100)$' }] } + total: { oneOf: [{ type: integer, format: int64, minimum: 0 }, { type: string, pattern: '^(?:0|[1-9][0-9]*)$' }] } + pages: { oneOf: [{ type: integer, format: int64, minimum: 0 }, { type: string, pattern: '^(?:0|[1-9][0-9]*)$' }] } diff --git a/warehouse/src/test/java/com/baiyue/warehouse/controller/PalletOpenApiContractTest.java b/warehouse/src/test/java/com/baiyue/warehouse/controller/PalletOpenApiContractTest.java index 39530c909..f7b72d35a 100644 --- a/warehouse/src/test/java/com/baiyue/warehouse/controller/PalletOpenApiContractTest.java +++ b/warehouse/src/test/java/com/baiyue/warehouse/controller/PalletOpenApiContractTest.java @@ -20,6 +20,10 @@ class PalletOpenApiContractTest { .contains("maximum: 100") .contains("maxItems: 100") .contains("PalletSnapshotPageResponse:") + .contains("page: { oneOf:") + .contains("size: { oneOf:") + .contains("total: { oneOf:") + .contains("pages: { oneOf:") .contains("required: [palletId, palletCode, palletType, capacityCbm, tareWeightKg]") .contains("additionalProperties: false") .doesNotContain("tenantId:")