diff --git a/back/src/__tests__/UserResolver.test.ts b/back/src/__tests__/UserResolver.test.ts new file mode 100644 index 0000000..c12447e --- /dev/null +++ b/back/src/__tests__/UserResolver.test.ts @@ -0,0 +1,88 @@ +import { User } from "../entities/User"; +import UserResolver from "../resolvers/UserResolver"; +import argon2 from "argon2"; +import jwt from "jsonwebtoken"; + +describe("UserResolver", () => { + let userResolver: UserResolver; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let mockContext: any; + let mockUser: User; + let passwordTest: string; + + beforeEach(() => { + userResolver = new UserResolver(); + mockContext = { + res: { + setHeader: jest.fn(), + }, + payload: { id: "e5fb990e-f9d9-4858-82d1-1fd1755485a5" }, + }; + mockUser = { + id: "e5fb990e-f9d9-4858-82d1-1fd1755485a5", + username: "Pierre", + email: "pierre@health-checker.fr", + } as User; + passwordTest = "password123"; + process.env.JWT_SECRET_KEY = "TEST_KEY"; + }); + + it("createUser should hash password, save user, generate token, and return user info", async () => { + jest.spyOn(argon2, "hash").mockResolvedValue("hashedPassword"); + jest.spyOn(User, "save").mockResolvedValue(mockUser); + jest.spyOn(jwt, "sign").mockImplementation(() => "mockToken"); + + const result = await userResolver.createUser( + mockUser.username, + mockUser.email, + passwordTest, + mockContext, + ); + + expect(User.save).toHaveBeenCalled(); + expect(mockContext.res.setHeader).toHaveBeenCalledWith( + "Set-Cookie", + expect.stringContaining("token=mockToken"), + ); + expect(JSON.parse(result)).toEqual({ + id: mockUser.id, + username: mockUser.username, + email: mockUser.email, + }); + }); + + it("should log in a user and set a cookie", async () => { + jest.spyOn(User, "findOneByOrFail").mockResolvedValue(mockUser); + jest.spyOn(argon2, "verify").mockResolvedValue(true); + jest.spyOn(jwt, "sign").mockImplementation(() => "mockToken"); + + const result = await userResolver.login( + mockUser.username, + passwordTest, + mockContext, + ); + + expect(User.findOneByOrFail).toHaveBeenCalledWith({ + email: mockUser.username, + }); + expect(mockContext.res.setHeader).toHaveBeenCalledWith( + "Set-Cookie", + expect.stringContaining("token=mockToken"), + ); + expect(JSON.parse(result)).toEqual({ + id: mockUser.id, + username: mockUser.username, + email: mockUser.email, + }); + }); + + it("should log out a user", async () => { + const result = await userResolver.logout(mockContext); + + expect(mockContext.res.setHeader).toHaveBeenCalledWith( + "Set-Cookie", + "token=;Max-Age=0", + ); + expect(result).toBe("Logged out"); + }); +}); diff --git a/back/src/resolvers/UserResolver.ts b/back/src/resolvers/UserResolver.ts index 5904d96..95cbad1 100644 --- a/back/src/resolvers/UserResolver.ts +++ b/back/src/resolvers/UserResolver.ts @@ -6,18 +6,18 @@ import MyContext from "../types/MyContext"; import { Roles } from "../entities/User"; import { NotifFrequency } from "../entities/NotifFrequency"; -function setCookie(context: MyContext, token: string) { +export function setCookie(context: MyContext, token: string) { const expireCookieUTC = new Date(); expireCookieUTC.setSeconds( expireCookieUTC.getSeconds() + Number(process.env.COOKIE_TTL), - ); + ) - const secureCookie = process.env.APP_ENV === "production" ? "; Secure" : ""; + const secureCookie = process.env.APP_ENV === "production" ? "; Secure" : "" context.res.setHeader( "Set-Cookie", `token=${token}${secureCookie};httpOnly;expires=${expireCookieUTC.toUTCString()}`, - ); + ) } function getUserBasicInfo(user: User) { @@ -26,7 +26,7 @@ function getUserBasicInfo(user: User) { email: user.email, username: user.username, role: user.role, - }; + } } class UserResolver { @@ -38,24 +38,24 @@ class UserResolver { @Ctx() context: MyContext, ) { if (process.env.JWT_SECRET_KEY === undefined) { - throw new Error("NO JWT SECRET KEY DEFINED"); + throw new Error("NO JWT SECRET KEY DEFINED") } - const hashedPassword = await argon2.hash(password); + const hashedPassword = await argon2.hash(password) const userFromDB = await User.save({ username: username, email: email, hashedPassword: hashedPassword, - }); + }) const token = jwt.sign( { id: userFromDB.id, email: userFromDB.email }, process.env.JWT_SECRET_KEY, - ); + ) - setCookie(context, token); + setCookie(context, token) - return JSON.stringify(getUserBasicInfo(userFromDB)); + return JSON.stringify(getUserBasicInfo(userFromDB)) } @Mutation(() => String) @@ -69,34 +69,34 @@ class UserResolver { throw new Error("NO JWT SECRET KEY DEFINED"); } - const userFromDB = await User.findOneByOrFail({ email }); + const userFromDB = await User.findOneByOrFail({ email }) const isCorrectPassword = await argon2.verify( userFromDB.hashedPassword, password, - ); + ) if (!isCorrectPassword) { - throw new Error(); + throw new Error() } const token = jwt.sign( { id: userFromDB.id, email: userFromDB.email }, process.env.JWT_SECRET_KEY, - ); - setCookie(context, token); + ) + setCookie(context, token) - return JSON.stringify(getUserBasicInfo(userFromDB)); + return JSON.stringify(getUserBasicInfo(userFromDB)) } catch (error) { - console.log(error); - throw new Error("Bad request"); + console.log(error) + throw new Error("Bad request") } } @Query(() => String) async logout(@Ctx() context: MyContext) { - context.res.setHeader("Set-Cookie", `token=;Max-Age=0`); - return "Logged out"; + context.res.setHeader("Set-Cookie", `token=;Max-Age=0`) + return "Logged out" } @Query(() => String) @@ -105,12 +105,12 @@ class UserResolver { if (context.payload) { const userFromDB = await User.findOneByOrFail({ id: context.payload.id, - }); - return JSON.stringify(getUserBasicInfo(userFromDB)); - } else throw new Error(); + }) + return JSON.stringify(getUserBasicInfo(userFromDB)) + } else throw new Error() } catch (error) { - console.log(error); - throw new Error("Bad request"); + console.log(error) + throw new Error("Bad request") } } @@ -120,18 +120,18 @@ class UserResolver { if (context.payload) { const requestingUser = await User.findOneByOrFail({ id: context.payload.id, - }); + }) if (requestingUser.role !== Roles.ADMIN) { - throw new Error("Unauthorized"); + throw new Error("Unauthorized") } - const users = await User.find(); - return JSON.stringify(users.map(getUserBasicInfo)); - } else throw new Error(); + const users = await User.find() + return JSON.stringify(users.map(getUserBasicInfo)) + } else throw new Error() } catch (error) { - console.log(error); - throw new Error("Bad request"); + console.log(error) + throw new Error("Bad request") } } @@ -140,15 +140,15 @@ class UserResolver { try { const user = await User.findOneByOrFail({ id: context.payload?.id, - }); - if (!user) throw new Error("User not found"); + }) + if (!user) throw new Error("User not found") const userNotifFrequency = await NotifFrequency.findOneByOrFail({ id: user.notifFrequency?.id, - }); - if (!userNotifFrequency) throw new Error("Frequency not found"); - return userNotifFrequency.interval; + }) + if (!userNotifFrequency) throw new Error("Frequency not found") + return userNotifFrequency.interval } catch (err) { - throw new Error(err); + throw new Error(err) } } @@ -161,20 +161,20 @@ class UserResolver { if (context.payload) { const user = await User.findOneByOrFail({ id: context.payload.id, - }); + }) try { const resFrequency = await NotifFrequency.findOneByOrFail({ interval: frequency, - }); - user.notifFrequency = resFrequency; - await User.save(user); - return JSON.stringify(getUserBasicInfo(user)); + }) + user.notifFrequency = resFrequency + await User.save(user) + return JSON.stringify(getUserBasicInfo(user)) } catch { - throw new Error("Frequency not found"); + throw new Error("Frequency not found") } - } else throw new Error(); + } else throw new Error() } catch (err) { - throw new Error(err); + throw new Error(err) } } }