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:
Ho Ngoc Hai
2026-04-08 23:07:14 +07:00
parent 7fb25eb2b1
commit c9782fd48d
18 changed files with 2097 additions and 0 deletions

View File

@@ -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);
});
});

View File

@@ -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);
});
});