Files
HealthLog/tests/integration/activity-calendar-read.test.ts

168 lines
4.6 KiB
TypeScript

/**
* The activity calendar's reader against a real Postgres (D1036).
*
* - steps resolve to the source the person's ladder picks per day, so a day
* two devices counted shows one device's total, never the sum;
* - the day is the person's local day: a reading at 23:30 Berlin time lands
* on that day, not on the UTC day after;
* - a workout two devices recorded counts once, its minutes once;
* - the window is exact: nothing before `fromKey` or after `toKey`.
*/
import { beforeAll, describe, expect, it } from "vitest";
import { getPrismaClient, truncateAllTables } from "./setup";
import { readActivityDays } from "@/lib/activity/calendar-read";
import { invalidateUserTimezone } from "@/lib/tz/resolver";
import { userDayKey } from "@/lib/tz/format";
const prisma = getPrismaClient();
const TZ = "Europe/Berlin";
const USER = "activity-reader";
const DAY = 86_400_000;
const NOW = new Date();
function daysAgo(n: number, hourUtc = 10): Date {
const d = new Date(NOW.getTime() - n * DAY);
d.setUTCHours(hourUtc, 0, 0, 0);
return d;
}
beforeAll(async () => {
await truncateAllTables(prisma);
await prisma.user.create({
data: {
id: USER,
username: USER,
timezone: TZ,
sourcePriorityJson: {
metricPriority: { steps: ["FITBIT", "APPLE_HEALTH"] },
},
},
});
invalidateUserTimezone(USER);
await prisma.measurement.createMany({
data: [
// Day 3: both devices; the ladder picks Fitbit's 8 000.
{
userId: USER,
type: "ACTIVITY_STEPS",
value: 9500,
unit: "steps",
source: "APPLE_HEALTH",
measuredAt: daysAgo(3),
},
{
userId: USER,
type: "ACTIVITY_STEPS",
value: 8000,
unit: "steps",
source: "FITBIT",
measuredAt: daysAgo(3),
},
// Day 2: Apple only, two slices that add up.
{
userId: USER,
type: "ACTIVITY_STEPS",
value: 3000,
unit: "steps",
source: "APPLE_HEALTH",
measuredAt: daysAgo(2, 8),
},
{
userId: USER,
type: "ACTIVITY_STEPS",
value: 4000,
unit: "steps",
source: "APPLE_HEALTH",
measuredAt: daysAgo(2, 15),
},
// 21:30 UTC is 23:30 in Berlin (summer) or 22:30 (winter): still day 5 locally.
{
userId: USER,
type: "ACTIVITY_STEPS",
value: 1200,
unit: "steps",
source: "APPLE_HEALTH",
measuredAt: daysAgo(5, 21),
},
{
userId: USER,
type: "EXERCISE_MINUTES",
value: 25,
unit: "min",
source: "APPLE_HEALTH",
measuredAt: daysAgo(3, 9),
},
{
userId: USER,
type: "EXERCISE_MINUTES",
value: 15,
unit: "min",
source: "APPLE_HEALTH",
measuredAt: daysAgo(3, 17),
},
// Outside the window asked for below.
{
userId: USER,
type: "ACTIVITY_STEPS",
value: 777,
unit: "steps",
source: "APPLE_HEALTH",
measuredAt: daysAgo(40),
},
],
});
const start = daysAgo(3, 7);
await prisma.workout.createMany({
data: [
{
userId: USER,
sportType: "running",
startedAt: start,
endedAt: new Date(start.getTime() + 40 * 60_000),
durationSec: 2400,
source: "APPLE_HEALTH",
externalId: "hk-1",
},
{
userId: USER,
sportType: "running",
startedAt: new Date(start.getTime() + 30_000),
endedAt: new Date(start.getTime() + 40 * 60_000),
durationSec: 2370,
source: "FITBIT",
externalId: "fb-1",
},
],
});
});
describe("readActivityDays", () => {
it("follows the ladder, the local day and the workout collapse", async () => {
const fromKey = userDayKey(daysAgo(10), TZ);
const toKey = userDayKey(NOW, TZ);
const { days } = await readActivityDays({
userId: USER,
tz: TZ,
fromKey,
toKey,
includeWorkouts: true,
});
const by = new Map(days.map((d) => [d.date, d]));
const d3 = by.get(userDayKey(daysAgo(3), TZ));
expect(d3?.steps).toBe(8000);
expect(d3?.exerciseMinutes).toBe(40);
expect(d3?.workoutCount).toBe(1);
expect(d3?.workoutsBySport).toEqual({ running: 1 });
expect(d3?.workoutMinutes).toBe(40);
expect(by.get(userDayKey(daysAgo(2), TZ))?.steps).toBe(7000);
expect(by.get(userDayKey(daysAgo(5, 21), TZ))?.steps).toBe(1200);
expect(days.every((d) => d.date >= fromKey && d.date <= toKey)).toBe(true);
expect(days.some((d) => d.steps === 777)).toBe(false);
});
});