import assert from "node:assert/strict"; import { readFileSync } from "node:fs"; import { test } from "node:test"; import { epayCanonical, epaySign } from "../src/lib/epay/sign"; import { createEpayNotifyHandler } from "../src/lib/epay/notify-core"; const migration = readFileSync(new URL("../supabase/migrations/20260805020000_reconcile_payment_admin_schema.sql", import.meta.url), "utf8"); test("易支付签名过滤空值并按键排序", () => { const params = { money: "10.00", pid: "10001", name: "套餐", empty: "", sign_type: "MD5" }; assert.equal(epayCanonical(params), "money=10.00&name=套餐&pid=10001"); assert.equal(epaySign(params, "secret"), "79dd3a13f9fd32622fa2197c0a2d7b66"); }); test("支付迁移包含套餐、订单、payment 类型与原子结算", () => { assert.match(migration, /create table if not exists public\.payment_packages/); assert.match(migration, /create table if not exists public\.payment_orders/); assert.match(migration, /transaction_type in \('redeem', 'reserve', 'refund', 'payment'\)/); assert.match(migration, /settle_epay_order/); assert.match(migration, /on conflict \(user_id, transaction_type, request_id\) do nothing/); }); test("self-hosted 管理角色具有支付后台最小权限", () => { assert.match(migration, /grant select, insert, update on table public\.payment_packages to admin_runtime/); assert.match(migration, /grant select on table public\.payment_orders to admin_runtime/); assert.doesNotMatch(migration, /grant (?:all|insert|update|delete) on table public\.payment_orders to admin_runtime/); assert.match(migration, /grant select on table public\.epay_settings to admin_runtime/); assert.match(migration, /grant execute on function public\.admin_save_epay_settings\([\s\S]*\) to admin_runtime/); }); test("支付通知只在原子结算成功后确认网关", async () => { const values = { pid: "10001", trade_status: "TRADE_SUCCESS", out_trade_no: "ORDER-1234567890", trade_no: "TRADE-1", money: "10.00" }; const body = new URLSearchParams({ ...values, sign: epaySign(values, "secret"), sign_type: "MD5" }).toString(); const request = () => new Request("https://example.test/api/payment/epay/notify", { method: "POST", body }); const handler = (result: { data: unknown; error: unknown } | Error) => createEpayNotifyHandler({ readConfig: async () => ({ pid: "10001", key: "secret" }), settle: async () => { if (result instanceof Error) throw result; return result; }, }); for (const failed of [ { data: null, error: new Error("database unavailable") }, { data: [{ success: false, status: "profile_missing", credits: null }], error: null }, new Error("database unavailable"), ]) { const response = await handler(failed)(request()); assert.equal(response.status, 500); assert.equal(await response.text(), "fail"); } const response = await handler({ data: [{ success: true, status: "paid", credits: 100 }], error: null })(request()); assert.equal(response.status, 200); assert.equal(await response.text(), "success"); });