Merge branch 'v1424/cards' into release/v1.42.4

This commit is contained in:
Marc-André Bombeck committed 2026-10-15 15:22:12 +02:00
commit f95c2bfd41
60 files changed
+5248 -5087

No files matched your search

@@ -96,12 +96,13 @@ beforeEach(async () => {
describe("integration watchdog", () => {
it("flags a normally-daily wearable gone silent, alerts once, then sends one recovery note", async () => {
// Fourteen consecutive days of data before the window, newest 30 h ago.
// Thirteen consecutive days of data before the window, newest 54 h ago:
// past Oura's 48 h window (it uploads only when its phone app is opened).
await seedDailyReadings(
USER,
"OURA",
Array.from({ length: 13 }, (_, i) => i + 2),
30,
Array.from({ length: 13 }, (_, i) => i + 3),
54,
);
vi.useFakeTimers({ toFake: ["Date"] });
vi.setSystemTime(NOW);
@@ -170,7 +171,7 @@ describe("integration watchdog", () => {
});
it("leaves a wearable alone when its owner wears it only now and then", async () => {
await seedDailyReadings(USER, "OURA", [3, 7, 11], 30);
await seedDailyReadings(USER, "OURA", [3, 7, 11], 54);
await recordSyncSuccess(USER, "oura");
const summary = await runIntegrationWatchdog({ mode: "hourly", now: NOW });
expect(summary.silencesFlagged).toBe(0);
@@ -0,0 +1,125 @@
/**
* A guardian reads the managed record's connections through the same
* server-resolved `health` the record's owner would see, and only the
* record's: a connection on the guardian's own account must not appear on
* the record, and the record's connection must answer from the record's rows.
*/
import { beforeEach, describe, expect, it, vi } from "vitest";
import { cookieJar, headerJar } from "./mock-next-headers";
import { getPrismaClient, truncateAllTables, switchSessionTo } from "./setup";
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);
},
})),
};
});
vi.mock("@/lib/db-compat", () => ({
ensureDbCompatibility: vi.fn().mockResolvedValue(undefined),
}));
interface ManagedIntegration {
integration: string;
state: string;
health: { state: string; reason: string } | null;
}
async function readIntegrations(): Promise<{
recordId: string;
integrations: ManagedIntegration[];
}> {
const { GET } = await import("@/app/api/record-settings/integrations/route");
const res = await GET();
expect(res.status).toBe(200);
return (await res.json()).data;
}
beforeEach(async () => {
await truncateAllTables(getPrismaClient());
cookieJar.clear();
headerJar.clear();
});
describe("GET /api/record-settings/integrations", () => {
it("publishes the record's own health per provider, never the guardian's", async () => {
const prisma = getPrismaClient();
const guardian = await prisma.user.create({
data: {
username: "health-guardian",
email: "health-guardian@example.test",
role: "USER",
timezone: "Europe/Berlin",
locale: "en",
onboardingCompletedAt: new Date(),
// The guardian's own Polar: must not surface on the record.
polarAccessTokenEncrypted: "not-a-real-token",
},
});
const { createManagedProfile } =
await import("@/lib/managed-profiles/create");
const { profile } = await createManagedProfile({
creatorId: guardian.id,
displayName: "Managed record",
dateOfBirth: null,
locale: "en",
timezone: "UTC",
gender: null,
});
// The record's own Oura, synced once.
await prisma.user.update({
where: { id: profile.id },
data: { ouraAccessTokenEncrypted: "not-a-real-token" },
});
const { recordSyncSuccess } = await import("@/lib/integrations/status");
await recordSyncSuccess(profile.id, "oura");
const session = await prisma.session.create({
data: {
userId: guardian.id,
expiresAt: new Date(Date.now() + 60_000),
},
});
cookieJar.set("healthlog_session", session.id);
await switchSessionTo(session.id, profile.id);
const data = await readIntegrations();
expect(data.recordId).toBe(profile.id);
expect(data.integrations).toHaveLength(8);
const byKey = new Map(data.integrations.map((i) => [i.integration, i]));
const oura = byKey.get("oura");
expect(oura?.health).not.toBeNull();
expect(oura?.health?.state).toBe("ok");
// Health and the legacy state agree on "a pipe exists".
expect(oura?.state).toBe("connected");
// The guardian's Polar is the guardian's, not the record's.
expect(byKey.get("polar")?.health).toBeNull();
for (const key of [
"withings",
"whoop",
"fitbit",
"nightscout",
"google-health",
"strava",
]) {
expect(byKey.get(key)?.health, key).toBeNull();
}
});
});