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.
162 lines
5.3 KiB
TypeScript
162 lines
5.3 KiB
TypeScript
/**
|
|
* #943 — untagged glucose readings light the dashboard tile.
|
|
*
|
|
* A reporter's meter syncs through Apple Health and never writes HealthKit's
|
|
* blood-glucose meal-time metadata, so all of their readings carry a NULL
|
|
* `glucose_context`. The tile strip filtered the four named contexts against
|
|
* the per-context summaries, matched none, and omitted the tile — module on,
|
|
* layout toggle on, a reading from today, and no warning anywhere.
|
|
*
|
|
* A real Postgres is what makes this convincing: the summaries are built from
|
|
* rows the database actually holds, with the column genuinely NULL, and the
|
|
* tile gate is the same pure resolver the page renders through.
|
|
*/
|
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
|
import { cookieJar, headerJar } from "./mock-next-headers";
|
|
import { getPrismaClient, truncateAllTables } from "./setup";
|
|
|
|
import { resolveGlucoseTiles } from "@/components/dashboard/dashboard-gates";
|
|
import type { DataSummary } from "@/lib/analytics/trends";
|
|
|
|
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);
|
|
},
|
|
})),
|
|
};
|
|
});
|
|
|
|
interface AnalyticsEnvelope {
|
|
data: {
|
|
glucoseByContext: Record<string, DataSummary> | null;
|
|
};
|
|
error: null;
|
|
}
|
|
|
|
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
const READINGS = 20;
|
|
|
|
beforeEach(async () => {
|
|
await truncateAllTables(getPrismaClient());
|
|
cookieJar.clear();
|
|
headerJar.clear();
|
|
});
|
|
|
|
async function seedUntaggedGlucose() {
|
|
const prisma = getPrismaClient();
|
|
const user = await prisma.user.create({
|
|
data: {
|
|
username: "glucose-untagged",
|
|
email: "glucose-untagged@example.test",
|
|
role: "USER",
|
|
timezone: "Europe/Berlin",
|
|
},
|
|
});
|
|
const session = await prisma.session.create({
|
|
data: { userId: user.id, expiresAt: new Date(Date.now() + 60_000) },
|
|
});
|
|
cookieJar.set("healthlog_session", session.id);
|
|
|
|
// 20 readings, one per day, newest first at 104 mg/dL. Every one of them
|
|
// arrives exactly as the reporter's sync writes them: no meal-time context.
|
|
const now = Date.now();
|
|
await prisma.measurement.createMany({
|
|
data: Array.from({ length: READINGS }, (_, i) => ({
|
|
userId: user.id,
|
|
type: "BLOOD_GLUCOSE" as const,
|
|
value: 104 + i,
|
|
unit: "mg/dL",
|
|
source: "APPLE_HEALTH" as const,
|
|
measuredAt: new Date(now - i * DAY_MS),
|
|
glucoseContext: null,
|
|
})),
|
|
});
|
|
return user;
|
|
}
|
|
|
|
async function readAnalytics(): Promise<AnalyticsEnvelope> {
|
|
const { GET } = await import("@/app/api/analytics/route");
|
|
const response = await (
|
|
GET as unknown as (req: Request) => Promise<Response>
|
|
)(new Request("http://localhost/api/analytics"));
|
|
expect(response.status).toBe(200);
|
|
return (await response.json()) as AnalyticsEnvelope;
|
|
}
|
|
|
|
describe("dashboard glucose tile with untagged readings (#943)", () => {
|
|
it("stores the readings with a NULL context", async () => {
|
|
const user = await seedUntaggedGlucose();
|
|
const rows = await getPrismaClient().measurement.findMany({
|
|
where: { userId: user.id, type: "BLOOD_GLUCOSE" },
|
|
select: { glucoseContext: true },
|
|
});
|
|
|
|
expect(rows).toHaveLength(READINGS);
|
|
expect(rows.every((row) => row.glucoseContext === null)).toBe(true);
|
|
});
|
|
|
|
it("reports the tile as eligible and carries the latest value", async () => {
|
|
await seedUntaggedGlucose();
|
|
|
|
const envelope = await readAnalytics();
|
|
const byContext = envelope.data.glucoseByContext;
|
|
expect(byContext).not.toBeNull();
|
|
|
|
// The summaries carry the untagged bucket, and nothing else.
|
|
expect(Object.keys(byContext ?? {})).toEqual(["UNSPECIFIED"]);
|
|
const summary = byContext!.UNSPECIFIED;
|
|
expect(summary.count).toBe(READINGS);
|
|
expect(summary.latest).toBe(104);
|
|
expect(summary.avg30).not.toBeNull();
|
|
|
|
// …and the tile gate the dashboard renders through says "paint it".
|
|
const tiles = resolveGlucoseTiles(byContext ?? undefined);
|
|
expect(tiles).toHaveLength(1);
|
|
expect(tiles[0]).toMatchObject({
|
|
bucket: "UNSPECIFIED",
|
|
labelKey: "targets.glucoseUnspecified",
|
|
});
|
|
expect(tiles[0].summary.latest).toBe(104);
|
|
});
|
|
|
|
it("keeps a tagged reading in its own bucket beside the untagged ones", async () => {
|
|
const user = await seedUntaggedGlucose();
|
|
await getPrismaClient().measurement.create({
|
|
data: {
|
|
userId: user.id,
|
|
type: "BLOOD_GLUCOSE",
|
|
value: 92,
|
|
unit: "mg/dL",
|
|
source: "MANUAL",
|
|
measuredAt: new Date(),
|
|
glucoseContext: "FASTING",
|
|
},
|
|
});
|
|
|
|
const envelope = await readAnalytics();
|
|
const byContext = envelope.data.glucoseByContext ?? {};
|
|
|
|
expect(Object.keys(byContext).sort()).toEqual(["FASTING", "UNSPECIFIED"]);
|
|
expect(byContext.FASTING.count).toBe(1);
|
|
expect(byContext.UNSPECIFIED.count).toBe(READINGS);
|
|
expect(resolveGlucoseTiles(byContext).map((tile) => tile.bucket)).toEqual([
|
|
"FASTING",
|
|
"UNSPECIFIED",
|
|
]);
|
|
});
|
|
});
|