mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-11 22:02:16 +00:00
Adds vitest unit tests for five previously zero-coverage routes: - /api/admin/users (GET) — admin gate, safe select shape regression, no password-hash leak. - /api/admin/users/[id] (PUT) — Zod validation, 404 for unknown id, last-admin demotion guard, audit-log call shape. - /api/admin/settings (GET / PUT) — singleton row regression, encrypted blob never reaches the client, AES encrypt() called for VAPID + GitHub-issue tokens, clear flags wipe encrypted columns, every schema-allowed field round-trips through upsert. - /api/admin/status (GET) — integration health summary aggregates umami / glitchtip / web-push / bug-report flags, only non-revoked tokens count as active, encrypted blobs never appear in the envelope. - /api/monitoring/glitchtip (POST) — IP rate-limit gate, short-circuit when DSN is null, message/stack size caps (2000 / 20 000 chars), upstream 5xx mapped to 502, environment fallback to "production". All five files now land at >=80% line coverage: - admin/users/route.ts: 100% - admin/users/[id]/route.ts: 100% - admin/settings/route.ts: 100% - admin/status/route.ts: 84.21% - monitoring/glitchtip/route.ts: 100% Test count: 469 -> 523 (+54; my files contribute 46, the other 8 were unblocked by re-running prisma generate in the worktree). Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
120 lines
3.3 KiB
TypeScript
120 lines
3.3 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
|
|
vi.mock("@/lib/db", () => ({
|
|
prisma: {
|
|
user: { findMany: vi.fn() },
|
|
},
|
|
}));
|
|
|
|
vi.mock("@/lib/api-handler", async () => {
|
|
const actual =
|
|
await vi.importActual<typeof import("@/lib/api-handler")>(
|
|
"@/lib/api-handler",
|
|
);
|
|
return {
|
|
...actual,
|
|
apiHandler: <T extends (...args: unknown[]) => Promise<Response>>(
|
|
h: T,
|
|
): T => h,
|
|
requireAdmin: vi.fn(),
|
|
};
|
|
});
|
|
|
|
vi.mock("@/lib/logging/context", () => ({
|
|
annotate: vi.fn(),
|
|
getEvent: vi.fn(() => null),
|
|
}));
|
|
|
|
import { GET } from "../route";
|
|
import { prisma } from "@/lib/db";
|
|
import { requireAdmin, HttpError } from "@/lib/api-handler";
|
|
|
|
const mockUserFindMany = vi.mocked(prisma.user.findMany);
|
|
const mockRequireAdmin = vi.mocked(requireAdmin);
|
|
|
|
describe("GET /api/admin/users", () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
mockRequireAdmin.mockResolvedValue({
|
|
session: { id: "s1", expiresAt: new Date(Date.now() + 3_600_000) },
|
|
user: {
|
|
id: "admin-1",
|
|
username: "admin",
|
|
role: "ADMIN",
|
|
} as never,
|
|
});
|
|
mockUserFindMany.mockResolvedValue([] as never);
|
|
});
|
|
|
|
it("rejects with 401 when no session", async () => {
|
|
mockRequireAdmin.mockRejectedValue(new HttpError(401, "Not authenticated"));
|
|
await expect(GET()).rejects.toThrow("Not authenticated");
|
|
});
|
|
|
|
it("rejects with 403 when caller is not admin", async () => {
|
|
mockRequireAdmin.mockRejectedValue(
|
|
new HttpError(403, "Admin access required"),
|
|
);
|
|
await expect(GET()).rejects.toThrow("Admin access required");
|
|
});
|
|
|
|
it("returns users sorted by createdAt ascending with passkey counts", async () => {
|
|
const created = new Date("2024-01-01T00:00:00Z");
|
|
mockUserFindMany.mockResolvedValue([
|
|
{
|
|
id: "u1",
|
|
username: "alice",
|
|
email: "alice@example.com",
|
|
role: "ADMIN",
|
|
createdAt: created,
|
|
_count: { passkeys: 2 },
|
|
},
|
|
{
|
|
id: "u2",
|
|
username: "bob",
|
|
email: null,
|
|
role: "USER",
|
|
createdAt: created,
|
|
_count: { passkeys: 0 },
|
|
},
|
|
] as never);
|
|
|
|
const res = await GET();
|
|
expect(res.status).toBe(200);
|
|
const body = (await res.json()) as {
|
|
data: Array<Record<string, unknown>>;
|
|
};
|
|
expect(body.data).toHaveLength(2);
|
|
expect(body.data[0]).toEqual({
|
|
id: "u1",
|
|
username: "alice",
|
|
email: "alice@example.com",
|
|
role: "ADMIN",
|
|
createdAt: created.toISOString(),
|
|
passkeyCount: 2,
|
|
});
|
|
expect(body.data[1].passkeyCount).toBe(0);
|
|
expect(body.data[1].email).toBeNull();
|
|
});
|
|
|
|
it("queries Prisma with the safe select shape (no password / sensitive cols)", async () => {
|
|
await GET();
|
|
expect(mockUserFindMany).toHaveBeenCalledTimes(1);
|
|
const args = mockUserFindMany.mock.calls[0]?.[0] as {
|
|
select: Record<string, unknown>;
|
|
orderBy: { createdAt: "asc" | "desc" };
|
|
};
|
|
// Regression: the route must NOT pull password hash / token columns.
|
|
expect(args.select).toEqual({
|
|
id: true,
|
|
username: true,
|
|
email: true,
|
|
role: true,
|
|
createdAt: true,
|
|
_count: { select: { passkeys: true } },
|
|
});
|
|
expect(args.select).not.toHaveProperty("passwordHash");
|
|
expect(args.orderBy).toEqual({ createdAt: "asc" });
|
|
});
|
|
});
|