mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 20:42:43 +00:00
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:
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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
});
|
||||
@@ -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: {
|
||||
|
||||
Reference in new issue
Block a user