- Extract shared fitToGpx into connected-services/fit.ts (FIT is a Garmin open standard used by Wahoo, Coros, Garmin — not provider-specific) - Add importActivity() to sync/imports.server.ts so providers call one function instead of createActivity + recordImport separately; eliminates direct dependency on activities.server.ts from provider adapters - Update wahoo importer + webhook to use both shared helpers - Extract useHostElection(yjs) hook from useRouting so host election is independently testable without mounting the full routing stack - Add setDb() to journal db.ts for module-level injection in unit tests Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
116 lines
3.4 KiB
TypeScript
116 lines
3.4 KiB
TypeScript
// Contract tests for the Wahoo WebhookReceiver capability adapter.
|
|
//
|
|
// Seam: parseWebhook(body) -> WebhookEvent | null
|
|
// handle(event) -> void (creates an activity if file present, dedups via sync_imports)
|
|
|
|
import { describe, it, expect, beforeEach, vi } from "vitest";
|
|
|
|
const fetchSpy = vi.fn();
|
|
const mockImportActivity = vi.fn();
|
|
const mockIsAlreadyImported = vi.fn();
|
|
const mockGetServiceByProviderUser = vi.fn();
|
|
const mockWithFreshCredentials = vi.fn();
|
|
|
|
vi.mock("../../../sync/imports.server.ts", () => ({
|
|
isAlreadyImported: mockIsAlreadyImported,
|
|
importActivity: mockImportActivity,
|
|
}));
|
|
vi.mock("../../manager.ts", () => ({
|
|
getServiceByProviderUser: mockGetServiceByProviderUser,
|
|
withFreshCredentials: mockWithFreshCredentials,
|
|
}));
|
|
|
|
beforeEach(() => {
|
|
fetchSpy.mockReset();
|
|
globalThis.fetch = fetchSpy as unknown as typeof fetch;
|
|
mockImportActivity.mockReset();
|
|
mockIsAlreadyImported.mockReset();
|
|
mockGetServiceByProviderUser.mockReset();
|
|
mockWithFreshCredentials.mockReset();
|
|
});
|
|
|
|
const { wahooWebhook } = await import("./webhook.ts");
|
|
|
|
describe("wahooWebhook.parseWebhook", () => {
|
|
it("returns a WebhookEvent for workout_summary payloads", () => {
|
|
const event = wahooWebhook.parseWebhook({
|
|
event_type: "workout_summary",
|
|
user: { id: 7 },
|
|
workout_summary: {
|
|
workout: { id: 42 },
|
|
file: { url: "https://cdn.example/42.fit" },
|
|
},
|
|
});
|
|
expect(event).toEqual({
|
|
eventType: "workout_summary",
|
|
providerUserId: "7",
|
|
workoutId: "42",
|
|
fileUrl: "https://cdn.example/42.fit",
|
|
});
|
|
});
|
|
|
|
it("returns null for unrecognized event types", () => {
|
|
expect(wahooWebhook.parseWebhook({ event_type: "other" })).toBeNull();
|
|
});
|
|
|
|
it("returns null when user.id is missing", () => {
|
|
expect(
|
|
wahooWebhook.parseWebhook({ event_type: "workout_summary", user: {} }),
|
|
).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe("wahooWebhook.handle", () => {
|
|
it("creates an activity and records the import for a known user", async () => {
|
|
mockGetServiceByProviderUser.mockResolvedValue({
|
|
id: "svc-1",
|
|
userId: "u1",
|
|
provider: "wahoo",
|
|
});
|
|
mockIsAlreadyImported.mockResolvedValue(false);
|
|
mockImportActivity.mockResolvedValue({ activityId: "act-1" });
|
|
|
|
await wahooWebhook.handle({
|
|
eventType: "workout_summary",
|
|
providerUserId: "7",
|
|
workoutId: "42",
|
|
// no fileUrl — the activity is created without GPX
|
|
});
|
|
|
|
expect(mockImportActivity).toHaveBeenCalledWith(
|
|
"u1",
|
|
"wahoo",
|
|
"42",
|
|
expect.objectContaining({ name: expect.stringContaining("Wahoo") }),
|
|
);
|
|
});
|
|
|
|
it("silently skips when the providerUserId is unknown (no leak)", async () => {
|
|
mockGetServiceByProviderUser.mockResolvedValue(null);
|
|
|
|
await wahooWebhook.handle({
|
|
eventType: "workout_summary",
|
|
providerUserId: "999",
|
|
workoutId: "42",
|
|
});
|
|
|
|
expect(mockImportActivity).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("silently skips when the workout was already imported (idempotency)", async () => {
|
|
mockGetServiceByProviderUser.mockResolvedValue({
|
|
id: "svc-1",
|
|
userId: "u1",
|
|
provider: "wahoo",
|
|
});
|
|
mockIsAlreadyImported.mockResolvedValue(true);
|
|
|
|
await wahooWebhook.handle({
|
|
eventType: "workout_summary",
|
|
providerUserId: "7",
|
|
workoutId: "42",
|
|
});
|
|
|
|
expect(mockImportActivity).not.toHaveBeenCalled();
|
|
});
|
|
});
|