mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 21:32:18 +00:00
style: apply prettier across the tree
One-shot prettier pass so the new format:check gate starts from a clean baseline. Formatting only — no behaviour change. Generator-owned and vendored artifacts stay untouched via .prettierignore.
This commit is contained in:
1 parent
b3fea2ef59
commit
1f4ba5939d
1000 files changed
+12130
-8385
No files matched your search
@@ -87,9 +87,8 @@ function makeRequest(body: unknown): NextRequest {
|
||||
describe("POST /api/admin/drain-per-sample-cumulative (real Postgres)", () => {
|
||||
it("returns 403 when the session belongs to a standard (non-admin) user", async () => {
|
||||
await setSessionFor(STANDARD_USER_ID);
|
||||
const { POST } = await import(
|
||||
"@/app/api/admin/drain-per-sample-cumulative/route"
|
||||
);
|
||||
const { POST } =
|
||||
await import("@/app/api/admin/drain-per-sample-cumulative/route");
|
||||
const res = await POST(makeRequest({ dryRun: true }));
|
||||
expect(res.status).toBe(403);
|
||||
});
|
||||
@@ -120,9 +119,8 @@ describe("POST /api/admin/drain-per-sample-cumulative (real Postgres)", () => {
|
||||
});
|
||||
|
||||
await setSessionFor(ADMIN_USER_ID);
|
||||
const { POST } = await import(
|
||||
"@/app/api/admin/drain-per-sample-cumulative/route"
|
||||
);
|
||||
const { POST } =
|
||||
await import("@/app/api/admin/drain-per-sample-cumulative/route");
|
||||
// No body = dryRun defaults to true.
|
||||
const res = await POST(makeRequest({ userId: STANDARD_USER_ID }));
|
||||
expect(res.status).toBe(200);
|
||||
@@ -168,9 +166,8 @@ describe("POST /api/admin/drain-per-sample-cumulative (real Postgres)", () => {
|
||||
});
|
||||
|
||||
await setSessionFor(ADMIN_USER_ID);
|
||||
const { POST } = await import(
|
||||
"@/app/api/admin/drain-per-sample-cumulative/route"
|
||||
);
|
||||
const { POST } =
|
||||
await import("@/app/api/admin/drain-per-sample-cumulative/route");
|
||||
const res = await POST(
|
||||
makeRequest({ userId: STANDARD_USER_ID, dryRun: false }),
|
||||
);
|
||||
|
||||
@@ -108,9 +108,8 @@ describe("POST /api/admin/users/[id]/reset-password (real Postgres)", () => {
|
||||
it("returns 401 when the caller is unauthenticated", async () => {
|
||||
const targetId = await seedTargetUser();
|
||||
|
||||
const { POST } = await import(
|
||||
"@/app/api/admin/users/[id]/reset-password/route"
|
||||
);
|
||||
const { POST } =
|
||||
await import("@/app/api/admin/users/[id]/reset-password/route");
|
||||
const res = await POST(
|
||||
makeRequest(targetId, { password: STRONG_NEW_PW }),
|
||||
paramsFor(targetId),
|
||||
@@ -131,9 +130,8 @@ describe("POST /api/admin/users/[id]/reset-password (real Postgres)", () => {
|
||||
},
|
||||
});
|
||||
|
||||
const { POST } = await import(
|
||||
"@/app/api/admin/users/[id]/reset-password/route"
|
||||
);
|
||||
const { POST } =
|
||||
await import("@/app/api/admin/users/[id]/reset-password/route");
|
||||
const res = await POST(
|
||||
makeRequest(targetId, { password: STRONG_NEW_PW }),
|
||||
paramsFor(targetId),
|
||||
@@ -151,9 +149,8 @@ describe("POST /api/admin/users/[id]/reset-password (real Postgres)", () => {
|
||||
it("returns 404 when the target user does not exist", async () => {
|
||||
await seedCaller("ADMIN");
|
||||
|
||||
const { POST } = await import(
|
||||
"@/app/api/admin/users/[id]/reset-password/route"
|
||||
);
|
||||
const { POST } =
|
||||
await import("@/app/api/admin/users/[id]/reset-password/route");
|
||||
const res = await POST(
|
||||
makeRequest("user-does-not-exist", { password: STRONG_NEW_PW }),
|
||||
paramsFor("user-does-not-exist"),
|
||||
@@ -169,9 +166,8 @@ describe("POST /api/admin/users/[id]/reset-password (real Postgres)", () => {
|
||||
|
||||
const before = await prisma.user.findUnique({ where: { id: targetId } });
|
||||
|
||||
const { POST } = await import(
|
||||
"@/app/api/admin/users/[id]/reset-password/route"
|
||||
);
|
||||
const { POST } =
|
||||
await import("@/app/api/admin/users/[id]/reset-password/route");
|
||||
const res = await POST(
|
||||
makeRequest(targetId, { password: "password1234" }),
|
||||
paramsFor(targetId),
|
||||
@@ -211,9 +207,8 @@ describe("POST /api/admin/users/[id]/reset-password (real Postgres)", () => {
|
||||
},
|
||||
});
|
||||
|
||||
const { POST } = await import(
|
||||
"@/app/api/admin/users/[id]/reset-password/route"
|
||||
);
|
||||
const { POST } =
|
||||
await import("@/app/api/admin/users/[id]/reset-password/route");
|
||||
const res = await POST(
|
||||
makeRequest(targetId, { password: STRONG_NEW_PW }),
|
||||
paramsFor(targetId),
|
||||
|
||||
@@ -132,9 +132,7 @@ describe("GET /api/analytics — chunked per-type reads (Sr-H1)", () => {
|
||||
// i = 0 is the oldest. The chunked helper reads
|
||||
// `(measuredAt asc, id asc)` so the latest value is the last
|
||||
// entry in the sorted stream — which the test asserts below.
|
||||
measuredAt: new Date(
|
||||
youngestRowAtMs - (ROW_COUNT - 1 - i) * STEP_MS,
|
||||
),
|
||||
measuredAt: new Date(youngestRowAtMs - (ROW_COUNT - 1 - i) * STEP_MS),
|
||||
}));
|
||||
// createMany batches efficiently; default Postgres parameter limit
|
||||
// accommodates this volume in a single statement.
|
||||
|
||||
@@ -266,9 +266,9 @@ describe("POST /api/auth/password (real Postgres)", () => {
|
||||
// Hit a session-protected endpoint — `apiHandler` raises HttpError(401)
|
||||
// which the wrapper converts to a 401 JSON response.
|
||||
const { GET } = await import("@/app/api/auth/me/route");
|
||||
const meRes = await (
|
||||
GET as unknown as (req: Request) => Promise<Response>
|
||||
)(new Request("http://localhost/api/auth/me"));
|
||||
const meRes = await (GET as unknown as (req: Request) => Promise<Response>)(
|
||||
new Request("http://localhost/api/auth/me"),
|
||||
);
|
||||
expect(meRes.status).toBe(401);
|
||||
});
|
||||
});
|
||||
@@ -84,12 +84,33 @@ describe("POST /api/measurements/bulk-delete (real Postgres)", () => {
|
||||
const now = new Date();
|
||||
const mine = await prisma.measurement.createManyAndReturn({
|
||||
data: [
|
||||
{ userId: USER_A, type: "WEIGHT", value: 80, unit: "kg", source: "MANUAL", measuredAt: now },
|
||||
{ userId: USER_A, type: "WEIGHT", value: 81, unit: "kg", source: "MANUAL", measuredAt: new Date(now.getTime() - 1000) },
|
||||
{
|
||||
userId: USER_A,
|
||||
type: "WEIGHT",
|
||||
value: 80,
|
||||
unit: "kg",
|
||||
source: "MANUAL",
|
||||
measuredAt: now,
|
||||
},
|
||||
{
|
||||
userId: USER_A,
|
||||
type: "WEIGHT",
|
||||
value: 81,
|
||||
unit: "kg",
|
||||
source: "MANUAL",
|
||||
measuredAt: new Date(now.getTime() - 1000),
|
||||
},
|
||||
],
|
||||
});
|
||||
const theirs = await prisma.measurement.create({
|
||||
data: { userId: USER_B, type: "WEIGHT", value: 70, unit: "kg", source: "MANUAL", measuredAt: now },
|
||||
data: {
|
||||
userId: USER_B,
|
||||
type: "WEIGHT",
|
||||
value: 70,
|
||||
unit: "kg",
|
||||
source: "MANUAL",
|
||||
measuredAt: now,
|
||||
},
|
||||
});
|
||||
|
||||
await loginAs(USER_A);
|
||||
@@ -123,7 +144,14 @@ describe("POST /api/measurements/bulk-delete (real Postgres)", () => {
|
||||
it("idempotency replay returns the cached count without re-mutating", async () => {
|
||||
const prisma = getPrismaClient();
|
||||
const row = await prisma.measurement.create({
|
||||
data: { userId: USER_A, type: "WEIGHT", value: 80, unit: "kg", source: "MANUAL", measuredAt: new Date() },
|
||||
data: {
|
||||
userId: USER_A,
|
||||
type: "WEIGHT",
|
||||
value: 80,
|
||||
unit: "kg",
|
||||
source: "MANUAL",
|
||||
measuredAt: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
await loginAs(USER_A);
|
||||
@@ -134,7 +162,9 @@ describe("POST /api/measurements/bulk-delete (real Postgres)", () => {
|
||||
postReq("/api/measurements/bulk-delete", { ids: [row.id] }, key),
|
||||
);
|
||||
expect(first.status).toBe(200);
|
||||
expect(((await first.json()) as { data: { deleted: number } }).data.deleted).toBe(1);
|
||||
expect(
|
||||
((await first.json()) as { data: { deleted: number } }).data.deleted,
|
||||
).toBe(1);
|
||||
|
||||
// Replay with the SAME key → cached body, same count, no re-mutation.
|
||||
const replay = await POST(
|
||||
@@ -142,11 +172,15 @@ describe("POST /api/measurements/bulk-delete (real Postgres)", () => {
|
||||
);
|
||||
expect(replay.status).toBe(200);
|
||||
expect(replay.headers.get("X-Idempotent-Replay")).toBe("true");
|
||||
expect(((await replay.json()) as { data: { deleted: number } }).data.deleted).toBe(1);
|
||||
expect(
|
||||
((await replay.json()) as { data: { deleted: number } }).data.deleted,
|
||||
).toBe(1);
|
||||
|
||||
// Measurement.syncVersion defaults to 1; the single real delete bumped
|
||||
// it to 2 and the replay did NOT re-run the mutation (so it stays 2).
|
||||
const after = await prisma.measurement.findUnique({ where: { id: row.id } });
|
||||
const after = await prisma.measurement.findUnique({
|
||||
where: { id: row.id },
|
||||
});
|
||||
expect(after!.syncVersion).toBe(2);
|
||||
});
|
||||
|
||||
@@ -164,12 +198,36 @@ describe("POST /api/mood-entries/bulk-delete (real Postgres)", () => {
|
||||
const prisma = getPrismaClient();
|
||||
const mine = await prisma.moodEntry.createManyAndReturn({
|
||||
data: [
|
||||
{ userId: USER_A, date: "2026-01-01", mood: "GUT", score: 4, source: "MANUAL", moodLoggedAt: new Date("2026-01-01T08:00:00Z"), tz: "UTC" },
|
||||
{ userId: USER_A, date: "2026-01-02", mood: "OKAY", score: 3, source: "MANUAL", moodLoggedAt: new Date("2026-01-02T08:00:00Z"), tz: "UTC" },
|
||||
{
|
||||
userId: USER_A,
|
||||
date: "2026-01-01",
|
||||
mood: "GUT",
|
||||
score: 4,
|
||||
source: "MANUAL",
|
||||
moodLoggedAt: new Date("2026-01-01T08:00:00Z"),
|
||||
tz: "UTC",
|
||||
},
|
||||
{
|
||||
userId: USER_A,
|
||||
date: "2026-01-02",
|
||||
mood: "OKAY",
|
||||
score: 3,
|
||||
source: "MANUAL",
|
||||
moodLoggedAt: new Date("2026-01-02T08:00:00Z"),
|
||||
tz: "UTC",
|
||||
},
|
||||
],
|
||||
});
|
||||
const theirs = await prisma.moodEntry.create({
|
||||
data: { userId: USER_B, date: "2026-01-01", mood: "LAUSIG", score: 1, source: "MANUAL", moodLoggedAt: new Date("2026-01-01T09:00:00Z"), tz: "UTC" },
|
||||
data: {
|
||||
userId: USER_B,
|
||||
date: "2026-01-01",
|
||||
mood: "LAUSIG",
|
||||
score: 1,
|
||||
source: "MANUAL",
|
||||
moodLoggedAt: new Date("2026-01-01T09:00:00Z"),
|
||||
tz: "UTC",
|
||||
},
|
||||
});
|
||||
|
||||
await loginAs(USER_A);
|
||||
@@ -180,7 +238,9 @@ describe("POST /api/mood-entries/bulk-delete (real Postgres)", () => {
|
||||
}),
|
||||
);
|
||||
expect(res.status).toBe(200);
|
||||
expect(((await res.json()) as { data: { deleted: number } }).data.deleted).toBe(2);
|
||||
expect(
|
||||
((await res.json()) as { data: { deleted: number } }).data.deleted,
|
||||
).toBe(2);
|
||||
|
||||
const after = await prisma.moodEntry.findMany({
|
||||
where: { id: { in: [mine[0].id, mine[1].id] } },
|
||||
@@ -202,9 +262,30 @@ describe("source filter narrows the list reads (real Postgres)", () => {
|
||||
const now = new Date();
|
||||
await prisma.measurement.createMany({
|
||||
data: [
|
||||
{ userId: USER_A, type: "WEIGHT", value: 80, unit: "kg", source: "MANUAL", measuredAt: now },
|
||||
{ userId: USER_A, type: "WEIGHT", value: 81, unit: "kg", source: "WITHINGS", measuredAt: new Date(now.getTime() - 1000) },
|
||||
{ userId: USER_A, type: "WEIGHT", value: 82, unit: "kg", source: "WITHINGS", measuredAt: new Date(now.getTime() - 2000) },
|
||||
{
|
||||
userId: USER_A,
|
||||
type: "WEIGHT",
|
||||
value: 80,
|
||||
unit: "kg",
|
||||
source: "MANUAL",
|
||||
measuredAt: now,
|
||||
},
|
||||
{
|
||||
userId: USER_A,
|
||||
type: "WEIGHT",
|
||||
value: 81,
|
||||
unit: "kg",
|
||||
source: "WITHINGS",
|
||||
measuredAt: new Date(now.getTime() - 1000),
|
||||
},
|
||||
{
|
||||
userId: USER_A,
|
||||
type: "WEIGHT",
|
||||
value: 82,
|
||||
unit: "kg",
|
||||
source: "WITHINGS",
|
||||
measuredAt: new Date(now.getTime() - 2000),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
@@ -221,7 +302,9 @@ describe("source filter narrows the list reads (real Postgres)", () => {
|
||||
};
|
||||
};
|
||||
expect(body.data.measurements).toHaveLength(2);
|
||||
expect(body.data.measurements.every((m) => m.source === "WITHINGS")).toBe(true);
|
||||
expect(body.data.measurements.every((m) => m.source === "WITHINGS")).toBe(
|
||||
true,
|
||||
);
|
||||
expect(body.data.meta.total).toBe(2);
|
||||
});
|
||||
|
||||
@@ -229,8 +312,24 @@ describe("source filter narrows the list reads (real Postgres)", () => {
|
||||
const prisma = getPrismaClient();
|
||||
await prisma.moodEntry.createMany({
|
||||
data: [
|
||||
{ userId: USER_A, date: "2026-01-01", mood: "GUT", score: 4, source: "MANUAL", moodLoggedAt: new Date("2026-01-01T08:00:00Z"), tz: "UTC" },
|
||||
{ userId: USER_A, date: "2026-01-02", mood: "OKAY", score: 3, source: "TELEGRAM", moodLoggedAt: new Date("2026-01-02T08:00:00Z"), tz: "UTC" },
|
||||
{
|
||||
userId: USER_A,
|
||||
date: "2026-01-01",
|
||||
mood: "GUT",
|
||||
score: 4,
|
||||
source: "MANUAL",
|
||||
moodLoggedAt: new Date("2026-01-01T08:00:00Z"),
|
||||
tz: "UTC",
|
||||
},
|
||||
{
|
||||
userId: USER_A,
|
||||
date: "2026-01-02",
|
||||
mood: "OKAY",
|
||||
score: 3,
|
||||
source: "TELEGRAM",
|
||||
moodLoggedAt: new Date("2026-01-02T08:00:00Z"),
|
||||
tz: "UTC",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
|
||||
@@ -116,9 +116,7 @@ beforeEach(async () => {
|
||||
await truncateAllTables(getPrismaClient());
|
||||
cookieJar.clear();
|
||||
headerJar.clear();
|
||||
const { __resetAllCachesForTests } = await import(
|
||||
"@/lib/cache/server-cache"
|
||||
);
|
||||
const { __resetAllCachesForTests } = await import("@/lib/cache/server-cache");
|
||||
__resetAllCachesForTests();
|
||||
// Run the real db-compat helper directly so a sibling test file's
|
||||
// `vi.mock("@/lib/db-compat", ...)` cannot leave the `app_settings`
|
||||
|
||||
@@ -15,7 +15,11 @@ import { getPrismaClient, truncateAllTables } from "./setup";
|
||||
|
||||
async function seedUser(): Promise<string> {
|
||||
const user = await getPrismaClient().user.create({
|
||||
data: { username: "budget-user", email: "budget@example.test", role: "USER" },
|
||||
data: {
|
||||
username: "budget-user",
|
||||
email: "budget@example.test",
|
||||
role: "USER",
|
||||
},
|
||||
});
|
||||
return user.id;
|
||||
}
|
||||
@@ -72,9 +76,8 @@ describe("reserveBudget (real Postgres)", () => {
|
||||
});
|
||||
|
||||
it("reconciles the reservation down to a smaller actual count", async () => {
|
||||
const { reserveBudget, reconcileSpend } = await import(
|
||||
"@/lib/ai/coach/budget"
|
||||
);
|
||||
const { reserveBudget, reconcileSpend } =
|
||||
await import("@/lib/ai/coach/budget");
|
||||
const userId = await seedUser();
|
||||
const dateKey = "2026-06-21";
|
||||
|
||||
@@ -89,9 +92,8 @@ describe("reserveBudget (real Postgres)", () => {
|
||||
});
|
||||
|
||||
it("still records burned tokens on an empty/sentinel reply (no undercount)", async () => {
|
||||
const { reserveBudget, reconcileSpend } = await import(
|
||||
"@/lib/ai/coach/budget"
|
||||
);
|
||||
const { reserveBudget, reconcileSpend } =
|
||||
await import("@/lib/ai/coach/budget");
|
||||
const userId = await seedUser();
|
||||
const dateKey = "2026-06-22";
|
||||
|
||||
|
||||
@@ -94,9 +94,9 @@ describe("consolidateLegacySteps (real Postgres)", () => {
|
||||
},
|
||||
});
|
||||
expect(tombstoned).toHaveLength(3);
|
||||
expect(tombstoned.every((r) => r.externalId?.startsWith("hk-legacy-"))).toBe(
|
||||
true,
|
||||
);
|
||||
expect(
|
||||
tombstoned.every((r) => r.externalId?.startsWith("hk-legacy-")),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("does not double-count when a stats: daily total already exists for the day", async () => {
|
||||
|
||||
@@ -129,7 +129,9 @@ describe("cycle import — reproductive HK samples route to CycleDayLog", () =>
|
||||
|
||||
expect(second.cycle.daysUpserted).toBe(1);
|
||||
expect(second.cycle.daysInserted).toBe(0); // already existed
|
||||
const count = await prisma.cycleDayLog.count({ where: { userId: USER_ID } });
|
||||
const count = await prisma.cycleDayLog.count({
|
||||
where: { userId: USER_ID },
|
||||
});
|
||||
expect(count).toBe(1);
|
||||
});
|
||||
|
||||
@@ -152,7 +154,9 @@ describe("cycle import — reproductive HK samples route to CycleDayLog", () =>
|
||||
|
||||
// Sample was bucketed but the gate suppressed the flush.
|
||||
expect(result.cycle.daysUpserted).toBe(0);
|
||||
const count = await prisma.cycleDayLog.count({ where: { userId: USER_ID } });
|
||||
const count = await prisma.cycleDayLog.count({
|
||||
where: { userId: USER_ID },
|
||||
});
|
||||
expect(count).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -405,7 +405,11 @@ describe("cycle period shortcut", () => {
|
||||
expect(replay.status).toBe(200);
|
||||
|
||||
const cycles = await prisma.menstrualCycle.count({
|
||||
where: { userId: FEMALE_USER_ID, startDate: "2026-05-10", deletedAt: null },
|
||||
where: {
|
||||
userId: FEMALE_USER_ID,
|
||||
startDate: "2026-05-10",
|
||||
deletedAt: null,
|
||||
},
|
||||
});
|
||||
expect(cycles).toBe(1);
|
||||
});
|
||||
@@ -515,9 +519,7 @@ describe("cycle-prefs gate flip", () => {
|
||||
|
||||
it("defaults secondarySymptom to MUCUS and persists a switch to CERVIX", async () => {
|
||||
await loginAs(FEMALE_USER_ID);
|
||||
const { GET, PATCH } = await import(
|
||||
"@/app/api/auth/me/cycle-prefs/route"
|
||||
);
|
||||
const { GET, PATCH } = await import("@/app/api/auth/me/cycle-prefs/route");
|
||||
|
||||
const initial = await GET();
|
||||
expect((await initial.json()).data.secondarySymptom).toBe("MUCUS");
|
||||
|
||||
@@ -259,7 +259,9 @@ describe("PUT /api/dashboard/widgets — Save persists full layouts", () => {
|
||||
const body = (await res.json()) as {
|
||||
data: null;
|
||||
error: string;
|
||||
details: { issues: Array<{ path: string; code: string; message: string }> };
|
||||
details: {
|
||||
issues: Array<{ path: string; code: string; message: string }>;
|
||||
};
|
||||
};
|
||||
expect(body.data).toBeNull();
|
||||
expect(body.error).toBe("Validation failed");
|
||||
|
||||
@@ -22,7 +22,8 @@ const TZ = "Europe/Berlin";
|
||||
const DAY_KEY = "2026-05-01";
|
||||
// The canonical local-noon instant the fold writes for the seeded day.
|
||||
const CANONICAL = canonicalDailyTimestamp(DAY_KEY, TZ);
|
||||
const HRV_STATS_ID = "stats:HKQuantityTypeIdentifierHeartRateVariabilitySDNN:2026-05-01";
|
||||
const HRV_STATS_ID =
|
||||
"stats:HKQuantityTypeIdentifierHeartRateVariabilitySDNN:2026-05-01";
|
||||
|
||||
beforeEach(async () => {
|
||||
await truncateAllTables(getPrismaClient());
|
||||
|
||||
@@ -418,7 +418,12 @@ describe("doctor-report — MedicationAdministration export cap", () => {
|
||||
},
|
||||
});
|
||||
const med = await prisma.medication.create({
|
||||
data: { userId: user.id, name: "Metformin", dose: "500 mg", active: true },
|
||||
data: {
|
||||
userId: user.id,
|
||||
name: "Metformin",
|
||||
dose: "500 mg",
|
||||
active: true,
|
||||
},
|
||||
});
|
||||
|
||||
// 1001 acted (taken) intakes spread across the window so the cap
|
||||
|
||||
@@ -262,9 +262,8 @@ describe("syncUserWorkout — Workout rows + cross-source dedup", () => {
|
||||
]);
|
||||
|
||||
// Read-time canonical picker collapses the twin to the ladder winner.
|
||||
const { pickCanonicalWorkoutRows } = await import(
|
||||
"@/lib/measurements/pick-canonical-workout-rows"
|
||||
);
|
||||
const { pickCanonicalWorkoutRows } =
|
||||
await import("@/lib/measurements/pick-canonical-workout-rows");
|
||||
const canonical = pickCanonicalWorkoutRows(
|
||||
allRows.map((r) => ({
|
||||
startedAt: r.startedAt,
|
||||
|
||||
@@ -49,7 +49,8 @@ beforeEach(async () => {
|
||||
refreshToken: encrypt("refresh-token"),
|
||||
// Token valid for an hour — keeps `getValidToken` off the refresh path.
|
||||
tokenExpiresAt: new Date(Date.now() + 60 * 60 * 1000),
|
||||
scope: "https://www.googleapis.com/auth/googlehealth.health_metrics_and_measurements.readonly",
|
||||
scope:
|
||||
"https://www.googleapis.com/auth/googlehealth.health_metrics_and_measurements.readonly",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -59,9 +59,7 @@ beforeEach(async () => {
|
||||
cookieJar.clear();
|
||||
headerJar.clear();
|
||||
clearLogBuffer();
|
||||
const { __resetAllCachesForTests } = await import(
|
||||
"@/lib/cache/server-cache"
|
||||
);
|
||||
const { __resetAllCachesForTests } = await import("@/lib/cache/server-cache");
|
||||
__resetAllCachesForTests();
|
||||
});
|
||||
|
||||
@@ -129,9 +127,9 @@ describe("GET /api/insights/comprehensive — server cache", () => {
|
||||
const { GET } = await import("@/app/api/insights/comprehensive/route");
|
||||
const { caches } = await import("@/lib/cache/server-cache");
|
||||
|
||||
const first = await (
|
||||
GET as unknown as (req: Request) => Promise<Response>
|
||||
)(new Request("http://localhost/api/insights/comprehensive"));
|
||||
const first = await (GET as unknown as (req: Request) => Promise<Response>)(
|
||||
new Request("http://localhost/api/insights/comprehensive"),
|
||||
);
|
||||
expect(first.status).toBe(200);
|
||||
expect(caches.analytics.stats().misses).toBe(1);
|
||||
expect(caches.analytics.stats().hits).toBe(0);
|
||||
@@ -228,7 +226,8 @@ describe("GET /api/insights/comprehensive — server cache", () => {
|
||||
data: Record<string, unknown> & {
|
||||
summaries: Record<
|
||||
string,
|
||||
{ count: number; latest: number | null; mean: number | null } | undefined
|
||||
| { count: number; latest: number | null; mean: number | null }
|
||||
| undefined
|
||||
>;
|
||||
totalMeasurements: number;
|
||||
bpPctInTarget: number | null;
|
||||
|
||||
@@ -79,9 +79,8 @@ async function seedRichWeightUser(username: string) {
|
||||
describe("status path delivers real provider text", () => {
|
||||
it("returns the provider's assessment, not the no-key fallback", async () => {
|
||||
const user = await seedRichWeightUser("status-real-text-user");
|
||||
const { generateWeightStatusForUser } = await import(
|
||||
"@/lib/insights/weight-status"
|
||||
);
|
||||
const { generateWeightStatusForUser } =
|
||||
await import("@/lib/insights/weight-status");
|
||||
|
||||
const result = await generateWeightStatusForUser(user.id, {
|
||||
locale: "en",
|
||||
@@ -127,9 +126,8 @@ describe("status path delivers real provider text", () => {
|
||||
},
|
||||
});
|
||||
|
||||
const { generateWeightStatusForUser } = await import(
|
||||
"@/lib/insights/weight-status"
|
||||
);
|
||||
const { generateWeightStatusForUser } =
|
||||
await import("@/lib/insights/weight-status");
|
||||
|
||||
const result = await generateWeightStatusForUser(user.id, {
|
||||
locale: "en",
|
||||
|
||||
@@ -129,10 +129,20 @@ describe("intake scheduledFor targeting — record the viewed dose", () => {
|
||||
await recordViaSchedule(medId, slot0700);
|
||||
|
||||
const morning = await prisma.medicationIntakeEvent.findMany({
|
||||
where: { userId: TEST_USER_ID, medicationId: medId, scheduledFor: slot0700, deletedAt: null },
|
||||
where: {
|
||||
userId: TEST_USER_ID,
|
||||
medicationId: medId,
|
||||
scheduledFor: slot0700,
|
||||
deletedAt: null,
|
||||
},
|
||||
});
|
||||
const evening = await prisma.medicationIntakeEvent.findMany({
|
||||
where: { userId: TEST_USER_ID, medicationId: medId, scheduledFor: slot1900, deletedAt: null },
|
||||
where: {
|
||||
userId: TEST_USER_ID,
|
||||
medicationId: medId,
|
||||
scheduledFor: slot1900,
|
||||
deletedAt: null,
|
||||
},
|
||||
});
|
||||
|
||||
// Exactly one taken morning row; the evening slot holds no taken row.
|
||||
@@ -144,10 +154,20 @@ describe("intake scheduledFor targeting — record the viewed dose", () => {
|
||||
await recordViaSchedule(medId, slot1900);
|
||||
|
||||
const morningAfter = await prisma.medicationIntakeEvent.findMany({
|
||||
where: { userId: TEST_USER_ID, medicationId: medId, scheduledFor: slot0700, deletedAt: null },
|
||||
where: {
|
||||
userId: TEST_USER_ID,
|
||||
medicationId: medId,
|
||||
scheduledFor: slot0700,
|
||||
deletedAt: null,
|
||||
},
|
||||
});
|
||||
const eveningAfter = await prisma.medicationIntakeEvent.findMany({
|
||||
where: { userId: TEST_USER_ID, medicationId: medId, scheduledFor: slot1900, deletedAt: null },
|
||||
where: {
|
||||
userId: TEST_USER_ID,
|
||||
medicationId: medId,
|
||||
scheduledFor: slot1900,
|
||||
deletedAt: null,
|
||||
},
|
||||
});
|
||||
expect(morningAfter).toHaveLength(1);
|
||||
expect(morningAfter[0]?.takenAt).not.toBeNull();
|
||||
|
||||
@@ -284,9 +284,7 @@ describe("IntegrationStatus end-to-end", () => {
|
||||
reauth_required: 0,
|
||||
persistent: 28,
|
||||
},
|
||||
persistentFailureStartedAt: new Date(
|
||||
Date.now() - 26 * 60 * 60 * 1000,
|
||||
),
|
||||
persistentFailureStartedAt: new Date(Date.now() - 26 * 60 * 60 * 1000),
|
||||
lastError: "encrypted-blob-placeholder",
|
||||
lastAttemptAt: new Date(),
|
||||
},
|
||||
|
||||
@@ -123,7 +123,12 @@ describe("measurement rollups — integration", () => {
|
||||
it("backfills with the expected (user × type × granularity × bucket) cardinality", async () => {
|
||||
const prisma = getPrismaClient();
|
||||
const userIds: string[] = [];
|
||||
const types = ["WEIGHT", "PULSE", "BLOOD_PRESSURE_SYS", "BLOOD_PRESSURE_DIA"] as const;
|
||||
const types = [
|
||||
"WEIGHT",
|
||||
"PULSE",
|
||||
"BLOOD_PRESSURE_SYS",
|
||||
"BLOOD_PRESSURE_DIA",
|
||||
] as const;
|
||||
|
||||
// 3 users × 4 types × ~50 rows over ~7 weeks
|
||||
for (let u = 0; u < 3; u++) {
|
||||
@@ -863,7 +868,11 @@ describe("measurement rollups — integration", () => {
|
||||
|
||||
// Confirm setup: BP has rollups, WEIGHT does not.
|
||||
const bpRollups = await prisma.measurementRollup.count({
|
||||
where: { userId: user.id, type: "BLOOD_PRESSURE_SYS", granularity: "DAY" },
|
||||
where: {
|
||||
userId: user.id,
|
||||
type: "BLOOD_PRESSURE_SYS",
|
||||
granularity: "DAY",
|
||||
},
|
||||
});
|
||||
const weightRollups = await prisma.measurementRollup.count({
|
||||
where: { userId: user.id, type: "WEIGHT", granularity: "DAY" },
|
||||
|
||||
@@ -151,7 +151,13 @@ describe("Measurement.deletedAt — tombstone invisibility (v1.4.40 W-DELETED)",
|
||||
data: {
|
||||
summaries: Record<
|
||||
string,
|
||||
{ count: number; latest: number | null; min: number | null; max: number | null; mean: number | null }
|
||||
{
|
||||
count: number;
|
||||
latest: number | null;
|
||||
min: number | null;
|
||||
max: number | null;
|
||||
mean: number | null;
|
||||
}
|
||||
>;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -389,9 +389,9 @@ describe("POST /api/measurements/batch (real Postgres)", () => {
|
||||
).length;
|
||||
expect(insertedEntries).toBe(json.data.inserted);
|
||||
expect(duplicateEntries).toBe(json.data.duplicates);
|
||||
expect(
|
||||
insertedEntries + duplicateEntries + skippedEntries,
|
||||
).toBe(json.data.processed);
|
||||
expect(insertedEntries + duplicateEntries + skippedEntries).toBe(
|
||||
json.data.processed,
|
||||
);
|
||||
}
|
||||
|
||||
// Invariant 2 — aggregate counts are non-negative. The previous
|
||||
@@ -498,8 +498,7 @@ describe("POST /api/measurements/batch (real Postgres)", () => {
|
||||
it("re-posting a stats:* day-aggregate row overwrites the value and returns status='updated'", async () => {
|
||||
const { POST } = await import("@/app/api/measurements/batch/route");
|
||||
|
||||
const externalId =
|
||||
"stats:HKQuantityTypeIdentifierStepCount:2026-05-24";
|
||||
const externalId = "stats:HKQuantityTypeIdentifierStepCount:2026-05-24";
|
||||
const firstBody = {
|
||||
entries: [
|
||||
{
|
||||
@@ -580,8 +579,9 @@ describe("POST /api/measurements/batch (real Postgres)", () => {
|
||||
|
||||
const first = await POST(makeRequest(body));
|
||||
expect(first.status).toBe(200);
|
||||
expect(((await first.json()) as { data: { inserted: number } }).data.inserted)
|
||||
.toBe(1);
|
||||
expect(
|
||||
((await first.json()) as { data: { inserted: number } }).data.inserted,
|
||||
).toBe(1);
|
||||
|
||||
// Replay with a different value — must NOT overwrite because the
|
||||
// externalId is not stats:*. Sample-class rows are immutable.
|
||||
@@ -688,7 +688,9 @@ describe("POST /api/measurements/batch (real Postgres)", () => {
|
||||
expect(json.data.updated).toBe(1);
|
||||
expect(json.data.duplicates).toBe(1);
|
||||
|
||||
const byIndex = new Map(json.data.entries.map((e) => [e.index, e.status]));
|
||||
const byIndex = new Map(
|
||||
json.data.entries.map((e) => [e.index, e.status]),
|
||||
);
|
||||
expect(byIndex.get(0)).toBe("updated");
|
||||
expect(byIndex.get(1)).toBe("duplicate");
|
||||
expect(byIndex.get(2)).toBe("inserted");
|
||||
@@ -818,7 +820,9 @@ describe("POST /api/measurements/batch (real Postgres)", () => {
|
||||
// externalId. The composite unique index would let both land; the
|
||||
// cross-source collapse drops the second copy as a `duplicate`.
|
||||
describe("cross-source same-reading merge (iOS #2)", () => {
|
||||
const weightEntry = (over: Partial<BatchEntryFixture>): BatchEntryFixture => ({
|
||||
const weightEntry = (
|
||||
over: Partial<BatchEntryFixture>,
|
||||
): BatchEntryFixture => ({
|
||||
hkIdentifier: "HKQuantityTypeIdentifierBodyMass",
|
||||
value: 81.4,
|
||||
unit: "kg",
|
||||
@@ -840,14 +844,20 @@ describe("POST /api/measurements/batch (real Postgres)", () => {
|
||||
}),
|
||||
);
|
||||
expect(first.status).toBe(200);
|
||||
expect(((await first.json()) as { data: { inserted: number } }).data.inserted).toBe(1);
|
||||
expect(
|
||||
((await first.json()) as { data: { inserted: number } }).data
|
||||
.inserted,
|
||||
).toBe(1);
|
||||
|
||||
// HealthKit background sync re-ingests the same physical reading
|
||||
// as APPLE_HEALTH with a different (HKSample.uuid) externalId.
|
||||
const second = await POST(
|
||||
makeRequest({
|
||||
entries: [
|
||||
weightEntry({ externalId: "hk-uuid-001", source: "APPLE_HEALTH" }),
|
||||
weightEntry({
|
||||
externalId: "hk-uuid-001",
|
||||
source: "APPLE_HEALTH",
|
||||
}),
|
||||
],
|
||||
}),
|
||||
);
|
||||
@@ -877,7 +887,10 @@ describe("POST /api/measurements/batch (real Postgres)", () => {
|
||||
const first = await POST(
|
||||
makeRequest({
|
||||
entries: [
|
||||
weightEntry({ externalId: "hk-uuid-002", source: "APPLE_HEALTH" }),
|
||||
weightEntry({
|
||||
externalId: "hk-uuid-002",
|
||||
source: "APPLE_HEALTH",
|
||||
}),
|
||||
],
|
||||
}),
|
||||
);
|
||||
@@ -911,7 +924,10 @@ describe("POST /api/measurements/batch (real Postgres)", () => {
|
||||
makeRequest({
|
||||
entries: [
|
||||
weightEntry({ externalId: "local-manual-003", source: "MANUAL" }),
|
||||
weightEntry({ externalId: "hk-uuid-003", source: "APPLE_HEALTH" }),
|
||||
weightEntry({
|
||||
externalId: "hk-uuid-003",
|
||||
source: "APPLE_HEALTH",
|
||||
}),
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -74,9 +74,7 @@ async function seedSession(username: string) {
|
||||
|
||||
async function callList(query: string): Promise<Response> {
|
||||
const { GET } = await import("@/app/api/measurements/route");
|
||||
return GET(
|
||||
new NextRequest(`http://localhost/api/measurements?${query}`),
|
||||
);
|
||||
return GET(new NextRequest(`http://localhost/api/measurements?${query}`));
|
||||
}
|
||||
|
||||
describe("GET /api/measurements?type=SLEEP_DURATION", () => {
|
||||
@@ -88,7 +86,11 @@ describe("GET /api/measurements?type=SLEEP_DURATION", () => {
|
||||
// stage rows ending this morning (Berlin default tz).
|
||||
const wakeBase = new Date();
|
||||
wakeBase.setUTCHours(4, 0, 0, 0);
|
||||
const seed = async (stage: "CORE" | "DEEP" | "REM", min: number, offsetMin: number) =>
|
||||
const seed = async (
|
||||
stage: "CORE" | "DEEP" | "REM",
|
||||
min: number,
|
||||
offsetMin: number,
|
||||
) =>
|
||||
prisma.measurement.create({
|
||||
data: {
|
||||
userId: user.id,
|
||||
@@ -124,7 +126,11 @@ describe("GET /api/measurements?type=SLEEP_DURATION", () => {
|
||||
|
||||
const wakeBase = new Date();
|
||||
wakeBase.setUTCHours(4, 0, 0, 0);
|
||||
const seed = async (stage: "CORE" | "DEEP" | "REM", min: number, offsetMin: number) =>
|
||||
const seed = async (
|
||||
stage: "CORE" | "DEEP" | "REM",
|
||||
min: number,
|
||||
offsetMin: number,
|
||||
) =>
|
||||
prisma.measurement.create({
|
||||
data: {
|
||||
userId: user.id,
|
||||
|
||||
@@ -191,7 +191,9 @@ function todayBounds(now: Date, tz: string): { start: Date; end: Date } {
|
||||
);
|
||||
return Math.round((asIfUtc - probe.getTime()) / 60_000);
|
||||
})();
|
||||
const start = new Date(Date.UTC(y, m - 1, d, 0, 0, 0, 0) - offsetMin * 60_000);
|
||||
const start = new Date(
|
||||
Date.UTC(y, m - 1, d, 0, 0, 0, 0) - offsetMin * 60_000,
|
||||
);
|
||||
const end = new Date(start.getTime() + 24 * 60 * 60 * 1000 - 1);
|
||||
return { start, end };
|
||||
}
|
||||
@@ -305,11 +307,7 @@ describe("monthly RRULE", () => {
|
||||
const { schedules, ctx } = await loadCanonical(med.id);
|
||||
const [schedule] = schedules;
|
||||
|
||||
const months = [
|
||||
"2026-02-01",
|
||||
"2026-03-01",
|
||||
"2026-04-01",
|
||||
] as const;
|
||||
const months = ["2026-02-01", "2026-03-01", "2026-04-01"] as const;
|
||||
for (const dayStr of months) {
|
||||
const b = todayBounds(
|
||||
new Date(`${dayStr}T10:00:00.000Z`),
|
||||
@@ -693,9 +691,9 @@ describe("legacy daysOfWeek fallback", () => {
|
||||
new Date("2026-06-03T10:00:00.000Z"),
|
||||
"Europe/Berlin",
|
||||
);
|
||||
expect(
|
||||
scheduleEmitsInWindow(schedule, ctx, anchor.start, anchor.end),
|
||||
).toBe(true);
|
||||
expect(scheduleEmitsInWindow(schedule, ctx, anchor.start, anchor.end)).toBe(
|
||||
true,
|
||||
);
|
||||
// Off week (Wed 10) — pre-v1.5 worker would have fired (the bug);
|
||||
// the legacy path in the engine now honours intervalWeeks.
|
||||
const off = todayBounds(
|
||||
|
||||
@@ -147,18 +147,40 @@ async function readCompliance(medicationId: string) {
|
||||
scheduledFor: e.scheduledFor,
|
||||
}));
|
||||
const lastIntakeAt = lastNonSkippedTakenAt(mapped);
|
||||
const ctx = buildComplianceMedicationContext(medication, lastIntakeAt, USER_TZ);
|
||||
const ctx = buildComplianceMedicationContext(
|
||||
medication,
|
||||
lastIntakeAt,
|
||||
USER_TZ,
|
||||
);
|
||||
const schedules = medication.schedules as ComplianceSchedule[];
|
||||
return {
|
||||
compliance7: calculateCompliance(mapped, schedules, 7, medication.createdAt, {
|
||||
medicationContext: ctx,
|
||||
}),
|
||||
compliance30: calculateCompliance(mapped, schedules, 30, medication.createdAt, {
|
||||
medicationContext: ctx,
|
||||
}),
|
||||
compliance90: calculateCompliance(mapped, schedules, 90, medication.createdAt, {
|
||||
medicationContext: ctx,
|
||||
}),
|
||||
compliance7: calculateCompliance(
|
||||
mapped,
|
||||
schedules,
|
||||
7,
|
||||
medication.createdAt,
|
||||
{
|
||||
medicationContext: ctx,
|
||||
},
|
||||
),
|
||||
compliance30: calculateCompliance(
|
||||
mapped,
|
||||
schedules,
|
||||
30,
|
||||
medication.createdAt,
|
||||
{
|
||||
medicationContext: ctx,
|
||||
},
|
||||
),
|
||||
compliance90: calculateCompliance(
|
||||
mapped,
|
||||
schedules,
|
||||
90,
|
||||
medication.createdAt,
|
||||
{
|
||||
medicationContext: ctx,
|
||||
},
|
||||
),
|
||||
display: buildComplianceDisplay(mapped, schedules, ctx),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -244,7 +244,11 @@ describe("POST /api/medications/intake/bulk (real Postgres)", () => {
|
||||
);
|
||||
expect(res.status).toBe(200);
|
||||
const json = (await res.json()) as {
|
||||
data: { inserted: number; updated: number; entries: Array<{ status: string }> };
|
||||
data: {
|
||||
inserted: number;
|
||||
updated: number;
|
||||
entries: Array<{ status: string }>;
|
||||
};
|
||||
};
|
||||
expect(json.data.inserted).toBe(0);
|
||||
expect(json.data.updated).toBe(1);
|
||||
@@ -385,14 +389,22 @@ describe("POST /api/medications/intake/bulk (real Postgres)", () => {
|
||||
await POST(
|
||||
makeRequest({
|
||||
entries: [
|
||||
{ medicationId: med.id, scheduledFor: t1.toISOString(), takenAt: t1.toISOString() },
|
||||
{
|
||||
medicationId: med.id,
|
||||
scheduledFor: t1.toISOString(),
|
||||
takenAt: t1.toISOString(),
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
const res = await POST(
|
||||
makeRequest({
|
||||
entries: [
|
||||
{ medicationId: med.id, scheduledFor: t2.toISOString(), takenAt: t2.toISOString() },
|
||||
{
|
||||
medicationId: med.id,
|
||||
scheduledFor: t2.toISOString(),
|
||||
takenAt: t2.toISOString(),
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -73,7 +73,9 @@ describe("mood-insights tag × metric crosstab (DB read path)", () => {
|
||||
data: {
|
||||
userId: TEST_USER_ID,
|
||||
type: "ACTIVE_ENERGY_BURNED",
|
||||
value: present ? 600 + (i % 2 === 0 ? 20 : -20) : 350 + (i % 2 === 0 ? 20 : -20),
|
||||
value: present
|
||||
? 600 + (i % 2 === 0 ? 20 : -20)
|
||||
: 350 + (i % 2 === 0 ? 20 : -20),
|
||||
unit: "kcal",
|
||||
source: "APPLE_HEALTH",
|
||||
measuredAt: ts,
|
||||
@@ -124,7 +126,9 @@ describe("mood-insights tag × metric crosstab (DB read path)", () => {
|
||||
score: present ? 4 : 3,
|
||||
source: "WEB",
|
||||
moodLoggedAt: ts,
|
||||
...(present ? { tagLinks: { create: [{ moodTagId: tag!.id }] } } : {}),
|
||||
...(present
|
||||
? { tagLinks: { create: [{ moodTagId: tag!.id }] } }
|
||||
: {}),
|
||||
},
|
||||
});
|
||||
const value = present
|
||||
|
||||
@@ -92,10 +92,7 @@ async function createGroup(label: string, icon?: string): Promise<string> {
|
||||
return body.data.key;
|
||||
}
|
||||
|
||||
async function createTag(
|
||||
label: string,
|
||||
categoryKey?: string,
|
||||
): Promise<string> {
|
||||
async function createTag(label: string, categoryKey?: string): Promise<string> {
|
||||
const { POST } = await import("@/app/api/mood/tags/custom/route");
|
||||
const res = await POST(
|
||||
jsonReq("http://localhost/api/mood/tags/custom", "POST", {
|
||||
@@ -161,7 +158,9 @@ describe("custom mood-tag groups (real Postgres)", () => {
|
||||
for (let i = 0; i < 12; i++) await createGroup(`G${i}`);
|
||||
const { POST } = await import("@/app/api/mood/tags/groups/route");
|
||||
const res = await POST(
|
||||
jsonReq("http://localhost/api/mood/tags/groups", "POST", { label: "Nope" }),
|
||||
jsonReq("http://localhost/api/mood/tags/groups", "POST", {
|
||||
label: "Nope",
|
||||
}),
|
||||
);
|
||||
expect(res.status).toBe(422);
|
||||
});
|
||||
@@ -171,9 +170,8 @@ describe("custom mood-tag groups (real Postgres)", () => {
|
||||
const groupKey = await createGroup("Privat");
|
||||
|
||||
await actAs(USER_B, "mtg-b");
|
||||
const { PATCH, DELETE } = await import(
|
||||
"@/app/api/mood/tags/groups/[key]/route"
|
||||
);
|
||||
const { PATCH, DELETE } =
|
||||
await import("@/app/api/mood/tags/groups/[key]/route");
|
||||
const patch = await PATCH(
|
||||
jsonReq(`http://localhost/api/mood/tags/groups/${groupKey}`, "PATCH", {
|
||||
label: "Hijack",
|
||||
@@ -204,9 +202,7 @@ describe("custom mood-tag groups (real Postgres)", () => {
|
||||
const tagKey = await createTag("Bouldern", groupKey);
|
||||
|
||||
// Seed a layout that references the group.
|
||||
const { PUT: putLayout } = await import(
|
||||
"@/app/api/mood/tags/layout/route"
|
||||
);
|
||||
const { PUT: putLayout } = await import("@/app/api/mood/tags/layout/route");
|
||||
const layoutRes = await putLayout(
|
||||
jsonReq("http://localhost/api/mood/tags/layout", "PUT", {
|
||||
groupOrder: [groupKey, "feelings"],
|
||||
@@ -264,9 +260,8 @@ describe("custom mood-tag groups (real Postgres)", () => {
|
||||
const groupKey = await createGroup("Sport");
|
||||
await createTag("Klettern", groupKey);
|
||||
|
||||
const { PUT: putLayout, GET: getLayout } = await import(
|
||||
"@/app/api/mood/tags/layout/route"
|
||||
);
|
||||
const { PUT: putLayout, GET: getLayout } =
|
||||
await import("@/app/api/mood/tags/layout/route");
|
||||
// 1st PUT: placements only.
|
||||
await putLayout(
|
||||
jsonReq("http://localhost/api/mood/tags/layout", "PUT", {
|
||||
|
||||
@@ -125,14 +125,17 @@ describe("MedicationIntakeEvent — re-take after delete (v1.12.1 / 0121)", () =
|
||||
|
||||
describe("MoodEntry — external-id re-import idempotency (v1.12.1 / 0122)", () => {
|
||||
function webhookRequest(secret: string, body: unknown): NextRequest {
|
||||
return new NextRequest("http://localhost/api/integrations/moodlog/webhook", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
"x-webhook-secret": secret,
|
||||
return new NextRequest(
|
||||
"http://localhost/api/integrations/moodlog/webhook",
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
"x-webhook-secret": secret,
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
);
|
||||
}
|
||||
|
||||
it("re-import with the same stable id but re-zoned time updates one row", async () => {
|
||||
@@ -149,9 +152,8 @@ describe("MoodEntry — external-id re-import idempotency (v1.12.1 / 0122)", ()
|
||||
},
|
||||
});
|
||||
|
||||
const { POST } = await import(
|
||||
"@/app/api/integrations/moodlog/webhook/route"
|
||||
);
|
||||
const { POST } =
|
||||
await import("@/app/api/integrations/moodlog/webhook/route");
|
||||
|
||||
// First import — entry carries a stable upstream id.
|
||||
const r1 = await POST(
|
||||
@@ -210,9 +212,8 @@ describe("MoodEntry — external-id re-import idempotency (v1.12.1 / 0122)", ()
|
||||
},
|
||||
});
|
||||
|
||||
const { POST } = await import(
|
||||
"@/app/api/integrations/moodlog/webhook/route"
|
||||
);
|
||||
const { POST } =
|
||||
await import("@/app/api/integrations/moodlog/webhook/route");
|
||||
|
||||
// Two entries, no id, DIFFERENT times → two rows (legacy behaviour
|
||||
// preserved). A re-emit at the SAME time updates in place.
|
||||
|
||||
@@ -54,9 +54,10 @@ vi.mock("@/lib/db-compat", () => ({
|
||||
// the success / failure branch shape; the route handler's
|
||||
// challenge-lookup + Passkey insertion still runs against real Postgres.
|
||||
vi.mock("@/lib/auth/passkey", async () => {
|
||||
const actual = await vi.importActual<typeof import("@/lib/auth/passkey")>(
|
||||
"@/lib/auth/passkey",
|
||||
);
|
||||
const actual =
|
||||
await vi.importActual<typeof import("@/lib/auth/passkey")>(
|
||||
"@/lib/auth/passkey",
|
||||
);
|
||||
return {
|
||||
...actual,
|
||||
verifyRegistration: vi.fn(),
|
||||
@@ -99,9 +100,8 @@ afterEach(() => {
|
||||
|
||||
describe("Passkey registration (real Postgres)", () => {
|
||||
it("register-options persists a challenge and returns the RP-ID from APP_URL", async () => {
|
||||
const { POST } = await import(
|
||||
"@/app/api/auth/passkey/register-options/route"
|
||||
);
|
||||
const { POST } =
|
||||
await import("@/app/api/auth/passkey/register-options/route");
|
||||
// The route handler takes no arguments at the TS level (the wrapper
|
||||
// ignores the request) — cast to a no-arg callable for clarity.
|
||||
const res = await (POST as unknown as () => Promise<Response>)();
|
||||
@@ -161,9 +161,8 @@ describe("Passkey registration (real Postgres)", () => {
|
||||
},
|
||||
});
|
||||
|
||||
const { POST } = await import(
|
||||
"@/app/api/auth/passkey/register-verify/route"
|
||||
);
|
||||
const { POST } =
|
||||
await import("@/app/api/auth/passkey/register-verify/route");
|
||||
const res = await POST(
|
||||
new NextRequest("http://localhost/api/auth/passkey/register-verify", {
|
||||
method: "POST",
|
||||
@@ -213,9 +212,8 @@ describe("Passkey registration (real Postgres)", () => {
|
||||
},
|
||||
});
|
||||
|
||||
const { POST } = await import(
|
||||
"@/app/api/auth/passkey/register-verify/route"
|
||||
);
|
||||
const { POST } =
|
||||
await import("@/app/api/auth/passkey/register-verify/route");
|
||||
const res = await POST(
|
||||
new NextRequest("http://localhost/api/auth/passkey/register-verify", {
|
||||
method: "POST",
|
||||
@@ -236,9 +234,8 @@ describe("Passkey registration (real Postgres)", () => {
|
||||
});
|
||||
|
||||
it("register-verify returns 422 when challengeId is missing", async () => {
|
||||
const { POST } = await import(
|
||||
"@/app/api/auth/passkey/register-verify/route"
|
||||
);
|
||||
const { POST } =
|
||||
await import("@/app/api/auth/passkey/register-verify/route");
|
||||
const res = await POST(
|
||||
new NextRequest("http://localhost/api/auth/passkey/register-verify", {
|
||||
method: "POST",
|
||||
|
||||
@@ -58,9 +58,7 @@ beforeEach(async () => {
|
||||
await truncateAllTables(getPrismaClient());
|
||||
cookieJar.clear();
|
||||
headerJar.clear();
|
||||
const { __resetAllCachesForTests } = await import(
|
||||
"@/lib/cache/server-cache"
|
||||
);
|
||||
const { __resetAllCachesForTests } = await import("@/lib/cache/server-cache");
|
||||
__resetAllCachesForTests();
|
||||
});
|
||||
|
||||
@@ -98,12 +96,10 @@ describe("v1.4.34 IW-G — server-cache route integration", () => {
|
||||
// the `pending` promise; the other nine join it. The cache's
|
||||
// counter snapshot witnesses the fan-in: misses === 1, stampedes
|
||||
// covers the remaining nine reads.
|
||||
const requests = Array.from(
|
||||
{ length: 10 },
|
||||
() =>
|
||||
(GET as unknown as (req: Request) => Promise<Response>)(
|
||||
new Request("http://localhost/api/analytics"),
|
||||
),
|
||||
const requests = Array.from({ length: 10 }, () =>
|
||||
(GET as unknown as (req: Request) => Promise<Response>)(
|
||||
new Request("http://localhost/api/analytics"),
|
||||
),
|
||||
);
|
||||
const responses = await Promise.all(requests);
|
||||
|
||||
@@ -118,9 +114,9 @@ describe("v1.4.34 IW-G — server-cache route integration", () => {
|
||||
expect(stats.misses).toBe(1);
|
||||
expect(stats.stampedes + stats.hits).toBe(9);
|
||||
// Subsequent serial reads inside the TTL hit the warm cache.
|
||||
const warm = await (
|
||||
GET as unknown as (req: Request) => Promise<Response>
|
||||
)(new Request("http://localhost/api/analytics"));
|
||||
const warm = await (GET as unknown as (req: Request) => Promise<Response>)(
|
||||
new Request("http://localhost/api/analytics"),
|
||||
);
|
||||
expect(warm.status).toBe(200);
|
||||
// Plus at least one extra hit from this serial read.
|
||||
expect(caches.analytics.stats().hits).toBeGreaterThanOrEqual(1);
|
||||
@@ -130,15 +126,14 @@ describe("v1.4.34 IW-G — server-cache route integration", () => {
|
||||
const user = await seedSession("cache-invalidate-user");
|
||||
|
||||
const { GET } = await import("@/app/api/analytics/route");
|
||||
const { POST: postMeasurement } = await import(
|
||||
"@/app/api/measurements/route"
|
||||
);
|
||||
const { POST: postMeasurement } =
|
||||
await import("@/app/api/measurements/route");
|
||||
const { caches } = await import("@/lib/cache/server-cache");
|
||||
|
||||
// Prime the cache — first read is a miss; second is a hit.
|
||||
const first = await (
|
||||
GET as unknown as (req: Request) => Promise<Response>
|
||||
)(new Request("http://localhost/api/analytics"));
|
||||
const first = await (GET as unknown as (req: Request) => Promise<Response>)(
|
||||
new Request("http://localhost/api/analytics"),
|
||||
);
|
||||
expect(first.status).toBe(200);
|
||||
expect(caches.analytics.stats().misses).toBe(1);
|
||||
expect(caches.analytics.stats().hits).toBe(0);
|
||||
@@ -168,9 +163,9 @@ describe("v1.4.34 IW-G — server-cache route integration", () => {
|
||||
|
||||
// The next read MUST miss — the cache for this userId|default key
|
||||
// was evicted by the write's invalidation call.
|
||||
const third = await (
|
||||
GET as unknown as (req: Request) => Promise<Response>
|
||||
)(new Request("http://localhost/api/analytics"));
|
||||
const third = await (GET as unknown as (req: Request) => Promise<Response>)(
|
||||
new Request("http://localhost/api/analytics"),
|
||||
);
|
||||
expect(third.status).toBe(200);
|
||||
expect(caches.analytics.stats().misses).toBe(2);
|
||||
|
||||
@@ -216,9 +211,8 @@ describe("v1.4.34 IW-G — server-cache route integration", () => {
|
||||
},
|
||||
});
|
||||
|
||||
const { GET: intakeGet, POST: intakePost } = await import(
|
||||
"@/app/api/medications/intake/route"
|
||||
);
|
||||
const { GET: intakeGet, POST: intakePost } =
|
||||
await import("@/app/api/medications/intake/route");
|
||||
const { NextRequest } = await import("next/server");
|
||||
const { caches } = await import("@/lib/cache/server-cache");
|
||||
|
||||
|
||||
@@ -27,9 +27,9 @@ export function getPrismaClient(): PrismaClient {
|
||||
}
|
||||
|
||||
/** Lazily-captured global mood-tag catalogue (NULL user_id rows). */
|
||||
let catalogueSnapshot:
|
||||
| Awaited<ReturnType<PrismaClient["moodTag"]["findMany"]>>
|
||||
| null = null;
|
||||
let catalogueSnapshot: Awaited<
|
||||
ReturnType<PrismaClient["moodTag"]["findMany"]>
|
||||
> | null = null;
|
||||
|
||||
/**
|
||||
* v1.17.0 — same treatment for the seeded mood-tag categories:
|
||||
@@ -37,9 +37,9 @@ let catalogueSnapshot:
|
||||
* users TRUNCATE cascades into the category table too. The categories must
|
||||
* restore BEFORE the tags (tags FK into categories).
|
||||
*/
|
||||
let categorySnapshot:
|
||||
| Awaited<ReturnType<PrismaClient["moodTagCategory"]["findMany"]>>
|
||||
| null = null;
|
||||
let categorySnapshot: Awaited<
|
||||
ReturnType<PrismaClient["moodTagCategory"]["findMany"]>
|
||||
> | null = null;
|
||||
|
||||
/**
|
||||
* Truncate every user-facing table in dependency-safe order using
|
||||
|
||||
@@ -157,7 +157,9 @@ async function seedMood(n: number): Promise<string[]> {
|
||||
}
|
||||
|
||||
/** Seed one medication + n live intake events with increasing updatedAt. */
|
||||
async function seedIntakes(n: number): Promise<{ medId: string; ids: string[] }> {
|
||||
async function seedIntakes(
|
||||
n: number,
|
||||
): Promise<{ medId: string; ids: string[] }> {
|
||||
const prisma = getPrismaClient();
|
||||
const base = new Date("2026-05-20T00:00:00.000Z").getTime();
|
||||
const med = await prisma.medication.create({
|
||||
@@ -258,9 +260,8 @@ describe("GET /api/sync/changes (real Postgres)", () => {
|
||||
});
|
||||
|
||||
// Soft-delete the first row via the DELETE route (the production path).
|
||||
const { DELETE } = await import(
|
||||
"@/app/api/measurements/by-external-ids/route"
|
||||
);
|
||||
const { DELETE } =
|
||||
await import("@/app/api/measurements/by-external-ids/route");
|
||||
const delReq = new NextRequest(
|
||||
"http://localhost/api/measurements/by-external-ids",
|
||||
{
|
||||
@@ -499,9 +500,8 @@ describe("GET /api/sync/changes — intake domain (real Postgres)", () => {
|
||||
it("surfaces a soft-deleted intake as a tombstone keyed on id, never an upsert", async () => {
|
||||
const { medId, ids } = await seedIntakes(2);
|
||||
const [delId, keepId] = ids;
|
||||
const { DELETE } = await import(
|
||||
"@/app/api/medications/[id]/intake/[eventId]/route"
|
||||
);
|
||||
const { DELETE } =
|
||||
await import("@/app/api/medications/[id]/intake/[eventId]/route");
|
||||
await DELETE(
|
||||
new NextRequest(
|
||||
`http://localhost/api/medications/${medId}/intake/${delId}`,
|
||||
|
||||
@@ -75,9 +75,21 @@ describe("GET /api/sync/state (real Postgres)", () => {
|
||||
timezone: string;
|
||||
lastSyncedAt: string | null;
|
||||
serverNow: string;
|
||||
measurements: { lastUpdatedAt: string | null; liveCount: number; tombstonedCount: number };
|
||||
mood: { lastUpdatedAt: string | null; liveCount: number; tombstonedCount: number };
|
||||
intakes: { lastUpdatedAt: string | null; liveCount: number; tombstonedCount: number };
|
||||
measurements: {
|
||||
lastUpdatedAt: string | null;
|
||||
liveCount: number;
|
||||
tombstonedCount: number;
|
||||
};
|
||||
mood: {
|
||||
lastUpdatedAt: string | null;
|
||||
liveCount: number;
|
||||
tombstonedCount: number;
|
||||
};
|
||||
intakes: {
|
||||
lastUpdatedAt: string | null;
|
||||
liveCount: number;
|
||||
tombstonedCount: number;
|
||||
};
|
||||
};
|
||||
};
|
||||
expect(json.data.userId).toBe(TEST_USER_ID);
|
||||
|
||||
@@ -99,11 +99,7 @@ describe("self-hosted avatar — end-to-end", () => {
|
||||
|
||||
// ── POST: upload the 1×1 PNG ──────────────────────────────
|
||||
const fd = new FormData();
|
||||
fd.append(
|
||||
"file",
|
||||
new Blob([PNG_1X1], { type: "image/png" }),
|
||||
"avatar.png",
|
||||
);
|
||||
fd.append("file", new Blob([PNG_1X1], { type: "image/png" }), "avatar.png");
|
||||
const uploadReq = new Request("http://localhost/api/user/avatar", {
|
||||
method: "POST",
|
||||
body: fd,
|
||||
|
||||
@@ -200,9 +200,7 @@ describe("Withings OAuth nonce ledger (real Postgres)", () => {
|
||||
);
|
||||
const callbackRes = await callback(callbackReq);
|
||||
expect(callbackRes.status).toBe(307);
|
||||
expect(callbackRes.headers.get("location")).toContain(
|
||||
"withings=connected",
|
||||
);
|
||||
expect(callbackRes.headers.get("location")).toContain("withings=connected");
|
||||
|
||||
const afterCallback = await prisma.withingsOAuthState.findUnique({
|
||||
where: { nonce },
|
||||
|
||||
@@ -343,10 +343,7 @@ describe("POST /api/workouts/batch (real Postgres)", () => {
|
||||
// reaching the DB. This is the Apple Watch + Withings ScanWatch
|
||||
// case the picker exists to handle.
|
||||
const body = {
|
||||
workouts: [
|
||||
baseWorkout("hk-uuid-dup-1"),
|
||||
baseWorkout("hk-uuid-dup-2"),
|
||||
],
|
||||
workouts: [baseWorkout("hk-uuid-dup-1"), baseWorkout("hk-uuid-dup-2")],
|
||||
};
|
||||
|
||||
const first = await POST(makeRequest(body));
|
||||
|
||||
@@ -217,9 +217,7 @@ describe("POST /api/workouts/batch — concurrent-write race", () => {
|
||||
where: { userId: TEST_USER_ID },
|
||||
});
|
||||
expect(stored).toHaveLength(3);
|
||||
const sharedRows = stored.filter(
|
||||
(r) => r.externalId === "hk-uuid-shared",
|
||||
);
|
||||
const sharedRows = stored.filter((r) => r.externalId === "hk-uuid-shared");
|
||||
expect(sharedRows).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user