|
|
@@ -35,8 +35,12 @@ class LeshouyingSignerTest {
|
|
|
// auth_code=134573607383541818&inst_no=900075&mch_no=Y52211090004
|
|
|
// &pay_time=20190101125959&pay_trace_no=18588dbd1c6e4572b915a2b33fcdc62e
|
|
|
// &pay_type=400&total_amount=1
|
|
|
- // 文档只给了 A,没给最终 sign,所以这里断言的是「A 的拼装结果」——
|
|
|
- // 用 md5(A + "&key=" + KEY) 反推,验证排序与拼接格式与文档一致。
|
|
|
+ // 【2026-09-28 订正】此处原写「文档只给了 A,没给最终 sign」——**不实**:文档 §3 第五步
|
|
|
+ // 给了一个最终 sign(841387ec9bd3ea78a580bd4629cab010,配 §3 第三步的示例密钥
|
|
|
+ // 8a9bd64e6585c6d8016585c6c8980002,与上面这个 A 正是同一组参数)。但它**复现不出来**:
|
|
|
+ // 按文档自己的四步算是 92bbe9b73e5657f8a5cdcae215c1d697(本次实测)。那条示例值只作示意,
|
|
|
+ // **别拿它当向量**。这里断言的仍是「A 的拼装结果」—— md5(A + "&key=" + KEY),
|
|
|
+ // 验证排序与拼接格式与文档一致。
|
|
|
Map<String, Object> p = params(
|
|
|
"auth_code", "134573607383541818",
|
|
|
"total_amount", "1",
|
|
|
@@ -231,4 +235,54 @@ class LeshouyingSignerTest {
|
|
|
resp.put("return_msg", "订单信息存在!");
|
|
|
assertFalse(signer.verify(resp, KEY), "反向守卫:改动被签字段必须被拒");
|
|
|
}
|
|
|
+
|
|
|
+ /**
|
|
|
+ * ★ <b>第四条独立向量,也是唯一一条「上游对空字符串签名」的向量</b> —— 2026-09-28 真实主扫下单。
|
|
|
+ *
|
|
|
+ * <p>用测试环境发了一次真实的 {@code POST /v3/qrpay}(1 分钱的聚合码单),上游回 370 字节。
|
|
|
+ * 这条应答里有<b>两个空字符串字段</b>:{@code shop_name} 与 {@code shop_no}
|
|
|
+ * (我们没发 shop_no —— 生产侧「非 null 才发」—— 上游回的是<b>空串而不是 null</b>),
|
|
|
+ * 而这两个空串<b>都在签名原文里</b>。
|
|
|
+ *
|
|
|
+ * <p>★ <b>为什么非要有这一条</b>:文档 §3 第一步把「参数值为空字符串参与,参数值为 null 不参与」
|
|
|
+ * 并列写着,但本类此前对「空串参与」只有<b>自证型</b>用例
|
|
|
+ * ({@link #sign_emptyStringParticipates}:期望值由我们自己按同一条规则算出)。自证型用例
|
|
|
+ * 答不了「上游是不是也这么算」。这条是外部证据 —— 签名是上游自己做的。
|
|
|
+ *
|
|
|
+ * <p>★ 与 {@link #verify_acceptsRealUpstreamResponseFromTestEnv} <b>互补,缺一不可</b>:
|
|
|
+ * 那条钉的是 <b>null 排除</b>(26 个字段里 24 个是 null),这条钉的是 <b>空串保留</b>。
|
|
|
+ * 两条合起来才覆盖文档那句话的两半。只留任一条的话,把 {@code buildSource()} 改成
|
|
|
+ * 「空串也排除」不会有任何用例变红,而入站验签会当场拒掉真实回调。
|
|
|
+ *
|
|
|
+ * <p>顺带钉住一条对账路径的事实:主扫下单成功时 {@code result_code=PAY_SUCCESS},
|
|
|
+ * 而<b>应答里没有 trade_no</b>(qrpay 的返回表本就没有这个字段)。因此主扫单被对账捞起时,
|
|
|
+ * {@code LsyQueryService.queryOrder} 走的一定是 {@code pay_trace_no + pay_time} 那条分支。
|
|
|
+ *
|
|
|
+ * <p>向量稳定性:全部 11 个字段都是那一笔真实订单的数据,不再变化 ⇒ sign 跨运行恒定。
|
|
|
+ */
|
|
|
+ @Test
|
|
|
+ void verify_acceptsRealQrpayResponse_whereUpstreamSignedEmptyStrings() {
|
|
|
+ // 逐字抄自上游应答(字段顺序照原文;两个空串与那串中文都是上游真发的)
|
|
|
+ Map<String, Object> resp = params(
|
|
|
+ "return_code", "SUCCESS",
|
|
|
+ "return_msg", "聚合码请求成功!",
|
|
|
+ "result_code", "PAY_SUCCESS",
|
|
|
+ "mch_name", "建行小店基本休息修改",
|
|
|
+ "shop_name", "",
|
|
|
+ "mch_no", "Y52211090004",
|
|
|
+ "shop_no", "",
|
|
|
+ "pay_trace_no", "probe20260928152205qr",
|
|
|
+ "pay_time", "20260928152205",
|
|
|
+ "total_amount", "1",
|
|
|
+ "code_url", "https://test.gdyfsk.com/yfpay/s/s-00002");
|
|
|
+ resp.put("sign", "00f5d5cb5c37a81ea40a5c35471ad0aa");
|
|
|
+
|
|
|
+ assertTrue(signer.verify(resp, KEY), "上游真实应答(含空串字段)必须被认");
|
|
|
+
|
|
|
+ // 反向守卫:把两个空串之一改成有值 ⇒ 签名原文变了 ⇒ 必须被拒。
|
|
|
+ // 这条守卫**专门**压空串那一格:若 buildSource 哪天把空串当 null 剔掉,
|
|
|
+ // 上面那句会红;若改成「空串与缺值等价」,这句会红。
|
|
|
+ resp.put("shop_no", "86000033007");
|
|
|
+ assertFalse(signer.verify(resp, KEY), "反向守卫:空串改成有值必须被拒");
|
|
|
+ }
|
|
|
}
|