test(soft-delete): pin gamification/achievements and doctor-report/availability

The W-DELETED-2 integration-test file header promises coverage for five
reader tiers; v1.4.41 shipped pin assertions for three (`/api/export`,
`/api/export/full-backup`, `/api/export/measurements`) but never asserted
the two routes that the W-DELETED-2 fix actually touched:

- `/api/gamification/achievements` — reads recent measurements via
  `findMany` to compute streak / badge progress. A tombstoned row must
  never count toward a milestone.
- `/api/doctor-report/availability` — four parallel `measurement.count`
  probes drive the section-availability switch. A soft-deleted history
  must not light up a section the user has since wiped.

Adds one `it()` block per route asserting that every measurement
read scopes to `deletedAt: null`. Mocks already in place; no
test-infra work needed.
This commit is contained in:
Marc-André Bombeck committed 2026-05-21 18:15:50 +02:00
1 parent 92753a7096
commit 9d8e0d7a07
1 file changed
+47
@@ -37,6 +37,9 @@ vi.mock("@/lib/db", () => ({
vi.mock("@/lib/auth/session", () => ({ getSession: vi.fn() }));
vi.mock("@/lib/rate-limit", () => ({ checkRateLimit: vi.fn() }));
vi.mock("@/lib/auth", () => ({
requireAuth: vi.fn().mockResolvedValue({ user: { id: "user-1" } }),
}));
vi.mock("@/lib/auth/audit", () => ({
auditLog: vi.fn().mockResolvedValue(undefined),
}));
@@ -114,4 +117,48 @@ describe("v1.4.41 W-DELETED-2 — soft-delete invisibility", () => {
}),
);
});
it("/api/gamification/achievements scopes the measurement read to deletedAt: null", async () => {
vi.mocked(prisma.userAchievement.findMany).mockResolvedValue([] as never);
const { GET } = await import(
"../../gamification/achievements/route"
);
await GET(mkReq("http://localhost/api/gamification/achievements"));
const calls = vi.mocked(prisma.measurement.findMany).mock.calls;
// The achievement aggregator reads recent measurements via
// findMany — a tombstoned row must never count toward a streak
// or PR badge.
expect(calls.length).toBeGreaterThan(0);
for (const [arg] of calls) {
expect(arg).toMatchObject({
where: expect.objectContaining({ deletedAt: null }),
});
}
});
it("/api/doctor-report/availability scopes every measurement count to deletedAt: null", async () => {
const { POST } = await import(
"../../doctor-report/availability/route"
);
const req = new NextRequest(
"http://localhost/api/doctor-report/availability",
{
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({}),
},
);
await POST(req);
const calls = vi.mocked(prisma.measurement.count).mock.calls;
// The probe runs four parallel measurement.count queries (BP /
// weight / pulse / sleep). All must filter tombstoned rows so a
// soft-deleted history does not light up a section the user has
// since wiped.
expect(calls.length).toBeGreaterThanOrEqual(4);
for (const [arg] of calls) {
expect(arg).toMatchObject({
where: expect.objectContaining({ deletedAt: null }),
});
}
});
});