113 lines
4.5 KiB
TypeScript
113 lines
4.5 KiB
TypeScript
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)
|
|
})
|
|
})
|