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,28 +1,34 @@
1
- <%_ if (architecture === 'Clean Architecture') { _%>
2
- import { AuthController } from '@/interfaces/controllers/auth/authController';
3
- import { JwtService } from '@/infrastructure/auth/jwtService';
4
- <%_ } else { _%>
5
- import { AuthController } from '@/controllers/authController';
6
- import { JwtService } from '@/services/jwtService';
7
- <%_ } _%>
8
- import { HTTP_STATUS } from '@/utils/httpCodes';
9
- import bcrypt from 'bcryptjs';
10
- import { Request, Response, NextFunction } from 'express';
11
-
12
1
  <%_ 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
+ <% } -%>
8
+ jest.mock('@/infrastructure/database/models/User', () => ({
9
+ findOne: jest.fn(),
10
+ create: jest.fn(),
11
+ }));
14
12
  <%_ } else { _%>
15
13
  jest.mock('@/services/jwtService');
14
+ <% if (socialAuth && socialAuth.filter(a => a !== 'None').length > 0) { -%>
15
+ jest.mock('@/services/socialAuthService');
16
+ <% } -%>
17
+ jest.mock('@/models/User', () => ({
18
+ findOne: jest.fn(),
19
+ create: jest.fn(),
20
+ }));
16
21
  <%_ } _%>
17
- jest.mock('bcryptjs');
18
22
 
19
- <%_ if (architecture === 'Clean Architecture') { _%>
20
- jest.mock('@/infrastructure/database/models/User', () => ({ findOne: jest.fn() }));
21
- const User = require('@/infrastructure/database/models/User');
22
- <%_ } else { _%>
23
- jest.mock('@/models/User', () => ({ findOne: jest.fn() }));
24
- const User = require('@/models/User');
25
- <%_ } _%>
23
+ import { AuthController } from '<%= (architecture === "MVC") ? "@/controllers/authController" : "@/interfaces/controllers/auth/authController" %>';
24
+ import { JwtService } from '<%= (architecture === "MVC") ? "@/services/jwtService" : "@/infrastructure/auth/jwtService" %>';
25
+ import User from '<%= (architecture === "MVC") ? "@/models/User" : "@/infrastructure/database/models/User" %>';
26
+ import { HTTP_STATUS } from '@/utils/httpCodes';
27
+ import bcrypt from 'bcryptjs';
28
+ import { Request, Response, NextFunction } from 'express';
29
+ <% if (architecture === 'Clean Architecture' && socialAuth && socialAuth.filter(a => a !== 'None').length > 0) { %>
30
+ import { SocialLoginUseCase } from '@/usecases/auth/socialLoginUseCase';
31
+ <% } %>
26
32
 
27
33
  <%_ if (caching !== 'None') { _%>
28
34
  <%_ const cachePath = (architecture === "MVC") ? (caching === "Redis" ? "@/config/redisClient" : "@/config/memoryCache") : (caching === "Redis" ? "@/infrastructure/caching/redisClient" : "@/infrastructure/caching/memoryCache"); _%>
@@ -34,23 +40,31 @@ jest.mock('<%= cachePath %>', () => ({
34
40
  del: jest.fn()
35
41
  }
36
42
  }), { virtual: true });
37
- import cacheService from '<%= cachePath %>';
43
+ import _cacheService from '<%= cachePath %>';
38
44
  <%_ } _%>
39
45
 
46
+ jest.mock('bcryptjs');
47
+
40
48
  describe('AuthController', () => {
41
49
  let authController: AuthController;
42
- let mockRequest: any;
43
- let mockResponse: any;
50
+ let mockRequest: Partial<Request>;
51
+ let mockResponse: Partial<Response>;
44
52
  const nextFunction: NextFunction = jest.fn();
45
53
 
46
54
  beforeEach(() => {
47
55
  authController = new AuthController();
48
56
  mockRequest = {
49
- body: {}
57
+ body: {},
58
+ headers: {},
59
+ query: {},
60
+ cookies: {}
50
61
  };
51
62
  mockResponse = {
52
63
  status: jest.fn().mockReturnThis(),
53
- json: jest.fn()
64
+ json: jest.fn(),
65
+ cookie: jest.fn(),
66
+ clearCookie: jest.fn(),
67
+ redirect: jest.fn()
54
68
  };
55
69
  jest.clearAllMocks();
56
70
  });
@@ -63,19 +77,6 @@ describe('AuthController', () => {
63
77
  await authController.login(mockRequest as Request, mockResponse as Response, nextFunction);
64
78
 
65
79
  expect(mockResponse.status).toHaveBeenCalledWith(HTTP_STATUS.UNAUTHORIZED);
66
- expect(mockResponse.json).toHaveBeenCalledWith({ message: 'Invalid credentials' });
67
- });
68
-
69
- it('should return 401 if password does not match', async () => {
70
- const user = { email: 'test@test.com', password: 'hashedpassword' };
71
- mockRequest.body = { email: 'test@test.com', password: 'wrongpassword' };
72
- (User.findOne as jest.Mock).mockResolvedValue(user);
73
- (bcrypt.compare as jest.Mock).mockResolvedValue(false);
74
-
75
- await authController.login(mockRequest as Request, mockResponse as Response, nextFunction);
76
-
77
- expect(mockResponse.status).toHaveBeenCalledWith(HTTP_STATUS.UNAUTHORIZED);
78
- expect(mockResponse.json).toHaveBeenCalledWith({ message: 'Invalid credentials' });
79
80
  });
80
81
 
81
82
  it('should return 200 and a token if credentials are valid', async () => {
@@ -89,14 +90,24 @@ describe('AuthController', () => {
89
90
 
90
91
  await authController.login(mockRequest as Request, mockResponse as Response, nextFunction);
91
92
 
92
- expect(JwtService.generateToken).toHaveBeenCalledWith(expect.objectContaining({ sid: 'test-jti' }));
93
- expect(mockResponse.json).toHaveBeenCalledWith({ token: 'mock-token', accessToken: 'mock-token', refreshToken: 'mock-refresh-token' });
93
+ expect(mockResponse.json).toHaveBeenCalledWith(expect.objectContaining({ accessToken: 'mock-token' }));
94
94
  });
95
95
 
96
- it('should call next with error if something fails', async () => {
97
- const error = new Error('DB Error');
98
- mockRequest.body = { email: 'test@test.com', password: 'password123' };
96
+ it('should return 401 if password is invalid', async () => {
97
+ const user = { id: 1, email: 'test@test.com', password: 'hashedpassword' };
98
+ mockRequest.body = { email: 'test@test.com', password: 'wrongpassword' };
99
+ (User.findOne as jest.Mock).mockResolvedValue(user);
100
+ (bcrypt.compare as jest.Mock).mockResolvedValue(false);
101
+
102
+ await authController.login(mockRequest as Request, mockResponse as Response, nextFunction);
103
+
104
+ expect(mockResponse.status).toHaveBeenCalledWith(HTTP_STATUS.UNAUTHORIZED);
105
+ });
106
+
107
+ it('should call next with error if login fails', async () => {
108
+ const error = new Error('Login failed');
99
109
  (User.findOne as jest.Mock).mockRejectedValue(error);
110
+ mockRequest.body = { email: 'test@test.com', password: 'password123' };
100
111
 
101
112
  await authController.login(mockRequest as Request, mockResponse as Response, nextFunction);
102
113
 
@@ -105,57 +116,348 @@ describe('AuthController', () => {
105
116
  });
106
117
 
107
118
  describe('refresh', () => {
108
- it('should return new tokens for a valid refresh token', async () => {
109
- mockRequest.body = { refreshToken: 'valid-refresh' };
110
- const decoded = { id: '1', email: 'test@test.com', jti: 'old-jti' };
119
+ it('should return 401 if refresh token is invalid', async () => {
120
+ mockRequest.body = { refreshToken: 'invalid-token' };
121
+ (JwtService.verifyRefreshToken as jest.Mock).mockReturnValue(null);
122
+
123
+ await authController.refresh(mockRequest as Request, mockResponse as Response, nextFunction);
124
+
125
+ expect(mockResponse.status).toHaveBeenCalledWith(HTTP_STATUS.UNAUTHORIZED);
126
+ });
127
+
128
+ it('should return 400 if refresh token is missing', async () => {
129
+ mockRequest.body = {};
130
+
131
+ await authController.refresh(mockRequest as Request, mockResponse as Response, nextFunction);
132
+
133
+ expect(mockResponse.status).toHaveBeenCalledWith(HTTP_STATUS.BAD_REQUEST);
134
+ });
135
+
136
+ it('should return new tokens if refresh token is valid', async () => {
137
+ mockRequest.body = { refreshToken: 'valid-token' };
138
+ const decoded = { id: '1', email: 'test@test.com', jti: 'test-jti' };
111
139
  (JwtService.verifyRefreshToken as jest.Mock).mockReturnValue(decoded);
112
- (JwtService.generateToken as jest.Mock).mockReturnValue('new-access');
113
- (JwtService.generateRefreshToken as jest.Mock).mockReturnValue('new-refresh');
140
+ (JwtService.generateRefreshToken as jest.Mock).mockReturnValue('new-refresh-token');
141
+ (JwtService.generateToken as jest.Mock).mockReturnValue('new-access-token');
114
142
  (JwtService.decodeToken as jest.Mock).mockReturnValue({ jti: 'new-jti' });
115
143
 
116
- // Mock cache success
117
144
  <% if (caching !== 'None') { %>
118
- (cacheService.get as jest.Mock).mockResolvedValue(['old-jti']);
145
+ (_cacheService.get as jest.Mock).mockResolvedValue(['test-jti']);
119
146
  <% } else { %>
120
- JwtService.activeRefreshTokens.set('1', ['old-jti']);
147
+ JwtService.activeRefreshTokens.set('1', ['test-jti']);
121
148
  <% } %>
122
149
 
123
150
  await authController.refresh(mockRequest as Request, mockResponse as Response, nextFunction);
124
151
 
125
- expect(JwtService.generateToken).toHaveBeenCalledWith(expect.objectContaining({ sid: 'new-jti' }));
126
- expect(mockResponse.json).toHaveBeenCalledWith({ accessToken: 'new-access', refreshToken: 'new-refresh' });
152
+ expect(mockResponse.json).toHaveBeenCalledWith(expect.objectContaining({ accessToken: 'new-access-token' }));
127
153
  });
128
154
 
129
- it('should detect theft and revoke all tokens if jti is not active', async () => {
130
- mockRequest.body = { refreshToken: 'stolen-refresh' };
155
+ it('should detect token theft if jti is not in active tokens', async () => {
156
+ mockRequest.body = { refreshToken: 'valid-token' };
131
157
  const decoded = { id: '1', email: 'test@test.com', jti: 'stolen-jti' };
132
158
  (JwtService.verifyRefreshToken as jest.Mock).mockReturnValue(decoded);
133
159
 
134
160
  <% if (caching !== 'None') { %>
135
- (cacheService.get as jest.Mock).mockResolvedValue(['some-other-jti']);
161
+ (_cacheService.get as jest.Mock).mockResolvedValue(['other-jti']);
136
162
  <% } else { %>
137
- JwtService.activeRefreshTokens.set('1', ['some-other-jti']);
163
+ JwtService.activeRefreshTokens.set('1', ['other-jti']);
138
164
  <% } %>
139
165
 
140
166
  await authController.refresh(mockRequest as Request, mockResponse as Response, nextFunction);
141
167
 
142
168
  expect(mockResponse.status).toHaveBeenCalledWith(HTTP_STATUS.UNAUTHORIZED);
143
- expect(mockResponse.json).toHaveBeenCalledWith({ message: 'Invalid session' });
169
+ });
170
+
171
+ it('should call next with error if refresh fails', async () => {
172
+ const error = new Error('Refresh failed');
173
+ (JwtService.verifyRefreshToken as jest.Mock).mockImplementation(() => { throw error; });
174
+ mockRequest.body = { refreshToken: 'token' };
175
+
176
+ await authController.refresh(mockRequest as Request, mockResponse as Response, nextFunction);
177
+
178
+ expect(nextFunction).toHaveBeenCalledWith(error);
144
179
  });
145
180
  });
146
181
 
147
182
  describe('logout', () => {
148
- it('should blacklist the access token and remove the refresh token', async () => {
149
- mockRequest.headers = { authorization: 'Bearer access-token' };
150
- mockRequest.body = { refreshToken: 'refresh-token' };
151
-
152
- (JwtService.decodeToken as jest.Mock)
153
- .mockReturnValueOnce({ jti: 'access-jti', exp: Math.floor(Date.now() / 1000) + 3600 }) // for access token
154
- .mockReturnValueOnce({ id: '1', jti: 'refresh-jti' }); // for refresh token
183
+ it('should return 400 if no token provided', async () => {
184
+ mockRequest.headers = {};
185
+ await authController.logout(mockRequest as Request, mockResponse as Response, nextFunction);
186
+ expect(mockResponse.status).toHaveBeenCalledWith(HTTP_STATUS.BAD_REQUEST);
187
+ });
188
+
189
+ it('should logout successfully', async () => {
190
+ mockRequest.headers = { authorization: 'Bearer valid-token' };
191
+ mockRequest.body = { refreshToken: 'valid-refresh-token' };
192
+ (JwtService.decodeToken as jest.Mock).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 as jest.Mock).mockResolvedValue(['refresh-jti']);
197
+ <% } else { %>
198
+ JwtService.activeRefreshTokens.set('1', ['refresh-jti']);
199
+ <% } %>
155
200
 
156
201
  await authController.logout(mockRequest as Request, mockResponse as Response, nextFunction);
157
202
 
158
203
  expect(mockResponse.json).toHaveBeenCalledWith({ message: 'Logged out successfully' });
159
204
  });
205
+
206
+ it('should call next with error if logout fails', async () => {
207
+ const error = new Error('Logout failed');
208
+ mockRequest.headers = { authorization: 'Bearer token' };
209
+ (JwtService.decodeToken as jest.Mock).mockImplementation(() => { throw error; });
210
+
211
+ await authController.logout(mockRequest as Request, mockResponse as Response, nextFunction);
212
+
213
+ expect(nextFunction).toHaveBeenCalledWith(error);
214
+ });
215
+ });
216
+
217
+ <% if (socialAuth && socialAuth.filter(a => a !== 'None').length > 0) { -%>
218
+ describe('socialExchange', () => {
219
+ it('should return 400 if code or provider missing', async () => {
220
+ mockRequest.body = {};
221
+ await authController.socialExchange(mockRequest as Request, mockResponse as Response, nextFunction);
222
+ expect(mockResponse.status).toHaveBeenCalledWith(HTTP_STATUS.BAD_REQUEST);
223
+ });
224
+
225
+ it('should exchange code for JWT tokens', async () => {
226
+ mockRequest.body = { code: 'test-code', provider: 'Google' };
227
+ const user = { id: 1, email: 'social@test.com' };
228
+
229
+ <%_ if (architecture === 'Clean Architecture') { _%>
230
+ const mockUseCaseInstance = {
231
+ execute: jest.fn().mockResolvedValue({
232
+ user,
233
+ accessToken: 'mock-token',
234
+ refreshToken: 'mock-refresh-token'
235
+ })
236
+ };
237
+ (SocialLoginUseCase as jest.Mock).mockImplementation(() => mockUseCaseInstance);
238
+ <%_ } else { _%>
239
+ const { SocialAuthService } = require('<%= architecture === "MVC" ? "@/services/socialAuthService" : "@/infrastructure/auth/socialAuthService" %>');
240
+ (SocialAuthService.getGoogleProfile as jest.Mock).mockResolvedValue({ email: 'social@test.com', id: 'google-id', name: 'Google User' });
241
+ (User.findOne as jest.Mock).mockResolvedValue(user);
242
+ (JwtService.generateToken as jest.Mock).mockReturnValue('mock-token');
243
+ (JwtService.generateRefreshToken as jest.Mock).mockReturnValue('mock-refresh-token');
244
+ <%_ } _%>
245
+ (JwtService.decodeToken as jest.Mock).mockReturnValue({ jti: 'test-jti' });
246
+
247
+ await authController.socialExchange(mockRequest as Request, mockResponse as Response, nextFunction);
248
+
249
+ expect(mockResponse.json).toHaveBeenCalledWith(expect.objectContaining({ accessToken: 'mock-token' }));
250
+ });
251
+
252
+ <% if (socialAuth.includes('GitHub')) { %>
253
+ it('should exchange GitHub code for JWT tokens', async () => {
254
+ mockRequest.body = { code: 'test-code', provider: 'GitHub' };
255
+ const user = { id: 1, email: 'github@test.com' };
256
+
257
+ <%_ if (architecture === 'Clean Architecture') { _%>
258
+ const mockUseCaseInstance = {
259
+ execute: jest.fn().mockResolvedValue({ user, accessToken: 'at', refreshToken: 'rt' })
260
+ };
261
+ (SocialLoginUseCase as jest.Mock).mockImplementation(() => mockUseCaseInstance);
262
+ <%_ } else { _%>
263
+ const { SocialAuthService } = require('<%= architecture === "MVC" ? "@/services/socialAuthService" : "@/infrastructure/auth/socialAuthService" %>');
264
+ (SocialAuthService.getGithubProfile as jest.Mock).mockResolvedValue({ email: 'github@test.com', id: 'github-id', name: 'GitHub User' });
265
+ (User.findOne as jest.Mock).mockResolvedValue(user);
266
+ (JwtService.generateToken as jest.Mock).mockReturnValue('at');
267
+ (JwtService.generateRefreshToken as jest.Mock).mockReturnValue('rt');
268
+ <%_ } _%>
269
+ (JwtService.decodeToken as jest.Mock).mockReturnValue({ jti: 'test-jti' });
270
+
271
+ await authController.socialExchange(mockRequest as Request, mockResponse as Response, nextFunction);
272
+ expect(mockResponse.json).toHaveBeenCalledWith(expect.objectContaining({ accessToken: 'at' }));
273
+ });
274
+ <% } %>
275
+
276
+ it('should create user if social user does not exist (MVC)', async () => {
277
+ <% if (architecture === 'MVC') { %>
278
+ mockRequest.body = { code: 'test-code', provider: 'Google' };
279
+ const { SocialAuthService } = require('@/services/socialAuthService');
280
+ (SocialAuthService.getGoogleProfile as jest.Mock).mockResolvedValue({ email: 'new@test.com', id: 'google-id', name: 'New User' });
281
+ (User.findOne as jest.Mock).mockResolvedValue(null);
282
+ (User.create as jest.Mock).mockResolvedValue({ id: '2', email: 'new@test.com', save: jest.fn() });
283
+ (JwtService.generateToken as jest.Mock).mockReturnValue('token');
284
+ (JwtService.generateRefreshToken as jest.Mock).mockReturnValue('rtoken');
285
+ (JwtService.decodeToken as jest.Mock).mockReturnValue({ jti: 'jti' });
286
+
287
+ await authController.socialExchange(mockRequest as Request, mockResponse as Response, nextFunction);
288
+ expect(User.create).toHaveBeenCalled();
289
+ <% } %>
290
+ });
291
+
292
+ it('should return 401 if social profile has no email', async () => {
293
+ <% if (architecture === 'MVC') { %>
294
+ mockRequest.body = { code: 'test-code', provider: 'Google' };
295
+ const { SocialAuthService } = require('@/services/socialAuthService');
296
+ (SocialAuthService.getGoogleProfile as jest.Mock).mockResolvedValue({ id: 'google-id' }); // No email
297
+
298
+ await authController.socialExchange(mockRequest as Request, mockResponse as Response, nextFunction);
299
+ expect(mockResponse.status).toHaveBeenCalledWith(HTTP_STATUS.UNAUTHORIZED);
300
+ <% } %>
301
+ });
302
+
303
+ it('should call next with error if socialExchange fails', async () => {
304
+ const error = new Error('Exchange failed');
305
+ mockRequest.body = { code: 'code', provider: 'Google' };
306
+ <% if (architecture === 'MVC') { %>
307
+ const { SocialAuthService } = require('@/services/socialAuthService');
308
+ (SocialAuthService.getGoogleProfile as jest.Mock).mockRejectedValue(error);
309
+ <% } else { %>
310
+ (SocialLoginUseCase as jest.Mock).mockImplementation(() => { throw error; });
311
+ <% } %>
312
+
313
+ await authController.socialExchange(mockRequest as Request, mockResponse as Response, nextFunction);
314
+ expect(nextFunction).toHaveBeenCalledWith(error);
315
+ });
316
+
317
+ it('should return 400 for invalid provider', async () => {
318
+ mockRequest.body = { code: 'test-code', provider: 'Invalid' };
319
+ await authController.socialExchange(mockRequest as Request, mockResponse as Response, nextFunction);
320
+ expect(mockResponse.status).toHaveBeenCalledWith(HTTP_STATUS.BAD_REQUEST);
321
+ });
322
+ });
323
+
324
+ describe('social redirect methods', () => {
325
+ <% if (socialAuth.includes('Google')) { %>
326
+ it('googleLogin should redirect to Google', async () => {
327
+ await authController.googleLogin(mockRequest as Request, mockResponse as Response);
328
+ expect(mockResponse.redirect).toHaveBeenCalledWith(expect.stringContaining('accounts.google.com'));
329
+ });
330
+ <% } %>
331
+ <% if (socialAuth.includes('GitHub')) { %>
332
+ it('githubLogin should redirect to GitHub', async () => {
333
+ await authController.githubLogin(mockRequest as Request, mockResponse as Response);
334
+ expect(mockResponse.redirect).toHaveBeenCalledWith(expect.stringContaining('github.com'));
335
+ });
336
+ <% } %>
337
+
338
+ <% if (socialAuth.includes('Google')) { %>
339
+ it('googleCallback should handle Google callback', async () => {
340
+ mockRequest.query = { code: 'test-code', state: 'test-state' };
341
+ mockRequest.cookies = { oauth_state: 'test-state' };
342
+ const user = { id: 1, email: 'google@test.com' };
343
+ <%_ if (architecture === 'Clean Architecture') { _%>
344
+ const mockUseCaseInstance = { execute: jest.fn().mockResolvedValue({ user, accessToken: 'at', refreshToken: 'rt' }) };
345
+ (SocialLoginUseCase as jest.Mock).mockImplementation(() => mockUseCaseInstance);
346
+ <%_ } else { _%>
347
+ const { SocialAuthService } = require('@/services/socialAuthService');
348
+ (SocialAuthService.getGoogleProfile as jest.Mock).mockResolvedValue({ email: 'google@test.com' });
349
+ (User.findOne as jest.Mock).mockResolvedValue(user);
350
+ (JwtService.generateToken as jest.Mock).mockReturnValue('at');
351
+ (JwtService.generateRefreshToken as jest.Mock).mockReturnValue('rt');
352
+ <%_ } _%>
353
+ (JwtService.decodeToken as jest.Mock).mockReturnValue({ jti: 'test-jti' });
354
+
355
+ await authController.googleCallback(mockRequest as Request, mockResponse as Response, nextFunction);
356
+ expect(mockResponse.cookie).toHaveBeenCalledWith('accessToken', 'at', expect.any(Object));
357
+ expect(mockResponse.redirect).toHaveBeenCalledWith('/');
358
+ });
359
+
360
+ it('googleCallback should return 403 if state is invalid', async () => {
361
+ mockRequest.query = { code: 'test-code', state: 'invalid-state' };
362
+ mockRequest.cookies = { oauth_state: 'valid-state' };
363
+ await authController.googleCallback(mockRequest as Request, mockResponse as Response, nextFunction);
364
+ expect(mockResponse.status).toHaveBeenCalledWith(HTTP_STATUS.FORBIDDEN);
365
+ });
366
+
367
+ it('googleCallback should create user if not exists (MVC)', async () => {
368
+ <% if (architecture === 'MVC') { %>
369
+ mockRequest.query = { code: 'test-code', state: 'test-state' };
370
+ mockRequest.cookies = { oauth_state: 'test-state' };
371
+ const { SocialAuthService } = require('@/services/socialAuthService');
372
+ (SocialAuthService.getGoogleProfile as jest.Mock).mockResolvedValue({ email: 'new@google.com', id: 'google-id' });
373
+ (User.findOne as jest.Mock).mockResolvedValue(null);
374
+ (User.create as jest.Mock).mockResolvedValue({ id: '2', email: 'new@google.com' });
375
+ (JwtService.generateToken as jest.Mock).mockReturnValue('at');
376
+ (JwtService.generateRefreshToken as jest.Mock).mockReturnValue('rt');
377
+ (JwtService.decodeToken as jest.Mock).mockReturnValue({ jti: 'jti' });
378
+
379
+ await authController.googleCallback(mockRequest as Request, mockResponse as Response, nextFunction);
380
+ expect(User.create).toHaveBeenCalled();
381
+ <% } %>
382
+ });
383
+
384
+ it('googleCallback should redirect to login on error', async () => {
385
+ const error = new Error('Callback failed');
386
+ mockRequest.query = { code: 'code', state: 'test-state' };
387
+ mockRequest.cookies = { oauth_state: 'test-state' };
388
+ <% if (architecture === 'MVC') { %>
389
+ const { SocialAuthService } = require('@/services/socialAuthService');
390
+ (SocialAuthService.getGoogleProfile as jest.Mock).mockRejectedValue(error);
391
+ <% } else { %>
392
+ (SocialLoginUseCase as jest.Mock).mockImplementation(() => { throw error; });
393
+ <% } %>
394
+
395
+ await authController.googleCallback(mockRequest as Request, mockResponse as Response, nextFunction);
396
+ expect(mockResponse.redirect).toHaveBeenCalledWith('/login?error=social_auth_failed');
397
+ });
398
+ <% } %>
399
+
400
+ <% if (socialAuth.includes('GitHub')) { %>
401
+ it('githubCallback should handle GitHub callback', async () => {
402
+ mockRequest.query = { code: 'test-code', state: 'test-state' };
403
+ mockRequest.cookies = { oauth_state: 'test-state' };
404
+ const user = { id: 1, email: 'github@test.com' };
405
+ <%_ if (architecture === 'Clean Architecture') { _%>
406
+ const mockUseCaseInstance = { execute: jest.fn().mockResolvedValue({ user, accessToken: 'at', refreshToken: 'rt' }) };
407
+ (SocialLoginUseCase as jest.Mock).mockImplementation(() => mockUseCaseInstance);
408
+ <%_ } else { _%>
409
+ const { SocialAuthService } = require('@/services/socialAuthService');
410
+ (SocialAuthService.getGithubProfile as jest.Mock).mockResolvedValue({ email: 'github@test.com' });
411
+ (User.findOne as jest.Mock).mockResolvedValue(user);
412
+ (JwtService.generateToken as jest.Mock).mockReturnValue('at');
413
+ (JwtService.generateRefreshToken as jest.Mock).mockReturnValue('rt');
414
+ <%_ } _%>
415
+ (JwtService.decodeToken as jest.Mock).mockReturnValue({ jti: 'test-jti' });
416
+
417
+ await authController.githubCallback(mockRequest as Request, mockResponse as Response, nextFunction);
418
+ expect(mockResponse.cookie).toHaveBeenCalledWith('accessToken', 'at', expect.any(Object));
419
+ expect(mockResponse.redirect).toHaveBeenCalledWith('/');
420
+ });
421
+
422
+ it('githubCallback should return 403 if state is invalid', async () => {
423
+ mockRequest.query = { code: 'test-code', state: 'invalid-state' };
424
+ mockRequest.cookies = { oauth_state: 'valid-state' };
425
+ await authController.githubCallback(mockRequest as Request, mockResponse as Response, nextFunction);
426
+ expect(mockResponse.status).toHaveBeenCalledWith(HTTP_STATUS.FORBIDDEN);
427
+ });
428
+
429
+ it('githubCallback should create user if not exists (MVC)', async () => {
430
+ <% if (architecture === 'MVC') { %>
431
+ mockRequest.query = { code: 'test-code', state: 'test-state' };
432
+ mockRequest.cookies = { oauth_state: 'test-state' };
433
+ const { SocialAuthService } = require('@/services/socialAuthService');
434
+ (SocialAuthService.getGithubProfile as jest.Mock).mockResolvedValue({ email: 'new@github.com', id: 'github-id' });
435
+ (User.findOne as jest.Mock).mockResolvedValue(null);
436
+ (User.create as jest.Mock).mockResolvedValue({ id: '2', email: 'new@github.com' });
437
+ (JwtService.generateToken as jest.Mock).mockReturnValue('at');
438
+ (JwtService.generateRefreshToken as jest.Mock).mockReturnValue('rt');
439
+ (JwtService.decodeToken as jest.Mock).mockReturnValue({ jti: 'jti' });
440
+
441
+ await authController.githubCallback(mockRequest as Request, mockResponse as Response, nextFunction);
442
+ expect(User.create).toHaveBeenCalled();
443
+ <% } %>
444
+ });
445
+
446
+ it('githubCallback should redirect to login on error', async () => {
447
+ const error = new Error('Callback failed');
448
+ mockRequest.query = { code: 'code', state: 'test-state' };
449
+ mockRequest.cookies = { oauth_state: 'test-state' };
450
+ <% if (architecture === 'MVC') { %>
451
+ const { SocialAuthService } = require('@/services/socialAuthService');
452
+ (SocialAuthService.getGithubProfile as jest.Mock).mockRejectedValue(error);
453
+ <% } else { %>
454
+ (SocialLoginUseCase as jest.Mock).mockImplementation(() => { throw error; });
455
+ <% } %>
456
+
457
+ await authController.githubCallback(mockRequest as Request, mockResponse as Response, nextFunction);
458
+ expect(mockResponse.redirect).toHaveBeenCalledWith('/login?error=social_auth_failed');
459
+ });
460
+ <% } %>
160
461
  });
462
+ <% } -%>
161
463
  });