|
|
@@ -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);
|