Files
HealthLog/tests/integration/google-health-workout-terminal.test.ts
T
Marc-André Bombeck 6f84629482 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
2026-07-29 14:22:49 +02:00

171 lines
4.8 KiB
TypeScript

/**
* 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,
},
],
});
});
});