test(platform-service): add promos route-level tests
This commit is contained in:
parent
e4ee23aab4
commit
7179f1e7c4
158
services/platform-service/src/modules/promos/routes.test.ts
Normal file
158
services/platform-service/src/modules/promos/routes.test.ts
Normal file
@ -0,0 +1,158 @@
|
|||||||
|
/**
|
||||||
|
* Route-level tests for promos module — Fastify inject with mocked Stripe.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import Fastify from 'fastify';
|
||||||
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
|
const stripeMock = {
|
||||||
|
promotionCodes: {
|
||||||
|
list: vi.fn(),
|
||||||
|
create: vi.fn(),
|
||||||
|
update: vi.fn(),
|
||||||
|
},
|
||||||
|
coupons: {
|
||||||
|
create: vi.fn(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
vi.mock('stripe', () => ({
|
||||||
|
default: vi.fn().mockImplementation(() => stripeMock),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../../lib/request-context.js', () => ({
|
||||||
|
getRequestProductId: () => 'lysnrai',
|
||||||
|
}));
|
||||||
|
|
||||||
|
function buildPromo(overrides?: Partial<Record<string, unknown>>) {
|
||||||
|
return {
|
||||||
|
id: 'promo_1',
|
||||||
|
code: 'SAVE20',
|
||||||
|
active: true,
|
||||||
|
coupon: {
|
||||||
|
id: 'coupon_1',
|
||||||
|
percent_off: 20,
|
||||||
|
amount_off: null,
|
||||||
|
currency: 'usd',
|
||||||
|
duration: 'once',
|
||||||
|
},
|
||||||
|
times_redeemed: 0,
|
||||||
|
max_redemptions: null,
|
||||||
|
expires_at: null,
|
||||||
|
created: 1708000000,
|
||||||
|
metadata: { createdBy: 'admin', productId: 'lysnrai' },
|
||||||
|
...overrides,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function buildApp() {
|
||||||
|
const { promoRoutes } = await import('./routes.js');
|
||||||
|
const app = Fastify({ logger: false });
|
||||||
|
await app.register(promoRoutes, { prefix: '/api' });
|
||||||
|
return app;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('promoRoutes', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
process.env.STRIPE_SECRET_KEY = 'sk_test_123';
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('GET /promos lists promo codes', async () => {
|
||||||
|
stripeMock.promotionCodes.list.mockResolvedValue({ data: [buildPromo()] });
|
||||||
|
const app = await buildApp();
|
||||||
|
|
||||||
|
const res = await app.inject({ method: 'GET', url: '/api/promos?active=true' });
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(200);
|
||||||
|
const data = JSON.parse(res.body);
|
||||||
|
expect(data.promos).toHaveLength(1);
|
||||||
|
expect(data.promos[0].code).toBe('SAVE20');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('POST /promos returns 400 when discount fields are missing', async () => {
|
||||||
|
const app = await buildApp();
|
||||||
|
|
||||||
|
const res = await app.inject({
|
||||||
|
method: 'POST',
|
||||||
|
url: '/api/promos',
|
||||||
|
payload: { code: 'bad', createdBy: 'admin' },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(400);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('POST /promos creates coupon and promotion code', async () => {
|
||||||
|
stripeMock.coupons.create.mockResolvedValue({ id: 'coupon_1' });
|
||||||
|
stripeMock.promotionCodes.create.mockResolvedValue(buildPromo());
|
||||||
|
const app = await buildApp();
|
||||||
|
|
||||||
|
const res = await app.inject({
|
||||||
|
method: 'POST',
|
||||||
|
url: '/api/promos',
|
||||||
|
payload: {
|
||||||
|
code: 'save-20!',
|
||||||
|
percentOff: 20,
|
||||||
|
duration: 'once',
|
||||||
|
createdBy: 'admin',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(201);
|
||||||
|
const data = JSON.parse(res.body);
|
||||||
|
expect(data.code).toBe('SAVE20');
|
||||||
|
expect(stripeMock.promotionCodes.create).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('POST /promos/validate returns 400 without code', async () => {
|
||||||
|
const app = await buildApp();
|
||||||
|
|
||||||
|
const res = await app.inject({
|
||||||
|
method: 'POST',
|
||||||
|
url: '/api/promos/validate',
|
||||||
|
payload: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(400);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('POST /promos/validate returns invalid when no active promo', async () => {
|
||||||
|
stripeMock.promotionCodes.list.mockResolvedValue({ data: [] });
|
||||||
|
const app = await buildApp();
|
||||||
|
|
||||||
|
const res = await app.inject({
|
||||||
|
method: 'POST',
|
||||||
|
url: '/api/promos/validate',
|
||||||
|
payload: { code: 'NOPE' },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(200);
|
||||||
|
const data = JSON.parse(res.body);
|
||||||
|
expect(data.valid).toBe(false);
|
||||||
|
expect(data.promo).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('PUT /promos/:id/deactivate deactivates promo', async () => {
|
||||||
|
stripeMock.promotionCodes.update.mockResolvedValue(buildPromo({ active: false }));
|
||||||
|
const app = await buildApp();
|
||||||
|
|
||||||
|
const res = await app.inject({ method: 'PUT', url: '/api/promos/promo_1/deactivate' });
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(200);
|
||||||
|
const data = JSON.parse(res.body);
|
||||||
|
expect(data.active).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('PUT /promos/:id/deactivate returns 400 on Stripe error', async () => {
|
||||||
|
stripeMock.promotionCodes.update.mockRejectedValue(new Error('stripe failure'));
|
||||||
|
const app = await buildApp();
|
||||||
|
|
||||||
|
const res = await app.inject({ method: 'PUT', url: '/api/promos/promo_bad/deactivate' });
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(400);
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue
Block a user