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>
166 lines
5.0 KiB
TypeScript
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 });
|
|
});
|
|
});
|