- H1: 支付宝响应验签 (verifyResponseSign + bracket-matching 提取签名内容) - H2/H3: EasyPay queryOrder 从 GET 改 POST,PKEY 不再暴露于 URL - H5: users/[id] IDOR 修复,校验当前用户只能查询自身信息 - H6: 限额校验移入 prisma.$transaction() 防止 TOCTOU 竞态 - C1: access_token 增加 24h 过期、userId 绑定、派生密钥分离 - M1: EasyPay 回调增加 pid 校验防跨商户注入 - M4: 充值码增加 crypto.randomBytes 随机后缀 - M5: 过期订单批量处理增加 BATCH_SIZE 限制 - M6: 退款失败增加 [CRITICAL] 日志和余额补偿标记 - M7: admin channels PUT 增加 Zod schema 校验 - M8: admin subscriptions 分页参数增加上限 - M9: orders src_url 限制 HTTP/HTTPS 协议 - L1: 微信支付回调时间戳 NaN 检查 - L9: WXPAY_API_V3_KEY 长度校验
99 lines
3.6 KiB
TypeScript
99 lines
3.6 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
|
|
vi.mock('@/lib/config', () => ({
|
|
getEnv: () => ({
|
|
ALIPAY_APP_ID: '2021000000000000',
|
|
ALIPAY_PRIVATE_KEY: 'test-private-key',
|
|
ALIPAY_PUBLIC_KEY: 'test-public-key',
|
|
ALIPAY_NOTIFY_URL: 'https://pay.example.com/api/alipay/notify',
|
|
ALIPAY_RETURN_URL: 'https://pay.example.com/pay/result',
|
|
}),
|
|
}));
|
|
|
|
const { mockGenerateSign } = vi.hoisted(() => ({
|
|
mockGenerateSign: vi.fn(() => 'signed-value'),
|
|
}));
|
|
vi.mock('@/lib/alipay/sign', () => ({
|
|
generateSign: mockGenerateSign,
|
|
verifyResponseSign: vi.fn(() => true),
|
|
}));
|
|
|
|
import { execute, pageExecute } from '@/lib/alipay/client';
|
|
|
|
describe('alipay client helpers', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
it('pageExecute includes notify_url and return_url by default', () => {
|
|
const url = new URL(
|
|
pageExecute({ out_trade_no: 'order-001', product_code: 'FAST_INSTANT_TRADE_PAY', total_amount: '10.00' }),
|
|
);
|
|
|
|
expect(url.origin + url.pathname).toBe('https://openapi.alipay.com/gateway.do');
|
|
expect(url.searchParams.get('notify_url')).toBe('https://pay.example.com/api/alipay/notify');
|
|
expect(url.searchParams.get('return_url')).toBe('https://pay.example.com/pay/result');
|
|
expect(url.searchParams.get('method')).toBe('alipay.trade.page.pay');
|
|
expect(url.searchParams.get('sign')).toBe('signed-value');
|
|
});
|
|
|
|
it('pageExecute omits return_url when explicitly disabled', () => {
|
|
const url = new URL(
|
|
pageExecute(
|
|
{ out_trade_no: 'order-002', product_code: 'QUICK_WAP_WAY', total_amount: '20.00' },
|
|
{ returnUrl: null, method: 'alipay.trade.wap.pay' },
|
|
),
|
|
);
|
|
|
|
expect(url.searchParams.get('method')).toBe('alipay.trade.wap.pay');
|
|
expect(url.searchParams.get('return_url')).toBeNull();
|
|
expect(url.searchParams.get('notify_url')).toBe('https://pay.example.com/api/alipay/notify');
|
|
});
|
|
|
|
it('execute posts form data and returns the named response payload', async () => {
|
|
global.fetch = vi.fn().mockResolvedValue(
|
|
new Response(
|
|
JSON.stringify({
|
|
alipay_trade_query_response: {
|
|
code: '10000',
|
|
msg: 'Success',
|
|
trade_status: 'TRADE_SUCCESS',
|
|
},
|
|
sign: 'server-sign',
|
|
}),
|
|
{ headers: { 'content-type': 'application/json; charset=utf-8' } },
|
|
),
|
|
) as typeof fetch;
|
|
|
|
const result = await execute('alipay.trade.query', { out_trade_no: 'order-003' });
|
|
|
|
expect(result).toEqual({ code: '10000', msg: 'Success', trade_status: 'TRADE_SUCCESS' });
|
|
expect(global.fetch).toHaveBeenCalledTimes(1);
|
|
const [url, init] = (global.fetch as ReturnType<typeof vi.fn>).mock.calls[0];
|
|
expect(url).toBe('https://openapi.alipay.com/gateway.do');
|
|
expect(init.method).toBe('POST');
|
|
expect(init.headers).toEqual({ 'Content-Type': 'application/x-www-form-urlencoded' });
|
|
expect(String(init.body)).toContain('method=alipay.trade.query');
|
|
});
|
|
|
|
it('execute throws when alipay response code is not successful', async () => {
|
|
global.fetch = vi.fn().mockResolvedValue(
|
|
new Response(
|
|
JSON.stringify({
|
|
alipay_trade_query_response: {
|
|
code: '40004',
|
|
msg: 'Business Failed',
|
|
sub_code: 'ACQ.TRADE_NOT_EXIST',
|
|
sub_msg: 'trade not exist',
|
|
},
|
|
}),
|
|
{ headers: { 'content-type': 'application/json; charset=utf-8' } },
|
|
),
|
|
) as typeof fetch;
|
|
|
|
await expect(execute('alipay.trade.query', { out_trade_no: 'order-004' })).rejects.toThrow(
|
|
'[ACQ.TRADE_NOT_EXIST] trade not exist',
|
|
);
|
|
});
|
|
});
|