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.
This commit is contained in:
Marc-André Bombeck authored and GitHub committed 2026-05-08 13:22:46 +02:00
1 parent 725cace865
commit b5bd730ec2
4 files changed
+49 -12

No files matched your search

@@ -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<string, string> = {}): 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;
}
@@ -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;
}
+8 -6
View File
@@ -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;
});
});
+16
View File
@@ -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: {