create-nodejs-express-starter 1.7.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 (70) hide show
  1. package/.dockerignore +3 -0
  2. package/.editorconfig +9 -0
  3. package/.env.example +22 -0
  4. package/.eslintignore +2 -0
  5. package/.eslintrc.json +32 -0
  6. package/.gitignore +14 -0
  7. package/.prettierignore +3 -0
  8. package/.prettierrc.json +4 -0
  9. package/Dockerfile +15 -0
  10. package/LICENSE +21 -0
  11. package/README.md +440 -0
  12. package/bin/createNodejsApp.js +106 -0
  13. package/docker-compose.dev.yml +4 -0
  14. package/docker-compose.prod.yml +4 -0
  15. package/docker-compose.test.yml +4 -0
  16. package/docker-compose.yml +30 -0
  17. package/jest.config.js +9 -0
  18. package/package.json +117 -0
  19. package/src/app.js +82 -0
  20. package/src/config/config.js +64 -0
  21. package/src/config/logger.js +26 -0
  22. package/src/config/morgan.js +25 -0
  23. package/src/config/passport.js +30 -0
  24. package/src/config/roles.js +12 -0
  25. package/src/config/tokens.js +10 -0
  26. package/src/controllers/auth.controller.js +59 -0
  27. package/src/controllers/index.js +2 -0
  28. package/src/controllers/user.controller.js +43 -0
  29. package/src/docs/components.yml +92 -0
  30. package/src/docs/swaggerDef.js +21 -0
  31. package/src/index.js +57 -0
  32. package/src/middlewares/auth.js +33 -0
  33. package/src/middlewares/error.js +47 -0
  34. package/src/middlewares/rateLimiter.js +11 -0
  35. package/src/middlewares/requestId.js +14 -0
  36. package/src/middlewares/validate.js +21 -0
  37. package/src/models/index.js +2 -0
  38. package/src/models/plugins/index.js +2 -0
  39. package/src/models/plugins/paginate.plugin.js +70 -0
  40. package/src/models/plugins/toJSON.plugin.js +43 -0
  41. package/src/models/token.model.js +44 -0
  42. package/src/models/user.model.js +91 -0
  43. package/src/routes/v1/auth.route.js +291 -0
  44. package/src/routes/v1/docs.route.js +21 -0
  45. package/src/routes/v1/health.route.js +43 -0
  46. package/src/routes/v1/index.js +39 -0
  47. package/src/routes/v1/user.route.js +252 -0
  48. package/src/services/auth.service.js +99 -0
  49. package/src/services/email.service.js +63 -0
  50. package/src/services/index.js +4 -0
  51. package/src/services/token.service.js +123 -0
  52. package/src/services/user.service.js +89 -0
  53. package/src/utils/ApiError.js +14 -0
  54. package/src/utils/catchAsync.js +5 -0
  55. package/src/utils/pick.js +17 -0
  56. package/src/validations/auth.validation.js +60 -0
  57. package/src/validations/custom.validation.js +21 -0
  58. package/src/validations/index.js +2 -0
  59. package/src/validations/user.validation.js +54 -0
  60. package/tests/fixtures/token.fixture.js +14 -0
  61. package/tests/fixtures/user.fixture.js +46 -0
  62. package/tests/integration/auth.test.js +587 -0
  63. package/tests/integration/docs.test.js +14 -0
  64. package/tests/integration/health.test.js +32 -0
  65. package/tests/integration/user.test.js +625 -0
  66. package/tests/unit/middlewares/error.test.js +168 -0
  67. package/tests/unit/models/plugins/paginate.plugin.test.js +61 -0
  68. package/tests/unit/models/plugins/toJSON.plugin.test.js +89 -0
  69. package/tests/unit/models/user.model.test.js +57 -0
  70. package/tests/utils/setupTestDB.js +18 -0
@@ -0,0 +1,587 @@
1
+ const request = require('supertest');
2
+ const { faker } = require('@faker-js/faker');
3
+ const httpStatus = require('http-status');
4
+ const httpMocks = require('node-mocks-http');
5
+ const moment = require('moment');
6
+ const bcrypt = require('bcryptjs');
7
+ const app = require('../../src/app');
8
+ const config = require('../../src/config/config');
9
+ const auth = require('../../src/middlewares/auth');
10
+ const { tokenService, emailService } = require('../../src/services');
11
+ const ApiError = require('../../src/utils/ApiError');
12
+ const setupTestDB = require('../utils/setupTestDB');
13
+ const { User, Token } = require('../../src/models');
14
+ const { roleRights } = require('../../src/config/roles');
15
+ const { tokenTypes } = require('../../src/config/tokens');
16
+ const { userOne, admin, insertUsers } = require('../fixtures/user.fixture');
17
+ const { userOneAccessToken, adminAccessToken } = require('../fixtures/token.fixture');
18
+
19
+ setupTestDB();
20
+
21
+ describe('Auth routes', () => {
22
+ describe('POST /v1/auth/register', () => {
23
+ let newUser;
24
+ beforeEach(() => {
25
+ newUser = {
26
+ name: faker.person.fullName(),
27
+ email: faker.internet.email().toLowerCase(),
28
+ password: 'password1',
29
+ };
30
+ });
31
+
32
+ test('should return 201 and successfully register user if request data is ok', async () => {
33
+ const res = await request(app).post('/v1/auth/register').send(newUser).expect(httpStatus.CREATED);
34
+
35
+ expect(res.body.user).not.toHaveProperty('password');
36
+ expect(res.body.user).toEqual({
37
+ id: expect.anything(),
38
+ name: newUser.name,
39
+ email: newUser.email,
40
+ role: 'user',
41
+ isEmailVerified: false,
42
+ });
43
+
44
+ const dbUser = await User.findById(res.body.user.id);
45
+ expect(dbUser).toBeDefined();
46
+ expect(dbUser.password).not.toBe(newUser.password);
47
+ expect(dbUser).toMatchObject({ name: newUser.name, email: newUser.email, role: 'user', isEmailVerified: false });
48
+
49
+ expect(res.body.tokens).toEqual({
50
+ access: { token: expect.anything(), expires: expect.anything() },
51
+ refresh: { token: expect.anything(), expires: expect.anything() },
52
+ });
53
+ });
54
+
55
+ test('should return 400 error if email is invalid', async () => {
56
+ newUser.email = 'invalidEmail';
57
+
58
+ await request(app).post('/v1/auth/register').send(newUser).expect(httpStatus.BAD_REQUEST);
59
+ });
60
+
61
+ test('should return 400 error if email is already used', async () => {
62
+ await insertUsers([userOne]);
63
+ newUser.email = userOne.email;
64
+
65
+ await request(app).post('/v1/auth/register').send(newUser).expect(httpStatus.BAD_REQUEST);
66
+ });
67
+
68
+ test('should return 400 error if password length is less than 8 characters', async () => {
69
+ newUser.password = 'passwo1';
70
+
71
+ await request(app).post('/v1/auth/register').send(newUser).expect(httpStatus.BAD_REQUEST);
72
+ });
73
+
74
+ test('should return 400 error if password does not contain both letters and numbers', async () => {
75
+ newUser.password = 'password';
76
+
77
+ await request(app).post('/v1/auth/register').send(newUser).expect(httpStatus.BAD_REQUEST);
78
+
79
+ newUser.password = '11111111';
80
+
81
+ await request(app).post('/v1/auth/register').send(newUser).expect(httpStatus.BAD_REQUEST);
82
+ });
83
+ });
84
+
85
+ describe('POST /v1/auth/login', () => {
86
+ test('should return 200 and login user if email and password match', async () => {
87
+ await insertUsers([userOne]);
88
+ const loginCredentials = {
89
+ email: userOne.email,
90
+ password: userOne.password,
91
+ };
92
+
93
+ const res = await request(app).post('/v1/auth/login').send(loginCredentials).expect(httpStatus.OK);
94
+
95
+ expect(res.body.user).toEqual({
96
+ id: expect.anything(),
97
+ name: userOne.name,
98
+ email: userOne.email,
99
+ role: userOne.role,
100
+ isEmailVerified: userOne.isEmailVerified,
101
+ });
102
+
103
+ expect(res.body.tokens).toEqual({
104
+ access: { token: expect.anything(), expires: expect.anything() },
105
+ refresh: { token: expect.anything(), expires: expect.anything() },
106
+ });
107
+ });
108
+
109
+ test('should return 401 error if there are no users with that email', async () => {
110
+ const loginCredentials = {
111
+ email: userOne.email,
112
+ password: userOne.password,
113
+ };
114
+
115
+ const res = await request(app).post('/v1/auth/login').send(loginCredentials).expect(httpStatus.UNAUTHORIZED);
116
+
117
+ expect(res.body).toMatchObject({ code: httpStatus.UNAUTHORIZED, message: 'Incorrect email or password' });
118
+ });
119
+
120
+ test('should return 401 error if password is wrong', async () => {
121
+ await insertUsers([userOne]);
122
+ const loginCredentials = {
123
+ email: userOne.email,
124
+ password: 'wrongPassword1',
125
+ };
126
+
127
+ const res = await request(app).post('/v1/auth/login').send(loginCredentials).expect(httpStatus.UNAUTHORIZED);
128
+
129
+ expect(res.body).toMatchObject({ code: httpStatus.UNAUTHORIZED, message: 'Incorrect email or password' });
130
+ });
131
+ });
132
+
133
+ describe('POST /v1/auth/logout', () => {
134
+ test('should return 204 if refresh token is valid', async () => {
135
+ await insertUsers([userOne]);
136
+ const expires = moment().add(config.jwt.refreshExpirationDays, 'days');
137
+ const refreshToken = tokenService.generateToken(userOne._id, expires, tokenTypes.REFRESH);
138
+ await tokenService.saveToken(refreshToken, userOne._id, expires, tokenTypes.REFRESH);
139
+
140
+ await request(app).post('/v1/auth/logout').send({ refreshToken }).expect(httpStatus.NO_CONTENT);
141
+
142
+ const dbRefreshTokenDoc = await Token.findOne({ token: refreshToken });
143
+ expect(dbRefreshTokenDoc).toBe(null);
144
+ });
145
+
146
+ test('should return 400 error if refresh token is missing from request body', async () => {
147
+ await request(app).post('/v1/auth/logout').send().expect(httpStatus.BAD_REQUEST);
148
+ });
149
+
150
+ test('should return 404 error if refresh token is not found in the database', async () => {
151
+ await insertUsers([userOne]);
152
+ const expires = moment().add(config.jwt.refreshExpirationDays, 'days');
153
+ const refreshToken = tokenService.generateToken(userOne._id, expires, tokenTypes.REFRESH);
154
+
155
+ await request(app).post('/v1/auth/logout').send({ refreshToken }).expect(httpStatus.NOT_FOUND);
156
+ });
157
+
158
+ test('should return 404 error if refresh token is blacklisted', async () => {
159
+ await insertUsers([userOne]);
160
+ const expires = moment().add(config.jwt.refreshExpirationDays, 'days');
161
+ const refreshToken = tokenService.generateToken(userOne._id, expires, tokenTypes.REFRESH);
162
+ await tokenService.saveToken(refreshToken, userOne._id, expires, tokenTypes.REFRESH, true);
163
+
164
+ await request(app).post('/v1/auth/logout').send({ refreshToken }).expect(httpStatus.NOT_FOUND);
165
+ });
166
+ });
167
+
168
+ describe('POST /v1/auth/refresh-tokens', () => {
169
+ test('should return 200 and new auth tokens if refresh token is valid', async () => {
170
+ await insertUsers([userOne]);
171
+ const expires = moment().add(config.jwt.refreshExpirationDays, 'days');
172
+ const refreshToken = tokenService.generateToken(userOne._id, expires, tokenTypes.REFRESH);
173
+ await tokenService.saveToken(refreshToken, userOne._id, expires, tokenTypes.REFRESH);
174
+
175
+ const res = await request(app).post('/v1/auth/refresh-tokens').send({ refreshToken }).expect(httpStatus.OK);
176
+
177
+ expect(res.body).toEqual({
178
+ access: { token: expect.anything(), expires: expect.anything() },
179
+ refresh: { token: expect.anything(), expires: expect.anything() },
180
+ });
181
+
182
+ const dbRefreshTokenDoc = await Token.findOne({ token: res.body.refresh.token });
183
+ expect(dbRefreshTokenDoc).toMatchObject({ type: tokenTypes.REFRESH, user: userOne._id, blacklisted: false });
184
+
185
+ const dbRefreshTokenCount = await Token.countDocuments();
186
+ expect(dbRefreshTokenCount).toBe(1);
187
+ });
188
+
189
+ test('should return 400 error if refresh token is missing from request body', async () => {
190
+ await request(app).post('/v1/auth/refresh-tokens').send().expect(httpStatus.BAD_REQUEST);
191
+ });
192
+
193
+ test('should return 401 error if refresh token is signed using an invalid secret', async () => {
194
+ await insertUsers([userOne]);
195
+ const expires = moment().add(config.jwt.refreshExpirationDays, 'days');
196
+ const refreshToken = tokenService.generateToken(userOne._id, expires, tokenTypes.REFRESH, 'invalidSecret');
197
+ await tokenService.saveToken(refreshToken, userOne._id, expires, tokenTypes.REFRESH);
198
+
199
+ await request(app).post('/v1/auth/refresh-tokens').send({ refreshToken }).expect(httpStatus.UNAUTHORIZED);
200
+ });
201
+
202
+ test('should return 401 error if refresh token is not found in the database', async () => {
203
+ await insertUsers([userOne]);
204
+ const expires = moment().add(config.jwt.refreshExpirationDays, 'days');
205
+ const refreshToken = tokenService.generateToken(userOne._id, expires, tokenTypes.REFRESH);
206
+
207
+ await request(app).post('/v1/auth/refresh-tokens').send({ refreshToken }).expect(httpStatus.UNAUTHORIZED);
208
+ });
209
+
210
+ test('should return 401 error if refresh token is blacklisted', async () => {
211
+ await insertUsers([userOne]);
212
+ const expires = moment().add(config.jwt.refreshExpirationDays, 'days');
213
+ const refreshToken = tokenService.generateToken(userOne._id, expires, tokenTypes.REFRESH);
214
+ await tokenService.saveToken(refreshToken, userOne._id, expires, tokenTypes.REFRESH, true);
215
+
216
+ await request(app).post('/v1/auth/refresh-tokens').send({ refreshToken }).expect(httpStatus.UNAUTHORIZED);
217
+ });
218
+
219
+ test('should return 401 error if refresh token is expired', async () => {
220
+ await insertUsers([userOne]);
221
+ const expires = moment().subtract(1, 'minutes');
222
+ const refreshToken = tokenService.generateToken(userOne._id, expires);
223
+ await tokenService.saveToken(refreshToken, userOne._id, expires, tokenTypes.REFRESH);
224
+
225
+ await request(app).post('/v1/auth/refresh-tokens').send({ refreshToken }).expect(httpStatus.UNAUTHORIZED);
226
+ });
227
+
228
+ test('should return 401 error if user is not found', async () => {
229
+ const expires = moment().add(config.jwt.refreshExpirationDays, 'days');
230
+ const refreshToken = tokenService.generateToken(userOne._id, expires, tokenTypes.REFRESH);
231
+ await tokenService.saveToken(refreshToken, userOne._id, expires, tokenTypes.REFRESH);
232
+
233
+ await request(app).post('/v1/auth/refresh-tokens').send({ refreshToken }).expect(httpStatus.UNAUTHORIZED);
234
+ });
235
+ });
236
+
237
+ describe('POST /v1/auth/forgot-password', () => {
238
+ beforeEach(() => {
239
+ jest.spyOn(emailService.transport, 'sendMail').mockResolvedValue();
240
+ });
241
+
242
+ test('should return 204 and send reset password email to the user', async () => {
243
+ await insertUsers([userOne]);
244
+ const sendResetPasswordEmailSpy = jest.spyOn(emailService, 'sendResetPasswordEmail');
245
+
246
+ await request(app).post('/v1/auth/forgot-password').send({ email: userOne.email }).expect(httpStatus.NO_CONTENT);
247
+
248
+ expect(sendResetPasswordEmailSpy).toHaveBeenCalledWith(userOne.email, expect.any(String));
249
+ const resetPasswordToken = sendResetPasswordEmailSpy.mock.calls[0][1];
250
+ const dbResetPasswordTokenDoc = await Token.findOne({ token: resetPasswordToken, user: userOne._id });
251
+ expect(dbResetPasswordTokenDoc).toBeDefined();
252
+ });
253
+
254
+ test('should return 400 if email is missing', async () => {
255
+ await insertUsers([userOne]);
256
+
257
+ await request(app).post('/v1/auth/forgot-password').send().expect(httpStatus.BAD_REQUEST);
258
+ });
259
+
260
+ test('should return 404 if email does not belong to any user', async () => {
261
+ await request(app).post('/v1/auth/forgot-password').send({ email: userOne.email }).expect(httpStatus.NOT_FOUND);
262
+ });
263
+ });
264
+
265
+ describe('POST /v1/auth/reset-password', () => {
266
+ test('should return 204 and reset the password', async () => {
267
+ await insertUsers([userOne]);
268
+ const expires = moment().add(config.jwt.resetPasswordExpirationMinutes, 'minutes');
269
+ const resetPasswordToken = tokenService.generateToken(userOne._id, expires, tokenTypes.RESET_PASSWORD);
270
+ await tokenService.saveToken(resetPasswordToken, userOne._id, expires, tokenTypes.RESET_PASSWORD);
271
+
272
+ await request(app)
273
+ .post('/v1/auth/reset-password')
274
+ .query({ token: resetPasswordToken })
275
+ .send({ password: 'password2' })
276
+ .expect(httpStatus.NO_CONTENT);
277
+
278
+ const dbUser = await User.findById(userOne._id);
279
+ const isPasswordMatch = await bcrypt.compare('password2', dbUser.password);
280
+ expect(isPasswordMatch).toBe(true);
281
+
282
+ const dbResetPasswordTokenCount = await Token.countDocuments({ user: userOne._id, type: tokenTypes.RESET_PASSWORD });
283
+ expect(dbResetPasswordTokenCount).toBe(0);
284
+ });
285
+
286
+ test('should return 400 if reset password token is missing', async () => {
287
+ await insertUsers([userOne]);
288
+
289
+ await request(app).post('/v1/auth/reset-password').send({ password: 'password2' }).expect(httpStatus.BAD_REQUEST);
290
+ });
291
+
292
+ test('should return 401 if reset password token is blacklisted', async () => {
293
+ await insertUsers([userOne]);
294
+ const expires = moment().add(config.jwt.resetPasswordExpirationMinutes, 'minutes');
295
+ const resetPasswordToken = tokenService.generateToken(userOne._id, expires, tokenTypes.RESET_PASSWORD);
296
+ await tokenService.saveToken(resetPasswordToken, userOne._id, expires, tokenTypes.RESET_PASSWORD, true);
297
+
298
+ await request(app)
299
+ .post('/v1/auth/reset-password')
300
+ .query({ token: resetPasswordToken })
301
+ .send({ password: 'password2' })
302
+ .expect(httpStatus.UNAUTHORIZED);
303
+ });
304
+
305
+ test('should return 401 if reset password token is expired', async () => {
306
+ await insertUsers([userOne]);
307
+ const expires = moment().subtract(1, 'minutes');
308
+ const resetPasswordToken = tokenService.generateToken(userOne._id, expires, tokenTypes.RESET_PASSWORD);
309
+ await tokenService.saveToken(resetPasswordToken, userOne._id, expires, tokenTypes.RESET_PASSWORD);
310
+
311
+ await request(app)
312
+ .post('/v1/auth/reset-password')
313
+ .query({ token: resetPasswordToken })
314
+ .send({ password: 'password2' })
315
+ .expect(httpStatus.UNAUTHORIZED);
316
+ });
317
+
318
+ test('should return 401 if user is not found', async () => {
319
+ const expires = moment().add(config.jwt.resetPasswordExpirationMinutes, 'minutes');
320
+ const resetPasswordToken = tokenService.generateToken(userOne._id, expires, tokenTypes.RESET_PASSWORD);
321
+ await tokenService.saveToken(resetPasswordToken, userOne._id, expires, tokenTypes.RESET_PASSWORD);
322
+
323
+ await request(app)
324
+ .post('/v1/auth/reset-password')
325
+ .query({ token: resetPasswordToken })
326
+ .send({ password: 'password2' })
327
+ .expect(httpStatus.UNAUTHORIZED);
328
+ });
329
+
330
+ test('should return 400 if password is missing or invalid', async () => {
331
+ await insertUsers([userOne]);
332
+ const expires = moment().add(config.jwt.resetPasswordExpirationMinutes, 'minutes');
333
+ const resetPasswordToken = tokenService.generateToken(userOne._id, expires, tokenTypes.RESET_PASSWORD);
334
+ await tokenService.saveToken(resetPasswordToken, userOne._id, expires, tokenTypes.RESET_PASSWORD);
335
+
336
+ await request(app).post('/v1/auth/reset-password').query({ token: resetPasswordToken }).expect(httpStatus.BAD_REQUEST);
337
+
338
+ await request(app)
339
+ .post('/v1/auth/reset-password')
340
+ .query({ token: resetPasswordToken })
341
+ .send({ password: 'short1' })
342
+ .expect(httpStatus.BAD_REQUEST);
343
+
344
+ await request(app)
345
+ .post('/v1/auth/reset-password')
346
+ .query({ token: resetPasswordToken })
347
+ .send({ password: 'password' })
348
+ .expect(httpStatus.BAD_REQUEST);
349
+
350
+ await request(app)
351
+ .post('/v1/auth/reset-password')
352
+ .query({ token: resetPasswordToken })
353
+ .send({ password: '11111111' })
354
+ .expect(httpStatus.BAD_REQUEST);
355
+ });
356
+ });
357
+
358
+ describe('POST /v1/auth/send-verification-email', () => {
359
+ beforeEach(() => {
360
+ jest.spyOn(emailService.transport, 'sendMail').mockResolvedValue();
361
+ });
362
+
363
+ test('should return 204 and send verification email to the user', async () => {
364
+ await insertUsers([userOne]);
365
+ const sendVerificationEmailSpy = jest.spyOn(emailService, 'sendVerificationEmail');
366
+
367
+ await request(app)
368
+ .post('/v1/auth/send-verification-email')
369
+ .set('Authorization', `Bearer ${userOneAccessToken}`)
370
+ .expect(httpStatus.NO_CONTENT);
371
+
372
+ expect(sendVerificationEmailSpy).toHaveBeenCalledWith(userOne.email, expect.any(String));
373
+ const verifyEmailToken = sendVerificationEmailSpy.mock.calls[0][1];
374
+ const dbVerifyEmailToken = await Token.findOne({ token: verifyEmailToken, user: userOne._id });
375
+
376
+ expect(dbVerifyEmailToken).toBeDefined();
377
+ });
378
+
379
+ test('should return 401 error if access token is missing', async () => {
380
+ await insertUsers([userOne]);
381
+
382
+ await request(app).post('/v1/auth/send-verification-email').send().expect(httpStatus.UNAUTHORIZED);
383
+ });
384
+ });
385
+
386
+ describe('POST /v1/auth/verify-email', () => {
387
+ test('should return 204 and verify the email', async () => {
388
+ await insertUsers([userOne]);
389
+ const expires = moment().add(config.jwt.verifyEmailExpirationMinutes, 'minutes');
390
+ const verifyEmailToken = tokenService.generateToken(userOne._id, expires);
391
+ await tokenService.saveToken(verifyEmailToken, userOne._id, expires, tokenTypes.VERIFY_EMAIL);
392
+
393
+ await request(app)
394
+ .post('/v1/auth/verify-email')
395
+ .query({ token: verifyEmailToken })
396
+ .send()
397
+ .expect(httpStatus.NO_CONTENT);
398
+
399
+ const dbUser = await User.findById(userOne._id);
400
+
401
+ expect(dbUser.isEmailVerified).toBe(true);
402
+
403
+ const dbVerifyEmailToken = await Token.countDocuments({
404
+ user: userOne._id,
405
+ type: tokenTypes.VERIFY_EMAIL,
406
+ });
407
+ expect(dbVerifyEmailToken).toBe(0);
408
+ });
409
+
410
+ test('should return 400 if verify email token is missing', async () => {
411
+ await insertUsers([userOne]);
412
+
413
+ await request(app).post('/v1/auth/verify-email').send().expect(httpStatus.BAD_REQUEST);
414
+ });
415
+
416
+ test('should return 401 if verify email token is blacklisted', async () => {
417
+ await insertUsers([userOne]);
418
+ const expires = moment().add(config.jwt.verifyEmailExpirationMinutes, 'minutes');
419
+ const verifyEmailToken = tokenService.generateToken(userOne._id, expires);
420
+ await tokenService.saveToken(verifyEmailToken, userOne._id, expires, tokenTypes.VERIFY_EMAIL, true);
421
+
422
+ await request(app)
423
+ .post('/v1/auth/verify-email')
424
+ .query({ token: verifyEmailToken })
425
+ .send()
426
+ .expect(httpStatus.UNAUTHORIZED);
427
+ });
428
+
429
+ test('should return 401 if verify email token is expired', async () => {
430
+ await insertUsers([userOne]);
431
+ const expires = moment().subtract(1, 'minutes');
432
+ const verifyEmailToken = tokenService.generateToken(userOne._id, expires);
433
+ await tokenService.saveToken(verifyEmailToken, userOne._id, expires, tokenTypes.VERIFY_EMAIL);
434
+
435
+ await request(app)
436
+ .post('/v1/auth/verify-email')
437
+ .query({ token: verifyEmailToken })
438
+ .send()
439
+ .expect(httpStatus.UNAUTHORIZED);
440
+ });
441
+
442
+ test('should return 401 if user is not found', async () => {
443
+ const expires = moment().add(config.jwt.verifyEmailExpirationMinutes, 'minutes');
444
+ const verifyEmailToken = tokenService.generateToken(userOne._id, expires);
445
+ await tokenService.saveToken(verifyEmailToken, userOne._id, expires, tokenTypes.VERIFY_EMAIL);
446
+
447
+ await request(app)
448
+ .post('/v1/auth/verify-email')
449
+ .query({ token: verifyEmailToken })
450
+ .send()
451
+ .expect(httpStatus.UNAUTHORIZED);
452
+ });
453
+ });
454
+ });
455
+
456
+ describe('Auth middleware', () => {
457
+ test('should call next with no errors if access token is valid', async () => {
458
+ await insertUsers([userOne]);
459
+ const req = httpMocks.createRequest({ headers: { Authorization: `Bearer ${userOneAccessToken}` } });
460
+ const next = jest.fn();
461
+
462
+ await auth()(req, httpMocks.createResponse(), next);
463
+
464
+ expect(next).toHaveBeenCalledWith();
465
+ expect(req.user._id).toEqual(userOne._id);
466
+ });
467
+
468
+ test('should call next with unauthorized error if access token is not found in header', async () => {
469
+ await insertUsers([userOne]);
470
+ const req = httpMocks.createRequest();
471
+ const next = jest.fn();
472
+
473
+ await auth()(req, httpMocks.createResponse(), next);
474
+
475
+ expect(next).toHaveBeenCalledWith(expect.any(ApiError));
476
+ expect(next).toHaveBeenCalledWith(
477
+ expect.objectContaining({ statusCode: httpStatus.UNAUTHORIZED, message: 'Please authenticate' }),
478
+ );
479
+ });
480
+
481
+ test('should call next with unauthorized error if access token is not a valid jwt token', async () => {
482
+ await insertUsers([userOne]);
483
+ const req = httpMocks.createRequest({ headers: { Authorization: 'Bearer randomToken' } });
484
+ const next = jest.fn();
485
+
486
+ await auth()(req, httpMocks.createResponse(), next);
487
+
488
+ expect(next).toHaveBeenCalledWith(expect.any(ApiError));
489
+ expect(next).toHaveBeenCalledWith(
490
+ expect.objectContaining({ statusCode: httpStatus.UNAUTHORIZED, message: 'Please authenticate' }),
491
+ );
492
+ });
493
+
494
+ test('should call next with unauthorized error if the token is not an access token', async () => {
495
+ await insertUsers([userOne]);
496
+ const expires = moment().add(config.jwt.accessExpirationMinutes, 'minutes');
497
+ const refreshToken = tokenService.generateToken(userOne._id, expires, tokenTypes.REFRESH);
498
+ const req = httpMocks.createRequest({ headers: { Authorization: `Bearer ${refreshToken}` } });
499
+ const next = jest.fn();
500
+
501
+ await auth()(req, httpMocks.createResponse(), next);
502
+
503
+ expect(next).toHaveBeenCalledWith(expect.any(ApiError));
504
+ expect(next).toHaveBeenCalledWith(
505
+ expect.objectContaining({ statusCode: httpStatus.UNAUTHORIZED, message: 'Please authenticate' }),
506
+ );
507
+ });
508
+
509
+ test('should call next with unauthorized error if access token is generated with an invalid secret', async () => {
510
+ await insertUsers([userOne]);
511
+ const expires = moment().add(config.jwt.accessExpirationMinutes, 'minutes');
512
+ const accessToken = tokenService.generateToken(userOne._id, expires, tokenTypes.ACCESS, 'invalidSecret');
513
+ const req = httpMocks.createRequest({ headers: { Authorization: `Bearer ${accessToken}` } });
514
+ const next = jest.fn();
515
+
516
+ await auth()(req, httpMocks.createResponse(), next);
517
+
518
+ expect(next).toHaveBeenCalledWith(expect.any(ApiError));
519
+ expect(next).toHaveBeenCalledWith(
520
+ expect.objectContaining({ statusCode: httpStatus.UNAUTHORIZED, message: 'Please authenticate' }),
521
+ );
522
+ });
523
+
524
+ test('should call next with unauthorized error if access token is expired', async () => {
525
+ await insertUsers([userOne]);
526
+ const expires = moment().subtract(1, 'minutes');
527
+ const accessToken = tokenService.generateToken(userOne._id, expires, tokenTypes.ACCESS);
528
+ const req = httpMocks.createRequest({ headers: { Authorization: `Bearer ${accessToken}` } });
529
+ const next = jest.fn();
530
+
531
+ await auth()(req, httpMocks.createResponse(), next);
532
+
533
+ expect(next).toHaveBeenCalledWith(expect.any(ApiError));
534
+ expect(next).toHaveBeenCalledWith(
535
+ expect.objectContaining({ statusCode: httpStatus.UNAUTHORIZED, message: 'Please authenticate' }),
536
+ );
537
+ });
538
+
539
+ test('should call next with unauthorized error if user is not found', async () => {
540
+ const req = httpMocks.createRequest({ headers: { Authorization: `Bearer ${userOneAccessToken}` } });
541
+ const next = jest.fn();
542
+
543
+ await auth()(req, httpMocks.createResponse(), next);
544
+
545
+ expect(next).toHaveBeenCalledWith(expect.any(ApiError));
546
+ expect(next).toHaveBeenCalledWith(
547
+ expect.objectContaining({ statusCode: httpStatus.UNAUTHORIZED, message: 'Please authenticate' }),
548
+ );
549
+ });
550
+
551
+ test('should call next with forbidden error if user does not have required rights and userId is not in params', async () => {
552
+ await insertUsers([userOne]);
553
+ const req = httpMocks.createRequest({ headers: { Authorization: `Bearer ${userOneAccessToken}` } });
554
+ const next = jest.fn();
555
+
556
+ await auth('anyRight')(req, httpMocks.createResponse(), next);
557
+
558
+ expect(next).toHaveBeenCalledWith(expect.any(ApiError));
559
+ expect(next).toHaveBeenCalledWith(expect.objectContaining({ statusCode: httpStatus.FORBIDDEN, message: 'Forbidden' }));
560
+ });
561
+
562
+ test('should call next with no errors if user does not have required rights but userId is in params', async () => {
563
+ await insertUsers([userOne]);
564
+ const req = httpMocks.createRequest({
565
+ headers: { Authorization: `Bearer ${userOneAccessToken}` },
566
+ params: { userId: userOne._id.toHexString() },
567
+ });
568
+ const next = jest.fn();
569
+
570
+ await auth('anyRight')(req, httpMocks.createResponse(), next);
571
+
572
+ expect(next).toHaveBeenCalledWith();
573
+ });
574
+
575
+ test('should call next with no errors if user has required rights', async () => {
576
+ await insertUsers([admin]);
577
+ const req = httpMocks.createRequest({
578
+ headers: { Authorization: `Bearer ${adminAccessToken}` },
579
+ params: { userId: userOne._id.toHexString() },
580
+ });
581
+ const next = jest.fn();
582
+
583
+ await auth(...roleRights.get('admin'))(req, httpMocks.createResponse(), next);
584
+
585
+ expect(next).toHaveBeenCalledWith();
586
+ });
587
+ });
@@ -0,0 +1,14 @@
1
+ const request = require('supertest');
2
+ const httpStatus = require('http-status');
3
+ const app = require('../../src/app');
4
+ const config = require('../../src/config/config');
5
+
6
+ describe('Docs routes', () => {
7
+ describe('GET /v1/docs', () => {
8
+ test('should return 404 when running in production', async () => {
9
+ config.env = 'production';
10
+ await request(app).get('/v1/docs').send().expect(httpStatus.NOT_FOUND);
11
+ config.env = process.env.NODE_ENV;
12
+ });
13
+ });
14
+ });
@@ -0,0 +1,32 @@
1
+ const request = require('supertest');
2
+ const httpStatus = require('http-status');
3
+ const mongoose = require('mongoose');
4
+ const app = require('../../src/app');
5
+ const setupTestDB = require('../utils/setupTestDB');
6
+
7
+ setupTestDB();
8
+
9
+ describe('Health routes', () => {
10
+ describe('GET /health', () => {
11
+ test('should return 200 and health status', async () => {
12
+ const res = await request(app).get('/health').send().expect(httpStatus.OK);
13
+
14
+ expect(res.body).toHaveProperty('status', 'ok');
15
+ expect(res.body).toHaveProperty('timestamp');
16
+ expect(res.body).toHaveProperty('uptime');
17
+ expect(res.headers['x-request-id']).toBeDefined();
18
+ });
19
+ });
20
+
21
+ describe('GET /health/ready', () => {
22
+ test('should return 200 when database is connected', async () => {
23
+ // setupTestDB connects in beforeAll, so mongoose should already be connected
24
+ expect(mongoose.connection.readyState).toBe(1);
25
+
26
+ const res = await request(app).get('/health/ready').send().expect(httpStatus.OK);
27
+
28
+ expect(res.body).toHaveProperty('status', 'ready');
29
+ expect(res.body).toHaveProperty('database', 'connected');
30
+ });
31
+ });
32
+ });