mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-07 04:12:04 +00:00
`oauth-kit/client.ts` builds a provider's consent URL, code exchange, refresh and deauthorization from a declaration of what differs: endpoints and scope, client authentication (form or HTTP Basic, with or without the client id beside it), the fields a grant adds, how an answer is classified and the refusal named, the error type, and where the token sits in the body. The seven hand-written copies of the redirect URI, authorization URL, token post, Basic header and PKCE pair are gone; each provider keeps an `oauth.ts` with its declaration. Shared behaviour is now uniform: a 2xx without a token object (an empty or non-JSON body) is a transient `empty_token_body` failure for every provider. WHOOP, Strava and Google Health used to return null, and Withings threw an unclassified SyntaxError; the OAuth wire golden records exactly these lines changing and nothing else. Oura's live/legacy endpoint fallback, Google's invalid_grant lift, Strava's Fault summary and Fitbit's errorType are declared per provider.
238 lines
7.5 KiB
TypeScript
238 lines
7.5 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
import type * as WhoopClientModule from "@/lib/whoop/client";
|
|
|
|
import { getPrismaClient, truncateAllTables } from "./setup";
|
|
|
|
process.env.ENCRYPTION_KEY =
|
|
"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
|
|
delete process.env.ENCRYPTION_KEYS;
|
|
delete process.env.ENCRYPTION_ACTIVE_KEY_ID;
|
|
|
|
const { whoopRefresh, withingsRefresh, whoopRecoveries } = vi.hoisted(() => ({
|
|
whoopRefresh: vi.fn(),
|
|
withingsRefresh: vi.fn(),
|
|
whoopRecoveries: vi.fn(),
|
|
}));
|
|
|
|
vi.mock("@/lib/whoop/client", async (importOriginal) => ({
|
|
...(await importOriginal<typeof WhoopClientModule>()),
|
|
fetchRecoveries: whoopRecoveries,
|
|
}));
|
|
|
|
vi.mock("@/lib/whoop/oauth", async (importOriginal) => {
|
|
const actual = await importOriginal<typeof import("@/lib/whoop/oauth")>();
|
|
return {
|
|
...actual,
|
|
whoopOAuth: { ...actual.whoopOAuth, refresh: whoopRefresh },
|
|
};
|
|
});
|
|
|
|
vi.mock("@/lib/withings/oauth", async (importOriginal) => {
|
|
const actual = await importOriginal<typeof import("@/lib/withings/oauth")>();
|
|
return {
|
|
...actual,
|
|
withingsOAuth: { ...actual.withingsOAuth, refresh: withingsRefresh },
|
|
};
|
|
});
|
|
|
|
import { encrypt, decrypt } from "@/lib/crypto";
|
|
import { getValidToken as getValidWhoopToken } from "@/lib/whoop/sync-core";
|
|
import { syncUserRecovery } from "@/lib/whoop/sync-recovery";
|
|
import { getValidToken as getValidWithingsToken } from "@/lib/withings/sync";
|
|
|
|
const CONTENDERS = 75;
|
|
const EXPIRED_AT = new Date("2026-01-01T00:00:00.000Z");
|
|
|
|
async function seedProviderUser(provider: "whoop" | "withings") {
|
|
const prisma = getPrismaClient();
|
|
const userId = `refresh-${provider}`;
|
|
await prisma.user.create({
|
|
data: {
|
|
id: userId,
|
|
username: `refresh-${provider}`,
|
|
email: `refresh-${provider}@example.test`,
|
|
...(provider === "whoop"
|
|
? {
|
|
whoopClientIdEncrypted: encrypt("whoop-client"),
|
|
whoopClientSecretEncrypted: encrypt("whoop-secret"),
|
|
}
|
|
: {
|
|
withingsClientIdEncrypted: encrypt("withings-client"),
|
|
withingsClientSecretEncrypted: encrypt("withings-secret"),
|
|
}),
|
|
},
|
|
});
|
|
|
|
if (provider === "whoop") {
|
|
await prisma.whoopConnection.create({
|
|
data: {
|
|
userId,
|
|
whoopUserId: "whoop-user",
|
|
accessToken: encrypt("stale-access"),
|
|
refreshToken: encrypt("stale-refresh"),
|
|
tokenExpiresAt: EXPIRED_AT,
|
|
},
|
|
});
|
|
} else {
|
|
await prisma.withingsConnection.create({
|
|
data: {
|
|
userId,
|
|
withingsUserId: "withings-user",
|
|
accessToken: encrypt("stale-access"),
|
|
refreshToken: encrypt("stale-refresh"),
|
|
tokenExpiresAt: EXPIRED_AT,
|
|
},
|
|
});
|
|
}
|
|
|
|
return userId;
|
|
}
|
|
|
|
async function runContenders(provider: "whoop" | "withings", userId: string) {
|
|
const getToken =
|
|
provider === "whoop" ? getValidWhoopToken : getValidWithingsToken;
|
|
return Promise.all(
|
|
Array.from({ length: CONTENDERS }, () => getToken(userId)),
|
|
);
|
|
}
|
|
|
|
beforeEach(async () => {
|
|
await truncateAllTables(getPrismaClient());
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
describe.each([
|
|
["whoop", whoopRefresh],
|
|
["withings", withingsRefresh],
|
|
] as const)(
|
|
"%s rotating token refresh serialization (real Postgres)",
|
|
(provider, refresh) => {
|
|
it("lets exactly one contender refresh a stale token and shares the winner", async () => {
|
|
const userId = await seedProviderUser(provider);
|
|
refresh.mockImplementation(async (refreshToken: string) => {
|
|
expect(refreshToken).toBe("stale-refresh");
|
|
return {
|
|
access_token: "winner-access",
|
|
refresh_token: "winner-refresh",
|
|
expires_in: 3600,
|
|
};
|
|
});
|
|
|
|
const results = await runContenders(provider, userId);
|
|
|
|
expect(refresh).toHaveBeenCalledTimes(1);
|
|
expect(results).toHaveLength(CONTENDERS);
|
|
expect(
|
|
results.every((result) => result?.accessToken === "winner-access"),
|
|
).toBe(true);
|
|
|
|
const prisma = getPrismaClient();
|
|
const row =
|
|
provider === "whoop"
|
|
? await prisma.whoopConnection.findUniqueOrThrow({
|
|
where: { userId },
|
|
})
|
|
: await prisma.withingsConnection.findUniqueOrThrow({
|
|
where: { userId },
|
|
});
|
|
expect(decrypt(row.accessToken)).toBe("winner-access");
|
|
expect(decrypt(row.refreshToken)).toBe("winner-refresh");
|
|
});
|
|
|
|
it("rolls back a failed winner so a later contender can retry", async () => {
|
|
const userId = await seedProviderUser(provider);
|
|
refresh
|
|
.mockRejectedValueOnce(new Error("injected refresh failure"))
|
|
.mockResolvedValue({
|
|
access_token: "retry-access",
|
|
refresh_token: "retry-refresh",
|
|
expires_in: 3600,
|
|
});
|
|
|
|
const results = await runContenders(provider, userId);
|
|
|
|
expect(refresh).toHaveBeenCalledTimes(2);
|
|
expect(results.filter((result) => result === null)).toHaveLength(1);
|
|
expect(
|
|
results.filter((result) => result?.accessToken === "retry-access"),
|
|
).toHaveLength(CONTENDERS - 1);
|
|
|
|
const prisma = getPrismaClient();
|
|
const row =
|
|
provider === "whoop"
|
|
? await prisma.whoopConnection.findUniqueOrThrow({
|
|
where: { userId },
|
|
})
|
|
: await prisma.withingsConnection.findUniqueOrThrow({
|
|
where: { userId },
|
|
});
|
|
expect(decrypt(row.accessToken)).toBe("retry-access");
|
|
expect(decrypt(row.refreshToken)).toBe("retry-refresh");
|
|
});
|
|
},
|
|
);
|
|
|
|
describe("WHOOP recovery cursor durability (real Postgres)", () => {
|
|
const oldCursor = "2026-06-01T00:00:00.000Z";
|
|
const recovery = {
|
|
cycle_id: 1,
|
|
sleep_id: "sleep-real",
|
|
user_id: 42,
|
|
created_at: "2026-06-01T06:00:00.000Z",
|
|
updated_at: "2026-06-01T07:00:00.000Z",
|
|
score_state: "SCORED",
|
|
score: {
|
|
user_calibrating: false,
|
|
recovery_score: 66,
|
|
resting_heart_rate: 52,
|
|
hrv_rmssd_milli: 48.7,
|
|
spo2_percentage: 97,
|
|
skin_temp_celsius: 33.4,
|
|
},
|
|
};
|
|
|
|
it("holds the cursor on a durable write failure, then advances idempotently", async () => {
|
|
const userId = await seedProviderUser("whoop");
|
|
const prisma = getPrismaClient();
|
|
await prisma.whoopConnection.update({
|
|
where: { userId },
|
|
data: {
|
|
tokenExpiresAt: new Date(Date.now() + 60 * 60 * 1000),
|
|
lastSyncedAt: new Date(oldCursor),
|
|
resourceCursors: { recovery: oldCursor },
|
|
},
|
|
});
|
|
whoopRecoveries.mockResolvedValue([recovery]);
|
|
|
|
await prisma.$executeRaw`
|
|
ALTER TABLE "measurements"
|
|
ADD CONSTRAINT "test_whoop_recovery_write_failure"
|
|
CHECK ("external_id" <> 'sleep-real:hrv_rmssd')
|
|
`;
|
|
|
|
try {
|
|
await expect(syncUserRecovery(userId)).rejects.toThrow();
|
|
const failed = await prisma.whoopConnection.findUniqueOrThrow({
|
|
where: { userId },
|
|
});
|
|
expect(failed.resourceCursors).toEqual({ recovery: oldCursor });
|
|
expect(await prisma.measurement.count({ where: { userId } })).toBe(0);
|
|
} finally {
|
|
await prisma.$executeRaw`
|
|
ALTER TABLE "measurements"
|
|
DROP CONSTRAINT IF EXISTS "test_whoop_recovery_write_failure"
|
|
`;
|
|
}
|
|
|
|
expect(await syncUserRecovery(userId)).toBe(5);
|
|
const succeeded = await prisma.whoopConnection.findUniqueOrThrow({
|
|
where: { userId },
|
|
});
|
|
expect(succeeded.resourceCursors).not.toEqual({ recovery: oldCursor });
|
|
expect(await prisma.measurement.count({ where: { userId } })).toBe(5);
|
|
|
|
expect(await syncUserRecovery(userId)).toBe(5);
|
|
expect(await prisma.measurement.count({ where: { userId } })).toBe(5);
|
|
});
|
|
});
|