mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-11 05:42:19 +00:00
fix(fitbit): show the retired card only to an account that had Fitbit
After the Web API turndown every account still saw a Fitbit card that can no longer connect. The status entry now publishes retirement.offered: false once the API is gone for an account with no Fitbit connection, no Fitbit app of its own and no Fitbit data, and the connections panel leaves the card out. Accounts that had Fitbit keep it with the retirement notice.
This commit is contained in:
1 parent
bf3830203f
commit
b53434daa0
8 files changed
+172
-40
No files matched your search
@@ -1,4 +1,4 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { afterEach, describe, it, expect, vi, beforeEach } from "vitest";
|
||||
|
||||
vi.mock("@/lib/api-handler", () => ({
|
||||
apiHandler: <T extends (...args: unknown[]) => unknown>(fn: T) => fn,
|
||||
@@ -16,6 +16,7 @@ vi.mock("@/lib/db", () => ({
|
||||
fitbitConnection: { findUnique: vi.fn(async () => null) },
|
||||
googleHealthConnection: { findUnique: vi.fn(async () => null) },
|
||||
moodEntry: { count: vi.fn(async () => 0) },
|
||||
measurement: { findFirst: vi.fn(async () => null) },
|
||||
$queryRaw: vi.fn(async () => []),
|
||||
workout: { groupBy: vi.fn(async () => []) },
|
||||
},
|
||||
@@ -65,6 +66,7 @@ vi.mock("@/lib/oura/credentials", async () => ({
|
||||
}));
|
||||
|
||||
import { GET } from "../route";
|
||||
import { isFitbitRetired } from "@/lib/fitbit/retirement";
|
||||
import { prisma } from "@/lib/db";
|
||||
import { listLiveMeasurementTypes } from "@/lib/measurements/live-types";
|
||||
import { ATTEMPT_STALE_AFTER_MS } from "@/lib/integrations/sync-verdict";
|
||||
@@ -508,3 +510,61 @@ describe("/api/integrations/status — one round of reads", () => {
|
||||
expect(envelope.integrations.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("/api/integrations/status — the Fitbit card after the turndown", () => {
|
||||
const fitbitFind = prisma.fitbitConnection.findUnique as ReturnType<
|
||||
typeof vi.fn
|
||||
>;
|
||||
const fitbitDataFind = prisma.measurement.findFirst as ReturnType<
|
||||
typeof vi.fn
|
||||
>;
|
||||
const offered = async () =>
|
||||
(
|
||||
(await fetchEntries()).find((e) => e.integration === "fitbit") as {
|
||||
retirement?: { ended: boolean; offered: boolean };
|
||||
}
|
||||
).retirement;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
userFind.mockResolvedValue({});
|
||||
fitbitFind.mockResolvedValue(null);
|
||||
fitbitDataFind.mockResolvedValue(null);
|
||||
// The suites run before the turndown by default (`vitest.setup.ts`).
|
||||
vi.mocked(isFitbitRetired).mockReturnValue(true);
|
||||
});
|
||||
afterEach(() => {
|
||||
vi.mocked(isFitbitRetired).mockReturnValue(false);
|
||||
});
|
||||
|
||||
it("is not offered to an account that never had Fitbit", async () => {
|
||||
expect(await offered()).toMatchObject({ ended: true, offered: false });
|
||||
expect(fitbitDataFind).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
where: expect.objectContaining({ userId: "u1", source: "FITBIT" }),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("stays, with its notice, for an account holding Fitbit data", async () => {
|
||||
fitbitDataFind.mockResolvedValue({ id: "m1" });
|
||||
expect(await offered()).toMatchObject({ ended: true, offered: true });
|
||||
});
|
||||
|
||||
it("stays for an account still holding its Fitbit connection", async () => {
|
||||
fitbitFind.mockResolvedValue({
|
||||
createdAt: new Date("2026-01-01T00:00:00.000Z"),
|
||||
tokenExpiresAt: new Date("2026-11-01T00:00:00.000Z"),
|
||||
lastSyncedAt: null,
|
||||
backfillCompletedAt: null,
|
||||
});
|
||||
expect(await offered()).toMatchObject({ ended: true, offered: true });
|
||||
expect(fitbitDataFind).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("is offered to everyone before the turndown", async () => {
|
||||
vi.mocked(isFitbitRetired).mockReturnValue(false);
|
||||
expect(await offered()).toMatchObject({ ended: false, offered: true });
|
||||
expect(fitbitDataFind).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -59,7 +59,7 @@ import {
|
||||
import type { ConnectionHealth } from "@/lib/integrations/connection-health";
|
||||
import {
|
||||
fitbitRetirement,
|
||||
type FitbitRetirement,
|
||||
type PublishedFitbitRetirement,
|
||||
} from "@/lib/fitbit/retirement";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
@@ -295,6 +295,19 @@ export const GET = apiHandler(async () => {
|
||||
!!dbUser?.fitbitClientIdEncrypted && !!dbUser?.fitbitClientSecretEncrypted;
|
||||
const fitbitLegacyLastSyncedAt =
|
||||
fitbitConn?.lastSyncedAt?.toISOString() ?? null;
|
||||
// Once the Web API is gone the Fitbit card only explains the move to an
|
||||
// account that had something to move: a connection, its own Fitbit app, or
|
||||
// imported Fitbit data. Anyone else is shown no dead card. The data probe
|
||||
// runs only after the turndown, for an account with neither of the others.
|
||||
const retirementNow = fitbitRetirement();
|
||||
const fitbitOffered =
|
||||
!retirementNow.ended ||
|
||||
fitbitConnected ||
|
||||
fitbitConfigured ||
|
||||
(await prisma.measurement.findFirst({
|
||||
where: { userId: user.id, source: "FITBIT", deletedAt: null },
|
||||
select: { id: true },
|
||||
})) !== null;
|
||||
const googleHealthConnected = !!googleHealthConn;
|
||||
const googleHealthConfigured =
|
||||
!!dbUser?.googleHealthClientIdEncrypted &&
|
||||
@@ -382,7 +395,7 @@ export const GET = apiHandler(async () => {
|
||||
? fitbitConn.tokenExpiresAt.getTime() <= now
|
||||
: null,
|
||||
backfillCompleted: fitbitConn ? !!fitbitConn.backfillCompletedAt : null,
|
||||
retirement: fitbitRetirement(),
|
||||
retirement: { ...retirementNow, offered: fitbitOffered },
|
||||
health: health["fitbit"],
|
||||
...resolve("fitbit", fitbitStatus, {
|
||||
connected: fitbitConnected,
|
||||
@@ -564,7 +577,7 @@ interface FitbitExtras {
|
||||
tokenExpired: boolean | null;
|
||||
backfillCompleted: boolean | null;
|
||||
/** The end of the legacy Fitbit Web API (`@/lib/fitbit/retirement`). */
|
||||
retirement: FitbitRetirement;
|
||||
retirement: PublishedFitbitRetirement;
|
||||
}
|
||||
|
||||
// v1.27.0 — Google Health mirrors the Fitbit shape plus `needsReauth`: Google
|
||||
|
||||
Reference in new issue
Block a user