From fa3a3ab3162811df510045bdb967191828ab869b Mon Sep 17 00:00:00 2001 From: prayansh_chhablani Date: Sun, 10 Nov 2024 10:09:01 +0530 Subject: [PATCH] increasing code coverage --- tests/resolvers/Mutation/login.spec.ts | 39 ++++++++++++ tests/resolvers/Mutation/signUp.spec.ts | 25 +++++++- tests/utilities/encryptionModule.spec.ts | 47 ++++++++++++++- tests/utilities/hashingModule.spec.ts | 75 +++++++++++++++++++++++- 4 files changed, 182 insertions(+), 4 deletions(-) diff --git a/tests/resolvers/Mutation/login.spec.ts b/tests/resolvers/Mutation/login.spec.ts index 3c00b6fdb3..1e05140beb 100644 --- a/tests/resolvers/Mutation/login.spec.ts +++ b/tests/resolvers/Mutation/login.spec.ts @@ -28,6 +28,7 @@ import { createTestEventWithRegistrants } from "../../helpers/eventsWithRegistra import type { TestUserType } from "../../helpers/userAndOrg"; import { decryptEmail, encryptEmail } from "../../../src/utilities/encryption"; import { hashEmail } from "../../../src/utilities/hashEmail"; +import { ValidationError } from "../../../src/libraries/errors"; let testUser: TestUserType; let MONGOOSE_INSTANCE: typeof mongoose; @@ -86,6 +87,44 @@ describe("resolvers -> Mutation -> login", () => { vi.resetModules(); }); + it("throws ValidationError if email is not found in args.data", async () => { + const { requestContext } = await import("../../../src/libraries"); + + // Spy on the translate function to capture error messages + const spy = vi + .spyOn(requestContext, "translate") + .mockImplementationOnce((message) => `Translated ${message}`); + + try { + const email = `nonexistentuser${nanoid().toLowerCase()}@gmail.com`; + const hashedEmail = hashEmail(email); + const newUser = await User.create({ + email: encryptEmail(email), + hashedEmail: hashedEmail, + password: "password", + firstName: "John", + lastName: "Doe", + }); + + const args: MutationLoginArgs = { + data: { + email: null, + password: "password", + }, + }; + + const { login: loginResolver } = await import( + "../../../src/resolvers/Mutation/login" + ); + + await loginResolver?.({}, args, {}); + } catch (error) { + expect(error).instanceOf(ValidationError); + if (error instanceof ValidationError) { + expect(error.message).toBe("Email is required"); + } + } + }); it("throws NotFoundError if the user is not found after creating AppUserProfile", async () => { const { requestContext } = await import("../../../src/libraries"); diff --git a/tests/resolvers/Mutation/signUp.spec.ts b/tests/resolvers/Mutation/signUp.spec.ts index f6dc03ede4..af0674a0a2 100644 --- a/tests/resolvers/Mutation/signUp.spec.ts +++ b/tests/resolvers/Mutation/signUp.spec.ts @@ -206,7 +206,30 @@ describe("resolvers -> Mutation -> signUp", () => { vi.restoreAllMocks(); }); - it(`throws ConflictError message if a user already with email === args.data.email already exists`, async () => { + it("throws error if email has invalid format", async () => { + try { + const args: MutationSignUpArgs = { + data: { + email: "", + firstName: "firstName", + lastName: "lastName", + password: "password", + appLanguageCode: "en", + selectedOrganization: testOrganization?._id, + }, + }; + + const { signUp: signUpResolver } = await import( + "../../../src/resolvers/Mutation/signUp" + ); + + await signUpResolver?.({}, args, {}); + } catch (error: unknown) { + expect((error as Error).message).toEqual("Invalid email format"); + } + }); + + it(`throws ConflictError message if a user already with email === args.data.email already exists`, async () => { let email = ""; if (testUser?.email) { email = decryptEmail(testUser.email).decrypted; diff --git a/tests/utilities/encryptionModule.spec.ts b/tests/utilities/encryptionModule.spec.ts index 4d24d2c979..e682524c65 100644 --- a/tests/utilities/encryptionModule.spec.ts +++ b/tests/utilities/encryptionModule.spec.ts @@ -1,4 +1,4 @@ -import { describe, it, expect } from "vitest"; +import { describe, it, expect, afterEach } from "vitest"; import { decryptEmail, encryptEmail, @@ -6,6 +6,13 @@ import { } from "../../src/utilities/encryption"; describe("encryptionModule", () => { + const validEncryptedData = + "11898325fe8807edeb99d37f0b168eaa:3991cd4d1a6372ed70492e23d499b066:4f209bb501460537fa9345ca16361023a19f9b2eff1860e8dadc80f29705d469cbe46edc4913e77d3418814b8eb7"; + const originalKey = process.env.ENCRYPTION_KEY; + + afterEach(() => { + process.env.ENCRYPTION_KEY = originalKey; + }); describe("generateRandomIV", () => { it("should generate a random salt of the specified length", () => { const salt = generateRandomIV(); @@ -41,6 +48,12 @@ describe("encryptionModule", () => { expect(decrypted).toEqual(email); }); + it("throws an error for invalid email format", () => { + expect(() => encryptEmail("a".repeat(10000))).toThrow( + "Invalid email format", + ); + }); + it("throws an error for empty email input", () => { expect(() => encryptEmail("")).toThrow("Empty or invalid email input."); }); @@ -82,4 +95,36 @@ describe("encryptionModule", () => { expect(encrypted).toMatch(/^[0-9a-f]+:[0-9a-f]+:[0-9a-f]+$/i); }); }); + + it("should throw an error if encrypted data format is invalid (missing iv, authTag, or encryptedHex)", () => { + expect(() => decryptEmail("a".repeat(10000))).toThrow( + "Invalid encrypted data format. Expected format 'iv:authTag:encryptedData'.", + ); + }); + + it("should throw an error if encryption key length is not 64 characters", () => { + process.env.ENCRYPTION_KEY = "a".repeat(32); // 32 characters instead of 64 + expect(() => decryptEmail(validEncryptedData)).toThrow( + "Encryption key must be a valid 256-bit hexadecimal string (64 characters).", + ); + }); + + it("should throw an error if encryption key contains non-hexadecimal characters", () => { + process.env.ENCRYPTION_KEY = "z".repeat(64); // 'z' is not a valid hex character + expect(() => decryptEmail(validEncryptedData)).toThrow( + "Encryption key must be a valid 256-bit hexadecimal string (64 characters).", + ); + }); + + it("should not throw an error for a valid 64-character hexadecimal encryption key", () => { + expect(() => decryptEmail(validEncryptedData)).not.toThrow(); + }); + + it("should throw an error for a invalid encrypted data", () => { + const invalidEncryptedData = + "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6:1234567890abcdef1234567890abcdef:abcd1234abcd1234"; + expect(() => decryptEmail(invalidEncryptedData)).toThrow( + "Decryption failed: invalid data or authentication tag.", + ); + }); }); diff --git a/tests/utilities/hashingModule.spec.ts b/tests/utilities/hashingModule.spec.ts index bdd3e196ca..d8c347d5fc 100644 --- a/tests/utilities/hashingModule.spec.ts +++ b/tests/utilities/hashingModule.spec.ts @@ -1,5 +1,5 @@ -import { describe, it, expect } from "vitest"; -import { hashEmail } from "../../src/utilities/hashEmail"; +import { describe, it, expect, vi } from "vitest"; +import { compareHashedEmails, hashEmail } from "../../src/utilities/hashEmail"; import { setHashPepper } from "../../setup"; describe("hashingModule", () => { @@ -50,5 +50,76 @@ describe("hashingModule", () => { process.env.HASH_PEPPER = originalPepper; } }); + it("should throw an error for an invalid email format", () => { + const invalidEmails = [ + "plainaddress", + "missing@domain", + "@missinglocal.com", + "missing@.com", + ]; + + invalidEmails.forEach((email) => { + expect(() => hashEmail(email)).toThrow("Invalid email format"); + }); + }); + + it("should throw an error if HASH_PEPPER is missing", () => { + const originalPepper = process.env.HASH_PEPPER; + delete process.env.HASH_PEPPER; + + expect(() => hashEmail("test@example.com")).toThrow( + "Missing HASH_PEPPER environment variable required for secure email hashing", + ); + + process.env.HASH_PEPPER = originalPepper; + }); + + it("should throw an error if HASH_PEPPER is shorter than 32 characters", () => { + const originalPepper = process.env.HASH_PEPPER; + process.env.HASH_PEPPER = "short_pepper"; + + expect(() => hashEmail("test@example.com")).toThrow( + "HASH_PEPPER must be at least 32 characters long", + ); + + process.env.HASH_PEPPER = originalPepper; + }); + }); + + describe("compareHashedEmails function error handling", () => { + it("should return false for invalid hashed email formats", () => { + const validHash = "a".repeat(64); + const invalidHashes = [ + "short", + "invalid_characters_!@#", + "", + null, + undefined, + ]; + + invalidHashes.forEach((invalidHash) => { + expect( + compareHashedEmails(invalidHash as unknown as string, validHash), + ).toBe(false); + expect( + compareHashedEmails(validHash, invalidHash as unknown as string), + ).toBe(false); + }); + }); + + it("should log an error and return false if crypto.timingSafeEqual fails due to invalid hex encoding", () => { + const invalidHash = "z".repeat(64); // deliberately invalid hex + let result; + try { + result = compareHashedEmails(invalidHash, invalidHash); + } catch (error) { + expect(result).toBe(false); + if (error instanceof Error) { + expect(error.message).toBe( + "Failed to compare hashes, likely due to invalid hex encoding", + ); + } + } + }); }); });