rebreak-monorepo/backend/tests/profile/demographics.get.test.ts
chahinebrini 0e94ddb68a feat(api): GET /api/profile/me/demographics endpoint
Read-counterpart zum existierenden PATCH/DELETE. Frontend braucht den endpoint
um nach Page-Reload die schon-gespeicherten Werte zu fetchen — sonst sieht User
leere Felder und denkt save funktioniert nicht.

- backend/server/db/profile.ts: getDemographics(userId) — SELECT der 9 fields +
  demographics_consent_at + demographics_withdrawn_at
- backend/server/api/profile/me/demographics.get.ts: requireUser + getDemographics
  + ISO-string conversion. 404 wenn Profile-row fehlt.
- backend/tests/profile/demographics.get.test.ts: 5 vitest cases
  (null fields, 404, populated, withdrawn, 401)

Response shape kompatibel mit PATCH-input (gleiche field names, camelCase) plus
metadata consentAt/withdrawnAt.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 21:40:42 +02:00

166 lines
5.0 KiB
TypeScript

/**
* Tests for GET /api/profile/me/demographics
*
* Covers:
* - returns all-null fields when Profile row has no demographics yet
* - returns 401 when not authenticated
* - returns saved values when row is populated
* - returns withdrawnAt when data was withdrawn
*/
import { describe, expect, it, vi, beforeEach } from "vitest";
const mocks = vi.hoisted(() => ({
profile: {
findUnique: vi.fn(),
},
requireUser: vi.fn(),
}));
vi.mock("../../server/utils/prisma", () => ({
usePrisma: () => ({ profile: mocks.profile }),
}));
vi.mock("../../server/utils/auth", () => ({
requireUser: mocks.requireUser,
}));
import { getDemographics } from "../../server/db/profile";
beforeEach(() => {
vi.clearAllMocks();
});
// ─── getDemographics DB-layer ─────────────────────────────────────────────
describe("getDemographics — all-null row", () => {
it("returns null for all 9 demographic fields + consent fields when not yet filled", async () => {
mocks.profile.findUnique.mockResolvedValueOnce({
birthYear: null,
gender: null,
maritalStatus: null,
employmentStatus: null,
shiftWork: null,
industry: null,
jobTenure: null,
bundesland: null,
city: null,
demographicsConsentAt: null,
demographicsWithdrawnAt: null,
});
const result = await getDemographics("user-1");
expect(result).toEqual({
birthYear: null,
gender: null,
maritalStatus: null,
employmentStatus: null,
shiftWork: null,
industry: null,
jobTenure: null,
bundesland: null,
city: null,
consentAt: null,
withdrawnAt: null,
});
expect(mocks.profile.findUnique).toHaveBeenCalledWith({
where: { id: "user-1" },
select: expect.objectContaining({
birthYear: true,
gender: true,
demographicsConsentAt: true,
demographicsWithdrawnAt: true,
}),
});
});
});
describe("getDemographics — 404 when profile missing", () => {
it("throws 404 when profile row does not exist", async () => {
mocks.profile.findUnique.mockResolvedValueOnce(null);
await expect(getDemographics("ghost-user")).rejects.toMatchObject({
statusCode: 404,
});
});
});
describe("getDemographics — populated row", () => {
it("returns ISO strings for dates and correct field values", async () => {
const consentDate = new Date("2026-04-01T10:00:00Z");
mocks.profile.findUnique.mockResolvedValueOnce({
birthYear: 1989,
gender: "male",
maritalStatus: "single",
employmentStatus: "employed",
shiftWork: false,
industry: "IT",
jobTenure: "3_5y",
bundesland: "DE-BY",
city: "München",
demographicsConsentAt: consentDate,
demographicsWithdrawnAt: null,
});
const result = await getDemographics("user-2");
expect(result.birthYear).toBe(1989);
expect(result.gender).toBe("male");
expect(result.consentAt).toBe("2026-04-01T10:00:00.000Z");
expect(result.withdrawnAt).toBeNull();
});
});
describe("getDemographics — withdrawn row", () => {
it("returns withdrawnAt as ISO string when data was withdrawn", async () => {
const consentDate = new Date("2026-03-01T00:00:00Z");
const withdrawnDate = new Date("2026-04-15T12:00:00Z");
mocks.profile.findUnique.mockResolvedValueOnce({
birthYear: null,
gender: null,
maritalStatus: null,
employmentStatus: null,
shiftWork: null,
industry: null,
jobTenure: null,
bundesland: null,
city: null,
demographicsConsentAt: consentDate,
demographicsWithdrawnAt: withdrawnDate,
});
const result = await getDemographics("user-3");
expect(result.consentAt).toBe("2026-03-01T00:00:00.000Z");
expect(result.withdrawnAt).toBe("2026-04-15T12:00:00.000Z");
// all data fields nulled after withdrawal
expect(result.birthYear).toBeNull();
expect(result.city).toBeNull();
});
});
// ─── 401 guard (endpoint-level) ───────────────────────────────────────────
// We test the auth guard by importing the handler and simulating a
// requireUser rejection — no real Nitro boot needed.
describe("demographics.get endpoint — 401 when not authenticated", () => {
it("propagates the 401 error from requireUser", async () => {
const authError = Object.assign(new Error("Unauthorized"), {
statusCode: 401,
});
mocks.requireUser.mockRejectedValueOnce(authError);
// Import handler (defineEventHandler stub in setup.ts returns the fn as-is)
const mod = await import(
"../../server/api/profile/me/demographics.get"
);
const handler =
typeof mod.default === "function" ? mod.default : (mod.default as { handler?: unknown }).handler;
await expect(
(handler as (e: unknown) => Promise<unknown>)({ body: null }),
).rejects.toMatchObject({ statusCode: 401 });
});
});