Files
HealthLog/src/lib/fitbit/__tests__/sync-workout-insert-identity.test.ts
T

157 lines
4.3 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from "vitest";
const {
createManyAndReturn,
findUnique,
update,
emitInsertedWorkoutArrival,
fetchActivityList,
getValidToken,
mapWorkout,
readActivityList,
} = vi.hoisted(() => ({
findUnique: vi.fn(),
createManyAndReturn: vi.fn(),
update: vi.fn(),
emitInsertedWorkoutArrival: vi.fn(async () => {}),
fetchActivityList: vi.fn(),
getValidToken: vi.fn(),
mapWorkout: vi.fn(),
readActivityList: vi.fn(),
}));
vi.mock("@/lib/db", () => ({
prisma: { workout: { createManyAndReturn, findUnique, update } },
toJson: <T>(value: T) => value,
}));
vi.mock("@/lib/arrivals/workout-emit", () => ({
emitInsertedWorkoutArrival,
}));
vi.mock("@/lib/logging/context", () => ({
annotate: vi.fn(),
getEvent: () => ({ addWarning: vi.fn() }),
}));
vi.mock("@/lib/tz/resolver", () => ({
resolveUserTimezone: vi.fn(async () => "UTC"),
}));
vi.mock("../client", () => ({
fetchActivityList,
mapWorkout,
readActivityList,
}));
vi.mock("../sync-core", () => ({
getValidToken,
handleCollectionFetchError: vi.fn(),
}));
import { syncUserWorkout } from "../sync-workout";
const startedAt = new Date("2026-07-19T08:00:00.000Z");
const mapped = {
externalId: "91001",
sportType: "running",
startedAt,
endedAt: new Date("2026-07-19T09:00:00.000Z"),
durationSec: 3600,
totalEnergyKcal: 500,
totalDistanceM: 10_000,
avgHeartRate: 145,
maxHeartRate: 170,
minHeartRate: 90,
metadata: {
fitbitActivityName: "Run",
fitbitActivityTypeId: 9001,
},
};
beforeEach(() => {
vi.clearAllMocks();
getValidToken.mockResolvedValue({ accessToken: "token" });
fetchActivityList.mockResolvedValue({});
readActivityList.mockReturnValue([{}]);
mapWorkout.mockReturnValue(mapped);
findUnique.mockResolvedValue({ metadata: {} });
update.mockResolvedValue({ id: "existing" });
});
describe("syncUserWorkout — exact inserted identity", () => {
it("emits the exact row returned by the insert statement", async () => {
const inserted = { id: "new-workout", startedAt };
createManyAndReturn.mockResolvedValue([inserted]);
await expect(syncUserWorkout("user-1")).resolves.toBe(1);
expect(emitInsertedWorkoutArrival).toHaveBeenCalledWith(
"user-1",
inserted,
"fitbit",
);
expect(update).not.toHaveBeenCalled();
expect(createManyAndReturn).toHaveBeenCalledWith(
expect.objectContaining({
data: expect.objectContaining({
metadata: {
fitbitActivityName: "Run",
fitbitActivityTypeId: 9001,
},
}),
}),
);
});
it("updates and counts an existing workout without emitting", async () => {
createManyAndReturn.mockResolvedValue([]);
findUnique.mockResolvedValue({
metadata: {
retainedField: "retained-value",
fitbitActivityName: "Previous Activity",
},
});
await expect(syncUserWorkout("user-1")).resolves.toBe(1);
expect(update).toHaveBeenCalledWith(
expect.objectContaining({
where: {
userId_source_externalId: {
userId: "user-1",
source: "FITBIT",
externalId: "91001",
},
},
data: expect.objectContaining({
sportType: "running",
startedAt,
totalDistanceM: 10_000,
metadata: {
retainedField: "retained-value",
fitbitActivityName: "Run",
fitbitActivityTypeId: 9001,
},
}),
}),
);
expect(emitInsertedWorkoutArrival).not.toHaveBeenCalled();
});
it("treats a duplicate-race short return as an update, never an insert", async () => {
createManyAndReturn.mockResolvedValue([]);
update.mockResolvedValue({ id: "concurrent-winner" });
await expect(syncUserWorkout("user-1")).resolves.toBe(1);
expect(createManyAndReturn).toHaveBeenCalledWith(
expect.objectContaining({ skipDuplicates: true }),
);
expect(update).toHaveBeenCalledTimes(1);
expect(emitInsertedWorkoutArrival).not.toHaveBeenCalled();
});
it("keeps a successful insert when arrival dispatch fails", async () => {
createManyAndReturn.mockResolvedValue([{ id: "new-workout", startedAt }]);
emitInsertedWorkoutArrival.mockRejectedValueOnce(new Error("queue down"));
await expect(syncUserWorkout("user-1")).resolves.toBe(1);
});
});