Skip to content

Commit f289b57

Browse files
fix: 🐛 fix error handling, type safety, security vulnerabilities, and add test coverage (#44)
Co-authored-by: lehuygiang28 <58503884+lehuygiang28@users.noreply.github.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
1 parent 3e31986 commit f289b57

File tree

5 files changed

+114
-22
lines changed

5 files changed

+114
-22
lines changed

package-lock.json

Lines changed: 20 additions & 18 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/services/verification.service.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ export class VerificationService {
8383

8484
let outputResults = {
8585
isVerified,
86-
isSuccess: cloneQuery.vnp_ResponseCode === '00',
86+
isSuccess: cloneQuery.vnp_ResponseCode === '00' || cloneQuery.vnp_ResponseCode === 0,
8787
message: getResponseByStatusCode(
8888
cloneQuery.vnp_ResponseCode?.toString() ?? '',
8989
this.config.vnp_Locale,

src/vnpay.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,12 +185,21 @@ export class VNPay {
185185
},
186186
);
187187

188+
if (!response.ok) {
189+
throw new Error(`Failed to fetch bank list: HTTP ${response.status}`);
190+
}
191+
188192
const bankList = (await response.json()) as Bank[];
189193

190194
for (const bank of bankList) {
195+
// Handle logo_link safely - only slice if it starts with a slash
196+
const logoPath =
197+
bank.logo_link && bank.logo_link.startsWith('/')
198+
? bank.logo_link.slice(1)
199+
: bank.logo_link;
191200
bank.logo_link = resolveUrlString(
192201
this.globalConfig.vnpayHost ?? VNPAY_GATEWAY_SANDBOX_HOST,
193-
bank.logo_link.slice(1),
202+
logoPath,
194203
);
195204
}
196205

test/integration/vnpay/get-bank-list.test.ts

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
1-
import { createTestVNPayInstance, DEFAULT_BANK_LIST, DEFAULT_VNPAY_CONFIG } from '../../__helpers__';
2-
import { mockFetchSuccess } from '../../__helpers__/mock-helpers';
1+
import {
2+
createTestVNPayInstance,
3+
DEFAULT_BANK_LIST,
4+
DEFAULT_VNPAY_CONFIG,
5+
} from '../../__helpers__';
6+
import { mockFetchSuccess, mockFetchError } from '../../__helpers__/mock-helpers';
37

48
global.fetch = jest.fn();
59

@@ -52,4 +56,52 @@ describe('VNPay.getBankList', () => {
5256
},
5357
);
5458
});
59+
60+
it('should throw error when fetch fails with non-ok response', async () => {
61+
// Arrange
62+
mockFetchError(500, 'Internal Server Error');
63+
64+
// Act & Assert
65+
await expect(vnpay.getBankList()).rejects.toThrow('Failed to fetch bank list: HTTP 500');
66+
});
67+
68+
it('should handle logo_link without leading slash', async () => {
69+
// Arrange
70+
const mockBankList = [
71+
{
72+
bank_code: 'TCB',
73+
bank_name: 'Techcombank',
74+
logo_link: 'images/tcb.png', // No leading slash
75+
bank_type: 1,
76+
display_order: 1,
77+
},
78+
];
79+
mockFetchSuccess(mockBankList);
80+
81+
// Act
82+
const bankList = await vnpay.getBankList();
83+
84+
// Assert
85+
expect(bankList[0].logo_link).toBe(`${DEFAULT_VNPAY_CONFIG.vnpayHost}/images/tcb.png`);
86+
});
87+
88+
it('should handle empty logo_link', async () => {
89+
// Arrange
90+
const mockBankList = [
91+
{
92+
bank_code: 'ACB',
93+
bank_name: 'ACB Bank',
94+
logo_link: '', // Empty string
95+
bank_type: 1,
96+
display_order: 1,
97+
},
98+
];
99+
mockFetchSuccess(mockBankList);
100+
101+
// Act
102+
const bankList = await vnpay.getBankList();
103+
104+
// Assert
105+
expect(bankList[0].logo_link).toBe(`${DEFAULT_VNPAY_CONFIG.vnpayHost}/`);
106+
});
55107
});

test/integration/vnpay/verify-return-url.test.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,4 +266,33 @@ describe('verifyReturnUrl', () => {
266266
expect(result.isSuccess).toBe(true);
267267
expect(result.message).toBe(getResponseByStatusCode('00', VnpLocale.VN));
268268
});
269+
270+
it('should handle vnp_ResponseCode as number 0 and mark as success', () => {
271+
// Arrange: Create input with vnp_ResponseCode as number 0 instead of string '00'
272+
const base = createReturnQueryInput();
273+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
274+
const { vnp_SecureHash: _oldHash, ...clone } = base as any;
275+
clone.vnp_ResponseCode = 0; // Number 0 instead of string '00'
276+
277+
const search = buildPaymentUrlSearchParams(clone);
278+
const newHash = calculateSecureHash({
279+
secureSecret: 'test_secret',
280+
data: search.toString(),
281+
hashAlgorithm: HashAlgorithm.SHA512,
282+
bufferEncode: 'utf-8',
283+
});
284+
285+
const input = {
286+
...clone,
287+
vnp_SecureHash: newHash,
288+
} as any;
289+
290+
// Act
291+
const result = vnpay.verifyReturnUrl(input);
292+
293+
// Assert
294+
expect(result.isVerified).toBe(true);
295+
expect(result.isSuccess).toBe(true);
296+
expect(result.message).toBe(getResponseByStatusCode('0', VnpLocale.VN));
297+
});
269298
});

0 commit comments

Comments
 (0)