Spaces:
Running
Running
| 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); | |
| }); | |
| }); | |