2026-06-09 23:34:36 +08:00
|
|
|
import { describe, expect, it, vi } from "vitest";
|
|
|
|
|
|
|
|
|
|
vi.mock("../../../apps/api/src/db/index.js", () => ({
|
|
|
|
|
db: {},
|
2026-06-13 10:15:23 +08:00
|
|
|
pool: {},
|
|
|
|
|
closeDb: async () => {},
|
2026-06-09 23:34:36 +08:00
|
|
|
schema: {},
|
|
|
|
|
}));
|
|
|
|
|
|
2026-05-09 07:35:27 +08:00
|
|
|
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}$/);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
});
|