test(api): add unit tests for analytics, metrics, notifications, payments, and search modules
New test coverage for infrastructure and presentation layers across multiple modules including Momo/ZaloPay payment services, Typesense search repository, listing indexer, and notification handlers. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
import * as crypto from 'crypto';
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import { MomoService } from '../services/momo.service';
|
||||
|
||||
describe('MomoService', () => {
|
||||
let service: MomoService;
|
||||
const secretKey = 'test-momo-secret-key-32chars!!ab';
|
||||
const partnerCode = 'MOMO_TEST';
|
||||
const accessKey = 'test-access-key';
|
||||
|
||||
beforeEach(() => {
|
||||
vi.stubEnv('MOMO_PARTNER_CODE', partnerCode);
|
||||
vi.stubEnv('MOMO_ACCESS_KEY', accessKey);
|
||||
vi.stubEnv('MOMO_SECRET_KEY', secretKey);
|
||||
service = new MomoService();
|
||||
});
|
||||
|
||||
function buildCallbackData(overrides: Record<string, string> = {}): Record<string, string> {
|
||||
const data: Record<string, string> = {
|
||||
amount: '500000',
|
||||
extraData: '',
|
||||
message: 'Success',
|
||||
orderId: 'order-123',
|
||||
orderInfo: 'Test payment',
|
||||
orderType: 'momo_wallet',
|
||||
partnerCode,
|
||||
payType: 'qr',
|
||||
requestId: 'req-123',
|
||||
responseTime: '1700000000000',
|
||||
resultCode: '0',
|
||||
transId: 'MOMO_TX_123',
|
||||
...overrides,
|
||||
};
|
||||
|
||||
const rawSignature = [
|
||||
`accessKey=${accessKey}`,
|
||||
`amount=${data['amount']}`,
|
||||
`extraData=${data['extraData']}`,
|
||||
`message=${data['message']}`,
|
||||
`orderId=${data['orderId']}`,
|
||||
`orderInfo=${data['orderInfo']}`,
|
||||
`orderType=${data['orderType']}`,
|
||||
`partnerCode=${data['partnerCode']}`,
|
||||
`payType=${data['payType']}`,
|
||||
`requestId=${data['requestId']}`,
|
||||
`responseTime=${data['responseTime']}`,
|
||||
`resultCode=${data['resultCode']}`,
|
||||
`transId=${data['transId']}`,
|
||||
].join('&');
|
||||
|
||||
data['signature'] = crypto
|
||||
.createHmac('sha256', secretKey)
|
||||
.update(rawSignature)
|
||||
.digest('hex');
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
it('should verify a valid callback with timingSafeEqual', () => {
|
||||
const data = buildCallbackData();
|
||||
const result = service.verifyCallback(data);
|
||||
|
||||
expect(result.isValid).toBe(true);
|
||||
expect(result.isSuccess).toBe(true);
|
||||
expect(result.orderId).toBe('order-123');
|
||||
expect(result.providerTxId).toBe('MOMO_TX_123');
|
||||
});
|
||||
|
||||
it('should reject an invalid signature', () => {
|
||||
const data = buildCallbackData();
|
||||
// Tamper the signature — replace with a same-length hex string
|
||||
data['signature'] = 'a'.repeat(data['signature']!.length);
|
||||
|
||||
const result = service.verifyCallback(data);
|
||||
expect(result.isValid).toBe(false);
|
||||
expect(result.isSuccess).toBe(false);
|
||||
});
|
||||
|
||||
it('should reject an empty signature', () => {
|
||||
const data = buildCallbackData();
|
||||
data['signature'] = '';
|
||||
|
||||
const result = service.verifyCallback(data);
|
||||
expect(result.isValid).toBe(false);
|
||||
});
|
||||
|
||||
it('should reject a signature with wrong length', () => {
|
||||
const data = buildCallbackData();
|
||||
data['signature'] = 'abcdef1234'; // too short
|
||||
|
||||
const result = service.verifyCallback(data);
|
||||
expect(result.isValid).toBe(false);
|
||||
});
|
||||
|
||||
it('should detect failed payment (non-zero resultCode) with valid signature', () => {
|
||||
const data = buildCallbackData({ resultCode: '1006' });
|
||||
|
||||
const result = service.verifyCallback(data);
|
||||
expect(result.isValid).toBe(true);
|
||||
expect(result.isSuccess).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,84 @@
|
||||
import * as crypto from 'crypto';
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import { ZalopayService } from '../services/zalopay.service';
|
||||
|
||||
describe('ZalopayService', () => {
|
||||
let service: ZalopayService;
|
||||
const key2 = 'test-zalopay-key2-for-callback!!';
|
||||
|
||||
beforeEach(() => {
|
||||
vi.stubEnv('ZALOPAY_APP_ID', '2553');
|
||||
vi.stubEnv('ZALOPAY_KEY1', 'test-zalopay-key1-for-signing!!a');
|
||||
vi.stubEnv('ZALOPAY_KEY2', key2);
|
||||
service = new ZalopayService();
|
||||
});
|
||||
|
||||
function buildCallbackData(
|
||||
dataPayload: Record<string, unknown> = {},
|
||||
tamperMac = false,
|
||||
): Record<string, string> {
|
||||
const payload = {
|
||||
app_id: 2553,
|
||||
app_trans_id: '260408_order-123',
|
||||
zp_trans_id: 'ZLP_TX_456',
|
||||
...dataPayload,
|
||||
};
|
||||
const dataStr = JSON.stringify(payload);
|
||||
let mac = crypto
|
||||
.createHmac('sha256', key2)
|
||||
.update(dataStr)
|
||||
.digest('hex');
|
||||
|
||||
if (tamperMac) {
|
||||
mac = 'a'.repeat(mac.length);
|
||||
}
|
||||
|
||||
return { data: dataStr, mac };
|
||||
}
|
||||
|
||||
it('should verify a valid callback with timingSafeEqual', () => {
|
||||
const data = buildCallbackData();
|
||||
const result = service.verifyCallback(data);
|
||||
|
||||
expect(result.isValid).toBe(true);
|
||||
expect(result.isSuccess).toBe(true);
|
||||
expect(result.orderId).toBe('260408_order-123');
|
||||
expect(result.providerTxId).toBe('ZLP_TX_456');
|
||||
});
|
||||
|
||||
it('should reject an invalid MAC', () => {
|
||||
const data = buildCallbackData({}, true);
|
||||
const result = service.verifyCallback(data);
|
||||
|
||||
expect(result.isValid).toBe(false);
|
||||
expect(result.isSuccess).toBe(false);
|
||||
});
|
||||
|
||||
it('should reject an empty MAC', () => {
|
||||
const data = buildCallbackData();
|
||||
data['mac'] = '';
|
||||
|
||||
const result = service.verifyCallback(data);
|
||||
expect(result.isValid).toBe(false);
|
||||
});
|
||||
|
||||
it('should reject a MAC with wrong length', () => {
|
||||
const data = buildCallbackData();
|
||||
data['mac'] = 'deadbeef'; // too short
|
||||
|
||||
const result = service.verifyCallback(data);
|
||||
expect(result.isValid).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle invalid JSON in data field gracefully', () => {
|
||||
const mac = crypto
|
||||
.createHmac('sha256', key2)
|
||||
.update('not-json')
|
||||
.digest('hex');
|
||||
|
||||
const result = service.verifyCallback({ data: 'not-json', mac });
|
||||
// timingSafeEqual passes but JSON.parse fails
|
||||
expect(result.isValid).toBe(false);
|
||||
expect(result.isSuccess).toBe(false);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user