nodejs-quickstart-structure 2.1.2 → 2.2.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 (70) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +12 -17
  3. package/bin/index.js +1 -0
  4. package/lib/generator.js +1 -1
  5. package/lib/modules/app-setup.js +16 -0
  6. package/lib/modules/auth-setup.js +46 -4
  7. package/lib/prompts.js +49 -5
  8. package/package.json +1 -1
  9. package/templates/clean-architecture/js/src/infrastructure/config/env.js.ejs +12 -2
  10. package/templates/clean-architecture/js/src/infrastructure/repositories/UserRepository.js.ejs +27 -0
  11. package/templates/clean-architecture/js/src/infrastructure/repositories/UserRepository.spec.js.ejs +24 -0
  12. package/templates/clean-architecture/js/src/infrastructure/webserver/server.js.ejs +5 -1
  13. package/templates/clean-architecture/ts/src/config/env.ts.ejs +12 -2
  14. package/templates/clean-architecture/ts/src/domain/user.ts.ejs +14 -0
  15. package/templates/clean-architecture/ts/src/index.ts.ejs +2 -0
  16. package/templates/clean-architecture/ts/src/infrastructure/repositories/UserRepository.spec.ts.ejs +24 -0
  17. package/templates/clean-architecture/ts/src/infrastructure/repositories/userRepository.ts.ejs +43 -45
  18. package/templates/clean-architecture/ts/src/interfaces/graphql/resolvers/user.resolvers.ts.ejs +5 -5
  19. package/templates/clean-architecture/ts/src/utils/httpCodes.ts +1 -0
  20. package/templates/common/.env.example.ejs +10 -0
  21. package/templates/common/README.md.ejs +65 -14
  22. package/templates/common/auth/js/controllers/authController.js.ejs +356 -13
  23. package/templates/common/auth/js/controllers/authController.spec.js.ejs +329 -53
  24. package/templates/common/auth/js/middleware/authMiddleware.js.ejs +10 -6
  25. package/templates/common/auth/js/routes/authRoutes.js.ejs +11 -0
  26. package/templates/common/auth/js/services/jwtService.spec.js.ejs +30 -0
  27. package/templates/common/auth/js/services/socialAuthService.js.ejs +175 -0
  28. package/templates/common/auth/js/services/socialAuthService.spec.js.ejs +192 -0
  29. package/templates/common/auth/js/usecases/SocialLoginUseCase.js.ejs +114 -0
  30. package/templates/common/auth/js/usecases/SocialLoginUseCase.spec.js.ejs +143 -0
  31. package/templates/common/auth/ts/controllers/authController.spec.ts.ejs +366 -64
  32. package/templates/common/auth/ts/controllers/authController.ts.ejs +370 -9
  33. package/templates/common/auth/ts/middleware/authMiddleware.ts.ejs +10 -6
  34. package/templates/common/auth/ts/routes/authRoutes.ts.ejs +11 -0
  35. package/templates/common/auth/ts/services/jwtService.spec.ts.ejs +18 -0
  36. package/templates/common/auth/ts/services/jwtService.ts.ejs +3 -3
  37. package/templates/common/auth/ts/services/socialAuthService.spec.ts.ejs +187 -0
  38. package/templates/common/auth/ts/services/socialAuthService.ts.ejs +189 -0
  39. package/templates/common/auth/ts/usecases/SocialLoginUseCase.spec.ts.ejs +143 -0
  40. package/templates/common/auth/ts/usecases/SocialLoginUseCase.ts.ejs +117 -0
  41. package/templates/common/database/js/models/User.js.ejs +13 -5
  42. package/templates/common/database/js/models/User.js.mongoose.ejs +15 -1
  43. package/templates/common/database/ts/models/User.ts.ejs +23 -7
  44. package/templates/common/database/ts/models/User.ts.mongoose.ejs +18 -2
  45. package/templates/common/docker-compose.yml.ejs +21 -0
  46. package/templates/common/ecosystem.config.js.ejs +10 -0
  47. package/templates/common/eslint.config.mjs.ejs +4 -1
  48. package/templates/common/jest.config.js.ejs +1 -1
  49. package/templates/common/kafka/ts/services/kafkaService.ts.ejs +1 -1
  50. package/templates/common/package.json.ejs +4 -0
  51. package/templates/common/src/tests/e2e/e2e.users.test.js.ejs +13 -1
  52. package/templates/common/src/tests/e2e/e2e.users.test.ts.ejs +13 -1
  53. package/templates/common/swagger.yml.ejs +62 -3
  54. package/templates/common/views/ejs/login.ejs.ejs +84 -0
  55. package/templates/common/views/ejs/signup.ejs.ejs +84 -0
  56. package/templates/common/views/pug/login.pug.ejs +78 -0
  57. package/templates/common/views/pug/signup.pug.ejs +78 -0
  58. package/templates/db/mysql/V1__Initial_Setup.sql.ejs +3 -1
  59. package/templates/db/postgres/V1__Initial_Setup.sql.ejs +3 -1
  60. package/templates/mvc/js/src/config/env.js.ejs +12 -2
  61. package/templates/mvc/js/src/controllers/userController.js.ejs +1 -1
  62. package/templates/mvc/js/src/graphql/resolvers/user.resolvers.js.ejs +4 -3
  63. package/templates/mvc/js/src/index.js.ejs +2 -0
  64. package/templates/mvc/js/src/utils/httpCodes.js +1 -0
  65. package/templates/mvc/ts/src/config/env.ts.ejs +12 -2
  66. package/templates/mvc/ts/src/controllers/userController.ts.ejs +1 -0
  67. package/templates/mvc/ts/src/graphql/resolvers/user.resolvers.ts.ejs +5 -5
  68. package/templates/mvc/ts/src/index.ts.ejs +4 -1
  69. package/templates/mvc/ts/src/utils/httpCodes.ts +1 -0
  70. package/templates/clean-architecture/ts/src/domain/user.ts +0 -9
@@ -1,27 +1,44 @@
1
1
  <%_ if (architecture === 'Clean Architecture') { _%>
2
- const AuthController = require('@/interfaces/controllers/auth/authController');
3
- const JwtService = require('@/infrastructure/auth/jwtService');
4
- <%_ } else { _%>
5
- const AuthController = require('@/controllers/authController');
6
- const JwtService = require('@/services/jwtService');
7
- <%_ } _%>
8
- const HTTP_STATUS = require('@/utils/httpCodes');
9
- const bcrypt = require('bcryptjs');
10
-
11
- jest.mock('bcryptjs');
12
- <%_ if (architecture === 'Clean Architecture') { _%>
13
2
  jest.mock('@/infrastructure/auth/jwtService');
3
+ <%_ if (socialAuth && socialAuth.filter(a => a !== 'None').length > 0) { _%>
4
+ jest.mock('@/infrastructure/auth/socialAuthService');
5
+ jest.mock('@/usecases/auth/socialLoginUseCase');
6
+ jest.mock('@/infrastructure/repositories/UserRepository');
7
+ <%_ } _%>
14
8
  jest.mock('@/infrastructure/database/models/User', () => ({
15
- findOne: jest.fn()
9
+ findOne: jest.fn(),
10
+ create: jest.fn()
16
11
  }));
17
- const User = require('@/infrastructure/database/models/User');
18
12
  <%_ } else { _%>
19
13
  jest.mock('@/services/jwtService');
14
+ <%_ if (socialAuth && socialAuth.filter(a => a !== 'None').length > 0) { _%>
15
+ jest.mock('@/services/socialAuthService');
16
+ <%_ } _%>
20
17
  jest.mock('@/models/User', () => ({
21
- findOne: jest.fn()
18
+ findOne: jest.fn(),
19
+ create: jest.fn()
22
20
  }));
21
+ <%_ } _%>
22
+ jest.mock('bcryptjs');
23
+
24
+ <%_ if (architecture === 'Clean Architecture') { _%>
25
+ const AuthController = require('@/interfaces/controllers/auth/authController');
26
+ const JwtService = require('@/infrastructure/auth/jwtService');
27
+ <%_ if (socialAuth && socialAuth.filter(a => a !== 'None').length > 0) { _%>
28
+ const { SocialLoginUseCase } = require('@/usecases/auth/socialLoginUseCase');
29
+ <%_ } _%>
30
+ const User = require('@/infrastructure/database/models/User');
31
+ <%_ } else { _%>
32
+ const AuthController = require('@/controllers/authController');
33
+ const JwtService = require('@/services/jwtService');
34
+ <%_ if (socialAuth && socialAuth.filter(a => a !== 'None').length > 0) { _%>
35
+ const { SocialAuthService } = require('@/services/socialAuthService');
36
+ <%_ } _%>
23
37
  const User = require('@/models/User');
24
38
  <%_ } _%>
39
+ const HTTP_STATUS = require('@/utils/httpCodes');
40
+ const bcrypt = require('bcryptjs');
41
+
25
42
  <%_ if (caching !== 'None') { _%>
26
43
  <%_ const cachePath = (architecture === "MVC") ? (caching === "Redis" ? "@/config/redisClient" : "@/config/memoryCache") : (caching === "Redis" ? "@/infrastructure/caching/redisClient" : "@/infrastructure/caching/memoryCache"); _%>
27
44
  jest.mock('<%= cachePath %>', () => ({
@@ -42,11 +59,16 @@ describe('AuthController', () => {
42
59
  controller = new AuthController();
43
60
  mockReq = {
44
61
  body: {},
45
- headers: {}
62
+ headers: {},
63
+ query: {},
64
+ cookies: {}
46
65
  };
47
66
  mockRes = {
48
67
  status: jest.fn().mockReturnThis(),
49
- json: jest.fn()
68
+ json: jest.fn(),
69
+ cookie: jest.fn(),
70
+ clearCookie: jest.fn(),
71
+ redirect: jest.fn()
50
72
  };
51
73
  next = jest.fn();
52
74
  jest.clearAllMocks();
@@ -62,7 +84,6 @@ describe('AuthController', () => {
62
84
  JwtService.generateRefreshToken.mockReturnValue('mock-refresh-token');
63
85
  JwtService.decodeToken.mockReturnValue({ jti: 'test-jti' });
64
86
 
65
- // Mock cacheService
66
87
  <% if (caching !== 'None') { %>
67
88
  cacheService.get.mockResolvedValue([]);
68
89
  cacheService.set.mockResolvedValue();<% } %>
@@ -73,7 +94,7 @@ describe('AuthController', () => {
73
94
  expect(mockRes.json).toHaveBeenCalledWith({ token: 'mock-access-token', accessToken: 'mock-access-token', refreshToken: 'mock-refresh-token' });
74
95
  });
75
96
 
76
- it('should return 401 on invalid email', async () => {
97
+ it('should return 401 if user not found', async () => {
77
98
  mockReq.body = { email: 'wrong@test.com', password: 'password123' };
78
99
  User.findOne.mockResolvedValue(null);
79
100
 
@@ -82,7 +103,7 @@ describe('AuthController', () => {
82
103
  expect(mockRes.status).toHaveBeenCalledWith(HTTP_STATUS.UNAUTHORIZED);
83
104
  });
84
105
 
85
- it('should return 401 on invalid password', async () => {
106
+ it('should return 401 if password invalid', async () => {
86
107
  const user = { id: 1, email: 'test@test.com', password: 'hashedpassword' };
87
108
  mockReq.body = { email: 'test@test.com', password: 'wrongpassword' };
88
109
  User.findOne.mockResolvedValue(user);
@@ -92,57 +113,312 @@ describe('AuthController', () => {
92
113
 
93
114
  expect(mockRes.status).toHaveBeenCalledWith(HTTP_STATUS.UNAUTHORIZED);
94
115
  });
116
+
117
+ it('should call next on error', async () => {
118
+ const error = new Error('Database error');
119
+ User.findOne.mockRejectedValue(error);
120
+ mockReq.body = { email: 'test@test.com', password: 'password123' };
121
+
122
+ await controller.login(mockReq, mockRes, next);
123
+
124
+ expect(next).toHaveBeenCalledWith(error);
125
+ });
95
126
  });
96
127
 
97
- describe('refresh', () => {
98
- it('should return new tokens for a valid refresh token', async () => {
99
- mockReq.body = { refreshToken: 'valid-refresh' };
100
- const decoded = { id: '1', email: 'test@test.com', jti: 'old-jti' };
101
- JwtService.verifyRefreshToken.mockReturnValue(decoded);
102
- JwtService.generateToken.mockReturnValue('new-access');
103
- JwtService.generateRefreshToken.mockReturnValue('new-refresh');
104
- JwtService.decodeToken.mockReturnValue({ jti: 'new-jti' });
128
+ describe('refresh', () => {
129
+ it('should return 401 if refresh token is invalid', async () => {
130
+ mockReq.body = { refreshToken: 'invalid-token' };
131
+ JwtService.verifyRefreshToken.mockReturnValue(null);
105
132
 
106
- <% if (caching !== 'None') { %>
107
- cacheService.get.mockResolvedValue(['old-jti']);
108
- cacheService.set.mockResolvedValue();
133
+ await controller.refresh(mockReq, mockRes, next);
134
+
135
+ expect(mockRes.status).toHaveBeenCalledWith(HTTP_STATUS.UNAUTHORIZED);
136
+ });
137
+
138
+ it('should return new tokens if refresh token is valid', async () => {
139
+ mockReq.body = { refreshToken: 'valid-token' };
140
+ const decoded = { id: '1', email: 'test@test.com', jti: 'test-jti' };
141
+ JwtService.verifyRefreshToken.mockReturnValue(decoded);
142
+ JwtService.generateRefreshToken.mockReturnValue('new-refresh-token');
143
+ JwtService.generateToken.mockReturnValue('new-access-token');
144
+ JwtService.decodeToken.mockReturnValue({ jti: 'new-jti' });
145
+
146
+ <% if (caching !== 'None') { %>
147
+ cacheService.get.mockResolvedValue(['test-jti']);
148
+ <% } else { %>
149
+ JwtService.activeRefreshTokens.set('1', ['test-jti']);
150
+ <% } %>
151
+
152
+ await controller.refresh(mockReq, mockRes, next);
153
+
154
+ expect(mockRes.json).toHaveBeenCalledWith(expect.objectContaining({ accessToken: 'new-access-token' }));
155
+ });
156
+
157
+ it('should return 401 if token theft detected', async () => {
158
+ mockReq.body = { refreshToken: 'stolen-token' };
159
+ const decoded = { id: '1', email: 'test@test.com', jti: 'stolen-jti' };
160
+ JwtService.verifyRefreshToken.mockReturnValue(decoded);
161
+ <% if (caching !== 'None') { %>
162
+ cacheService.get.mockResolvedValue(['other-jti']);
163
+ cacheService.del.mockResolvedValue();
164
+ <% } %>
165
+
166
+ await controller.refresh(mockReq, mockRes, next);
167
+
168
+ expect(mockRes.status).toHaveBeenCalledWith(HTTP_STATUS.UNAUTHORIZED);
169
+ });
170
+
171
+ it('should call next on refresh error', async () => {
172
+ const error = new Error('Redis error');
173
+ mockReq.body = { refreshToken: 'valid-token' };
174
+ JwtService.verifyRefreshToken.mockImplementation(() => { throw error; });
175
+
176
+ await controller.refresh(mockReq, mockRes, next);
177
+
178
+ expect(next).toHaveBeenCalledWith(error);
179
+ });
180
+ });
181
+
182
+ describe('logout', () => {
183
+ it('should return 400 if no token provided', async () => {
184
+ mockReq.headers = {};
185
+ await controller.logout(mockReq, mockRes, next);
186
+ expect(mockRes.status).toHaveBeenCalledWith(HTTP_STATUS.BAD_REQUEST);
187
+ });
188
+
189
+ it('should logout successfully', async () => {
190
+ mockReq.headers = { authorization: 'Bearer valid-token' };
191
+ mockReq.body = { refreshToken: 'valid-refresh-token' };
192
+ JwtService.decodeToken.mockReturnValueOnce({ jti: 'access-jti', exp: Math.floor(Date.now() / 1000) + 3600 })
193
+ .mockReturnValueOnce({ id: '1', jti: 'refresh-jti' });
194
+
195
+ <% if (caching !== 'None') { %>
196
+ cacheService.get.mockResolvedValue(['refresh-jti']);
197
+ <% } else { %>
198
+ JwtService.activeRefreshTokens.set('1', ['refresh-jti']);
199
+ <% } %>
200
+
201
+ await controller.logout(mockReq, mockRes, next);
202
+
203
+ expect(mockRes.json).toHaveBeenCalledWith({ message: 'Logged out successfully' });
204
+ });
205
+
206
+ it('should handle logout even if no refresh token provided', async () => {
207
+ mockReq.headers = { authorization: 'Bearer valid-token' };
208
+ mockReq.body = {};
209
+ JwtService.decodeToken.mockReturnValue({ jti: 'access-jti', exp: Math.floor(Date.now() / 1000) + 3600 });
210
+
211
+ await controller.logout(mockReq, mockRes, next);
212
+
213
+ expect(mockRes.json).toHaveBeenCalledWith({ message: 'Logged out successfully' });
214
+ });
215
+
216
+ it('should call next on logout error', async () => {
217
+ const error = new Error('Logout error');
218
+ mockReq.headers = { authorization: 'Bearer valid-token' };
219
+ JwtService.decodeToken.mockImplementation(() => { throw error; });
220
+
221
+ await controller.logout(mockReq, mockRes, next);
222
+
223
+ expect(next).toHaveBeenCalledWith(error);
224
+ });
225
+ });
226
+
227
+ <% if (socialAuth && socialAuth.filter(a => a !== 'None').length > 0) { -%>
228
+ describe('socialExchange', () => {
229
+ it('should return 400 if code or provider missing', async () => {
230
+ mockReq.body = {};
231
+ await controller.socialExchange(mockReq, mockRes, next);
232
+ expect(mockRes.status).toHaveBeenCalledWith(HTTP_STATUS.BAD_REQUEST);
233
+ });
234
+
235
+ it('should exchange code for JWT tokens', async () => {
236
+ mockReq.body = { code: 'test-code', provider: 'Google' };
237
+ const user = { id: 1, email: 'social@test.com' };
238
+
239
+ <% if (architecture === 'Clean Architecture') { %>
240
+ const mockUseCase = {
241
+ execute: jest.fn().mockResolvedValue({
242
+ user,
243
+ accessToken: 'mock-token',
244
+ refreshToken: 'mock-refresh-token'
245
+ })
246
+ };
247
+ SocialLoginUseCase.mockImplementation(() => mockUseCase);
109
248
  <% } else { %>
110
- JwtService.activeRefreshTokens.set('1', ['old-jti']);<% } %>
249
+ SocialAuthService.getGoogleProfile.mockResolvedValue({ email: 'social@test.com', id: 'google-id', name: 'Google User' });
250
+ User.findOne.mockResolvedValue(user);
251
+ JwtService.generateToken.mockReturnValue('mock-token');
252
+ JwtService.generateRefreshToken.mockReturnValue('mock-refresh-token');
253
+ <% } %>
254
+ JwtService.decodeToken.mockReturnValue({ jti: 'test-jti' });
111
255
 
112
- await controller.refresh(mockReq, mockRes, next);
256
+ await controller.socialExchange(mockReq, mockRes, next);
113
257
 
114
- expect(JwtService.generateToken).toHaveBeenCalledWith(expect.objectContaining({ sid: 'new-jti' }));
115
- expect(mockRes.json).toHaveBeenCalledWith({ accessToken: 'new-access', refreshToken: 'new-refresh' });
258
+ expect(mockRes.json).toHaveBeenCalledWith(expect.objectContaining({ accessToken: 'mock-token' }));
116
259
  });
117
260
 
118
- it('should detect theft and revoke if jti is not active', async () => {
119
- mockReq.body = { refreshToken: 'stolen-refresh' };
120
- const decoded = { id: '1', email: 'test@test.com', jti: 'stolen-jti' };
121
- JwtService.verifyRefreshToken.mockReturnValue(decoded);
261
+ it('should return 400 for invalid provider', async () => {
262
+ mockReq.body = { code: 'test-code', provider: 'Invalid' };
263
+ await controller.socialExchange(mockReq, mockRes, next);
264
+ expect(mockRes.status).toHaveBeenCalledWith(HTTP_STATUS.BAD_REQUEST);
265
+ });
266
+ });
122
267
 
123
- <% if (caching !== 'None') { %>
124
- cacheService.get.mockResolvedValue(['different-jti']);
268
+ describe('social redirect methods', () => {
269
+ <% if (socialAuth.includes('Google')) { %>
270
+ it('googleLogin should redirect to Google', async () => {
271
+ await controller.googleLogin(mockReq, mockRes);
272
+ expect(mockRes.redirect).toHaveBeenCalledWith(expect.stringContaining('accounts.google.com'));
273
+ });
274
+
275
+ it('googleCallback should handle Google callback', async () => {
276
+ mockReq.query = { code: 'test-code', state: 'test-state' };
277
+ mockReq.cookies = { oauth_state: 'test-state' };
278
+ const user = { id: 1, email: 'google@test.com' };
279
+
280
+ <% if (architecture === 'Clean Architecture') { %>
281
+ const mockUseCase = {
282
+ execute: jest.fn().mockResolvedValue({
283
+ user,
284
+ accessToken: 'mock-token',
285
+ refreshToken: 'mock-refresh-token'
286
+ })
287
+ };
288
+ SocialLoginUseCase.mockImplementation(() => mockUseCase);
125
289
  <% } else { %>
126
- JwtService.activeRefreshTokens.set('1', ['different-jti']);<% } %>
290
+ SocialAuthService.getGoogleProfile.mockResolvedValue({ email: 'google@test.com' });
291
+ User.findOne.mockResolvedValue(user);
292
+ JwtService.generateToken.mockReturnValue('mock-token');
293
+ JwtService.generateRefreshToken.mockReturnValue('mock-refresh-token');
294
+ <% } %>
295
+ JwtService.decodeToken.mockReturnValue({ jti: 'test-jti' });
127
296
 
128
- await controller.refresh(mockReq, mockRes, next);
297
+ await controller.googleCallback(mockReq, mockRes, next);
129
298
 
130
- expect(mockRes.status).toHaveBeenCalledWith(HTTP_STATUS.UNAUTHORIZED);
299
+ expect(mockRes.cookie).toHaveBeenCalledWith('accessToken', 'mock-token', expect.any(Object));
300
+ expect(mockRes.redirect).toHaveBeenCalledWith('/');
131
301
  });
132
- });
133
302
 
134
- describe('logout', () => {
135
- it('should blacklist access jti and remove refresh jti', async () => {
136
- mockReq.headers.authorization = 'Bearer access-token';
137
- mockReq.body = { refreshToken: 'refresh-token' };
303
+ it('googleCallback should return 403 if state is invalid', async () => {
304
+ mockReq.query = { code: 'test-code', state: 'invalid-state' };
305
+ mockReq.cookies = { oauth_state: 'valid-state' };
306
+ await controller.googleCallback(mockReq, mockRes, next);
307
+ expect(mockRes.status).toHaveBeenCalledWith(HTTP_STATUS.FORBIDDEN);
308
+ });
309
+
310
+ it('googleCallback should create user if not exists (MVC)', async () => {
311
+ <% if (architecture === 'MVC') { %>
312
+ mockReq.query = { code: 'test-code', state: 'test-state' };
313
+ mockReq.cookies = { oauth_state: 'test-state' };
314
+ SocialAuthService.getGoogleProfile.mockResolvedValue({ email: 'new@google.com', id: 'google-id' });
315
+ User.findOne.mockResolvedValue(null);
316
+ User.create.mockResolvedValue({ id: '2', email: 'new@google.com' });
317
+ JwtService.generateToken.mockReturnValue('at');
318
+ JwtService.generateRefreshToken.mockReturnValue('rt');
319
+ JwtService.decodeToken.mockReturnValue({ jti: 'jti' });
320
+
321
+ await controller.googleCallback(mockReq, mockRes, next);
322
+ expect(User.create).toHaveBeenCalled();
323
+ <% } else { %>
324
+ expect(true).toBe(true);
325
+ <% } %>
326
+ });
327
+
328
+ it('googleCallback should redirect to login on error', async () => {
329
+ const error = new Error('Callback failed');
330
+ mockReq.query = { code: 'code', state: 'test-state' };
331
+ mockReq.cookies = { oauth_state: 'test-state' };
332
+ <% if (architecture === 'Clean Architecture') { %>
333
+ const mockUseCase = {
334
+ execute: jest.fn().mockRejectedValue(error)
335
+ };
336
+ SocialLoginUseCase.mockImplementation(() => mockUseCase);
337
+ <% } else { %>
338
+ SocialAuthService.getGoogleProfile.mockRejectedValue(error);
339
+ <% } %>
340
+
341
+ await controller.googleCallback(mockReq, mockRes, next);
342
+ expect(mockRes.redirect).toHaveBeenCalledWith('/login?error=social_auth_failed');
343
+ });
344
+ <% } %>
345
+
346
+ <% if (socialAuth.includes('GitHub')) { %>
347
+ it('githubLogin should redirect to GitHub', async () => {
348
+ await controller.githubLogin(mockReq, mockRes);
349
+ expect(mockRes.redirect).toHaveBeenCalledWith(expect.stringContaining('github.com'));
350
+ });
351
+
352
+ it('githubCallback should handle GitHub callback', async () => {
353
+ mockReq.query = { code: 'test-code', state: 'test-state' };
354
+ mockReq.cookies = { oauth_state: 'test-state' };
355
+ const user = { id: 1, email: 'github@test.com' };
138
356
 
139
- JwtService.decodeToken
140
- .mockReturnValueOnce({ jti: 'access-jti', exp: Math.floor(Date.now() / 1000) + 3600 })
141
- .mockReturnValueOnce({ id: '1', jti: 'refresh-jti' });
357
+ <% if (architecture === 'Clean Architecture') { %>
358
+ const mockUseCase = {
359
+ execute: jest.fn().mockResolvedValue({
360
+ user,
361
+ accessToken: 'mock-token',
362
+ refreshToken: 'mock-refresh-token'
363
+ })
364
+ };
365
+ SocialLoginUseCase.mockImplementation(() => mockUseCase);
366
+ <% } else { %>
367
+ SocialAuthService.getGithubProfile.mockResolvedValue({ email: 'github@test.com' });
368
+ User.findOne.mockResolvedValue(user);
369
+ JwtService.generateToken.mockReturnValue('mock-token');
370
+ JwtService.generateRefreshToken.mockReturnValue('mock-refresh-token');
371
+ <% } %>
372
+ JwtService.decodeToken.mockReturnValue({ jti: 'test-jti' });
373
+
374
+ await controller.githubCallback(mockReq, mockRes, next);
375
+
376
+ expect(mockRes.cookie).toHaveBeenCalledWith('accessToken', 'mock-token', expect.any(Object));
377
+ expect(mockRes.redirect).toHaveBeenCalledWith('/');
378
+ });
379
+
380
+ it('githubCallback should return 403 if state is invalid', async () => {
381
+ mockReq.query = { code: 'test-code', state: 'invalid-state' };
382
+ mockReq.cookies = { oauth_state: 'valid-state' };
383
+ await controller.githubCallback(mockReq, mockRes, next);
384
+ expect(mockRes.status).toHaveBeenCalledWith(HTTP_STATUS.FORBIDDEN);
385
+ });
386
+
387
+ it('githubCallback should create user if not exists (MVC)', async () => {
388
+ <% if (architecture === 'MVC') { %>
389
+ mockReq.query = { code: 'test-code', state: 'test-state' };
390
+ mockReq.cookies = { oauth_state: 'test-state' };
391
+ SocialAuthService.getGithubProfile.mockResolvedValue({ email: 'new@github.com', id: 'github-id' });
392
+ User.findOne.mockResolvedValue(null);
393
+ User.create.mockResolvedValue({ id: '2', email: 'new@github.com' });
394
+ JwtService.generateToken.mockReturnValue('at');
395
+ JwtService.generateRefreshToken.mockReturnValue('rt');
396
+ JwtService.decodeToken.mockReturnValue({ jti: 'jti' });
142
397
 
143
- await controller.logout(mockReq, mockRes, next);
398
+ await controller.githubCallback(mockReq, mockRes, next);
399
+ expect(User.create).toHaveBeenCalled();
400
+ <% } else { %>
401
+ expect(true).toBe(true);
402
+ <% } %>
403
+ });
404
+
405
+ it('githubCallback should redirect to login on error', async () => {
406
+ const error = new Error('Callback failed');
407
+ mockReq.query = { code: 'code', state: 'test-state' };
408
+ mockReq.cookies = { oauth_state: 'test-state' };
409
+ <% if (architecture === 'Clean Architecture') { %>
410
+ const mockUseCase = {
411
+ execute: jest.fn().mockRejectedValue(error)
412
+ };
413
+ SocialLoginUseCase.mockImplementation(() => mockUseCase);
414
+ <% } else { %>
415
+ SocialAuthService.getGithubProfile.mockRejectedValue(error);
416
+ <% } %>
144
417
 
145
- expect(mockRes.json).toHaveBeenCalledWith({ message: 'Logged out successfully' });
418
+ await controller.githubCallback(mockReq, mockRes, next);
419
+ expect(mockRes.redirect).toHaveBeenCalledWith('/login?error=social_auth_failed');
146
420
  });
421
+ <% } %>
147
422
  });
423
+ <% } -%>
148
424
  });
@@ -27,28 +27,32 @@ const authMiddleware = async (req, res, next) => {
27
27
  }
28
28
 
29
29
  if (decoded.jti) {
30
- <%_ if (caching !== 'None') { -%>
30
+ <%_ if (caching !== 'None') { _%>
31
31
  const isBlacklisted = await cacheService.get(`blacklist:${decoded.jti}`);
32
32
  if (isBlacklisted) {
33
33
  return res.status(HTTP_STATUS.UNAUTHORIZED).json({ message: 'Token revoked' });
34
- }<%_ } else { -%>
34
+ }
35
+ <%_ } else { _%>
35
36
  const expiryDate = JwtService.blacklistedTokens.get(decoded.jti);
36
37
  if (expiryDate && Date.now() < expiryDate) {
37
38
  return res.status(HTTP_STATUS.UNAUTHORIZED).json({ message: 'Token revoked' });
38
- }<% } %>
39
+ }
40
+ <%_ } _%>
39
41
  }
40
42
 
41
43
  if (decoded.sid) {
42
- <%_ if (caching !== 'None') { %>
44
+ <%_ if (caching !== 'None') { _%>
43
45
  const cacheKey = `refresh_tokens:${decoded.id}`;
44
46
  const activeTokens = await cacheService.get(cacheKey) || [];
45
47
  if (!activeTokens.includes(decoded.sid)) {
46
48
  return res.status(HTTP_STATUS.UNAUTHORIZED).json({ message: 'Session expired' });
47
- }<%_ } else { -%>
49
+ }
50
+ <%_ } else { _%>
48
51
  const activeTokens = JwtService.activeRefreshTokens.get(String(decoded.id)) || [];
49
52
  if (!activeTokens.includes(decoded.sid)) {
50
53
  return res.status(HTTP_STATUS.UNAUTHORIZED).json({ message: 'Session expired' });
51
- }<%_ } -%>
54
+ }
55
+ <%_ } _%>
52
56
  }
53
57
 
54
58
  req.user = decoded;
@@ -12,5 +12,16 @@ const authController = new AuthController();
12
12
  router.post('/login', (req, res, next) => authController.login(req, res, next));
13
13
  router.post('/refresh', (req, res, next) => authController.refresh(req, res, next));
14
14
  router.post('/logout', authMiddleware, (req, res, next) => authController.logout(req, res, next));
15
+ <%_ if (socialAuth && socialAuth.filter(a => a !== 'None').length > 0) { _%>
16
+ router.post('/social/exchange', (req, res, next) => authController.socialExchange(req, res, next));
17
+ <%_ if (socialAuth.includes('Google')) { _%>
18
+ router.get('/google', (req, res) => authController.googleLogin(req, res));
19
+ router.get('/google/callback', (req, res, next) => authController.googleCallback(req, res, next));
20
+ <%_ } _%>
21
+ <%_ if (socialAuth.includes('GitHub')) { _%>
22
+ router.get('/github', (req, res) => authController.githubLogin(req, res));
23
+ router.get('/github/callback', (req, res, next) => authController.githubCallback(req, res, next));
24
+ <%_ } _%>
25
+ <%_ } _%>
15
26
 
16
27
  module.exports = router;
@@ -58,6 +58,12 @@ describe('JwtService', () => {
58
58
  expect(jwt.verify).toHaveBeenCalledWith(token, secret);
59
59
  expect(result).toEqual(payload);
60
60
  });
61
+
62
+ it('should return null for an invalid token', () => {
63
+ jwt.verify.mockImplementation(() => { throw new Error('Invalid token'); });
64
+ const result = JwtService.verifyToken(token);
65
+ expect(result).toBeNull();
66
+ });
61
67
  });
62
68
 
63
69
  describe('verifyRefreshToken', () => {
@@ -69,6 +75,12 @@ describe('JwtService', () => {
69
75
  expect(jwt.verify).toHaveBeenCalledWith(token, 'test-refresh-secret');
70
76
  expect(result).toEqual(payload);
71
77
  });
78
+
79
+ it('should return null for an invalid refresh token', () => {
80
+ jwt.verify.mockImplementation(() => { throw new Error('Invalid token'); });
81
+ const result = JwtService.verifyRefreshToken(token);
82
+ expect(result).toBeNull();
83
+ });
72
84
  });
73
85
 
74
86
  describe('decodeToken', () => {
@@ -80,5 +92,23 @@ describe('JwtService', () => {
80
92
  expect(jwt.decode).toHaveBeenCalledWith(token);
81
93
  expect(result).toEqual(payload);
82
94
  });
95
+
96
+ it('should return null if decode fails', () => {
97
+ jwt.decode.mockImplementation(() => { throw new Error('Decode failed'); });
98
+ const result = JwtService.decodeToken(token);
99
+ expect(result).toBeNull();
100
+ });
101
+ });
102
+
103
+ describe('fallback values', () => {
104
+ it('should use default values if env not set', () => {
105
+ delete process.env.JWT_SECRET;
106
+ delete process.env.JWT_REFRESH_SECRET;
107
+ delete process.env.JWT_EXPIRES_IN;
108
+ delete process.env.JWT_REFRESH_EXPIRES_IN;
109
+
110
+ expect(JwtService.SECRET).toBeDefined();
111
+ expect(JwtService.REFRESH_SECRET).toBeDefined();
112
+ });
83
113
  });
84
114
  });