fix(oura): ask for cycle phases over the user's own days, like the other collections

This commit is contained in:
Marc-André Bombeck committed 2026-09-27 01:01:29 +02:00
1 parent 3cbfe7f7f9
commit ab31c9d89c
4 files changed
+42 -22

No files matched your search

+12 -10
View File
@@ -35,6 +35,8 @@ vi.mock("../client", async (importOriginal) => {
import { syncUserOuraCyclePhases } from "../cycle-sync";
const WINDOW = { startDate: "2026-09-20", endDate: "2026-09-27" };
function uniqueViolation() {
const err = new Error("Unique constraint failed") as Error & {
code: string;
@@ -52,7 +54,7 @@ beforeEach(() => {
describe("syncUserOuraCyclePhases", () => {
it("no-ops without touching the DB when the collection is empty", async () => {
const created = await syncUserOuraCyclePhases("u1", "tok", 7);
const created = await syncUserOuraCyclePhases("u1", "tok", WINDOW);
expect(created).toBe(0);
expect(isCycleAvailableMock).not.toHaveBeenCalled();
expect(createMock).not.toHaveBeenCalled();
@@ -63,7 +65,7 @@ describe("syncUserOuraCyclePhases", () => {
{ day: "2026-06-08", phase: "luteal" },
{ day: "2026-06-09", phase: "luteal" },
]);
const created = await syncUserOuraCyclePhases("u1", "tok", 7);
const created = await syncUserOuraCyclePhases("u1", "tok", WINDOW);
expect(created).toBe(0);
expect(isCycleAvailableMock).not.toHaveBeenCalled();
});
@@ -73,7 +75,7 @@ describe("syncUserOuraCyclePhases", () => {
{ day: "2026-06-05", phase: "menstrual" },
]);
isCycleAvailableMock.mockResolvedValue(false);
const created = await syncUserOuraCyclePhases("u1", "tok", 7);
const created = await syncUserOuraCyclePhases("u1", "tok", WINDOW);
expect(created).toBe(0);
expect(createMock).not.toHaveBeenCalled();
});
@@ -82,7 +84,7 @@ describe("syncUserOuraCyclePhases", () => {
fetchCyclePhasesMock.mockResolvedValue([
{ day: "2026-06-05", phase: "menstrual" },
]);
const created = await syncUserOuraCyclePhases("u1", "tok", 7);
const created = await syncUserOuraCyclePhases("u1", "tok", WINDOW);
expect(created).toBe(1);
expect(createMock).toHaveBeenCalledWith({
data: {
@@ -100,7 +102,7 @@ describe("syncUserOuraCyclePhases", () => {
{ day: "2026-06-05", phase: "menstrual" },
]);
createMock.mockRejectedValue(uniqueViolation());
const created = await syncUserOuraCyclePhases("u1", "tok", 7);
const created = await syncUserOuraCyclePhases("u1", "tok", WINDOW);
expect(created).toBe(0);
});
@@ -112,7 +114,7 @@ describe("syncUserOuraCyclePhases", () => {
createMock
.mockRejectedValueOnce(uniqueViolation()) // 06-05 already logged (manual)
.mockResolvedValueOnce({ id: "cdl-2" }); // 06-06 genuinely empty
const created = await syncUserOuraCyclePhases("u1", "tok", 7);
const created = await syncUserOuraCyclePhases("u1", "tok", WINDOW);
expect(created).toBe(1);
expect(createMock).toHaveBeenCalledTimes(2);
});
@@ -125,7 +127,7 @@ describe("syncUserOuraCyclePhases", () => {
createMock
.mockRejectedValueOnce(new Error("connection reset"))
.mockResolvedValueOnce({ id: "cdl-2" });
const created = await syncUserOuraCyclePhases("u1", "tok", 7);
const created = await syncUserOuraCyclePhases("u1", "tok", WINDOW);
// The failing day is skipped (warned, not thrown); the healthy sibling
// still writes.
expect(created).toBe(1);
@@ -133,7 +135,7 @@ describe("syncUserOuraCyclePhases", () => {
it("propagates the Oura fetch error to the caller (the sync layer decides how to swallow it)", async () => {
fetchCyclePhasesMock.mockRejectedValue(new Error("403 forbidden"));
await expect(syncUserOuraCyclePhases("u1", "tok", 7)).rejects.toThrow(
await expect(syncUserOuraCyclePhases("u1", "tok", WINDOW)).rejects.toThrow(
"403 forbidden",
);
expect(createMock).not.toHaveBeenCalled();
@@ -144,7 +146,7 @@ describe("syncUserOuraCyclePhases", () => {
{ day: "2026-06-05", phase: "menstrual" },
]);
findUserMock.mockResolvedValue(null);
await syncUserOuraCyclePhases("u1", "tok", 7);
await syncUserOuraCyclePhases("u1", "tok", WINDOW);
expect(createMock).toHaveBeenCalledWith(
expect.objectContaining({ data: expect.objectContaining({ tz: null }) }),
);
@@ -155,7 +157,7 @@ describe("syncUserOuraCyclePhases", () => {
{ day: "2026-06-08", phase: "luteal" },
{ day: "2026-06-09", phase: "follicular" },
]);
const created = await syncUserOuraCyclePhases("u1", "tok", 7);
const created = await syncUserOuraCyclePhases("u1", "tok", WINDOW);
expect(created).toBe(1);
expect(createMock).toHaveBeenCalledWith(
expect.objectContaining({
+17 -1
View File
@@ -1,4 +1,12 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import {
afterEach,
beforeEach,
describe,
expect,
it,
onTestFinished,
vi,
} from "vitest";
const {
getConnMock,
@@ -842,6 +850,14 @@ describe("syncUserOura — window resolution", () => {
});
it("passes the resolved window to the cycle-phases leg too", async () => {
// Pinned where the user's date and the UTC date differ (01:30 in Berlin,
// 23:30 UTC the day before), so a leg that cuts its window on UTC days
// asks Oura for a different range and the assertion below catches it.
vi.useFakeTimers({ toFake: ["Date"] });
vi.setSystemTime(new Date("2026-09-26T23:30:00Z"));
onTestFinished(() => {
vi.useRealTimers();
});
getConnMock.mockResolvedValue(CONN);
findFirstMock.mockResolvedValue({
measuredAt: new Date(Date.now() - 10 * DAY_MS),
+6 -7
View File
@@ -75,14 +75,13 @@ function isUniqueViolation(err: unknown): boolean {
export async function syncUserOuraCyclePhases(
userId: string,
accessToken: string,
lookbackDays: number,
window: { startDate: string; endDate: string },
): Promise<number> {
const now = new Date();
const start = new Date(now.getTime() - lookbackDays * 24 * 60 * 60 * 1000);
const records = await fetchDailyCyclePhases(accessToken, {
startDate: start.toISOString().slice(0, 10),
endDate: now.toISOString().slice(0, 10),
});
// The window is the measurement sync's own, cut on the user's calendar days.
// Deriving it here from UTC dates made the two legs ask Oura for different
// days whenever the user's date and the UTC date differ (every evening west
// of UTC, every early morning east of it).
const records = await fetchDailyCyclePhases(accessToken, window);
if (records.length === 0) return 0;
const periodDays = derivePeriodDaysFromCyclePhases(records);
+7 -4
View File
@@ -509,11 +509,14 @@ export async function syncUserOura(
try {
const freshConn = await getOuraConnection(userId);
if (freshConn) {
await syncUserOuraCyclePhases(
userId,
freshConn.accessToken,
lookbackDays,
const cycleToday = userDayKey(
new Date(),
await resolveUserTimezone(userId),
);
await syncUserOuraCyclePhases(userId, freshConn.accessToken, {
startDate: shiftDateKey(cycleToday, -lookbackDays),
endDate: cycleToday,
});
}
} catch (err) {
getEvent()?.addWarning(