Преглед изворни кода

fix(进件): Task 1 修复轮 1 - 订正三处失实注释 + 补五键名与空默认值的钉子

本轮**不改任何运行时行为**,只动一个测试文件:注释可信度 + 一条新钉子。

F1 原第 193 行引「§0.9 的裁定」,而计划 §0.9 是「实现前必须知道的**三条既有代码事实**」
(review_status 词表 / requireByMchNo 不带租户条件 / save 默认 APPROVED),不含这条裁定
⇒ 悬空引用。改引 Global Constraint 3/4 + spec §6.4 —— 与 LeshouyingProperties.java
第 210-213 行、计划第 292 行同一组依据,不自创章节号。

F2 原文称「没有这条用例,那次『加进 validate()』全库不会有任何测试变红」是**假的**:
本类另有三条也在「四必填齐备」前提下断言 validate() 不抛(docTestConfigWithDevProfile /
realConfigWithProdProfile / uploadMaxBytes),两个 props 工厂都不设那五个凭据字段。
改成「本条是这条裁定的正对照之一」并写入变异实测读数(见下)。

F3 新增 onboardingYmlKeys_useTheirEnvVarPlaceholdersWithEmptyDefault:五个进件键的
「键名 → 环境变量名」此前零钉子,而 ignoreUnknownFields 默认为 true(java/src 未关掉)
⇒ 键名打错一个字母即静默失效,闸却报正确的环境变量名缺失,把人指向错误方向。
断言「值恰好等于 ${ENV:}」+「默认值为空」,复用既有的 ymlLine / placeholderDefault,
不另写解析器。

F4 类 javadoc 第 20-21 行「全仓没有任何 @SpringBootTest」自 2693ab6 起已失效
(同一文件第 197-199 行正好说着相反的话)。改成「本类建立时(2026-09-22)/ 现状已变」
两段式,现状那句由全量日志里的 validate() warn(profile=dev)实证。

变异实测(两向都做,读数与还原证据见 task-1-fix1-report.md):
  yml 键名 channel-id -> chennel-id        ⇒ 新用例红(application.yml 里找不到 channel-id)
  yml 默认值 ${...:} -> ${...:REPLACE_ME}  ⇒ 新用例红(expected <${...:}> but was <${...:REPLACE_ME}>)
  五凭据加进 validate() 的 missing 清单     ⇒ 本类 5 红(那三条 + 本条 + docTestConfigWithProdProfile)
三次还原均按字节核对:sha256 回到变异前值 + cmp 与备份逐字节相同 + git diff 与变异前逐字节相同。

测试:全量 503 -> 504,0 fail / 0 error / 0 skipped,BUILD SUCCESS。
alphaH пре 3 недеља
родитељ
комит
014d3fbf02

+ 59 - 5
java/src/test/java/com/payment/platform/module/payment/leshouying/config/LeshouyingPropertiesTest.java

@@ -17,8 +17,11 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
 /**
  * LeshouyingProperties.validate() 的单测。
  *
- * ★ 这是该方法**第一次**被测试覆盖:它挂 @PostConstruct,而全仓没有任何启动 Spring 上下文的测试
- *   (无 @SpringBootTest),所以在此之前它从未在测试里跑过。这里直接调它。
+ * ★ 本类建立时(2026-09-22)全仓还没有任何启动 Spring 上下文的测试(无 @SpringBootTest)——
+ *   那时挂着 @PostConstruct 的 validate() 从未在测试里跑过,本类是它**第一处覆盖**:这里直接调它。
+ *   ★ 现状已变,别再照旧读:{@code PaymentApplicationContextTest}({@code @SpringBootTest},
+ *   {@code 2693ab6} 引入)会 refresh 整个容器 ⇒ 本 bean 被创建、validate() 随之执行;
+ *   但那条测试只断言装配成功,**不覆盖本方法的分支** —— 分支断言仍然只在本类。
  */
 class LeshouyingPropertiesTest {
 
@@ -190,7 +193,9 @@ class LeshouyingPropertiesTest {
     // ---- ★ 正对照:进件凭据**不得**进 validate() ----
 
     /**
-     * ★ <b>正对照</b>:本用例钉住 §0.9 的裁定 —— 进件凭据**不得**进 {@code validate()}。
+     * ★ <b>正对照</b>:本用例钉住「进件凭据**不得**进 {@code validate()}」这条裁定。
+     * 依据是 {@code Global Constraint 3}「凭据不进仓库」/ {@code Global Constraint 4}
+     * 「不留测试用假值」(spec §6.4 逐字),加上下面两条实测的代码事实。
      *
      * <p>它只写 {@code assertDoesNotThrow},看着像「没断言」,实际是**挡后来者**:
      * <ul>
@@ -201,8 +206,17 @@ class LeshouyingPropertiesTest {
      *   <li>而 §6.4 又禁止我们往那份 yml 填假值(凭据不进仓库)。</li>
      * </ul>
      * 两条路都堵死 ⇒ 闸放在进件服务入口,而不是启动期。
-     * <b>没有这条用例,上面那次「加进 validate()」全库不会有任何测试变红</b>
-     * —— 本类的另外几条只造缺项、不看「四必填齐备时是否放行」。
+     *
+     * <p>★ <b>本条是这条裁定的正对照之一,不是唯一防线。</b>本类另有三条也在「四必填齐备」的
+     * 前提下断言 {@code validate()} 不抛 —— {@code validate_docTestConfigWithDevProfile_passes} /
+     * {@code validate_realConfigWithProdProfile_passes} / {@code validate_uploadMaxBytes_isNotInRequiredList};
+     * 两个 props 工厂({@code docTestProps} / {@code realProps})都不设这五个凭据字段(= null),
+     * 所以「加进 {@code validate()}」会把那三条一起打红(2026-09-23 变异实测:本类共 **5 红** ——
+     * 那三条 + 本条 + {@code validate_docTestConfigWithProdProfile_throws};最后一条红是因为
+     * 第一道闸先炸,异常消息从「文档测试配置」那句换成了「配置缺失」那句)。
+     * 那三条的**目的**是别的事(dev 放行 / 非文档测试参数放行 / uploadMaxBytes 不在必填清单),
+     * 只是恰好共用同一个前提;本条是唯一一条**专为这条裁定**写的,构造路径是
+     * 四必填填非文档测试值 + {@code dev}。
      */
     @Test
     void validate_passesWhenOnboardingCredentialsAreAbsent() {
@@ -236,6 +250,46 @@ class LeshouyingPropertiesTest {
                 "DOC_TEST_BASE_URL 与 application.yml 的 base-url 不一致");
     }
 
+    /**
+     * ★ 钉住五个进件键的「键名 → 环境变量名」这一对关系,**外加默认值必须为空**。
+     *
+     * <p><b>为什么值得一条用例</b>:{@code @ConfigurationProperties} 默认
+     * {@code ignoreUnknownFields = true}(本仓 {@code java/src} 没有关掉它),所以键名写错一个字母
+     * ({@code channel-id} → {@code chennel-id})不会报错,只会**静默失效** —— 该键被忽略、正主留空。
+     * 而闸({@code missingOnboardingCredentials()})的文案原样写着正确的环境变量名:运维照着设好了
+     * {@code LESHOUYING_CHANNEL_ID},闸仍报这一项缺失,**报错把人指向错误方向**。
+     * 这五个键名此前**零钉子**({@code docTestConstants_matchApplicationYml} 钉的是三个既有键)。
+     *
+     * <p><b>为什么断言的是「值恰好等于占位符」而不是「键存在」</b>:只查键在不在会丢掉另一半 ——
+     * 默认值必须留空({@code Global Constraint 4}:不留测试用假值)。填一个假值同样是静默的:
+     * 闸不再报缺失,进件却拿着假通道 id 打上游。
+     */
+    @Test
+    void onboardingYmlKeys_useTheirEnvVarPlaceholdersWithEmptyDefault() throws Exception {
+        String yml;
+        try (InputStream in = getClass().getResourceAsStream("/application.yml")) {
+            assertNotNull(in, "application.yml 必须在测试 classpath 上(src/main/resources)");
+            yml = new String(in.readAllBytes(), StandardCharsets.UTF_8);
+        }
+
+        String[][] keyToEnv = {
+                {"channel-id", "LESHOUYING_CHANNEL_ID"},
+                {"bd-id", "LESHOUYING_BD_ID"},
+                {"channel-type", "LESHOUYING_CHANNEL_TYPE"},
+                {"rate-code", "LESHOUYING_RATE_CODE"},
+                {"onboarding-notify-url", "LESHOUYING_ONBOARDING_NOTIFY_URL"},
+        };
+        for (String[] pair : keyToEnv) {
+            String key = pair[0];
+            String env = pair[1];
+            assertEquals("${" + env + ":}", ymlLine(yml, key),
+                    "leshouying." + key + " 的值必须恰好是 ${" + env
+                            + ":}(键名或环境变量名漂移都会让它在运行时静默失效)");
+            assertEquals("", placeholderDefault(yml, env),
+                    env + " 的默认值必须留空(Global Constraint 4:不留测试用假值)");
+        }
+    }
+
     /** 取 `${ENV_NAME:<默认值>}` 里的默认值。 */
     private static String placeholderDefault(String yml, String envName) {
         Matcher m = Pattern.compile("\\$\\{" + envName + ":([^}]*)\\}").matcher(yml);