mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-14 14:40:52 +00:00
Six provider syncs returned a bare count, which narrowed their failures away: a Nightscout window in which every SGV row was refused by a constraint returned the same 0 as a quiet night, and an Oura or Polar run whose collections all failed returned 0 too. The routes then answered 200 with a count and the settings card drew a green tick over it. Every sync now reports what it imported AND whether any part of it did not land, and the route resolves the outcome server-side so the web card and the native client cannot disagree. Google Health keeps its 502 for a run that failed and wrote nothing, but a run that failed after writing some resources is now a partial rather than a discarded result. The labs OCR commit carries the same resolved outcome.
105 lines
3.4 KiB
TypeScript
105 lines
3.4 KiB
TypeScript
/**
|
|
* v1.32.28 — manual Strava sync. Incremental by nature (Strava carries a real
|
|
* cursor); no full-history arm is exposed here, that stays with the backfill.
|
|
*/
|
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
import { NextRequest } from "next/server";
|
|
import type * as ApiHandlerModule from "@/lib/api-handler";
|
|
|
|
vi.mock("@/lib/api-handler", async () => {
|
|
const actual =
|
|
await vi.importActual<typeof ApiHandlerModule>("@/lib/api-handler");
|
|
return {
|
|
...actual,
|
|
requireAuth: vi.fn(async () => ({ user: { id: "u1" } })),
|
|
};
|
|
});
|
|
|
|
vi.mock("@/lib/logging/transports", () => ({ emitIfSampled: vi.fn() }));
|
|
|
|
vi.mock("@/lib/rate-limit", () => ({
|
|
checkRateLimit: vi.fn(async () => ({ allowed: true })),
|
|
}));
|
|
|
|
vi.mock("@/lib/strava/sync", () => ({ syncUserStrava: vi.fn() }));
|
|
|
|
import { POST } from "../route";
|
|
import { HttpError, requireAuth } from "@/lib/api-handler";
|
|
import { checkRateLimit } from "@/lib/rate-limit";
|
|
import { syncUserStrava } from "@/lib/strava/sync";
|
|
|
|
function request(): NextRequest {
|
|
return new NextRequest("http://localhost/api/strava/sync", {
|
|
method: "POST",
|
|
});
|
|
}
|
|
|
|
// The route takes no arguments (no body, no flags); the alias keeps the
|
|
// direct invoke typed while still handing apiHandler a real request.
|
|
const post = POST as unknown as (r: NextRequest) => Promise<Response>;
|
|
|
|
async function envelope(response: Response) {
|
|
return (await response.json()) as {
|
|
data: { imported?: number } | null;
|
|
error: string | null;
|
|
meta?: { errorCode?: string };
|
|
};
|
|
}
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
vi.mocked(checkRateLimit).mockResolvedValue({ allowed: true } as never);
|
|
vi.mocked(requireAuth).mockResolvedValue({ user: { id: "u1" } } as never);
|
|
});
|
|
|
|
describe("POST /api/strava/sync", () => {
|
|
it("syncs the session user and answers with the imported count", async () => {
|
|
vi.mocked(syncUserStrava).mockResolvedValue(4);
|
|
|
|
const response = await post(request());
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(await envelope(response)).toEqual({
|
|
data: { imported: 4, failed: false, outcome: "success" },
|
|
error: null,
|
|
});
|
|
expect(syncUserStrava).toHaveBeenCalledWith("u1");
|
|
});
|
|
|
|
it("refuses an unauthenticated caller before syncing anything", async () => {
|
|
vi.mocked(requireAuth).mockRejectedValue(
|
|
new HttpError(401, "Not authenticated"),
|
|
);
|
|
|
|
const response = await post(request());
|
|
|
|
expect(response.status).toBe(401);
|
|
expect(syncUserStrava).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("refuses past the per-user rate limit without touching the sync", async () => {
|
|
vi.mocked(checkRateLimit).mockResolvedValue({ allowed: false } as never);
|
|
|
|
const response = await post(request());
|
|
const body = await envelope(response);
|
|
|
|
expect(response.status).toBe(429);
|
|
expect(body.meta?.errorCode).toBe("rate_limited_self");
|
|
expect(syncUserStrava).not.toHaveBeenCalled();
|
|
expect(checkRateLimit).toHaveBeenCalledWith("strava-sync:u1", 5, 60_000);
|
|
});
|
|
|
|
it("answers 502 generically when the sync throws", async () => {
|
|
vi.mocked(syncUserStrava).mockRejectedValue(
|
|
new Error("strava said: token abc123 is invalid"),
|
|
);
|
|
|
|
const response = await post(request());
|
|
const body = await envelope(response);
|
|
|
|
expect(response.status).toBe(502);
|
|
expect(body.error).toBe("Strava sync failed");
|
|
expect(JSON.stringify(body)).not.toContain("abc123");
|
|
});
|
|
});
|