Selaa lähdekoodia

test(进件): 补一条用例钉住「level 整个缺席 ⇒ 400」(裁定 R-T9a-b)

主控裁定 R-T9a-b:`level` 缺席也必须是 400、不能落 500 —— 与本任务实测到的
`required-param-ABSENT: http=500` 正好对上。控制器侧**无需改动**(已按裁定用
required=false + 同一条白名单判断兜住缺席),所以本轮只碰测试。

- 把「整个参数不在」从边界①那条用例里**移出来单独成条**
  (areasEndpoint_rejectsAbsentLevel_withoutTouchingService):白名单判据有两半
  (`level == null` 与 `!ALLOWED_LEVELS.contains(level)`),一条用例塞两种形态时
  变异只能证明「合起来有用」,分别摘掉哪一半都还能绿。分开后两半各自可钉。
- 该请求**给了**合法 parentCode="1" ⇒ 边界②的谓词 `!"1".equals(null) && isBlank("1")`
  为假、不可能抛 ⇒ 这个 400 只能出自边界①。(两个参数都不给的话 400 就有了两个来源,
  那条用例会失去归因能力。)
- 只断言 `$.code == 400` + 零接触服务;不钉文案、不钉两条边界规则的先后。

两向变异实测:摘掉 `level == null ||` 这一半 ⇒ **只有**新用例红,失败形态
`Status expected:<200> but was:<500>`(顺带实测确认 `Set.of(...).contains(null)`
确实抛 NPE 而非返回 false,「level == null 单独判」那句注释不是摆设);摘掉整条
白名单调用 ⇒ 新用例与边界①那条一起红;对调两条边界规则顺序 + 改写 400 文案 ⇒ 20 全绿。

实测:该类 20 绿(19 → 20);全量 623 绿(BUILD SUCCESS)。
alphaH 3 viikkoa sitten
vanhempi
sitoutus
6718ab3e05

+ 42 - 12
java/src/test/java/com/payment/platform/module/payment/leshouying/controller/LeshouyingOnboardingControllerTest.java

@@ -559,14 +559,13 @@ class LeshouyingOnboardingControllerTest {
     }
     }
 
 
     /**
     /**
-     * ★ 边界①:{@code level} 白名单 = {@code {"1","2","3"}},其余 ⇒ 400,且**一次都不触达服务**。
-     *
-     * <p>「其余」**包含整个参数不在** —— 2026-09-23 实测:本仓 {@code GlobalExceptionHandler}
-     * 是全仓唯一的 advice,它**没有** {@code MissingServletRequestParameterException} 分支
-     * ⇒ 若把 {@code level} 声明成必填,缺失落进兜底的 {@code Exception} 分支,得到
-     * <b>HTTP 500 + 业务码 500「服务器内部错误」</b>(读数见 task-9a-report.md)
-     * —— 那是「请重试」的语义,不是 400。所以 {@code level} 声明为 {@code required = false},
-     * 缺失与非法走**同一条**白名单判断。
+     * ★ 边界①:{@code level} 白名单 = {@code {"1","2","3"}},**值**不在其中 ⇒ 400,
+     * 且**一次都不触达服务**。
+     *
+     * <p>与 {@link #areasEndpoint_rejectsAbsentLevel_withoutTouchingService} 是**一对**:
+     * 本条钉「值不合法」,那条钉「参数整个缺席」。分开写不是排版偏好 —— 白名单判据有**两半**
+     * ({@code level == null} 与 {@code !ALLOWED_LEVELS.contains(level)}),一条用例里塞两种
+     * 形态的话,变异只能证明「合起来有用」,分别摘掉哪一半都还能绿。
      */
      */
     @Test
     @Test
     void areasEndpoint_rejectsLevelOutsideWhitelist_withoutTouchingService() throws Exception {
     void areasEndpoint_rejectsLevelOutsideWhitelist_withoutTouchingService() throws Exception {
@@ -576,10 +575,6 @@ class LeshouyingOnboardingControllerTest {
                     .andExpect(status().isOk())
                     .andExpect(status().isOk())
                     .andExpect(jsonPath("$.code").value(400));
                     .andExpect(jsonPath("$.code").value(400));
         }
         }
-        // 整个参数不在:同样是「其余」
-        mockMvc.perform(get(AREAS_PATH).param("parentCode", "1"))
-                .andExpect(status().isOk())
-                .andExpect(jsonPath("$.code").value(400));
 
 
         // 反例先行 ⇒ 这是一条**无歧义**的零接触读数
         // 反例先行 ⇒ 这是一条**无歧义**的零接触读数
         verifyNoInteractions(onboardingService, merchantService);
         verifyNoInteractions(onboardingService, merchantService);
@@ -591,6 +586,41 @@ class LeshouyingOnboardingControllerTest {
         verify(onboardingService).areaList("3", "2");
         verify(onboardingService).areaList("3", "2");
     }
     }
 
 
+    /**
+     * ★ 边界①的**另一半**:{@code level} **整个参数不在** ⇒ 400,且**不得触达服务**
+     * (裁定 R-T9a-b)。
+     *
+     * <p>【为什么不交给 {@code @RequestParam} 的必填】2026-09-23 实测:本仓
+     * {@code GlobalExceptionHandler} 是**全仓唯一**的 advice,它**没有**
+     * {@code MissingServletRequestParameterException} 分支 ⇒ 必填参数缺失落进兜底的
+     * {@code Exception} 分支,得到 <b>HTTP 500 + 业务码 500「服务器内部错误」</b>
+     * (探针读数见 task-9a-report.md §一),那是「请重试」的语义,不是 400。
+     * 所以 {@code level} 声明为 {@code required = false},缺席与非法走**同一条**白名单判断
+     * —— **不**为缺席另写一条规则。
+     *
+     * <p>【这个 400 是**可归因**的】本请求**给了**合法的 {@code parentCode="1"} ⇒ 边界②的谓词
+     * {@code !"1".equals(null) && isBlank("1")} 求值为 {@code true && false} = 假、不可能抛
+     * ⇒ 这个 400 只可能出自边界①。若两个参数都不给,400 就有了两个可能来源,这条用例
+     * 会失去归因能力(那种形态的「谁先谁后」实测不可观测,见 task-9a-report.md §四(3))。
+     */
+    @Test
+    void areasEndpoint_rejectsAbsentLevel_withoutTouchingService() throws Exception {
+        login(TENANT, false);
+        mockMvc.perform(get(AREAS_PATH).param("parentCode", "1"))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.code").value(400));
+
+        // 反例先行 ⇒ 这是一条**无歧义**的零接触读数
+        verifyNoInteractions(onboardingService, merchantService);
+
+        // 正对照:同形请求(同样只带 parentCode 的位置)+ 合法 level ⇒ 受理
+        // (否则上面的 400 可能只是「压根没路由到这个方法」)
+        when(onboardingService.areaList("2", "1")).thenReturn(List.of());
+        mockMvc.perform(get(AREAS_PATH).param("level", "2").param("parentCode", "1"))
+                .andExpect(jsonPath("$.code").value(0));
+        verify(onboardingService).areaList("2", "1");
+    }
+
     /**
     /**
      * ★ 边界②:{@code level} ≠ {@code "1"} 而 {@code parentCode} 缺失 / 空白(空串、纯空白)⇒ 400,
      * ★ 边界②:{@code level} ≠ {@code "1"} 而 {@code parentCode} 缺失 / 空白(空串、纯空白)⇒ 400,
      * 且**不得触达服务**。
      * 且**不得触达服务**。