Files
HealthLog/src/lib/integrations/__tests__/oauth-refresh.test.ts
T
Marc-André Bombeck 0f5e563403 fix(integrations): make rotating-refresh persistence race-safe
Fitbit, WHOOP, and Oura all issue one-time-use refresh tokens. The token
persistence did an unguarded read → refresh → write, so two overlapping
syncs for one user both read the same stored refresh token and both POST
it. The provider honours only the first; the loser's refresh comes back 401,
gets classified as reauth, and the connection is parked at `error_reauth`
spuriously even though the peer rotated the token successfully.

Add a shared `persistRotatedToken` helper that writes the rotated pair with
a compare-and-swap on the stored ciphertext (matched verbatim, since AES-GCM
re-encryption of the same plaintext differs byte-for-byte). When the
conditional update touches zero rows a peer already won the race; re-read the
row and reuse the peer's freshly rotated access token instead of erroring.

Wire it into all three providers — Fitbit and WHOOP `getValidToken`
(proactive expiry refresh, keyed by connection id) and Oura `storeOuraTokens`
(reactive 401 refresh, keyed by user id). Both racing callers now end with a
usable token and neither drops the connection.
2026-06-22 08:16:04 +02:00

42 lines
1.3 KiB
TypeScript

import { describe, expect, it, vi } from "vitest";
import { persistRotatedToken } from "../oauth-refresh";
describe("persistRotatedToken", () => {
it("returns the freshly minted access token when the CAS write wins (1 row)", async () => {
const conditionalUpdate = vi.fn(async () => 1);
const readPeerAccessToken = vi.fn(async () => "peer");
const token = await persistRotatedToken("mine", {
conditionalUpdate,
readPeerAccessToken,
});
expect(token).toBe("mine");
expect(conditionalUpdate).toHaveBeenCalledTimes(1);
// No re-read on a win.
expect(readPeerAccessToken).not.toHaveBeenCalled();
});
it("reuses the peer's rotated token on a lost race (0 rows)", async () => {
const conditionalUpdate = vi.fn(async () => 0);
const readPeerAccessToken = vi.fn(async () => "peer");
const token = await persistRotatedToken("mine", {
conditionalUpdate,
readPeerAccessToken,
});
expect(token).toBe("peer");
expect(readPeerAccessToken).toHaveBeenCalledTimes(1);
});
it("returns null when the row vanished after a lost race", async () => {
const token = await persistRotatedToken("mine", {
conditionalUpdate: async () => 0,
readPeerAccessToken: async () => null,
});
expect(token).toBeNull();
});
});