From 19ecb89f63077d8ebc65d256fa7579b5f82b40ac Mon Sep 17 00:00:00 2001 From: Yousuf Muhammud Date: Sat, 28 Feb 2026 19:01:55 -0600 Subject: [PATCH 1/7] building test cases --- .../tests/__snapshots__/_index.test.js.snap | 36 ++++ api/routes/auth/tests/_index.test.js | 200 ++++++++++++++++++ api/util/tests/setup.js | 22 ++ 3 files changed, 258 insertions(+) create mode 100644 api/routes/auth/tests/__snapshots__/_index.test.js.snap create mode 100644 api/routes/auth/tests/_index.test.js diff --git a/api/routes/auth/tests/__snapshots__/_index.test.js.snap b/api/routes/auth/tests/__snapshots__/_index.test.js.snap new file mode 100644 index 00000000..0778b243 --- /dev/null +++ b/api/routes/auth/tests/__snapshots__/_index.test.js.snap @@ -0,0 +1,36 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`/auth/forgotPassword/ > GET > returns a user's shop information to admins 1`] = ` +{ + "accountTitle": null, + "accountType": "CUSTOMER", + "active": true, + "blacklisted": false, + "createdAt": Any, + "id": Any, + "shopId": Any, + "updatedAt": Any, + "user": { + "admin": true, + "balance": 0, + "createdAt": Any, + "email": "test@email.com", + "firstName": "TestFirstName", + "id": Any, + "isMe": true, + "jobCounts": { + "completedCount": 0, + "excludedCount": 0, + "inProgressCount": 0, + "notStartedCount": 0, + }, + "jobs": [], + "lastName": "TestLastName", + "simple": true, + "suspended": false, + "suspensionReason": null, + "updatedAt": Any, + }, + "userId": Any, +} +`; diff --git a/api/routes/auth/tests/_index.test.js b/api/routes/auth/tests/_index.test.js new file mode 100644 index 00000000..4bce8d79 --- /dev/null +++ b/api/routes/auth/tests/_index.test.js @@ -0,0 +1,200 @@ +import { describe, expect, it } from "vitest"; +import prisma from "#prisma"; +import { LogType } from "@prisma/client"; +import { prisma as mockPrisma } from "#mock-prisma"; +import request from "supertest"; +import { app } from "#index"; +import { tc } from "#setup"; +import { gt } from "#gt"; + +//forgotPassword.js +describe("/auth/forgotPassword/", () => { + describe("GET", () => { + it("returns a user's shop information to admins", async () => { + const res = await request(app) + .get(`/api/shop/${tc.shop.id}/user/${tc.globalUser.id}`) + .set(...(await gt({ ga: true }))) + .send(); + + expect(res.status).toBe(200); + + expect(res.body.userShop).toMatchSnapshot({ + id: expect.any(String), + userId: expect.any(String), + shopId: expect.any(String), + createdAt: expect.any(String), + updatedAt: expect.any(String), + user: { + id: expect.any(String), + createdAt: expect.any(String), + updatedAt: expect.any(String), + isMe: true, + }, + }); + }); + + + }); +}); + + +//login.js +describe("/auth/login/", () => { + describe("PUT", () => { + it("resets a users password", async () => { //tests the fact that there is a password in the globalLocalUser + const res = await request(app) + .put(`/api/login/`) + .send({ + userId: tc.globalLocalUser.id, + password: "newPassword", + }); + expect(res.status).toBe(200); + + //get the user + const User = await prisma.user.findUnique({ + where: { id: tc.globalLocalUser.id }, + }); + + expect(User.password).toBeDefined(); + expect(User.password).not.toBe("TestPassword"); + expect(User.password).toBe("newPassword"); //might be overkill + + //check the logs + const log = await prisma.logs.findFirst({ + where: { userId: tc.globalLocalUser.id }, + }); + + expect(log).toBeDefined(); + + }); + + it("throws a 404 because no user exists", async () => { //tests the fact that we dont have the user in our db + const res = await request(app) + .put(`/api/login/`) + .send({ + userId: "nonExistentUser", + password: "newPassword", + }); + + expect(res.status).toBe(404); + expect(res.body).toEqual({ error: "Invalid credentials or SSO required" }); + + }); + + it("throws a 404 because no password field exisits in the db", async () => { //tests the fact that user doesnt have a password field i.e (SSO) + const res = await request(app) + .put(`/api/login/`) + .send({ + userId: tc.globalUser.id, //we can just use the globalUser because this has no password attribute and should throw an error. + password: "newPassword", + }); + + expect(res.status).toBe(404); + expect(res.body).toEqual({ error: "Invalid credentials or SSO required" }); + + }); + + }); + + + + + + describe("POST", () => { + it("emables a user to login", async () => { + const res = await request(app) + .post(`/api/shop/${tc.shop.id}/user/${tc.globalUser.id}`) + .set(...(await gt({ ga: true }))) + .send(); + + expect(res.status).toBe(200); + + expect(res.body.userShop).toMatchSnapshot({ + id: expect.any(String), + userId: expect.any(String), + shopId: expect.any(String), + createdAt: expect.any(String), + updatedAt: expect.any(String), + user: { + id: expect.any(String), + createdAt: expect.any(String), + updatedAt: expect.any(String), + isMe: true, + }, + }); + }); + it("throws a 401 because no password field exists in the db", async () => { //tests the fact that user doesnt have a password field i.e (SSO) + const res = await request(app) + .post(`/api/login/`) + .send({ + email: tc.globalLocalUser.email, //we can just use the globalUser because this has no password attribute and should throw an error. + password: "password", + }); + + + const log = await prisma.logs.findFirst({ + where: { + type: LogType.USER_LOGIN_FAILURE, + }, + orderBy: { createdAt: "desc" }, + }); + + expect(log).toBeDefined(); + expect(log.message).toContain("Failed to login"); + + expect(res.status).toBe(401); + expect(res.body).toEqual({ error: "Invalid credentials or SSO required" }); + + }); + it("throws a 401 because no user exisits in the db", async () => { //tests a random email or person not in the db + const res = await request(app) + .post(`/api/login/`) + .send({ + email: "randomEmail@email.com", //user a random email + password: "password", + }); + + + const log = await prisma.logs.findFirst({ + where: { + type: LogType.USER_LOGIN_FAILURE, + }, + orderBy: { createdAt: "desc" }, + }); + + expect(log).toBeDefined(); + expect(log.message).toContain("Failed to login"); + + expect(res.status).toBe(401); + expect(res.body).toEqual({ error: "Invalid credentials or SSO required" }); + + }); + + it("throws a 401 the passwords dont match", async () => { //tests a password doesnt match what is in the db + const res = await request(app) + .post(`/api/login/`) + .send({ + email: globalLocalUser.email, + password: "passwordNotMatch", + }); + + + const log = await prisma.logs.findFirst({ + where: { + type: LogType.USER_LOGIN_FAILURE, + }, + orderBy: { createdAt: "desc" }, + }); + + expect(log).toBeDefined(); + expect(log.message).toContain("Failed to login"); + + expect(res.status).toBe(401); + expect(res.body).toEqual({ error: "Invalid credentials or SSO required" }); + + }); + + + }); + +}); diff --git a/api/util/tests/setup.js b/api/util/tests/setup.js index 6291da50..56d05932 100644 --- a/api/util/tests/setup.js +++ b/api/util/tests/setup.js @@ -1,6 +1,7 @@ import prisma from "#prisma"; import { AccountType } from "@prisma/client"; import { beforeEach } from "vitest"; +import jwt from "jsonwebtoken"; export let tc = {}; @@ -53,6 +54,27 @@ beforeEach(async () => { tc.globalUser = globalUser; tc.user = globalUser; + + + const globalLocalUser = await prisma.user.create({ + data: { + firstName: "TestFirstName", + lastName: "TestLastName", + email: "localTest@email.com", + password: "TestPassword", + }, + }); + + + const token = jwt.sign( + { id: globalLocalUser.id }, + process.env.JWT_SECRET + ); + tc.token = token + + tc.globalLocalUser = globalLocalUser; + + const targetUser = await prisma.user.create({ data: { firstName: "TARGET_TestFirstName", From ced72794e10f6b9ecba3facb578cc4c8440c2dc3 Mon Sep 17 00:00:00 2001 From: Yousuf Muhammud Date: Sat, 28 Feb 2026 19:04:13 -0600 Subject: [PATCH 2/7] test cases --- api/routes/auth/tests/_index.test.js | 91 +++++++++++++++++----------- 1 file changed, 56 insertions(+), 35 deletions(-) diff --git a/api/routes/auth/tests/_index.test.js b/api/routes/auth/tests/_index.test.js index 4bce8d79..1b3764dc 100644 --- a/api/routes/auth/tests/_index.test.js +++ b/api/routes/auth/tests/_index.test.js @@ -6,15 +6,40 @@ import request from "supertest"; import { app } from "#index"; import { tc } from "#setup"; import { gt } from "#gt"; +import passport from "passport"; //forgotPassword.js describe("/auth/forgotPassword/", () => { - describe("GET", () => { - it("returns a user's shop information to admins", async () => { + describe("PUT", () => { //this route is very similar to the put request in login, but we are passing a token + it("resets a users password with postmark", async () => { const res = await request(app) - .get(`/api/shop/${tc.shop.id}/user/${tc.globalUser.id}`) - .set(...(await gt({ ga: true }))) - .send(); + .put(`/api/shop/${tc.shop.id}/user/${tc.globalUser.id}`) + + expect(res.status).toBe(200); + + expect(res.body.userShop).toMatchSnapshot({ + id: expect.any(String), + userId: expect.any(String), + shopId: expect.any(String), + createdAt: expect.any(String), + updatedAt: expect.any(String), + user: { + id: expect.any(String), + createdAt: expect.any(String), + updatedAt: expect.any(String), + isMe: true, + }, + }); + }); + + + + it("returns status 400, invalid token", async () => { + const res = await request(app) + .put("/auth/forgotPassword/") + .send({ + + }); expect(res.status).toBe(200); @@ -101,31 +126,28 @@ describe("/auth/login/", () => { describe("POST", () => { - it("emables a user to login", async () => { + it("emables a user to login", async () => { //test standard login const res = await request(app) - .post(`/api/shop/${tc.shop.id}/user/${tc.globalUser.id}`) - .set(...(await gt({ ga: true }))) - .send(); + .post("/api/login") + .send({ + email: tc.globalLocalUser.email, + password: tc.globalLocalUser.password, + }); expect(res.status).toBe(200); - - expect(res.body.userShop).toMatchSnapshot({ - id: expect.any(String), - userId: expect.any(String), - shopId: expect.any(String), - createdAt: expect.any(String), - updatedAt: expect.any(String), - user: { - id: expect.any(String), - createdAt: expect.any(String), - updatedAt: expect.any(String), - isMe: true, + expect(res.body).toHaveProperty("token"); + const log = await prisma.logs.findFirst({ + where: { + type: LogType.USER_LOGIN_LOCAL, + userId: tc.globalLocalUser.id, }, }); + + expect(log).toBeDefined(); }); it("throws a 401 because no password field exists in the db", async () => { //tests the fact that user doesnt have a password field i.e (SSO) const res = await request(app) - .post(`/api/login/`) + .post("/api/login/") .send({ email: tc.globalLocalUser.email, //we can just use the globalUser because this has no password attribute and should throw an error. password: "password", @@ -134,21 +156,21 @@ describe("/auth/login/", () => { const log = await prisma.logs.findFirst({ where: { - type: LogType.USER_LOGIN_FAILURE, + type: LogType.USER_LOGIN_FAILURE, }, orderBy: { createdAt: "desc" }, }); expect(log).toBeDefined(); - expect(log.message).toContain("Failed to login"); - + expect(log.type).toBe(LogType.USER_LOGIN_FAILURE); + expect(log.message).toContain("Failed to login"); //overkill? expect(res.status).toBe(401); expect(res.body).toEqual({ error: "Invalid credentials or SSO required" }); }); it("throws a 401 because no user exisits in the db", async () => { //tests a random email or person not in the db const res = await request(app) - .post(`/api/login/`) + .post("/api/login/") .send({ email: "randomEmail@email.com", //user a random email password: "password", @@ -157,14 +179,14 @@ describe("/auth/login/", () => { const log = await prisma.logs.findFirst({ where: { - type: LogType.USER_LOGIN_FAILURE, + type: LogType.USER_LOGIN_FAILURE, }, orderBy: { createdAt: "desc" }, }); expect(log).toBeDefined(); - expect(log.message).toContain("Failed to login"); - + expect(log.type).toBe(LogType.USER_LOGIN_FAILURE); + expect(log.message).toContain("Failed to login"); //overkill? expect(res.status).toBe(401); expect(res.body).toEqual({ error: "Invalid credentials or SSO required" }); @@ -172,23 +194,22 @@ describe("/auth/login/", () => { it("throws a 401 the passwords dont match", async () => { //tests a password doesnt match what is in the db const res = await request(app) - .post(`/api/login/`) + .post("/api/login/") .send({ - email: globalLocalUser.email, + email: globalLocalUser.email, password: "passwordNotMatch", }); const log = await prisma.logs.findFirst({ where: { - type: LogType.USER_LOGIN_FAILURE, + type: LogType.USER_LOGIN_FAILURE, + userId: tc.globalLocalUser.id, }, - orderBy: { createdAt: "desc" }, }); expect(log).toBeDefined(); - expect(log.message).toContain("Failed to login"); - + expect(log.type).toBe(LogType.USER_LOGIN_FAILURE); expect(res.status).toBe(401); expect(res.body).toEqual({ error: "Invalid credentials or SSO required" }); From 8184e6e1dded7cef602370fd0d0dedd0400828f3 Mon Sep 17 00:00:00 2001 From: Yousuf Muhammud Date: Sat, 28 Feb 2026 21:24:52 -0600 Subject: [PATCH 3/7] most test cases are passing --- api/routes/auth/forgotPassword.js | 16 +- api/routes/auth/login.js | 6 +- api/routes/auth/tests/_forgotPassword.test.js | 117 ++++++++++ api/routes/auth/tests/_index.test.js | 221 ------------------ api/routes/auth/tests/_login.test.js | 165 +++++++++++++ api/routes/users/tests/_user.test.js | 2 +- api/util/tests/setup.js | 8 +- 7 files changed, 304 insertions(+), 231 deletions(-) create mode 100644 api/routes/auth/tests/_forgotPassword.test.js create mode 100644 api/routes/auth/tests/_login.test.js diff --git a/api/routes/auth/forgotPassword.js b/api/routes/auth/forgotPassword.js index 29476ba2..7c8567ff 100644 --- a/api/routes/auth/forgotPassword.js +++ b/api/routes/auth/forgotPassword.js @@ -2,6 +2,7 @@ import bcrypt from "bcrypt"; import jwt from "jsonwebtoken"; import { prisma } from "#prisma"; import postmark from "postmark"; +import { LogType } from "@prisma/client"; export const post = [ async (req, res) => { @@ -10,10 +11,16 @@ export const post = [ const user = await prisma.user.findUnique({ where: { email: email.toLowerCase() }, }); //emails need to be exact so when we create user, the email needs to be stored with lowercase. - if (!user) { - return res.status(401).json({ error: "User does not exist." }); + + + if (!user) { + console.log("error user not found"); + return res.status(404).json({ error: "User does not exist." }); } + + console.log("User found, sending email..."); + const client = new postmark.ServerClient(process.env.POSTMARK_API_KEY); const resetToken = jwt.sign({ id: user.id }, process.env.JWT_SECRET, { @@ -60,11 +67,14 @@ export const put = [ where: { id: decodedToken.id }, data: { password: hashedPassword }, }); + await prisma.logs.create({ + data: { type: LogType.USER_PASSWORD_CHANGE, userId: user.id }, + }); return res.status(200).json({ success: true }); } catch (error) { console.error(error); - return res.status(400).json({ error: "Invalid or expired link." }); + return res.status(401).json({ error: "Invalid or expired link." }); } }, ]; diff --git a/api/routes/auth/login.js b/api/routes/auth/login.js index f4396084..8f3911d5 100644 --- a/api/routes/auth/login.js +++ b/api/routes/auth/login.js @@ -70,16 +70,16 @@ export const put = [ async (req, res) => { const { userId, password } = req.body; try { - const user = await prisma.user.findUnique({ where: { userId } }); + const user = await prisma.user.findUnique({ where: { id: userId } }); if (!user || !user.password) { //if there is no user matching the userId return res - .status(404) + .status(401) .json({ error: "Invalid credentials or SSO required" }); } - const hashedPassword = bcrypt.hash(password, 10); + const hashedPassword = await bcrypt.hash(password, 10); await prisma.user.update({ where: { id: userId }, diff --git a/api/routes/auth/tests/_forgotPassword.test.js b/api/routes/auth/tests/_forgotPassword.test.js new file mode 100644 index 00000000..4cdba5a4 --- /dev/null +++ b/api/routes/auth/tests/_forgotPassword.test.js @@ -0,0 +1,117 @@ +import { describe, expect, it, vi } from "vitest"; +import prisma from "#prisma"; +import { LogType } from "@prisma/client"; +import request from "supertest"; +import { app } from "#index"; +import { tc } from "#setup"; +import postmark from "postmark"; +import jwt from "jsonwebtoken"; + + +const sendEmailMock = vi.fn().mockResolvedValue(true); + +vi.mock("postmark", () => { + return { + ServerClient: vi.fn().mockImplementation(() => { + return { + sendEmail: sendEmailMock, + }; + }), + }; +}); + + +describe("/api/auth/forgotPassword", () => { + describe("POST", () => { + it("sends a user a reset link with postmark", async () => { + const res = await request(app) + .post("/api/auth/forgotPassword") + .send({ + email: tc.globalLocalUser.email, + }) + + expect(res.status).toBe(200); + expect(res.body).toEqual({ success: true }); + expect(postmark.ServerClient).toHaveBeenCalled(); + expect(sendEmailMock).toHaveBeenCalled(); + }); + + + it("returns status 404, user does not exist", async () => { //token is invalid + const res = await request(app) + .post("/api/auth/forgotPassword") + .send({ + email: "nonExistentUser@email.com", + }); + + expect(res.status).toBe(404); + expect(res.body).toEqual({ error: "User does not exist." }); + + }); + + + }); + + + describe("PUT", () => { //this route is very similar to the put request in login, but we are passing a token + it("resets a users password with postmark", async () => { + const res = await request(app) + .put("/api/auth/forgotPassword") + .send({ + token: tc.token, + newPassword: "newPassword", + }) + + expect(res.status).toBe(200); + expect(res.body).toEqual({ success: true }); + + const log = await prisma.logs.findFirst({ + where: { + type: LogType.USER_PASSWORD_CHANGE, + userId: tc.globalLocalUser.id, + }, + }); + + expect(log).toBeDefined(); + expect(log.type).toBe(LogType.USER_PASSWORD_CHANGE); + }); + + + + it("returns status 401, invalid token", async () => { //token is invalid + const res = await request(app) + .put("/api/auth/forgotPassword") + .send({ + newPassword: "newPassword", + token: "fake-token", + }); + + expect(res.status).toBe(401); + expect(res.body).toEqual({ error: "Invalid or expired link." }); + + }); + + + it("returns status 401, user not found", async () => { //token is valid, but the user doesnt exist with that token + const fakeUserId = "00000000-0000-0000-0000-000000000000"; + + const token = jwt.sign( //we need the jwt token to verify successfully + { id: fakeUserId }, + process.env.JWT_SECRET + ); + const res = await request(app) + .put("/api/auth/forgotPassword") + .send({ + token: token, + newPassword: "newPassword", + }); + + expect(res.status).toBe(401); + expect(res.body).toEqual({ error: "Invalid or expired link." }); + + }); + + + }); +}); + diff --git a/api/routes/auth/tests/_index.test.js b/api/routes/auth/tests/_index.test.js index 1b3764dc..e69de29b 100644 --- a/api/routes/auth/tests/_index.test.js +++ b/api/routes/auth/tests/_index.test.js @@ -1,221 +0,0 @@ -import { describe, expect, it } from "vitest"; -import prisma from "#prisma"; -import { LogType } from "@prisma/client"; -import { prisma as mockPrisma } from "#mock-prisma"; -import request from "supertest"; -import { app } from "#index"; -import { tc } from "#setup"; -import { gt } from "#gt"; -import passport from "passport"; - -//forgotPassword.js -describe("/auth/forgotPassword/", () => { - describe("PUT", () => { //this route is very similar to the put request in login, but we are passing a token - it("resets a users password with postmark", async () => { - const res = await request(app) - .put(`/api/shop/${tc.shop.id}/user/${tc.globalUser.id}`) - - expect(res.status).toBe(200); - - expect(res.body.userShop).toMatchSnapshot({ - id: expect.any(String), - userId: expect.any(String), - shopId: expect.any(String), - createdAt: expect.any(String), - updatedAt: expect.any(String), - user: { - id: expect.any(String), - createdAt: expect.any(String), - updatedAt: expect.any(String), - isMe: true, - }, - }); - }); - - - - it("returns status 400, invalid token", async () => { - const res = await request(app) - .put("/auth/forgotPassword/") - .send({ - - }); - - expect(res.status).toBe(200); - - expect(res.body.userShop).toMatchSnapshot({ - id: expect.any(String), - userId: expect.any(String), - shopId: expect.any(String), - createdAt: expect.any(String), - updatedAt: expect.any(String), - user: { - id: expect.any(String), - createdAt: expect.any(String), - updatedAt: expect.any(String), - isMe: true, - }, - }); - }); - - - }); -}); - - -//login.js -describe("/auth/login/", () => { - describe("PUT", () => { - it("resets a users password", async () => { //tests the fact that there is a password in the globalLocalUser - const res = await request(app) - .put(`/api/login/`) - .send({ - userId: tc.globalLocalUser.id, - password: "newPassword", - }); - expect(res.status).toBe(200); - - //get the user - const User = await prisma.user.findUnique({ - where: { id: tc.globalLocalUser.id }, - }); - - expect(User.password).toBeDefined(); - expect(User.password).not.toBe("TestPassword"); - expect(User.password).toBe("newPassword"); //might be overkill - - //check the logs - const log = await prisma.logs.findFirst({ - where: { userId: tc.globalLocalUser.id }, - }); - - expect(log).toBeDefined(); - - }); - - it("throws a 404 because no user exists", async () => { //tests the fact that we dont have the user in our db - const res = await request(app) - .put(`/api/login/`) - .send({ - userId: "nonExistentUser", - password: "newPassword", - }); - - expect(res.status).toBe(404); - expect(res.body).toEqual({ error: "Invalid credentials or SSO required" }); - - }); - - it("throws a 404 because no password field exisits in the db", async () => { //tests the fact that user doesnt have a password field i.e (SSO) - const res = await request(app) - .put(`/api/login/`) - .send({ - userId: tc.globalUser.id, //we can just use the globalUser because this has no password attribute and should throw an error. - password: "newPassword", - }); - - expect(res.status).toBe(404); - expect(res.body).toEqual({ error: "Invalid credentials or SSO required" }); - - }); - - }); - - - - - - describe("POST", () => { - it("emables a user to login", async () => { //test standard login - const res = await request(app) - .post("/api/login") - .send({ - email: tc.globalLocalUser.email, - password: tc.globalLocalUser.password, - }); - - expect(res.status).toBe(200); - expect(res.body).toHaveProperty("token"); - const log = await prisma.logs.findFirst({ - where: { - type: LogType.USER_LOGIN_LOCAL, - userId: tc.globalLocalUser.id, - }, - }); - - expect(log).toBeDefined(); - }); - it("throws a 401 because no password field exists in the db", async () => { //tests the fact that user doesnt have a password field i.e (SSO) - const res = await request(app) - .post("/api/login/") - .send({ - email: tc.globalLocalUser.email, //we can just use the globalUser because this has no password attribute and should throw an error. - password: "password", - }); - - - const log = await prisma.logs.findFirst({ - where: { - type: LogType.USER_LOGIN_FAILURE, - }, - orderBy: { createdAt: "desc" }, - }); - - expect(log).toBeDefined(); - expect(log.type).toBe(LogType.USER_LOGIN_FAILURE); - expect(log.message).toContain("Failed to login"); //overkill? - expect(res.status).toBe(401); - expect(res.body).toEqual({ error: "Invalid credentials or SSO required" }); - - }); - it("throws a 401 because no user exisits in the db", async () => { //tests a random email or person not in the db - const res = await request(app) - .post("/api/login/") - .send({ - email: "randomEmail@email.com", //user a random email - password: "password", - }); - - - const log = await prisma.logs.findFirst({ - where: { - type: LogType.USER_LOGIN_FAILURE, - }, - orderBy: { createdAt: "desc" }, - }); - - expect(log).toBeDefined(); - expect(log.type).toBe(LogType.USER_LOGIN_FAILURE); - expect(log.message).toContain("Failed to login"); //overkill? - expect(res.status).toBe(401); - expect(res.body).toEqual({ error: "Invalid credentials or SSO required" }); - - }); - - it("throws a 401 the passwords dont match", async () => { //tests a password doesnt match what is in the db - const res = await request(app) - .post("/api/login/") - .send({ - email: globalLocalUser.email, - password: "passwordNotMatch", - }); - - - const log = await prisma.logs.findFirst({ - where: { - type: LogType.USER_LOGIN_FAILURE, - userId: tc.globalLocalUser.id, - }, - }); - - expect(log).toBeDefined(); - expect(log.type).toBe(LogType.USER_LOGIN_FAILURE); - expect(res.status).toBe(401); - expect(res.body).toEqual({ error: "Invalid credentials or SSO required" }); - - }); - - - }); - -}); diff --git a/api/routes/auth/tests/_login.test.js b/api/routes/auth/tests/_login.test.js new file mode 100644 index 00000000..275378d1 --- /dev/null +++ b/api/routes/auth/tests/_login.test.js @@ -0,0 +1,165 @@ +import { describe, expect, it } from "vitest"; +import prisma from "#prisma"; +import { LogType } from "@prisma/client"; +import request from "supertest"; +import { app } from "#index"; +import { tc } from "#setup"; + +describe("/api/auth/login", () => { + describe("POST", () => { + it("enables a user to login", async () => { //test standard login + const res = await request(app) + .post("/api/auth/login") + .send({ + email: tc.globalLocalUser.email, + password: tc.globalLocalUser.password, + }); + + expect(res.status).toBe(200); + expect(res.body).toHaveProperty("token"); + const log = await prisma.logs.findFirst({ + where: { + type: LogType.USER_LOGIN_LOCAL, + userId: tc.globalLocalUser.id, + }, + }); + + expect(log).toBeDefined(); + }); + it("returns status 401, no password field exists in the db", async () => { //tests the fact that user doesnt have a password field i.e (SSO) + const res = await request(app) + .post("/api/auth/login") + .send({ + email: tc.globalLocalUser.email, //we can just use the globalUser because this has no password attribute and should throw an error. + password: "password", + }); + + + const log = await prisma.logs.findFirst({ + where: { + type: LogType.USER_LOGIN_FAILURE, + }, + orderBy: { createdAt: "desc" }, + }); + + expect(log).toBeDefined(); + expect(log.type).toBe(LogType.USER_LOGIN_FAILURE); + expect(res.status).toBe(401); + expect(res.body).toEqual({ error: "Invalid credentials or SSO required" }); + + }); + it("returns status 401, no user exists in the db", async () => { //tests a random email or person not in the db + const res = await request(app) + .post("/api/auth/login") + .send({ + email: "randomEmail@email.com", //user a random email + password: "password", + }); + + + const log = await prisma.logs.findFirst({ + where: { + type: LogType.USER_LOGIN_FAILURE, + }, + orderBy: { createdAt: "desc" }, + }); + + expect(log).toBeDefined(); + expect(log.type).toBe(LogType.USER_LOGIN_FAILURE); + expect(res.status).toBe(401); + expect(res.body).toEqual({ error: "Invalid credentials or SSO required" }); + + }); + + it("returns status 401, passwords dont match", async () => { //tests a password doesnt match what is in the db + const res = await request(app) + .post("/api/auth/login") + .send({ + email: tc.globalLocalUser.email, + password: "passwordNotMatch", + }); + + + const log = await prisma.logs.findFirst({ + where: { + type: LogType.USER_LOGIN_FAILURE, + userId: tc.globalLocalUser.id, + }, + }); + + expect(log).toBeDefined(); + expect(log.type).toBe(LogType.USER_LOGIN_FAILURE); + expect(res.status).toBe(401); + expect(res.body).toEqual({ error: "Invalid credentials or SSO required" }); + + }); + + }); + + + + describe("PUT", () => { + it("resets a users password", async () => { //tests the fact that there is a password in the globalLocalUser + const res = await request(app) + .put("/api/auth/login") + .send({ + userId: tc.globalLocalUser.id, + password: "newPassword", + }); + expect(res.status).toBe(200); + + //get the user + const User = await prisma.user.findUnique({ + where: { id: tc.globalLocalUser.id }, + }); + + expect(User.password).toBeDefined(); + + //check the logs + const log = await prisma.logs.findFirst({ + where: { + type: LogType.USER_PASSWORD_CHANGE, + userId: tc.globalLocalUser.id, + }, + }); + + expect(log).toBeDefined(); + expect(log.type).toBe(LogType.USER_PASSWORD_CHANGE); + + }); + + it("returns status 401, no user exists", async () => { //tests the fact that we dont have the user in our db + const res = await request(app) + .put("/api/auth/login") + .send({ + userId: "nonExistentUser", + password: "newPassword", + }); + + expect(res.status).toBe(401); + expect(res.body).toEqual({ error: "Invalid credentials or SSO required" }); + + }); + + + + + it("returns status 401, no password field exisits in the db", async () => { //tests the fact that user doesnt have a password field i.e (SSO) + const res = await request(app) + .put("/api/auth/login") + .send({ + userId: tc.globalUser.id, //we can just use the globalUser because this has no password attribute and should throw an error. + password: "newPassword", + }); + + expect(res.status).toBe(401); + expect(res.body).toEqual({ error: "Invalid credentials or SSO required" }); + + }); + + }); + + + + +}); diff --git a/api/routes/users/tests/_user.test.js b/api/routes/users/tests/_user.test.js index 401e35fb..3900b810 100644 --- a/api/routes/users/tests/_user.test.js +++ b/api/routes/users/tests/_user.test.js @@ -50,7 +50,7 @@ describe("/users", () => { .send(); expect(res.status).toBe(200); - expect(res.body.users).toHaveLength(2); + expect(res.body.users).toHaveLength(3); expect(res.body.users[0]).toMatchObject({ admin: expect.any(Boolean), diff --git a/api/util/tests/setup.js b/api/util/tests/setup.js index 56d05932..0995c9cd 100644 --- a/api/util/tests/setup.js +++ b/api/util/tests/setup.js @@ -2,6 +2,7 @@ import prisma from "#prisma"; import { AccountType } from "@prisma/client"; import { beforeEach } from "vitest"; import jwt from "jsonwebtoken"; +import bcrypt from "bcrypt" export let tc = {}; @@ -43,6 +44,7 @@ beforeEach(async () => { await prisma.$executeRawUnsafe("SET session_replication_role = 'origin';"); // Create an initial user + const globalUser = await prisma.user.create({ data: { firstName: "TestFirstName", @@ -55,13 +57,13 @@ beforeEach(async () => { tc.user = globalUser; - + const hashedPassword = await bcrypt.hash("TestPassword", 10); const globalLocalUser = await prisma.user.create({ data: { firstName: "TestFirstName", lastName: "TestLastName", - email: "localTest@email.com", - password: "TestPassword", + email: "localtest@email.com", //needs to be lowercase + password: hashedPassword, }, }); From 5bce43175940eb631a65803f3417bfff213e7d5d Mon Sep 17 00:00:00 2001 From: Yousuf Muhammud Date: Sat, 28 Feb 2026 21:53:11 -0600 Subject: [PATCH 4/7] all test cases passing --- api/routes/auth/forgotPassword.js | 3 --- api/routes/auth/login.js | 7 ++++--- api/routes/auth/tests/_forgotPassword.test.js | 14 +++++++------- api/routes/auth/tests/_index.test.js | 0 api/routes/auth/tests/_login.test.js | 3 ++- api/routes/users/tests/_user.test.js | 2 +- 6 files changed, 14 insertions(+), 15 deletions(-) delete mode 100644 api/routes/auth/tests/_index.test.js diff --git a/api/routes/auth/forgotPassword.js b/api/routes/auth/forgotPassword.js index 7c8567ff..6c0f8499 100644 --- a/api/routes/auth/forgotPassword.js +++ b/api/routes/auth/forgotPassword.js @@ -14,13 +14,10 @@ export const post = [ if (!user) { - console.log("error user not found"); return res.status(404).json({ error: "User does not exist." }); } - console.log("User found, sending email..."); - const client = new postmark.ServerClient(process.env.POSTMARK_API_KEY); const resetToken = jwt.sign({ id: user.id }, process.env.JWT_SECRET, { diff --git a/api/routes/auth/login.js b/api/routes/auth/login.js index 8f3911d5..4f8036a9 100644 --- a/api/routes/auth/login.js +++ b/api/routes/auth/login.js @@ -88,11 +88,12 @@ export const put = [ await prisma.logs.create({ data: { type: LogType.USER_PASSWORD_CHANGE, userId: user.id }, }); - return res.status(200); + return res.status(200).json({ success: true }); } catch (error) { console.log("Error", error); - - return res.status(500).json({ error: "Internal server error" }); + return res + .status(401) + .json({ error: "Invalid credentials or SSO required" }); } }, ]; diff --git a/api/routes/auth/tests/_forgotPassword.test.js b/api/routes/auth/tests/_forgotPassword.test.js index 4cdba5a4..4dad0cf8 100644 --- a/api/routes/auth/tests/_forgotPassword.test.js +++ b/api/routes/auth/tests/_forgotPassword.test.js @@ -11,13 +11,13 @@ import jwt from "jsonwebtoken"; const sendEmailMock = vi.fn().mockResolvedValue(true); vi.mock("postmark", () => { - return { - ServerClient: vi.fn().mockImplementation(() => { - return { - sendEmail: sendEmailMock, - }; - }), - }; + const ServerClient = vi.fn().mockImplementation(() => ({ + sendEmail: sendEmailMock, + })); + + return { + default: { ServerClient }, + }; }); diff --git a/api/routes/auth/tests/_index.test.js b/api/routes/auth/tests/_index.test.js deleted file mode 100644 index e69de29b..00000000 diff --git a/api/routes/auth/tests/_login.test.js b/api/routes/auth/tests/_login.test.js index 275378d1..b222a5d3 100644 --- a/api/routes/auth/tests/_login.test.js +++ b/api/routes/auth/tests/_login.test.js @@ -12,7 +12,7 @@ describe("/api/auth/login", () => { .post("/api/auth/login") .send({ email: tc.globalLocalUser.email, - password: tc.globalLocalUser.password, + password: "TestPassword", }); expect(res.status).toBe(200); @@ -107,6 +107,7 @@ describe("/api/auth/login", () => { password: "newPassword", }); expect(res.status).toBe(200); + expect(res.body).toEqual({ success: true }); //get the user const User = await prisma.user.findUnique({ diff --git a/api/routes/users/tests/_user.test.js b/api/routes/users/tests/_user.test.js index 3900b810..2ad7926e 100644 --- a/api/routes/users/tests/_user.test.js +++ b/api/routes/users/tests/_user.test.js @@ -57,7 +57,7 @@ describe("/users", () => { createdAt: expect.any(String), email: expect.any(String), firstName: expect.any(String), - hasPassword: false, + hasPassword: expect.any(Boolean), //there is sso user and non-sso user, this will vary id: expect.any(String), isMe: expect.any(Boolean), jobCount: expect.any(Number), From c2201f405fa2412e03f769fbcde92435fbafe1da Mon Sep 17 00:00:00 2001 From: Yousuf Muhammud Date: Sun, 8 Mar 2026 18:23:59 -0500 Subject: [PATCH 5/7] added backend test case and backend check for 8 chars password --- api/routes/auth/forgotPassword.js | 7 +++++++ api/routes/auth/tests/_forgotPassword.test.js | 21 +++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/api/routes/auth/forgotPassword.js b/api/routes/auth/forgotPassword.js index 6c0f8499..82874fbf 100644 --- a/api/routes/auth/forgotPassword.js +++ b/api/routes/auth/forgotPassword.js @@ -47,7 +47,14 @@ export const put = [ async (req, res) => { const { newPassword, token } = req.body; + try { + + if (newPassword.length < 8){ + return res.status(400).json({error : "Password must be at least 8 characters." }); + } + + const decodedToken = jwt.verify(token, process.env.JWT_SECRET); const hashedPassword = await bcrypt.hash(newPassword, 10); diff --git a/api/routes/auth/tests/_forgotPassword.test.js b/api/routes/auth/tests/_forgotPassword.test.js index 4dad0cf8..9f1816f5 100644 --- a/api/routes/auth/tests/_forgotPassword.test.js +++ b/api/routes/auth/tests/_forgotPassword.test.js @@ -92,6 +92,7 @@ describe("/api/auth/forgotPassword", () => { }); + it("returns status 401, user not found", async () => { //token is valid, but the user doesnt exist with that token const fakeUserId = "00000000-0000-0000-0000-000000000000"; @@ -112,6 +113,26 @@ describe("/api/auth/forgotPassword", () => { }); + it("returns status 400, Password must be at least 8 characters.", async () => { //passsword is not 8 charachters + const fakeUserId = "00000000-0000-0000-0000-000000000000"; + + const token = jwt.sign( //we need the jwt token to verify successfully + { id: fakeUserId }, + process.env.JWT_SECRET + ); + const res = await request(app) + .put("/api/auth/forgotPassword") + .send({ + token: token, + newPassword: "newPass", + }); + + expect(res.status).toBe(400); + expect(res.body).toEqual({ error: "Password must be at least 8 characters." }); + + }); + + }); }); From 7a7c3c55bf31e3d018e5c21eff1ee29f532035a6 Mon Sep 17 00:00:00 2001 From: Yousuf Muhammud Date: Sun, 8 Mar 2026 18:51:25 -0500 Subject: [PATCH 6/7] everything added --- app/src/components/authentication/ResetPasswordForm.jsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/src/components/authentication/ResetPasswordForm.jsx b/app/src/components/authentication/ResetPasswordForm.jsx index 5e8f2409..ed495c33 100644 --- a/app/src/components/authentication/ResetPasswordForm.jsx +++ b/app/src/components/authentication/ResetPasswordForm.jsx @@ -12,6 +12,10 @@ export const ResetPasswordForm = ({ token, setMode }) => { const handleSubmit = async (e) => { e.preventDefault(); + if (newPassword.length < 8) { + setError("Password must be at least 8 characters."); + return; + } if (newPassword !== confirmPassword) { setError("Passwords do not match"); From ff9a4de65b4875fc6ebedfdfac3d2ea91f904f95 Mon Sep 17 00:00:00 2001 From: Yousuf Muhammud Date: Thu, 12 Mar 2026 01:52:01 -0500 Subject: [PATCH 7/7] small changes on admin side --- app/src/routes/users/[userId].jsx | 34 +++++++++++++++++-------------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/app/src/routes/users/[userId].jsx b/app/src/routes/users/[userId].jsx index 3050625e..056e2330 100644 --- a/app/src/routes/users/[userId].jsx +++ b/app/src/routes/users/[userId].jsx @@ -133,13 +133,17 @@ const ChangePassword = ({ user }) => { const updatePassword = async (user) => { try { - const r = await authFetch(`/api/routes/login`, { + if (password.length < 8){ + toast.error("Password must be at least 8 characters."); + return; + } + const r = await authFetch(`/api/auth/login`, { method: "PUT", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ - userId: user.userId, + userId: user.id, password: password, }), @@ -147,15 +151,17 @@ const ChangePassword = ({ user }) => { }); const data = await r.json(); - - if (!r.ok) { toast.error(data.error) return; } + toast.success("Password updated!"); } catch (error) { - toast.error(error); - //add to sentry later? + toast.error(error.message || "Something went wrong."); + } + finally{ + setPassword(""); + } }; @@ -169,10 +175,11 @@ const ChangePassword = ({ user }) => { fontSize: "0.875rem" }} > - Set New Password for {user.firstName} + Set New Password for {user?.firstName} {user?.lastName} -
+
+
{ onChange={(val) => setPassword(val)} style={{ borderTopRightRadius: 0, - borderBottomRightRadius: 0, + borderBottomRightRadius: 0 }} />
@@ -189,17 +196,14 @@ const ChangePassword = ({ user }) => { +