mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-11 13:52:20 +00:00
test: pin three invariants that were fixed but unguarded
Each of these was already correct in the tree and had nothing asserting it, so a refactor would have regressed it silently. The decrypted-snapshot LRU keys on userId, but only same-user memoisation and scope-change recomputation were covered — dropping the userId segment would have served one account's health snapshot to another with a green suite. The project has shipped one cross-user cache-leak fix already; this is a repeat class. The GlitchTip forwarder redacts path-segment secrets in the url it sends. The query-string strip was tested; the path-segment pass was not, and that is the one that reaches a durable external incident UI. The Withings credentials DELETE narrows its connection-row delete to P2025 and rethrows everything else, so a real failure can no longer null the client credentials and report a successful disconnect while the encrypted OAuth tokens survive. The route had no test file at all.
This commit is contained in:
1 parent
94157a50c7
commit
4b87b9e33b
3 files changed
+294
No files matched your search
@@ -0,0 +1,96 @@
|
||||
/**
|
||||
* `DELETE /api/withings/credentials` — disconnect honesty.
|
||||
*
|
||||
* The handler deletes the connection row (holding the encrypted OAuth access +
|
||||
* refresh tokens) before nulling the credential columns. That delete used to be
|
||||
* `.catch(() => {})`: on a real failure the handler carried on, nulled the
|
||||
* client id/secret, and returned `{ deleted: true }` while the token-bearing
|
||||
* row survived — the user is told they are disconnected and they are not.
|
||||
*
|
||||
* It now narrows to P2025 ("already gone", a genuine no-op) and rethrows
|
||||
* everything else. Withings had no test file at all, which is why this one
|
||||
* exists; the WHOOP and Fitbit twins carry the same shape.
|
||||
*/
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
|
||||
vi.mock("@/lib/db", () => ({
|
||||
prisma: {
|
||||
withingsConnection: { delete: vi.fn() },
|
||||
user: { update: vi.fn() },
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/auth/session", () => ({ getSession: vi.fn() }));
|
||||
vi.mock("@/lib/auth/audit", () => ({
|
||||
auditLog: vi.fn().mockResolvedValue(undefined),
|
||||
}));
|
||||
vi.mock("@/lib/logging/transports", () => ({ emitIfSampled: vi.fn() }));
|
||||
vi.mock("@/lib/db-compat", () => ({
|
||||
ensureDbCompatibility: vi.fn().mockResolvedValue(undefined),
|
||||
}));
|
||||
vi.mock("next/headers", () => ({
|
||||
headers: vi.fn(async () => ({ get: () => null })),
|
||||
cookies: vi.fn(async () => ({
|
||||
get: () => undefined,
|
||||
set: () => {},
|
||||
delete: () => {},
|
||||
})),
|
||||
}));
|
||||
|
||||
import { DELETE } from "../route";
|
||||
import { prisma } from "@/lib/db";
|
||||
import { getSession } from "@/lib/auth/session";
|
||||
|
||||
const SESSION_OK = {
|
||||
session: { id: "sess-1", expiresAt: new Date(Date.now() + 3_600_000) },
|
||||
user: { id: "user-1", username: "tester", role: "USER" as const },
|
||||
};
|
||||
|
||||
/** Shape of a Prisma "record to delete does not exist" rejection. */
|
||||
function p2025() {
|
||||
return Object.assign(new Error("Record to delete does not exist."), {
|
||||
code: "P2025",
|
||||
name: "PrismaClientKnownRequestError",
|
||||
});
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
vi.mocked(getSession).mockResolvedValue(SESSION_OK as never);
|
||||
vi.mocked(prisma.user.update).mockResolvedValue({} as never);
|
||||
});
|
||||
|
||||
describe("DELETE /api/withings/credentials", () => {
|
||||
it("disconnects and nulls the credential columns on the happy path", async () => {
|
||||
vi.mocked(prisma.withingsConnection.delete).mockResolvedValue({} as never);
|
||||
const res = await DELETE();
|
||||
expect(res.status).toBe(200);
|
||||
expect(prisma.user.update).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
data: {
|
||||
withingsClientIdEncrypted: null,
|
||||
withingsClientSecretEncrypted: null,
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("treats an already-missing connection row (P2025) as a benign no-op", async () => {
|
||||
vi.mocked(prisma.withingsConnection.delete).mockRejectedValue(p2025());
|
||||
const res = await DELETE();
|
||||
expect(res.status).toBe(200);
|
||||
// Idempotent disconnect still clears the credentials.
|
||||
expect(prisma.user.update).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does NOT report success when the token-bearing row fails to delete", async () => {
|
||||
vi.mocked(prisma.withingsConnection.delete).mockRejectedValue(
|
||||
new Error("connection terminated unexpectedly"),
|
||||
);
|
||||
const res = await DELETE();
|
||||
expect(res.status).toBe(500);
|
||||
// Critically: the credentials must NOT be nulled, because doing so while
|
||||
// the encrypted OAuth tokens survive is the worst of both states.
|
||||
expect(prisma.user.update).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user