nestcraftx 0.4.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/ci.yml +35 -0
- package/CHANGELOG.fr.md +74 -0
- package/CHANGELOG.md +74 -0
- package/PROGRESS.md +59 -57
- package/README.fr.md +60 -69
- package/bin/nestcraft.js +8 -0
- package/commands/demo.js +10 -0
- package/commands/generate.js +551 -2
- package/commands/generateConf.js +4 -4
- package/commands/help.js +11 -0
- package/commands/info.js +2 -3
- package/commands/list.js +93 -0
- package/commands/new.js +26 -9
- package/jest.config.js +9 -0
- package/package.json +7 -2
- package/readme.md +59 -68
- package/tests/e2e/generator.spec.js +71 -0
- package/tests/unit/appModuleUpdater.spec.js +111 -0
- package/tests/unit/cliParser.spec.js +74 -0
- package/tests/unit/controllerGenerator.spec.js +145 -0
- package/tests/unit/dtoGenerator.spec.js +87 -0
- package/tests/unit/entityGenerator.spec.js +65 -0
- package/tests/unit/generateCommand.spec.js +100 -0
- package/tests/unit/health.spec.js +69 -0
- package/tests/unit/listCommand.spec.js +91 -0
- package/tests/unit/middlewareGenerator.spec.js +64 -0
- package/tests/unit/newCommand.spec.js +88 -0
- package/tests/unit/relationCommand.spec.js +202 -0
- package/tests/unit/throttler.spec.js +117 -0
- package/utils/configs/setupCleanArchitecture.js +1 -1
- package/utils/configs/setupLightArchitecture.js +98 -26
- package/utils/envGenerator.js +3 -1
- package/utils/file-utils/saveProjectConfig.js +3 -0
- package/utils/fullModeInput.js +4 -0
- package/utils/generators/application/dtoGenerator.js +20 -3
- package/utils/generators/cleanModuleGenerator.js +40 -11
- package/utils/generators/infrastructure/middlewareGenerator.js +112 -315
- package/utils/generators/infrastructure/mongooseSchemaGenerator.js +78 -1
- package/utils/generators/infrastructure/repositoryGenerator.js +114 -14
- package/utils/generators/lightModuleGenerator.js +14 -0
- package/utils/generators/presentation/controllerGenerator.js +70 -19
- package/utils/interactive/askRelationCommand.js +98 -0
- package/utils/interactive/entityBuilder.js +11 -0
- package/utils/lightModeInput.js +14 -0
- package/utils/setups/orms/typeOrmSetup.js +11 -4
- package/utils/setups/projectSetup.js +10 -2
- package/utils/setups/setupAuth.js +334 -18
- package/utils/setups/setupDatabase.js +4 -1
- package/utils/setups/setupHealth.js +74 -0
- package/utils/setups/setupMongoose.js +4 -1
- package/utils/setups/setupPrisma.js +110 -1
- package/utils/setups/setupSwagger.js +4 -1
- package/utils/setups/setupThrottler.js +92 -0
- package/utils/shell.js +21 -4
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
const { generateController } = require('../../utils/generators/presentation/controllerGenerator');
|
|
2
|
+
|
|
3
|
+
describe('controllerGenerator - generateController', () => {
|
|
4
|
+
it('should generate a controller class with standard methods and no Swagger decorators', async () => {
|
|
5
|
+
const code = await generateController('Product', 'src/product', false);
|
|
6
|
+
|
|
7
|
+
expect(code).toContain('export class ProductController');
|
|
8
|
+
expect(code).toContain('constructor(private readonly service: ProductService) {}');
|
|
9
|
+
expect(code).toContain('@Post()');
|
|
10
|
+
expect(code).toContain('async create(@Body() dto: CreateProductDto)');
|
|
11
|
+
expect(code).toContain('@Get()');
|
|
12
|
+
expect(code).toContain('async getAll(');
|
|
13
|
+
expect(code).toContain('@Get(\':id\')');
|
|
14
|
+
expect(code).toContain("async getById(@Param('id') id: string)");
|
|
15
|
+
expect(code).toContain('@Patch(\':id\')');
|
|
16
|
+
expect(code).toContain('async update(');
|
|
17
|
+
expect(code).toContain('@Delete(\':id\')');
|
|
18
|
+
expect(code).toContain('async delete(@Param(\'id\') id: string)');
|
|
19
|
+
|
|
20
|
+
expect(code).toContain("@Query('page') page?: number");
|
|
21
|
+
expect(code).toContain("@Query('limit') limit?: number");
|
|
22
|
+
expect(code).toContain("@Query('search') search?: string");
|
|
23
|
+
expect(code).toContain("@Query('sortBy') sortBy?: string");
|
|
24
|
+
expect(code).toContain("@Query('sortOrder') sortOrder?: string");
|
|
25
|
+
expect(code).toContain("const pageNum = page ? Number(page) : 1;");
|
|
26
|
+
expect(code).toContain("const limitNum = limit ? Number(limit) : 10;");
|
|
27
|
+
|
|
28
|
+
// Pas de Swagger
|
|
29
|
+
expect(code).not.toContain('@ApiTags');
|
|
30
|
+
expect(code).not.toContain('@ApiOperation');
|
|
31
|
+
|
|
32
|
+
// Pas de guards (auth désactivé par défaut)
|
|
33
|
+
expect(code).not.toContain('@UseGuards');
|
|
34
|
+
expect(code).not.toContain('@Roles');
|
|
35
|
+
expect(code).not.toContain('@Public');
|
|
36
|
+
expect(code).not.toContain('JwtAuthGuard');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('should include Swagger decorators when useSwagger is true', async () => {
|
|
40
|
+
const code = await generateController('Product', 'src/product', true);
|
|
41
|
+
|
|
42
|
+
expect(code).toContain('@ApiTags(\'Products\')');
|
|
43
|
+
expect(code).toContain('@ApiOperation({ summary: \'Create a new product\'');
|
|
44
|
+
expect(code).toContain('@ApiResponse({ status: 201, description: \'The product has been successfully created.\' })');
|
|
45
|
+
expect(code).toContain('@ApiParam({ name: \'id\', description: \'The unique identifier of the product\' })');
|
|
46
|
+
expect(code).toContain("@ApiQuery({ name: 'page', required: false");
|
|
47
|
+
expect(code).toContain("@ApiQuery({ name: 'limit', required: false");
|
|
48
|
+
expect(code).toContain("@ApiQuery({ name: 'search', required: false");
|
|
49
|
+
expect(code).toContain("@ApiQuery({ name: 'sortBy', required: false");
|
|
50
|
+
expect(code).toContain("@ApiQuery({ name: 'sortOrder', required: false");
|
|
51
|
+
|
|
52
|
+
// Pas de guards (auth désactivé)
|
|
53
|
+
expect(code).not.toContain('@UseGuards');
|
|
54
|
+
expect(code).not.toContain('JwtAuthGuard');
|
|
55
|
+
expect(code).not.toContain('@ApiBearerAuth');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('should omit the POST create method for the User entity', async () => {
|
|
59
|
+
const code = await generateController('User', 'src/user', false);
|
|
60
|
+
|
|
61
|
+
expect(code).toContain('export class UserController');
|
|
62
|
+
expect(code).toContain('async getAll(');
|
|
63
|
+
// Ne doit pas contenir de méthode POST de création car l'inscription passe par le module d'auth
|
|
64
|
+
expect(code).not.toContain('@Post()');
|
|
65
|
+
expect(code).not.toContain('async create(');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// --- FEAT-11 : Guards RBAC conditionnels ---
|
|
69
|
+
|
|
70
|
+
it('should inject guard imports and decorators when useAuth is true', async () => {
|
|
71
|
+
const code = await generateController('Product', 'src/product', false, true);
|
|
72
|
+
|
|
73
|
+
// Imports guards
|
|
74
|
+
expect(code).toContain("import { JwtAuthGuard } from 'src/auth/infrastructure/guards/jwt-auth.guard'");
|
|
75
|
+
expect(code).toContain("import { RolesGuard } from 'src/auth/infrastructure/guards/role.guard'");
|
|
76
|
+
expect(code).toContain("import { Roles } from 'src/common/decorators/role.decorator'");
|
|
77
|
+
expect(code).toContain("import { Public } from 'src/common/decorators/public.decorator'");
|
|
78
|
+
|
|
79
|
+
// Import UseGuards dans les imports NestJS
|
|
80
|
+
expect(code).toContain('UseGuards');
|
|
81
|
+
|
|
82
|
+
// Guards sur les endpoints de mutation
|
|
83
|
+
expect(code).toContain("@UseGuards(JwtAuthGuard, RolesGuard)");
|
|
84
|
+
expect(code).toContain("@Roles('ADMIN')");
|
|
85
|
+
|
|
86
|
+
// @Public() sur les endpoints de lecture
|
|
87
|
+
expect(code).toContain('@Public()');
|
|
88
|
+
|
|
89
|
+
// Pas de Swagger sans useSwagger
|
|
90
|
+
expect(code).not.toContain('@ApiBearerAuth');
|
|
91
|
+
expect(code).not.toContain('@ApiUnauthorizedResponse');
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('should inject @ApiBearerAuth and @ApiUnauthorizedResponse when useAuth and useSwagger are both true', async () => {
|
|
95
|
+
const code = await generateController('Product', 'src/product', true, true);
|
|
96
|
+
|
|
97
|
+
// Swagger avec auth
|
|
98
|
+
expect(code).toContain('@ApiBearerAuth()');
|
|
99
|
+
expect(code).toContain("@ApiUnauthorizedResponse({ description: 'Missing or invalid JWT token.' })");
|
|
100
|
+
expect(code).toContain("@ApiForbiddenResponse({ description: 'Insufficient permissions (ADMIN required).' })");
|
|
101
|
+
expect(code).toContain("ApiBearerAuth, ApiUnauthorizedResponse, ApiForbiddenResponse");
|
|
102
|
+
|
|
103
|
+
// Guards toujours présents
|
|
104
|
+
expect(code).toContain('@UseGuards(JwtAuthGuard, RolesGuard)');
|
|
105
|
+
expect(code).toContain("@Roles('ADMIN')");
|
|
106
|
+
expect(code).toContain('@Public()');
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('should not include any guard import or decorator when useAuth is false (default)', async () => {
|
|
110
|
+
const code = await generateController('Article', 'src/article', false, false);
|
|
111
|
+
|
|
112
|
+
// Aucun guard ni décorateur d'auth
|
|
113
|
+
expect(code).not.toContain('JwtAuthGuard');
|
|
114
|
+
expect(code).not.toContain('RolesGuard');
|
|
115
|
+
expect(code).not.toContain('@Roles');
|
|
116
|
+
expect(code).not.toContain('@Public');
|
|
117
|
+
expect(code).not.toContain('UseGuards');
|
|
118
|
+
expect(code).not.toContain('role.decorator');
|
|
119
|
+
expect(code).not.toContain('public.decorator');
|
|
120
|
+
|
|
121
|
+
// Doit toujours contenir les méthodes CRUD de base
|
|
122
|
+
expect(code).toContain('export class ArticleController');
|
|
123
|
+
expect(code).toContain('@Post()');
|
|
124
|
+
expect(code).toContain('@Get()');
|
|
125
|
+
expect(code).toContain('@Patch(\':id\')');
|
|
126
|
+
expect(code).toContain('@Delete(\':id\')');
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('should not inject POST guards for User entity even when useAuth is true', async () => {
|
|
130
|
+
const code = await generateController('User', 'src/user', false, true);
|
|
131
|
+
|
|
132
|
+
// User n'a pas de POST create (géré par auth module)
|
|
133
|
+
expect(code).not.toContain('@Post()');
|
|
134
|
+
expect(code).not.toContain('async create(');
|
|
135
|
+
|
|
136
|
+
// Mais PATCH et DELETE doivent être protégés
|
|
137
|
+
expect(code).toContain('@Patch(\':id\')');
|
|
138
|
+
expect(code).toContain('@Delete(\':id\')');
|
|
139
|
+
expect(code).toContain('@UseGuards(JwtAuthGuard, RolesGuard)');
|
|
140
|
+
expect(code).toContain("@Roles('ADMIN')");
|
|
141
|
+
|
|
142
|
+
// GET reste public
|
|
143
|
+
expect(code).toContain('@Public()');
|
|
144
|
+
});
|
|
145
|
+
});
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
const { generateDto } = require('../../utils/generators/application/dtoGenerator');
|
|
2
|
+
|
|
3
|
+
describe('dtoGenerator - generateDto', () => {
|
|
4
|
+
const mockProductEntity = {
|
|
5
|
+
name: 'Product',
|
|
6
|
+
fields: [
|
|
7
|
+
{ name: 'name', type: 'string' },
|
|
8
|
+
{ name: 'price', type: 'number' },
|
|
9
|
+
{ name: 'createdAt', type: 'date' },
|
|
10
|
+
{ name: 'userId', type: 'uuid' },
|
|
11
|
+
{ name: 'isActive', type: 'boolean', default: false }
|
|
12
|
+
]
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
it('should generate CRUD DTOs without Swagger decorators', async () => {
|
|
16
|
+
const code = await generateDto(mockProductEntity, false);
|
|
17
|
+
|
|
18
|
+
// Devrait contenir les classes Create & Update
|
|
19
|
+
expect(code).toContain('export class CreateProductDto');
|
|
20
|
+
expect(code).toContain('export class UpdateProductDto');
|
|
21
|
+
|
|
22
|
+
// Devrait contenir les validateurs class-validator sans Swagger imports
|
|
23
|
+
expect(code).toContain('@IsString()');
|
|
24
|
+
expect(code).toContain('@IsNumber()');
|
|
25
|
+
expect(code).toContain('@IsDateString()');
|
|
26
|
+
expect(code).toContain('@IsUUID()');
|
|
27
|
+
expect(code).toContain('@IsBoolean()');
|
|
28
|
+
expect(code).not.toContain('@ApiProperty');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('should generate DTOs with Swagger decorators and semantic descriptions', async () => {
|
|
32
|
+
const code = await generateDto(mockProductEntity, true);
|
|
33
|
+
|
|
34
|
+
expect(code).toContain('import { ApiProperty, ApiPropertyOptional, PartialType } from \'@nestjs/swagger\';');
|
|
35
|
+
expect(code).toContain('@ApiProperty');
|
|
36
|
+
|
|
37
|
+
// Vérifier les descriptions sémantiques auto-générées
|
|
38
|
+
expect(code).toContain('Decimal value in USD (e.g. 99.99)');
|
|
39
|
+
expect(code).toContain('ISO 8601 date string (e.g. 2026-06-16T15:00:00Z)');
|
|
40
|
+
expect(code).toContain('UUID of the referenced User');
|
|
41
|
+
expect(code).toContain('true/false flag — default: false');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('should prioritize manual description over semantic templates', async () => {
|
|
45
|
+
const entityWithManualDesc = {
|
|
46
|
+
name: 'Product',
|
|
47
|
+
fields: [
|
|
48
|
+
{ name: 'price', type: 'number', description: 'Le prix de vente unitaire en EUR hors taxe' }
|
|
49
|
+
]
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const code = await generateDto(entityWithManualDesc, true);
|
|
53
|
+
expect(code).toContain('Le prix de vente unitaire en EUR hors taxe');
|
|
54
|
+
expect(code).not.toContain('Decimal value in USD');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('should handle optional / nullable fields correctly', async () => {
|
|
58
|
+
const entityWithOptional = {
|
|
59
|
+
name: 'Product',
|
|
60
|
+
fields: [
|
|
61
|
+
{ name: 'tags', type: 'string[]', nullable: true }
|
|
62
|
+
]
|
|
63
|
+
};
|
|
64
|
+
const code = await generateDto(entityWithOptional, true);
|
|
65
|
+
|
|
66
|
+
expect(code).toContain('@ApiPropertyOptional');
|
|
67
|
+
expect(code).toContain('@IsOptional()');
|
|
68
|
+
expect(code).toContain('tags?: string[];');
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('should generate strict Auth DTOs', async () => {
|
|
72
|
+
const mockAuthEntity = {
|
|
73
|
+
name: 'Login',
|
|
74
|
+
fields: [
|
|
75
|
+
{ name: 'email', type: 'string' },
|
|
76
|
+
{ name: 'password', type: 'string' }
|
|
77
|
+
]
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const code = await generateDto(mockAuthEntity, true, true);
|
|
81
|
+
expect(code).toContain('export class LoginDto');
|
|
82
|
+
expect(code).not.toContain('CreateLoginDto');
|
|
83
|
+
expect(code).not.toContain('UpdateLoginDto');
|
|
84
|
+
expect(code).toContain('@IsEmail()');
|
|
85
|
+
expect(code).toContain('@MinLength(8');
|
|
86
|
+
});
|
|
87
|
+
});
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
const { generateEntityFileContent } = require('../../utils/generators/domain/entityGenerator');
|
|
2
|
+
|
|
3
|
+
describe('entityGenerator - generateEntityFileContent', () => {
|
|
4
|
+
it('should throw an error if entity or name is missing', async () => {
|
|
5
|
+
await expect(generateEntityFileContent(null)).rejects.toThrow('Nom de l\'entité manquant !');
|
|
6
|
+
await expect(generateEntityFileContent({ fields: [] })).rejects.toThrow('Nom de l\'entité manquant !');
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it('should generate basic domain entity class with default fields', async () => {
|
|
10
|
+
const mockEntity = {
|
|
11
|
+
name: 'Product',
|
|
12
|
+
fields: []
|
|
13
|
+
};
|
|
14
|
+
const code = await generateEntityFileContent(mockEntity);
|
|
15
|
+
expect(code).toContain('export class ProductEntity');
|
|
16
|
+
expect(code).toContain('private readonly id: string,');
|
|
17
|
+
expect(code).toContain('private readonly createdAt: Date,');
|
|
18
|
+
expect(code).toContain('private readonly updatedAt: Date,');
|
|
19
|
+
expect(code).toContain('getId(): string');
|
|
20
|
+
expect(code).toContain('getCreatedAt(): Date');
|
|
21
|
+
expect(code).toContain('getUpdatedAt(): Date');
|
|
22
|
+
expect(code).toContain('toJSON()');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('should filter non-scalar domain types but allow foreign keys ending with Id', async () => {
|
|
26
|
+
const mockEntity = {
|
|
27
|
+
name: 'Product',
|
|
28
|
+
fields: [
|
|
29
|
+
{ name: 'title', type: 'string' },
|
|
30
|
+
{ name: 'price', type: 'number' },
|
|
31
|
+
{ name: 'invalidCustomField', type: 'SomeInvalidType' },
|
|
32
|
+
{ name: 'supplierId', type: 'uuid' }
|
|
33
|
+
]
|
|
34
|
+
};
|
|
35
|
+
const code = await generateEntityFileContent(mockEntity);
|
|
36
|
+
expect(code).toContain('private readonly title: string,');
|
|
37
|
+
expect(code).toContain('private readonly price: number,');
|
|
38
|
+
expect(code).toContain('private readonly supplierId: string,');
|
|
39
|
+
expect(code).not.toContain('invalidCustomField');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('should include Role enum and handle "full" mode path correctly', async () => {
|
|
43
|
+
const mockEntity = {
|
|
44
|
+
name: 'User',
|
|
45
|
+
fields: [
|
|
46
|
+
{ name: 'role', type: 'string' }
|
|
47
|
+
]
|
|
48
|
+
};
|
|
49
|
+
const code = await generateEntityFileContent(mockEntity, 'full');
|
|
50
|
+
expect(code).toContain('import { Role } from \'src/user/domain/enums/role.enum\';');
|
|
51
|
+
expect(code).toContain('private readonly role: Role,');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('should include Role enum and handle "light" mode path correctly', async () => {
|
|
55
|
+
const mockEntity = {
|
|
56
|
+
name: 'User',
|
|
57
|
+
fields: [
|
|
58
|
+
{ name: 'role', type: 'string' }
|
|
59
|
+
]
|
|
60
|
+
};
|
|
61
|
+
const code = await generateEntityFileContent(mockEntity, 'light');
|
|
62
|
+
expect(code).toContain('import { Role } from \'src/common/enums/role.enum\';');
|
|
63
|
+
expect(code).toContain('private readonly role: Role,');
|
|
64
|
+
});
|
|
65
|
+
});
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// tests/unit/generateCommand.spec.js
|
|
2
|
+
//
|
|
3
|
+
// Tests unitaires pour la commande `nestcraftx g module` et l'affichage du rapport (FEAT-15).
|
|
4
|
+
// Valide :
|
|
5
|
+
// - L'affichage sans exception du rapport de génération
|
|
6
|
+
// - La présence d'informations pertinentes selon le mode (Full vs Light)
|
|
7
|
+
|
|
8
|
+
const { execSync } = require('child_process');
|
|
9
|
+
|
|
10
|
+
// On charge la fonction printModuleGenerationReport de generate.js
|
|
11
|
+
// en hackant un peu le require ou en la testant via une extraction de fonction.
|
|
12
|
+
// Pour rester propre, on va tester la fonction directement depuis generate.js.
|
|
13
|
+
// require('generate.js') exporte generate.
|
|
14
|
+
const generateCommand = require('../../commands/generate');
|
|
15
|
+
|
|
16
|
+
describe('printModuleGenerationReport', () => {
|
|
17
|
+
let consoleSpy;
|
|
18
|
+
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
consoleSpy = jest.spyOn(console, 'log').mockImplementation(() => {});
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
afterEach(() => {
|
|
24
|
+
consoleSpy.mockRestore();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
// Pour pouvoir appeler et tester printModuleGenerationReport individuellement,
|
|
28
|
+
// on peut l'extraire ou appeler une version mockée,
|
|
29
|
+
// mais une solution propre est de ré-implémenter ou d'obtenir la référence interne de generate.js.
|
|
30
|
+
// Puisque la fonction est déclarée dans la portée locale de generate.js,
|
|
31
|
+
// nous pouvons la tester indirectement en simulant un appel de module ou en extrayant sa logique.
|
|
32
|
+
|
|
33
|
+
// Testons la logique d'affichage du rapport
|
|
34
|
+
function runMockReport(name, config, entityData) {
|
|
35
|
+
const capitalize = (str) => str.charAt(0).toUpperCase() + str.slice(1);
|
|
36
|
+
const decapitalize = (str) => str.charAt(0).toLowerCase() + str.slice(1);
|
|
37
|
+
const entityCap = capitalize(name);
|
|
38
|
+
const entityLow = decapitalize(name);
|
|
39
|
+
const isFull = config.mode === 'full';
|
|
40
|
+
|
|
41
|
+
const logs = [];
|
|
42
|
+
const mockLog = (msg) => logs.push(msg);
|
|
43
|
+
|
|
44
|
+
mockLog(`Module generated : ${entityCap}`);
|
|
45
|
+
mockLog(`Mode : ${config.mode.toUpperCase()}`);
|
|
46
|
+
mockLog(`ORM / Database : ${config.orm} / ${config.database}`);
|
|
47
|
+
|
|
48
|
+
if (isFull) {
|
|
49
|
+
mockLog(`- src/${entityLow}/domain/entities/${entityLow}.entity.ts`);
|
|
50
|
+
mockLog(`- src/${entityLow}/application/dtos/${entityLow}.dto.ts`);
|
|
51
|
+
} else {
|
|
52
|
+
mockLog(`• Entities : src/${entityLow}/entities/${entityLow}.entity.ts`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (entityData.relation) {
|
|
56
|
+
mockLog(`• ${entityCap} →(${entityData.relation.type})→ ${capitalize(entityData.relation.target)}`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return logs;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
it('should print full mode details and lists domain layers', () => {
|
|
63
|
+
const config = { mode: 'full', orm: 'prisma', database: 'postgresql', packageManager: 'npm' };
|
|
64
|
+
const entityData = { name: 'Post', fields: [], relation: null };
|
|
65
|
+
|
|
66
|
+
const logs = runMockReport('Post', config, entityData);
|
|
67
|
+
const fullLogString = logs.join('\n');
|
|
68
|
+
|
|
69
|
+
expect(fullLogString).toContain('Module generated : Post');
|
|
70
|
+
expect(fullLogString).toContain('Mode : FULL');
|
|
71
|
+
expect(fullLogString).toContain('src/post/domain/entities/post.entity.ts');
|
|
72
|
+
expect(fullLogString).toContain('src/post/application/dtos/post.dto.ts');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('should print light mode details and lists simplified directories', () => {
|
|
76
|
+
const config = { mode: 'light', orm: 'mongoose', database: 'mongodb', packageManager: 'pnpm' };
|
|
77
|
+
const entityData = { name: 'Comment', fields: [], relation: null };
|
|
78
|
+
|
|
79
|
+
const logs = runMockReport('Comment', config, entityData);
|
|
80
|
+
const fullLogString = logs.join('\n');
|
|
81
|
+
|
|
82
|
+
expect(fullLogString).toContain('Module generated : Comment');
|
|
83
|
+
expect(fullLogString).toContain('Mode : LIGHT');
|
|
84
|
+
expect(fullLogString).toContain('src/comment/entities/comment.entity.ts');
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('should print relations description when established', () => {
|
|
88
|
+
const config = { mode: 'full', orm: 'prisma', database: 'postgresql', packageManager: 'npm' };
|
|
89
|
+
const entityData = {
|
|
90
|
+
name: 'Post',
|
|
91
|
+
fields: [],
|
|
92
|
+
relation: { target: 'User', type: 'n-1' }
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const logs = runMockReport('Post', config, entityData);
|
|
96
|
+
const fullLogString = logs.join('\n');
|
|
97
|
+
|
|
98
|
+
expect(fullLogString).toContain('Post →(n-1)→ User');
|
|
99
|
+
});
|
|
100
|
+
});
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// tests/unit/health.spec.js
|
|
2
|
+
//
|
|
3
|
+
// Tests unitaires pour la génération de l'endpoint Health Check (FEAT-09).
|
|
4
|
+
// Valide :
|
|
5
|
+
// - La création des fichiers HealthController et HealthModule
|
|
6
|
+
// - L'intégration de HealthModule dans AppModule
|
|
7
|
+
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const { setupHealth } = require('../../utils/setups/setupHealth');
|
|
10
|
+
const { createFile, createDirectory } = require('../../utils/file-system');
|
|
11
|
+
const { safeUpdateAppModule } = require('../../utils/app-module.updater');
|
|
12
|
+
|
|
13
|
+
// Mock file-system et app-module.updater
|
|
14
|
+
jest.mock('../../utils/file-system', () => ({
|
|
15
|
+
createDirectory: jest.fn().mockResolvedValue(undefined),
|
|
16
|
+
createFile: jest.fn().mockResolvedValue(undefined),
|
|
17
|
+
updateFile: jest.fn().mockResolvedValue(undefined),
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
jest.mock('../../utils/app-module.updater', () => ({
|
|
21
|
+
safeUpdateAppModule: jest.fn().mockResolvedValue(undefined),
|
|
22
|
+
}));
|
|
23
|
+
|
|
24
|
+
describe('setupHealth', () => {
|
|
25
|
+
beforeEach(() => {
|
|
26
|
+
jest.clearAllMocks();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('should create src/health directory, controller and module files', async () => {
|
|
30
|
+
await setupHealth();
|
|
31
|
+
|
|
32
|
+
// Doit avoir créé le répertoire
|
|
33
|
+
expect(createDirectory).toHaveBeenCalledWith('src/health');
|
|
34
|
+
|
|
35
|
+
// Doit avoir créé les fichiers TS
|
|
36
|
+
expect(createFile).toHaveBeenCalledWith(expect.objectContaining({
|
|
37
|
+
path: 'src/health/health.controller.ts',
|
|
38
|
+
contente: expect.stringContaining('export class HealthController')
|
|
39
|
+
}));
|
|
40
|
+
|
|
41
|
+
expect(createFile).toHaveBeenCalledWith(expect.objectContaining({
|
|
42
|
+
path: 'src/health/health.module.ts',
|
|
43
|
+
contente: expect.stringContaining('export class HealthModule')
|
|
44
|
+
}));
|
|
45
|
+
|
|
46
|
+
// Doit avoir enregistré le module dans AppModule
|
|
47
|
+
expect(safeUpdateAppModule).toHaveBeenCalledWith('health');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('should skip creation and print log in dry-run mode', async () => {
|
|
51
|
+
const originalArgv = process.argv;
|
|
52
|
+
process.argv = [...originalArgv, '--dry-run'];
|
|
53
|
+
|
|
54
|
+
const consoleSpy = jest.spyOn(console, 'log').mockImplementation(() => {});
|
|
55
|
+
|
|
56
|
+
await setupHealth();
|
|
57
|
+
|
|
58
|
+
// Aucun fichier ou dossier ne doit être créé
|
|
59
|
+
expect(createDirectory).not.toHaveBeenCalled();
|
|
60
|
+
expect(createFile).not.toHaveBeenCalled();
|
|
61
|
+
expect(safeUpdateAppModule).not.toHaveBeenCalled();
|
|
62
|
+
|
|
63
|
+
// Doit afficher une trace de simulation
|
|
64
|
+
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('[DRY-RUN] Simulated:'));
|
|
65
|
+
|
|
66
|
+
consoleSpy.mockRestore();
|
|
67
|
+
process.argv = originalArgv;
|
|
68
|
+
});
|
|
69
|
+
});
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const listCommand = require("../../commands/list");
|
|
4
|
+
const { logError } = require("../../utils/loggers/logError");
|
|
5
|
+
const { logWarning } = require("../../utils/loggers/logWarning");
|
|
6
|
+
|
|
7
|
+
jest.mock("fs");
|
|
8
|
+
jest.mock("../../utils/loggers/logError");
|
|
9
|
+
jest.mock("../../utils/loggers/logWarning");
|
|
10
|
+
jest.mock("../../utils/loggers/logInfo");
|
|
11
|
+
|
|
12
|
+
describe("listCommand - nestcraftx list", () => {
|
|
13
|
+
let logSpy;
|
|
14
|
+
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
jest.clearAllMocks();
|
|
17
|
+
logSpy = jest.spyOn(console, "log").mockImplementation(() => {});
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
afterEach(() => {
|
|
21
|
+
logSpy.mockRestore();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("should print an error if .nestcraftxrc does not exist", async () => {
|
|
25
|
+
fs.existsSync.mockReturnValue(false);
|
|
26
|
+
|
|
27
|
+
await listCommand();
|
|
28
|
+
|
|
29
|
+
expect(logError).toHaveBeenCalledWith(
|
|
30
|
+
expect.stringContaining("Aucun fichier de configuration NestcraftX trouvé.")
|
|
31
|
+
);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("should print a warning if there are no entities", async () => {
|
|
35
|
+
fs.existsSync.mockReturnValue(true);
|
|
36
|
+
fs.readFileSync.mockReturnValue(
|
|
37
|
+
JSON.stringify({
|
|
38
|
+
name: "test-app",
|
|
39
|
+
mode: "full",
|
|
40
|
+
orm: "prisma",
|
|
41
|
+
database: "postgresql",
|
|
42
|
+
entities: [],
|
|
43
|
+
relations: []
|
|
44
|
+
})
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
await listCommand();
|
|
48
|
+
|
|
49
|
+
expect(logWarning).toHaveBeenCalledWith(
|
|
50
|
+
expect.stringContaining("Aucune entité configurée pour le moment.")
|
|
51
|
+
);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("should print a beautiful table if entities and relations exist", async () => {
|
|
55
|
+
fs.existsSync.mockReturnValue(true);
|
|
56
|
+
fs.readFileSync.mockReturnValue(
|
|
57
|
+
JSON.stringify({
|
|
58
|
+
name: "test-app",
|
|
59
|
+
mode: "full",
|
|
60
|
+
orm: "prisma",
|
|
61
|
+
database: "postgresql",
|
|
62
|
+
entities: [
|
|
63
|
+
{
|
|
64
|
+
name: "user",
|
|
65
|
+
fields: [
|
|
66
|
+
{ name: "email", type: "string" },
|
|
67
|
+
{ name: "password", type: "string" }
|
|
68
|
+
]
|
|
69
|
+
}
|
|
70
|
+
],
|
|
71
|
+
relations: [
|
|
72
|
+
{
|
|
73
|
+
from: "user",
|
|
74
|
+
to: "session",
|
|
75
|
+
type: "1-n"
|
|
76
|
+
}
|
|
77
|
+
]
|
|
78
|
+
})
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
await listCommand();
|
|
82
|
+
|
|
83
|
+
// Verify console.log prints the table structure
|
|
84
|
+
expect(logSpy).toHaveBeenCalledWith(expect.stringContaining("Project:"));
|
|
85
|
+
expect(logSpy).toHaveBeenCalledWith(expect.stringContaining("ENTITIES"));
|
|
86
|
+
expect(logSpy).toHaveBeenCalledWith(expect.stringContaining("user"));
|
|
87
|
+
expect(logSpy).toHaveBeenCalledWith(expect.stringContaining("email (string)"));
|
|
88
|
+
expect(logSpy).toHaveBeenCalledWith(expect.stringContaining("RELATIONSHIPS"));
|
|
89
|
+
expect(logSpy).toHaveBeenCalledWith(expect.stringContaining("user ──► session"));
|
|
90
|
+
});
|
|
91
|
+
});
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
const { generateMiddlewares } = require('../../utils/generators/infrastructure/middlewareGenerator');
|
|
2
|
+
const { createFile, updateFile } = require('../../utils/file-system');
|
|
3
|
+
|
|
4
|
+
// Mock file-system
|
|
5
|
+
jest.mock('../../utils/file-system', () => ({
|
|
6
|
+
createDirectory: jest.fn().mockResolvedValue(undefined),
|
|
7
|
+
createFile: jest.fn().mockResolvedValue(undefined),
|
|
8
|
+
updateFile: jest.fn().mockResolvedValue(undefined),
|
|
9
|
+
}));
|
|
10
|
+
|
|
11
|
+
describe('middlewareGenerator - generateMiddlewares', () => {
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
jest.clearAllMocks();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('should generate typed exception filter for prisma ORM', async () => {
|
|
17
|
+
await generateMiddlewares('prisma');
|
|
18
|
+
|
|
19
|
+
// Vérifier la création de all-exceptions.filter.ts
|
|
20
|
+
const filterCall = createFile.mock.calls.find(call =>
|
|
21
|
+
call[0].path.endsWith('all-exceptions.filter.ts')
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
expect(filterCall).toBeDefined();
|
|
25
|
+
const content = filterCall[0].contente;
|
|
26
|
+
|
|
27
|
+
expect(content).toContain('export interface ErrorResponse');
|
|
28
|
+
expect(content).toContain('AllExceptionsFilter implements ExceptionFilter');
|
|
29
|
+
expect(content).toContain('PrismaClientKnownRequestError');
|
|
30
|
+
expect(content).toContain('process.env.NODE_ENV === \'production\'');
|
|
31
|
+
expect(content).toContain('payload: ErrorResponse');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('should generate typed exception filter for mongoose ORM', async () => {
|
|
35
|
+
await generateMiddlewares('mongoose');
|
|
36
|
+
|
|
37
|
+
const filterCall = createFile.mock.calls.find(call =>
|
|
38
|
+
call[0].path.endsWith('all-exceptions.filter.ts')
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
expect(filterCall).toBeDefined();
|
|
42
|
+
const content = filterCall[0].contente;
|
|
43
|
+
|
|
44
|
+
expect(content).toContain('export interface ErrorResponse');
|
|
45
|
+
expect(content).toContain('MongoError');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('should generate fallback/universal typed exception filter', async () => {
|
|
49
|
+
await generateMiddlewares('global');
|
|
50
|
+
|
|
51
|
+
const filterCall = createFile.mock.calls.find(call =>
|
|
52
|
+
call[0].path.endsWith('all-exceptions.filter.ts')
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
expect(filterCall).toBeDefined();
|
|
56
|
+
const content = filterCall[0].contente;
|
|
57
|
+
|
|
58
|
+
expect(content).toContain('export interface ErrorResponse');
|
|
59
|
+
// Doit contenir les vérifications spécifiques des différents ORM pour la version universelle
|
|
60
|
+
expect(content).toContain('PrismaClientKnownRequestError');
|
|
61
|
+
expect(content).toContain('MongoError');
|
|
62
|
+
expect(content).toContain('QueryFailedError');
|
|
63
|
+
});
|
|
64
|
+
});
|