From a68c919510fe740d6186a641424d703952eed395 Mon Sep 17 00:00:00 2001 From: "Embruch, Gerd" <gerd.embruch@uni-hamburg.de> Date: Sat, 27 Jul 2024 16:19:24 +0200 Subject: [PATCH] finished testing route auth/login --- .../auth/__snapshots__/login.test.js.snap | 48 +++++ __tests__/auth/confirmverification.test.js | 6 +- __tests__/auth/login.test.js | 171 ++++++++++++++++++ __tests__/auth/requestverification.test.js | 1 - __tests__/manualREST/users.rest | 2 +- __tests__/users/signup.test.js | 6 +- controllers/Auth.js | 15 +- controllers/User.js | 2 +- logs/__tests__/users/login.test.js | 156 ---------------- models/User.js | 6 +- 10 files changed, 240 insertions(+), 173 deletions(-) create mode 100644 __tests__/auth/__snapshots__/login.test.js.snap create mode 100644 __tests__/auth/login.test.js delete mode 100644 logs/__tests__/users/login.test.js diff --git a/__tests__/auth/__snapshots__/login.test.js.snap b/__tests__/auth/__snapshots__/login.test.js.snap new file mode 100644 index 0000000..756066c --- /dev/null +++ b/__tests__/auth/__snapshots__/login.test.js.snap @@ -0,0 +1,48 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`user login > given email and password are valid, but accout is unverified > should respond with a proper body 1`] = ` +{ + "message": "Your account is still unverified. Check your emails for the verification link.", +} +`; + +exports[`user login > given the email is unknown > should respond with a proper body 1`] = ` +{ + "message": "Unknown combination of login credentials.", +} +`; + +exports[`user login > given the inputs are valid > should respond with a proper body 1`] = ` +{ + "accessToken": Any<String>, + "document": { + "__v": 0, + "_id": "66a29da2942b3eb", + "createdAt": "2024-07 - 25T18: 46: 58.982Z", + "email": "user@mail.local", + "id": "66a29da2942b3ebcaf047f07", + "name": "My User", + "role": 0, + "updatedAt": "2024-07 - 25T18: 46: 58.982Z", + "username": "snoopy", + "verified": true, + }, + "message": "Successfully logged in", +} +`; + +exports[`user login > given the password is wrong > should respond with a proper body 1`] = ` +{ + "message": "Unknown combination of login credentials", +} +`; + +exports[`user login > given the request body is empty > should respond with a proper body 1`] = ` +{ + "message": "Validation errors. Please check the error messages.", + "validationErrors": { + "email": "Required", + "password": "Required", + }, +} +`; diff --git a/__tests__/auth/confirmverification.test.js b/__tests__/auth/confirmverification.test.js index 4851810..43041e1 100644 --- a/__tests__/auth/confirmverification.test.js +++ b/__tests__/auth/confirmverification.test.js @@ -25,17 +25,16 @@ const mockedVals = vi.hoisted(() => { createdAt: '2024-07 - 25T18: 46: 58.982Z', updatedAt: '2024-07 - 25T18: 46: 58.982Z', __v: 0, - fullname: '', id: '66a29da2942b3ebcaf047f07' } }, validInput: { - email: 'john.doe@local.local', + email: 'user@mail.local', token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbGIBBERISHTl9.lxQ5ZqO8qWJt15bbnSa4wrPQ02_7fvY4CgN1ZRM' }, jwtPayload: { "id": "66a29da2942b3ebcaf047f07", - "email": "john.doe@local.local", + "email": "user@mail.local", "iat": 1722018249, "exp": 1722021849 }, @@ -58,7 +57,6 @@ vi.mock('../../utils/handleDB.js', async (importOriginal) => { ...await importOriginal(), dbConnection: vi.fn(() => 'mocked'), findOneRecord: vi.fn(() => mockedVals.foundUser), - createRecord: vi.fn(() => mockedVals.foundUser), updateOneRecord: vi.fn(() => mockedVals.foundUser) }; }); diff --git a/__tests__/auth/login.test.js b/__tests__/auth/login.test.js new file mode 100644 index 0000000..99ae46d --- /dev/null +++ b/__tests__/auth/login.test.js @@ -0,0 +1,171 @@ +// import vitest, supertest & app +import { vi, beforeAll, beforeEach, describe, expect, expectTypeOf, test, it, afterEach } from 'vitest'; +import supertest from "supertest"; +import app from "../../app.js"; +import bcrypt from 'bcrypt'; + +// set route +const ROUTE = '/auth/login'; +// prepare response of each test +let response; + +// ############################ +// OBJECTS +// ############################ +const mockedVals = vi.hoisted(() => { + // const password = await bcrypt.hash('StrongPass1!', Number(process.env.BCRYPT_STRENGTH)); + + return { + foundUser: { + _id: '66a29da2942b3eb', + username: 'snoopy', + name: 'My User', + email: 'user@mail.local', + verified: true, + role: 0, + createdAt: '2024-07 - 25T18: 46: 58.982Z', + updatedAt: '2024-07 - 25T18: 46: 58.982Z', + __v: 0, + password: 'StrongPass1!', + // password, + id: '66a29da2942b3ebcaf047f07' + }, + validInput: { + email: 'user@mail.local', + password: 'StrongPass1!' + } + }; +}); + +// ############################ +// MOCKS +// ############################ +// import Database Service +import * as dbService from '../../utils/handleDB.js'; +// mock dbService +vi.mock('../../utils/handleDB.js', async (importOriginal) => { + return { + ...await importOriginal(), + dbConnection: vi.fn(() => 'mocked'), + findOneRecord: vi.fn(() => mockedVals.foundUser), + findByIdAndUpdate: vi.fn(() => { return { ...mockedVals.foundUser, refreshToken: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjY2MOCKED' }; }) + }; +}); + +// ############################ +// TESTS +// ############################ +describe('user login', async () => { + // prepare a hash function with current .env bcrypt settings + const _hashPw = async () => { + return await bcrypt.hash(mockedVals.foundUser.password, Number(process.env.BCRYPT_STRENGTH)); + }; + + describe('given the inputs are valid', () => { + beforeAll(async () => { + // hash password + dbService.findOneRecord.mockImplementationOnce(async () => { + return { ...mockedVals.foundUser, password: await _hashPw() }; + }); + + // set response by running route + response = await supertest(app) + .post(ROUTE) + .send(mockedVals.validInput); + }); + + it('should return a proper status code', () => { + expect(response.status).toBe(200); + }); + it('should respond with a proper body', () => { + expect(response.body).toMatchSnapshot({ + accessToken: expect.any(String), + }); + }); + }); + + // ############################ + + describe('given email and password are valid, but accout is unverified', () => { + + beforeAll(async () => { + // hash password & unverify user + dbService.findOneRecord.mockImplementationOnce(async () => { + return { ...mockedVals.foundUser, verified: false, password: await _hashPw() }; + }); + + // set response by running route + response = await supertest(app) + .post(ROUTE) + .send(mockedVals.validInput); + }); + + it('should return a proper status code', () => { + expect(response.status).toBe(401); + }); + it('should respond with a proper body', () => { + expect(response.body).toMatchSnapshot(); + }); + }); + + // ############################ + + describe('given the password is wrong', async () => { + // set response by running route + beforeAll(async () => { + + // hash password + dbService.findOneRecord.mockImplementationOnce(async () => { + return { ...mockedVals.foundUser, password: await _hashPw() }; + }); + + const input = { ...mockedVals.validInput, password: 'invalid-password' }; + + response = await supertest(app) + .post(ROUTE) + .send(input); + }); + it('should return a proper status code', () => { + expect(response.status).toBe(401); + }); + it('should respond with a proper body', () => { + expect(response.body).toMatchSnapshot(); + }); + }); + + // ############################ + + describe('given the email is unknown', async () => { + // set response by running route + beforeAll(async () => { + dbService.findOneRecord.mockImplementationOnce(() => null); + + response = await supertest(app) + .post(ROUTE) + .send(mockedVals.validInput); + }); + it('should return a proper status code', () => { + expect(response.status).toBe(401); + }); + it('should respond with a proper body', () => { + expect(response.body).toMatchSnapshot(); + }); + }); + + // ############################ + + describe('given the request body is empty', async () => { + // set response by running route + beforeAll(async () => { + response = await supertest(app) + .post(ROUTE) + .send(); + }); + it('should return a proper status code', () => { + expect(response.status).toBe(400); + }); + it('should respond with a proper body', () => { + expect(response.body).toMatchSnapshot(); + }); + }); +}); \ No newline at end of file diff --git a/__tests__/auth/requestverification.test.js b/__tests__/auth/requestverification.test.js index 662f166..01c8ebc 100644 --- a/__tests__/auth/requestverification.test.js +++ b/__tests__/auth/requestverification.test.js @@ -23,7 +23,6 @@ const mockedVals = vi.hoisted(() => { createdAt: '2024-07 - 25T18: 46: 58.982Z', updatedAt: '2024-07 - 25T18: 46: 58.982Z', __v: 0, - fullname: '', id: '66a29da2942b3ebcaf047f07' } }, diff --git a/__tests__/manualREST/users.rest b/__tests__/manualREST/users.rest index d5d8c36..a7f44ca 100644 --- a/__tests__/manualREST/users.rest +++ b/__tests__/manualREST/users.rest @@ -50,7 +50,7 @@ Content-Type: application/json { "email": "{{email}}", - "token": "yJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjY2YTNkYTViYTEwNjUzMmNhZTEyYTYwOSIsImVtYWlsIjoiZW1icnVjaEB6YmgudW5pLWhhbWJ1cmcuZGUiLCJpYXQiOjE3MjIwMTgyNDksImV4cCI6MTcyMjAyMTg0OX0.X0-m9RWqCsE7gSi1ywN9zTcbtpWsWRrHGv50tVy5JmI" + "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjY2YTNkYTViYTEwNjUzMmNhZTEyYTYwOSIsImVtYWlsIjoiZW1icnVjaEB6YmgudW5pLWhhbWJ1cmcuZGUiLCJpYXQiOjE3MjIwODUyMTAsImV4cCI6MTcyMjA4ODgxMH0.YaNozo8sdCHcdWn5qDqgZdMjtGPJFqazSVZCZOsXAMc" } diff --git a/__tests__/users/signup.test.js b/__tests__/users/signup.test.js index f608858..ef68a14 100644 --- a/__tests__/users/signup.test.js +++ b/__tests__/users/signup.test.js @@ -29,9 +29,9 @@ const mockedVals = vi.hoisted(() => { } }, validInput: { - name: 'John Doe', - username: 'johndoe', - email: 'john.doe@local.local', + name: 'My User', + username: 'snoopy', + email: 'user@mail.local', password: 'StrongPass1!', confirmPassword: 'StrongPass1!' } diff --git a/controllers/Auth.js b/controllers/Auth.js index 3462bd3..43b5f72 100644 --- a/controllers/Auth.js +++ b/controllers/Auth.js @@ -36,7 +36,7 @@ export const confirmVerification = async (req, res, next) => { req.document.verified = true; const updatedUser = await updateOneRecord(req.document); // remember document but remove confidential info - const document = hideConfidentialFields(User, updatedUser._doc); + const document = hideConfidentialFields(User, updatedUser); return res.json({ message: 'Account successfully verified. You can now login.' }); } catch (error) { next(error); @@ -56,12 +56,17 @@ export const login = async (req, res, next) => { try { // search for matching document foundUser = await findOneRecord(User, { email: req.body.email }, '+password'); + + // console.log("🚀 ~ login ~ passwords:", req.body.password, foundUser.password); + + + // wrong login name if (!foundUser) { return res.status(401).json({ message: 'Unknown combination of login credentials.' }); } - // wrong login name + // unverified account if (!foundUser.verified) { return res.status(401).json({ message: 'Your account is still unverified. Check your emails for the verification link.' }); } @@ -69,11 +74,12 @@ export const login = async (req, res, next) => { // check for correct password if (await bcrypt.compare(req.body.password, foundUser.password)) { // remember document but remove confidential info - const user = hideConfidentialFields(User, foundUser._doc); + // res.json({ message: foundUser._doc }); + const user = hideConfidentialFields(User, foundUser); + // create jsonwebtoken const accessToken = createAccessToken({ id: user._id, role: user.role }); const refreshToken = await createRefreshToken({ id: user._id }); - if (refreshToken == null) return res.status(500).json({ message: 'Error creating refresh token' }); // success @@ -85,6 +91,7 @@ export const login = async (req, res, next) => { return res.status(401).json({ message: 'Unknown combination of login credentials' }); } } catch (error) { + console.error('login error: ', error); next(error); } }; diff --git a/controllers/User.js b/controllers/User.js index 5ccec32..2dbeee1 100644 --- a/controllers/User.js +++ b/controllers/User.js @@ -10,7 +10,7 @@ export const createUser = async (req, res, next) => { // create user object const newRecord = await createRecord(User, prefillDocumentObject(User, req.body)); // remember document but remove confidential info - req.document = hideConfidentialFields(User, newRecord._doc); + req.document = hideConfidentialFields(User, newRecord); next(); // on error } catch (error) { diff --git a/logs/__tests__/users/login.test.js b/logs/__tests__/users/login.test.js deleted file mode 100644 index dea8343..0000000 --- a/logs/__tests__/users/login.test.js +++ /dev/null @@ -1,156 +0,0 @@ -// import vitest, supertest & app -import { vi, beforeAll, beforeEach, describe, expect, expectTypeOf, test, it, afterEach } from 'vitest'; -import supertest from "supertest"; -import app from "../../app.js"; -// ignore expiration of the (self-signed) certificate -process.env.NODE_TLS_REJECT_UNAUTHORIZED = 0; -// set timeout -const BEFORE_ALL_TIMEOUT = 30000; // 30 sec -// set route -const ROUTE = '/users/login'; -// prepare response of each test -let response; - -// ############################ -// OBJECTS -// ############################ - -const userLoginVerifiedResponse = { - "record": { - avatar: "", - collectionId: "_pb_users_auth_", - collectionName: "users", - created: "2024-05-06 07:45:18.836Z", - email: "johndoe@local.local", - emailVisibility: false, - id: "jr9mt8yvuri3sbd", - name: "John Doe", - updated: "2024-07-02 13:23:52.155Z", - username: "johndoe", - verified: true - }, - "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJfcGJfdXNlcnNfYXV0aF8iLCJleHAiOjE3MjA2NDk3NTQsImlkIjoianI5bXQ4eXZ1cmkzc2JkIiwidHlwZSI6ImF1dGhSZWNvcmQifQ.yFP1vlM_N2Fvpa_56INlaefSXnpwrm9ASCJuxPwf1Vk" -}; - -const userLoginUnverifiedResponse = { - "record": { - avatar: "", - collectionId: "_pb_users_auth_", - collectionName: "users", - created: "2024-05-06 07:45:18.836Z", - email: "johndoe@local.local", - emailVisibility: false, - id: "jr9mt8yvuri3sbd", - name: "John Doe", - updated: "2024-07-02 13:23:52.155Z", - username: "johndoe", - verified: false - }, - "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJfcGJfdXNlcnNfYXV0aF8iLCJleHAiOjE3MjA2NDk3NTQsImlkIjoianI5bXQ4eXZ1cmkzc2JkIiwidHlwZSI6ImF1dGhSZWNvcmQifQ.yFP1vlM_N2Fvpa_56INlaefSXnpwrm9ASCJuxPwf1Vk" -}; - -const userLoginFailedResponse = { - code: 400, - message: 'Failed to authenticate.', - data: {} -}; - -// ############################ -// MOCKS -// ############################ -// import PocketBase Service -import * as pbService from '../../utils/pocketbase/handlePocketBase.js'; -// mock pbService -vi.mock('../../utils/pocketbase/handlePocketBase.js', async (importOriginal) => { - return { - ...await importOriginal(), - pbUserLogin: vi.fn(() => userLoginVerifiedResponse) - }; -}); - -// ############################ -// TESTS -// ############################ -describe('user login', () => { - describe('given email and password are valid', () => { - beforeAll(async () => { - response = await supertest(app) - .post(ROUTE) - .send({ - email: 'valid.user@local.local', - password: 'ValidPassword123' - }); - }, BEFORE_ALL_TIMEOUT); - it('should return a proper status code', () => { - expect(response.status).toBe(200); - }); - it('should respond with a proper record and token', () => { - expect(response.body).toEqual(userLoginVerifiedResponse); - }); - }); - - describe('given email and password are valid, but accout is unverified', () => { - beforeAll(async () => { - pbService.pbUserLogin.mockImplementation(() => userLoginUnverifiedResponse); - - response = await supertest(app) - .post(ROUTE) - .send({ - email: 'valid.user@local.local', - password: 'ValidPassword123' - }); - }, BEFORE_ALL_TIMEOUT); - it('should return a proper status code', () => { - expect(response.status).toBe(401); - }); - it('should respond with a proper record and token', () => { - expect(response.body.message).toEqual("Your account is still unverified. Check your emails for the verification link."); - }); - }); - - // ############################ - - describe('given email or password are invalid', () => { - beforeAll(async () => { - let error = new Error('Failed to authenticate.'); - error.name = 'ClientResponseError'; - error.response = userLoginFailedResponse; - error.status = 400; - pbService.pbUserLogin.mockImplementation(() => { throw error; }); - - response = await supertest(app) - .post(ROUTE) - .send({ - email: 'invalid.user@local.local', - password: 'invalidPassword123' - }); - }, BEFORE_ALL_TIMEOUT); - it('should force pbUserLogin to throw an error', () => { - expect(pbService.pbUserLogin).toThrowError(); - }); - it('should return a proper status code', () => { - expect(response.status).toBe(400); - }); - it('should respond with a proper message', () => { - expect(response.body.message).toEqual('Failed to authenticate.'); - }); - }); - - // ############################ - - describe('given the request body is empty', async () => { - // set response by running route - beforeAll(async () => { - response = await supertest(app) - .post(ROUTE) - .send(); - }, BEFORE_ALL_TIMEOUT); - it('should return a proper status code', () => { - expect(response.status).toBe(400); - }); - it('should respond with a proper message', () => { - expect(response.body.validationErrors.email).toEqual('Required'); - expect(response.body.validationErrors.password).toEqual('Required'); - }); - }); -}); \ No newline at end of file diff --git a/models/User.js b/models/User.js index 476a916..a1a983a 100644 --- a/models/User.js +++ b/models/User.js @@ -68,9 +68,9 @@ const UserSchema = new Schema( // ################################# VIRTUALS // fullName -UserSchema.virtual('fullname').get(function () { - return `${this.title || ''} ${this.firstname || ''} ${this.lastname || ''}`.replace(/\s+/g, ' ').trim(); -}); +// UserSchema.virtual('fullname').get(function () { +// return `${this.title || ''} ${this.firstname || ''} ${this.lastname || ''}`.replace(/\s+/g, ' ').trim(); +// }); // ################################# MIDDLEWARES // middleware pre|post on validate|save|remove|updateOne|deleteOne -- GitLab