test(garmin): pin the key exchange, the mapping and a sync against a fake Garmin

Unit tests cover the token cache, the single re-exchange on a refused
token, the key's end read from a capped token, the 429 wait, the range
chunking, and every resource's mapping over made-up records shaped from
Garmin's document. The card test pins the shared anatomy, the menu and
the expiry sentences. The integration test runs the sync, the backfill,
connect and disconnect against a fake Garmin served over HTTP and the
real schema: rows per resource under GARMIN, no second row on a re-sync,
a refused key parked after one retry, a 429 that stops the run, a
history walked to its start across slices, and a purge on disconnect
that keeps the imported history.
This commit is contained in:
Marc-André Bombeck committed 2026-11-04 10:14:17 +01:00
1 parent e2ba86b12f
commit 13e232c836
4 files changed
+1510

No files matched your search

@@ -0,0 +1,165 @@
/**
* Garmin on the shared integration card.
*
* 1. Not connected: the key form, the link to Garmin Connect's key page,
* the validity choice and Connect, inside the shared card.
* 2. Connected: the same menu as Nightscout, a refused key opens the form,
* and disconnect asks first and invalidates the envelope.
* 3. The key's end: a sentence in the body while it is close, past or
* unknown, and the date in the details while it is comfortably valid.
*/
import { describe, expect, it, vi } from "vitest";
import { createElement } from "react";
import { renderToStaticMarkup } from "react-dom/server";
import { queryKeys } from "@/lib/query-keys";
const invalidateSpy = vi.fn();
const capturedDisconnectOnSuccess: { fn: (() => void) | null } = { fn: null };
vi.mock("@tanstack/react-query", () => ({
useMutation: ({ onSuccess }: { onSuccess?: () => void }) => {
capturedDisconnectOnSuccess.fn = onSuccess ?? null;
return { mutate: vi.fn(), isPending: false };
},
useQueryClient: () => ({ invalidateQueries: invalidateSpy }),
}));
let lastCardProps: IntegrationCardProps | null = null;
vi.mock("../integration-card", async (importOriginal) => {
const actual = await importOriginal<typeof import("../integration-card")>();
return {
...actual,
IntegrationCard: (props: IntegrationCardProps) => {
lastCardProps = props;
return createElement(actual.IntegrationCard, props);
},
};
});
import { I18nProvider } from "@/lib/i18n/context";
import type { GarminKeyExpiry } from "@/lib/garmin/key-expiry";
import type { IntegrationCardProps } from "../integration-card";
import { GarminCard, GARMIN_KEY_SETTINGS_URL } from "../garmin-card";
import type { IntegrationStatusViewModel } from "../shared";
import { healthFixture } from "./health-fixture";
function render(viewModel?: Partial<IntegrationStatusViewModel>) {
return renderToStaticMarkup(
<I18nProvider initialLocale="en">
<GarminCard
viewModel={
viewModel
? ({
integration: "garmin",
...viewModel,
} as IntegrationStatusViewModel)
: undefined
}
/>
</I18nProvider>,
);
}
function connected(keyExpiry: GarminKeyExpiry | null) {
return render({
connected: true,
configured: true,
health: healthFixture(),
keyExpiry,
metricFreshness: [],
});
}
describe("GarminCard on the shared card", () => {
it("shows the key form inside the shared card while not connected", () => {
const html = render({ connected: false, configured: false, health: null });
expect(html).toContain('data-slot="integration-card"');
expect(html).toContain('data-state="not_connected"');
expect(html).toContain('id="garmin-api-key"');
expect(html).toContain('id="garmin-key-validity"');
expect(html).toContain(`href="${GARMIN_KEY_SETTINGS_URL}"`);
expect(html).toContain('data-testid="garmin-connect"');
expect(html).toContain(
'href="https://docs.healthlog.dev/guides/connect-garmin/"',
);
// 30, 90, 180 and 365 days, plus "I don't know".
expect(html.match(/<option /g)).toHaveLength(5);
});
it("keeps the sibling menu and opens the form for a new key", () => {
render({
connected: true,
configured: true,
health: healthFixture({
state: "action_required",
reason: "reauth_required",
action: "reconnect",
}),
});
expect((lastCardProps?.menu ?? []).map((i) => i.id)).toEqual([
"sync",
"test",
"view-data",
"credentials",
"setup-guide",
"disconnect",
]);
expect(lastCardProps?.primary?.onReconnect).toBeTypeOf("function");
const disconnect = lastCardProps?.menu?.find((i) => i.id === "disconnect");
expect(disconnect?.destructive).toBe(true);
expect(disconnect?.confirm?.title).toBe("Disconnect Garmin?");
invalidateSpy.mockClear();
capturedDisconnectOnSuccess.fn?.();
expect(invalidateSpy).toHaveBeenCalledWith({
queryKey: queryKeys.integrationsStatus(),
});
});
it("warns in the body when the key is about to expire", () => {
const html = connected({
state: "expiring",
expiresAt: "2026-11-08T00:00:00.000Z",
daysLeft: 3,
});
expect(html).toMatch(
/data-testid="garmin-key-expiry"[^>]*data-state="expiring"/,
);
expect(html).toContain("Your Garmin key expires on");
expect(html).not.toContain('data-testid="garmin-key-valid-until"');
});
it("says when the key expires today and when it has expired", () => {
expect(
connected({
state: "expiring",
expiresAt: "2026-11-04T20:00:00.000Z",
daysLeft: 0,
}),
).toContain("Your Garmin key expires today.");
expect(
connected({
state: "expired",
expiresAt: "2026-11-01T00:00:00.000Z",
daysLeft: null,
}),
).toContain("Your Garmin key expired on");
});
it("asks for the validity when the end is unknown", () => {
expect(
connected({ state: "unknown", expiresAt: null, daysLeft: null }),
).toContain("Garmin does not say when this key expires.");
});
it("keeps a valid key's date in the details, not the body", () => {
const html = connected({
state: "ok",
expiresAt: "2027-01-30T00:00:00.000Z",
daysLeft: 87,
});
expect(html).not.toContain('data-testid="garmin-key-expiry"');
const details = html.slice(html.indexOf('data-slot="integration-details"'));
expect(details).toContain('data-testid="garmin-key-valid-until"');
});
});