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