Spaces:
Running
Running
File size: 2,715 Bytes
9509f5b | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 | import { argon2Verify } from "hash-wasm";
import { beforeEach, describe, expect, it, vi } from "vitest";
const mockGetLastSearchTokenHash = vi.fn();
const mockUpdateLastSearchTokenHash = vi.fn();
vi.mock("./logEntries", () => ({ addLogEntry: vi.fn() }));
vi.mock("./pubSub", () => ({
getLastSearchTokenHash: () => mockGetLastSearchTokenHash(),
updateLastSearchTokenHash: (hash: string) =>
mockUpdateLastSearchTokenHash(hash),
}));
const searchToken = "a".repeat(64);
vi.stubGlobal("VITE_SEARCH_TOKEN", searchToken);
/** Digest length in bytes, read from the trailing field of the encoded hash. */
function getDigestLength(encodedHash: string) {
const digest = encodedHash.split("$").pop() as string;
return atob(digest.replace(/-/g, "+").replace(/_/g, "/")).length;
}
beforeEach(() => {
vi.clearAllMocks();
mockGetLastSearchTokenHash.mockReturnValue("");
});
describe("getSearchTokenHash", () => {
it("should produce a hash the server can verify against the search token", async () => {
const { getSearchTokenHash } = await import("./searchTokenHash");
const hash = await getSearchTokenHash();
// Mirrors the server-side check in verifyTokenAndRateLimit.ts
await expect(argon2Verify({ password: searchToken, hash })).resolves.toBe(
true,
);
await expect(
argon2Verify({ password: "b".repeat(64), hash }),
).resolves.toBe(false);
expect(mockUpdateLastSearchTokenHash).toHaveBeenCalledWith(hash);
});
it("should use a digest long enough to resist blind forgery", async () => {
const { getSearchTokenHash } = await import("./searchTokenHash");
const hash = await getSearchTokenHash();
expect(getDigestLength(hash)).toBe(32);
});
it("should reuse a cached hash that still matches the search token", async () => {
const { getSearchTokenHash } = await import("./searchTokenHash");
const cachedHash = await getSearchTokenHash();
mockGetLastSearchTokenHash.mockReturnValue(cachedHash);
mockUpdateLastSearchTokenHash.mockClear();
expect(await getSearchTokenHash()).toBe(cachedHash);
expect(mockUpdateLastSearchTokenHash).not.toHaveBeenCalled();
});
it("should generate a fresh hash when the cached one is from another token", async () => {
const { getSearchTokenHash } = await import("./searchTokenHash");
mockGetLastSearchTokenHash.mockReturnValue(
"$argon2id$v=19$m=512,t=16,p=1$c29tZXNhbHRzb21lc2FsdA$0000000000000000000000000000000000000000000",
);
const hash = await getSearchTokenHash();
await expect(argon2Verify({ password: searchToken, hash })).resolves.toBe(
true,
);
expect(mockUpdateLastSearchTokenHash).toHaveBeenCalledWith(hash);
});
});
|