Files
HealthLog/tests/integration/routes/auth-me-source-priority.test.ts
T
Marc-André Bombeck c0c0fdb421 test(integration): move the account preference route tests onto Postgres
Fourteen route tests for the /api/auth/me preference family replaced Prisma
with a mock and asserted the calls it received. Their claims are now shown
through the real handler on the stored row and the response: validation
refusals with their codes, normalisation and defaults on read, optimistic
lock conflicts against a real second writer, rate limits driven to their
first refusal in the Postgres limiter, audit rows read back, cache entries
evicted, and the jobs a change queues observed on a real pg-boss queue.

Assertions that were only about the shape of a Prisma call are replaced by
the behaviour they stood for, and two that described a state only a mock
can reach (a missing column) by the stored value that state resolves to.
2026-10-22 05:31:59 +02:00

153 lines
4.9 KiB
TypeScript

/**
* `/api/auth/me/source-priority` — the per-metric source ladders, against the
* real database and through the real API kit.
*
* Pinned here:
* - the read fills every key the analytics layer needs from the defaults
* when nothing is stored, and carries the sources the account holds data
* from beside the ladders;
* - the PUT stores the partial shape as sent (missing keys keep reading as
* defaults), audits previous and next, and refuses a malformed shape
* without touching the row.
*/
import { beforeEach, describe, expect, it } from "vitest";
import { GET, PUT } from "@/app/api/auth/me/source-priority/route";
import { anonymous, callRoute } from "../../kit/call-route";
import { makeMeasurement, makeSession, makeUser } from "../../kit/factories";
import { getPrismaClient, truncateAllTables } from "../setup";
const prisma = getPrismaClient();
const ROUTE = "/api/auth/me/source-priority";
type SourcePriority = Record<string, unknown> & {
weight: string[];
inUse: string[];
};
beforeEach(() => truncateAllTables(prisma));
async function storedLadder(userId: string) {
const row = await prisma.user.findUniqueOrThrow({
where: { id: userId },
select: { sourcePriorityJson: true },
});
return row.sourcePriorityJson;
}
describe("GET /api/auth/me/source-priority", () => {
it("refuses an anonymous caller with 401", async () => {
const res = await callRoute(GET, {
method: "GET",
route: ROUTE,
auth: anonymous,
});
expect(res.status).toBe(401);
});
it("answers the defaulted shape when nothing is stored", async () => {
const user = await makeUser();
const res = await callRoute<SourcePriority>(GET, {
method: "GET",
route: ROUTE,
auth: await makeSession(user),
});
expect(res.status).toBe(200);
expect(res.data).toHaveProperty("weight");
expect(res.data).toHaveProperty("metricPriority");
expect(res.data).toHaveProperty("deviceTypePriority");
expect(res.data.inUse).toEqual([]);
});
it("carries the sources the account holds data from beside the ladders", async () => {
const user = await makeUser();
const other = await makeUser();
await makeMeasurement(user, { source: "WITHINGS" });
await makeMeasurement(user, { source: "MANUAL" });
await makeMeasurement(other, { source: "APPLE_HEALTH" });
const res = await callRoute<SourcePriority>(GET, {
method: "GET",
route: ROUTE,
auth: await makeSession(user),
});
expect(res.status).toBe(200);
expect([...res.data.inUse].sort()).toEqual(["MANUAL", "WITHINGS"]);
});
});
describe("PUT /api/auth/me/source-priority", () => {
it("refuses an anonymous caller with 401", async () => {
const res = await callRoute(PUT, {
method: "PUT",
route: ROUTE,
body: { weight: ["WITHINGS", "APPLE_HEALTH", "MANUAL"] },
auth: anonymous,
});
expect(res.status).toBe(401);
});
it("stores the partial shape as sent and reads it back over the defaults", async () => {
const user = await makeUser();
const session = await makeSession(user);
const body = { weight: ["MANUAL", "APPLE_HEALTH", "WITHINGS"] };
const res = await callRoute<SourcePriority>(PUT, {
method: "PUT",
route: ROUTE,
body,
auth: session,
});
expect(res.status).toBe(200);
// The resolved ladder leads with the chosen order; the resolver appends
// the sources the body did not rank.
expect(res.data.weight.slice(0, 3)).toEqual(body.weight);
expect(await storedLadder(user.id)).toEqual(body);
const read = await callRoute<SourcePriority>(GET, {
method: "GET",
route: ROUTE,
auth: session,
});
expect(read.data.weight).toEqual(res.data.weight);
expect(read.data).toHaveProperty("metricPriority");
});
it("refuses an invalid shape with 422 and stores nothing", async () => {
const user = await makeUser();
const res = await callRoute(PUT, {
method: "PUT",
route: ROUTE,
body: { weight: "not-an-array" },
auth: await makeSession(user),
});
expect(res.status).toBe(422);
expect(res.meta?.errorCode).toBe("source-priority.body.invalid_shape");
expect(await storedLadder(user.id)).toBeNull();
});
it("audits the previous and the new shape", async () => {
const previousShape = { weight: ["WITHINGS", "APPLE_HEALTH", "MANUAL"] };
const user = await makeUser({ sourcePriorityJson: previousShape });
const body = { weight: ["APPLE_HEALTH", "WITHINGS", "MANUAL"] };
const res = await callRoute(PUT, {
method: "PUT",
route: ROUTE,
body,
auth: await makeSession(user),
});
expect(res.status).toBe(200);
const audit = await prisma.auditLog.findMany({
where: { userId: user.id, action: "user.source-priority.update" },
});
expect(audit).toHaveLength(1);
expect(JSON.parse(audit[0]?.details ?? "null")).toEqual({
previous: previousShape,
next: body,
});
});
});