Переглянути джерело

test(乐收赢): 用测试环境的**真实应答**做入站验签向量(上游本人给的,非文档、非自算)

2026-09-28 首次对上测试环境(`https://test.gdyfsk.com/yfpay`,文档「接入指引」
page 196236523 给的测试机构号/密钥)。先发了一次**只读**的 `POST /v3/queryorder`
(查一个刻意不存在的订单),上游回 200 + 481 字节。三条读数:

1. **出站签名被上游接受** —— 回的是 `return_code=FAIL` + `return_msg=订单信息不存在!`,
   **不是**「签名错误」⇒ 我们的 `buildSource()`(null 排除 / 空串参与 / ASCII 升序 /
   `&key=` 后缀 / md5 小写)与上游一致。这是本通道第一次拿到「规则对」的外部证据。
2. **应答自带 sign,可以反验入站** —— 用上游那条应答重算:
   `null 不参与` 命中(`a3cb8169…`),`null 当空串` 与 `null 写字面量` 两种读法都不命中。
   即 `verify()` 认的原文规则 == 上游签的原文规则。
3. 出站 URL 拼法是 `props.getBaseUrl() + path`(纯拼接)⇒ 实跑命中的
   `https://test.gdyfsk.com/yfpay/v3/queryorder` 就是这个式子算出来的。

本提交把 ② 做成回归向量。**为什么非要有这一条**:本类其余 `verify*` 用例的期望值
要么由 `sign()` 产出(自洽),要么由**文档**给出 —— 两条都答不了「上游真实发出来的
报文,我们的验签认不认」。而验签是回调入口的**唯一身份证明**。

顺带钉住一条只能由真货证伪的规则:该应答 26 个字段里 24 个是显式 null,上游只对
两个非 null 字段签名。变异实测(摘掉 `buildSource()` 的 `filter(v != null)`):
- 红 2 条:`sign_nullValueExcluded`(自证型)与本条(外部向量)
- 文档那条独立向量 **没红** —— 它没有 null 字段,够不着这一格

读数:本类 13 绿(+1);全量 `mvn test` **901 绿 / 0 失败**
(Maven 控制台与 surefire XML 逐文件求和两支量具一致,69 个 testsuite);
变异后还原经 `git diff` 逐字节核回 HEAD。
alphaH 2 тижнів тому
батько
коміт
c58800eb67

+ 57 - 0
java/src/test/java/com/payment/platform/module/payment/leshouying/client/LeshouyingSignerTest.java

@@ -174,4 +174,61 @@ class LeshouyingSignerTest {
         inbound.put("total_amount", "2");
         assertFalse(signer.verify(inbound, KEY), "反向守卫:改一个字节必须被拒");
     }
+
+    /**
+     * ★ <b>入站验签的第三条独立向量 —— 这条来自上游本人,不是文档、也不是我们自算的。</b>
+     *
+     * <p>2026-09-28 用测试环境({@code https://test.gdyfsk.com/yfpay})发了一次**真实的**
+     * {@code POST /v3/queryorder}(查一个刻意不存在的订单),上游回了 481 字节,**应答里自带
+     * {@code sign}**。把那条应答原样喂进 {@code verify()}:它必须认。
+     *
+     * <p>★ <b>为什么非要有这一条</b>:本类其余 {@code verify*} 用例的期望值要么由 {@code sign()}
+     * 产出(自洽),要么由**文档**给出({@link #verify_acceptsSignBuiltFromTheDocumentedVectorIndependently})。
+     * 两条都答不了「<b>上游真实发出来的报文,我们的验签认不认</b>」。而验签是回调入口的**唯一身份证明**,
+     * 认错了就是「伪造回调把订单改成已支付」。
+     *
+     * <p>★ <b>这条向量顺带钉住了一条只能由真货证伪的规则</b>:应答里 26 个字段有 24 个是显式
+     * {@code null}。上游的 sign 是**只对两个非 null 字段**算的
+     * ({@code return_code=FAIL&return_msg=订单信息不存在!})。
+     * 若哪天有人把 {@code buildSource()} 的 {@code filter(v != null)} 去掉(改成「null 当空串参与」),
+     * 本类其余用例**全绿**,而线上**每一条回调都会被判成验签失败** —— 这条会红。
+     *
+     * <p>向量的稳定性:两次独立运行(不同 {@code query_trace_no} / {@code pay_time})拿到**同一个**
+     * {@code sign},因为参与签名的只有那两个非 null 字段 —— 它不随我们的查询参数变化。
+     */
+    @Test
+    void verify_acceptsRealUpstreamResponseFromTestEnv() {
+        // 逐字抄自上游应答(字段顺序照原文;那 24 个 null 是上游真发的,不是我们补的)
+        Map<String, Object> resp = params(
+                "return_code", "FAIL",
+                "return_msg", "订单信息不存在!",
+                "result_code", null,
+                "pay_type", null,
+                "mch_name", null,
+                "shop_name", null,
+                "mch_no", null,
+                "shop_no", null,
+                "query_trace_no", null,
+                "pay_trace_no", null,
+                "pay_time", null,
+                "total_amount", null,
+                "end_time", null,
+                "trade_no", null,
+                "td_trade_no", null,
+                "qd_trade_no", null,
+                "user_id", null,
+                "attach", null,
+                "trade_state", null,
+                "receipt_amount", null,
+                "pay_amount", null,
+                "coupon_amount", null,
+                "discount_amount", null);
+        resp.put("sign", "a3cb8169d863e438486a6ea0ae72fea7");
+
+        assertTrue(signer.verify(resp, KEY), "上游真实应答必须被认 —— 这条挂了说明入站验签与上游不一致");
+
+        // 反向守卫:改了被签字段必须被拒(少了它,上面那句在「verify 恒真」下也会绿)
+        resp.put("return_msg", "订单信息存在!");
+        assertFalse(signer.verify(resp, KEY), "反向守卫:改动被签字段必须被拒");
+    }
 }