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.
- package/CHANGELOG.md +11 -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 +44 -4
- 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 +3 -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/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/common/.env.example.ejs +10 -0
- package/templates/common/README.md.ejs +65 -14
- package/templates/common/auth/js/controllers/authController.js.ejs +326 -13
- package/templates/common/auth/js/controllers/authController.spec.js.ejs +237 -51
- 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.js.ejs +3 -3
- 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 +194 -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 +344 -64
- package/templates/common/auth/ts/controllers/authController.ts.ejs +341 -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/jest.config.js.ejs +1 -1
- package/templates/common/kafka/js/services/kafkaService.js.ejs +1 -1
- package/templates/common/kafka/ts/services/kafkaService.ts.ejs +1 -1
- package/templates/common/package.json.ejs +2 -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/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 +2 -1
- 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,29 @@ 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: {}
|
|
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(
|
|
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
|
|
97
|
-
const
|
|
98
|
-
mockRequest.body = { email: 'test@test.com', password: '
|
|
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
|
|
109
|
-
mockRequest.body = { refreshToken: '
|
|
110
|
-
|
|
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.
|
|
113
|
-
(JwtService.
|
|
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
|
-
(
|
|
143
|
+
(_cacheService.get as jest.Mock).mockResolvedValue(['test-jti']);
|
|
119
144
|
<% } else { %>
|
|
120
|
-
JwtService.activeRefreshTokens.set('1', ['
|
|
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(
|
|
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
|
|
130
|
-
mockRequest.body = { refreshToken: '
|
|
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
|
-
(
|
|
159
|
+
(_cacheService.get as jest.Mock).mockResolvedValue(['other-jti']);
|
|
136
160
|
<% } else { %>
|
|
137
|
-
JwtService.activeRefreshTokens.set('1', ['
|
|
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
|
-
|
|
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
|
|
149
|
-
mockRequest.headers = {
|
|
150
|
-
mockRequest
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
});
|