diff --git a/services/platform-service/src/modules/subscriptions/routes.test.ts b/services/platform-service/src/modules/subscriptions/routes.test.ts new file mode 100644 index 00000000..8228f7eb --- /dev/null +++ b/services/platform-service/src/modules/subscriptions/routes.test.ts @@ -0,0 +1,190 @@ +/** + * Route-level tests for subscriptions module — Fastify inject. + */ + +import Fastify from 'fastify'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +const repoMock = { + getByUserId: vi.fn(), + getByStripeCustomerId: vi.fn(), + getByStripeCustomerIdAnyProduct: vi.fn(), + createSubscription: vi.fn(), + updateSubscription: vi.fn(), + getPaymentsByUser: vi.fn(), + createPayment: vi.fn(), +}; + +vi.mock('./repository.js', () => repoMock); + +vi.mock('../../lib/request-context.js', () => ({ + getRequestProductId: () => 'lysnrai', +})); + +const baseSub = { + id: 'sub_user_1_123', + productId: 'lysnrai', + userId: 'user_1', + plan: 'pro', + status: 'active', + currentPeriodStart: '2026-02-01T00:00:00Z', + currentPeriodEnd: '2026-03-01T00:00:00Z', + cancelAtPeriodEnd: false, + monthlyPrice: 9.99, + tokensIncluded: 100000, + tokensUsed: 5000, + createdAt: '2026-02-01T00:00:00Z', + updatedAt: '2026-02-01T00:00:00Z', +}; + +describe('subscriptionRoutes', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('GET /subscriptions/:userId returns subscription', async () => { + repoMock.getByUserId.mockResolvedValue(baseSub); + const { subscriptionRoutes } = await import('./routes.js'); + const app = Fastify({ logger: false }); + await app.register(subscriptionRoutes, { prefix: '/api' }); + + const res = await app.inject({ method: 'GET', url: '/api/subscriptions/user_1' }); + expect(res.statusCode).toBe(200); + const data = JSON.parse(res.body); + expect(data.plan).toBe('pro'); + }); + + it('GET /subscriptions/:userId returns 404 when not found', async () => { + repoMock.getByUserId.mockResolvedValue(null); + const { subscriptionRoutes } = await import('./routes.js'); + const app = Fastify({ logger: false }); + await app.register(subscriptionRoutes, { prefix: '/api' }); + + const res = await app.inject({ method: 'GET', url: '/api/subscriptions/user_1' }); + expect(res.statusCode).toBe(404); + }); + + it('POST /subscriptions creates a subscription', async () => { + repoMock.createSubscription.mockResolvedValue(baseSub); + const { subscriptionRoutes } = await import('./routes.js'); + const app = Fastify({ logger: false }); + await app.register(subscriptionRoutes, { prefix: '/api' }); + + const res = await app.inject({ + method: 'POST', + url: '/api/subscriptions', + payload: { + userId: 'user_1', + plan: 'pro', + status: 'active', + monthlyPrice: 9.99, + tokensIncluded: 100000, + }, + }); + expect(res.statusCode).toBe(201); + }); + + it('POST /subscriptions with trial days', async () => { + repoMock.createSubscription.mockResolvedValue(baseSub); + const { subscriptionRoutes } = await import('./routes.js'); + const app = Fastify({ logger: false }); + await app.register(subscriptionRoutes, { prefix: '/api' }); + + const res = await app.inject({ + method: 'POST', + url: '/api/subscriptions', + payload: { + userId: 'user_1', + plan: 'pro', + status: 'trialing', + monthlyPrice: 9.99, + tokensIncluded: 100000, + trialDays: 14, + }, + }); + expect(res.statusCode).toBe(201); + }); + + it('POST /subscriptions rejects invalid input', async () => { + const { subscriptionRoutes } = await import('./routes.js'); + const app = Fastify({ logger: false }); + await app.register(subscriptionRoutes, { prefix: '/api' }); + + const res = await app.inject({ + method: 'POST', + url: '/api/subscriptions', + payload: { plan: 'invalid' }, + }); + expect(res.statusCode).toBe(400); + }); + + it('PUT /subscriptions/:userId updates subscription', async () => { + repoMock.getByUserId.mockResolvedValue(baseSub); + repoMock.updateSubscription.mockResolvedValue({ ...baseSub, plan: 'enterprise' }); + const { subscriptionRoutes } = await import('./routes.js'); + const app = Fastify({ logger: false }); + await app.register(subscriptionRoutes, { prefix: '/api' }); + + const res = await app.inject({ + method: 'PUT', + url: '/api/subscriptions/user_1', + payload: { plan: 'enterprise' }, + }); + expect(res.statusCode).toBe(200); + const data = JSON.parse(res.body); + expect(data.plan).toBe('enterprise'); + }); + + it('PUT /subscriptions/:userId returns 404 when not found', async () => { + repoMock.getByUserId.mockResolvedValue(null); + const { subscriptionRoutes } = await import('./routes.js'); + const app = Fastify({ logger: false }); + await app.register(subscriptionRoutes, { prefix: '/api' }); + + const res = await app.inject({ + method: 'PUT', + url: '/api/subscriptions/user_1', + payload: { plan: 'enterprise' }, + }); + expect(res.statusCode).toBe(404); + }); + + it('GET /payments/:userId lists payments', async () => { + const payment = { id: 'pay_1', amount: 999, currency: 'usd', status: 'succeeded' }; + repoMock.getPaymentsByUser.mockResolvedValue([payment]); + const { subscriptionRoutes } = await import('./routes.js'); + const app = Fastify({ logger: false }); + await app.register(subscriptionRoutes, { prefix: '/api' }); + + const res = await app.inject({ method: 'GET', url: '/api/payments/user_1' }); + expect(res.statusCode).toBe(200); + const data = JSON.parse(res.body); + expect(data.payments).toHaveLength(1); + }); + + it('POST /payments records a payment', async () => { + const payment = { id: 'pay_1', amount: 999, currency: 'usd', status: 'succeeded' }; + repoMock.createPayment.mockResolvedValue(payment); + const { subscriptionRoutes } = await import('./routes.js'); + const app = Fastify({ logger: false }); + await app.register(subscriptionRoutes, { prefix: '/api' }); + + const res = await app.inject({ + method: 'POST', + url: '/api/payments', + payload: { + userId: 'user_1', + amount: 999, + currency: 'usd', + status: 'succeeded', + description: 'Pro plan', + method: 'card', + }, + }); + expect(res.statusCode).toBe(201); + }); +});