55 lines
3.0 KiB
TypeScript
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");
|
|
});
|