diff --git a/e2e/fixtures/index.ts b/e2e/fixtures/index.ts index 07a6b64..55c8396 100644 --- a/e2e/fixtures/index.ts +++ b/e2e/fixtures/index.ts @@ -2,3 +2,4 @@ export { test, expect } from './auth.fixture'; export { createTestUser, registerUser, loginUser } from './auth.fixture'; export type { TokenPair } from './auth.fixture'; export { createTestListing, createListing } from './listings.fixture'; +export { buildVnpayCallbackData, buildMomoCallbackData } from './payments.fixture'; diff --git a/e2e/fixtures/payments.fixture.ts b/e2e/fixtures/payments.fixture.ts new file mode 100644 index 0000000..b325297 --- /dev/null +++ b/e2e/fixtures/payments.fixture.ts @@ -0,0 +1,111 @@ +import * as crypto from 'crypto'; + +/** + * Generates a valid VNPay callback payload with a correct HMAC-SHA512 signature. + * + * Used in E2E tests to simulate a real VNPay callback without needing + * the actual sandbox environment. + */ +export function buildVnpayCallbackData( + hashSecret: string, + opts: { + orderId: string; + amountVND: number; + responseCode?: string; + transactionNo?: string; + }, +): Record { + const params: Record = { + vnp_TxnRef: opts.orderId, + vnp_ResponseCode: opts.responseCode ?? '00', + vnp_Amount: (opts.amountVND * 100).toString(), + vnp_TransactionNo: opts.transactionNo ?? `VNP${Date.now()}`, + vnp_BankCode: 'NCB', + vnp_CardType: 'ATM', + vnp_OrderInfo: `E2E test payment ${opts.orderId}`, + vnp_PayDate: formatVnpayDate(new Date()), + vnp_TmnCode: process.env.VNPAY_TMN_CODE ?? 'TESTCODE', + vnp_TransactionStatus: opts.responseCode === '00' ? '00' : '02', + }; + + const sorted = sortObject(params); + const signData = new URLSearchParams(sorted).toString(); + const hmac = crypto.createHmac('sha512', hashSecret); + const secureHash = hmac.update(Buffer.from(signData, 'utf-8')).digest('hex'); + + return { ...sorted, vnp_SecureHash: secureHash }; +} + +/** + * Generates a valid MoMo IPN callback payload with a correct HMAC-SHA256 signature. + */ +export function buildMomoCallbackData( + secretKey: string, + opts: { + orderId: string; + amount: number; + resultCode?: number; + transId?: string; + }, +): Record { + const resultCode = opts.resultCode ?? 0; + const transId = opts.transId ?? `MOMO${Date.now()}`; + const requestId = crypto.randomUUID(); + const extraData = ''; + const message = resultCode === 0 ? 'Successful.' : 'Transaction failed.'; + const orderInfo = `E2E test payment ${opts.orderId}`; + const orderType = 'momo_wallet'; + const payType = 'qr'; + const responseTime = Date.now().toString(); + const partnerCode = process.env.MOMO_PARTNER_CODE ?? 'TESTPARTNER'; + const accessKey = process.env.MOMO_ACCESS_KEY ?? 'TESTACCESSKEY'; + + const rawSignature = [ + `accessKey=${accessKey}`, + `amount=${opts.amount}`, + `extraData=${extraData}`, + `message=${message}`, + `orderId=${opts.orderId}`, + `orderInfo=${orderInfo}`, + `orderType=${orderType}`, + `partnerCode=${partnerCode}`, + `payType=${payType}`, + `requestId=${requestId}`, + `responseTime=${responseTime}`, + `resultCode=${resultCode}`, + `transId=${transId}`, + ].join('&'); + + const signature = crypto + .createHmac('sha256', secretKey) + .update(rawSignature) + .digest('hex'); + + return { + orderId: opts.orderId, + amount: opts.amount.toString(), + extraData, + message, + orderInfo, + orderType, + partnerCode, + payType, + requestId, + responseTime, + resultCode: resultCode.toString(), + transId, + signature, + }; +} + +function formatVnpayDate(date: Date): string { + return date.toISOString().replace(/[-:T]/g, '').slice(0, 14); +} + +function sortObject(obj: Record): Record { + const sorted: Record = {}; + for (const key of Object.keys(obj).sort()) { + sorted[key] = obj[key]!; + } + return sorted; +}