mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-13 14:32:03 +00:00
ensureDbCompatibility ran ALTER TABLE / CREATE TABLE IF NOT EXISTS on the login, register, token and session paths, mirroring columns every migration under prisma/migrations already creates. The entrypoint applies those migrations before the server listens and stops the boot when one fails, so the mirror never had work to do, and DDL on a request path needs the table's owner: a connection that may only read and write rows fails every login on it. Drop the module, its call sites and the mocks that stubbed it out.
122 lines
4.0 KiB
TypeScript
122 lines
4.0 KiB
TypeScript
/**
|
|
* "Pulse" names one statistic on the dashboard and in the day view.
|
|
*
|
|
* The dashboard's pulse tile led with the latest single reading while the day
|
|
* view showed the day's pulse as the mean of its local hours' means. The slim
|
|
* summaries slice now reports `latest` of an hourly-mean type as the value of
|
|
* its latest local day, through the day view's own canonical-source pick and
|
|
* the shared day-mean helper, so the two read the same number for one day.
|
|
*
|
|
* The day (three weeks back, Europe/Berlin): three readings at 60 in one
|
|
* hour, one at 90 in the next, and a last one at 70 an hour later. Hour means
|
|
* 60, 90 and 70: the day's pulse is 73.33. The latest reading is 70, the plain
|
|
* mean of the five readings 68.
|
|
*/
|
|
import { NextRequest } from "next/server";
|
|
import { beforeAll, describe, expect, it, vi } from "vitest";
|
|
|
|
import { cookieJar } from "./mock-next-headers";
|
|
import { getPrismaClient, truncateAllTables } from "./setup";
|
|
import { invalidateUserTimezone } from "@/lib/tz/resolver";
|
|
|
|
vi.mock("next/headers", async () => {
|
|
const { cookieJar, headerJar } = await import("./mock-next-headers");
|
|
return {
|
|
headers: vi.fn(async () => ({
|
|
get: (name: string) => headerJar.get(name.toLowerCase()) ?? null,
|
|
})),
|
|
cookies: vi.fn(async () => ({
|
|
get: (name: string) => {
|
|
const value = cookieJar.get(name);
|
|
return value ? { name, value } : undefined;
|
|
},
|
|
set: (name: string, value: string) => cookieJar.set(name, value),
|
|
delete: (name: string) => cookieJar.delete(name),
|
|
})),
|
|
};
|
|
});
|
|
|
|
const prisma = getPrismaClient();
|
|
const USER = "pulse-latest-day";
|
|
const now = new Date();
|
|
/** UTC midnight three weeks back; 08:00Z is 10:00 in Berlin in any season. */
|
|
const D = Date.UTC(
|
|
now.getUTCFullYear(),
|
|
now.getUTCMonth(),
|
|
now.getUTCDate() - 21,
|
|
);
|
|
const at = (h: number, m: number) => new Date(D + h * 3_600_000 + m * 60_000);
|
|
const DAY_KEY = new Date(D).toISOString().slice(0, 10);
|
|
|
|
beforeAll(async () => {
|
|
await truncateAllTables(prisma);
|
|
await prisma.user.create({
|
|
data: {
|
|
id: USER,
|
|
username: USER,
|
|
email: `${USER}@example.test`,
|
|
timezone: "Europe/Berlin",
|
|
locale: "en",
|
|
},
|
|
});
|
|
invalidateUserTimezone(USER);
|
|
const readings: Array<[Date, number]> = [
|
|
[at(8, 0), 60],
|
|
[at(8, 20), 60],
|
|
[at(8, 40), 60],
|
|
[at(9, 10), 90],
|
|
[at(10, 5), 70],
|
|
];
|
|
await prisma.measurement.createMany({
|
|
data: readings.map(([measuredAt, value], i) => ({
|
|
id: `${USER}-${i}`,
|
|
userId: USER,
|
|
type: "PULSE",
|
|
unit: "bpm",
|
|
source: "APPLE_HEALTH",
|
|
value,
|
|
measuredAt,
|
|
})),
|
|
});
|
|
const session = await prisma.session.create({
|
|
data: { userId: USER, expiresAt: new Date(Date.now() + 60_000) },
|
|
});
|
|
cookieJar.set("healthlog_session", session.id);
|
|
}, 120_000);
|
|
|
|
describe("the dashboard's pulse and the day view's pulse", () => {
|
|
it("report the latest day's value, not its latest reading", async () => {
|
|
const { computeSummariesSlice } =
|
|
await import("@/lib/analytics/summaries-slice");
|
|
const slice = await computeSummariesSlice(USER);
|
|
expect(slice.summaries.PULSE.latest).toBeCloseTo(220 / 3, 6);
|
|
});
|
|
|
|
it("is the number the day view shows for that day", async () => {
|
|
const { GET } = await import("@/app/api/day/[date]/route");
|
|
const res = await (
|
|
GET as unknown as (
|
|
req: NextRequest,
|
|
ctx: { params: Promise<{ date: string }> },
|
|
) => Promise<Response>
|
|
)(new NextRequest(`http://localhost/api/day/${DAY_KEY}`), {
|
|
params: Promise.resolve({ date: DAY_KEY }),
|
|
});
|
|
expect(res.status).toBe(200);
|
|
const day = (
|
|
(await res.json()) as {
|
|
data: { values: Array<{ type: string; value: number }> };
|
|
}
|
|
).data;
|
|
const pulse = day.values.find((v) => v.type === "PULSE");
|
|
expect(pulse?.value).toBeCloseTo(73.33, 2);
|
|
|
|
const { computeSummariesSlice } =
|
|
await import("@/lib/analytics/summaries-slice");
|
|
const slice = await computeSummariesSlice(USER);
|
|
expect(Math.round(slice.summaries.PULSE.latest! * 100) / 100).toBe(
|
|
pulse?.value,
|
|
);
|
|
});
|
|
});
|