Files
freestylefly__awesome-gpt-i…/api/_lib/community.test.js
2026-07-23 00:49:36 +08:00

205 lines
8.5 KiB
JavaScript

import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import test from 'node:test';
import { fileURLToPath } from 'node:url';
import {
COMMUNITY_CURRENCY,
COMMUNITY_PRICE_CENTS,
canAccessCommunityQr,
communityOrderPayload,
communityRefundRequestNo,
detectCommunityQrMediaType,
isCommunityAdmin,
isCommunityPaymentEnabled,
isOrderOwnedBy,
shouldExpireCommunityOrder,
takeCommunityRateLimit,
validateCommunityPaidNotification,
validateCommunityQrUpload,
validateSameOrigin
} from './community.js';
import {
queryCommunityOrderAtAlipay,
shouldQueryCommunityOrderAtAlipay
} from './community-alipay.js';
const migrationPath = fileURLToPath(new URL(
'../../supabase/migrations/20260722090000_paid_community.sql',
import.meta.url
));
const migration = readFileSync(migrationPath, 'utf8');
function paidNotification(overrides = {}) {
return {
notify_type: 'trade_status_sync',
notify_id: 'notify-1',
app_id: 'app-1',
seller_id: 'seller-1',
out_trade_no: '11111111-1111-4111-8111-111111111111',
trade_no: 'trade-1',
trade_status: 'TRADE_SUCCESS',
total_amount: '9.90',
currency: 'CNY',
...overrides
};
}
const order = {
id: '11111111-1111-4111-8111-111111111111',
user_id: 'user-1',
status: 'PENDING',
amount_cents: COMMUNITY_PRICE_CENTS,
currency: COMMUNITY_CURRENCY
};
test('community checkout payload keeps the server-owned ¥9.90 CNY price', () => {
const payload = communityOrderPayload('user-1', 'terms-v1');
assert.equal(payload.amountCents, 990);
assert.equal(payload.currency, 'CNY');
assert.equal(Object.hasOwn(payload, 'clientAmount'), false);
});
test('migration enforces one active PENDING or PAID order per user', () => {
assert.match(migration, /create unique index if not exists community_orders_one_active_per_user_idx/i);
assert.match(migration, /where status in \('PENDING', 'PAID'\)/i);
assert.match(migration, /pg_advisory_xact_lock\(hashtext\(p_user_id::text\)\)/i);
});
test('expired pending orders are identified before replacement', () => {
const oldOrder = { status: 'PENDING', created_at: '2026-07-22T00:00:00.000Z' };
assert.equal(shouldExpireCommunityOrder(oldOrder, Date.parse('2026-07-22T00:30:00.000Z')), true);
assert.equal(shouldExpireCommunityOrder(oldOrder, Date.parse('2026-07-22T00:29:59.000Z')), false);
assert.equal(shouldExpireCommunityOrder({ ...oldOrder, status: 'PAID' }, Date.now()), false);
});
test('community order ownership is bound to the authenticated user', () => {
assert.equal(isOrderOwnedBy(order, 'user-1'), true);
assert.equal(isOrderOwnedBy(order, 'user-2'), false);
});
test('paid notification rejects mismatched business fields', () => {
const runtime = { appId: 'app-1', sellerId: 'seller-1' };
assert.equal(validateCommunityPaidNotification(order, paidNotification(), runtime).ok, true);
for (const params of [
paidNotification({ app_id: 'wrong' }),
paidNotification({ seller_id: 'wrong' }),
paidNotification({ out_trade_no: 'wrong' }),
paidNotification({ total_amount: '9.91' }),
paidNotification({ currency: 'USD' }),
paidNotification({ trade_status: 'WAIT_BUYER_PAY' }),
paidNotification({ refund_fee: '9.90' })
]) {
assert.equal(validateCommunityPaidNotification(order, params, runtime).ok, false);
}
});
test('notification and paid transition are persistently idempotent', () => {
assert.match(migration, /notify_id text not null unique/i);
assert.match(migration, /on conflict \(notify_id\) do nothing/i);
assert.match(migration, /if v_order\.status in \('PAID', 'REFUNDED', 'REVOKED'\)/i);
});
test('only PAID status can read the protected QR', () => {
assert.equal(canAccessCommunityQr('PAID'), true);
for (const status of ['PENDING', 'CLOSED', 'REFUNDED', 'REVOKED', null]) {
assert.equal(canAccessCommunityQr(status), false);
}
});
test('refund keeps PAID access until final success then revokes it', () => {
assert.match(migration, /refund_status\s*=\s*'PROCESSING'/i);
assert.match(migration, /set status = 'REFUNDED',[\s\S]*refund_status = 'SUCCEEDED'/i);
assert.match(migration, /if v_order\.status <> 'PAID' or v_order\.refund_status <> 'PROCESSING'/i);
assert.equal(communityRefundRequestNo(order.id), 'cg_11111111111141118111111111111111');
});
test('QR replacement uses a transaction lock and one-current-row index', () => {
assert.match(migration, /community_group_qr_one_current_idx/i);
assert.match(migration, /pg_advisory_xact_lock\(hashtext\('community_group_qr_current'\)\)/i);
assert.match(migration, /set is_current = false,[\s\S]*insert into public\.community_group_qr_assets/i);
});
test('QR uploads validate signatures and size', () => {
const png = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00]);
const jpg = Buffer.from([0xff, 0xd8, 0xff, 0x00]);
const webp = Buffer.from('RIFF0000WEBP', 'ascii');
assert.equal(detectCommunityQrMediaType(png), 'image/png');
assert.equal(detectCommunityQrMediaType(jpg), 'image/jpeg');
assert.equal(detectCommunityQrMediaType(webp), 'image/webp');
assert.equal(validateCommunityQrUpload(png, 'image/png').ok, true);
assert.equal(validateCommunityQrUpload(png, 'image/jpeg').ok, false);
assert.equal(validateCommunityQrUpload(Buffer.alloc(2 * 1024 * 1024 + 1), '').error, 'COMMUNITY_QR_TOO_LARGE');
});
test('admin operations require super_admin and same-origin writes', () => {
assert.equal(isCommunityAdmin({ profile: { role: 'super_admin' } }), true);
assert.equal(isCommunityAdmin({ profile: { role: 'user' } }), false);
const previousAppUrl = process.env.APP_URL;
process.env.APP_URL = 'https://gpt-image2.canghe.ai';
try {
assert.equal(validateSameOrigin({ headers: { origin: 'https://gpt-image2.canghe.ai', host: 'gpt-image2.canghe.ai' } }), true);
assert.equal(validateSameOrigin({ headers: { origin: 'https://evil.example', host: 'gpt-image2.canghe.ai' } }), false);
} finally {
if (previousAppUrl === undefined) delete process.env.APP_URL;
else process.env.APP_URL = previousAppUrl;
}
});
test('payment kill switch defaults closed and changes only on explicit true', () => {
assert.equal(isCommunityPaymentEnabled({}), false);
assert.equal(isCommunityPaymentEnabled({ COMMUNITY_PAYMENT_ENABLED: 'false' }), false);
assert.equal(isCommunityPaymentEnabled({ COMMUNITY_PAYMENT_ENABLED: 'true' }), true);
});
test('active reconciliation queries Alipay for both pending and already-paid orders', async () => {
assert.equal(shouldQueryCommunityOrderAtAlipay('PENDING'), true);
assert.equal(shouldQueryCommunityOrderAtAlipay('PAID'), true);
for (const status of ['CLOSED', 'REFUNDED', 'REVOKED']) {
assert.equal(shouldQueryCommunityOrderAtAlipay(status), false);
}
const calls = [];
const sdk = {
async exec(method, payload, options) {
calls.push({ method, payload, options });
return {
code: '10000',
out_trade_no: order.id,
trade_no: 'trade-query-1',
trade_status: 'TRADE_SUCCESS',
total_amount: '9.90',
currency: 'CNY'
};
}
};
const client = {
async rpc(name, payload) {
calls.push({ name, payload });
return { data: [{ current_status: 'PAID', transitioned: false }], error: null };
}
};
const result = await queryCommunityOrderAtAlipay(client, sdk, { ...order, status: 'PAID' });
assert.equal(result.state, 'PAID');
assert.equal(calls[0].method, 'alipay.trade.query');
assert.equal(calls[0].payload.bizContent.out_trade_no, order.id);
assert.equal(calls[0].options.validateSign, true);
assert.equal(calls[1].name, 'mark_community_order_paid');
});
test('community actions use bounded per-user rate limits', () => {
const key = `test-rate-${Date.now()}`;
assert.equal(takeCommunityRateLimit(key, { limit: 2, windowMs: 1000, now: 100 }).allowed, true);
assert.equal(takeCommunityRateLimit(key, { limit: 2, windowMs: 1000, now: 101 }).allowed, true);
const blocked = takeCommunityRateLimit(key, { limit: 2, windowMs: 1000, now: 102 });
assert.equal(blocked.allowed, false);
assert.equal(blocked.retryAfterSeconds, 1);
assert.equal(takeCommunityRateLimit(key, { limit: 2, windowMs: 1000, now: 1100 }).allowed, true);
});
test('browser roles have no direct table grants for orders or QR bytes', () => {
assert.match(migration, /revoke all on table public\.community_orders from public, anon, authenticated/i);
assert.match(migration, /revoke all on table public\.community_group_qr_assets from public, anon, authenticated/i);
assert.match(migration, /grant execute on function public\.replace_community_group_qr_asset[\s\S]*to service_role/i);
});