From 56f0fe934fd5012b84b74c883cae84b2ef490cb3 Mon Sep 17 00:00:00 2001 From: subnub <44621867+subnub@users.noreply.github.com> Date: Mon, 24 Feb 2025 02:14:57 -0500 Subject: [PATCH] added more user test, improved validation, and added validation to login screen --- backend/express-routers/user-router.ts | 7 +- backend/middleware/user/user-middleware.ts | 37 ++- backend/models/user-model.ts | 8 +- backend/services/user-service/user-service.ts | 5 +- src/components/LoginPage/LoginPage.tsx | 40 ++- tests/controller/file-controller.test.js | 2 +- tests/controller/folder.controller.test.js | 4 +- tests/controller/user-controller.test.js | 232 +++++++++++++++++- 8 files changed, 316 insertions(+), 19 deletions(-) diff --git a/backend/express-routers/user-router.ts b/backend/express-routers/user-router.ts index a77366d..f5453eb 100644 --- a/backend/express-routers/user-router.ts +++ b/backend/express-routers/user-router.ts @@ -6,6 +6,7 @@ import authLogout from "../middleware/authLogout"; import { changePasswordValidationRules, createAccountValidationRules, + loginAccountValidationRules, } from "../middleware/user/user-middleware"; const userController = new UserController(); @@ -20,7 +21,11 @@ router.get("/user-service/user-detailed", auth, userController.getUserDetailed); // POST -router.post("/user-service/login", userController.login); +router.post( + "/user-service/login", + loginAccountValidationRules, + userController.login +); router.post("/user-service/logout", authLogout, userController.logout); diff --git a/backend/middleware/user/user-middleware.ts b/backend/middleware/user/user-middleware.ts index 34b4076..3f2e73f 100644 --- a/backend/middleware/user/user-middleware.ts +++ b/backend/middleware/user/user-middleware.ts @@ -9,13 +9,17 @@ export const changePasswordValidationRules = [ .exists() .withMessage("Old password is required") .isString() - .withMessage("Old password must be a string"), + .withMessage("Old password must be a string") + .isLength({ min: 6, max: 256 }) + .withMessage( + "Old password must be at least 6 characters and at most 256 characters" + ), body("newPassword") .exists() .withMessage("New password is required") .isString() .withMessage("New password must be a string") - .isLength({ min: 1, max: 256 }) + .isLength({ min: 6, max: 256 }) .withMessage( "New password must be at least 6 characters and at most 256 characters" ), @@ -30,7 +34,34 @@ export const createAccountValidationRules = [ .withMessage("Email is required") .isString() .withMessage("Email must be a string") - .isLength({ min: 1, max: 256 }) + .isLength({ min: 3, max: 320 }) + .withMessage( + "Email must be at least 3 characters and at most 320 characters" + ) + .isEmail() + .withMessage("Email is invalid"), + body("password") + .exists() + .withMessage("Password is required") + .isString() + .withMessage("Password must be a string") + .isLength({ min: 6, max: 256 }) + .withMessage( + "Password must be at least 6 characters and at most 256 characters" + ), + middlewareValidationFunction, +]; + +export const loginAccountValidationRules = [ + body("email") + .exists() + .withMessage("Email is required") + .isString() + .withMessage("Email must be a string") + .isLength({ min: 3, max: 320 }) + .withMessage( + "Email must be at least 3 characters and at most 320 characters" + ) .isEmail() .withMessage("Email is invalid"), body("password") diff --git a/backend/models/user-model.ts b/backend/models/user-model.ts index 1b9087a..0b47274 100644 --- a/backend/models/user-model.ts +++ b/backend/models/user-model.ts @@ -17,6 +17,10 @@ const userSchema = new mongoose.Schema( validate(value: any): any { if (!validator.isEmail(value)) { throw new Error("Email is invalid"); + } else if (value.length > 320) { + throw new Error("Email length must be less than 320 characters"); + } else if (value.length < 3) { + throw new Error("Email length must be at least 3 characters"); } }, }, @@ -26,7 +30,9 @@ const userSchema = new mongoose.Schema( required: true, validate(value: any): any { if (value.length < 6) { - throw new Error("Password Length Not Sufficent"); + throw new Error("Password length must be at least 6 characters"); + } else if (value.length > 256) { + throw new Error("Password length must be less than 256 characters"); } }, }, diff --git a/backend/services/user-service/user-service.ts b/backend/services/user-service/user-service.ts index 06a7e35..3c00f3f 100644 --- a/backend/services/user-service/user-service.ts +++ b/backend/services/user-service/user-service.ts @@ -10,6 +10,7 @@ import sendVerificationEmail from "../../utils/sendVerificationEmail"; import sendPasswordResetEmail from "../../utils/sendPasswordResetEmail"; import ForbiddenError from "../../utils/ForbiddenError"; import ConflictError from "../../utils/ConflictError"; +import NotAuthorizedError from "../../utils/NotAuthorizedError"; type UserDataType = { email: string; @@ -131,13 +132,13 @@ class UserService { ) => { const user = await User.findById(userID); - if (!user) throw new NotFoundError("Could Not Find User"); + if (!user) throw new NotAuthorizedError("User information is incorrect"); const date = new Date(); const isMatch = await bcrypt.compare(oldPassword, user.password); - if (!isMatch) throw new ForbiddenError("Old Password Is Incorrect"); + if (!isMatch) throw new NotAuthorizedError("User information is incorrect"); const encryptionKey = user.getEncryptionKey(); diff --git a/src/components/LoginPage/LoginPage.tsx b/src/components/LoginPage/LoginPage.tsx index e05d366..9fc0e98 100755 --- a/src/components/LoginPage/LoginPage.tsx +++ b/src/components/LoginPage/LoginPage.tsx @@ -14,6 +14,7 @@ import Spinner from "../Spinner/Spinner"; import { toast, ToastContainer } from "react-toastify"; import Swal from "sweetalert2"; import { AxiosError } from "axios"; +import isEmail from "validator/es/lib/isEmail"; const LoginPage = () => { const [email, setEmail] = useState(""); @@ -56,7 +57,10 @@ const LoginPage = () => { navigate("/home"); setLoadingLogin(false); } catch (e) { - if (e instanceof AxiosError && e.response?.status === 401) { + if ( + e instanceof AxiosError && + [400, 401].includes(e.response?.status || 0) + ) { setError("Incorrect email or password"); } else { setError("Login Error"); @@ -82,6 +86,8 @@ const LoginPage = () => { } catch (e) { if (e instanceof AxiosError && e.response?.status === 409) { setError("Email Already Exists"); + } else if (e instanceof AxiosError && e.response?.status === 400) { + setError("Validation Error"); } else { setError("Create Account Error"); } @@ -162,12 +168,42 @@ const LoginPage = () => { if (mode === "login" || mode === "reset") return ""; if (mode === "create") { - if (password !== verifyPassword) return "Passwords do not match"; + if (password.length) { + if (password.length < 6) { + return "Password must be at least 6 characters"; + } else if (password.length > 256) { + return "Password must be less than 256 characters"; + } + } + + if ( + password.length && + verifyPassword.length && + password !== verifyPassword + ) { + return "Passwords do not match"; + } + + if (email.length) { + const isValidEmail = isEmail(email); + + if (email.length < 3) { + return "Email must be at least 3 characters"; + } else if (email.length > 320) { + return "Email must be less than 320 characters"; + } else if (!isValidEmail) { + return "Email is invalid"; + } + } } return ""; })(); + useEffect(() => { + setError(""); + }, [email.length, password.length, verifyPassword.length]); + useEffect(() => { const loggedIn = window.localStorage.getItem("hasPreviouslyLoggedIn"); if (loggedIn === "true") { diff --git a/tests/controller/file-controller.test.js b/tests/controller/file-controller.test.js index 94916cc..01547ab 100644 --- a/tests/controller/file-controller.test.js +++ b/tests/controller/file-controller.test.js @@ -228,7 +228,7 @@ describe("File Controller", () => { .set("Cookie", authToken) .send({ id: file._id, - title: "a" * 257, + title: "a".repeat(257), }); expect(fileResponse.status).toBe(400); diff --git a/tests/controller/folder.controller.test.js b/tests/controller/folder.controller.test.js index 5bb3faf..3443743 100644 --- a/tests/controller/folder.controller.test.js +++ b/tests/controller/folder.controller.test.js @@ -175,7 +175,7 @@ describe("File Controller", () => { .post(`/folder-service/create`) .set("Cookie", authToken) .send({ - name: "a" * 257, + name: "a".repeat(257), parent: "/", }); @@ -316,7 +316,7 @@ describe("File Controller", () => { .set("Cookie", authToken) .send({ id: folder._id, - title: "a" * 257, + title: "a".repeat(257), }); expect(folderResponse.status).toBe(400); diff --git a/tests/controller/user-controller.test.js b/tests/controller/user-controller.test.js index b921c0e..44704a7 100644 --- a/tests/controller/user-controller.test.js +++ b/tests/controller/user-controller.test.js @@ -12,8 +12,6 @@ const { ObjectId } = require("mongodb"); let mongoServer; let authToken; let authToken2; -let file; -let file2; let user; let user2; @@ -29,6 +27,7 @@ describe("File Controller", () => { await mongoose.model("fs.files").deleteMany({}); await mongoose.model("Folder").deleteMany({}); await mongoose.model("User").deleteMany({}); + envFileFix(env); user = await request(app) .post("/user-service/create") @@ -77,6 +76,7 @@ describe("File Controller", () => { expect(userResponse.status).toBe(401); }); }); + describe("User login: POST /user-service/login", () => { test("Should login user", async () => { const userResponse = await request(app).post("/user-service/login").send({ @@ -102,7 +102,52 @@ describe("File Controller", () => { }); expect(userResponse.status).toBe(401); }); + test("Should return 400 if email length is less than 3", async () => { + const userResponse = await request(app).post("/user-service/login").send({ + email: "ab", + password: "test1234", + }); + + expect(userResponse.status).toBe(400); + }); + test("Should return 400 if email length is greater than 320", async () => { + const userResponse = await request(app) + .post("/user-service/login") + .send({ + email: "a".repeat(321) + "@test.com", + password: "test1234", + }); + + expect(userResponse.status).toBe(400); + }); + test("Should return 400 if email address is invalid", async () => { + const userResponse = await request(app).post("/user-service/login").send({ + email: "a@b", + password: "test1234", + }); + + expect(userResponse.status).toBe(400); + }); + test("Should return 400 if password length is less than 6", async () => { + const userResponse = await request(app).post("/user-service/login").send({ + email: "test@test.com", + password: "a", + }); + + expect(userResponse.status).toBe(400); + }); + test("Should return 400 if password length is greater than 256", async () => { + const userResponse = await request(app) + .post("/user-service/login") + .send({ + email: "test@test.com", + password: "a".repeat(257), + }); + + expect(userResponse.status).toBe(400); + }); }); + describe("Create User: POST /user-service/create", () => { test("Should create user", async () => { const userResponse = await request(app) @@ -138,21 +183,21 @@ describe("File Controller", () => { expect(userResponse.status).toBe(400); }); - test("Should return 400 if email length is less than 1", async () => { + test("Should return 400 if email length is less than 3", async () => { const userResponse = await request(app) .post("/user-service/create") .send({ - email: "", + email: "a@b", password: "test1234", }); expect(userResponse.status).toBe(400); }); - test("Should return 400 if email length is greater than 256", async () => { + test("Should return 400 if email length is greater than 320", async () => { const userResponse = await request(app) .post("/user-service/create") .send({ - email: "a" * 257, + email: "a".repeat(321) + "@test.com", password: "test1234", }); @@ -173,7 +218,7 @@ describe("File Controller", () => { .post("/user-service/create") .send({ email: "newuser@test.com", - password: "a" * 257, + password: "a".repeat(267), }); expect(userResponse.status).toBe(400); @@ -201,4 +246,177 @@ describe("File Controller", () => { expect(userResponse.status).toBe(403); }); }); + + describe("Change Password: PATCH /user-service/change-password", () => { + test("Should change password", async () => { + const changePasswordResponse = await request(app) + .patch("/user-service/change-password") + .set("Cookie", authToken) + .send({ + oldPassword: "test1234", + newPassword: "test12345", + }); + expect(changePasswordResponse.status).toBe(200); + const loginResponse = await request(app) + .post("/user-service/login") + .send({ + email: user.body.user.email, + password: "test12345", + }); + expect(loginResponse.status).toBe(200); + }); + test("Should return 401 if not authorized", async () => { + const changePasswordResponse = await request(app) + .patch("/user-service/change-password") + .set("Cookie", "access-token=test") + .send({ + oldPassword: "test1234", + newPassword: "test12345", + }); + expect(changePasswordResponse.status).toBe(401); + }); + test("Should return 401 if incorrect password", async () => { + const changePasswordResponse = await request(app) + .patch("/user-service/change-password") + .set("Cookie", authToken) + .send({ + oldPassword: "test12345", + newPassword: "test1234", + }); + expect(changePasswordResponse.status).toBe(401); + }); + test("Should return 400 if no old password", async () => { + const changePasswordResponse = await request(app) + .patch("/user-service/change-password") + .set("Cookie", authToken) + .send({ + newPassword: "test12345", + }); + expect(changePasswordResponse.status).toBe(400); + }); + test("Should return 400 if no new password", async () => { + const changePasswordResponse = await request(app) + .patch("/user-service/change-password") + .set("Cookie", authToken) + .send({ + oldPassword: "test1234", + }); + expect(changePasswordResponse.status).toBe(400); + }); + test("Should return 400 if old password length is less than 6", async () => { + const changePasswordResponse = await request(app) + .patch("/user-service/change-password") + .set("Cookie", authToken) + .send({ + oldPassword: "", + newPassword: "test12345", + }); + expect(changePasswordResponse.status).toBe(400); + }); + test("Should return 400 if old password length is greater than 256", async () => { + const changePasswordResponse = await request(app) + .patch("/user-service/change-password") + .set("Cookie", authToken) + .send({ + oldPassword: "a".repeat(257), + newPassword: "test12345", + }); + expect(changePasswordResponse.status).toBe(400); + }); + test("Should return 400 if new password length is less than 6", async () => { + const changePasswordResponse = await request(app) + .patch("/user-service/change-password") + .set("Cookie", authToken) + .send({ + oldPassword: "test1234", + newPassword: "", + }); + expect(changePasswordResponse.status).toBe(400); + }); + test("Should return 400 if new password length is greater than 256", async () => { + const changePasswordResponse = await request(app) + .patch("/user-service/change-password") + .set("Cookie", authToken) + .send({ + oldPassword: "test1234", + newPassword: "a".repeat(257), + }); + expect(changePasswordResponse.status).toBe(400); + }); + }); + + describe("Get/Create user token: POST /user-service/get-token", () => { + test("Should return user token", async () => { + const getTokenResponse = await request(app) + .post("/user-service/get-token") + .set("Cookie", authToken) + .send(); + + expect(getTokenResponse.status).toBe(201); + }); + test("Should return 401 if not authorized", async () => { + const getTokenResponse = await request(app) + .post("/user-service/get-token") + .set("Cookie", "access-token=test") + .send(); + + expect(getTokenResponse.status).toBe(401); + }); + }); + + describe("Logout: POST /user-service/logout", () => { + test("Should logout user", async () => { + const userDbCheck = await mongoose.model("User").findOne({ + email: user.body.user.email, + }); + + expect(userDbCheck.tokens.length).toBe(1); + + const logoutResponse = await request(app) + .post("/user-service/logout") + .set("Cookie", authToken) + .send(); + + const userDbCheck2 = await mongoose.model("User").findOne({ + email: user.body.user.email, + }); + + expect(userDbCheck2.tokens.length).toBe(0); + expect(logoutResponse.status).toBe(200); + }); + test("Should return 401 if not authorized", async () => { + const logoutResponse = await request(app) + .post("/user-service/logout") + .set("Cookie", "access-token=test") + .send(); + + expect(logoutResponse.status).toBe(401); + }); + }); + describe("Logout all: POST /user-service/logout-all", () => { + test("Should logout all users", async () => { + await request(app).post("/user-service/login").send({ + email: user.body.user.email, + password: "test1234", + }); + + const userDbCheck = await mongoose.model("User").findOne({ + email: user.body.user.email, + }); + + expect(userDbCheck.tokens.length).toBe(2); + + const logoutAllResponse = await request(app) + .post("/user-service/logout-all") + .set("Cookie", authToken) + .send(); + + const userDbCheck2 = await mongoose.model("User").findOne({ + email: user.body.user.email, + }); + + expect(userDbCheck2.tokens.length).toBe(0); + expect(logoutAllResponse.status).toBe(200); + }); + }); });