mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 13:01:00 +00:00
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:
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"');
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user