mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-11 05:42:19 +00:00
fix(coach-prefs): return the pure default with no token for a never-saved user
The GET spread the optimistic-concurrency `updatedAt` into the response even when the user had never saved preferences, which broke the strict-equality contract that a never-saved read is exactly `DEFAULT_COACH_PREFS`. Mirror the insights-layout never-saved GET: a null `coachPrefsJson` returns the pure default with no token (the first save is the tokenless unconditional write), and only a saved row and the PUT response carry `updatedAt`. Update the integration round-trip to expect the token on the saved GET and PUT while keeping the `coachPrefsJson` column assertion pure.
This commit is contained in:
1 parent
e65390792d
commit
574367737f
3 files changed
+42
-14
No files matched your search
@@ -64,7 +64,24 @@ beforeEach(() => {
|
||||
});
|
||||
|
||||
describe("GET /api/auth/me/coach-prefs", () => {
|
||||
it("returns the prefs plus the optimistic-concurrency token", async () => {
|
||||
it("returns a SAVED user's prefs plus the optimistic-concurrency token", async () => {
|
||||
vi.mocked(prisma.user.findUnique).mockResolvedValue({
|
||||
coachPrefsJson: { tone: "concise" },
|
||||
updatedAt: new Date("2026-07-24T10:00:00.000Z"),
|
||||
} as never);
|
||||
|
||||
const res = await (GET as () => Promise<Response>)();
|
||||
expect(res.status).toBe(200);
|
||||
const env = (await res.json()) as {
|
||||
data: { tone: string; updatedAt?: string };
|
||||
};
|
||||
expect(env.data.tone).toBe("concise");
|
||||
expect(env.data.updatedAt).toBe("2026-07-24T10:00:00.000Z");
|
||||
});
|
||||
|
||||
it("returns the PURE default with NO token for a never-saved user", async () => {
|
||||
// Mirrors the insights-layout never-saved GET: nothing to guard yet, and
|
||||
// the first save is the tokenless unconditional write (backward-compat).
|
||||
vi.mocked(prisma.user.findUnique).mockResolvedValue({
|
||||
coachPrefsJson: null,
|
||||
updatedAt: new Date("2026-07-24T10:00:00.000Z"),
|
||||
@@ -76,7 +93,7 @@ describe("GET /api/auth/me/coach-prefs", () => {
|
||||
data: { tone: string; updatedAt?: string };
|
||||
};
|
||||
expect(env.data.tone).toBe("warm");
|
||||
expect(env.data.updatedAt).toBe("2026-07-24T10:00:00.000Z");
|
||||
expect(env.data.updatedAt).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -39,12 +39,16 @@ export const GET = apiHandler(async () => {
|
||||
where: { id: user.id },
|
||||
select: { coachPrefsJson: true, updatedAt: true },
|
||||
});
|
||||
return apiSuccess({
|
||||
...parseCoachPrefs(row?.coachPrefsJson),
|
||||
// v1.32.22 (M4) — optimistic-concurrency token (additive sibling key; the
|
||||
// prefs shape is a fixed key set, so `updatedAt` cannot collide).
|
||||
updatedAt: row?.updatedAt?.toISOString(),
|
||||
});
|
||||
const prefs = parseCoachPrefs(row?.coachPrefsJson);
|
||||
// v1.32.22 (M4) — a never-saved user gets the PURE default with NO token
|
||||
// (mirrors the insights-layout never-saved GET). There is nothing to guard
|
||||
// against yet, and the first save is just the tokenless unconditional write.
|
||||
// A saved row carries `updatedAt` (additive sibling key; the prefs shape is a
|
||||
// fixed key set, so it cannot collide) so the client can echo it.
|
||||
if (row?.coachPrefsJson == null) {
|
||||
return apiSuccess(prefs);
|
||||
}
|
||||
return apiSuccess({ ...prefs, updatedAt: row.updatedAt?.toISOString() });
|
||||
});
|
||||
|
||||
export const PUT = apiHandler(async (req: Request) => {
|
||||
|
||||
@@ -104,23 +104,30 @@ describe("PUT /api/auth/me/coach-prefs", () => {
|
||||
}),
|
||||
);
|
||||
expect(putRes.status).toBe(200);
|
||||
const putEnv = (await putRes.json()) as { data: typeof expected };
|
||||
expect(putEnv.data).toEqual(expected);
|
||||
// v1.32.22 (M4) — 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.
|
||||
// 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.
|
||||
// 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 };
|
||||
expect(getEnv.data).toEqual(expected);
|
||||
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 () => {
|
||||
|
||||
Reference in new issue
Block a user