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