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.
- package/CHANGELOG.md +20 -0
- package/README.md +12 -17
- package/bin/index.js +1 -0
- package/lib/generator.js +1 -1
- package/lib/modules/app-setup.js +16 -0
- package/lib/modules/auth-setup.js +46 -4
- package/lib/prompts.js +49 -5
- package/package.json +1 -1
- package/templates/clean-architecture/js/src/infrastructure/config/env.js.ejs +12 -2
- package/templates/clean-architecture/js/src/infrastructure/repositories/UserRepository.js.ejs +27 -0
- package/templates/clean-architecture/js/src/infrastructure/repositories/UserRepository.spec.js.ejs +24 -0
- package/templates/clean-architecture/js/src/infrastructure/webserver/server.js.ejs +5 -1
- package/templates/clean-architecture/ts/src/config/env.ts.ejs +12 -2
- package/templates/clean-architecture/ts/src/domain/user.ts.ejs +14 -0
- package/templates/clean-architecture/ts/src/index.ts.ejs +2 -0
- package/templates/clean-architecture/ts/src/infrastructure/repositories/UserRepository.spec.ts.ejs +24 -0
- package/templates/clean-architecture/ts/src/infrastructure/repositories/userRepository.ts.ejs +43 -45
- package/templates/clean-architecture/ts/src/interfaces/graphql/resolvers/user.resolvers.ts.ejs +5 -5
- package/templates/clean-architecture/ts/src/utils/httpCodes.ts +1 -0
- package/templates/common/.env.example.ejs +10 -0
- package/templates/common/README.md.ejs +65 -14
- package/templates/common/auth/js/controllers/authController.js.ejs +356 -13
- package/templates/common/auth/js/controllers/authController.spec.js.ejs +329 -53
- package/templates/common/auth/js/middleware/authMiddleware.js.ejs +10 -6
- package/templates/common/auth/js/routes/authRoutes.js.ejs +11 -0
- package/templates/common/auth/js/services/jwtService.spec.js.ejs +30 -0
- package/templates/common/auth/js/services/socialAuthService.js.ejs +175 -0
- package/templates/common/auth/js/services/socialAuthService.spec.js.ejs +192 -0
- package/templates/common/auth/js/usecases/SocialLoginUseCase.js.ejs +114 -0
- package/templates/common/auth/js/usecases/SocialLoginUseCase.spec.js.ejs +143 -0
- package/templates/common/auth/ts/controllers/authController.spec.ts.ejs +366 -64
- package/templates/common/auth/ts/controllers/authController.ts.ejs +370 -9
- package/templates/common/auth/ts/middleware/authMiddleware.ts.ejs +10 -6
- package/templates/common/auth/ts/routes/authRoutes.ts.ejs +11 -0
- package/templates/common/auth/ts/services/jwtService.spec.ts.ejs +18 -0
- package/templates/common/auth/ts/services/jwtService.ts.ejs +3 -3
- package/templates/common/auth/ts/services/socialAuthService.spec.ts.ejs +187 -0
- package/templates/common/auth/ts/services/socialAuthService.ts.ejs +189 -0
- package/templates/common/auth/ts/usecases/SocialLoginUseCase.spec.ts.ejs +143 -0
- package/templates/common/auth/ts/usecases/SocialLoginUseCase.ts.ejs +117 -0
- package/templates/common/database/js/models/User.js.ejs +13 -5
- package/templates/common/database/js/models/User.js.mongoose.ejs +15 -1
- package/templates/common/database/ts/models/User.ts.ejs +23 -7
- package/templates/common/database/ts/models/User.ts.mongoose.ejs +18 -2
- package/templates/common/docker-compose.yml.ejs +21 -0
- package/templates/common/ecosystem.config.js.ejs +10 -0
- package/templates/common/eslint.config.mjs.ejs +4 -1
- package/templates/common/jest.config.js.ejs +1 -1
- package/templates/common/kafka/ts/services/kafkaService.ts.ejs +1 -1
- package/templates/common/package.json.ejs +4 -0
- package/templates/common/src/tests/e2e/e2e.users.test.js.ejs +13 -1
- package/templates/common/src/tests/e2e/e2e.users.test.ts.ejs +13 -1
- package/templates/common/swagger.yml.ejs +62 -3
- package/templates/common/views/ejs/login.ejs.ejs +84 -0
- package/templates/common/views/ejs/signup.ejs.ejs +84 -0
- package/templates/common/views/pug/login.pug.ejs +78 -0
- package/templates/common/views/pug/signup.pug.ejs +78 -0
- package/templates/db/mysql/V1__Initial_Setup.sql.ejs +3 -1
- package/templates/db/postgres/V1__Initial_Setup.sql.ejs +3 -1
- package/templates/mvc/js/src/config/env.js.ejs +12 -2
- package/templates/mvc/js/src/controllers/userController.js.ejs +1 -1
- package/templates/mvc/js/src/graphql/resolvers/user.resolvers.js.ejs +4 -3
- package/templates/mvc/js/src/index.js.ejs +2 -0
- package/templates/mvc/js/src/utils/httpCodes.js +1 -0
- package/templates/mvc/ts/src/config/env.ts.ejs +12 -2
- package/templates/mvc/ts/src/controllers/userController.ts.ejs +1 -0
- package/templates/mvc/ts/src/graphql/resolvers/user.resolvers.ts.ejs +5 -5
- package/templates/mvc/ts/src/index.ts.ejs +4 -1
- package/templates/mvc/ts/src/utils/httpCodes.ts +1 -0
- 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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
|
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:
|
|
43
|
-
let mockResponse:
|
|
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(
|
|
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
|
|
97
|
-
const
|
|
98
|
-
mockRequest.body = { email: 'test@test.com', password: '
|
|
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
|
|
109
|
-
mockRequest.body = { refreshToken: '
|
|
110
|
-
|
|
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.
|
|
113
|
-
(JwtService.
|
|
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
|
-
(
|
|
145
|
+
(_cacheService.get as jest.Mock).mockResolvedValue(['test-jti']);
|
|
119
146
|
<% } else { %>
|
|
120
|
-
JwtService.activeRefreshTokens.set('1', ['
|
|
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(
|
|
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
|
|
130
|
-
mockRequest.body = { refreshToken: '
|
|
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
|
-
(
|
|
161
|
+
(_cacheService.get as jest.Mock).mockResolvedValue(['other-jti']);
|
|
136
162
|
<% } else { %>
|
|
137
|
-
JwtService.activeRefreshTokens.set('1', ['
|
|
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
|
-
|
|
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
|
|
149
|
-
mockRequest.headers = {
|
|
150
|
-
mockRequest
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
});
|