mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-13 22:32:04 +00:00
157 lines
4.3 KiB
TypeScript
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);
|
|
});
|
|
});
|