Bläddra i källkod

test(乐收赢): yml 默认值改走生产值后,把那条防漂移断言的方向反过来

用户 2026-09-29 跑全量:923 里 1 红,落在
LeshouyingPropertiesTest.docTestConstants_matchApplicationYml。

不是缺陷,是那条断言在正常报警。量具读的是**文件字节**
(getResourceAsStream("/application.yml") + 正则取 ${LESHOUYING_KEY: 后面那段),
不经过 Spring Environment ⇒ 环境变量不可能造成这条红。实测三项都被换过:

  ${LESHOUYING_KEY:...}     长度 32,≠ DOC_TEST_KEY
  ${LESHOUYING_INST_NO:...} 长度 6,≠ 900112
  leshouying.base-url       https://open.gdyfsk.com/yfpay(生产网关)

即 application.yml 的默认值已从「文档测试配置」换成「真实生产值」。

前身断言的是「DOC_TEST_* 常量 == yml 默认值」—— 那是「yml 默认值填测试配置、
真实值只从环境变量进」那个世界的不变式。新形态下它恒假,而唯一能满足它的动作是
把生产 key 写进 Java 常量,即写进 git 历史(Global Constraint 3 堵死)。
两条路里用户选了「yml 即部署配置」,所以改的是断言,不是 yml。

新断言钉两件不依赖具体值的事(LeshouyingPropertiesTest):
  - 三项默认值非空:本仓不靠环境变量跑,清空 = 启动即报「配置缺失」,指向错方向
  - 三项默认值不得等于那三个公开字面量:文档测试密钥是厂商公开的,谁都能拿它伪造
    回调把订单改成已支付;而第二道闸只在非 dev 才响,这条任何 profile 都响

两向读数:
  - 现状跑 ⇒ 14/14 绿(该类原 13 条,本条一拆二)
  - 变异:把 assertLeshouyingYmlDefaults 的断言体掏空 ⇒ 只 canFail 那条红
    (Expected AssertionError to be thrown, but nothing was thrown,:282),
    真 yml 那条照旧绿 ⇒ 承重的是 canFail 那条,不是空壳
  - 还原后 sha256 逐字节相同(1e64f1ea…),MUTANT 残留 0
  - 全量 924 绿 / 0 失败(surefire-reports 跑前已清空;独立从 71 份报告求和复核)

顺带订正两处会误导后来者的注释(LeshouyingProperties):常量块那段原写着
「改 application.yml 的默认值时必须同步改这里」—— 照做就是把凭据写进 git;
validate() 第二道闸的注释原写着「忘设环境变量会拿到测试配置」,现在那条路落出来的
是真实值。两处都改成与现状相符,并写明新断言的方向与旧的正相反。

★ 已知代价,用户在知情下选的:真实值落在 yml 默认值里 ⇒ 本机 dev 起服务也会打到
乐收赢生产网关、用生产机构号。与本仓记过的 P-F3b 同形,已写进新断言的 javadoc。
★ application.yml 本身未入库、未 git add(它同时含 HAR 私钥与 DB 口令)。
alphaH 2 veckor sedan
förälder
incheckning
ae3ec6b044

+ 13 - 8
java/src/main/java/com/payment/platform/module/payment/leshouying/config/LeshouyingProperties.java

@@ -56,10 +56,14 @@ public class LeshouyingProperties {
     /** 进件审核通知地址(我方固定地址,spec §6.5)。**与 notifyUrl 是两个不同的地址。** */
     private String onboardingNotifyUrl;
 
-    // ===== 下面三个是 application.yml 给出的「文档**测试**环境」默认值 =====
-    // ★ 它们是测试环境的:base-url 指向乐收赢测试网关,inst-no/key 是文档给的测试机构。
-    //   非 dev profile 下出现任意一个 ⇒ validate() 拒绝启动。
-    //   改 application.yml 的默认值时必须同步改这里 —— 有测试(docTestConstants_matchApplicationYml)钉着。
+    // ===== 下面三个常量是厂商文档**公开**的测试值(不是秘密,谁都能查到)=====
+    // ★ 唯一用途是 validate() 的第二道闸:生效值命中任意一个 ⇒ 非 dev profile 拒绝启动。
+    // ★ 2026-09-29 起 application.yml 的默认值已换成**真实生产值**(用户裁定:yml 即部署配置),
+    //   所以这道闸现在只在「有人把环境变量显式设成文档测试值」时才命中。
+    // ★★ **不要**为了「让常量追平 yml」把它们改成生产值 —— 那等于把凭据写进 git 历史。
+    //   钉住这件事的是 LeshouyingPropertiesTest
+    //   #leshouyingYmlDefaults_areNonBlankAndNotThePublicDocTestValues:它断言的方向与此处
+    //   改向**之前**那条正好相反(yml 默认值 ≠ 这三个常量,而不是 ==)。
     static final String DOC_TEST_BASE_URL = "https://test.gdyfsk.com/yfpay";
     static final String DOC_TEST_INST_NO = "900112";
     static final String DOC_TEST_KEY = "1c4d83f0c3c24a7d9fcaa1fed2682076";
@@ -193,10 +197,11 @@ public class LeshouyingProperties {
         }
 
         // ---- 第二道:文档给的**测试**配置不得在非 dev 环境生效 ----
-        // 上面只判"空",而 application.yml 给这三项都留了默认值 ⇒ 忘设环境变量**不会**为空,
-        // 校验会通过,然后非 dev 环境就拿测试机构号 + 文档公开密钥去收款,
-        // 并把请求打到**测试网关**。对 key 尤其致命:验签用的是一个公开可查的字面量,
-        // 任何人都能伪造回调把订单改成已支付。
+        // 威胁是「验签用的是公开可查的字面量」:那样任何人都能伪造回调把订单改成已支付。
+        // ★ 2026-09-29 起 application.yml 的默认值是真实生产值,本闸**不再**由「忘设环境变量」
+        //   触发(那条路现在落出来的是真实值,不是测试值),只在环境变量被显式设成文档测试值时命中。
+        //   而「默认值塌回这三个字面量」这个形状由 LeshouyingPropertiesTest 独立钉着 ——
+        //   那条断言在**任何 profile 下**都会响,本闸只在非 dev 响。
         List<String> testDefaults = new ArrayList<>();
         if (DOC_TEST_KEY.equals(key)) {
             testDefaults.add("leshouying.key(环境变量 LESHOUYING_KEY)");

+ 120 - 21
java/src/test/java/com/payment/platform/module/payment/leshouying/config/LeshouyingPropertiesTest.java

@@ -10,7 +10,9 @@ import java.util.regex.Pattern;
 
 import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
@@ -229,25 +231,70 @@ class LeshouyingPropertiesTest {
         assertDoesNotThrow(p::validate);
     }
 
-    // ---- ★ 防漂移:Java 常量必须与 application.yml 里的默认值一致 ----
+    // ---- ★ 防漂移(2026-09-29 改向):yml 默认值必须是真的,且不得是公开的文档测试值 ----
 
+    /**
+     * ★ <b>本条与前身断言的是**相反**的事,改向理由必须一起读。</b>
+     *
+     * <p>前身({@code docTestConstants_matchApplicationYml})断言「{@code DOC_TEST_*} 常量
+     * == application.yml 的默认值」。那是「yml 默认值填**文档测试**配置、真实值只从环境变量进」
+     * 那个世界里的不变式。2026-09-29 用户明确改走另一条路:**application.yml 就是部署配置**
+     * (HAR 私钥、DB 口令本来就在里面),真实值直接落在默认值上,不靠环境变量。
+     * 于是前身断言恒假,而能满足它的**唯一**动作是把生产 key 写进本类的常量 ——
+     * 那是把凭据写进 git 历史,被 {@code Global Constraint 3} 堵死。**所以改的是断言,不是 yml。**
+     *
+     * <p>改向之后仍有两件事可钉,都**不依赖具体值**:
+     * <ol>
+     *   <li><b>默认值非空</b>:本仓的部署形态是「jar 自带可用配置」。谁把默认值清空
+     *       (退回 {@code ${ENV:}}),失败不是报错而是**启动中止**,且错误文案会把运维指向
+     *       环境变量 —— 而本仓不靠环境变量跑,那个方向是错的;</li>
+     *   <li><b>默认值不得等于 {@code DOC_TEST_*} 那三个公开字面量</b>:厂商文档公开的密钥,
+     *       谁都能拿去伪造回调把订单改成已支付({@code validate()} 第二道闸的注释自己写着
+     *       这条威胁)。而第二道闸**只在非 dev profile 才炸**,dev 下静默放行 ——
+     *       本断言把这个形状的塌陷变成**任何 profile 下都会响**。</li>
+     * </ol>
+     *
+     * <p>⚠ <b>改向同时接受了一个代价,写在这里免得后来者当成漏做</b>:真实值落在 yml 默认值里
+     * ⇒ 本机 dev 起服务也会打到乐收赢**生产**网关、用生产机构号。这与本仓记过的 P-F3b
+     * ({@code notify-url} 的生产默认值被 dev 用)是同一个形状,用户在知情下选了它。
+     */
     @Test
-    void docTestConstants_matchApplicationYml() 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);
-        }
+    void leshouyingYmlDefaults_areNonBlankAndNotThePublicDocTestValues() throws Exception {
+        assertLeshouyingYmlDefaults(readApplicationYml());
+    }
 
-        assertEquals(LeshouyingProperties.DOC_TEST_KEY,
-                placeholderDefault(yml, "LESHOUYING_KEY"),
-                "DOC_TEST_KEY 与 application.yml 的 ${LESHOUYING_KEY:...} 默认值不一致");
-        assertEquals(LeshouyingProperties.DOC_TEST_INST_NO,
-                placeholderDefault(yml, "LESHOUYING_INST_NO"),
-                "DOC_TEST_INST_NO 与 application.yml 的 ${LESHOUYING_INST_NO:...} 默认值不一致");
-        assertEquals(LeshouyingProperties.DOC_TEST_BASE_URL,
-                ymlLine(yml, "leshouying", "base-url"),
-                "DOC_TEST_BASE_URL 与 application.yml 的 base-url 不一致");
+    /**
+     * 上一条的**两向证明**。为什么要单独一条:上一条在现状下只会报绿,而绿本身证明不了它能红 ——
+     * 且它**没有**「旧版失败读数」可引(失败的那版断言的是另一件事,读数对不上)。
+     * 所以把断言抽成 {@link #assertLeshouyingYmlDefaults(String)},用**合成 yml** 驱动:
+     * 一个正控必须过,三个变异必须各在**预期的那一句**上炸。
+     *
+     * <p>★ 「炸在预期那一句」是这条用例一半的内容:只断言 {@code AssertionError} 会在
+     * **合成件自己写坏**(占位符找不到、段截错)时照样报绿 —— 那正是本仓记过的
+     * 「坏量具造出合情合理的假 finding」。所以每个变异都核对异常消息。
+     */
+    @Test
+    void leshouyingYmlDefaultsAssertion_canFail() {
+        assertDoesNotThrow(() -> assertLeshouyingYmlDefaults(
+                        syntheticYml("a-real-key", "88888888", "https://open.example.invalid/yfpay")),
+                "正控:三个真实值必须过(少了这一句,下面三条红可能只是被别的原因短路)");
+
+        AssertionError keyIsDocTest = assertThrows(AssertionError.class, () -> assertLeshouyingYmlDefaults(
+                syntheticYml(LeshouyingProperties.DOC_TEST_KEY, "88888888",
+                        "https://open.example.invalid/yfpay")));
+        assertTrue(keyIsDocTest.getMessage().contains("LESHOUYING_KEY"),
+                "必须炸在 key 那条上,实际: " + keyIsDocTest.getMessage());
+
+        AssertionError keyIsBlank = assertThrows(AssertionError.class, () -> assertLeshouyingYmlDefaults(
+                syntheticYml("", "88888888", "https://open.example.invalid/yfpay")));
+        assertTrue(keyIsBlank.getMessage().contains("不得为空"),
+                "必须炸在「非空」那条上(占位符找不到也是 AssertionError,两者必须分得开),实际: "
+                        + keyIsBlank.getMessage());
+
+        AssertionError baseUrlIsDocTest = assertThrows(AssertionError.class, () -> assertLeshouyingYmlDefaults(
+                syntheticYml("a-real-key", "88888888", LeshouyingProperties.DOC_TEST_BASE_URL)));
+        assertTrue(baseUrlIsDocTest.getMessage().contains("base-url"),
+                "必须炸在 base-url 那条上,实际: " + baseUrlIsDocTest.getMessage());
     }
 
     /**
@@ -266,11 +313,7 @@ class LeshouyingPropertiesTest {
      */
     @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 yml = readApplicationYml();
 
         String[][] keyToEnv = {
                 {"channel-id", "LESHOUYING_CHANNEL_ID"},
@@ -290,6 +333,62 @@ class LeshouyingPropertiesTest {
         }
     }
 
+    /**
+     * 读测试 classpath 上的 application.yml —— 即 {@code src/main/resources} 那份,
+     * {@code src/test/resources} 不存在,所以不存在「测试专用副本」把真文件遮住这回事。
+     *
+     * <p>★ 量具是**文件字节**({@code getResourceAsStream} + 正则),不经过 Spring 的
+     * {@code Environment} ⇒ 环境变量**不可能**影响这里的读数。2026-09-29 排查那次红时
+     * 正是靠这一点排除了「是环境变量被设了」这个假设。
+     */
+    private static String readApplicationYml() throws Exception {
+        try (InputStream in = LeshouyingPropertiesTest.class.getResourceAsStream("/application.yml")) {
+            assertNotNull(in, "application.yml 必须在测试 classpath 上(src/main/resources)");
+            return new String(in.readAllBytes(), StandardCharsets.UTF_8);
+        }
+    }
+
+    /**
+     * 造一份**只含本用例要读的三项**(外加一个干扰段)的 yml 片段,供两向证明驱动断言。
+     *
+     * <p>★ 干扰段 {@code har:} 放在 {@code leshouying:} **之前**,且它的 {@code base-url} 取的正是
+     * {@code DOC_TEST_BASE_URL} —— 不是随手写的:{@link #ymlLine} 2026-09-24 就因为
+     * 「整文件取第一次出现」而命中过 {@code har.base-url}(见其 javadoc)。这样安排以后,
+     * 谁把段内限定去掉,上面那条**正控**会立刻红(读到的是 har 那个文档测试地址),
+     * 而不必等到某天两个段的 base-url 真被配成同一个值才暴露。
+     */
+    private static String syntheticYml(String keyDefault, String instNoDefault, String baseUrl) {
+        return "har:\n"
+                + "  base-url: " + LeshouyingProperties.DOC_TEST_BASE_URL + "\n"
+                + "\n"
+                + "leshouying:\n"
+                + "  base-url: " + baseUrl + "\n"
+                + "  key: ${LESHOUYING_KEY:" + keyDefault + "}\n"
+                + "  inst-no: ${LESHOUYING_INST_NO:" + instNoDefault + "}\n";
+    }
+
+    /**
+     * 断言逻辑本体,见 {@link #leshouyingYmlDefaults_areNonBlankAndNotThePublicDocTestValues} 的 javadoc。
+     * 抽成静态方法**只为**让那条的两向证明能用合成 yml 驱动它 —— 不必去改那份真的 yml。
+     */
+    static void assertLeshouyingYmlDefaults(String yml) {
+        String key = placeholderDefault(yml, "LESHOUYING_KEY");
+        String instNo = placeholderDefault(yml, "LESHOUYING_INST_NO");
+        String baseUrl = ymlLine(yml, "leshouying", "base-url");
+
+        assertFalse(key.isBlank(), "application.yml 的 ${LESHOUYING_KEY:...} 默认值不得为空"
+                + "(本仓不靠 LESHOUYING_KEY 环境变量跑,jar 带的就是它;清空 = 启动即报「配置缺失」)");
+        assertFalse(instNo.isBlank(), "application.yml 的 ${LESHOUYING_INST_NO:...} 默认值不得为空");
+        assertFalse(baseUrl.isBlank(), "application.yml 的 leshouying.base-url 不得为空");
+
+        assertNotEquals(LeshouyingProperties.DOC_TEST_KEY, key,
+                "application.yml 的 ${LESHOUYING_KEY:...} 默认值不得是公开的文档测试密钥");
+        assertNotEquals(LeshouyingProperties.DOC_TEST_INST_NO, instNo,
+                "application.yml 的 ${LESHOUYING_INST_NO:...} 默认值不得是公开的文档测试机构号");
+        assertNotEquals(LeshouyingProperties.DOC_TEST_BASE_URL, baseUrl,
+                "application.yml 的 leshouying.base-url 不得是乐收赢测试网关");
+    }
+
     /** 取 `${ENV_NAME:<默认值>}` 里的默认值。 */
     private static String placeholderDefault(String yml, String envName) {
         Matcher m = Pattern.compile("\\$\\{" + envName + ":([^}]*)\\}").matcher(yml);