import 'reflect-metadata'; import { Body, Controller, Get, HttpCode, Post, Query } from '@nestjs/common'; import { Test } from '@nestjs/testing'; import request from 'supertest'; import { z } from 'zod'; import { configureApp } from '../configure-app'; import { parseEnvironment } from '../config/environment'; import { SchemaPipe } from '../common/validation.pipe'; import { Public } from '../identity/access.decorator'; import { configureSwagger } from './configure-swagger'; @Controller('sample') class SampleController { @Public() @Post() create( @Body( new SchemaPipe( z.strictObject({ email: z.email(), date: z.iso.datetime().transform((value) => new Date(value)), }), ), ) input: unknown, ) { return input; } @Get() list( @Query( new SchemaPipe( z.object({ limit: z.coerce.number().int().min(1).max(100).default(20), }), ), ) input: unknown, ) { return input; } @Post('logout') @HttpCode(204) logout() {} } describe('Swagger documentation', () => { async function fixture(nodeEnv: string, enabled?: string) { const module = await Test.createTestingModule({ controllers: [SampleController], }).compile(); const app = module.createNestApplication(); app.useLogger(false); const env = parseEnvironment({ DATABASE_URL: 'postgresql://local/test', NODE_ENV: nodeEnv, SWAGGER_ENABLED: enabled, }); configureApp(app, env); configureSwagger(app, env); await app.init(); return { app, api: request(app.getHttpServer()) }; } it('serves UI, local assets and accurate input/auth/error documentation', async () => { const { app, api } = await fixture('development'); try { const ui = await api.get('/api/docs/').expect(200); expect(ui.text).toContain('swagger-ui'); expect(ui.headers['content-security-policy']).not.toContain( 'upgrade-insecure-requests', ); await api.get('/api/docs/swagger-ui-bundle.js').expect(200); const init = await api.get('/api/docs/swagger-ui-init.js').expect(200); expect(init.text).toContain('"persistAuthorization": false'); const { body: doc } = await api.get('/api/docs-json').expect(200); const sample = doc.paths['/api/v1/sample']; expect(sample.post.security).toEqual([]); expect(sample.get.security).toEqual([{ bearer: [] }]); expect( sample.post.requestBody.content['application/json'].schema, ).toMatchObject({ required: ['email', 'date'], properties: { email: { format: 'email' }, date: { type: 'string' } }, }); expect(sample.get.parameters).toContainEqual( expect.objectContaining({ name: 'limit', in: 'query', schema: expect.objectContaining({ maximum: 100, default: 20 }), }), ); expect( doc.paths['/api/v1/sample/logout'].post.responses['204'], ).toBeDefined(); expect(doc.components.schemas.ApiError.properties.code.enum).toContain( 'REQUEST_INVALID', ); expect( (await api.get('/api/v1/sample')).headers['content-security-policy'], ).toContain('upgrade-insecure-requests'); } finally { await app.close(); } }); it.each([ ['production', undefined], ['test', undefined], ['development', 'false'], ])('hides docs in %s when enabled=%s', async (mode, enabled) => { const { app, api } = await fixture(mode!, enabled); try { await api.get('/api/docs-json').expect(404); await api.get('/api/docs').expect(404); } finally { await app.close(); } }); it('allows an explicit opt-in and rejects invalid settings', async () => { const { app, api } = await fixture('production', 'true'); try { await api.get('/api/docs-json').expect(200); } finally { await app.close(); } expect(() => parseEnvironment({ DATABASE_URL: 'postgresql://local/test', SWAGGER_ENABLED: 'yes', }), ).toThrow('SWAGGER_ENABLED'); }); });