test(passwords): add unit tests for encode/verify/round-trip
This commit is contained in:
@@ -0,0 +1,74 @@
|
|||||||
|
/**
|
||||||
|
* Tests for the password helper. Exercises:
|
||||||
|
* - hash round-trip (verify(hashed) returns true for the same plaintext)
|
||||||
|
* - wrong password returns false
|
||||||
|
* - tampered/malformed stored values return false (no exception)
|
||||||
|
* - distinct passwords produce distinct hashes (salting works)
|
||||||
|
* - format is `scrypt$N$salt$hash` and self-describing
|
||||||
|
*/
|
||||||
|
import { describe, it, expect } from "vitest";
|
||||||
|
import { hashPassword, verifyPassword } from "@/lib/passwords";
|
||||||
|
|
||||||
|
// `src/lib/passwords.ts` uses `import "server-only"`. Stub it so the test
|
||||||
|
// runner can import the module.
|
||||||
|
import { vi } from "vitest";
|
||||||
|
vi.mock("server-only", () => ({}));
|
||||||
|
|
||||||
|
describe("hashPassword / verifyPassword", () => {
|
||||||
|
it("round-trips: verify(hash(p)) is true for the same plaintext", () => {
|
||||||
|
const h = hashPassword("admin");
|
||||||
|
expect(verifyPassword("admin", h)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns false for the wrong password", () => {
|
||||||
|
const h = hashPassword("admin");
|
||||||
|
expect(verifyPassword("wrong", h)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns false for an empty password (without throwing)", () => {
|
||||||
|
const h = hashPassword("admin");
|
||||||
|
expect(verifyPassword("", h)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns false for a tampered stored value (no exception)", () => {
|
||||||
|
expect(verifyPassword("admin", "not-a-real-hash")).toBe(false);
|
||||||
|
expect(verifyPassword("admin", "scrypt$999$abc$def")).toBe(false);
|
||||||
|
expect(verifyPassword("admin", "")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses a random salt — same password produces different hashes", () => {
|
||||||
|
const a = hashPassword("admin");
|
||||||
|
const b = hashPassword("admin");
|
||||||
|
expect(a).not.toBe(b);
|
||||||
|
expect(verifyPassword("admin", a)).toBe(true);
|
||||||
|
expect(verifyPassword("admin", b)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("hashes in the self-describing `scrypt$N$salt$hash` format", () => {
|
||||||
|
const h = hashPassword("admin");
|
||||||
|
const parts = h.split("$");
|
||||||
|
expect(parts).toHaveLength(4);
|
||||||
|
expect(parts[0]).toBe("scrypt");
|
||||||
|
expect(Number.parseInt(parts[1], 10)).toBeGreaterThan(0);
|
||||||
|
expect(parts[2]).toMatch(/^[0-9a-f]+$/);
|
||||||
|
expect(parts[3]).toMatch(/^[0-9a-f]+$/);
|
||||||
|
expect(parts[3].length).toBe(128); // 64 bytes hex = 128 chars
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles long passwords", () => {
|
||||||
|
const long = "x".repeat(1024);
|
||||||
|
const h = hashPassword(long);
|
||||||
|
expect(verifyPassword(long, h)).toBe(true);
|
||||||
|
expect(verifyPassword(long + "x", h)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles unicode passwords", () => {
|
||||||
|
const h = hashPassword("пароль密碼🔐");
|
||||||
|
expect(verifyPassword("пароль密碼🔐", h)).toBe(true);
|
||||||
|
expect(verifyPassword("пароль密碼", h)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("throws on empty input to hashPassword (caller bug, fail fast)", () => {
|
||||||
|
expect(() => hashPassword("")).toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user