Install @nestjs/swagger@^11.4.2 + nestjs-zod@^5.3.0. Bootstrap SwaggerModule in main.ts with cleanupOpenApiDoc (dev/staging only; SWAGGER_ENABLED=true for prod). Add @ApiTags, @ApiBearerAuth, @ApiOperation, @ApiResponse, @ApiParam, @ApiQuery, @ApiBody decorators to all 16 acadenice /v1/* controllers. Add swagger.spec.ts (8 tests green). Add docs/api-docs.md (SDK gen guide). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
199 lines
7.3 KiB
TypeScript
199 lines
7.3 KiB
TypeScript
/**
|
|
* Swagger / OpenAPI document metadata tests (R5.3).
|
|
*
|
|
* These tests verify OpenAPI decorator metadata on the AcadeDoc controllers
|
|
* without booting a full NestJS application (avoids ESM module chain issues
|
|
* with prosemirror/collaboration utilities in the test environment).
|
|
*
|
|
* Strategy:
|
|
* - Import only the controller class (not its full DI tree)
|
|
* - Use `Reflect.getMetadata` to inspect @nestjs/swagger decorator metadata
|
|
* - Use `DocumentBuilder` standalone to verify tag/security configuration
|
|
*/
|
|
|
|
import 'reflect-metadata';
|
|
import { DocumentBuilder } from '@nestjs/swagger';
|
|
import { DECORATORS } from '@nestjs/swagger/dist/constants';
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Minimal stubs so controller imports do not pull the full DI graph
|
|
// ---------------------------------------------------------------------------
|
|
jest.mock(
|
|
'../../../../common/helpers/prosemirror/html/index',
|
|
() => ({}),
|
|
{ virtual: true },
|
|
);
|
|
jest.mock(
|
|
'../../../../collaboration/collaboration.util',
|
|
() => ({}),
|
|
{ virtual: true },
|
|
);
|
|
|
|
// Import only what we need to verify — just the controller classes for metadata
|
|
// We avoid importing services/entities to sidestep the prosemirror/collab chain.
|
|
// The Swagger decorator metadata is stored on the class constructor itself.
|
|
|
|
describe('Swagger OpenAPI configuration (R5.3)', () => {
|
|
// ---------------------------------------------------------------------------
|
|
// Test 1: DocumentBuilder produces a valid OpenAPI 3 spec skeleton
|
|
// ---------------------------------------------------------------------------
|
|
it('should build a valid OpenAPI 3 config object', () => {
|
|
const config = new DocumentBuilder()
|
|
.setTitle('AcadeDoc API')
|
|
.setDescription('API officielle AcadeDoc')
|
|
.setVersion('1.0')
|
|
.addBearerAuth()
|
|
.addCookieAuth('authToken')
|
|
.addTag('templates', 'Templates de pages')
|
|
.addTag('sync-blocks', 'Sync blocks (cross-page content)')
|
|
.addTag('audit-log', 'Audit log (read-only)')
|
|
.addTag('api-keys', 'Personal API tokens')
|
|
.addTag('clipper', 'Web clipper')
|
|
.addTag('graph', 'Knowledge graph')
|
|
.addTag('rbac', 'RBAC permissions/roles')
|
|
.addTag('comments', 'Page + row comments')
|
|
.addTag('notifications', 'Notifications + preferences')
|
|
.addTag('security', 'OIDC / SSO config')
|
|
.addTag('backlinks', 'Bidirectional backlinks')
|
|
.addTag('slash-commands', 'Custom slash commands')
|
|
.build();
|
|
|
|
expect(config.openapi).toBe('3.0.0');
|
|
expect(config.info.title).toBe('AcadeDoc API');
|
|
expect(config.info.version).toBe('1.0');
|
|
expect(config.tags).toBeDefined();
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Test 2: All expected tags are present
|
|
// ---------------------------------------------------------------------------
|
|
it('should declare all 12 expected resource tags', () => {
|
|
const config = new DocumentBuilder()
|
|
.addTag('templates')
|
|
.addTag('sync-blocks')
|
|
.addTag('audit-log')
|
|
.addTag('api-keys')
|
|
.addTag('clipper')
|
|
.addTag('graph')
|
|
.addTag('rbac')
|
|
.addTag('comments')
|
|
.addTag('notifications')
|
|
.addTag('security')
|
|
.addTag('backlinks')
|
|
.addTag('slash-commands')
|
|
.build();
|
|
|
|
const tagNames = (config.tags ?? []).map((t) => t.name);
|
|
const expected = [
|
|
'templates',
|
|
'sync-blocks',
|
|
'audit-log',
|
|
'api-keys',
|
|
'clipper',
|
|
'graph',
|
|
'rbac',
|
|
'comments',
|
|
'notifications',
|
|
'security',
|
|
'backlinks',
|
|
'slash-commands',
|
|
];
|
|
|
|
for (const tag of expected) {
|
|
expect(tagNames).toContain(tag);
|
|
}
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Test 3: BearerAuth security scheme is configured
|
|
// ---------------------------------------------------------------------------
|
|
it('should include BearerAuth and CookieAuth security schemes', () => {
|
|
const config = new DocumentBuilder()
|
|
.addBearerAuth()
|
|
.addCookieAuth('authToken')
|
|
.build();
|
|
|
|
const schemes = config.components?.securitySchemes ?? {};
|
|
const schemeValues = Object.values(schemes) as any[];
|
|
|
|
const hasBearerAuth = schemeValues.some(
|
|
(s) => s.type === 'http' && s.scheme === 'bearer',
|
|
);
|
|
const hasCookieAuth = schemeValues.some((s) => s.type === 'apiKey' && s.in === 'cookie');
|
|
|
|
expect(hasBearerAuth).toBe(true);
|
|
expect(hasCookieAuth).toBe(true);
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Test 4: @ApiTags metadata is resolvable on controller classes
|
|
// We load the file dynamically in this test only to isolate the import error.
|
|
// ---------------------------------------------------------------------------
|
|
it('should find @ApiTags on RowCommentsController', async () => {
|
|
// RowCommentsController has no service import issues
|
|
const { RowCommentsController } = await import(
|
|
'./comments/controllers/row-comments.controller'
|
|
);
|
|
const tags = Reflect.getMetadata(
|
|
DECORATORS.API_TAGS,
|
|
RowCommentsController,
|
|
);
|
|
expect(tags).toContain('comments');
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Test 5: @ApiTags metadata is resolvable on SyncBlocksController
|
|
// ---------------------------------------------------------------------------
|
|
it('should find @ApiTags on SyncBlocksController', async () => {
|
|
const { SyncBlocksController } = await import(
|
|
'./sync-blocks/controllers/sync-blocks.controller'
|
|
);
|
|
const tags = Reflect.getMetadata(
|
|
DECORATORS.API_TAGS,
|
|
SyncBlocksController,
|
|
);
|
|
expect(tags).toContain('sync-blocks');
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Test 6: @ApiTags metadata on AcadenicePermissionsController
|
|
// ---------------------------------------------------------------------------
|
|
it('should find @ApiTags on AcadenicePermissionsController', async () => {
|
|
const { AcadenicePermissionsController } = await import(
|
|
'./rbac/controllers/permissions.controller'
|
|
);
|
|
const tags = Reflect.getMetadata(
|
|
DECORATORS.API_TAGS,
|
|
AcadenicePermissionsController,
|
|
);
|
|
expect(tags).toContain('rbac');
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Test 7: @ApiTags on BacklinksController
|
|
// ---------------------------------------------------------------------------
|
|
it('should find @ApiTags on BacklinksController', async () => {
|
|
const { BacklinksController } = await import(
|
|
'./backlinks/controllers/backlinks.controller'
|
|
);
|
|
const tags = Reflect.getMetadata(
|
|
DECORATORS.API_TAGS,
|
|
BacklinksController,
|
|
);
|
|
expect(tags).toContain('backlinks');
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Test 8: @ApiTags on AcadeniceOidcStatusController
|
|
// ---------------------------------------------------------------------------
|
|
it('should find @ApiTags on AcadeniceOidcStatusController', async () => {
|
|
const { AcadeniceOidcStatusController } = await import(
|
|
'./security/controllers/oidc-status.controller'
|
|
);
|
|
const tags = Reflect.getMetadata(
|
|
DECORATORS.API_TAGS,
|
|
AcadeniceOidcStatusController,
|
|
);
|
|
expect(tags).toContain('security');
|
|
});
|
|
});
|