archetecture security fix
This commit is contained in:
@@ -0,0 +1,112 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const originalEnv = { ...process.env }
|
||||
|
||||
async function importService(env: Record<string, string | undefined> = {}) {
|
||||
vi.resetModules()
|
||||
process.env = { ...originalEnv, ...env }
|
||||
return import('./paypalService')
|
||||
}
|
||||
|
||||
describe('paypalService', () => {
|
||||
afterEach(() => {
|
||||
process.env = { ...originalEnv }
|
||||
vi.unstubAllGlobals()
|
||||
vi.resetModules()
|
||||
})
|
||||
|
||||
it('reports unconfigured when client credentials are placeholders or missing', async () => {
|
||||
const service = await importService({ PAYPAL_CLIENT_ID: 'your-paypal-client-id', PAYPAL_CLIENT_SECRET: 'secret' })
|
||||
expect(service.isConfigured()).toBe(false)
|
||||
})
|
||||
|
||||
it('creates an order after obtaining and caching an access token', async () => {
|
||||
const fetchMock = vi.fn()
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: vi.fn().mockResolvedValue({ access_token: 'access_1', expires_in: 3600 }),
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: vi.fn().mockResolvedValue({
|
||||
id: 'paypal_order_1',
|
||||
status: 'CREATED',
|
||||
links: [{ rel: 'approve', href: 'https://paypal.test/approve' }],
|
||||
}),
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: vi.fn().mockResolvedValue({ id: 'paypal_order_2', status: 'CREATED', links: [] }),
|
||||
})
|
||||
vi.stubGlobal('fetch', fetchMock)
|
||||
|
||||
const service = await importService({
|
||||
PAYPAL_BASE_URL: 'https://paypal.test',
|
||||
PAYPAL_CLIENT_ID: 'client_1',
|
||||
PAYPAL_CLIENT_SECRET: 'secret_1',
|
||||
})
|
||||
|
||||
await expect(service.createOrder({
|
||||
amount: 12345,
|
||||
currency: 'MAD',
|
||||
orderId: 'reservation_1',
|
||||
description: 'Reservation deposit',
|
||||
returnUrl: 'https://app.test/return',
|
||||
cancelUrl: 'https://app.test/cancel',
|
||||
})).resolves.toEqual({
|
||||
orderId: 'paypal_order_1',
|
||||
approveUrl: 'https://paypal.test/approve',
|
||||
status: 'CREATED',
|
||||
})
|
||||
|
||||
await service.createOrder({
|
||||
amount: 500,
|
||||
currency: 'MAD',
|
||||
orderId: 'reservation_2',
|
||||
description: 'Second payment',
|
||||
returnUrl: 'https://app.test/return',
|
||||
cancelUrl: 'https://app.test/cancel',
|
||||
})
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledTimes(3)
|
||||
expect(fetchMock).toHaveBeenNthCalledWith(1, 'https://paypal.test/v1/oauth2/token', expect.objectContaining({
|
||||
method: 'POST',
|
||||
body: 'grant_type=client_credentials',
|
||||
headers: expect.objectContaining({ Authorization: `Basic ${Buffer.from('client_1:secret_1').toString('base64')}` }),
|
||||
}))
|
||||
expect(fetchMock).toHaveBeenNthCalledWith(2, 'https://paypal.test/v2/checkout/orders', expect.objectContaining({
|
||||
method: 'POST',
|
||||
headers: expect.objectContaining({ Authorization: 'Bearer access_1' }),
|
||||
body: expect.stringContaining('"value":"123.45"'),
|
||||
}))
|
||||
})
|
||||
|
||||
it('captures and refunds through authenticated PayPal endpoints with cent-to-decimal formatting', async () => {
|
||||
const fetchMock = vi.fn()
|
||||
.mockResolvedValueOnce({ ok: true, json: vi.fn().mockResolvedValue({ access_token: 'access_1', expires_in: 3600 }) })
|
||||
.mockResolvedValueOnce({ ok: true, json: vi.fn().mockResolvedValue({ id: 'capture_result' }) })
|
||||
.mockResolvedValueOnce({ ok: true, json: vi.fn().mockResolvedValue({ id: 'refund_result' }) })
|
||||
vi.stubGlobal('fetch', fetchMock)
|
||||
|
||||
const service = await importService({ PAYPAL_BASE_URL: 'https://paypal.test', PAYPAL_CLIENT_ID: 'client_1', PAYPAL_CLIENT_SECRET: 'secret_1' })
|
||||
|
||||
await expect(service.captureOrder('order_1')).resolves.toEqual({ id: 'capture_result' })
|
||||
await expect(service.refundCapture('capture_1', 2500, 'MAD', 'Customer cancellation')).resolves.toEqual({ id: 'refund_result' })
|
||||
|
||||
expect(fetchMock).toHaveBeenNthCalledWith(2, 'https://paypal.test/v2/checkout/orders/order_1/capture', expect.objectContaining({
|
||||
method: 'POST',
|
||||
body: '{}',
|
||||
}))
|
||||
expect(fetchMock).toHaveBeenNthCalledWith(3, 'https://paypal.test/v2/payments/captures/capture_1/refund', expect.objectContaining({
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ amount: { currency_code: 'MAD', value: '25.00' }, note_to_payer: 'Customer cancellation' }),
|
||||
}))
|
||||
})
|
||||
|
||||
it('returns false instead of throwing when webhook verification cannot be completed', async () => {
|
||||
vi.stubGlobal('fetch', vi.fn().mockRejectedValue(new Error('network down')))
|
||||
const service = await importService({ PAYPAL_CLIENT_ID: 'client_1', PAYPAL_CLIENT_SECRET: 'secret_1' })
|
||||
|
||||
await expect(service.verifyWebhookEvent({}, '{"id":"event_1"}')).resolves.toBe(false)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user