nodejs-quickstart-structure 1.18.0 → 1.19.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/CHANGELOG.md +17 -4
- package/README.md +2 -1
- package/bin/index.js +93 -92
- package/lib/generator.js +1 -1
- package/lib/modules/caching-setup.js +76 -73
- package/lib/modules/config-files.js +4 -0
- package/lib/modules/kafka-setup.js +249 -191
- package/lib/modules/project-setup.js +1 -0
- package/package.json +13 -2
- package/templates/clean-architecture/js/src/errors/BadRequestError.js +11 -10
- package/templates/clean-architecture/js/src/errors/BadRequestError.spec.js.ejs +22 -21
- package/templates/clean-architecture/js/src/errors/NotFoundError.js +11 -10
- package/templates/clean-architecture/js/src/errors/NotFoundError.spec.js.ejs +22 -21
- package/templates/clean-architecture/js/src/infrastructure/repositories/UserRepository.js.ejs +69 -39
- package/templates/clean-architecture/js/src/infrastructure/repositories/UserRepository.spec.js.ejs +142 -81
- package/templates/clean-architecture/js/src/infrastructure/webserver/server.js.ejs +1 -1
- package/templates/clean-architecture/js/src/interfaces/controllers/userController.js.ejs +156 -75
- package/templates/clean-architecture/js/src/interfaces/controllers/userController.spec.js.ejs +234 -138
- package/templates/clean-architecture/js/src/interfaces/graphql/resolvers/user.resolvers.js.ejs +27 -21
- package/templates/clean-architecture/js/src/interfaces/graphql/resolvers/user.resolvers.spec.js.ejs +66 -49
- package/templates/clean-architecture/js/src/interfaces/graphql/typeDefs/user.types.js.ejs +19 -17
- package/templates/clean-architecture/js/src/interfaces/routes/api.js +12 -10
- package/templates/clean-architecture/js/src/usecases/DeleteUser.js +11 -0
- package/templates/clean-architecture/js/src/usecases/DeleteUser.spec.js.ejs +47 -0
- package/templates/clean-architecture/js/src/usecases/UpdateUser.js +11 -0
- package/templates/clean-architecture/js/src/usecases/UpdateUser.spec.js.ejs +48 -0
- package/templates/clean-architecture/js/src/utils/errorMessages.js +14 -0
- package/templates/clean-architecture/ts/src/errors/BadRequestError.spec.ts.ejs +22 -21
- package/templates/clean-architecture/ts/src/errors/BadRequestError.ts +9 -8
- package/templates/clean-architecture/ts/src/errors/NotFoundError.spec.ts.ejs +22 -21
- package/templates/clean-architecture/ts/src/errors/NotFoundError.ts +9 -8
- package/templates/clean-architecture/ts/src/index.ts.ejs +1 -1
- package/templates/clean-architecture/ts/src/infrastructure/repositories/UserRepository.spec.ts.ejs +175 -85
- package/templates/clean-architecture/ts/src/infrastructure/repositories/userRepository.ts.ejs +74 -0
- package/templates/clean-architecture/ts/src/interfaces/controllers/userController.spec.ts.ejs +331 -185
- package/templates/clean-architecture/ts/src/interfaces/controllers/userController.ts.ejs +173 -84
- package/templates/clean-architecture/ts/src/interfaces/graphql/resolvers/user.resolvers.spec.ts.ejs +68 -51
- package/templates/clean-architecture/ts/src/interfaces/graphql/resolvers/user.resolvers.ts.ejs +29 -21
- package/templates/clean-architecture/ts/src/interfaces/graphql/typeDefs/user.types.ts.ejs +17 -15
- package/templates/clean-architecture/ts/src/interfaces/routes/userRoutes.ts +13 -11
- package/templates/clean-architecture/ts/src/usecases/deleteUser.spec.ts.ejs +47 -0
- package/templates/clean-architecture/ts/src/usecases/deleteUser.ts +9 -0
- package/templates/clean-architecture/ts/src/usecases/updateUser.spec.ts.ejs +48 -0
- package/templates/clean-architecture/ts/src/usecases/updateUser.ts +9 -0
- package/templates/clean-architecture/ts/src/utils/errorMessages.ts +12 -0
- package/templates/common/.gitattributes +46 -0
- package/templates/common/.snyk.ejs +45 -0
- package/templates/common/Dockerfile +17 -9
- package/templates/common/README.md.ejs +295 -263
- package/templates/common/caching/clean/js/DeleteUser.js.ejs +27 -0
- package/templates/common/caching/clean/js/UpdateUser.js.ejs +27 -0
- package/templates/common/caching/clean/ts/deleteUser.ts.ejs +24 -0
- package/templates/common/caching/clean/ts/updateUser.ts.ejs +25 -0
- package/templates/common/caching/ts/memoryCache.ts.ejs +73 -64
- package/templates/common/caching/ts/redisClient.ts.ejs +89 -80
- package/templates/common/database/js/models/User.js.ejs +79 -53
- package/templates/common/database/js/models/User.js.mongoose.ejs +23 -19
- package/templates/common/database/js/models/User.spec.js.ejs +94 -84
- package/templates/common/database/ts/models/User.spec.ts.ejs +100 -84
- package/templates/common/database/ts/models/User.ts.ejs +87 -61
- package/templates/common/database/ts/models/User.ts.mongoose.ejs +30 -25
- package/templates/common/health/js/healthRoute.js.ejs +50 -47
- package/templates/common/health/ts/healthRoute.ts.ejs +49 -46
- package/templates/common/jest.e2e.config.js.ejs +8 -8
- package/templates/common/kafka/js/messaging/baseConsumer.js.ejs +30 -30
- package/templates/common/kafka/js/messaging/userEventSchema.js.ejs +12 -11
- package/templates/common/kafka/js/messaging/welcomeEmailConsumer.js.ejs +44 -31
- package/templates/common/kafka/js/messaging/welcomeEmailConsumer.spec.js.ejs +86 -49
- package/templates/common/kafka/js/services/kafkaService.js.ejs +93 -93
- package/templates/common/kafka/js/utils/kafkaEvents.js.ejs +7 -0
- package/templates/common/kafka/ts/messaging/userEventSchema.spec.ts.ejs +51 -51
- package/templates/common/kafka/ts/messaging/userEventSchema.ts.ejs +12 -11
- package/templates/common/kafka/ts/messaging/welcomeEmailConsumer.spec.ts.ejs +86 -49
- package/templates/common/kafka/ts/messaging/welcomeEmailConsumer.ts.ejs +38 -25
- package/templates/common/kafka/ts/services/kafkaService.ts.ejs +95 -95
- package/templates/common/kafka/ts/utils/kafkaEvents.ts.ejs +5 -0
- package/templates/common/package.json.ejs +10 -2
- package/templates/common/shutdown/js/gracefulShutdown.js.ejs +65 -61
- package/templates/common/shutdown/js/gracefulShutdown.spec.js.ejs +149 -160
- package/templates/common/shutdown/ts/gracefulShutdown.spec.ts.ejs +179 -158
- package/templates/common/shutdown/ts/gracefulShutdown.ts.ejs +59 -55
- package/templates/common/src/tests/e2e/e2e.users.test.js.ejs +120 -49
- package/templates/common/src/tests/e2e/e2e.users.test.ts.ejs +120 -49
- package/templates/common/swagger.yml.ejs +118 -66
- package/templates/db/mysql/V1__Initial_Setup.sql.ejs +10 -9
- package/templates/db/postgres/V1__Initial_Setup.sql.ejs +10 -9
- package/templates/mvc/js/src/controllers/userController.js.ejs +246 -105
- package/templates/mvc/js/src/controllers/userController.spec.js.ejs +481 -209
- package/templates/mvc/js/src/errors/BadRequestError.js +11 -10
- package/templates/mvc/js/src/errors/BadRequestError.spec.js.ejs +22 -21
- package/templates/mvc/js/src/errors/NotFoundError.js +11 -10
- package/templates/mvc/js/src/errors/NotFoundError.spec.js.ejs +22 -21
- package/templates/mvc/js/src/graphql/resolvers/user.resolvers.js.ejs +25 -19
- package/templates/mvc/js/src/graphql/resolvers/user.resolvers.spec.js.ejs +64 -47
- package/templates/mvc/js/src/graphql/typeDefs/user.types.js.ejs +19 -17
- package/templates/mvc/js/src/index.js.ejs +1 -1
- package/templates/mvc/js/src/routes/api.js +10 -8
- package/templates/mvc/js/src/routes/api.spec.js.ejs +41 -36
- package/templates/mvc/js/src/utils/errorMessages.js +14 -0
- package/templates/mvc/ts/src/controllers/userController.spec.ts.ejs +481 -203
- package/templates/mvc/ts/src/controllers/userController.ts.ejs +248 -107
- package/templates/mvc/ts/src/errors/BadRequestError.spec.ts.ejs +22 -21
- package/templates/mvc/ts/src/errors/BadRequestError.ts +9 -8
- package/templates/mvc/ts/src/errors/NotFoundError.spec.ts.ejs +27 -21
- package/templates/mvc/ts/src/errors/NotFoundError.ts +9 -8
- package/templates/mvc/ts/src/graphql/resolvers/user.resolvers.spec.ts.ejs +68 -51
- package/templates/mvc/ts/src/graphql/resolvers/user.resolvers.ts.ejs +29 -21
- package/templates/mvc/ts/src/graphql/typeDefs/user.types.ts.ejs +17 -15
- package/templates/mvc/ts/src/index.ts.ejs +156 -153
- package/templates/mvc/ts/src/routes/api.spec.ts.ejs +59 -40
- package/templates/mvc/ts/src/routes/api.ts +12 -10
- package/templates/mvc/ts/src/utils/errorMessages.ts +12 -0
- package/templates/clean-architecture/ts/src/infrastructure/repositories/UserRepository.ts.ejs +0 -37
|
@@ -1,160 +1,149 @@
|
|
|
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
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
-
});
|
|
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
|
+
it('should handle errors during shutdown and exit 1', async () => {
|
|
126
|
+
// Force server.close to fail to test the error path
|
|
127
|
+
mockServer.close.mockImplementationOnce((cb) => {
|
|
128
|
+
if (cb) Promise.resolve().then(() => cb(new Error('Server Close Error')));
|
|
129
|
+
return mockServer;
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
setupGracefulShutdown(mockServer);
|
|
133
|
+
processListeners['SIGTERM']();
|
|
134
|
+
|
|
135
|
+
await flushPromises();
|
|
136
|
+
await flushPromises();
|
|
137
|
+
|
|
138
|
+
expect(mockExit).toHaveBeenCalledWith(1);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('should forcefully shutdown if cleanup takes too long', async () => {
|
|
142
|
+
setupGracefulShutdown(mockServer);
|
|
143
|
+
processListeners['SIGTERM']();
|
|
144
|
+
|
|
145
|
+
jest.advanceTimersByTime(15000);
|
|
146
|
+
|
|
147
|
+
expect(mockExit).toHaveBeenCalledWith(1);
|
|
148
|
+
});
|
|
149
|
+
});
|
|
@@ -1,158 +1,179 @@
|
|
|
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
|
-
<%_
|
|
131
|
-
(mongoose.connection.close as jest.Mock).
|
|
132
|
-
<%_ } else if (database !== 'None') {
|
|
133
|
-
(sequelize.close as jest.Mock).
|
|
134
|
-
<%_ }
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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
|
+
<%_ if (database === 'MongoDB') { -%>
|
|
131
|
+
(mongoose.connection.close as jest.Mock).mockResolvedValueOnce(true);
|
|
132
|
+
<%_ } else if (database !== 'None') { -%>
|
|
133
|
+
(sequelize.close as jest.Mock).mockResolvedValueOnce(true);
|
|
134
|
+
<%_ } -%>
|
|
135
|
+
<%_ } else if (database === 'MongoDB') { -%>
|
|
136
|
+
(mongoose.connection.close as jest.Mock).mockRejectedValueOnce(new Error('Shutdown Error'));
|
|
137
|
+
<%_ } else if (database !== 'None') { -%>
|
|
138
|
+
(sequelize.close as jest.Mock).mockRejectedValueOnce(new Error('Shutdown Error'));
|
|
139
|
+
<%_ } else if (caching === 'Redis') { -%>
|
|
140
|
+
(redisService.quit as jest.Mock).mockRejectedValueOnce(new Error('Shutdown Error'));
|
|
141
|
+
<%_ } -%>
|
|
142
|
+
|
|
143
|
+
setupGracefulShutdown(mockServer as Server<% if (communication === 'Kafka') { %>, mockKafkaService<% } %>);
|
|
144
|
+
processListeners['SIGTERM']();
|
|
145
|
+
|
|
146
|
+
await flushPromises();
|
|
147
|
+
await flushPromises();
|
|
148
|
+
await flushPromises();
|
|
149
|
+
|
|
150
|
+
expect(mockExit).toHaveBeenCalledWith(1);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('should handle server close errors', async () => {
|
|
154
|
+
const serverError = new Error('Server Close Error');
|
|
155
|
+
mockServer.close = jest.fn().mockImplementation((cb?: (err?: Error) => void) => {
|
|
156
|
+
if (cb) Promise.resolve().then(() => cb(serverError));
|
|
157
|
+
return mockServer;
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
setupGracefulShutdown(mockServer as Server<% if (communication === 'Kafka') { %>, mockKafkaService<% } %>);
|
|
161
|
+
processListeners['SIGTERM']();
|
|
162
|
+
|
|
163
|
+
await flushPromises();
|
|
164
|
+
// Since it's inside server.close callback, we need to wait
|
|
165
|
+
await flushPromises();
|
|
166
|
+
|
|
167
|
+
expect(mockExit).toHaveBeenCalledWith(1);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('should forcefully shutdown if cleanup takes too long', async () => {
|
|
171
|
+
setupGracefulShutdown(mockServer as Server<% if (communication === 'Kafka') { %>, mockKafkaService<% } %>);
|
|
172
|
+
processListeners['SIGTERM']();
|
|
173
|
+
|
|
174
|
+
jest.advanceTimersByTime(15000);
|
|
175
|
+
|
|
176
|
+
expect(mockExit).toHaveBeenCalledWith(1);
|
|
177
|
+
});
|
|
178
|
+
<%_ } -%>
|
|
179
|
+
});
|