From 1cb9af3ae407217511050ca836d3f0b437d39569 Mon Sep 17 00:00:00 2001 From: saravanakumardb1 Date: Mon, 16 Feb 2026 12:15:40 -0800 Subject: [PATCH] =?UTF-8?q?test(platform-service):=20add=20usage=20route-l?= =?UTF-8?q?evel=20tests=20=E2=80=94=20386=20tests,=2037.9%=20stmts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/modules/usage/routes.test.ts | 155 ++++++++++++++++++ 1 file changed, 155 insertions(+) create mode 100644 services/platform-service/src/modules/usage/routes.test.ts diff --git a/services/platform-service/src/modules/usage/routes.test.ts b/services/platform-service/src/modules/usage/routes.test.ts new file mode 100644 index 00000000..79ffe44f --- /dev/null +++ b/services/platform-service/src/modules/usage/routes.test.ts @@ -0,0 +1,155 @@ +/** + * Route-level tests for usage module — Fastify inject. + */ + +import Fastify from 'fastify'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +const repoMock = { + list: vi.fn(), + upsert: vi.fn(), + getMonthlyUsage: vi.fn(), + getByDate: vi.fn(), +}; + +vi.mock('./repository.js', () => repoMock); + +vi.mock('../../lib/request-context.js', () => ({ + getRequestProductId: () => 'lysnrai', +})); + +const baseUsage = { + id: 'usg_2026-02-16_user_1', + productId: 'lysnrai', + userId: 'user_1', + date: '2026-02-16', + dictations: 5, + words: 250, + durationMs: 30000, + tokensUsed: 1200, + costUsd: 0.01, + model: 'gpt-4o-mini', + source: 'desktop', + createdAt: '2026-02-16T00:00:00Z', +}; + +describe('usageRoutes', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('GET /usage returns usage records', async () => { + repoMock.list.mockResolvedValue([baseUsage]); + const { usageRoutes } = await import('./routes.js'); + const app = Fastify({ logger: false }); + await app.register(usageRoutes, { prefix: '/api' }); + + const res = await app.inject({ method: 'GET', url: '/api/usage' }); + expect(res.statusCode).toBe(200); + const data = JSON.parse(res.body); + expect(data.records).toHaveLength(1); + expect(data.count).toBe(1); + }); + + it('GET /usage/summary returns aggregated summary', async () => { + repoMock.list.mockResolvedValue([baseUsage]); + const { usageRoutes } = await import('./routes.js'); + const app = Fastify({ logger: false }); + await app.register(usageRoutes, { prefix: '/api' }); + + const res = await app.inject({ method: 'GET', url: '/api/usage/summary' }); + expect(res.statusCode).toBe(200); + const data = JSON.parse(res.body); + expect(data.totalWords).toBe(250); + expect(data.totalDictations).toBe(5); + expect(data.totalTokens).toBe(1200); + expect(data.modelBreakdown).toHaveLength(1); + expect(data.sourceBreakdown).toHaveLength(1); + expect(data.productBreakdown).toHaveLength(1); + }); + + it('POST /usage upserts a usage record', async () => { + repoMock.upsert.mockResolvedValue(baseUsage); + const { usageRoutes } = await import('./routes.js'); + const app = Fastify({ logger: false }); + await app.register(usageRoutes, { prefix: '/api' }); + + const res = await app.inject({ + method: 'POST', + url: '/api/usage', + payload: { + userId: 'user_1', + date: '2026-02-16', + dictations: 5, + words: 250, + tokensUsed: 1200, + }, + }); + expect(res.statusCode).toBe(200); + expect(repoMock.upsert).toHaveBeenCalled(); + }); + + it('POST /usage rejects invalid input', async () => { + const { usageRoutes } = await import('./routes.js'); + const app = Fastify({ logger: false }); + await app.register(usageRoutes, { prefix: '/api' }); + + const res = await app.inject({ + method: 'POST', + url: '/api/usage', + payload: { date: 'invalid' }, + }); + expect(res.statusCode).toBe(400); + }); + + it('POST /usage/check-limits returns allowed status', async () => { + repoMock.getMonthlyUsage.mockResolvedValue({ tokens: 500, words: 200, dictations: 5 }); + const { usageRoutes } = await import('./routes.js'); + const app = Fastify({ logger: false }); + await app.register(usageRoutes, { prefix: '/api' }); + + const res = await app.inject({ + method: 'POST', + url: '/api/usage/check-limits', + payload: { userId: 'user_1', plan: 'free' }, + }); + expect(res.statusCode).toBe(200); + const data = JSON.parse(res.body); + expect(data.allowed).toBe(true); + expect(data.exceeded).toEqual([]); + }); + + it('POST /usage/check-limits detects exceeded limits', async () => { + repoMock.getMonthlyUsage.mockResolvedValue({ tokens: 20000, words: 10000, dictations: 200 }); + const { usageRoutes } = await import('./routes.js'); + const app = Fastify({ logger: false }); + await app.register(usageRoutes, { prefix: '/api' }); + + const res = await app.inject({ + method: 'POST', + url: '/api/usage/check-limits', + payload: { userId: 'user_1', plan: 'free' }, + }); + expect(res.statusCode).toBe(200); + const data = JSON.parse(res.body); + expect(data.allowed).toBe(false); + expect(data.exceeded.length).toBeGreaterThan(0); + }); + + it('POST /usage/check-limits rejects invalid plan', async () => { + const { usageRoutes } = await import('./routes.js'); + const app = Fastify({ logger: false }); + await app.register(usageRoutes, { prefix: '/api' }); + + const res = await app.inject({ + method: 'POST', + url: '/api/usage/check-limits', + payload: { userId: 'user_1', plan: 'invalid' }, + }); + expect(res.statusCode).toBe(400); + }); +});