pumuki 6.3.293 → 6.3.295

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.
@@ -6,14 +6,27 @@ import {
6
6
  findDtoPropertyWithoutValidationMatch,
7
7
  findDtoNestedPropertyWithoutNestedValidationMatch,
8
8
  findBackendControllerRouteWithoutGuardLines,
9
+ findBackendControllerRouteWithoutRolesLines,
10
+ findBackendEventHandlerWithoutOnEventLines,
11
+ findBackendAuthRouteWithoutThrottleLines,
9
12
  findBackendPermissiveCorsConfigurationLines,
10
13
  findBackendStringLiteralUnionEnumCandidateLines,
14
+ findBackendSensitiveCacheWriteLines,
11
15
  findBackendHardDeleteWithoutSoftDeleteLines,
12
16
  findBackendLogWithoutContextLines,
17
+ findBackendAuthResponseWithoutRefreshTokenLines,
18
+ findBackendProcessEnvDefaultFallbackLines,
19
+ findBackendDirectProcessEnvReadLines,
20
+ findBackendConfigModuleWithoutValidationLines,
13
21
  findBackendInconsistentErrorResponseLines,
14
22
  findBackendErrorPayloadWithSuccessStatusLines,
15
23
  findBackendControllerEntityResponseLines,
24
+ findBackendControllerBusinessLogicMatch,
25
+ findBackendRepositoryBusinessLogicMatch,
16
26
  findBackendRawThrowExpressionLines,
27
+ findBackendMissingGlobalValidationPipeLines,
28
+ findBackendMissingHelmetSecurityHeadersLines,
29
+ findBackendMissingCompressionMiddlewareLines,
17
30
  findFrameworkDependencyImportMatch,
18
31
  findMixedCommandQueryClassMatch,
19
32
  findMixedCommandQueryInterfaceMatch,
@@ -22,6 +35,7 @@ import {
22
35
  findNetworkCallWithoutErrorHandlingLines,
23
36
  findNestJsConstructorDependencyWithoutDecoratorMatch,
24
37
  findPersistenceMutationWithoutAuditEventMatch,
38
+ findDtoPropertyWithoutApiPropertyLines,
25
39
  findRecordStringUnknownTypeLines,
26
40
  findUndefinedInBaseTypeUnionLines,
27
41
  findUnknownWithoutGuardLines,
@@ -35,15 +49,25 @@ import {
35
49
  hasDefaultExportedApiRouteHandler,
36
50
  hasDirectNetworkCall,
37
51
  hasDtoPropertyWithoutValidation,
52
+ hasDtoPropertyWithoutApiProperty,
38
53
  hasDtoNestedPropertyWithoutNestedValidation,
39
54
  hasBackendControllerRouteWithoutGuard,
55
+ hasBackendControllerRouteWithoutRoles,
56
+ hasBackendEventHandlerWithoutOnEvent,
57
+ hasBackendAuthRouteWithoutThrottle,
40
58
  hasBackendPermissiveCorsConfiguration,
41
59
  hasBackendStringLiteralUnionEnumCandidate,
60
+ hasBackendSensitiveCacheWrite,
42
61
  hasBackendHardDeleteWithoutSoftDelete,
43
62
  hasBackendInconsistentErrorResponse,
44
63
  hasBackendErrorPayloadWithSuccessStatus,
45
64
  hasBackendControllerEntityResponse,
65
+ hasBackendControllerBusinessLogic,
66
+ hasBackendRepositoryBusinessLogic,
46
67
  hasBackendRawThrowExpression,
68
+ hasBackendMissingGlobalValidationPipe,
69
+ hasBackendMissingHelmetSecurityHeaders,
70
+ hasBackendMissingCompressionMiddleware,
47
71
  hasEmptyCatchClause,
48
72
  hasEvalCall,
49
73
  hasExplicitAnyType,
@@ -54,6 +78,10 @@ import {
54
78
  hasNestedIfElseStatement,
55
79
  hasNestJsConstructorDependencyWithoutDecorator,
56
80
  hasBackendLogWithoutContext,
81
+ hasBackendAuthResponseWithoutRefreshToken,
82
+ hasBackendProcessEnvDefaultFallback,
83
+ hasBackendDirectProcessEnvRead,
84
+ hasBackendConfigModuleWithoutValidation,
57
85
  hasBackendMagicNumberLiteral,
58
86
  hasBackendGenericErrorThrow,
59
87
  hasBackendProductionMockOrSpy,
@@ -307,6 +335,237 @@ test('hasBackendLogWithoutContext detecta logs backend sin request user o trace
307
335
  assert.equal(hasBackendLogWithoutContext(contextualLoggerAst), false);
308
336
  });
309
337
 
338
+ test('hasBackendAuthResponseWithoutRefreshToken detecta respuestas auth solo con access token', () => {
339
+ const accessOnlyResponseAst = {
340
+ type: 'Program',
341
+ body: [
342
+ {
343
+ type: 'ReturnStatement',
344
+ argument: {
345
+ type: 'ObjectExpression',
346
+ loc: { start: { line: 16 }, end: { line: 19 } },
347
+ properties: [
348
+ {
349
+ type: 'ObjectProperty',
350
+ key: { type: 'Identifier', name: 'accessToken' },
351
+ value: { type: 'Identifier', name: 'accessToken' },
352
+ },
353
+ ],
354
+ },
355
+ },
356
+ ],
357
+ };
358
+ const refreshTokenResponseAst = {
359
+ type: 'Program',
360
+ body: [
361
+ {
362
+ type: 'ReturnStatement',
363
+ argument: {
364
+ type: 'ObjectExpression',
365
+ loc: { start: { line: 27 }, end: { line: 31 } },
366
+ properties: [
367
+ {
368
+ type: 'ObjectProperty',
369
+ key: { type: 'Identifier', name: 'accessToken' },
370
+ value: { type: 'Identifier', name: 'accessToken' },
371
+ },
372
+ {
373
+ type: 'ObjectProperty',
374
+ key: { type: 'Identifier', name: 'refreshToken' },
375
+ value: { type: 'Identifier', name: 'refreshToken' },
376
+ },
377
+ ],
378
+ },
379
+ },
380
+ ],
381
+ };
382
+
383
+ assert.equal(hasBackendAuthResponseWithoutRefreshToken(accessOnlyResponseAst), true);
384
+ assert.deepEqual(findBackendAuthResponseWithoutRefreshTokenLines(accessOnlyResponseAst), [16]);
385
+ assert.equal(hasBackendAuthResponseWithoutRefreshToken(refreshTokenResponseAst), false);
386
+ });
387
+
388
+ test('hasBackendProcessEnvDefaultFallback detecta defaults literales en process.env', () => {
389
+ const envFallbackAst = {
390
+ type: 'Program',
391
+ body: [
392
+ {
393
+ type: 'VariableDeclaration',
394
+ declarations: [
395
+ {
396
+ type: 'VariableDeclarator',
397
+ id: { type: 'Identifier', name: 'jwtSecret' },
398
+ init: {
399
+ type: 'LogicalExpression',
400
+ operator: '||',
401
+ loc: { start: { line: 6 }, end: { line: 6 } },
402
+ left: {
403
+ type: 'MemberExpression',
404
+ object: {
405
+ type: 'MemberExpression',
406
+ object: { type: 'Identifier', name: 'process' },
407
+ property: { type: 'Identifier', name: 'env' },
408
+ },
409
+ property: { type: 'Identifier', name: 'JWT_SECRET' },
410
+ },
411
+ right: { type: 'StringLiteral', value: 'dev-secret' },
412
+ },
413
+ },
414
+ {
415
+ type: 'VariableDeclarator',
416
+ id: { type: 'Identifier', name: 'port' },
417
+ init: {
418
+ type: 'LogicalExpression',
419
+ operator: '??',
420
+ loc: { start: { line: 9 }, end: { line: 9 } },
421
+ left: {
422
+ type: 'MemberExpression',
423
+ object: {
424
+ type: 'MemberExpression',
425
+ object: { type: 'Identifier', name: 'process' },
426
+ property: { type: 'Identifier', name: 'env' },
427
+ },
428
+ property: { type: 'Identifier', name: 'PORT' },
429
+ },
430
+ right: { type: 'NumericLiteral', value: 3000 },
431
+ },
432
+ },
433
+ ],
434
+ },
435
+ ],
436
+ };
437
+ const strictEnvAst = {
438
+ type: 'Program',
439
+ body: [
440
+ {
441
+ type: 'VariableDeclaration',
442
+ declarations: [
443
+ {
444
+ type: 'VariableDeclarator',
445
+ id: { type: 'Identifier', name: 'jwtSecret' },
446
+ init: {
447
+ type: 'CallExpression',
448
+ callee: { type: 'Identifier', name: 'requiredEnv' },
449
+ arguments: [
450
+ {
451
+ type: 'MemberExpression',
452
+ object: {
453
+ type: 'MemberExpression',
454
+ object: { type: 'Identifier', name: 'process' },
455
+ property: { type: 'Identifier', name: 'env' },
456
+ },
457
+ property: { type: 'Identifier', name: 'JWT_SECRET' },
458
+ },
459
+ ],
460
+ },
461
+ },
462
+ ],
463
+ },
464
+ ],
465
+ };
466
+
467
+ assert.equal(hasBackendProcessEnvDefaultFallback(envFallbackAst), true);
468
+ assert.deepEqual(findBackendProcessEnvDefaultFallbackLines(envFallbackAst), [6, 9]);
469
+ assert.equal(hasBackendProcessEnvDefaultFallback(strictEnvAst), false);
470
+ });
471
+
472
+ test('hasBackendDirectProcessEnvRead detecta lecturas directas de process.env', () => {
473
+ const directEnvAst = {
474
+ type: 'Program',
475
+ body: [
476
+ {
477
+ type: 'VariableDeclaration',
478
+ declarations: [
479
+ {
480
+ type: 'VariableDeclarator',
481
+ id: { type: 'Identifier', name: 'databaseUrl' },
482
+ init: {
483
+ type: 'MemberExpression',
484
+ loc: { start: { line: 6 }, end: { line: 6 } },
485
+ object: {
486
+ type: 'MemberExpression',
487
+ object: { type: 'Identifier', name: 'process' },
488
+ property: { type: 'Identifier', name: 'env' },
489
+ },
490
+ property: { type: 'Identifier', name: 'DATABASE_URL' },
491
+ },
492
+ },
493
+ ],
494
+ },
495
+ ],
496
+ };
497
+ const configServiceAst = {
498
+ type: 'CallExpression',
499
+ callee: {
500
+ type: 'MemberExpression',
501
+ object: { type: 'Identifier', name: 'configService' },
502
+ property: { type: 'Identifier', name: 'getOrThrow' },
503
+ },
504
+ arguments: [{ type: 'StringLiteral', value: 'DATABASE_URL' }],
505
+ };
506
+
507
+ assert.equal(hasBackendDirectProcessEnvRead(directEnvAst), true);
508
+ assert.deepEqual(findBackendDirectProcessEnvReadLines(directEnvAst), [6]);
509
+ assert.equal(hasBackendDirectProcessEnvRead(configServiceAst), false);
510
+ });
511
+
512
+ test('hasBackendConfigModuleWithoutValidation detecta ConfigModule sin validacion de env', () => {
513
+ const missingValidationAst = {
514
+ type: 'Program',
515
+ body: [
516
+ {
517
+ type: 'ExpressionStatement',
518
+ expression: {
519
+ type: 'CallExpression',
520
+ loc: { start: { line: 8 }, end: { line: 11 } },
521
+ callee: {
522
+ type: 'MemberExpression',
523
+ object: { type: 'Identifier', name: 'ConfigModule' },
524
+ property: { type: 'Identifier', name: 'forRoot' },
525
+ },
526
+ arguments: [
527
+ {
528
+ type: 'ObjectExpression',
529
+ properties: [
530
+ {
531
+ type: 'ObjectProperty',
532
+ key: { type: 'Identifier', name: 'isGlobal' },
533
+ value: { type: 'BooleanLiteral', value: true },
534
+ },
535
+ ],
536
+ },
537
+ ],
538
+ },
539
+ },
540
+ ],
541
+ };
542
+ const validatedAst = {
543
+ type: 'CallExpression',
544
+ loc: { start: { line: 18 }, end: { line: 21 } },
545
+ callee: {
546
+ type: 'MemberExpression',
547
+ object: { type: 'Identifier', name: 'ConfigModule' },
548
+ property: { type: 'Identifier', name: 'forRoot' },
549
+ },
550
+ arguments: [
551
+ {
552
+ type: 'ObjectExpression',
553
+ properties: [
554
+ {
555
+ type: 'ObjectProperty',
556
+ key: { type: 'Identifier', name: 'validationSchema' },
557
+ value: { type: 'Identifier', name: 'envSchema' },
558
+ },
559
+ ],
560
+ },
561
+ ],
562
+ };
563
+
564
+ assert.equal(hasBackendConfigModuleWithoutValidation(missingValidationAst), true);
565
+ assert.deepEqual(findBackendConfigModuleWithoutValidationLines(missingValidationAst), [8]);
566
+ assert.equal(hasBackendConfigModuleWithoutValidation(validatedAst), false);
567
+ });
568
+
310
569
  test('hasBackendMagicNumberLiteral detecta numeros inline y permite constantes nombradas', () => {
311
570
  const inlineMagicNumberAst = {
312
571
  type: 'ReturnStatement',
@@ -715,87 +974,280 @@ test('hasBackendControllerEntityResponse detecta entidades expuestas por control
715
974
  assert.equal(hasBackendControllerEntityResponse(dtoReturnTypeAst), false);
716
975
  });
717
976
 
718
- test('hasBackendAnemicDomainModel detecta entidades de dominio anemicas', () => {
719
- const anemicEntityAst = {
720
- type: 'ClassDeclaration',
721
- id: { type: 'Identifier', name: 'OrderEntity' },
722
- body: {
723
- type: 'ClassBody',
724
- body: [
725
- { type: 'ClassProperty', key: { type: 'Identifier', name: 'id' } },
726
- { type: 'ClassProperty', key: { type: 'Identifier', name: 'status' } },
727
- {
728
- type: 'ClassMethod',
729
- kind: 'method',
730
- key: { type: 'Identifier', name: 'getStatus' },
731
- body: { type: 'BlockStatement', body: [] },
732
- },
733
- {
734
- type: 'ClassMethod',
735
- kind: 'method',
736
- key: { type: 'Identifier', name: 'setStatus' },
737
- body: { type: 'BlockStatement', body: [] },
738
- },
739
- ],
740
- },
741
- };
742
- const richEntityAst = {
743
- type: 'ClassDeclaration',
744
- id: { type: 'Identifier', name: 'OrderEntity' },
745
- body: {
746
- type: 'ClassBody',
747
- body: [
748
- { type: 'ClassProperty', key: { type: 'Identifier', name: 'id' } },
749
- { type: 'ClassProperty', key: { type: 'Identifier', name: 'status' } },
750
- {
751
- type: 'ClassMethod',
752
- kind: 'method',
753
- key: { type: 'Identifier', name: 'markAsPaid' },
754
- body: { type: 'BlockStatement', body: [] },
755
- },
756
- ],
757
- },
758
- };
759
-
760
- assert.equal(hasBackendAnemicDomainModel(anemicEntityAst), true);
761
- assert.equal(hasBackendAnemicDomainModel(richEntityAst), false);
762
- });
763
-
764
- test('hasBackendPermissiveCorsConfiguration detecta CORS permisivo y permite origenes acotados', () => {
765
- const permissiveAst = {
977
+ test('hasBackendControllerBusinessLogic detecta logica dentro de rutas controller NestJS', () => {
978
+ const controllerWithLogicAst = {
766
979
  type: 'Program',
767
980
  body: [
768
981
  {
769
- type: 'CallExpression',
770
- loc: { start: { line: 3 }, end: { line: 3 } },
771
- callee: {
772
- type: 'MemberExpression',
773
- computed: false,
774
- object: { type: 'Identifier', name: 'app' },
775
- property: { type: 'Identifier', name: 'enableCors' },
776
- },
777
- arguments: [],
778
- },
779
- {
780
- type: 'CallExpression',
781
- loc: { start: { line: 7 }, end: { line: 7 } },
782
- callee: {
783
- type: 'MemberExpression',
784
- computed: false,
785
- object: { type: 'Identifier', name: 'NestFactory' },
786
- property: { type: 'Identifier', name: 'create' },
787
- },
788
- arguments: [
789
- { type: 'Identifier', name: 'AppModule' },
982
+ type: 'ClassDeclaration',
983
+ decorators: [
790
984
  {
791
- type: 'ObjectExpression',
792
- properties: [
793
- {
794
- type: 'ObjectProperty',
795
- key: { type: 'Identifier', name: 'cors' },
796
- value: { type: 'BooleanLiteral', value: true },
797
- },
798
- ],
985
+ type: 'Decorator',
986
+ expression: {
987
+ type: 'CallExpression',
988
+ callee: { type: 'Identifier', name: 'Controller' },
989
+ arguments: [],
990
+ },
991
+ },
992
+ ],
993
+ body: {
994
+ type: 'ClassBody',
995
+ body: [
996
+ {
997
+ type: 'ClassMethod',
998
+ key: { type: 'Identifier', name: 'createOrder' },
999
+ loc: { start: { line: 18 }, end: { line: 30 } },
1000
+ decorators: [
1001
+ {
1002
+ type: 'Decorator',
1003
+ expression: {
1004
+ type: 'CallExpression',
1005
+ callee: { type: 'Identifier', name: 'Post' },
1006
+ arguments: [],
1007
+ },
1008
+ },
1009
+ ],
1010
+ body: {
1011
+ type: 'BlockStatement',
1012
+ body: [
1013
+ {
1014
+ type: 'IfStatement',
1015
+ test: { type: 'Identifier', name: 'invalid' },
1016
+ consequent: { type: 'BlockStatement', body: [] },
1017
+ },
1018
+ ],
1019
+ },
1020
+ },
1021
+ ],
1022
+ },
1023
+ },
1024
+ ],
1025
+ };
1026
+ const thinControllerAst = {
1027
+ type: 'Program',
1028
+ body: [
1029
+ {
1030
+ type: 'ClassDeclaration',
1031
+ decorators: [
1032
+ {
1033
+ type: 'Decorator',
1034
+ expression: {
1035
+ type: 'CallExpression',
1036
+ callee: { type: 'Identifier', name: 'Controller' },
1037
+ arguments: [],
1038
+ },
1039
+ },
1040
+ ],
1041
+ body: {
1042
+ type: 'ClassBody',
1043
+ body: [
1044
+ {
1045
+ type: 'ClassMethod',
1046
+ key: { type: 'Identifier', name: 'createOrder' },
1047
+ decorators: [
1048
+ {
1049
+ type: 'Decorator',
1050
+ expression: {
1051
+ type: 'CallExpression',
1052
+ callee: { type: 'Identifier', name: 'Post' },
1053
+ arguments: [],
1054
+ },
1055
+ },
1056
+ ],
1057
+ body: {
1058
+ type: 'BlockStatement',
1059
+ body: [
1060
+ {
1061
+ type: 'ReturnStatement',
1062
+ argument: {
1063
+ type: 'CallExpression',
1064
+ callee: {
1065
+ type: 'MemberExpression',
1066
+ object: {
1067
+ type: 'MemberExpression',
1068
+ object: { type: 'ThisExpression' },
1069
+ property: { type: 'Identifier', name: 'ordersService' },
1070
+ },
1071
+ property: { type: 'Identifier', name: 'create' },
1072
+ },
1073
+ arguments: [],
1074
+ },
1075
+ },
1076
+ ],
1077
+ },
1078
+ },
1079
+ ],
1080
+ },
1081
+ },
1082
+ ],
1083
+ };
1084
+
1085
+ assert.equal(hasBackendControllerBusinessLogic(controllerWithLogicAst), true);
1086
+ assert.equal(hasBackendControllerBusinessLogic(thinControllerAst), false);
1087
+
1088
+ const match = findBackendControllerBusinessLogicMatch(controllerWithLogicAst);
1089
+ assert.ok(match);
1090
+ assert.deepEqual(match.lines, [18]);
1091
+ assert.equal(match.primary_node, 'createOrder');
1092
+ });
1093
+
1094
+ test('hasBackendRepositoryBusinessLogic detecta decisiones de negocio dentro de repositories', () => {
1095
+ const repositoryWithLogicAst = {
1096
+ type: 'Program',
1097
+ body: [
1098
+ {
1099
+ type: 'ClassDeclaration',
1100
+ id: { type: 'Identifier', name: 'OrdersRepository' },
1101
+ body: {
1102
+ type: 'ClassBody',
1103
+ body: [
1104
+ {
1105
+ type: 'ClassMethod',
1106
+ key: { type: 'Identifier', name: 'saveOrder' },
1107
+ loc: { start: { line: 27 }, end: { line: 35 } },
1108
+ body: {
1109
+ type: 'BlockStatement',
1110
+ body: [
1111
+ {
1112
+ type: 'IfStatement',
1113
+ test: { type: 'Identifier', name: 'isPremium' },
1114
+ consequent: { type: 'BlockStatement', body: [] },
1115
+ },
1116
+ ],
1117
+ },
1118
+ },
1119
+ ],
1120
+ },
1121
+ },
1122
+ ],
1123
+ };
1124
+ const thinRepositoryAst = {
1125
+ type: 'Program',
1126
+ body: [
1127
+ {
1128
+ type: 'ClassDeclaration',
1129
+ id: { type: 'Identifier', name: 'OrdersRepository' },
1130
+ body: {
1131
+ type: 'ClassBody',
1132
+ body: [
1133
+ {
1134
+ type: 'ClassMethod',
1135
+ key: { type: 'Identifier', name: 'findActiveOrdersByUserId' },
1136
+ body: {
1137
+ type: 'BlockStatement',
1138
+ body: [
1139
+ {
1140
+ type: 'ReturnStatement',
1141
+ argument: {
1142
+ type: 'CallExpression',
1143
+ callee: {
1144
+ type: 'MemberExpression',
1145
+ object: { type: 'Identifier', name: 'queryBuilder' },
1146
+ property: { type: 'Identifier', name: 'where' },
1147
+ },
1148
+ arguments: [],
1149
+ },
1150
+ },
1151
+ ],
1152
+ },
1153
+ },
1154
+ ],
1155
+ },
1156
+ },
1157
+ ],
1158
+ };
1159
+
1160
+ assert.equal(hasBackendRepositoryBusinessLogic(repositoryWithLogicAst), true);
1161
+ assert.equal(hasBackendRepositoryBusinessLogic(thinRepositoryAst), false);
1162
+
1163
+ const match = findBackendRepositoryBusinessLogicMatch(repositoryWithLogicAst);
1164
+ assert.ok(match);
1165
+ assert.deepEqual(match.lines, [27]);
1166
+ assert.equal(match.primary_node, 'OrdersRepository');
1167
+ assert.deepEqual(match.related_nodes, ['saveOrder']);
1168
+ });
1169
+
1170
+ test('hasBackendAnemicDomainModel detecta entidades de dominio anemicas', () => {
1171
+ const anemicEntityAst = {
1172
+ type: 'ClassDeclaration',
1173
+ id: { type: 'Identifier', name: 'OrderEntity' },
1174
+ body: {
1175
+ type: 'ClassBody',
1176
+ body: [
1177
+ { type: 'ClassProperty', key: { type: 'Identifier', name: 'id' } },
1178
+ { type: 'ClassProperty', key: { type: 'Identifier', name: 'status' } },
1179
+ {
1180
+ type: 'ClassMethod',
1181
+ kind: 'method',
1182
+ key: { type: 'Identifier', name: 'getStatus' },
1183
+ body: { type: 'BlockStatement', body: [] },
1184
+ },
1185
+ {
1186
+ type: 'ClassMethod',
1187
+ kind: 'method',
1188
+ key: { type: 'Identifier', name: 'setStatus' },
1189
+ body: { type: 'BlockStatement', body: [] },
1190
+ },
1191
+ ],
1192
+ },
1193
+ };
1194
+ const richEntityAst = {
1195
+ type: 'ClassDeclaration',
1196
+ id: { type: 'Identifier', name: 'OrderEntity' },
1197
+ body: {
1198
+ type: 'ClassBody',
1199
+ body: [
1200
+ { type: 'ClassProperty', key: { type: 'Identifier', name: 'id' } },
1201
+ { type: 'ClassProperty', key: { type: 'Identifier', name: 'status' } },
1202
+ {
1203
+ type: 'ClassMethod',
1204
+ kind: 'method',
1205
+ key: { type: 'Identifier', name: 'markAsPaid' },
1206
+ body: { type: 'BlockStatement', body: [] },
1207
+ },
1208
+ ],
1209
+ },
1210
+ };
1211
+
1212
+ assert.equal(hasBackendAnemicDomainModel(anemicEntityAst), true);
1213
+ assert.equal(hasBackendAnemicDomainModel(richEntityAst), false);
1214
+ });
1215
+
1216
+ test('hasBackendPermissiveCorsConfiguration detecta CORS permisivo y permite origenes acotados', () => {
1217
+ const permissiveAst = {
1218
+ type: 'Program',
1219
+ body: [
1220
+ {
1221
+ type: 'CallExpression',
1222
+ loc: { start: { line: 3 }, end: { line: 3 } },
1223
+ callee: {
1224
+ type: 'MemberExpression',
1225
+ computed: false,
1226
+ object: { type: 'Identifier', name: 'app' },
1227
+ property: { type: 'Identifier', name: 'enableCors' },
1228
+ },
1229
+ arguments: [],
1230
+ },
1231
+ {
1232
+ type: 'CallExpression',
1233
+ loc: { start: { line: 7 }, end: { line: 7 } },
1234
+ callee: {
1235
+ type: 'MemberExpression',
1236
+ computed: false,
1237
+ object: { type: 'Identifier', name: 'NestFactory' },
1238
+ property: { type: 'Identifier', name: 'create' },
1239
+ },
1240
+ arguments: [
1241
+ { type: 'Identifier', name: 'AppModule' },
1242
+ {
1243
+ type: 'ObjectExpression',
1244
+ properties: [
1245
+ {
1246
+ type: 'ObjectProperty',
1247
+ key: { type: 'Identifier', name: 'cors' },
1248
+ value: { type: 'BooleanLiteral', value: true },
1249
+ },
1250
+ ],
799
1251
  },
800
1252
  ],
801
1253
  },
@@ -891,6 +1343,55 @@ test('hasBackendStringLiteralUnionEnumCandidate detecta uniones de literales str
891
1343
  assert.equal(hasBackendStringLiteralUnionEnumCandidate(openUnionAst), false);
892
1344
  });
893
1345
 
1346
+ test('hasBackendSensitiveCacheWrite detecta datos sensibles escritos en cache', () => {
1347
+ const sensitiveCacheAst = {
1348
+ type: 'Program',
1349
+ body: [
1350
+ {
1351
+ type: 'ExpressionStatement',
1352
+ expression: {
1353
+ type: 'CallExpression',
1354
+ loc: { start: { line: 18 }, end: { line: 18 } },
1355
+ callee: {
1356
+ type: 'MemberExpression',
1357
+ object: { type: 'Identifier', name: 'cacheManager' },
1358
+ property: { type: 'Identifier', name: 'set' },
1359
+ },
1360
+ arguments: [
1361
+ { type: 'StringLiteral', value: 'session:token' },
1362
+ { type: 'Identifier', name: 'accessToken' },
1363
+ ],
1364
+ },
1365
+ },
1366
+ ],
1367
+ };
1368
+ const safeCacheAst = {
1369
+ type: 'Program',
1370
+ body: [
1371
+ {
1372
+ type: 'ExpressionStatement',
1373
+ expression: {
1374
+ type: 'CallExpression',
1375
+ loc: { start: { line: 27 }, end: { line: 27 } },
1376
+ callee: {
1377
+ type: 'MemberExpression',
1378
+ object: { type: 'Identifier', name: 'cacheManager' },
1379
+ property: { type: 'Identifier', name: 'set' },
1380
+ },
1381
+ arguments: [
1382
+ { type: 'StringLiteral', value: 'products:list' },
1383
+ { type: 'Identifier', name: 'products' },
1384
+ ],
1385
+ },
1386
+ },
1387
+ ],
1388
+ };
1389
+
1390
+ assert.equal(hasBackendSensitiveCacheWrite(sensitiveCacheAst), true);
1391
+ assert.deepEqual(findBackendSensitiveCacheWriteLines(sensitiveCacheAst), [18]);
1392
+ assert.equal(hasBackendSensitiveCacheWrite(safeCacheAst), false);
1393
+ });
1394
+
894
1395
  test('hasBackendHardDeleteWithoutSoftDelete detecta borrados fisicos en persistencia backend', () => {
895
1396
  const hardDeleteAst = {
896
1397
  type: 'Program',
@@ -2677,8 +3178,8 @@ test('hasDtoPropertyWithoutValidation detecta DTOs sin validacion class-validato
2677
3178
  assert.deepEqual(match.related_nodes, ['email']);
2678
3179
  });
2679
3180
 
2680
- test('hasDtoNestedPropertyWithoutNestedValidation detecta DTOs anidados sin ValidateNested y Type', () => {
2681
- const missingNestedValidationAst = {
3181
+ test('hasDtoPropertyWithoutApiProperty detecta DTOs sin decoradores Swagger', () => {
3182
+ const missingSwaggerAst = {
2682
3183
  type: 'Program',
2683
3184
  body: [
2684
3185
  {
@@ -2689,22 +3190,25 @@ test('hasDtoNestedPropertyWithoutNestedValidation detecta DTOs anidados sin Vali
2689
3190
  body: [
2690
3191
  {
2691
3192
  type: 'ClassProperty',
2692
- key: { type: 'Identifier', name: 'address' },
2693
- loc: { start: { line: 9 }, end: { line: 9 } },
2694
- typeAnnotation: {
2695
- type: 'TSTypeAnnotation',
2696
- typeAnnotation: {
2697
- type: 'TSTypeReference',
2698
- typeName: { type: 'Identifier', name: 'AddressDto' },
3193
+ key: { type: 'Identifier', name: 'email' },
3194
+ loc: { start: { line: 7 }, end: { line: 7 } },
3195
+ decorators: [
3196
+ {
3197
+ type: 'Decorator',
3198
+ expression: {
3199
+ type: 'CallExpression',
3200
+ callee: { type: 'Identifier', name: 'IsEmail' },
3201
+ arguments: [],
3202
+ },
2699
3203
  },
2700
- },
3204
+ ],
2701
3205
  },
2702
3206
  ],
2703
3207
  },
2704
3208
  },
2705
3209
  ],
2706
3210
  };
2707
- const validNestedDtoAst = {
3211
+ const swaggerDtoAst = {
2708
3212
  type: 'Program',
2709
3213
  body: [
2710
3214
  {
@@ -2715,20 +3219,82 @@ test('hasDtoNestedPropertyWithoutNestedValidation detecta DTOs anidados sin Vali
2715
3219
  body: [
2716
3220
  {
2717
3221
  type: 'ClassProperty',
2718
- key: { type: 'Identifier', name: 'address' },
3222
+ key: { type: 'Identifier', name: 'email' },
3223
+ loc: { start: { line: 14 }, end: { line: 14 } },
2719
3224
  decorators: [
2720
3225
  {
2721
3226
  type: 'Decorator',
2722
3227
  expression: {
2723
3228
  type: 'CallExpression',
2724
- callee: { type: 'Identifier', name: 'ValidateNested' },
3229
+ callee: { type: 'Identifier', name: 'ApiProperty' },
2725
3230
  arguments: [],
2726
3231
  },
2727
3232
  },
2728
- {
2729
- type: 'Decorator',
2730
- expression: {
2731
- type: 'CallExpression',
3233
+ ],
3234
+ },
3235
+ ],
3236
+ },
3237
+ },
3238
+ ],
3239
+ };
3240
+
3241
+ assert.equal(hasDtoPropertyWithoutApiProperty(missingSwaggerAst), true);
3242
+ assert.deepEqual(findDtoPropertyWithoutApiPropertyLines(missingSwaggerAst), [7]);
3243
+ assert.equal(hasDtoPropertyWithoutApiProperty(swaggerDtoAst), false);
3244
+ });
3245
+
3246
+ test('hasDtoNestedPropertyWithoutNestedValidation detecta DTOs anidados sin ValidateNested y Type', () => {
3247
+ const missingNestedValidationAst = {
3248
+ type: 'Program',
3249
+ body: [
3250
+ {
3251
+ type: 'ClassDeclaration',
3252
+ id: { type: 'Identifier', name: 'CreateOrderDto' },
3253
+ body: {
3254
+ type: 'ClassBody',
3255
+ body: [
3256
+ {
3257
+ type: 'ClassProperty',
3258
+ key: { type: 'Identifier', name: 'address' },
3259
+ loc: { start: { line: 9 }, end: { line: 9 } },
3260
+ typeAnnotation: {
3261
+ type: 'TSTypeAnnotation',
3262
+ typeAnnotation: {
3263
+ type: 'TSTypeReference',
3264
+ typeName: { type: 'Identifier', name: 'AddressDto' },
3265
+ },
3266
+ },
3267
+ },
3268
+ ],
3269
+ },
3270
+ },
3271
+ ],
3272
+ };
3273
+ const validNestedDtoAst = {
3274
+ type: 'Program',
3275
+ body: [
3276
+ {
3277
+ type: 'ClassDeclaration',
3278
+ id: { type: 'Identifier', name: 'CreateOrderDto' },
3279
+ body: {
3280
+ type: 'ClassBody',
3281
+ body: [
3282
+ {
3283
+ type: 'ClassProperty',
3284
+ key: { type: 'Identifier', name: 'address' },
3285
+ decorators: [
3286
+ {
3287
+ type: 'Decorator',
3288
+ expression: {
3289
+ type: 'CallExpression',
3290
+ callee: { type: 'Identifier', name: 'ValidateNested' },
3291
+ arguments: [],
3292
+ },
3293
+ },
3294
+ {
3295
+ type: 'Decorator',
3296
+ expression: {
3297
+ type: 'CallExpression',
2732
3298
  callee: { type: 'Identifier', name: 'Type' },
2733
3299
  arguments: [],
2734
3300
  },
@@ -2758,6 +3324,225 @@ test('hasDtoNestedPropertyWithoutNestedValidation detecta DTOs anidados sin Vali
2758
3324
  assert.deepEqual(match.related_nodes, ['address']);
2759
3325
  });
2760
3326
 
3327
+ test('hasBackendMissingGlobalValidationPipe detecta bootstrap NestJS sin ValidationPipe estricto', () => {
3328
+ const missingPipeAst = {
3329
+ type: 'Program',
3330
+ body: [
3331
+ {
3332
+ type: 'ExpressionStatement',
3333
+ expression: {
3334
+ type: 'CallExpression',
3335
+ loc: { start: { line: 7 }, end: { line: 7 } },
3336
+ callee: {
3337
+ type: 'MemberExpression',
3338
+ object: { type: 'Identifier', name: 'NestFactory' },
3339
+ property: { type: 'Identifier', name: 'create' },
3340
+ },
3341
+ arguments: [],
3342
+ },
3343
+ },
3344
+ ],
3345
+ };
3346
+ const unsafePipeAst = {
3347
+ type: 'Program',
3348
+ body: [
3349
+ {
3350
+ type: 'ExpressionStatement',
3351
+ expression: {
3352
+ type: 'CallExpression',
3353
+ loc: { start: { line: 11 }, end: { line: 11 } },
3354
+ callee: {
3355
+ type: 'MemberExpression',
3356
+ object: { type: 'Identifier', name: 'app' },
3357
+ property: { type: 'Identifier', name: 'useGlobalPipes' },
3358
+ },
3359
+ arguments: [
3360
+ {
3361
+ type: 'NewExpression',
3362
+ callee: { type: 'Identifier', name: 'ValidationPipe' },
3363
+ arguments: [],
3364
+ },
3365
+ ],
3366
+ },
3367
+ },
3368
+ ],
3369
+ };
3370
+ const strictPipeAst = {
3371
+ type: 'Program',
3372
+ body: [
3373
+ {
3374
+ type: 'ExpressionStatement',
3375
+ expression: {
3376
+ type: 'CallExpression',
3377
+ callee: {
3378
+ type: 'MemberExpression',
3379
+ object: { type: 'Identifier', name: 'NestFactory' },
3380
+ property: { type: 'Identifier', name: 'create' },
3381
+ },
3382
+ arguments: [],
3383
+ },
3384
+ },
3385
+ {
3386
+ type: 'ExpressionStatement',
3387
+ expression: {
3388
+ type: 'CallExpression',
3389
+ callee: {
3390
+ type: 'MemberExpression',
3391
+ object: { type: 'Identifier', name: 'app' },
3392
+ property: { type: 'Identifier', name: 'useGlobalPipes' },
3393
+ },
3394
+ arguments: [
3395
+ {
3396
+ type: 'NewExpression',
3397
+ callee: { type: 'Identifier', name: 'ValidationPipe' },
3398
+ arguments: [
3399
+ {
3400
+ type: 'ObjectExpression',
3401
+ properties: [
3402
+ {
3403
+ type: 'ObjectProperty',
3404
+ key: { type: 'Identifier', name: 'whitelist' },
3405
+ value: { type: 'BooleanLiteral', value: true },
3406
+ },
3407
+ ],
3408
+ },
3409
+ ],
3410
+ },
3411
+ ],
3412
+ },
3413
+ },
3414
+ ],
3415
+ };
3416
+
3417
+ assert.equal(hasBackendMissingGlobalValidationPipe(missingPipeAst), true);
3418
+ assert.deepEqual(findBackendMissingGlobalValidationPipeLines(missingPipeAst), [7]);
3419
+ assert.equal(hasBackendMissingGlobalValidationPipe(unsafePipeAst), true);
3420
+ assert.deepEqual(findBackendMissingGlobalValidationPipeLines(unsafePipeAst), [11]);
3421
+ assert.equal(hasBackendMissingGlobalValidationPipe(strictPipeAst), false);
3422
+ });
3423
+
3424
+ test('hasBackendMissingHelmetSecurityHeaders detecta bootstrap NestJS sin Helmet', () => {
3425
+ const missingHelmetAst = {
3426
+ type: 'Program',
3427
+ body: [
3428
+ {
3429
+ type: 'ExpressionStatement',
3430
+ expression: {
3431
+ type: 'CallExpression',
3432
+ loc: { start: { line: 9 }, end: { line: 9 } },
3433
+ callee: {
3434
+ type: 'MemberExpression',
3435
+ object: { type: 'Identifier', name: 'NestFactory' },
3436
+ property: { type: 'Identifier', name: 'create' },
3437
+ },
3438
+ arguments: [],
3439
+ },
3440
+ },
3441
+ ],
3442
+ };
3443
+ const helmetAst = {
3444
+ type: 'Program',
3445
+ body: [
3446
+ {
3447
+ type: 'ExpressionStatement',
3448
+ expression: {
3449
+ type: 'CallExpression',
3450
+ loc: { start: { line: 11 }, end: { line: 11 } },
3451
+ callee: {
3452
+ type: 'MemberExpression',
3453
+ object: { type: 'Identifier', name: 'NestFactory' },
3454
+ property: { type: 'Identifier', name: 'create' },
3455
+ },
3456
+ arguments: [],
3457
+ },
3458
+ },
3459
+ {
3460
+ type: 'ExpressionStatement',
3461
+ expression: {
3462
+ type: 'CallExpression',
3463
+ callee: {
3464
+ type: 'MemberExpression',
3465
+ object: { type: 'Identifier', name: 'app' },
3466
+ property: { type: 'Identifier', name: 'use' },
3467
+ },
3468
+ arguments: [
3469
+ {
3470
+ type: 'CallExpression',
3471
+ callee: { type: 'Identifier', name: 'helmet' },
3472
+ arguments: [],
3473
+ },
3474
+ ],
3475
+ },
3476
+ },
3477
+ ],
3478
+ };
3479
+
3480
+ assert.equal(hasBackendMissingHelmetSecurityHeaders(missingHelmetAst), true);
3481
+ assert.deepEqual(findBackendMissingHelmetSecurityHeadersLines(missingHelmetAst), [9]);
3482
+ assert.equal(hasBackendMissingHelmetSecurityHeaders(helmetAst), false);
3483
+ });
3484
+
3485
+ test('hasBackendMissingCompressionMiddleware detecta bootstrap NestJS sin compression', () => {
3486
+ const missingCompressionAst = {
3487
+ type: 'Program',
3488
+ body: [
3489
+ {
3490
+ type: 'ExpressionStatement',
3491
+ expression: {
3492
+ type: 'CallExpression',
3493
+ loc: { start: { line: 9 }, end: { line: 9 } },
3494
+ callee: {
3495
+ type: 'MemberExpression',
3496
+ object: { type: 'Identifier', name: 'NestFactory' },
3497
+ property: { type: 'Identifier', name: 'create' },
3498
+ },
3499
+ arguments: [],
3500
+ },
3501
+ },
3502
+ ],
3503
+ };
3504
+ const compressionAst = {
3505
+ type: 'Program',
3506
+ body: [
3507
+ {
3508
+ type: 'ExpressionStatement',
3509
+ expression: {
3510
+ type: 'CallExpression',
3511
+ loc: { start: { line: 7 }, end: { line: 7 } },
3512
+ callee: {
3513
+ type: 'MemberExpression',
3514
+ object: { type: 'Identifier', name: 'NestFactory' },
3515
+ property: { type: 'Identifier', name: 'create' },
3516
+ },
3517
+ arguments: [],
3518
+ },
3519
+ },
3520
+ {
3521
+ type: 'ExpressionStatement',
3522
+ expression: {
3523
+ type: 'CallExpression',
3524
+ callee: {
3525
+ type: 'MemberExpression',
3526
+ object: { type: 'Identifier', name: 'app' },
3527
+ property: { type: 'Identifier', name: 'use' },
3528
+ },
3529
+ arguments: [
3530
+ {
3531
+ type: 'CallExpression',
3532
+ callee: { type: 'Identifier', name: 'compression' },
3533
+ arguments: [],
3534
+ },
3535
+ ],
3536
+ },
3537
+ },
3538
+ ],
3539
+ };
3540
+
3541
+ assert.equal(hasBackendMissingCompressionMiddleware(missingCompressionAst), true);
3542
+ assert.deepEqual(findBackendMissingCompressionMiddlewareLines(missingCompressionAst), [9]);
3543
+ assert.equal(hasBackendMissingCompressionMiddleware(compressionAst), false);
3544
+ });
3545
+
2761
3546
  test('hasBackendControllerRouteWithoutGuard detecta rutas NestJS sin UseGuards', () => {
2762
3547
  const unguardedRouteAst = {
2763
3548
  type: 'Program',
@@ -2828,3 +3613,239 @@ test('hasBackendControllerRouteWithoutGuard detecta rutas NestJS sin UseGuards',
2828
3613
  assert.equal(hasBackendControllerRouteWithoutGuard(classGuardedRouteAst), false);
2829
3614
  assert.deepEqual(findBackendControllerRouteWithoutGuardLines(unguardedRouteAst), [12]);
2830
3615
  });
3616
+
3617
+ test('hasBackendAuthRouteWithoutThrottle detecta endpoints auth sin rate limiting', () => {
3618
+ const loginWithoutThrottleAst = {
3619
+ type: 'Program',
3620
+ body: [
3621
+ {
3622
+ type: 'ClassDeclaration',
3623
+ decorators: [
3624
+ {
3625
+ type: 'Decorator',
3626
+ expression: { type: 'CallExpression', callee: { type: 'Identifier', name: 'Controller' }, arguments: [] },
3627
+ },
3628
+ ],
3629
+ body: {
3630
+ type: 'ClassBody',
3631
+ body: [
3632
+ {
3633
+ type: 'ClassMethod',
3634
+ key: { type: 'Identifier', name: 'login' },
3635
+ loc: { start: { line: 21 }, end: { line: 24 } },
3636
+ decorators: [
3637
+ {
3638
+ type: 'Decorator',
3639
+ expression: {
3640
+ type: 'CallExpression',
3641
+ callee: { type: 'Identifier', name: 'Post' },
3642
+ arguments: [{ type: 'StringLiteral', value: 'login' }],
3643
+ },
3644
+ },
3645
+ ],
3646
+ },
3647
+ ],
3648
+ },
3649
+ },
3650
+ ],
3651
+ };
3652
+ const classThrottledLoginAst = {
3653
+ type: 'Program',
3654
+ body: [
3655
+ {
3656
+ type: 'ClassDeclaration',
3657
+ decorators: [
3658
+ {
3659
+ type: 'Decorator',
3660
+ expression: { type: 'CallExpression', callee: { type: 'Identifier', name: 'Controller' }, arguments: [] },
3661
+ },
3662
+ {
3663
+ type: 'Decorator',
3664
+ expression: { type: 'CallExpression', callee: { type: 'Identifier', name: 'Throttle' }, arguments: [] },
3665
+ },
3666
+ ],
3667
+ body: {
3668
+ type: 'ClassBody',
3669
+ body: [
3670
+ {
3671
+ type: 'ClassMethod',
3672
+ key: { type: 'Identifier', name: 'login' },
3673
+ loc: { start: { line: 34 }, end: { line: 37 } },
3674
+ decorators: [
3675
+ {
3676
+ type: 'Decorator',
3677
+ expression: {
3678
+ type: 'CallExpression',
3679
+ callee: { type: 'Identifier', name: 'Post' },
3680
+ arguments: [{ type: 'StringLiteral', value: 'login' }],
3681
+ },
3682
+ },
3683
+ ],
3684
+ },
3685
+ ],
3686
+ },
3687
+ },
3688
+ ],
3689
+ };
3690
+
3691
+ assert.equal(hasBackendAuthRouteWithoutThrottle(loginWithoutThrottleAst), true);
3692
+ assert.deepEqual(findBackendAuthRouteWithoutThrottleLines(loginWithoutThrottleAst), [21]);
3693
+ assert.equal(hasBackendAuthRouteWithoutThrottle(classThrottledLoginAst), false);
3694
+ });
3695
+
3696
+ test('hasBackendControllerRouteWithoutRoles detecta rutas NestJS protegidas sin RBAC explicito', () => {
3697
+ const guardedWithoutRolesAst = {
3698
+ type: 'Program',
3699
+ body: [
3700
+ {
3701
+ type: 'ClassDeclaration',
3702
+ decorators: [
3703
+ {
3704
+ type: 'Decorator',
3705
+ expression: { type: 'CallExpression', callee: { type: 'Identifier', name: 'Controller' }, arguments: [] },
3706
+ },
3707
+ {
3708
+ type: 'Decorator',
3709
+ expression: { type: 'CallExpression', callee: { type: 'Identifier', name: 'UseGuards' }, arguments: [] },
3710
+ },
3711
+ ],
3712
+ body: {
3713
+ type: 'ClassBody',
3714
+ body: [
3715
+ {
3716
+ type: 'ClassMethod',
3717
+ key: { type: 'Identifier', name: 'deleteOrder' },
3718
+ loc: { start: { line: 31 }, end: { line: 33 } },
3719
+ decorators: [
3720
+ {
3721
+ type: 'Decorator',
3722
+ expression: { type: 'CallExpression', callee: { type: 'Identifier', name: 'Delete' }, arguments: [] },
3723
+ },
3724
+ ],
3725
+ },
3726
+ ],
3727
+ },
3728
+ },
3729
+ ],
3730
+ };
3731
+ const methodRolesAst = {
3732
+ type: 'Program',
3733
+ body: [
3734
+ {
3735
+ type: 'ClassDeclaration',
3736
+ decorators: [
3737
+ {
3738
+ type: 'Decorator',
3739
+ expression: { type: 'CallExpression', callee: { type: 'Identifier', name: 'Controller' }, arguments: [] },
3740
+ },
3741
+ {
3742
+ type: 'Decorator',
3743
+ expression: { type: 'CallExpression', callee: { type: 'Identifier', name: 'UseGuards' }, arguments: [] },
3744
+ },
3745
+ ],
3746
+ body: {
3747
+ type: 'ClassBody',
3748
+ body: [
3749
+ {
3750
+ type: 'ClassMethod',
3751
+ key: { type: 'Identifier', name: 'deleteOrder' },
3752
+ loc: { start: { line: 42 }, end: { line: 44 } },
3753
+ decorators: [
3754
+ {
3755
+ type: 'Decorator',
3756
+ expression: { type: 'CallExpression', callee: { type: 'Identifier', name: 'Roles' }, arguments: [] },
3757
+ },
3758
+ {
3759
+ type: 'Decorator',
3760
+ expression: { type: 'CallExpression', callee: { type: 'Identifier', name: 'Delete' }, arguments: [] },
3761
+ },
3762
+ ],
3763
+ },
3764
+ ],
3765
+ },
3766
+ },
3767
+ ],
3768
+ };
3769
+ const unguardedRouteAst = {
3770
+ type: 'ClassMethod',
3771
+ key: { type: 'Identifier', name: 'publicHealth' },
3772
+ loc: { start: { line: 51 }, end: { line: 53 } },
3773
+ decorators: [
3774
+ {
3775
+ type: 'Decorator',
3776
+ expression: { type: 'CallExpression', callee: { type: 'Identifier', name: 'Get' }, arguments: [] },
3777
+ },
3778
+ ],
3779
+ };
3780
+
3781
+ assert.equal(hasBackendControllerRouteWithoutRoles(guardedWithoutRolesAst), true);
3782
+ assert.deepEqual(findBackendControllerRouteWithoutRolesLines(guardedWithoutRolesAst), [31]);
3783
+ assert.equal(hasBackendControllerRouteWithoutRoles(methodRolesAst), false);
3784
+ assert.equal(hasBackendControllerRouteWithoutRoles(unguardedRouteAst), false);
3785
+ });
3786
+
3787
+ test('hasBackendEventHandlerWithoutOnEvent detecta handlers backend sin decorador OnEvent', () => {
3788
+ const missingOnEventAst = {
3789
+ type: 'Program',
3790
+ body: [
3791
+ {
3792
+ type: 'ClassDeclaration',
3793
+ id: { type: 'Identifier', name: 'OrderCreatedEventHandler' },
3794
+ loc: { start: { line: 9 }, end: { line: 20 } },
3795
+ decorators: [
3796
+ {
3797
+ type: 'Decorator',
3798
+ expression: { type: 'CallExpression', callee: { type: 'Identifier', name: 'Injectable' }, arguments: [] },
3799
+ },
3800
+ ],
3801
+ body: {
3802
+ type: 'ClassBody',
3803
+ body: [
3804
+ {
3805
+ type: 'ClassMethod',
3806
+ key: { type: 'Identifier', name: 'handleOrderCreatedEvent' },
3807
+ loc: { start: { line: 14 }, end: { line: 18 } },
3808
+ decorators: [],
3809
+ },
3810
+ ],
3811
+ },
3812
+ },
3813
+ ],
3814
+ };
3815
+ const decoratedMethodAst = {
3816
+ type: 'Program',
3817
+ body: [
3818
+ {
3819
+ type: 'ClassDeclaration',
3820
+ id: { type: 'Identifier', name: 'OrderCreatedEventHandler' },
3821
+ loc: { start: { line: 29 }, end: { line: 40 } },
3822
+ decorators: [
3823
+ {
3824
+ type: 'Decorator',
3825
+ expression: { type: 'CallExpression', callee: { type: 'Identifier', name: 'Injectable' }, arguments: [] },
3826
+ },
3827
+ ],
3828
+ body: {
3829
+ type: 'ClassBody',
3830
+ body: [
3831
+ {
3832
+ type: 'ClassMethod',
3833
+ key: { type: 'Identifier', name: 'handleOrderCreatedEvent' },
3834
+ loc: { start: { line: 34 }, end: { line: 38 } },
3835
+ decorators: [
3836
+ {
3837
+ type: 'Decorator',
3838
+ expression: { type: 'CallExpression', callee: { type: 'Identifier', name: 'OnEvent' }, arguments: [] },
3839
+ },
3840
+ ],
3841
+ },
3842
+ ],
3843
+ },
3844
+ },
3845
+ ],
3846
+ };
3847
+
3848
+ assert.equal(hasBackendEventHandlerWithoutOnEvent(missingOnEventAst), true);
3849
+ assert.deepEqual(findBackendEventHandlerWithoutOnEventLines(missingOnEventAst), [9, 14]);
3850
+ assert.equal(hasBackendEventHandlerWithoutOnEvent(decoratedMethodAst), false);
3851
+ });