import assert from "node:assert/strict"; import { existsSync, readFileSync } from "node:fs"; import test from "node:test"; import { formatPaymentOrders, PAYMENT_ORDERS_SELECT, } from "../src/lib/payment-orders.ts"; import { redeemErrorResponse, redeemInputErrorResponse, } from "../src/lib/redeem-response.ts"; const redeemRoute = readFileSync( new URL("../src/app/api/redeem/route.ts", import.meta.url), "utf8", ); const ordersRoute = readFileSync( new URL("../src/app/api/payment/orders/route.ts", import.meta.url), "utf8", ); // Only the supabase compatibility migration exists: business-schema // migrations must not be duplicated into db/migrations (identity-only // fixtures apply that directory without the business schema; see BUG-144). const redeemSecurityMigration = readFileSync( new URL("../supabase/migrations/20260807030000_redeem_security.sql", import.meta.url), "utf8", ); test("redeem API trims surrounding whitespace only, never changes case, and never pre-validates format", () => { assert.match(redeemRoute, /\.trim\(\)/); assert.doesNotMatch(redeemRoute, /normalizeRedeemCode|\.toUpperCase\(\)/); // No client-side format short-circuit: lowercase or malformed non-empty // values must reach the RPC so they count against the failure rate limit. assert.doesNotMatch(redeemRoute, /JYOTISH-\[A-Z0-9\]|codeFormat/); assert.match(redeemRoute, /if \(!code\)/); // The trimmed input is hashed as-is so a lowercase attempt cannot match. assert.match(redeemRoute, /hashRedeemCode\(code\)/); }); test("redeem API returns stable error codes with the 429 rate-limit copy", () => { assert.deepEqual(redeemErrorResponse("rate_limited"), { status: 429, message: "请求过于频繁", code: "rate_limited", }); assert.deepEqual(redeemErrorResponse("invalid_code"), { status: 404, message: "兑换码不存在", code: "invalid_code", }); assert.deepEqual(redeemErrorResponse("revoked_code"), { status: 404, message: "兑换码已撤销", code: "invalid_code", }); assert.deepEqual(redeemErrorResponse("expired_code"), { status: 410, message: "兑换码已过期", code: "expired_code", }); assert.deepEqual(redeemErrorResponse("already_redeemed"), { status: 409, message: "兑换码已被使用", code: "already_redeemed", }); assert.deepEqual(redeemErrorResponse("account_not_eligible"), { status: 403, message: "该兑换码不适用于当前账户", code: "account_not_eligible", }); assert.deepEqual(redeemErrorResponse("profile_missing"), { status: 500, message: "系统异常,请稍后重试", code: "system_error", }); assert.deepEqual(redeemErrorResponse("unknown_future_code"), { status: 500, message: "系统异常,请稍后重试", code: "system_error", }); assert.deepEqual(redeemInputErrorResponse(), { status: 400, message: "请输入有效兑换码", code: "invalid_code", }); assert.match(redeemRoute, /redeemErrorResponse\(/); assert.match(redeemRoute, /code: mapped\.code/); assert.match(redeemRoute, /status: 401/); }); test("redeem API success returns awardedCredits and the latest balance", () => { assert.match(redeemRoute, /awardedCredits: result\.awarded_credits/); assert.match(redeemRoute, /credits: result\.credits/); assert.match(redeemRoute, /result\??\.error_code/); }); test("payment orders API is authenticated, owner-filtered and limited", () => { assert.match(ordersRoute, /请先登录/); assert.match(ordersRoute, /status: 401/); assert.match(ordersRoute, /\.eq\("user_id", user\.id\)/); assert.match(ordersRoute, /\.order\("created_at", \{ ascending: false \}\)/); assert.match(ordersRoute, /\.limit\(MAX_ORDERS\)/); assert.doesNotMatch(ordersRoute, /epay_trade_no|raw_notify_payload_hash/); }); test("payment orders summary is desensitized to the allowlist", () => { assert.deepEqual(PAYMENT_ORDERS_SELECT, [ "order_no", "product_code", "product_snapshot", "money_cents", "status", "grant_status", "created_at", "paid_at", ]); assert.doesNotMatch( PAYMENT_ORDERS_SELECT.join(","), /user_id|epay_trade_no|raw_notify|entitlement/, ); assert.deepEqual( formatPaymentOrders([ { order_no: "JY-ONE", product_code: "standard_monthly", product_snapshot: { name: "标准月卡" }, money_cents: 9900, status: "paid", grant_status: "granted", created_at: new Date("2026-08-07T00:00:00.000Z"), paid_at: new Date("2026-08-07T00:05:00.000Z"), }, { order_no: "JY-TWO", product_code: null, product_snapshot: null, money_cents: 990, status: "pending", grant_status: "pending", created_at: "2026-08-06T00:00:00.000Z", paid_at: null, }, ]), [ { orderNo: "JY-ONE", product: "standard_monthly", name: "标准月卡", price: 9900, status: "paid", grantStatus: "granted", createdAt: "2026-08-07T00:00:00.000Z", paidAt: "2026-08-07T00:05:00.000Z", }, { orderNo: "JY-TWO", product: null, name: null, price: 990, status: "pending", grantStatus: "pending", createdAt: "2026-08-06T00:00:00.000Z", paidAt: null, }, ], ); }); test("redeem security migration adds a hash-free failure-only audit table with least privilege", () => { const tableBlock = redeemSecurityMigration.match( /create table if not exists public\.redemption_attempts[\s\S]*?\);/, ); assert.ok(tableBlock, "redemption_attempts table must be created"); assert.match(tableBlock[0], /user_id uuid not null references auth\.users\(id\) on delete cascade/); assert.match(tableBlock[0], /created_at timestamptz not null default now\(\)/); assert.match(tableBlock[0], /generated always as identity/); // Failure-only: no success column, no plaintext code and no code hash. assert.doesNotMatch(tableBlock[0], /success|code|hash|mask/i); assert.match(redeemSecurityMigration, /drop function if exists public\.redeem_code\(text\);/); assert.match(redeemSecurityMigration, /alter table public\.redemption_attempts enable row level security/); assert.match(redeemSecurityMigration, /revoke all on table public\.redemption_attempts from anon, authenticated/); assert.match(redeemSecurityMigration, /grant select on table public\.redemption_attempts to service_role/); assert.match(redeemSecurityMigration, /pg_advisory_xact_lock/); assert.match(redeemSecurityMigration, /interval '10 minutes'/); assert.match(redeemSecurityMigration, /v_failed >= 5/); // Business failures insert a single-column audit row; success deletes all // of the account's attempts instead of inserting a permanent success row. assert.match(redeemSecurityMigration, /insert into public\.redemption_attempts \(user_id\) values \(v_user_id\)/); assert.match(redeemSecurityMigration, /delete from public\.redemption_attempts where user_id = v_user_id;/); // The redemption-code row lock is kept and the credit_transactions // unique constraint is preserved untouched (nothing is dropped). assert.match(redeemSecurityMigration, /from public\.redemption_codes rc\s+where rc\.code_hash = p_code_hash\s+for update/); assert.doesNotMatch(redeemSecurityMigration, /alter table public\.credit_transactions/); assert.match(redeemSecurityMigration, /grant execute on function public\.redeem_code\(text\) to authenticated/); assert.match(redeemSecurityMigration, /revoke all on function public\.redeem_code\(text\) from public, anon/); }); test("db/migrations must not carry the business-schema redemption copy (BUG-144 guard)", () => { assert.equal( existsSync( new URL( "../db/migrations/20260807020000_redeem_security.sql", import.meta.url, ), ), false, ); });