浏览代码

test(乐收赢): 补一条**真实** PAY_IN_PROCESS 应答的向量,并用反向守卫钉住「以 result_code 为准」

本类此前对 PAY_IN_PROCESS 只有一条**两字段**的合成报文。合成报文答不了
「上游真发的 PAY_IN_PROCESS 长什么样」—— trade_state、receipt_amount 这些字段会不会
出现、出现成什么值,只有真货知道。

向量来源(真货,非构造):2026-09-28 打测试环境,给一笔真实的 H5 单
(/v3/h5pay 回 PAY_SUCCESS + trade_no)按 trade_no 查 /v3/queryorder,
上游回 614 字节 / 24 字段,应答自带的 sign 已用 LeshouyingSigner.verify 复算通过
(null 排除读法)⇒ 这份 JSON 是上游自己发的。抄进测试后另用脚本核对过字面量与
上游原始字节**逐字节相同**(584 字符 / 12 片段;同尺子做单字节变异后报不同,
证明这把尺子读得出差异,不是恒真的比对)。

它钉住三件事:

1. 判定跟 result_code,不跟 trade_state。
   真报文里 trade_state=USERPAYING 与 result_code=PAY_IN_PROCESS **恰好同向**
   ⇒ 光看这一条**区分不了**实现读的是哪个。真正钉住它的是那句反向守卫:
   把 trade_state 改成 SUCCESS(与 result_code 相反),判定必须仍跟随 result_code。
   守卫自带正对照(先断言替换真的发生了),避免搜索串写错导致守卫静默失效。

2. 支付中既不是成功也不是失败。
   isPaySuccess / isPayFail 必须都为 false —— 否则会提前写死一个终态。

3. 真实陷阱:receipt_amount / discount_amount 都是**非 null 的 "0"**。
   这一单一分钱都还没收到(用户连支付页都没打开)。别写「非 null 即已收款」这种判据。

两向变异实测(跑 LeshouyingResponseParserTest):
- 把 isPaySuccess 改成也读 trade_state ⇒ **红**,且精确落在
  LeshouyingResponseParserTest.java:308 那句反向守卫上;非矛盾的那三条断言仍全绿
  ⇒ 真报文本身抓不住这个缺陷,抓住它的确实是守卫
- 无害改动(isPayInProcess 加一句冗余 null 检查)⇒ 仍绿 18/0/0/0;
  变异体指纹 8615509e…(≠ 基线 8fd500c2…),确认变异真的写进去了才认这个「绿」
- 还原后三个文件指纹逐一复核,均与基线逐字节相同

全量:903 / 0 / 0 / 0,BUILD SUCCESS(第二把尺子:surefire 逐文件 XML 求和,
69 个文件,同为 903)。
alphaH 2 周之前
父节点
当前提交
380630f397

+ 58 - 0
java/src/test/java/com/payment/platform/module/payment/leshouying/client/LeshouyingResponseParserTest.java

@@ -251,4 +251,62 @@ class LeshouyingResponseParserTest {
             assertNull(ex.getCause(), "空报文没进过 JSON 解析,不该有 cause");
         }
     }
+
+    /**
+     * ★ <b>真实的 {@code PAY_IN_PROCESS} 应答</b> —— 2026-09-28 打测试环境拿到的,逐字抄录。
+     *
+     * <p>来源:给一笔真实的 H5 单({@code /v3/h5pay} 回了 {@code PAY_SUCCESS} + {@code trade_no})
+     * 按 {@code trade_no} 查 {@code /v3/queryorder},上游回了 614 字节 / 24 字段。
+     * 应答自带的 {@code sign} 已用 {@code LeshouyingSigner.verify} 复算通过(null 排除读法)
+     * ⇒ 这份 JSON 是**上游自己发的**,不是我构造的。
+     *
+     * <p>★ <b>为什么必须有一条真货向量</b>:本类此前对 PAY_IN_PROCESS 只有一条**两字段**的合成报文
+     * ({@link #parseV3_payInProcess_isNotSuccessButNotException})。合成报文答不了
+     * 「上游真发的 PAY_IN_PROCESS 长什么样」—— {@code trade_state}、{@code receipt_amount}
+     * 这些字段会不会出现、出现成什么值,只有真货知道。
+     *
+     * <p>★ <b>它同时要钉住文档那句「判断交易结果以 {@code result_code} 为准,{@code trade_state}
+     * 仅供参考」</b>。但注意:真报文里 {@code trade_state=USERPAYING} 与
+     * {@code result_code=PAY_IN_PROCESS} **恰好同向** ⇒ 光看这一条**区分不了**「实现看的是哪个」。
+     * 真正钉住它的是下面那句反向守卫(把两者拆开)。没有那句,本用例在「实现其实读 trade_state」
+     * 下照样全绿。
+     */
+    @Test
+    void parseV3_realPayInProcessResponse_resultCodeBeatsTradeState() {
+        // 逐字抄自 logs/_lsy-query-by-tradeno-resp.bin(614 字节;字段顺序照原文,未重排)
+        String real = "{\"return_code\":\"SUCCESS\",\"return_msg\":\"用户支付中\","
+                + "\"result_code\":\"PAY_IN_PROCESS\",\"pay_type\":\"300\","
+                + "\"mch_name\":\"建行小店基本休息修改\",\"shop_name\":\"\","
+                + "\"mch_no\":\"Y52211090004\",\"shop_no\":\"\","
+                + "\"query_trace_no\":\"483a97c660a14921942751ba3ad4af79\","
+                + "\"pay_trace_no\":\"probe20260928160450h5\",\"pay_time\":\"20260928160450\","
+                + "\"total_amount\":\"1\",\"end_time\":null,"
+                + "\"trade_no\":\"200001070X4326092816045010002\",\"td_trade_no\":null,"
+                + "\"qd_trade_no\":null,\"user_id\":null,\"attach\":null,"
+                + "\"trade_state\":\"USERPAYING\",\"receipt_amount\":\"0\",\"pay_amount\":\"1\","
+                + "\"coupon_amount\":null,\"discount_amount\":\"0\","
+                + "\"sign\":\"18146ea94b94f4264992243d90d199be\"}";
+
+        Map<String, Object> r = parser.parseV3(real);
+        assertEquals("PAY_IN_PROCESS", r.get("result_code"));
+        assertTrue(parser.isPayInProcess(r), "上游真实应答必须是「支付中」");
+        assertFalse(parser.isPaySuccess(r), "支付中不得被判成成功 —— 那会提前写死一个终态");
+        assertFalse(parser.isPayFail(r), "支付中不得被判成失败 —— 那正是本类要防的方向");
+
+        // ★ 真实陷阱:这一单一分钱都还没收到(用户连支付页都没打开),
+        //   而 receipt_amount / discount_amount 都是**非 null 的 "0"**。
+        assertEquals("0", r.get("receipt_amount"));
+        assertEquals("0", r.get("discount_amount"));
+        assertNotNull(r.get("receipt_amount"), "是零,不是 null —— 别写「非 null 即已收款」这种判据");
+
+        // ★ 反向守卫:把 trade_state 改成与 result_code **相反**的说法 ⇒ 判定必须仍跟随 result_code。
+        String contradictoryJson = real.replace(
+                "\"trade_state\":\"USERPAYING\"", "\"trade_state\":\"SUCCESS\"");
+        assertNotEquals(real, contradictoryJson,
+                "正对照:替换必须真的发生,否则下面两句是自证的(搜索串写错就静默失效)");
+        Map<String, Object> contradictory = parser.parseV3(contradictoryJson);
+        assertFalse(parser.isPaySuccess(contradictory),
+                "trade_state 说成功、result_code 说支付中 ⇒ 必须听 result_code(文档明说 trade_state 仅供参考)");
+        assertTrue(parser.isPayInProcess(contradictory));
+    }
 }