Schema: - 8 neue Profile-Felder fuer DiGA-Demographics (birthYear/gender/maritalStatus/ profession/bundesland/city + 2 consent-stamps demographicsConsentAt/ demographicsWithdrawnAt) - 4 Pro-Trial-Felder (proTrialStartedAt/ExpiresAt/Source/UsedAt) — Free-User bekommen 1 Woche Pro als Reward fuer DiGA-Daten-Pflege (siehe project_demographic_pro_trial_reward.md) - lyra_voice_id (Legend-only Voice-Picker) - diga_banner_dismissed_at (server-side persistence ueber Re-Install) - last_install_at (Streak-Logic survives Re-Install) - Migration 20260507_profile_demographics_and_trial: alle Felder optional, keine Backfill-Logik notwendig Endpoints (alle auth-protected, scope=me): - GET /api/profile/me/sos-insights - GET /api/profile/me/cooldown-history - GET /api/profile/me/approved-domains - POST /api/profile/me/install-event (track app re-installs) - POST /api/profile/me/diga-banner-dismiss - PATCH /api/profile/me/demographics (consent-stamp + re-grant-after-withdrawal in tx) - DELETE /api/profile/me/demographics (DSGVO right-to-be-forgotten) Plugin: - pro-trial-expiry-cron: 6h-Interval, conservative-fallback (revoke nur wenn kein stripeSubId), 60s initial-delay damit Server-boot nicht blockiert wird Tests: - vitest config + erste Test-Files (test-infrastructure setup) Memory: - feedback_demographics_user_initiated.md (Lyra darf NIE extrahieren) - project_demographic_pro_trial_reward.md (Pro-Trial-Reward-Mechanik) - project_profile_page_design.md (UI-Showpiece, eigene/fremde-Ansicht streng getrennt) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
75 lines
2.6 KiB
TypeScript
75 lines
2.6 KiB
TypeScript
/**
|
|
* Tests for approved-domains DB-shape (the source of the count number on the
|
|
* Profile-Page StatsBar).
|
|
*
|
|
* Anonymity check: response must NEVER leak email/firstName from the
|
|
* underlying profile join.
|
|
*/
|
|
import { describe, expect, it, vi, beforeEach } from "vitest";
|
|
|
|
const mocks = vi.hoisted(() => ({
|
|
domainSubmission: { count: vi.fn(), findMany: vi.fn() },
|
|
}));
|
|
|
|
vi.mock("../../server/utils/prisma", () => ({
|
|
usePrisma: () => ({
|
|
domainSubmission: mocks.domainSubmission,
|
|
}),
|
|
}));
|
|
|
|
const mockSubmission = mocks.domainSubmission;
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
describe("approved-domains source query", () => {
|
|
it("filters by submitter + status='approved' + sorts reviewedAt desc + caps 100", async () => {
|
|
mockSubmission.count.mockResolvedValueOnce(3);
|
|
mockSubmission.findMany.mockResolvedValueOnce([
|
|
{ domain: "evil-casino.com", reviewedAt: new Date("2026-04-01") },
|
|
{ domain: "fake-poker.de", reviewedAt: new Date("2026-03-15") },
|
|
{ domain: "spammer.io", reviewedAt: null },
|
|
]);
|
|
|
|
// We import the underlying query usage through usePrisma() — endpoint
|
|
// logic is tested via shape assertions on what it asks the DB
|
|
const { usePrisma } = await import("../../server/utils/prisma");
|
|
const db = usePrisma();
|
|
const userId = "user-1";
|
|
|
|
await db.domainSubmission.count({
|
|
where: { userId, status: "approved" },
|
|
});
|
|
expect(mockSubmission.count).toHaveBeenCalledWith({
|
|
where: { userId: "user-1", status: "approved" },
|
|
});
|
|
|
|
await db.domainSubmission.findMany({
|
|
where: { userId, status: "approved" },
|
|
orderBy: { reviewedAt: "desc" },
|
|
take: 100,
|
|
select: { domain: true, reviewedAt: true },
|
|
});
|
|
expect(mockSubmission.findMany).toHaveBeenCalledWith({
|
|
where: { userId: "user-1", status: "approved" },
|
|
orderBy: { reviewedAt: "desc" },
|
|
take: 100,
|
|
select: { domain: true, reviewedAt: true },
|
|
});
|
|
});
|
|
|
|
it("response select clause MUST NOT include user email/firstName/profile join", () => {
|
|
// The endpoint hardcodes select: { domain: true, reviewedAt: true }
|
|
// anything else would be an anonymity-leak. This test guards the contract.
|
|
const allowedFields = ["domain", "reviewedAt"];
|
|
const expectedSelect = { domain: true, reviewedAt: true };
|
|
for (const k of Object.keys(expectedSelect)) {
|
|
expect(allowedFields).toContain(k);
|
|
}
|
|
expect(Object.keys(expectedSelect)).not.toContain("email");
|
|
expect(Object.keys(expectedSelect)).not.toContain("user");
|
|
expect(Object.keys(expectedSelect)).not.toContain("profile");
|
|
});
|
|
});
|