Files
HealthLog/vitest.setup.ts
T
Marc-André Bombeck ba1271275f feat(fitbit): stop at the Web API turndown and point to Google Health
Google turns the legacy Fitbit Web API off on 30 October 2026 (support
ended on 30 September). From that day the hourly poll, the backfill,
the sleep repair and the token refresh make no call, the connect, sync
and test routes refuse with `provider_retired`, and the connection
reads as not connected to the health read and the watchdog, so the
shutdown pages nobody and asks nobody to reconnect. The connection row
and everything it imported stay.

The status envelope publishes `retirement` on the Fitbit entry (the
day, whether it has come, the successor), and the Fitbit card shows
when the connection stops, how to move to Google Health (same devices,
one more consent, tokens cannot be carried over) and that the data
stays; sync and connect grey out after the day. Both integrations can
run side by side meanwhile: every type Fitbit writes is laddered with
Fitbit just above Google Health, so a day both deliver counts once.

The test setups pin the turndown off, so the Fitbit suites do not turn
red on the day; the turndown's own tests flip it.
2026-10-21 12:46:14 +02:00

86 lines
3.8 KiB
TypeScript

/**
* Unit-test setup.
*
* Seed the client-side locale cache with the EN bundle. In production
* only the active locale reaches the client (boot script, then dynamic
* import on switch — see `src/lib/i18n/load-locale.ts`). Tests mount
* `I18nProvider` standalone — often with `renderToStaticMarkup`, which
* is synchronous and cannot await a dynamic import — so EN, the fallback
* floor of the t() chain, is primed up front.
*
* Only EN. Every catalog is several hundred kilobytes and this file runs
* once per test file, so priming all of them made setup the largest share
* of a unit run. A test that renders another locale primes it itself:
*
* import deMessages from "../../../messages/de.json";
* primeMessages("de", deMessages);
*
* Server-side translation (`getServerTranslator`, `shared-resolve`) holds
* every catalog on its own and needs no priming.
*/
import { afterEach, vi } from "vitest";
import { settleBackgroundTasks } from "@/lib/logging/background-tasks";
import { primeMessages } from "@/lib/i18n/load-locale";
import enMessages from "./messages/en.json";
primeMessages("en", enMessages);
/**
* Leave no background work in flight when a test ends.
*
* Several paths start a promise, return without awaiting it, and log on the
* detached handle so the failure is not swallowed. Vitest forwards every
* console write to the main thread as an `onUserConsoleLog` RPC call and
* rejects whatever is still pending on that channel while it tears a test file
* down, so a write that lands in that window fails the whole run with
* `EnvironmentTeardownError: Closing rpc while "onUserConsoleLog" was pending`
* — blamed on the file the worker was tearing down, which is not the test that
* started the work and need not log anything of its own. Awaiting the
* registered tasks here removes the precondition instead of muting the
* console, which would have thrown away a real signal and left the
* non-determinism in place. See `src/lib/logging/background-tasks.ts`.
*/
afterEach(async () => {
await settleBackgroundTasks();
});
/**
* The shared write ceiling is charged by every identity resolver on a write
* (`src/lib/auth/write-meter.ts`), which is a database round-trip most route
* tests have no fake for: their Prisma mock carries the models the handler
* touches and nothing else. The meter is therefore a no-op in unit tests by
* default. Its own behaviour is pinned by
* `src/lib/auth/__tests__/write-meter.test.ts`, which loads the real module,
* and against Postgres by `tests/integration/write-meter.test.ts`; a route test
* that wants the refusal mocks `meterAccountWrite` to throw.
*/
vi.mock("@/lib/auth/write-meter", async (importOriginal) => {
const actual =
await importOriginal<typeof import("@/lib/auth/write-meter")>();
return { ...actual, meterAccountWrite: vi.fn(async () => undefined) };
});
/**
* The end of the legacy Fitbit Web API is a calendar date
* (`src/lib/fitbit/retirement.ts`). Read off the real clock, every suite that
* exercises the Fitbit sync, its jobs or its routes would turn red on that day
* and stay red until the provider code is removed. The suites therefore run
* before the turndown by default; a test about the turndown itself flips
* `isFitbitRetired` (`vi.mocked(isFitbitRetired).mockReturnValue(true)`) or
* loads the real module with `vi.importActual`, as
* `src/lib/fitbit/__tests__/retirement.test.ts` does.
*/
vi.mock("@/lib/fitbit/retirement", async (importOriginal) => {
const actual =
await importOriginal<typeof import("@/lib/fitbit/retirement")>();
const isFitbitRetired = vi.fn((_now?: Date) => false);
return {
...actual,
isFitbitRetired,
fitbitRetirement: vi.fn((now?: Date) => ({
...actual.fitbitRetirement(now),
ended: isFitbitRetired(now),
})),
};
});