Files
HiveGrid-Wireframes/hivegrid-cloud/apps/web/app/utils/auth/passwordHasher.server.test.ts
T

90 lines
4.2 KiB
TypeScript

// @vitest-environment node
import argon2 from "argon2";
import bcryptjs from "bcryptjs";
import { describe, expect, test, vi } from "vitest";
import { hashPassword, isKnownHashFormat, verifyPassword } from "./passwordHasher.server.js";
const PLAINTEXT = "correct-horse-battery-staple";
const WRONG = "incorrect-zebra-cell-paperclip";
describe("passwordHasher.server", () => {
test("hashPassword produces an argon2id hash that round-trips", async () => {
const hash = await hashPassword(PLAINTEXT);
expect(hash.startsWith("$argon2id$")).toBe(true);
expect(await argon2.verify(hash, PLAINTEXT)).toBe(true);
});
test("verifyPassword accepts a valid argon2 hash without proposing a rehash", async () => {
const hash = await hashPassword(PLAINTEXT);
const result = await verifyPassword(PLAINTEXT, hash);
expect(result.valid).toBe(true);
expect(result.rehashedHash).toBeUndefined();
});
test("verifyPassword rejects a wrong password against an argon2 hash", async () => {
const hash = await hashPassword(PLAINTEXT);
const result = await verifyPassword(WRONG, hash);
expect(result.valid).toBe(false);
expect(result.rehashedHash).toBeUndefined();
});
test("verifyPassword accepts a valid bcrypt hash AND proposes an argon2 rehash", async () => {
const legacyHash = await bcryptjs.hash(PLAINTEXT, 4);
expect(legacyHash.startsWith("$2")).toBe(true);
const result = await verifyPassword(PLAINTEXT, legacyHash);
expect(result.valid).toBe(true);
expect(result.rehashedHash).toBeDefined();
expect(result.rehashedHash!.startsWith("$argon2id$")).toBe(true);
// The proposed rehash must itself verify the same plaintext.
expect(await argon2.verify(result.rehashedHash!, PLAINTEXT)).toBe(true);
});
test("verifyPassword rejects a wrong password against a bcrypt hash and does NOT propose a rehash", async () => {
const legacyHash = await bcryptjs.hash(PLAINTEXT, 4);
const result = await verifyPassword(WRONG, legacyHash);
expect(result.valid).toBe(false);
expect(result.rehashedHash).toBeUndefined();
});
test("verifyPassword rejects unknown / empty hashes without throwing", async () => {
await expect(verifyPassword(PLAINTEXT, "")).resolves.toEqual({ valid: false });
await expect(verifyPassword(PLAINTEXT, "garbage")).resolves.toEqual({ valid: false });
await expect(verifyPassword(PLAINTEXT, "$pbkdf2-sha256$i=29000,l=32$abc$def")).resolves.toEqual({ valid: false });
});
test("verifyPassword swallows decryption errors and reports invalid", async () => {
// An argon2-prefixed but structurally invalid hash makes argon2.verify throw.
const result = await verifyPassword(PLAINTEXT, "$argon2id$v=19$m=19456,t=2,p=1$THISisNOTaValidSalt$norIsThisAValidHash");
expect(result.valid).toBe(false);
});
test("verifyPassword reports invalid when argon2 verification throws", async () => {
const verifySpy = vi.spyOn(argon2, "verify").mockRejectedValueOnce(new Error("argon2 unavailable"));
await expect(verifyPassword(PLAINTEXT, "$argon2id$valid-looking-prefix")).resolves.toEqual({ valid: false });
expect(verifySpy).toHaveBeenCalled();
verifySpy.mockRestore();
});
test("verifyPassword reports invalid when legacy bcrypt comparison throws", async () => {
const compareSpy = vi.spyOn(bcryptjs, "compare").mockRejectedValueOnce(new Error("bcrypt unavailable"));
const legacyHash = "$2b$10$abcdefghijklmnopqrstuu5sBKjRarAkPvaemc5yQMk3QHsOJXxUy";
await expect(verifyPassword(PLAINTEXT, legacyHash)).resolves.toEqual({ valid: false });
expect(compareSpy).toHaveBeenCalledWith(PLAINTEXT, legacyHash);
compareSpy.mockRestore();
});
test("isKnownHashFormat recognises argon2, bcrypt, and unknown shapes", async () => {
expect(isKnownHashFormat(await hashPassword(PLAINTEXT))).toBe("argon2");
expect(isKnownHashFormat(await bcryptjs.hash(PLAINTEXT, 4))).toBe("bcrypt");
expect(isKnownHashFormat("$2a$10$abc")).toBe("bcrypt");
expect(isKnownHashFormat("$2y$12$abc")).toBe("bcrypt");
expect(isKnownHashFormat("$argon2i$v=19$...")).toBe("argon2");
expect(isKnownHashFormat("plaintext")).toBe("unknown");
expect(isKnownHashFormat("")).toBe("unknown");
});
});