manicanldes-backend/src/documentation/configure-swagger.spec.ts

133 lines
4.0 KiB
TypeScript

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');
});
});