test(01-13): add red Google sync terminal contracts

- cover authenticated status polling and bounded resource outcomes
- prove committed workouts invalidate server and client cache surfaces
This commit is contained in:
Marc-André Bombeck committed 2026-07-29 14:22:49 +02:00
1 parent b8766fa93b
commit 6f84629482
4 files changed
+442

No files matched your search

@@ -99,4 +99,111 @@ describe("POST /api/google-health/sync", () => {
fullSync: false,
});
});
it("preserves bounded per-resource terminal outcomes for the manual-sync client", async () => {
vi.mocked(syncUserGoogleHealth).mockResolvedValue({
runId: "run-terminal-1",
state: "partial",
imported: 3,
failed: true,
resources: [
{
resource: "workout",
pages: 1,
fetched: 2,
mapped: 2,
written: 2,
status: "complete",
durationMs: 18,
truncated: false,
reasonCode: null,
},
{
resource: "dense-heart-rate",
pages: 786,
fetched: 1,
mapped: 1,
written: 1,
status: "partial",
durationMs: 323_849,
truncated: false,
reasonCode: "collection_failed",
},
],
} as never);
const response = await POST(request());
const envelope = (await response.json()) as {
data: Record<string, unknown>;
error: string | null;
};
expect(response.status).toBe(200);
expect(envelope.error).toBeNull();
expect(envelope.data).toEqual({
runId: "run-terminal-1",
state: "partial",
imported: 3,
failed: true,
outcome: "partial",
fullSync: false,
resources: [
{
resource: "workout",
pages: 1,
fetched: 2,
mapped: 2,
written: 2,
status: "complete",
durationMs: 18,
truncated: false,
reasonCode: null,
},
{
resource: "dense-heart-rate",
pages: 786,
fetched: 1,
mapped: 1,
written: 1,
status: "partial",
durationMs: 323_849,
truncated: false,
reasonCode: "collection_failed",
},
],
});
});
it("does not serialize provider payloads, URLs, tokens, or health values", async () => {
vi.mocked(syncUserGoogleHealth).mockResolvedValue({
runId: "run-private",
state: "failed",
imported: 1,
failed: true,
resources: [
{
resource: "workout",
pages: 1,
fetched: 2,
mapped: 2,
written: 1,
status: "partial",
durationMs: 20,
truncated: false,
reasonCode: "upsert_failed",
accessToken: "raw-access-token",
url: "https://health.googleapis.com/private",
rawError: "provider response body",
samples: [{ bpm: 181 }],
},
],
} as never);
const response = await POST(request());
const serialized = JSON.stringify(await response.json());
expect(serialized).not.toMatch(
/raw-access-token|health\.googleapis\.com|provider response body|samples|bpm|181/,
);
});
});
@@ -0,0 +1,113 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { NextRequest } from "next/server";
import type * as ApiHandlerModule from "@/lib/api-handler";
const { readGoogleHealthSyncProgress } = vi.hoisted(() => ({
readGoogleHealthSyncProgress: vi.fn(),
}));
vi.mock("@/lib/api-handler", async () => {
const actual =
await vi.importActual<typeof ApiHandlerModule>("@/lib/api-handler");
return {
...actual,
requireAuth: vi.fn(async () => ({ user: { id: "subject-user" } })),
};
});
vi.mock("@/lib/logging/transports", () => ({ emitIfSampled: vi.fn() }));
vi.mock("@/lib/google-health/sync-progress", () => ({
readGoogleHealthSyncProgress,
}));
import { HttpError, requireAuth } from "@/lib/api-handler";
async function getStatus(request: NextRequest) {
const route = await import("../route");
return route.GET(request);
}
beforeEach(() => {
vi.clearAllMocks();
vi.mocked(requireAuth).mockResolvedValue({
user: { id: "subject-user" },
} as never);
});
describe("GET /api/google-health/sync/status", () => {
it("reads only the authenticated subject despite forged enumeration parameters", async () => {
readGoogleHealthSyncProgress.mockResolvedValue({
runId: "run-current",
state: "in_progress",
startedAt: "2026-07-29T10:00:00.000Z",
updatedAt: "2026-07-29T10:00:05.000Z",
resources: [],
});
const response = await getStatus(
new NextRequest(
"http://localhost/api/google-health/sync/status?userId=other-user&all=true",
),
);
const serialized = JSON.stringify(await response.json());
expect(response.status).toBe(200);
expect(readGoogleHealthSyncProgress).toHaveBeenCalledTimes(1);
expect(readGoogleHealthSyncProgress).toHaveBeenCalledWith("subject-user");
expect(serialized).not.toContain("other-user");
});
it("rejects an unauthenticated read without touching progress storage", async () => {
vi.mocked(requireAuth).mockRejectedValueOnce(
new HttpError(401, "Not authenticated"),
);
const response = await getStatus(
new NextRequest("http://localhost/api/google-health/sync/status"),
);
expect(response.status).toBe(401);
expect(readGoogleHealthSyncProgress).not.toHaveBeenCalled();
});
it("returns one bounded current-run envelope, never an owner list or private payload", async () => {
readGoogleHealthSyncProgress.mockResolvedValue({
runId: "run-private",
state: "partial",
startedAt: "2026-07-29T10:00:00.000Z",
updatedAt: "2026-07-29T10:05:00.000Z",
terminalAt: "2026-07-29T10:05:00.000Z",
resources: [
{
resource: "workout",
pages: 1,
fetched: 1,
mapped: 1,
written: 1,
status: "complete",
durationMs: 25,
truncated: false,
reasonCode: null,
accessToken: "raw-access-token",
rawError: "provider response body",
url: "https://provider.invalid/private",
samples: [{ bpm: 181 }],
},
],
owners: ["subject-user", "other-user"],
});
const response = await getStatus(
new NextRequest("http://localhost/api/google-health/sync/status"),
);
const body = await response.json();
const serialized = JSON.stringify(body);
expect(response.status).toBe(200);
expect(body.data).not.toBeInstanceOf(Array);
expect(serialized).not.toMatch(
/raw-access-token|provider response body|provider\.invalid|samples|bpm|181|owners|other-user/,
);
});
});
@@ -0,0 +1,52 @@
import { readFileSync } from "node:fs";
import { resolve } from "node:path";
import { describe, expect, it } from "vitest";
const source = readFileSync(
resolve(
process.cwd(),
"src/components/settings/integrations/google-health-card.tsx",
),
"utf8",
);
describe("GoogleHealthCard manual-sync terminal contract", () => {
it("polls the authenticated status route when POST completion is uncertain", () => {
expect(source).toContain('"/api/google-health/sync/status"');
expect(source).toMatch(
/catch[\s\S]{0,1200}(poll|status)|timeout[\s\S]{0,1200}(poll|status)/i,
);
expect(source).toMatch(/complete/);
expect(source).toMatch(/partial/);
expect(source).toMatch(/failed/);
expect(source).toMatch(/interrupted/);
});
it("invalidates every affected client query after a committed workout outcome", () => {
expect(source).toContain(
"queryClient.invalidateQueries({ queryKey: queryKeys.workouts() })",
);
expect(source).toContain(
"queryClient.invalidateQueries({ queryKey: queryKeys.dashboardSnapshot() })",
);
expect(source).toContain(
"queryClient.invalidateQueries({ queryKey: queryKeys.analytics() })",
);
expect(source).toContain(
"queryClient.invalidateQueries({ queryKey: queryKeys.googleHealth() })",
);
expect(source).toContain("queryKey: queryKeys.integrationsStatus()");
expect(source).toMatch(
/resource\s*===?\s*["']workout["'][\s\S]{0,500}written\s*>\s*0|written\s*>\s*0[\s\S]{0,500}resource\s*===?\s*["']workout["']/,
);
});
it("does not send a caller-selected owner or use ad-hoc query-key arrays", () => {
expect(source).not.toMatch(
/sync\/status[^"']*(?:userId|ownerId)|body:\s*JSON\.stringify\(\{[^}]*userId/,
);
expect(source).not.toMatch(
/invalidateQueries\(\s*\{\s*queryKey:\s*\[[^\]]+\]/,
);
});
});
@@ -0,0 +1,170 @@
/**
* RED real-PostgreSQL contract: a committed Google workout must be visible
* through the existing server caches before the sync reports completion.
*/
import { beforeEach, describe, expect, it, vi } from "vitest";
import { getPrismaClient, truncateAllTables } from "./setup";
const mocks = vi.hoisted(() => ({
fetchDataPoints: vi.fn(),
mapWorkout: vi.fn(),
getValidToken: vi.fn(),
handleCollectionFetchError: vi.fn(),
noteHardFailure: vi.fn(),
}));
vi.mock("@/lib/google-health/client", () => ({
GOOGLE_HEALTH_ACTIVITY_PAGE_SIZE: 25,
GOOGLE_HEALTH_DATA_TYPES: {
exercise: { key: "exercise", dataType: "exercise" },
},
fetchDataPoints: mocks.fetchDataPoints,
mapWorkout: mocks.mapWorkout,
}));
vi.mock("@/lib/google-health/sync-core", () => ({
getValidToken: mocks.getValidToken,
handleCollectionFetchError: mocks.handleCollectionFetchError,
noteHardFailure: mocks.noteHardFailure,
}));
vi.mock("@/lib/arrivals/workout-emit", () => ({
emitInsertedWorkoutArrival: vi.fn(async () => undefined),
}));
vi.mock("@/lib/tz/resolver", () => ({
resolveUserTimezone: vi.fn(async () => "Europe/Berlin"),
}));
import {
__resetAllCachesForTests,
cached,
caches,
} from "@/lib/cache/server-cache";
import { syncUserWorkout } from "@/lib/google-health/sync-workout";
import { readWorkoutsListCached } from "@/lib/workouts/list-read";
const USER_ID = "google-workout-owner";
const LIST_PARAMS = {
limit: 20,
offset: 0,
since: null,
until: null,
sportType: null,
} as const;
async function seedUser(): Promise<void> {
await getPrismaClient().user.create({
data: {
id: USER_ID,
username: USER_ID,
email: `${USER_ID}@example.test`,
},
});
}
function mappedWorkout(sportType = "running") {
return {
externalId: "google-exercise-1",
sportType,
sportTypeRaw: "RUNNING",
startedAt: new Date("2026-07-29T08:00:00.000Z"),
endedAt: new Date("2026-07-29T08:30:00.000Z"),
durationSec: 1800,
totalEnergyKcal: 280,
totalDistanceM: 5_000,
avgHeartRate: 151,
maxHeartRate: 178,
minHeartRate: 104,
};
}
beforeEach(async () => {
await truncateAllTables(getPrismaClient());
__resetAllCachesForTests();
vi.clearAllMocks();
mocks.getValidToken.mockResolvedValue({
accessToken: "test-token",
connection: { id: "connection-1", googleUserId: "google-owner" },
});
mocks.fetchDataPoints.mockResolvedValue([{ name: "exercise-1" }]);
mocks.mapWorkout.mockReturnValue(mappedWorkout());
await seedUser();
});
describe("Google Health workout terminal cache visibility (real Postgres)", () => {
it("exposes a newly committed workout immediately and invalidates dependent server cells", async () => {
expect(
(await readWorkoutsListCached(USER_ID, LIST_PARAMS)).workouts,
).toEqual([]);
await cached(caches.analytics, `${USER_ID}|default`, async () => ({
marker: "analytics-before",
}));
await cached(
caches.analytics,
`${USER_ID}|dashboard-snapshot|en`,
async () => ({ marker: "dashboard-before" }),
);
await expect(syncUserWorkout(USER_ID)).resolves.toBe(1);
const row = await getPrismaClient().workout.findUnique({
where: {
userId_source_externalId: {
userId: USER_ID,
source: "GOOGLE_HEALTH",
externalId: "google-exercise-1",
},
},
});
expect(row).toMatchObject({ sportType: "running", durationSec: 1800 });
await expect(
readWorkoutsListCached(USER_ID, LIST_PARAMS),
).resolves.toMatchObject({
workouts: [
{
externalId: "google-exercise-1",
sportType: "running",
source: "GOOGLE_HEALTH",
},
],
});
expect(caches.analytics.get(`${USER_ID}|default`)).toBeNull();
expect(caches.analytics.get(`${USER_ID}|dashboard-snapshot|en`)).toBeNull();
});
it("exposes an updated committed workout instead of the primed pre-sync projection", async () => {
await getPrismaClient().workout.create({
data: {
userId: USER_ID,
source: "GOOGLE_HEALTH",
externalId: "google-exercise-1",
sportType: "walking",
startedAt: new Date("2026-07-29T08:00:00.000Z"),
endedAt: new Date("2026-07-29T08:20:00.000Z"),
durationSec: 1200,
},
});
const primed = await readWorkoutsListCached(USER_ID, LIST_PARAMS);
expect(primed.workouts[0]).toMatchObject({
sportType: "walking",
durationSec: 1200,
});
mocks.mapWorkout.mockReturnValue(mappedWorkout("running"));
await expect(syncUserWorkout(USER_ID)).resolves.toBe(1);
await expect(
readWorkoutsListCached(USER_ID, LIST_PARAMS),
).resolves.toMatchObject({
workouts: [
{
externalId: "google-exercise-1",
sportType: "running",
durationSec: 1800,
},
],
});
});
});