From b5bd730ec28eac22ae83adf09dc68e03009d6b8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Fri, 8 May 2026 13:22:46 +0200 Subject: [PATCH] test(auth): tighten native-token hash assertions, isolate hmac env, expand coverage scope (#136) Three small follow-ups to the v1.3.3 audit: - src/lib/auth/__tests__/hmac.test.ts now stubs API_TOKEN_HMAC_KEY via vi.stubEnv per test instead of mutating process.env in beforeAll, so the variable is restored cleanly after every case (and worker leaks under --no-isolate are gone). - The two native-token issuance tests (password login + passkey login) now mock hashToken as (raw) => "hashed:" + raw and assert the persisted tokenHash equals hashToken(plaintext) instead of accepting any string. Catches a regression where the route would hash the wrong value. - vitest.config.mts adopts coverage.include for the full src/ tree (the vitest 4 replacement for coverage.all=true) so untouched files surface with 0% rather than being silently omitted, and excludes the future tests/integration/** suite so unit-test runs stay fast. No production code touched; suite stays at 454 green. --- .../auth/login/__tests__/native-token.test.ts | 21 +++++++++++++++---- .../__tests__/native-token.test.ts | 10 +++++++-- src/lib/auth/__tests__/hmac.test.ts | 14 +++++++------ vitest.config.mts | 16 ++++++++++++++ 4 files changed, 49 insertions(+), 12 deletions(-) diff --git a/src/app/api/auth/login/__tests__/native-token.test.ts b/src/app/api/auth/login/__tests__/native-token.test.ts index 4e29595c1..7e3d73b58 100644 --- a/src/app/api/auth/login/__tests__/native-token.test.ts +++ b/src/app/api/auth/login/__tests__/native-token.test.ts @@ -23,7 +23,9 @@ vi.mock("@/lib/auth/audit", () => ({ })); vi.mock("@/lib/rate-limit", () => ({ - checkRateLimit: vi.fn().mockResolvedValue({ allowed: true, remaining: 5, reset: 0 }), + checkRateLimit: vi + .fn() + .mockResolvedValue({ allowed: true, remaining: 5, reset: 0 }), rateLimitHeaders: vi.fn(() => ({})), })); @@ -32,7 +34,7 @@ vi.mock("@/lib/db-compat", () => ({ })); vi.mock("@/lib/auth/hmac", () => ({ - hashToken: vi.fn(() => "deadbeef"), + hashToken: vi.fn((raw: string) => `hashed:${raw}`), })); vi.mock("@/lib/logging/transports", () => ({ @@ -41,7 +43,11 @@ vi.mock("@/lib/logging/transports", () => ({ vi.mock("next/headers", () => ({ headers: vi.fn(async () => ({ get: () => null })), - cookies: vi.fn(async () => ({ get: () => undefined, set: () => {}, delete: () => {} })), + cookies: vi.fn(async () => ({ + get: () => undefined, + set: () => {}, + delete: () => {}, + })), })); import { POST } from "../route"; @@ -66,7 +72,10 @@ function makeRequest(headers: Record = {}): NextRequest { "content-type": "application/json", ...headers, }, - body: JSON.stringify({ email: "marc@example.com", password: "supersecret" }), + body: JSON.stringify({ + email: "marc@example.com", + password: "supersecret", + }), }); } @@ -100,6 +109,8 @@ describe("POST /api/auth/login — native token issuance", () => { expect(body.data.token).toMatch(/^hlk_[a-f0-9]{64}$/); expect(body.data.tokenExpiresAt).toEqual(expect.any(String)); expect(prisma.apiToken.create).toHaveBeenCalledTimes(1); + const createArgs = vi.mocked(prisma.apiToken.create).mock.calls[0][0]; + expect(createArgs.data.tokenHash).toBe(`hashed:${body.data.token}`); } finally { process.env.API_TOKEN_HMAC_KEY = original; } @@ -126,6 +137,8 @@ describe("POST /api/auth/login — native token issuance", () => { const body = (await res.json()) as { data: { token?: string } }; expect(res.status).toBe(200); expect(body.data.token).toMatch(/^hlk_/); + const createArgs = vi.mocked(prisma.apiToken.create).mock.calls[0][0]; + expect(createArgs.data.tokenHash).toBe(`hashed:${body.data.token}`); } finally { process.env.API_TOKEN_HMAC_KEY = original; } diff --git a/src/app/api/auth/passkey/login-verify/__tests__/native-token.test.ts b/src/app/api/auth/passkey/login-verify/__tests__/native-token.test.ts index c854ec98a..0ee48dcb2 100644 --- a/src/app/api/auth/passkey/login-verify/__tests__/native-token.test.ts +++ b/src/app/api/auth/passkey/login-verify/__tests__/native-token.test.ts @@ -29,7 +29,7 @@ vi.mock("@/lib/db-compat", () => ({ })); vi.mock("@/lib/auth/hmac", () => ({ - hashToken: vi.fn(() => "deadbeef"), + hashToken: vi.fn((raw: string) => `hashed:${raw}`), })); vi.mock("@/lib/logging/transports", () => ({ @@ -38,7 +38,11 @@ vi.mock("@/lib/logging/transports", () => ({ vi.mock("next/headers", () => ({ headers: vi.fn(async () => ({ get: () => null })), - cookies: vi.fn(async () => ({ get: () => undefined, set: () => {}, delete: () => {} })), + cookies: vi.fn(async () => ({ + get: () => undefined, + set: () => {}, + delete: () => {}, + })), })); import { POST } from "../route"; @@ -91,6 +95,8 @@ describe("POST /api/auth/passkey/login-verify — native token issuance", () => expect(body.data.token).toMatch(/^hlk_[a-f0-9]{64}$/); expect(body.data.tokenExpiresAt).toEqual(expect.any(String)); expect(prisma.apiToken.create).toHaveBeenCalledTimes(1); + const createArgs = vi.mocked(prisma.apiToken.create).mock.calls[0][0]; + expect(createArgs.data.tokenHash).toBe(`hashed:${body.data.token}`); } finally { process.env.API_TOKEN_HMAC_KEY = original; } diff --git a/src/lib/auth/__tests__/hmac.test.ts b/src/lib/auth/__tests__/hmac.test.ts index 022d4b026..7852f9ed3 100644 --- a/src/lib/auth/__tests__/hmac.test.ts +++ b/src/lib/auth/__tests__/hmac.test.ts @@ -1,11 +1,15 @@ -import { describe, it, expect, beforeAll } from "vitest"; +import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; import { verifyHmacSignature, hashToken } from "../hmac"; import { createHmac } from "node:crypto"; const TEST_HMAC_KEY = "test-hmac-key-for-unit-tests"; -beforeAll(() => { - process.env.API_TOKEN_HMAC_KEY = TEST_HMAC_KEY; +beforeEach(() => { + vi.stubEnv("API_TOKEN_HMAC_KEY", TEST_HMAC_KEY); +}); + +afterEach(() => { + vi.unstubAllEnvs(); }); describe("verifyHmacSignature", () => { @@ -75,11 +79,9 @@ describe("hashToken", () => { }); it("throws if API_TOKEN_HMAC_KEY is not set", () => { - const original = process.env.API_TOKEN_HMAC_KEY; - delete process.env.API_TOKEN_HMAC_KEY; + vi.stubEnv("API_TOKEN_HMAC_KEY", ""); expect(() => hashToken("test")).toThrow( "API_TOKEN_HMAC_KEY env var must be set", ); - process.env.API_TOKEN_HMAC_KEY = original; }); }); diff --git a/vitest.config.mts b/vitest.config.mts index 726694c84..f84484088 100644 --- a/vitest.config.mts +++ b/vitest.config.mts @@ -9,6 +9,22 @@ export default defineConfig({ test: { globals: true, environment: "node", + // Default `pnpm test` keeps unit tests only — integration suite uses + // testcontainers and runs separately via `pnpm test:integration`. + exclude: ["**/node_modules/**", "**/dist/**", "tests/integration/**"], + coverage: { + provider: "v8", + // `include` enumerates the full source tree so the report shows + // per-file coverage for files never imported by a test (vitest 4 + // dropped the `coverage.all` flag and uses `include` for this). + include: ["src/**/*.ts", "src/**/*.tsx"], + exclude: [ + "src/generated/**", + "src/**/__tests__/**", + "src/**/*.test.ts", + "src/**/*.test.tsx", + ], + }, }, resolve: { alias: {