mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 13:01:00 +00:00
ensureDbCompatibility ran ALTER TABLE / CREATE TABLE IF NOT EXISTS on the login, register, token and session paths, mirroring columns every migration under prisma/migrations already creates. The entrypoint applies those migrations before the server listens and stops the boot when one fails, so the mirror never had work to do, and DDL on a request path needs the table's owner: a connection that may only read and write rows fails every login on it. Drop the module, its call sites and the mocks that stubbed it out.
143 lines
5.0 KiB
TypeScript
143 lines
5.0 KiB
TypeScript
/**
|
|
* v1.4.23 — per-user Coach prompt-tuning preferences route.
|
|
*
|
|
* Round-trips through GET → PUT → GET and asserts that:
|
|
* - the GET returns the documented defaults when the row is null
|
|
* - the PUT persists the supplied shape and returns the canonical
|
|
* defaulted form
|
|
* - subsequent GETs reflect the persisted values
|
|
* - malformed input is rejected with 422
|
|
*/
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
|
process.env.ENCRYPTION_KEY ??=
|
|
"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
|
|
|
|
import { DEFAULT_COACH_PREFS } from "@/lib/validations/coach-prefs";
|
|
|
|
import { cookieJar, headerJar } from "./mock-next-headers";
|
|
import { getPrismaClient, truncateAllTables } from "./setup";
|
|
|
|
vi.mock("next/headers", async () => {
|
|
const { cookieJar, headerJar } = await import("./mock-next-headers");
|
|
return {
|
|
headers: vi.fn(async () => ({
|
|
get: (name: string) => headerJar.get(name.toLowerCase()) ?? null,
|
|
})),
|
|
cookies: vi.fn(async () => ({
|
|
get: (name: string) => {
|
|
const value = cookieJar.get(name);
|
|
return value ? { name, value } : undefined;
|
|
},
|
|
set: (name: string, value: string) => {
|
|
cookieJar.set(name, value);
|
|
},
|
|
delete: (name: string) => {
|
|
cookieJar.delete(name);
|
|
},
|
|
})),
|
|
};
|
|
});
|
|
|
|
beforeEach(async () => {
|
|
await truncateAllTables(getPrismaClient());
|
|
cookieJar.clear();
|
|
headerJar.clear();
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
async function seedSession(username: string) {
|
|
const prisma = getPrismaClient();
|
|
const user = await prisma.user.create({
|
|
data: { username, email: `${username}@example.test` },
|
|
});
|
|
const session = await prisma.session.create({
|
|
data: { userId: user.id, expiresAt: new Date(Date.now() + 60_000) },
|
|
});
|
|
cookieJar.set("healthlog_session", session.id);
|
|
return user;
|
|
}
|
|
|
|
describe("GET /api/auth/me/coach-prefs", () => {
|
|
it("returns documented defaults when the user has never saved prefs", async () => {
|
|
await seedSession("coach-prefs-defaults");
|
|
|
|
const { GET } = await import("@/app/api/auth/me/coach-prefs/route");
|
|
const res = await (GET as (r: Request) => Promise<Response>)(
|
|
new Request("http://localhost/api/auth/me/coach-prefs"),
|
|
);
|
|
expect(res.status).toBe(200);
|
|
const env = (await res.json()) as { data: typeof DEFAULT_COACH_PREFS };
|
|
expect(env.data).toEqual(DEFAULT_COACH_PREFS);
|
|
});
|
|
});
|
|
|
|
describe("PUT /api/auth/me/coach-prefs", () => {
|
|
it("persists the supplied shape and round-trips through GET", async () => {
|
|
const user = await seedSession("coach-prefs-roundtrip");
|
|
const prisma = getPrismaClient();
|
|
|
|
const { PUT, GET } = await import("@/app/api/auth/me/coach-prefs/route");
|
|
// The request body omits `defaultWindow`; the schema (v1.4.25 W5)
|
|
// defaults it to "allTime", so the canonical PUT response and the
|
|
// persisted row both carry the defaulted field even when the
|
|
// caller didn't pass it.
|
|
const body = {
|
|
tone: "concise" as const,
|
|
verbosity: "brief" as const,
|
|
excludeMetrics: ["mood" as const, "compliance" as const],
|
|
showEvidenceByDefault: true,
|
|
};
|
|
const expected = { ...body, defaultWindow: "allTime" as const };
|
|
const putRes = await (PUT as (r: Request) => Promise<Response>)(
|
|
new Request("http://localhost/api/auth/me/coach-prefs", {
|
|
method: "PUT",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(body),
|
|
}),
|
|
);
|
|
expect(putRes.status).toBe(200);
|
|
// v1.32.22 — the write echoes the fresh optimistic-concurrency
|
|
// `updatedAt` token alongside the canonical prefs.
|
|
const putEnv = (await putRes.json()) as {
|
|
data: typeof expected & { updatedAt?: string };
|
|
};
|
|
expect(putEnv.data).toEqual({ ...expected, updatedAt: expect.any(String) });
|
|
|
|
// DB row reflects the canonical defaulted form. `coachPrefsJson` must NOT
|
|
// carry `updatedAt` — that token is the separate `User.updatedAt` column.
|
|
const row = await prisma.user.findUnique({
|
|
where: { id: user.id },
|
|
select: { coachPrefsJson: true },
|
|
});
|
|
expect(row?.coachPrefsJson).toEqual(expected);
|
|
|
|
// GET reads the saved values back, now carrying the token (saved row).
|
|
const getRes = await (GET as (r: Request) => Promise<Response>)(
|
|
new Request("http://localhost/api/auth/me/coach-prefs"),
|
|
);
|
|
expect(getRes.status).toBe(200);
|
|
const getEnv = (await getRes.json()) as {
|
|
data: typeof expected & { updatedAt?: string };
|
|
};
|
|
expect(getEnv.data).toEqual({ ...expected, updatedAt: expect.any(String) });
|
|
});
|
|
|
|
it("rejects unknown tone values with 422", async () => {
|
|
await seedSession("coach-prefs-invalid");
|
|
|
|
const { PUT } = await import("@/app/api/auth/me/coach-prefs/route");
|
|
const res = await (PUT as (r: Request) => Promise<Response>)(
|
|
new Request("http://localhost/api/auth/me/coach-prefs", {
|
|
method: "PUT",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ tone: "stoic" }),
|
|
}),
|
|
);
|
|
expect(res.status).toBe(422);
|
|
});
|
|
});
|