Files
SnapOtter/tests/unit/api/auth-functions.test.ts
T

113 lines
3.5 KiB
TypeScript
Raw Normal View History

import { describe, expect, it, vi } from "vitest";
vi.mock("../../../apps/api/src/db/index.js", () => ({
db: {},
pool: {},
closeDb: async () => {},
schema: {},
}));
import {
computeKeyPrefix,
hashPassword,
verifyPassword,
} from "../../../apps/api/src/plugins/auth.js";
describe("hashPassword", () => {
it("returns a string in salt:hash format", async () => {
const result = await hashPassword("TestPass1");
const parts = result.split(":");
expect(parts).toHaveLength(2);
});
it("produces a 64-character hex salt (32 bytes)", async () => {
const result = await hashPassword("TestPass1");
const [salt] = result.split(":");
expect(salt).toHaveLength(64);
expect(salt).toMatch(/^[0-9a-f]{64}$/);
});
it("produces a 128-character hex hash (64 bytes)", async () => {
const result = await hashPassword("TestPass1");
const [, hash] = result.split(":");
expect(hash).toHaveLength(128);
expect(hash).toMatch(/^[0-9a-f]{128}$/);
});
it("generates different salts on each call", async () => {
const a = await hashPassword("TestPass1");
const b = await hashPassword("TestPass1");
const saltA = a.split(":")[0];
const saltB = b.split(":")[0];
expect(saltA).not.toBe(saltB);
});
it("produces a hash that can be verified back", async () => {
const stored = await hashPassword("MySecret9");
const ok = await verifyPassword("MySecret9", stored);
expect(ok).toBe(true);
});
});
describe("verifyPassword", () => {
it("returns true for the correct password", async () => {
const stored = await hashPassword("Correct1");
expect(await verifyPassword("Correct1", stored)).toBe(true);
});
it("returns false for a wrong password", async () => {
const stored = await hashPassword("Correct1");
expect(await verifyPassword("Wrong1abc", stored)).toBe(false);
});
it("returns false for an empty stored string", async () => {
expect(await verifyPassword("anything", "")).toBe(false);
});
it("returns false for a malformed stored string (no colon)", async () => {
expect(await verifyPassword("anything", "nocolonhere")).toBe(false);
});
it("returns false for a truncated hash", async () => {
const stored = await hashPassword("Correct1");
const [salt] = stored.split(":");
const truncated = `${salt}:abcd`;
expect(await verifyPassword("Correct1", truncated)).toBe(false);
});
it("handles unicode passwords", async () => {
const stored = await hashPassword("üéàñö");
expect(await verifyPassword("üéàñö", stored)).toBe(true);
expect(await verifyPassword("plain", stored)).toBe(false);
});
});
describe("computeKeyPrefix", () => {
it("returns a 16-character hex string", () => {
const prefix = computeKeyPrefix("si_some-api-key");
expect(prefix).toHaveLength(16);
expect(prefix).toMatch(/^[0-9a-f]{16}$/);
});
it("is deterministic (same input gives same output)", () => {
const a = computeKeyPrefix("si_test-key-123");
const b = computeKeyPrefix("si_test-key-123");
expect(a).toBe(b);
});
it("produces different prefixes for different inputs", () => {
const a = computeKeyPrefix("si_key-alpha");
const b = computeKeyPrefix("si_key-beta");
expect(a).not.toBe(b);
});
it("works with various key formats", () => {
const inputs = ["si_short", "si_a-very-long-api-key-with-many-segments-1234567890", "si_"];
for (const input of inputs) {
const prefix = computeKeyPrefix(input);
expect(prefix).toHaveLength(16);
expect(prefix).toMatch(/^[0-9a-f]{16}$/);
}
});
});