import assert from "node:assert/strict"; import crypto from "node:crypto"; import { readFileSync } from "node:fs"; import test from "node:test"; import { decryptEpayKey, encryptEpayKey, EpayEncryptionError } from "../src/lib/epay/encryption-core"; import { resolveEpayConfig } from "../src/lib/epay/config-core"; import { assertPublicEpayGateway, isPublicEpayAddress } from "../src/lib/epay/gateway-policy"; const root = new URL("../", import.meta.url); const route = readFileSync(new URL("src/app/api/admin/epay-settings/route.ts", root), "utf8"); const management = readFileSync(new URL("src/components/admin/payment-management.tsx", root), "utf8"); const createRoute = readFileSync(new URL("src/app/api/payment/epay/create/route.ts", root), "utf8"); const notifyRoute = readFileSync(new URL("src/app/api/payment/epay/notify/route.ts", root), "utf8"); const notifyCore = readFileSync(new URL("src/lib/epay/notify-core.ts", root), "utf8"); const configRoute = readFileSync(new URL("src/lib/epay/config.ts", root), "utf8"); const availability = readFileSync(new URL("src/lib/epay/availability.ts", root), "utf8"); const packagesRoute = readFileSync(new URL("src/app/api/payment/packages/route.ts", root), "utf8"); const testRoute = readFileSync(new URL("src/app/api/admin/epay-settings/test/route.ts", root), "utf8"); const page = readFileSync(new URL("src/app/page.tsx", root), "utf8"); const migration = readFileSync(new URL("supabase/migrations/20260805020000_reconcile_payment_admin_schema.sql", root), "utf8"); const key = crypto.randomBytes(32).toString("base64"); test("易支付密钥 AES-256-GCM 往返、篡改与错误主密钥", () => { const encrypted = encryptEpayKey("merchant-secret", key); assert.match(encrypted, /^v1\.[^.]+\.[^.]+\.[^.]+$/); assert.equal(decryptEpayKey(encrypted, key), "merchant-secret"); const tampered = `${encrypted.slice(0, -1)}${encrypted.endsWith("A") ? "B" : "A"}`; assert.throws(() => decryptEpayKey(tampered, key), EpayEncryptionError); assert.throws(() => decryptEpayKey(encrypted, crypto.randomBytes(32).toString("base64")), EpayEncryptionError); assert.throws(() => encryptEpayKey("merchant-secret", "not-base64"), EpayEncryptionError); }); test("配置解析数据库优先且无行时回退环境变量", async () => { const encrypted = encryptEpayKey("database-secret", key); const database = await resolveEpayConfig(async () => ({ gateway_url: "https://database-pay.example.com/", pid: "database-pid", encrypted_key: encrypted, notify_url: "https://staging.example.com/api/payment/epay/notify", return_url: "https://staging.example.com/", site_name: "Staging", chat_enabled: false, }), { NODE_ENV: "test", EPAY_CONFIG_ENCRYPTION_KEY: key, EPAY_GATEWAY_URL: "https://environment-pay.example.com", EPAY_PID: "environment-pid", EPAY_KEY: "environment-secret", }); assert.equal(database.gatewayUrl.toString(), "https://database-pay.example.com/"); assert.equal(database.pid, "database-pid"); assert.equal(database.key, "database-secret"); assert.equal(database.chatEnabled, false); const environment = await resolveEpayConfig(async () => null, { NODE_ENV: "test", SITE_ADDRESS: "https://staging.example.com", EPAY_GATEWAY_URL: "https://environment-pay.example.com", EPAY_PID: "environment-pid", EPAY_KEY: "environment-secret", EPAY_CHAT_ENABLED: "1", }); assert.equal(environment.chatEnabled, true); assert.equal(environment.notifyUrl, "https://staging.example.com/api/payment/epay/notify"); assert.equal(environment.returnUrl, "https://staging.example.com/"); }); test("管理员 API 不回显任何密钥并强制首次显式录入", () => { assert.match(route, /requireAdminSession\("read"\)/); assert.match(route, /requireAdminSession\("write"\)/); assert.match(route, /\.strict\(\)/); assert.match(route, /crypto\.randomUUID\(\)/); assert.match(route, /首次保存数据库配置时必须输入新的商户密钥/); assert.match(route, /chatEnabled: z\.boolean\(\)/); assert.match(route, /chatEnabled: row\.chat_enabled/); assert.match(route, /queryAdminRows/); assert.match(route, /select gateway_url, pid, encrypted_key, notify_url, return_url, site_name, chat_enabled, updated_at[\s\S]*from public\.epay_settings[\s\S]*where id = true[\s\S]*limit 1/); assert.match(route, /select \* from public\.admin_save_epay_settings\([\s\S]*\$1, \$2, \$3, \$4, \$5, \$6, \$7, \$8, \$9, \$10, \$11, \$12/); assert.match(route, /parsed\.data\.chatEnabled,[\s\S]*Boolean\(parsed\.data\.newKey\)/); assert.match(route, /keyConfigured/); assert.doesNotMatch(route, /createAdminSupabaseClient|\.from\(|\.rpc\(/); assert.doesNotMatch(route, /NextResponse\.json\([^\n]*(?:encrypted_key|newKey|encryptedKey|maskedKey|keyMask)/); assert.doesNotMatch(route, /BETTER_AUTH_SECRET/); }); test("迁移前仅在配置表不存在时继续使用环境变量", () => { assert.match(configRoute, /error\?\.code === "42P01"/); assert.match(route, /isPostgresError\(error\) && error\.code === "42P01"/); assert.match(route, /throw error/); assert.match(configRoute, /if \(error\) throw new Error\(\)/); }); test("支付调用点等待异步数据库配置", () => { assert.match(createRoute, /await readEpayConfig\(\)/); assert.match(notifyRoute, /readConfig: readEpayConfig/); assert.match(notifyCore, /await dependencies\.readConfig\(\)/); assert.match(notifyRoute, /export async function POST/); assert.match(notifyRoute, /export async function GET/); }); test("统一支付页面含默认折叠的 Z-Pay 渠道配置与永不预填的 Password", () => { assert.match(management, / { assert.match(migration, /chat_enabled boolean not null default false/); assert.match(migration, /p_chat_enabled boolean/); assert.match(migration, /'chatEnabled'/); assert.match(availability, /EPAY_CHAT_ENABLED/); assert.doesNotMatch(availability, /EPAY_CONFIG_ENCRYPTION_KEY|decryptEpayKey/); assert.match(packagesRoute, /enabled: false, packages: \[\]/); assert.match(packagesRoute, /enabled: true/); assert.match(createRoute, /在线支付暂未开放/); assert.match(createRoute, /EPAY_DISABLED/); assert.match(createRoute, /await readEpayAvailability\(\)/); assert.ok(createRoute.indexOf("availability.enabled") < createRoute.indexOf("await readEpayConfig()")); assert.ok(createRoute.indexOf("availability.enabled") < createRoute.indexOf("payment_packages")); assert.match(management, /在对话页开放支付/); assert.match(page, /paymentEnabled &&
/); assert.match(page, /setPaymentEnabled\(false\)[\s\S]*setPaymentPackages\(\[\]\)[\s\S]*setPaymentOrder\(null\)[\s\S]*setPaymentError\(""\)/); }); test("创建订单返回已签名收银台 URL 且不服务端请求网关", () => { assert.match(createRoute, /assertPublicGatewayUrl\(submitUrl\)/); assert.match(createRoute, /const signedParams = \{ \.\.\.params, sign: epaySign\(params, config\.key\), sign_type: "MD5" \}/); assert.match(createRoute, /const payUrl = new URL\(submitUrl\)/); assert.match(createRoute, /payUrl\.searchParams\.set\(name, value\)/); assert.match(createRoute, /NextResponse\.json\(\{ orderNo, payUrl: payUrl\.toString\(\), qrCode: null \}\)/); for (const field of ["money", "name", "notify_url", "out_trade_no", "pid", "return_url", "sitename", "type", "sign", "sign_type"]) assert.match(createRoute, new RegExp(field)); assert.doesNotMatch(createRoute, /fetch\(submitUrl|document\.createElement\("form"\)|submitUrl:|fields[, }]/); assert.doesNotMatch(createRoute, /NextResponse\.json\([^\n]*config\.key|searchParams\.set\([^\n]*config\.key/); assert.match(page, /window\.open\(payload\.payUrl, "_blank", "noopener,noreferrer"\)/); assert.match(page, /

套餐充值<\/h3>/); assert.match(page, /立即支付/); assert.match(page, /套餐支付暂时不可用,请稍后重试/); assert.doesNotMatch(page, /document\.createElement\("form"\)|payload\.submitUrl|payload\.fields/); }); test("网关探测只使用 HEAD/GET、受限响应且共享 SSRF 门禁", () => { assert.match(testRoute, /requireAdminSession\("write"\)/); assert.match(testRoute, /method: "HEAD"/); assert.match(testRoute, /response\.status === 405 \|\| response\.status === 501/); assert.match(testRoute, /method: "GET"/); assert.doesNotMatch(testRoute, /method: "POST"|payment_orders|\.text\(\)|\.json\(\)/); assert.match(testRoute, /AbortSignal\.timeout\(8_000\)/); assert.match(testRoute, /redirect: "manual"/); assert.match(testRoute, /available,[\s\S]*message:[\s\S]*latencyMs:[\s\S]*status:/); assert.doesNotMatch(testRoute, /pid:|key:|gatewayUrl:|headers:|body:|payment_orders/); assert.match(testRoute, /assertPublicGatewayUrl\(submitUrl\)/); assert.match(createRoute, /assertPublicGatewayUrl\(submitUrl\)/); }); test("纯地址判断拒绝私网、回环、链路本地并接受公网", () => { for (const address of ["127.0.0.1", "10.0.0.1", "172.16.0.1", "192.168.1.1", "169.254.1.1", "::1", "fc00::1", "fe80::1", "2001:db8::1"]) { assert.equal(isPublicEpayAddress(address), false, address); } for (const address of ["8.8.8.8", "1.1.1.1", "2606:4700:4700::1111"]) { assert.equal(isPublicEpayAddress(address), true, address); } assert.throws(() => assertPublicEpayGateway("http://localhost/pay")); assert.throws(() => assertPublicEpayGateway("http://127.0.0.1/pay")); assert.doesNotThrow(() => assertPublicEpayGateway("https://pay.example.com")); }); test("迁移锁定单行、RLS、最小权限与脱敏原子审计", () => { assert.match(migration, /create table if not exists public\.epay_settings/); assert.match(migration, /id boolean primary key default true check \(id\)/); assert.match(migration, /alter table public\.epay_settings enable row level security/); assert.match(migration, /revoke all on table public\.epay_settings from public, anon, authenticated, service_role/); assert.match(migration, /grant select on table public\.epay_settings to service_role/); assert.match(migration, /security definer/); assert.match(migration, /insert into audit\.admin_audit_logs/); assert.match(migration, /'keyConfigured'/); assert.match(migration, /'keyChanged'/); const auditBlock = migration.slice(migration.indexOf("insert into audit.admin_audit_logs")); assert.doesNotMatch(auditBlock, /'encryptedKey'|'encrypted_key'|'secret'|jsonb_build_object\([\s\S]*?'key'/); });