From 61350f0afbebacb49c00969a60e673438953ae1f Mon Sep 17 00:00:00 2001 From: juliecoust Date: Fri, 26 Jan 2024 15:57:54 +0100 Subject: [PATCH] TST - convergeance and reset password --- src/domain/entities/user.ts | 13 +- .../repositories/user-repository.ts | 4 +- src/domain/repositories/user-repository.ts | 8 +- src/presentation/routers/auth-router.ts | 13 +- .../repositories/user-repository.test.ts | 112 ++++++++++- test/presentation/routes/auth-router.test.ts | 188 +++++++++++++++++- 6 files changed, 318 insertions(+), 20 deletions(-) diff --git a/src/domain/entities/user.ts b/src/domain/entities/user.ts index 0a9dacc..894383c 100644 --- a/src/domain/entities/user.ts +++ b/src/domain/entities/user.ts @@ -47,17 +47,22 @@ export interface UserUpdateModel { } // the user response model -export interface UserResponseModel { +export interface UserResponseModel extends PublicUserModel { + confirmation_code?: string | null; + reset_password_code?: string | null; +} +export interface PublicUserModel { user_id: number; first_name: string; last_name: string; email: string; valid_email: boolean; - confirmation_code?: string | null; - reset_password_code?: string | null; //TODO UTILE? is_admin: boolean; organisation: string; country: string; user_planned_usage: string; - user_creation_date: string; //YYYY-MM-DD HH:MM:SS TimeStamp + user_creation_date: string; } +export interface PrivateUserModel extends PublicUserModel, UserResponseModel { + password_hash?: string; +} \ No newline at end of file diff --git a/src/domain/interfaces/repositories/user-repository.ts b/src/domain/interfaces/repositories/user-repository.ts index de1b617..845e7f7 100644 --- a/src/domain/interfaces/repositories/user-repository.ts +++ b/src/domain/interfaces/repositories/user-repository.ts @@ -1,6 +1,6 @@ import { AuthUserCredentialsModel, DecodedToken, ChangeCredentialsModel } from "../../entities/auth"; -import { UserRequesCreationtModel, UserResponseModel, UserRequestModel, UserUpdateModel } from "../../entities/user"; +import { UserRequesCreationtModel, UserResponseModel, UserRequestModel, UserUpdateModel, PublicUserModel, PrivateUserModel } from "../../entities/user"; export interface UserRepository { changePassword(user_to_update: ChangeCredentialsModel): Promise; getUser(user: UserRequestModel): Promise; @@ -16,5 +16,5 @@ export interface UserRepository { generateResetPasswordToken(user: UserRequestModel): string; verifyResetPasswordToken(reset_password_token: string): DecodedToken | null; setResetPasswordCode(user: UserUpdateModel): Promise; - toPublicUser(createdUser: UserResponseModel): UserResponseModel; + toPublicUser(createdUser: PrivateUserModel): PublicUserModel; } \ No newline at end of file diff --git a/src/domain/repositories/user-repository.ts b/src/domain/repositories/user-repository.ts index eeef75c..be1499e 100644 --- a/src/domain/repositories/user-repository.ts +++ b/src/domain/repositories/user-repository.ts @@ -2,7 +2,7 @@ import { CryptoWrapper } from "../../infra/cryptography/crypto-wrapper"; import { UserDataSource } from "../../data/interfaces/data-sources/user-data-source"; import { AuthUserCredentialsModel, ChangeCredentialsModel, DecodedToken } from "../entities/auth"; -import { UserResponseModel, UserRequesCreationtModel, UserRequestModel, UserUpdateModel } from "../entities/user"; +import { UserResponseModel, UserRequesCreationtModel, UserRequestModel, UserUpdateModel, PublicUserModel, PrivateUserModel } from "../entities/user"; import { UserRepository } from "../interfaces/repositories/user-repository"; import { JwtWrapper } from "../../infra/auth/jwt-wrapper"; @@ -144,8 +144,8 @@ export class UserRepositoryImpl implements UserRepository { return user.is_admin } - toPublicUser(createdUser: UserResponseModel): UserResponseModel { - const publicUser: UserResponseModel = { + toPublicUser(createdUser: PrivateUserModel): PublicUserModel { + const publicUser: PublicUserModel = { user_id: createdUser.user_id, first_name: createdUser.first_name, last_name: createdUser.last_name, @@ -155,7 +155,7 @@ export class UserRepositoryImpl implements UserRepository { organisation: createdUser.organisation, country: createdUser.country, user_planned_usage: createdUser.user_planned_usage, - user_creation_date: "" + user_creation_date: createdUser.user_creation_date } return publicUser diff --git a/src/presentation/routers/auth-router.ts b/src/presentation/routers/auth-router.ts index 653e55d..252c3c1 100644 --- a/src/presentation/routers/auth-router.ts +++ b/src/presentation/routers/auth-router.ts @@ -110,8 +110,8 @@ export default function AuthRouter( .json({ response: "Reset password request email sent." }); } catch (err) { console.log(err) - if (err.message === "User does not exist") res.status(200).send({ errors: ["Reset password request email sent."] }) - else if (err.message === "User email is not validated") res.status(200).send({ errors: ["Reset password request email sent."] }) + if (err.message === "User does not exist") res.status(200).send({ response: "Reset password request email sent." }) + else if (err.message === "User email is not validated") res.status(200).send({ response: "Reset password request email sent." }) else if (err.message === "Can't set password reset code") res.status(500).send({ errors: ["Can't reset password"] }) else if (err.message === "Can't find updated user") res.status(500).send({ errors: ["Can't reset password"] }) else res.status(500).send({ errors: ["Can't reset password"] }) @@ -121,17 +121,16 @@ export default function AuthRouter( // reset password confirm router.put('/password/reset', async (req: Request, res: Response) => { try { - console.log("req.body", req.body) await resetPasswordUseCase.execute(req.body) res .status(200) .json({ response: "Password sucessfully reset, please login" }); } catch (err) { console.log(err) - if (err.message === "Token is not valid") res.status(401).send({ errors: ["Can't change password"] }) - if (err.message === "No token provided") res.status(401).send({ errors: ["Can't change password"] }) - if (err.message === "User does not exist or token is not valid") res.status(404).send({ errors: ["Can't change password"] }) - if (err.message === "User email is not validated") res.status(403).send({ errors: ["Can't change password"] }) + if (err.message === "Token is not valid") res.status(401).send({ errors: ["Can't reset password"] }) + if (err.message === "No token provided") res.status(401).send({ errors: ["Can't reset password"] }) + if (err.message === "User does not exist or token is not valid") res.status(404).send({ errors: ["Can't reset password"] }) + if (err.message === "User email is not validated") res.status(403).send({ errors: ["Can't reset password"] }) else res.status(500).send({ errors: ["Can't reset password"] }) } }) diff --git a/test/domain/repositories/user-repository.test.ts b/test/domain/repositories/user-repository.test.ts index 8cc72cf..c6cc886 100644 --- a/test/domain/repositories/user-repository.test.ts +++ b/test/domain/repositories/user-repository.test.ts @@ -1,7 +1,7 @@ //test/domain/repositories/user-repository.test.ts import { UserDataSource } from "../../../src/data/interfaces/data-sources/user-data-source"; import { AuthUserCredentialsModel, ChangeCredentialsModel, DecodedToken } from "../../../src/domain/entities/auth"; -import { UserRequesCreationtModel, UserRequestModel, UserResponseModel, UserUpdateModel } from "../../../src/domain/entities/user"; +import { PrivateUserModel, PublicUserModel, UserRequesCreationtModel, UserRequestModel, UserResponseModel, UserUpdateModel } from "../../../src/domain/entities/user"; import { UserRepository } from "../../../src/domain/interfaces/repositories/user-repository"; import { UserRepositoryImpl } from "../../../src/domain/repositories/user-repository"; import { BcryptAdapter } from "../../../src/infra/cryptography/bcript" @@ -213,8 +213,9 @@ describe("User Repository", () => { const result = await userRepository.verifyValidationToken(InputData); expect(result).toBe(OutputData) - + expect(jwtAdapter.verify).toHaveBeenCalledWith(InputData, TEST_VALIDATION_TOKEN_SECRET) }); + test("should handle error and return null", async () => { const InputData: string = "validation_token" @@ -226,6 +227,46 @@ describe("User Repository", () => { }); }); + describe("verifyResetPasswordToken", () => { + test("Should decode token and return it", async () => { + const InputData: string = "validation_token" + + const OutputData: DecodedToken = { + user_id: 1, + last_name: "Smith", + first_name: "John", + email: "john@gmail.com", + valid_email: false, + confirmation_code: "123456", + is_admin: false, + organisation: "LOV", + country: "France", + user_planned_usage: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.", + user_creation_date: '2023-08-01 10:30:00', + + iat: 1693237789, + exp: 1724795389 + } + + jest.spyOn(jwtAdapter, "verify").mockImplementation(() => Promise.resolve(OutputData)) + + const result = await userRepository.verifyResetPasswordToken(InputData); + expect(result).toBe(OutputData) + expect(jwtAdapter.verify).toHaveBeenCalledWith(InputData, TEST_RESET_PASSWORD_TOKEN_SECRET) + + }); + + test("should handle error and return null", async () => { + const InputData: string = "validation_token" + + jest.spyOn(jwtAdapter, "verify").mockImplementation(() => { throw new Error() }) + + const result = await userRepository.verifyResetPasswordToken(InputData); + expect(result).toBe(null) + + }); + }); + describe("UpdateUser", () => { test("Things to update : any user try to validate his unvalidated account", async () => { @@ -499,6 +540,39 @@ describe("User Repository", () => { }); }); + describe("GenerateResetPasswordToken", () => { + test("Should return generated token ", async () => { + const User: UserRequestModel = { + user_id: 1, + reset_password_code: "123456", + } + + jest.spyOn(jwtAdapter, "sign").mockImplementation(() => { return "reset_password_token" }) + + const result = await userRepository.generateResetPasswordToken(User); + + expect(jwtAdapter.sign).toHaveBeenCalledWith( + { user_id: 1, reset_password_code: "123456" }, + TEST_RESET_PASSWORD_TOKEN_SECRET, + { expiresIn: '3h' }) + expect(result).toBe("reset_password_token") + + }); + }); + + describe("setResetPasswordCode", () => { + test("Should set reset password code", async () => { + const inputData: UserUpdateModel = { + user_id: 1, + } + + jest.spyOn(mockUserDataSource, "updateOne").mockImplementation(() => Promise.resolve(1)) + const result = await userRepository.setResetPasswordCode(inputData); + expect(result).toBe(1) + expect(mockUserDataSource.updateOne).toHaveBeenCalledWith({ user_id: 1, reset_password_code: expect.any(String) }) + }); + }); + describe("changePassword", () => { test("Should return 1 in nominal case", async () => { @@ -536,4 +610,38 @@ describe("User Repository", () => { }); }); + describe("toPublicUser", () => { + test("Should return one user", async () => { + const inputData: PrivateUserModel = { + user_id: 1, + last_name: "Smith", + first_name: "John", + email: "john@gmail.com", + valid_email: true, + is_admin: false, + organisation: "LOV", + country: "France", + user_planned_usage: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.", + user_creation_date: '2023-08-01 10:30:00', + password_hash: "code", + confirmation_code: "code", + reset_password_code: "code" + } + + const expectedData: PublicUserModel = { + user_id: 1, + first_name: 'John', + last_name: 'Smith', + email: 'john@gmail.com', + valid_email: true, + is_admin: false, + organisation: 'LOV', + country: 'France', + user_planned_usage: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.', + user_creation_date: '2023-08-01 10:30:00' + } + const result: PublicUserModel = userRepository.toPublicUser(inputData); + expect(result).toStrictEqual(expectedData) + }); + }) }) diff --git a/test/presentation/routes/auth-router.test.ts b/test/presentation/routes/auth-router.test.ts index 3bd48d0..cc5a2c2 100644 --- a/test/presentation/routes/auth-router.test.ts +++ b/test/presentation/routes/auth-router.test.ts @@ -4,7 +4,7 @@ import 'dotenv/config' import server from '../../../src/server' import AuthRouter from '../../../src/presentation/routers/auth-router' -import { AuthJwtRefreshedResponseModel, AuthJwtResponseModel, AuthUserCredentialsModel } from "../../../src/domain/entities/auth"; +import { AuthJwtRefreshedResponseModel, AuthJwtResponseModel, AuthUserCredentialsModel, ResetCredentialsModel } from "../../../src/domain/entities/auth"; import { UserResponseModel } from "../../../src/domain/entities/user"; import { MiddlewareAuth } from "../../../src/presentation/interfaces/middleware/auth"; @@ -348,4 +348,190 @@ describe("User Router", () => { expect(response.body).toStrictEqual(expectedResponse); }); }) + + // reset password request + describe("Test POST /auth/password/reset endpoint", () => { + test("Should send email", async () => { + const InputData = { + "email": "john@gmail.com", + } + + const expectedResponse = { response: "Reset password request email sent." } + + jest.spyOn(mockResetPasswordRequestUseCase, "execute").mockImplementation(() => Promise.resolve()) + + const response = await request(server).post("/auth/password/reset").send(InputData) + + expect(response.status).toBe(200) + expect(mockResetPasswordRequestUseCase.execute).toBeCalledTimes(1) + expect(response.body).toStrictEqual(expectedResponse) + }); + + test("Should handle error seamlessly if user already exist and return 200", async () => { + const InputData = { + "email": "john@gmail.com", + } + + const expectedResponse = { response: "Reset password request email sent." } + + jest.spyOn(mockResetPasswordRequestUseCase, "execute").mockImplementation(() => Promise.reject(new Error("User does not exist"))) + + const response = await request(server).post("/auth/password/reset").send(InputData) + + expect(response.status).toBe(200) + expect(mockResetPasswordRequestUseCase.execute).toBeCalledTimes(1) + expect(response.body).toStrictEqual(expectedResponse) + }); + + test("Should handle error seamlessly if user account not validated and return 200", async () => { + const InputData = { + "email": "john@gmail.com", + } + + const expectedResponse = { response: "Reset password request email sent." } + + jest.spyOn(mockResetPasswordRequestUseCase, "execute").mockImplementation(() => Promise.reject(new Error("User email is not validated"))) + + const response = await request(server).post("/auth/password/reset").send(InputData) + + expect(response.status).toBe(200) + expect(mockResetPasswordRequestUseCase.execute).toBeCalledTimes(1) + expect(response.body).toStrictEqual(expectedResponse) + }); + + test("Should handle internal errors and handle it explicitely and return a 500 response", async () => { + const InputData = { + "email": "john@gmail.com", + } + + const expectedResponse = { errors: ["Can't reset password"] } + + // Can't set password reset code + jest.spyOn(mockResetPasswordRequestUseCase, "execute").mockImplementation(() => Promise.reject(new Error("Can't set password reset code"))) + const response = await request(server).post("/auth/password/reset").send(InputData) + expect(response.status).toBe(500) + expect(mockResetPasswordRequestUseCase.execute).toBeCalledTimes(1) + expect(response.body).toStrictEqual(expectedResponse) + + //can't find updated user + jest.spyOn(mockResetPasswordRequestUseCase, "execute").mockImplementation(() => Promise.reject(new Error("Can't set password reset code"))) + const response_2 = await request(server).post("/auth/password/reset").send(InputData) + expect(response_2.status).toBe(500) + expect(mockResetPasswordRequestUseCase.execute).toBeCalledTimes(2) + expect(response_2.body).toStrictEqual(expectedResponse) + + // can't reset password + jest.spyOn(mockResetPasswordRequestUseCase, "execute").mockImplementation(() => Promise.reject(new Error())) + const response_3 = await request(server).post("/auth/password/reset").send(InputData) + expect(response_3.status).toBe(500) + expect(mockResetPasswordRequestUseCase.execute).toBeCalledTimes(3) + expect(response_3.body).toStrictEqual(expectedResponse) + + + }); + }) + // reset password + describe("Test PUT /auth/password/reset endpoint", () => { + test("Should update password", async () => { + const InputData: ResetCredentialsModel = { + new_password: "test123!", + reset_password_token: "reset_password_token", + } + const expectedResponse = { response: "Password sucessfully reset, please login" } + + jest.spyOn(mockResetPasswordUseCase, "execute").mockImplementation(() => Promise.resolve()) + + const response = await request(server).put("/auth/password/reset").send(InputData) + + expect(response.status).toBe(200) + expect(response.body).toStrictEqual(expectedResponse) + expect(mockResetPasswordUseCase.execute).toBeCalledTimes(1) + }); + + + test("Should handle error if Token is not valid and return 401", async () => { + const InputData: ResetCredentialsModel = { + new_password: "test123!!!!!!!", + reset_password_token: "BAD_reset_password_token", + } + const error_message = "Token is not valid" + const expectedResponse = { errors: ["Can't reset password"] } + + jest.spyOn(mockResetPasswordUseCase, "execute").mockImplementation(() => Promise.reject(new Error(error_message))) + + const response = await request(server).put("/auth/password/reset").send(InputData) + + expect(response.status).toBe(401) + expect(response.body).toStrictEqual(expectedResponse) + expect(mockResetPasswordUseCase.execute).toBeCalledTimes(1) + }); + + test("Should handle error if Token is missing and return 401", async () => { + const InputData: ResetCredentialsModel = { + new_password: "test123!!!!!!!", + reset_password_token: null, + } + const error_message = "No token provided" + const expectedResponse = { errors: ["Can't reset password"] } + + jest.spyOn(mockResetPasswordUseCase, "execute").mockImplementation(() => Promise.reject(new Error(error_message))) + + const response = await request(server).put("/auth/password/reset").send(InputData) + + expect(response.status).toBe(401) + expect(response.body).toStrictEqual(expectedResponse) + expect(mockResetPasswordUseCase.execute).toBeCalledTimes(1) + }); + + test("Should handle error if ser does not exist or token is not valid and return 404", async () => { + const InputData: ResetCredentialsModel = { + new_password: "test123!!!!!!!", + reset_password_token: "reset_password_token", + } + const error_message = "User does not exist or token is not valid" + const expectedResponse = { errors: ["Can't reset password"] } + + jest.spyOn(mockResetPasswordUseCase, "execute").mockImplementation(() => Promise.reject(new Error(error_message))) + + const response = await request(server).put("/auth/password/reset").send(InputData) + + expect(response.status).toBe(404) + expect(response.body).toStrictEqual(expectedResponse) + expect(mockResetPasswordUseCase.execute).toBeCalledTimes(1) + }); + + test("Should handle error if User email is not validated and return 403", async () => { + const InputData: ResetCredentialsModel = { + new_password: "test123!!!!!!!", + reset_password_token: "reset_password_token", + } + const error_message = "User email is not validated" + const expectedResponse = { errors: ["Can't reset password"] } + + jest.spyOn(mockResetPasswordUseCase, "execute").mockImplementation(() => Promise.reject(new Error(error_message))) + + const response = await request(server).put("/auth/password/reset").send(InputData) + + expect(response.status).toBe(403) + expect(response.body).toStrictEqual(expectedResponse) + expect(mockResetPasswordUseCase.execute).toBeCalledTimes(1) + }); + + test("Should handle any other error and return 500", async () => { + const InputData: ResetCredentialsModel = { + new_password: "test123!!!!!!!", + reset_password_token: "reset_password_token", + } + const expectedResponse = { errors: ["Can't reset password"] } + + jest.spyOn(mockResetPasswordUseCase, "execute").mockImplementation(() => Promise.reject(new Error())) + + const response = await request(server).put("/auth/password/reset").send(InputData) + + expect(response.status).toBe(500) + expect(response.body).toStrictEqual(expectedResponse) + expect(mockResetPasswordUseCase.execute).toBeCalledTimes(1) + }); + + }) }) \ No newline at end of file