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 { EpayConfigurationError, resolveEpayConfig } from "../src/lib/epay/config-core"; import { assertConfiguredEpayUrl, assertPublicEpayGateway, assertPublicGatewayUrl, 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 gatewayPolicy = readFileSync(new URL("src/lib/epay/gateway-policy.ts", root), "utf8"); const page = readFileSync(new URL("src/app/page.tsx", root), "utf8"); const membershipPage = readFileSync(new URL("src/app/membership/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("生产与默认开发测试配置强制 HTTPS,仅显式开关允许 loopback HTTP", async () => { const base = { EPAY_GATEWAY_URL: "https://pay.example.com", EPAY_PID: "merchant", EPAY_KEY: "secret", EPAY_NOTIFY_URL: "https://app.example.com/api/payment/epay/notify", EPAY_RETURN_URL: "https://app.example.com/", }; for (const name of ["EPAY_GATEWAY_URL", "EPAY_NOTIFY_URL", "EPAY_RETURN_URL"] as const) { await assert.rejects( resolveEpayConfig(async () => null, { ...base, NODE_ENV: "production", [name]: "http://pay.example.com" }), EpayConfigurationError, ); await assert.rejects( resolveEpayConfig(async () => null, { ...base, NODE_ENV: "test", [name]: "http://localhost:3000" }), EpayConfigurationError, ); } const loopback = await resolveEpayConfig(async () => null, { NODE_ENV: "test", EPAY_ALLOW_INSECURE_LOOPBACK_HTTP: "true", EPAY_GATEWAY_URL: "http://127.0.0.1:8080", EPAY_PID: "merchant", EPAY_KEY: "secret", EPAY_NOTIFY_URL: "http://localhost:3000/api/payment/epay/notify", EPAY_RETURN_URL: "http://[::1]:3000/", }); assert.equal(loopback.gatewayUrl.toString(), "http://127.0.0.1:8080/"); assert.throws(() => assertConfiguredEpayUrl("http://pay.example.com", { NODE_ENV: "test", EPAY_ALLOW_INSECURE_LOOPBACK_HTTP: "true", })); assert.throws(() => assertConfiguredEpayUrl("http://localhost:3000", { NODE_ENV: "production", EPAY_ALLOW_INSECURE_LOOPBACK_HTTP: "true", })); }); test("公网网关解析拒绝混入私网地址且每次校验只解析一次", async () => { let lookups = 0; await assert.rejects( assertPublicGatewayUrl("https://pay.example.com", { NODE_ENV: "production" }, async () => { lookups += 1; return [ { address: "93.184.216.34", family: 4 }, { address: "127.0.0.1", family: 4 }, ]; }), /内网|保留地址/, ); assert.equal(lookups, 1); }); test("管理员 API 不回显任何密钥并强制首次显式录入", () => { assert.match(route, /requirePermission\("billing\.orders\.read"\)/); assert.match(route, /requireAdminMutation\(request, "billing\.adjustments\.write"\)/); assert.doesNotMatch(route, /requireHighRiskAdminMutation/); assert.match(route, /\.strict\(\)/); assert.match(route, /crypto\.randomUUID\(\)/); assert.match(route, /首次保存数据库配置时必须输入新的商户密钥/); assert.match(route, /assertConfiguredEpayUrl\(value\)/); assert.match(route, /await assertPublicGatewayUrl\(parsed\.data\.gatewayUrl\)/); 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("billing_products")); assert.match(management, /在对话页开放支付/); assert.match(membershipPage, /fetch\("\/api\/payment\/packages"/); assert.match(membershipPage, /payload\?\..*enabled === true/); assert.match(membershipPage, /setPaymentEnabled\(true\)/); assert.match(membershipPage, /setPaymentEnabled\(false\)[\s\S]*setPaymentPackages\(\[\]\)[\s\S]*setPaymentOrder\(null\)[\s\S]*setPaymentError\(""\)/); assert.doesNotMatch(page, /paymentEnabled|paymentPackages|paymentOrder|payingPackageId/); }); test("创建订单返回已签名收银台 URL 且不服务端请求网关", () => { assert.match(createRoute, /assertPublicGatewayUrl\(submitUrl\)/); assert.match(createRoute, /const signedParams = \{\s*\.\.\.params,\s*sign: epaySign\(params, config\.key\),\s*sign_type: "MD5",?\s*\}/); assert.match(createRoute, /const payUrl = new URL\(submitUrl\)/); assert.match(createRoute, /payUrl\.searchParams\.set\(name, value\)/); assert.match(createRoute, /NextResponse\.json\(\{\s*orderNo,\s*payUrl: payUrl\.toString\(\),\s*qrCode: null,\s*product: productSnapshot,?\s*\}\)/); 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(membershipPage, /window\.open\(payload\.payUrl, "_blank", "noopener,noreferrer"\)/); assert.match(membershipPage, /id="membership-plans-tab"/); assert.match(membershipPage, /id="membership-credits-tab"/); assert.match(membershipPage, /立即购买/); assert.match(membershipPage, /套餐支付暂时不可用,请稍后重试/); assert.doesNotMatch(membershipPage, /document\.createElement\("form"\)|payload\.submitUrl|payload\.fields/); }); test("网关探测固定已验证公网 IP、保留 TLS hostname 且限制重定向与响应", () => { assert.match(testRoute, /requireAdminMutation\(request, "billing\.adjustments\.write"\)/); assert.match(testRoute, /probePublicEpayGateway\(submitUrl\)/); assert.doesNotMatch(testRoute, /fetch\(|method: "POST"|payment_orders|\.text\(\)|\.json\(\)/); 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(gatewayPolicy, /const resolved = await withinTimeout\([\s\S]*resolvePublicUrl\(value, options\.lookup \?\? defaultLookup\)/); assert.match(gatewayPolicy, /const pinned = resolved\.addresses\[0\]!/); assert.match(gatewayPolicy, /servername: isIP\(hostname\) \? undefined : hostname/); assert.match(gatewayPolicy, /lookup: pinnedAddressLookup\(pinned\)/); assert.match(gatewayPolicy, /requestPinnedHttps\([\s\S]*resolved,[\s\S]*"HEAD"/); assert.match(gatewayPolicy, /headStatus === 405 \|\| headStatus === 501[\s\S]*requestPinnedHttps\([\s\S]*resolved,[\s\S]*"GET"/); assert.match(gatewayPolicy, /status >= 300 && status < 400/); assert.match(gatewayPolicy, /maxResponseBytes|setTimeout\(timeoutMs/); 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", "192.0.2.1", "198.51.100.1", "203.0.113.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'/); });