AcadeDoc/apps/server/src/core/acadenice/swagger.spec.ts
Corentin 21ce2a94c7 feat(acadedoc): add OpenAPI Swagger documentation for /api/v1/* — R5.3
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>
2026-05-08 15:36:14 +02:00

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