|
|
@@ -0,0 +1,150 @@
|
|
|
+package com.payment.platform.module.payment.har.client;
|
|
|
+
|
|
|
+import com.payment.platform.module.payment.har.client.dto.HarQueryRequest;
|
|
|
+import com.payment.platform.module.payment.har.client.dto.HarShopCreateRequest;
|
|
|
+import com.payment.platform.module.payment.har.config.HarProperties;
|
|
|
+import com.sun.net.httpserver.HttpExchange;
|
|
|
+import com.sun.net.httpserver.HttpServer;
|
|
|
+import org.junit.jupiter.api.AfterEach;
|
|
|
+import org.junit.jupiter.api.BeforeEach;
|
|
|
+import org.junit.jupiter.api.Test;
|
|
|
+
|
|
|
+import java.io.IOException;
|
|
|
+import java.net.InetSocketAddress;
|
|
|
+import java.net.URLDecoder;
|
|
|
+import java.nio.charset.StandardCharsets;
|
|
|
+import java.security.KeyPairGenerator;
|
|
|
+import java.util.Base64;
|
|
|
+import java.util.LinkedHashMap;
|
|
|
+import java.util.Map;
|
|
|
+import java.util.concurrent.atomic.AtomicReference;
|
|
|
+
|
|
|
+import static org.junit.jupiter.api.Assertions.assertEquals;
|
|
|
+import static org.junit.jupiter.api.Assertions.assertNotNull;
|
|
|
+
|
|
|
+/**
|
|
|
+ * 出站 URL 契约 —— 钉住 {@link HarClient} 真正打到上游的那个 path。
|
|
|
+ *
|
|
|
+ * <p>补这条的原因:本轮联调发现门店创建的 URL 一直是错的(见下),而已有用例里
|
|
|
+ * {@code HarClient} 在三处服务测试里全是 mock,**没有任何用例看过出站 URL** ——
|
|
|
+ * 所以「路径写错」这一类缺陷在本仓的测试里是完全不可见的。
|
|
|
+ *
|
|
|
+ * <p>台账:规范 docx 的接口声明表(表 1)四列形态如下,前四行 method 与 url 严格同构
|
|
|
+ * ({@code kf.X.Y.Z} ↔ {@code /api/X/Y/Z}),第五行两列同时破例:
|
|
|
+ * <pre>
|
|
|
+ * 订单创建 kf.alipay.trade.precreate /api/alipay/trade/precreate
|
|
|
+ * 扫码收款 kf.alipay.trade.pay /api/alipay/trade/pay
|
|
|
+ * 订单查询 kf.alipay.trade.query /api/alipay/trade/query
|
|
|
+ * 订单退款 kf.alipay.trade.refund /api/alipay/trade/refund
|
|
|
+ * 门店创建 kf.har.shop.create /api/shop/create ← 上游 2026-09-24 澄清:url 写错了
|
|
|
+ * </pre>
|
|
|
+ * 上游澄清的正确 url 是 {@code /api/merchant/shop/create}。
|
|
|
+ *
|
|
|
+ * <p><b>method 那半仍是未验证的</b>:上游只更正了 url,没提 method。所以这条用例把
|
|
|
+ * method 单独断言、单独写失败消息 —— 如果联调回的是「method 不存在」,要改的就是那一行,
|
|
|
+ * 而不是把 path 改回去。
|
|
|
+ *
|
|
|
+ * <p>台子用 JDK 自带的 {@code com.sun.net.httpserver}(pom 里没有 MockWebServer/WireMock,
|
|
|
+ * 不为一条断言引新依赖),记录**真实收到**的 URI 与表单,而不是去断言我们自己的构造过程。
|
|
|
+ */
|
|
|
+class HarClientOutboundUrlTest {
|
|
|
+
|
|
|
+ /** 只为让 {@link HarSigner#sign} 有钥匙可用;本地生成,与被测行为无关。 */
|
|
|
+ private static final String TEST_PRIVATE_KEY_B64 = generatePkcs8Base64();
|
|
|
+
|
|
|
+ private HttpServer server;
|
|
|
+ private HarProperties props;
|
|
|
+ private final AtomicReference<String> lastPath = new AtomicReference<>();
|
|
|
+ private final AtomicReference<Map<String, String>> lastForm = new AtomicReference<>();
|
|
|
+
|
|
|
+ @BeforeEach
|
|
|
+ void setUp() throws IOException {
|
|
|
+ server = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0);
|
|
|
+ server.createContext("/", this::handle);
|
|
|
+ server.start();
|
|
|
+
|
|
|
+ props = new HarProperties();
|
|
|
+ props.setEnabled(true);
|
|
|
+ props.setBaseUrl("http://127.0.0.1:" + server.getAddress().getPort());
|
|
|
+ props.setAppId("test-app");
|
|
|
+ props.setVersion("2.0");
|
|
|
+ props.setOurPrivateKey(TEST_PRIVATE_KEY_B64);
|
|
|
+ }
|
|
|
+
|
|
|
+ @AfterEach
|
|
|
+ void tearDown() {
|
|
|
+ server.stop(0);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void query_hitsTheAlipayPath_positiveControl() {
|
|
|
+ HarQueryRequest req = new HarQueryRequest();
|
|
|
+ req.setOrderNo("1111");
|
|
|
+
|
|
|
+ new HarClient(props).query(req);
|
|
|
+
|
|
|
+ assertEquals("/kf-alipay-api/api/alipay/trade/query", lastPath.get(),
|
|
|
+ "正对照:这条 URL 本轮联调已实际打到上游并拿到业务响应。"
|
|
|
+ + "它若也红,红的是台子(没记到出站路径),不是被测代码。实际收到 = " + lastPath.get());
|
|
|
+ assertEquals("kf.alipay.trade.query", method());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void createShop_hitsTheMerchantShopPath() {
|
|
|
+ HarShopCreateRequest req = new HarShopCreateRequest();
|
|
|
+ req.setMerchantCode("M001");
|
|
|
+ req.setThirdShopId("SHOP-TEST-001");
|
|
|
+ req.setName("联调测试门店");
|
|
|
+
|
|
|
+ new HarClient(props).createShop(req);
|
|
|
+
|
|
|
+ assertEquals("/kf-alipay-api/api/merchant/shop/create", lastPath.get(),
|
|
|
+ "上游 2026-09-24 澄清:门店创建的 url 是 /api/merchant/shop/create,"
|
|
|
+ + "规范 docx 表 1 的 URL 列写错了(写的是 /api/shop/create)。"
|
|
|
+ + "实际收到 = " + lastPath.get());
|
|
|
+ assertEquals("kf.har.shop.create", method(),
|
|
|
+ "method 取值上游没有更正过。若联调回「method 不存在」,改这一行(同构推测是 kf.merchant.shop.create),"
|
|
|
+ + "别把 path 改回去。");
|
|
|
+ }
|
|
|
+
|
|
|
+ private String method() {
|
|
|
+ Map<String, String> form = lastForm.get();
|
|
|
+ assertNotNull(form, "台子没收到任何表单 —— 出站请求根本没发出去");
|
|
|
+ return form.get("method");
|
|
|
+ }
|
|
|
+
|
|
|
+ private void handle(HttpExchange ex) throws IOException {
|
|
|
+ lastPath.set(ex.getRequestURI().getPath());
|
|
|
+ lastForm.set(parseForm(new String(ex.getRequestBody().readAllBytes(), StandardCharsets.UTF_8)));
|
|
|
+
|
|
|
+ byte[] body = "response_code=10000&msg=ok".getBytes(StandardCharsets.UTF_8);
|
|
|
+ ex.getResponseHeaders().add("Content-Type", "text/plain; charset=UTF-8");
|
|
|
+ ex.sendResponseHeaders(200, body.length);
|
|
|
+ ex.getResponseBody().write(body);
|
|
|
+ ex.close();
|
|
|
+ }
|
|
|
+
|
|
|
+ private static Map<String, String> parseForm(String body) {
|
|
|
+ Map<String, String> form = new LinkedHashMap<>();
|
|
|
+ for (String pair : body.split("&")) {
|
|
|
+ if (pair.isEmpty()) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ int idx = pair.indexOf('=');
|
|
|
+ String k = idx < 0 ? pair : pair.substring(0, idx);
|
|
|
+ String v = idx < 0 ? "" : URLDecoder.decode(pair.substring(idx + 1), StandardCharsets.UTF_8);
|
|
|
+ form.put(k, v);
|
|
|
+ }
|
|
|
+ return form;
|
|
|
+ }
|
|
|
+
|
|
|
+ private static String generatePkcs8Base64() {
|
|
|
+ try {
|
|
|
+ KeyPairGenerator gen = KeyPairGenerator.getInstance("RSA");
|
|
|
+ gen.initialize(2048);
|
|
|
+ return Base64.getEncoder().encodeToString(gen.generateKeyPair().getPrivate().getEncoded());
|
|
|
+ } catch (Exception e) {
|
|
|
+ throw new IllegalStateException("生成测试私钥失败", e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|