diff --git a/src/offerings/distributionsRoute.test.ts b/src/offerings/distributionsRoute.test.ts index e1fbd046..03d8e741 100644 --- a/src/offerings/distributionsRoute.test.ts +++ b/src/offerings/distributionsRoute.test.ts @@ -32,7 +32,7 @@ const createNext = (): NextFunction => jest.fn(); const createRequest = (overrides: { params?: Record; - query?: Record; + query?: Record; userId?: string; }): Request => { return { @@ -269,6 +269,28 @@ describe('createListDistributionsByOfferingHandler', () => { expect(mockDistributionRepository.listByOffering).not.toHaveBeenCalled(); }); + it.each([ + { description: 'page is repeated', query: { page: ['1', '2'] } }, + { description: 'pageSize is repeated', query: { pageSize: ['10', '20'] } }, + { description: 'page is zero', query: { page: '0' } }, + { description: 'pageSize is fractional', query: { pageSize: '1.5' } }, + ])('returns 400 when $description', async ({ query }) => { + const handler = createListDistributionsByOfferingHandler({ + distributionRepository: mockDistributionRepository, + offeringOwnershipRepository: mockOfferingOwnershipRepository, + }); + + const req = createRequest({ userId: 'user-1', query }); + const res = createResponse(); + + await handler(req, res, createNext()); + + expect(res.status).toHaveBeenCalledWith(400); + expect(res.json).toHaveBeenCalledWith({ error: 'Invalid pagination parameters' }); + expect(mockOfferingOwnershipRepository.isOwnedByUser).not.toHaveBeenCalled(); + expect(mockDistributionRepository.listByOffering).not.toHaveBeenCalled(); + }); + // 10. pageSize is capped at MAX_PAGE_SIZE (100) it('caps pageSize at 100', async () => { mockOfferingOwnershipRepository.isOwnedByUser.mockResolvedValueOnce(true); diff --git a/src/routes/apiDocs.test.ts b/src/routes/apiDocs.test.ts new file mode 100644 index 00000000..c5183101 --- /dev/null +++ b/src/routes/apiDocs.test.ts @@ -0,0 +1,56 @@ +import express, { Express, NextFunction, Request, Response } from 'express'; +import path from 'path'; +import request from 'supertest'; +import { createApiDocsRouter } from './apiDocs'; + +describe('createApiDocsRouter', () => { + let app: Express; + + beforeEach(() => { + app = express(); + app.use(createApiDocsRouter()); + }); + + it('serves the OpenAPI specification', async () => { + const response = await request(app).get('/openapi.yaml'); + + expect(response.status).toBe(200); + expect(response.text).toContain('openapi: 3.0.0'); + expect(response.text).toContain('title: Revora Backend API'); + }); + + it('serves Swagger UI configured to load the OpenAPI specification', async () => { + const response = await request(app).get('/api-docs/'); + + expect(response.status).toBe(200); + expect(response.headers['content-type']).toMatch(/text\/html/); + expect(response.text).toContain('Swagger UI'); + expect(response.text).toContain('/openapi.yaml'); + }); + + it('returns not found for unsupported paths and methods', async () => { + const unknownPath = await request(app).get('/not-api-docs'); + const unsupportedMethod = await request(app).post('/openapi.yaml'); + + expect(unknownPath.status).toBe(404); + expect(unsupportedMethod.status).toBe(404); + }); + + it('passes OpenAPI file errors to Express error middleware', async () => { + const resolveSpy = jest.spyOn(path, 'resolve').mockReturnValue('/missing/openapi.yaml'); + try { + app = express(); + app.use(createApiDocsRouter()); + } finally { + resolveSpy.mockRestore(); + } + app.use((error: NodeJS.ErrnoException, _req: Request, res: Response, _next: NextFunction) => { + res.status(500).json({ code: error.code }); + }); + + const response = await request(app).get('/openapi.yaml'); + + expect(response.status).toBe(500); + expect(response.body).toEqual({ code: 'ENOENT' }); + }); +}); \ No newline at end of file