mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
@@ -0,0 +1,201 @@
|
||||
'use strict';
|
||||
|
||||
// ── Integration test: paypal.provider.js against a mocked HTTP boundary ───────
|
||||
// Only axios (the true external boundary) is mocked — everything else in the
|
||||
// provider (token exchange, URL selection, request shaping) runs for real.
|
||||
// This is what tells us the provider is ready to go live: wrong env vars, a
|
||||
// broken sandbox/live switch, or a malformed request body will fail here
|
||||
// exactly like it would against the real PayPal API.
|
||||
|
||||
jest.mock('axios');
|
||||
|
||||
const BASE_ENV = {
|
||||
PAYPAL_CLIENT_ID: 'test-client-id',
|
||||
PAYPAL_CLIENT_SECRET: 'test-client-secret',
|
||||
PAYPAL_BRAND_NAME: 'STARR',
|
||||
FRONTEND_URL: 'https://app.new-starr.test',
|
||||
};
|
||||
|
||||
// BASE_URL is computed once at module load time from PAYPAL_ENV, so every
|
||||
// test that cares about sandbox/live must reset the module registry first.
|
||||
// axios must be re-required from the same fresh registry, otherwise its mock
|
||||
// calls land on a different automock instance than the one the provider uses.
|
||||
function loadProvider(envOverrides = {}) {
|
||||
jest.resetModules();
|
||||
Object.assign(process.env, BASE_ENV, envOverrides);
|
||||
const axios = require('axios');
|
||||
const provider = require('../../providers/paypal.provider');
|
||||
return { provider, axios };
|
||||
}
|
||||
|
||||
function mockAccessToken(axios, token = 'test-access-token') {
|
||||
axios.post.mockImplementationOnce((url) => {
|
||||
expect(url).toMatch(/\/v1\/oauth2\/token$/);
|
||||
return Promise.resolve({ data: { access_token: token } });
|
||||
});
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
delete process.env.PAYPAL_ENV;
|
||||
});
|
||||
|
||||
// ── Config: sandbox vs live host selection ────────────────────────────────────
|
||||
|
||||
describe('environment / config wiring', () => {
|
||||
|
||||
test('defaults to the sandbox host when PAYPAL_ENV is unset', async () => {
|
||||
const { provider, axios } = loadProvider({});
|
||||
mockAccessToken(axios);
|
||||
axios.post.mockResolvedValueOnce({ data: {} });
|
||||
|
||||
await provider.createOrder({ amount: 10, referenceId: 'ref-1' });
|
||||
|
||||
expect(axios.post.mock.calls[0][0]).toBe('https://api-m.sandbox.paypal.com/v1/oauth2/token');
|
||||
expect(axios.post.mock.calls[1][0]).toBe('https://api-m.sandbox.paypal.com/v2/checkout/orders');
|
||||
});
|
||||
|
||||
test('switches to the live host when PAYPAL_ENV=live', async () => {
|
||||
const { provider, axios } = loadProvider({ PAYPAL_ENV: 'live' });
|
||||
mockAccessToken(axios);
|
||||
axios.post.mockResolvedValueOnce({ data: {} });
|
||||
|
||||
await provider.createOrder({ amount: 10, referenceId: 'ref-1' });
|
||||
|
||||
expect(axios.post.mock.calls[0][0]).toBe('https://api-m.paypal.com/v1/oauth2/token');
|
||||
expect(axios.post.mock.calls[1][0]).toBe('https://api-m.paypal.com/v2/checkout/orders');
|
||||
});
|
||||
|
||||
test('token request authenticates with PAYPAL_CLIENT_ID / PAYPAL_CLIENT_SECRET via HTTP Basic auth', async () => {
|
||||
const { provider, axios } = loadProvider({});
|
||||
mockAccessToken(axios);
|
||||
axios.post.mockResolvedValueOnce({ data: {} });
|
||||
|
||||
await provider.createOrder({ amount: 10, referenceId: 'ref-1' });
|
||||
|
||||
const [, , config] = axios.post.mock.calls[0];
|
||||
expect(config.auth).toEqual({ username: 'test-client-id', password: 'test-client-secret' });
|
||||
expect(config.headers['Content-Type']).toBe('application/x-www-form-urlencoded');
|
||||
});
|
||||
|
||||
test('a rejected token exchange (bad credentials) surfaces as a rejected promise, not a silent failure', async () => {
|
||||
const { provider, axios } = loadProvider({});
|
||||
axios.post.mockRejectedValueOnce(new Error('401 invalid_client'));
|
||||
|
||||
await expect(provider.createOrder({ amount: 10, referenceId: 'ref-1' }))
|
||||
.rejects.toThrow('401 invalid_client');
|
||||
});
|
||||
});
|
||||
|
||||
// ── createOrder ────────────────────────────────────────────────────────────────
|
||||
|
||||
describe('createOrder()', () => {
|
||||
|
||||
test('sends a CAPTURE intent order with the reference id, amount and currency', async () => {
|
||||
const { provider, axios } = loadProvider({});
|
||||
mockAccessToken(axios, 'tok-abc');
|
||||
axios.post.mockResolvedValueOnce({ data: { id: 'ORDER-1', status: 'CREATED' } });
|
||||
|
||||
const result = await provider.createOrder({
|
||||
amount: 19.99,
|
||||
currency: 'PHP',
|
||||
referenceId: 'plan-42',
|
||||
});
|
||||
|
||||
const [url, body, config] = axios.post.mock.calls[1];
|
||||
expect(url).toBe('https://api-m.sandbox.paypal.com/v2/checkout/orders');
|
||||
expect(body.intent).toBe('CAPTURE');
|
||||
expect(body.purchase_units[0]).toMatchObject({
|
||||
reference_id: 'plan-42',
|
||||
amount: { currency_code: 'PHP', value: '19.99' },
|
||||
});
|
||||
expect(config.headers.Authorization).toBe('Bearer tok-abc');
|
||||
expect(result).toEqual({ id: 'ORDER-1', status: 'CREATED' });
|
||||
});
|
||||
|
||||
test('defaults currency to USD when not provided', async () => {
|
||||
const { provider, axios } = loadProvider({});
|
||||
mockAccessToken(axios);
|
||||
axios.post.mockResolvedValueOnce({ data: {} });
|
||||
|
||||
await provider.createOrder({ amount: 5, referenceId: 'ref-1' });
|
||||
|
||||
const [, body] = axios.post.mock.calls[1];
|
||||
expect(body.purchase_units[0].amount.currency_code).toBe('USD');
|
||||
});
|
||||
|
||||
test('falls back to FRONTEND_URL for return/cancel urls when not provided', async () => {
|
||||
const { provider, axios } = loadProvider({});
|
||||
mockAccessToken(axios);
|
||||
axios.post.mockResolvedValueOnce({ data: {} });
|
||||
|
||||
await provider.createOrder({ amount: 5, referenceId: 'ref-1' });
|
||||
|
||||
const [, body] = axios.post.mock.calls[1];
|
||||
expect(body.application_context.return_url).toBe('https://app.new-starr.test/plans/checkout');
|
||||
expect(body.application_context.cancel_url).toBe('https://app.new-starr.test/plans/checkout?cancelled=true');
|
||||
});
|
||||
|
||||
test('honors explicit return/cancel urls when provided', async () => {
|
||||
const { provider, axios } = loadProvider({});
|
||||
mockAccessToken(axios);
|
||||
axios.post.mockResolvedValueOnce({ data: {} });
|
||||
|
||||
await provider.createOrder({
|
||||
amount: 5, referenceId: 'ref-1',
|
||||
returnUrl: 'https://custom.test/ok',
|
||||
cancelUrl: 'https://custom.test/cancel',
|
||||
});
|
||||
|
||||
const [, body] = axios.post.mock.calls[1];
|
||||
expect(body.application_context.return_url).toBe('https://custom.test/ok');
|
||||
expect(body.application_context.cancel_url).toBe('https://custom.test/cancel');
|
||||
});
|
||||
});
|
||||
|
||||
// ── captureOrder ───────────────────────────────────────────────────────────────
|
||||
|
||||
describe('captureOrder()', () => {
|
||||
|
||||
test('posts to the order capture endpoint with a bearer token and returns the capture payload', async () => {
|
||||
const { provider, axios } = loadProvider({});
|
||||
mockAccessToken(axios, 'tok-xyz');
|
||||
axios.post.mockResolvedValueOnce({ data: { id: 'ORDER-1', status: 'COMPLETED' } });
|
||||
|
||||
const result = await provider.captureOrder('ORDER-1');
|
||||
|
||||
const [url, body, config] = axios.post.mock.calls[1];
|
||||
expect(url).toBe('https://api-m.sandbox.paypal.com/v2/checkout/orders/ORDER-1/capture');
|
||||
expect(body).toEqual({});
|
||||
expect(config.headers.Authorization).toBe('Bearer tok-xyz');
|
||||
expect(result).toEqual({ id: 'ORDER-1', status: 'COMPLETED' });
|
||||
});
|
||||
});
|
||||
|
||||
// ── refundCapture ──────────────────────────────────────────────────────────────
|
||||
|
||||
describe('refundCapture()', () => {
|
||||
|
||||
test('posts a refund with the given amount and currency', async () => {
|
||||
const { provider, axios } = loadProvider({});
|
||||
mockAccessToken(axios);
|
||||
axios.post.mockResolvedValueOnce({ data: { id: 'REFUND-1', status: 'COMPLETED' } });
|
||||
|
||||
const result = await provider.refundCapture('CAPTURE-1', 9.5, 'PHP');
|
||||
|
||||
const [url, body] = axios.post.mock.calls[1];
|
||||
expect(url).toBe('https://api-m.sandbox.paypal.com/v2/payments/captures/CAPTURE-1/refund');
|
||||
expect(body).toEqual({ amount: { value: '9.5', currency_code: 'PHP' } });
|
||||
expect(result).toEqual({ id: 'REFUND-1', status: 'COMPLETED' });
|
||||
});
|
||||
|
||||
test('defaults currency to USD when not provided', async () => {
|
||||
const { provider, axios } = loadProvider({});
|
||||
mockAccessToken(axios);
|
||||
axios.post.mockResolvedValueOnce({ data: {} });
|
||||
|
||||
await provider.refundCapture('CAPTURE-1', 9.5);
|
||||
|
||||
const [, body] = axios.post.mock.calls[1];
|
||||
expect(body.amount.currency_code).toBe('USD');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,51 @@
|
||||
'use strict';
|
||||
|
||||
// ── Provider contract test ────────────────────────────────────────────────────
|
||||
// This suite is the "gate" for onboarding new payment providers. It does not
|
||||
// hardcode "paypal" as the only case — it walks registry.list() and asserts
|
||||
// every registered provider satisfies the shape payment.service.js relies on.
|
||||
// Drop a second provider into providers/registry.js and this file validates it
|
||||
// for free, with zero new test code required.
|
||||
|
||||
const registry = require('../../providers/registry');
|
||||
|
||||
const REQUIRED_METHODS = ['createOrder', 'captureOrder', 'refundCapture'];
|
||||
|
||||
describe('payment provider registry', () => {
|
||||
|
||||
test('lists at least one provider', () => {
|
||||
expect(registry.list().length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('currently registers paypal', () => {
|
||||
expect(registry.list()).toContain('paypal');
|
||||
});
|
||||
|
||||
test('get() returns the provider module for a known name', () => {
|
||||
const provider = registry.get('paypal');
|
||||
expect(provider).toBeDefined();
|
||||
});
|
||||
|
||||
test('get() throws a descriptive error for an unknown provider', () => {
|
||||
expect(() => registry.get('stripe')).toThrow(/Unknown payment provider: "stripe"/);
|
||||
});
|
||||
|
||||
test('unknown-provider error lists the available providers so misconfiguration is easy to diagnose', () => {
|
||||
try {
|
||||
registry.get('does-not-exist');
|
||||
throw new Error('expected registry.get to throw');
|
||||
} catch (err) {
|
||||
registry.list().forEach((name) => {
|
||||
expect(err.message).toContain(name);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe.each(registry.list())('provider contract: %s', (name) => {
|
||||
const provider = registry.get(name);
|
||||
|
||||
test.each(REQUIRED_METHODS)('exposes %s as a function', (method) => {
|
||||
expect(typeof provider[method]).toBe('function');
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user