nodejs-quickstart-structure 1.13.0 → 1.15.1

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.
Files changed (103) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +4 -3
  3. package/bin/index.js +84 -80
  4. package/lib/generator.js +28 -4
  5. package/lib/modules/app-setup.js +111 -19
  6. package/lib/modules/caching-setup.js +13 -0
  7. package/lib/modules/config-files.js +50 -62
  8. package/lib/modules/database-setup.js +35 -30
  9. package/lib/modules/kafka-setup.js +78 -10
  10. package/package.json +8 -4
  11. package/templates/clean-architecture/js/src/errors/BadRequestError.js +1 -1
  12. package/templates/clean-architecture/js/src/errors/BadRequestError.spec.js.ejs +21 -0
  13. package/templates/clean-architecture/js/src/errors/NotFoundError.js +1 -1
  14. package/templates/clean-architecture/js/src/errors/NotFoundError.spec.js.ejs +21 -0
  15. package/templates/clean-architecture/js/src/infrastructure/log/logger.spec.js.ejs +63 -0
  16. package/templates/clean-architecture/js/src/infrastructure/repositories/UserRepository.js.ejs +2 -3
  17. package/templates/clean-architecture/js/src/infrastructure/repositories/UserRepository.spec.js.ejs +81 -0
  18. package/templates/clean-architecture/js/src/interfaces/controllers/userController.js.ejs +8 -4
  19. package/templates/clean-architecture/js/src/interfaces/controllers/userController.spec.js.ejs +102 -0
  20. package/templates/clean-architecture/js/src/interfaces/graphql/context.spec.js.ejs +31 -0
  21. package/templates/clean-architecture/js/src/interfaces/graphql/resolvers/user.resolvers.spec.js.ejs +49 -0
  22. package/templates/clean-architecture/js/src/interfaces/routes/api.spec.js.ejs +38 -0
  23. package/templates/clean-architecture/js/src/usecases/CreateUser.spec.js.ejs +51 -0
  24. package/templates/clean-architecture/js/src/usecases/GetAllUsers.spec.js.ejs +61 -0
  25. package/templates/clean-architecture/ts/src/errors/BadRequestError.spec.ts.ejs +21 -0
  26. package/templates/clean-architecture/ts/src/errors/BadRequestError.ts +1 -1
  27. package/templates/clean-architecture/ts/src/errors/NotFoundError.spec.ts.ejs +21 -0
  28. package/templates/clean-architecture/ts/src/errors/NotFoundError.ts +1 -1
  29. package/templates/clean-architecture/ts/src/infrastructure/log/logger.spec.ts.ejs +64 -0
  30. package/templates/clean-architecture/ts/src/infrastructure/repositories/UserRepository.spec.ts.ejs +85 -0
  31. package/templates/clean-architecture/ts/src/infrastructure/repositories/UserRepository.ts.ejs +2 -3
  32. package/templates/clean-architecture/ts/src/interfaces/controllers/userController.spec.ts.ejs +166 -0
  33. package/templates/clean-architecture/ts/src/interfaces/graphql/context.spec.ts.ejs +32 -0
  34. package/templates/clean-architecture/ts/src/interfaces/graphql/resolvers/user.resolvers.spec.ts.ejs +51 -0
  35. package/templates/clean-architecture/ts/src/interfaces/routes/userRoutes.spec.ts.ejs +40 -0
  36. package/templates/clean-architecture/ts/src/usecases/createUser.spec.ts.ejs +51 -0
  37. package/templates/clean-architecture/ts/src/usecases/getAllUsers.spec.ts.ejs +63 -0
  38. package/templates/clean-architecture/ts/src/utils/errorMiddleware.ts.ejs +1 -2
  39. package/templates/common/.cursorrules.ejs +60 -0
  40. package/templates/common/.dockerignore +2 -0
  41. package/templates/common/.gitlab-ci.yml.ejs +5 -5
  42. package/templates/common/Jenkinsfile.ejs +1 -1
  43. package/templates/common/README.md.ejs +11 -1
  44. package/templates/common/_github/workflows/ci.yml +7 -4
  45. package/templates/common/caching/js/memoryCache.spec.js.ejs +101 -0
  46. package/templates/common/caching/js/redisClient.spec.js.ejs +149 -0
  47. package/templates/common/caching/ts/memoryCache.spec.ts.ejs +102 -0
  48. package/templates/common/caching/ts/redisClient.spec.ts.ejs +157 -0
  49. package/templates/common/database/js/database.spec.js.ejs +56 -0
  50. package/templates/common/database/js/models/User.js.ejs +22 -0
  51. package/templates/common/database/js/models/User.spec.js.ejs +84 -0
  52. package/templates/common/database/js/mongoose.spec.js.ejs +43 -0
  53. package/templates/common/database/ts/database.spec.ts.ejs +56 -0
  54. package/templates/common/database/ts/models/User.spec.ts.ejs +84 -0
  55. package/templates/common/database/ts/models/User.ts.ejs +26 -0
  56. package/templates/common/database/ts/mongoose.spec.ts.ejs +42 -0
  57. package/templates/common/eslint.config.mjs.ejs +11 -2
  58. package/templates/common/health/js/healthRoute.spec.js.ejs +70 -0
  59. package/templates/common/health/ts/healthRoute.spec.ts.ejs +76 -0
  60. package/templates/common/jest.config.js.ejs +19 -5
  61. package/templates/common/kafka/js/config/kafka.spec.js.ejs +21 -0
  62. package/templates/common/kafka/js/services/kafkaService.js.ejs +9 -5
  63. package/templates/common/kafka/js/services/kafkaService.spec.js.ejs +60 -0
  64. package/templates/common/kafka/ts/config/kafka.spec.ts.ejs +21 -0
  65. package/templates/common/kafka/ts/services/kafkaService.spec.ts.ejs +61 -0
  66. package/templates/common/kafka/ts/services/kafkaService.ts.ejs +1 -1
  67. package/templates/common/package.json.ejs +0 -3
  68. package/templates/common/prompts/add-feature.md.ejs +26 -0
  69. package/templates/common/prompts/project-context.md.ejs +43 -0
  70. package/templates/common/prompts/troubleshoot.md.ejs +28 -0
  71. package/templates/common/shutdown/js/gracefulShutdown.spec.js.ejs +160 -0
  72. package/templates/common/shutdown/ts/gracefulShutdown.spec.ts.ejs +158 -0
  73. package/templates/common/src/utils/errorMiddleware.spec.js.ejs +79 -0
  74. package/templates/common/src/utils/errorMiddleware.spec.ts.ejs +94 -0
  75. package/templates/common/tsconfig.json +1 -1
  76. package/templates/mvc/js/src/controllers/userController.js.ejs +4 -31
  77. package/templates/mvc/js/src/controllers/userController.spec.js.ejs +170 -0
  78. package/templates/mvc/js/src/errors/BadRequestError.js +1 -1
  79. package/templates/mvc/js/src/errors/BadRequestError.spec.js.ejs +21 -0
  80. package/templates/mvc/js/src/errors/NotFoundError.js +1 -1
  81. package/templates/mvc/js/src/errors/NotFoundError.spec.js.ejs +21 -0
  82. package/templates/mvc/js/src/graphql/context.spec.js.ejs +29 -0
  83. package/templates/mvc/js/src/graphql/resolvers/user.resolvers.spec.js.ejs +47 -0
  84. package/templates/mvc/js/src/index.js.ejs +1 -1
  85. package/templates/mvc/js/src/routes/api.spec.js.ejs +36 -0
  86. package/templates/mvc/js/src/utils/logger.spec.js.ejs +63 -0
  87. package/templates/mvc/ts/src/controllers/userController.spec.ts.ejs +185 -0
  88. package/templates/mvc/ts/src/controllers/userController.ts.ejs +4 -31
  89. package/templates/mvc/ts/src/errors/BadRequestError.spec.ts.ejs +21 -0
  90. package/templates/mvc/ts/src/errors/BadRequestError.ts +1 -1
  91. package/templates/mvc/ts/src/errors/NotFoundError.spec.ts.ejs +21 -0
  92. package/templates/mvc/ts/src/errors/NotFoundError.ts +1 -1
  93. package/templates/mvc/ts/src/graphql/context.spec.ts.ejs +30 -0
  94. package/templates/mvc/ts/src/graphql/resolvers/user.resolvers.spec.ts.ejs +51 -0
  95. package/templates/mvc/ts/src/routes/api.spec.ts.ejs +40 -0
  96. package/templates/mvc/ts/src/utils/errorMiddleware.ts.ejs +1 -2
  97. package/templates/mvc/ts/src/utils/logger.spec.ts.ejs +64 -0
  98. package/docs/demo.gif +0 -0
  99. package/docs/generateCase.md +0 -265
  100. package/docs/generatorFlow.md +0 -233
  101. package/docs/releaseNoteRule.md +0 -42
  102. package/docs/ruleDevelop.md +0 -30
  103. package/templates/common/tests/health.test.ts.ejs +0 -24
@@ -17,7 +17,7 @@ export class KafkaService {
17
17
  await this.consumer.subscribe({ topic: 'test-topic', fromBeginning: true });
18
18
 
19
19
  await this.consumer.run({
20
- eachMessage: async ({ topic, partition, message }: EachMessagePayload) => {
20
+ eachMessage: async ({ message }: EachMessagePayload) => {
21
21
  logger.info({
22
22
  value: message.value?.toString(),
23
23
  });
@@ -94,9 +94,6 @@
94
94
  "jest": "^29.7.0",
95
95
  "supertest": "6.3.3"<% } %>
96
96
  },
97
- "overrides": {
98
- "minimatch": "^10.2.1"
99
- },
100
97
  "lint-staged": {
101
98
  "*.{js,ts}": [
102
99
  "eslint --fix",
@@ -0,0 +1,26 @@
1
+ # Add New Feature
2
+
3
+ I want to add a new feature to the existing application.
4
+ Please follow the strict standards defined in the project context.
5
+
6
+ ## Feature Description
7
+ [INSERT EXPLANATION OF WHAT YOU WANT TO ADD HERE]
8
+
9
+ ## Implementation Guidelines
10
+
11
+ Please provide the code implementation following these steps:
12
+
13
+ 1. **Plan first**: Outline the files you need to create/modify and the logic they'll contain.
14
+ <% if (architecture === 'Clean Architecture') { -%>
15
+ 2. **Domain/Entity**: Define the core entity structure or interfaces if applicable.
16
+ 3. **Use Case**: Implement the business logic handling the feature.
17
+ 4. **Adapter (Controller & Route)**: Create the necessary endpoints and validate input.
18
+ 5. **Infrastructure (Repository)**: Implement database queries or external service calls.
19
+ <% } else { -%>
20
+ 2. **Model**: Define the database schema/model if applicable.
21
+ 3. **Controller**: Implement the business logic and request handling.
22
+ 4. **Route**: Create the API endpoints and wire them to the controller.
23
+ <% } -%>
24
+ 6. **Testing**: Write comprehensive Jest unit tests covering the "Happy Path" and "Edge Cases/Errors" (AAA pattern). Remember, our coverage requirement is > 70%!
25
+
26
+ Please provide the plan first so I can review it before we write the code.
@@ -0,0 +1,43 @@
1
+ # Project Context
2
+
3
+ Hello AI! I am working on a Node.js project. Here is the context to help you understand the architecture, domain, and standards.
4
+
5
+ ## Domain Overview
6
+ **Project Name**: <%= projectName %>
7
+ You are an expert working on **<%= projectName %>**.
8
+ **Project Goal**: [Replace this with your business logic, e.g., E-commerce API]
9
+ *(Keep this goal in mind when writing business logic, proposing data schemas, or considering edge cases like security and performance.)*
10
+
11
+ ## Tech Stack
12
+ - **Language**: <%= language %>
13
+ - **Architecture**: <%= architecture %>
14
+ - **Database**: <%= database %>
15
+ - **Communication Protocol**: <%= communication %>
16
+ <% if (caching !== 'None') { -%>
17
+ - **Caching**: <%= caching %>
18
+ <% } -%>
19
+
20
+ ## High-Level Architecture
21
+ <% if (architecture === 'Clean Architecture') { -%>
22
+ We use Clean Architecture. The project separates concerns into:
23
+ - `src/domain`: Core entities and rules. No external dependencies.
24
+ - `src/usecases`: Application business logic.
25
+ - `src/interfaces`: Adapters (Controllers, Routes) that mediate between the outside world and use cases.
26
+ - `src/infrastructure`: External tools (Database, Web Server, Config, Caching).
27
+ <% } else { -%>
28
+ We use the MVC (Model-View-Controller) pattern.
29
+ - `src/models`: Database schemas/models.
30
+ - `src/controllers`: Handling incoming requests and implementing business logic.
31
+ - `src/routes`: API endpoints mapped to controllers.
32
+ <% } -%>
33
+
34
+ ## Core Standards
35
+ 1. **Testing**: We enforce > 70% coverage. Tests use Jest and the AAA (Arrange, Act, Assert) pattern.
36
+ 2. **Error Handling**: We use centralized custom errors (e.g., `ApiError`) and global error middleware. Status codes come from standard constants, not hardcoded numbers.
37
+ 3. **Paths & Naming**:
38
+ <% if (language === 'TypeScript') { -%>
39
+ - We use `@/` path aliases for internal imports.
40
+ <% } -%>
41
+ - Files are mostly `camelCase`.
42
+
43
+ Please acknowledge you understand this context by saying "Context loaded successfully! How can I help you build the <%= projectName %>?"
@@ -0,0 +1,28 @@
1
+ # Troubleshoot Error
2
+
3
+ I am encountering an error in the <%= projectName %> application. Please help me diagnose and fix it based on our architectural standards.
4
+
5
+ ## The Error Log / Issue Description
6
+ \`\`\`
7
+ [PASTE YOUR ERROR LOG OR DESCRIBE THE ISSUE HERE]
8
+ \`\`\`
9
+
10
+ ## Context Variables
11
+ - **Architecture**: <%= architecture %>
12
+ - **Language**: <%= language %>
13
+
14
+ ## Guidelines for Fixing
15
+
16
+ When analyzing this error, please keep these project standards in mind:
17
+
18
+ 1. **Centralized Error Handling**:
19
+ - Ensure the error uses the standard custom error classes from `src/errors/` (e.g., `ApiError`, `NotFoundError`, `BadRequestError`).
20
+ - If an error occurs in a controller, it should be passed to the global error middleware via `throw` (for async handlers, or `next(error)` in MVC).
21
+ 2. **Standard Status Codes**:
22
+ - Verify that appropriate status codes from `httpCodes` are being used correctly, rather than generic 500s unless unexpected.
23
+ 3. **Dependencies**:
24
+ - Check if this is a connection issue (e.g., Database, Kafka, Redis) and see if our standard configuration or health checks provide hints.
25
+ 4. **Fix Suggestion**:
26
+ - Explain *why* the error happened.
27
+ - Provide a targeted code fix matching our coding style (<%= language %>, <%= architecture %>).
28
+ - Only modify what is strictly necessary to solve the issue.
@@ -0,0 +1,160 @@
1
+ <%_
2
+ let loggerPath = '@/utils/logger';
3
+ let dbPath = '@/config/database';
4
+ let redisPath = '@/config/redisClient';
5
+ let kafkaPath = '@/services/kafkaService';
6
+
7
+ if (architecture === 'Clean Architecture') {
8
+ loggerPath = '@/infrastructure/log/logger';
9
+ dbPath = '@/infrastructure/database/database';
10
+ redisPath = '@/infrastructure/caching/redisClient';
11
+ kafkaPath = '@/infrastructure/messaging/kafkaClient';
12
+ }
13
+ _%>
14
+ const setupGracefulShutdown = require('@/utils/gracefulShutdown');
15
+
16
+ <%_ if (database === 'MongoDB') { -%>
17
+ jest.mock('mongoose', () => {
18
+ return {
19
+ connection: {
20
+ close: jest.fn().mockResolvedValue(true)
21
+ }
22
+ };
23
+ });
24
+ <%_ } else if (database !== 'None') { -%>
25
+ jest.mock('<%- dbPath %>', () => {
26
+ return {
27
+ close: jest.fn().mockResolvedValue(true)
28
+ };
29
+ });
30
+ <%_ } -%>
31
+
32
+ <%_ if (caching === 'Redis') { -%>
33
+ jest.mock('<%- redisPath %>', () => {
34
+ return {
35
+ quit: jest.fn().mockResolvedValue(true)
36
+ };
37
+ });
38
+ <%_ } -%>
39
+
40
+ <%_ if (communication === 'Kafka') { -%>
41
+ jest.mock('<%- kafkaPath %>', () => {
42
+ return {
43
+ disconnectKafka: jest.fn().mockResolvedValue(true)
44
+ };
45
+ });
46
+ <%_ } -%>
47
+
48
+ const flushPromises = () => new Promise((resolve) => setImmediate(resolve));
49
+
50
+ describe('Graceful Shutdown', () => {
51
+ let mockServer;
52
+ let mockExit;
53
+ let processListeners;
54
+
55
+ beforeEach(() => {
56
+ jest.useFakeTimers({ legacyFakeTimers: true });
57
+ jest.clearAllMocks();
58
+ processListeners = {};
59
+
60
+ mockServer = {
61
+ close: jest.fn().mockImplementation((cb) => {
62
+ if (cb) Promise.resolve().then(() => cb());
63
+ return mockServer;
64
+ })
65
+ };
66
+
67
+ mockExit = jest.spyOn(process, 'exit').mockImplementation(() => {});
68
+ jest.spyOn(process, 'on').mockImplementation((event, handler) => {
69
+ processListeners[event] = handler;
70
+ return process;
71
+ });
72
+ });
73
+
74
+ afterEach(() => {
75
+ jest.restoreAllMocks();
76
+ jest.useRealTimers();
77
+ });
78
+
79
+ it('should register SIGTERM and SIGINT events', () => {
80
+ setupGracefulShutdown(mockServer);
81
+ expect(processListeners['SIGTERM']).toBeDefined();
82
+ expect(processListeners['SIGINT']).toBeDefined();
83
+ });
84
+
85
+ it('should cleanly shutdown all connections and exit 0', async () => {
86
+ setupGracefulShutdown(mockServer);
87
+
88
+ processListeners['SIGTERM']();
89
+
90
+ await flushPromises();
91
+ await flushPromises();
92
+ await flushPromises();
93
+
94
+ expect(mockServer.close).toHaveBeenCalled();
95
+
96
+ <%_ if (database === 'MongoDB') { -%>
97
+ const mongoose = require('mongoose');
98
+ expect(mongoose.connection.close).toHaveBeenCalledWith(false);
99
+ <%_ } else if (database !== 'None') { -%>
100
+ const sequelize = require('<%- dbPath %>');
101
+ expect(sequelize.close).toHaveBeenCalled();
102
+ <%_ } -%>
103
+
104
+ <%_ if (caching === 'Redis') { -%>
105
+ const redisService = require('<%- redisPath %>');
106
+ expect(redisService.quit).toHaveBeenCalled();
107
+ <%_ } -%>
108
+
109
+ <%_ if (communication === 'Kafka') { -%>
110
+ const { disconnectKafka } = require('<%- kafkaPath %>');
111
+ expect(disconnectKafka).toHaveBeenCalled();
112
+ <%_ } -%>
113
+
114
+ expect(mockExit).toHaveBeenCalledWith(0);
115
+ });
116
+
117
+ it('should exit 0 on SIGINT', async () => {
118
+ setupGracefulShutdown(mockServer);
119
+ processListeners['SIGINT']();
120
+ await flushPromises();
121
+ await flushPromises();
122
+ expect(mockExit).toHaveBeenCalledWith(0);
123
+ });
124
+
125
+ <%_ if (communication === 'Kafka' || database !== 'None' || caching === 'Redis') { _%>
126
+ it('should handle errors during shutdown and exit 1', async () => {
127
+ <%_ if (communication === 'Kafka') { _%>
128
+ const { disconnectKafka } = require('<%- kafkaPath %>');
129
+ disconnectKafka.mockRejectedValueOnce(new Error('Shutdown Error'));
130
+ <%_ } else if (database === 'MongoDB') { _%>
131
+ const mongoose = require('mongoose');
132
+ mongoose.connection.close = jest.fn().mockRejectedValueOnce(new Error('Shutdown Error'));
133
+ <%_ } else if (database !== 'None') { _%>
134
+ const sequelize = require('<%- dbPath %>');
135
+ sequelize.close = jest.fn().mockRejectedValueOnce(new Error('Shutdown Error'));
136
+ <%_ } else if (caching === 'Redis') { _%>
137
+ const redisService = require('<%- redisPath %>');
138
+ redisService.quit = jest.fn().mockRejectedValueOnce(new Error('Shutdown Error'));
139
+ <%_ } _%>
140
+
141
+ setupGracefulShutdown(mockServer);
142
+ processListeners['SIGTERM']();
143
+
144
+ await flushPromises();
145
+ await flushPromises();
146
+ await flushPromises();
147
+
148
+ expect(mockExit).toHaveBeenCalledWith(1);
149
+ });
150
+
151
+ it('should forcefully shutdown if cleanup takes too long', async () => {
152
+ setupGracefulShutdown(mockServer);
153
+ processListeners['SIGTERM']();
154
+
155
+ jest.advanceTimersByTime(15000);
156
+
157
+ expect(mockExit).toHaveBeenCalledWith(1);
158
+ });
159
+ <%_ } _%>
160
+ });
@@ -0,0 +1,158 @@
1
+ import { setupGracefulShutdown } from '@/utils/gracefulShutdown';
2
+ import { Server } from 'http';
3
+ <%_ if (database === 'MongoDB') { -%>
4
+ import mongoose from 'mongoose';
5
+ <%_ } else if (database !== 'None') { -%>
6
+ import sequelize from '<% if (architecture === "MVC") { %>@/config/database<% } else { %>@/infrastructure/database/database<% } %>';
7
+ <%_ } -%>
8
+ <%_ if (caching === 'Redis') { -%>
9
+ import redisService from '<% if (architecture === "MVC") { %>@/config/redisClient<% } else { %>@/infrastructure/caching/redisClient<% } %>';
10
+ <%_ } -%>
11
+
12
+ <%_ if (database === 'MongoDB') { -%>
13
+ jest.mock('mongoose', () => {
14
+ return {
15
+ __esModule: true,
16
+ default: {
17
+ connection: {
18
+ close: jest.fn().mockResolvedValue(true)
19
+ }
20
+ }
21
+ };
22
+ });
23
+ <%_ } else if (database !== 'None') { -%>
24
+ jest.mock('<% if (architecture === "MVC") { %>@/config/database<% } else { %>@/infrastructure/database/database<% } %>', () => {
25
+ return {
26
+ __esModule: true,
27
+ default: {
28
+ close: jest.fn().mockResolvedValue(true)
29
+ }
30
+ };
31
+ });
32
+ <%_ } -%>
33
+
34
+ <%_ if (caching === 'Redis') { -%>
35
+ jest.mock('<% if (architecture === "MVC") { %>@/config/redisClient<% } else { %>@/infrastructure/caching/redisClient<% } %>', () => {
36
+ return {
37
+ __esModule: true,
38
+ default: {
39
+ quit: jest.fn().mockResolvedValue(true)
40
+ }
41
+ };
42
+ });
43
+ <%_ } -%>
44
+
45
+ const flushPromises = () => new Promise((resolve) => setImmediate(resolve));
46
+
47
+ describe('Graceful Shutdown', () => {
48
+ let mockServer: Partial<Server>;
49
+ let mockExit: jest.SpyInstance;
50
+ let processListeners: Record<string, (...args: any[]) => void>;
51
+ <%_ if (communication === 'Kafka') { -%>
52
+ let mockKafkaService: { disconnect: jest.Mock };
53
+ <%_ } -%>
54
+
55
+ beforeEach(() => {
56
+ jest.useFakeTimers({ legacyFakeTimers: true });
57
+ jest.clearAllMocks();
58
+ processListeners = {};
59
+
60
+ mockServer = {
61
+ close: jest.fn().mockImplementation((cb?: (err?: Error) => void) => {
62
+ if (cb) Promise.resolve().then(() => cb());
63
+ return mockServer;
64
+ })
65
+ };
66
+
67
+ mockExit = jest.spyOn(process, 'exit').mockImplementation((() => {}) as any);
68
+ jest.spyOn(process, 'on').mockImplementation(((event: string, handler: (...args: any[]) => void) => {
69
+ processListeners[event] = handler;
70
+ return process;
71
+ }) as any);
72
+
73
+ <%_ if (communication === 'Kafka') { -%>
74
+ mockKafkaService = { disconnect: jest.fn().mockResolvedValue(true) };
75
+ <%_ } -%>
76
+ });
77
+
78
+ afterEach(() => {
79
+ jest.restoreAllMocks();
80
+ jest.useRealTimers();
81
+ });
82
+
83
+ it('should register SIGTERM and SIGINT events', () => {
84
+ setupGracefulShutdown(mockServer as Server<% if (communication === 'Kafka') { %>, mockKafkaService<% } %>);
85
+ expect(processListeners['SIGTERM']).toBeDefined();
86
+ expect(processListeners['SIGINT']).toBeDefined();
87
+ });
88
+
89
+ it('should cleanly shutdown all connections and exit 0 on SIGTERM', async () => {
90
+ setupGracefulShutdown(mockServer as Server<% if (communication === 'Kafka') { %>, mockKafkaService<% } %>);
91
+
92
+ processListeners['SIGTERM']();
93
+
94
+ // Flush microtask queue multiple times for nested async operations
95
+ await flushPromises();
96
+ await flushPromises();
97
+ await flushPromises();
98
+
99
+ expect(mockServer.close).toHaveBeenCalled();
100
+
101
+ <%_ if (database === 'MongoDB') { -%>
102
+ expect(mongoose.connection.close).toHaveBeenCalledWith(false);
103
+ <%_ } else if (database !== 'None') { -%>
104
+ expect(sequelize.close).toHaveBeenCalled();
105
+ <%_ } -%>
106
+
107
+ <%_ if (caching === 'Redis') { -%>
108
+ expect(redisService.quit).toHaveBeenCalled();
109
+ <%_ } -%>
110
+
111
+ <%_ if (communication === 'Kafka') { -%>
112
+ expect(mockKafkaService.disconnect).toHaveBeenCalled();
113
+ <%_ } -%>
114
+
115
+ expect(mockExit).toHaveBeenCalledWith(0);
116
+ });
117
+
118
+ it('should exit 0 on SIGINT', async () => {
119
+ setupGracefulShutdown(mockServer as Server<% if (communication === 'Kafka') { %>, mockKafkaService<% } %>);
120
+ processListeners['SIGINT']();
121
+ await flushPromises();
122
+ await flushPromises();
123
+ expect(mockExit).toHaveBeenCalledWith(0);
124
+ });
125
+
126
+ <%_ if (communication === 'Kafka' || database !== 'None' || caching === 'Redis') { _%>
127
+ it('should handle errors during shutdown and exit 1', async () => {
128
+ <%_ if (communication === 'Kafka') { _%>
129
+ mockKafkaService.disconnect.mockRejectedValueOnce(new Error('Shutdown Error'));
130
+ <%_ } else if (database === 'MongoDB') { _%>
131
+ (mongoose.connection.close as jest.Mock).mockRejectedValueOnce(new Error('Shutdown Error'));
132
+ <%_ } else if (database !== 'None') { _%>
133
+ (sequelize.close as jest.Mock).mockRejectedValueOnce(new Error('Shutdown Error'));
134
+ <%_ } else if (caching === 'Redis') { _%>
135
+ (redisService.quit as jest.Mock).mockRejectedValueOnce(new Error('Shutdown Error'));
136
+ <%_ } _%>
137
+
138
+ setupGracefulShutdown(mockServer as Server<% if (communication === 'Kafka') { %>, mockKafkaService<% } %>);
139
+ processListeners['SIGTERM']();
140
+
141
+ await flushPromises();
142
+ await flushPromises();
143
+ await flushPromises();
144
+
145
+ expect(mockExit).toHaveBeenCalledWith(1);
146
+ });
147
+
148
+ it('should forcefully shutdown if cleanup takes too long', async () => {
149
+ setupGracefulShutdown(mockServer as Server<% if (communication === 'Kafka') { %>, mockKafkaService<% } %>);
150
+ processListeners['SIGTERM']();
151
+
152
+ jest.advanceTimersByTime(15000);
153
+
154
+ expect(mockExit).toHaveBeenCalledWith(1);
155
+ });
156
+ <%_ } _%>
157
+
158
+ });
@@ -0,0 +1,79 @@
1
+ const { errorMiddleware } = require('<% if (architecture === "MVC") { %>@/utils/errorMiddleware<% } else { %>@/infrastructure/webserver/middleware/errorMiddleware<% } %>');
2
+ const { ApiError } = require('@/errors/ApiError');
3
+ const HTTP_STATUS = require('@/utils/httpCodes');
4
+ const logger = require('<% if (architecture === "MVC") { %>@/utils/logger<% } else { %>@/infrastructure/log/logger<% } %>');
5
+
6
+ jest.mock('<% if (architecture === "MVC") { %>@/utils/logger<% } else { %>@/infrastructure/log/logger<% } %>');
7
+
8
+ describe('Error Middleware', () => {
9
+ let mockRequest;
10
+ let mockResponse;
11
+ let nextFunction;
12
+ const originalEnv = process.env.NODE_ENV;
13
+
14
+ beforeEach(() => {
15
+ mockRequest = {
16
+ originalUrl: '/test',
17
+ method: 'GET',
18
+ ip: '127.0.0.1'
19
+ };
20
+ mockResponse = {
21
+ status: jest.fn().mockReturnThis(),
22
+ json: jest.fn()
23
+ };
24
+ nextFunction = jest.fn();
25
+ jest.clearAllMocks();
26
+ });
27
+
28
+ afterEach(() => {
29
+ process.env.NODE_ENV = originalEnv;
30
+ });
31
+
32
+ it('should handle standard Error by wrapping it in a 500 ApiError', () => {
33
+ const error = new Error('Standard Error');
34
+ errorMiddleware(error, mockRequest, mockResponse, nextFunction);
35
+
36
+ expect(logger.error).toHaveBeenCalled();
37
+ expect(mockResponse.status).toHaveBeenCalledWith(HTTP_STATUS.INTERNAL_SERVER_ERROR);
38
+ });
39
+
40
+ it('should handle custom ApiError directly', () => {
41
+ const customError = new ApiError(HTTP_STATUS.BAD_REQUEST, 'Bad Request Data', true);
42
+ errorMiddleware(customError, mockRequest, mockResponse, nextFunction);
43
+
44
+ expect(mockResponse.status).toHaveBeenCalledWith(HTTP_STATUS.BAD_REQUEST);
45
+ expect(mockResponse.json).toHaveBeenCalledWith(expect.objectContaining({
46
+ statusCode: HTTP_STATUS.BAD_REQUEST,
47
+ message: 'Bad Request Data'
48
+ }));
49
+ });
50
+
51
+ it('should include stack trace in development environment', () => {
52
+ process.env.NODE_ENV = 'development';
53
+ const error = new Error('Test Error');
54
+ errorMiddleware(error, mockRequest, mockResponse, nextFunction);
55
+
56
+ expect(mockResponse.json).toHaveBeenCalledWith(expect.objectContaining({
57
+ stack: expect.any(String)
58
+ }));
59
+ });
60
+
61
+ it('should omit stack trace in production environment', () => {
62
+ process.env.NODE_ENV = 'production';
63
+ const error = new Error('Test Error');
64
+ errorMiddleware(error, mockRequest, mockResponse, nextFunction);
65
+
66
+ const jsonArg = mockResponse.json.mock.calls[0][0];
67
+ expect(jsonArg.stack).toBeUndefined();
68
+ });
69
+
70
+ it('should handle error without stack trace', () => {
71
+ const { ApiError } = require('@/errors/ApiError');
72
+ const customError = new ApiError(500, 'No Stack', false);
73
+ delete customError.stack;
74
+ errorMiddleware(customError, mockRequest, mockResponse, nextFunction);
75
+
76
+ expect(logger.error).toHaveBeenCalledWith(expect.stringContaining('No Stack'));
77
+ expect(logger.error).toHaveBeenCalledWith('No stack trace');
78
+ });
79
+ });
@@ -0,0 +1,94 @@
1
+ import { errorMiddleware } from '<% if (architecture === "MVC" || language === "TypeScript") { %>@/utils/errorMiddleware<% } else { %>@/infrastructure/webserver/middleware/errorMiddleware<% } %>';
2
+ import { Request, Response } from 'express';
3
+ import { ApiError } from '@/errors/ApiError';
4
+ import { HTTP_STATUS } from '@/utils/httpCodes';
5
+ import logger from '<% if (architecture === "MVC") { %>@/utils/logger<% } else { %>@/infrastructure/log/logger<% } %>';
6
+
7
+ jest.mock('<% if (architecture === "MVC") { %>@/utils/logger<% } else { %>@/infrastructure/log/logger<% } %>');
8
+
9
+ describe('Error Middleware', () => {
10
+ let mockRequest: Partial<Request>;
11
+ let mockResponse: Partial<Response>;
12
+ let nextFunction: jest.Mock;
13
+ const originalEnv = process.env.NODE_ENV;
14
+
15
+ beforeEach(() => {
16
+ mockRequest = {
17
+ originalUrl: '/test',
18
+ method: 'GET',
19
+ ip: '127.0.0.1'
20
+ };
21
+ mockResponse = {
22
+ status: jest.fn().mockReturnThis(),
23
+ json: jest.fn()
24
+ };
25
+ nextFunction = jest.fn();
26
+ jest.clearAllMocks();
27
+ });
28
+
29
+ afterEach(() => {
30
+ process.env.NODE_ENV = originalEnv;
31
+ });
32
+
33
+ it('should handle standard Error by wrapping it in a 500 ApiError', () => {
34
+ const error = new Error('Standard Error');
35
+ errorMiddleware(error, mockRequest as Request, mockResponse as Response, nextFunction);
36
+
37
+ expect(logger.error).toHaveBeenCalled();
38
+ expect(mockResponse.status).toHaveBeenCalledWith(HTTP_STATUS.INTERNAL_SERVER_ERROR);
39
+ expect(mockResponse.json).toHaveBeenCalledWith(expect.objectContaining({
40
+ statusCode: HTTP_STATUS.INTERNAL_SERVER_ERROR,
41
+ message: 'Standard Error'
42
+ }));
43
+ });
44
+
45
+ it('should default to Internal Server Error message if none provided on Error', () => {
46
+ const error = new Error();
47
+ errorMiddleware(error, mockRequest as Request, mockResponse as Response, nextFunction);
48
+
49
+ expect(mockResponse.status).toHaveBeenCalledWith(HTTP_STATUS.INTERNAL_SERVER_ERROR);
50
+ expect(mockResponse.json).toHaveBeenCalledWith(expect.objectContaining({
51
+ message: 'Internal Server Error'
52
+ }));
53
+ });
54
+
55
+ it('should handle custom ApiError directly', () => {
56
+ const customError = new ApiError(HTTP_STATUS.BAD_REQUEST, 'Bad Request Data', true);
57
+ errorMiddleware(customError, mockRequest as Request, mockResponse as Response, nextFunction);
58
+
59
+ expect(logger.error).not.toHaveBeenCalled();
60
+ expect(mockResponse.status).toHaveBeenCalledWith(HTTP_STATUS.BAD_REQUEST);
61
+ expect(mockResponse.json).toHaveBeenCalledWith(expect.objectContaining({
62
+ statusCode: HTTP_STATUS.BAD_REQUEST,
63
+ message: 'Bad Request Data'
64
+ }));
65
+ });
66
+
67
+ it('should include stack trace in development environment', () => {
68
+ process.env.NODE_ENV = 'development';
69
+ const error = new Error('Test Error');
70
+ errorMiddleware(error, mockRequest as Request, mockResponse as Response, nextFunction);
71
+
72
+ expect(mockResponse.json).toHaveBeenCalledWith(expect.objectContaining({
73
+ stack: expect.any(String)
74
+ }));
75
+ });
76
+
77
+ it('should omit stack trace in production environment', () => {
78
+ process.env.NODE_ENV = 'production';
79
+ const error = new Error('Test Error');
80
+ errorMiddleware(error, mockRequest as Request, mockResponse as Response, nextFunction);
81
+
82
+ const jsonArg = (mockResponse.json as jest.Mock).mock.calls[0][0];
83
+ expect(jsonArg.stack).toBeUndefined();
84
+ });
85
+
86
+ it('should handle error without stack trace', () => {
87
+ const customError = new ApiError(HTTP_STATUS.INTERNAL_SERVER_ERROR, 'No Stack', false);
88
+ delete customError.stack;
89
+ errorMiddleware(customError, mockRequest as Request, mockResponse as Response, nextFunction);
90
+
91
+ expect(logger.error).toHaveBeenCalledWith(expect.stringContaining('No Stack'));
92
+ expect(logger.error).toHaveBeenCalledWith('No stack trace');
93
+ });
94
+ });
@@ -18,6 +18,6 @@
18
18
  ],
19
19
  "exclude": [
20
20
  "node_modules",
21
- "**/*.test.ts"
21
+ "dist"
22
22
  ]
23
23
  }