Files
HealthLog/tests/integration/profile-display-name.test.ts
T
Marc-André Bombeck 1e606834d8 fix(settings): let the display name change after the setup flow
The setup flow asks for the name the greeting and the reports use, and
the profile route has always accepted it, but no settings surface offered
the field and /api/auth/me did not publish it. A name chosen on the
first day stayed for good.

The account payload now carries `displayName`, the profile card shows the
field beside the username and saves it with the rest of the profile, and
an emptied field clears the name.
2026-10-21 12:36:15 +02:00

111 lines
3.4 KiB
TypeScript

/**
* The display name round-trips through the profile save and the account
* payload, against a real account row.
*
* The profile card seeds its display-name field from `GET /api/auth/me` and
* saves it through `PUT /api/auth/profile`. Until now `/me` did not publish
* the name at all, so the two ends could not meet; this pins the pipe: a save
* lands in the column and comes back on the payload, and an emptied field
* clears it.
*/
import { beforeEach, describe, expect, it, vi } from "vitest";
import { NextRequest } from "next/server";
import { cookieJar, headerJar } from "./mock-next-headers";
import { getPrismaClient, truncateAllTables } from "./setup";
process.env.ENCRYPTION_KEY ??=
"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
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);
},
})),
};
});
vi.mock("@/lib/db-compat", () => ({
ensureDbCompatibility: vi.fn().mockResolvedValue(undefined),
}));
async function seed(displayName: string | null): Promise<string> {
const prisma = getPrismaClient();
const user = await prisma.user.create({
data: {
username: "display-name",
email: "display-name@example.test",
displayName,
},
});
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.id;
}
async function save(body: Record<string, unknown>): Promise<Response> {
const { PUT } = await import("@/app/api/auth/profile/route");
return PUT(
new NextRequest("http://localhost/api/auth/profile", {
method: "PUT",
headers: { "content-type": "application/json" },
body: JSON.stringify(body),
}),
);
}
async function me(): Promise<{ displayName?: string | null }> {
const { GET } = await import("@/app/api/auth/me/route");
const res = await GET();
expect(res.status).toBe(200);
return ((await res.json()) as { data: { displayName?: string | null } }).data;
}
beforeEach(async () => {
await truncateAllTables(getPrismaClient());
cookieJar.clear();
headerJar.clear();
});
describe("display name: profile save and account payload", () => {
it("publishes the stored name on /api/auth/me", async () => {
await seed("Sam");
expect((await me()).displayName).toBe("Sam");
});
it("a save lands in the column and comes back on the payload", async () => {
const userId = await seed("Sam");
const res = await save({ displayName: " Alex " });
expect(res.status).toBe(200);
const row = await getPrismaClient().user.findUniqueOrThrow({
where: { id: userId },
select: { displayName: true },
});
expect(row.displayName).toBe("Alex");
expect((await me()).displayName).toBe("Alex");
});
it("an emptied field clears the name", async () => {
await seed("Sam");
expect((await save({ displayName: null })).status).toBe(200);
expect((await me()).displayName).toBeNull();
});
});