Files
Jyotisha/frontend/tests/epay-payment-contract.test.ts
T
2026-08-05 11:13:34 +08:00

55 lines
3.0 KiB
TypeScript

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");
});