- 新增 src/lib/alipay/ 模块:RSA2 签名、网关客户端、AlipayProvider - 新增 /api/alipay/notify 异步通知回调路由 - config.ts 添加 ALIPAY_* 环境变量 - payment/index.ts 注册 alipaydirect 提供商 - 27 个单元测试全部通过
113 lines
3.9 KiB
TypeScript
113 lines
3.9 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
||
import crypto from 'crypto';
|
||
import { generateSign, verifySign } from '@/lib/alipay/sign';
|
||
|
||
// 生成测试用 RSA 密钥对
|
||
const { publicKey, privateKey } = crypto.generateKeyPairSync('rsa', {
|
||
modulusLength: 2048,
|
||
publicKeyEncoding: { type: 'spki', format: 'pem' },
|
||
privateKeyEncoding: { type: 'pkcs8', format: 'pem' },
|
||
});
|
||
|
||
// 提取裸 base64(去掉 PEM 头尾)
|
||
const barePrivateKey = privateKey
|
||
.replace(/-----BEGIN PRIVATE KEY-----/, '')
|
||
.replace(/-----END PRIVATE KEY-----/, '')
|
||
.replace(/\n/g, '');
|
||
const barePublicKey = publicKey
|
||
.replace(/-----BEGIN PUBLIC KEY-----/, '')
|
||
.replace(/-----END PUBLIC KEY-----/, '')
|
||
.replace(/\n/g, '');
|
||
|
||
describe('Alipay RSA2 Sign', () => {
|
||
const testParams: Record<string, string> = {
|
||
app_id: '2021000000000000',
|
||
method: 'alipay.trade.page.pay',
|
||
charset: 'utf-8',
|
||
timestamp: '2026-03-05 12:00:00',
|
||
version: '1.0',
|
||
biz_content: '{"out_trade_no":"order-001","total_amount":"100.00"}',
|
||
};
|
||
|
||
describe('generateSign', () => {
|
||
it('should generate a valid RSA2 signature', () => {
|
||
const sign = generateSign(testParams, privateKey);
|
||
expect(sign).toBeTruthy();
|
||
expect(typeof sign).toBe('string');
|
||
// base64 格式
|
||
expect(() => Buffer.from(sign, 'base64')).not.toThrow();
|
||
});
|
||
|
||
it('should produce consistent signatures for same input', () => {
|
||
const sign1 = generateSign(testParams, privateKey);
|
||
const sign2 = generateSign(testParams, privateKey);
|
||
expect(sign1).toBe(sign2);
|
||
});
|
||
|
||
it('should filter out sign and sign_type fields', () => {
|
||
const paramsWithSign = { ...testParams, sign: 'old_sign', sign_type: 'RSA2' };
|
||
const sign1 = generateSign(testParams, privateKey);
|
||
const sign2 = generateSign(paramsWithSign, privateKey);
|
||
expect(sign1).toBe(sign2);
|
||
});
|
||
|
||
it('should filter out empty values', () => {
|
||
const paramsWithEmpty = { ...testParams, empty_field: '' };
|
||
const sign1 = generateSign(testParams, privateKey);
|
||
const sign2 = generateSign(paramsWithEmpty, privateKey);
|
||
expect(sign1).toBe(sign2);
|
||
});
|
||
|
||
it('should sort parameters alphabetically', () => {
|
||
const reversed: Record<string, string> = {};
|
||
const keys = Object.keys(testParams).reverse();
|
||
for (const key of keys) {
|
||
reversed[key] = testParams[key];
|
||
}
|
||
const sign1 = generateSign(testParams, privateKey);
|
||
const sign2 = generateSign(reversed, privateKey);
|
||
expect(sign1).toBe(sign2);
|
||
});
|
||
});
|
||
|
||
describe('verifySign', () => {
|
||
it('should verify a valid signature', () => {
|
||
const sign = generateSign(testParams, privateKey);
|
||
const valid = verifySign(testParams, publicKey, sign);
|
||
expect(valid).toBe(true);
|
||
});
|
||
|
||
it('should reject an invalid signature', () => {
|
||
const valid = verifySign(testParams, publicKey, 'invalid_base64_signature');
|
||
expect(valid).toBe(false);
|
||
});
|
||
|
||
it('should reject tampered params', () => {
|
||
const sign = generateSign(testParams, privateKey);
|
||
const tampered = { ...testParams, total_amount: '999.99' };
|
||
const valid = verifySign(tampered, publicKey, sign);
|
||
expect(valid).toBe(false);
|
||
});
|
||
});
|
||
|
||
describe('PEM auto-formatting', () => {
|
||
it('should work with bare base64 private key (no PEM headers)', () => {
|
||
const sign = generateSign(testParams, barePrivateKey);
|
||
const valid = verifySign(testParams, publicKey, sign);
|
||
expect(valid).toBe(true);
|
||
});
|
||
|
||
it('should work with bare base64 public key (no PEM headers)', () => {
|
||
const sign = generateSign(testParams, privateKey);
|
||
const valid = verifySign(testParams, barePublicKey, sign);
|
||
expect(valid).toBe(true);
|
||
});
|
||
|
||
it('should work with both bare keys', () => {
|
||
const sign = generateSign(testParams, barePrivateKey);
|
||
const valid = verifySign(testParams, barePublicKey, sign);
|
||
expect(valid).toBe(true);
|
||
});
|
||
});
|
||
});
|