Skip to content

Add test infrastructure, with the Strava boundary mocked #73

Description

@c-harding

The project currently has no test infrastructure — no test runner, no test scripts, no fixtures. This issue tracks introducing one, with the Strava boundary mocked so the whole stack can run offline and in CI.

Approach: a fake Strava server behind two env vars

Strava is reached from exactly three places, all in server/strava/raw-api.ts:

Line URL
84 https://www.strava.com/oauth/token
189 https://www.strava.com/api/v3
213 http://www.strava.com/oauth/authorize

Hoisting these to STRAVA_API_BASE / STRAVA_OAUTH_BASE (defaulting to the real values, wired through shared/config/dotenv.js like every other setting) is enough to cover both halves of the integration:

  • the server's own outbound token exchange and API calls;
  • the OAuth popup — because the authorize URL is built server-side at raw-api.ts:213 and handed to the browser over the websocket, pointing STRAVA_OAUTH_BASE at the fake server means window.open lands there naturally. The fake server 302s straight to ${SERVER_DOMAIN}/api/token?code&state&scope, closing the loop with no interception anywhere.

redirect_uri is derived from SERVER_DOMAIN and is unaffected.

The fake server needs six endpoints: POST /oauth/token, GET /oauth/authorize (instant 302), GET /athlete, GET /athlete/activities, GET /athletes/:id/routes, GET /gear/:id. The pagination loops in server/strava/index.ts terminate when a page returns fewer than 200 items, so a single short page ends them.

Alternatives considered

  • MSW in the server process — works without the env vars (it patches http.ClientRequest, which node-fetch v2 sits on) and gives nice per-test error injection, but it has to be preloaded into the server process for e2e, and it can't touch the OAuth popup: that's a top-level cross-origin navigation, which a service worker structurally cannot intercept.
  • Playwright's context.route alone — fine for the popup, but it only sees browser-originated requests, so the server's own Strava calls are invisible to it. The two halves are coupled: fulfilling the popup with a fake code while the server still talks to real Strava fails the exchange and trips the process.exit(1) below. Half-mocking is worse than either extreme.
  • Both remain useful in narrow spots — see the optional task at the end.

Fix first

These will actively obstruct testing:

  • process.exit(1) on a failed token exchange (raw-api.ts:96-100, already carries a TODO: why such a hard exit?). Any auth-failure test takes down the server and every test after it.
  • app.listen at module scope (server/app.ts:45). Split it so app.ts exports the app and a separate entrypoint listens, enabling in-process integration tests.
  • cwd-relative paths. SESSIONS_DIR = 'sessions' (raw-api.ts:15) and 'static/auth.html' (server/routes/token.ts:10) resolve against process.cwd(). Either run each test server in a temp cwd or make the sessions dir configurable — needed for state isolation between tests.
  • Non-terminating waits. await new Promise(() => undefined) (raw-api.ts:222) never resolves by design, and CALLBACK_TIMEOUT is 15 min (server/strava/token.ts:11). A broken test hangs instead of failing; make the timeout configurable and set aggressive per-test timeouts.

Test layers

1. Pure functions (Vitest, no mocking). Highest value per unit of effort.

  • Export and test the conversion layer in server/routes/activities.ts:30-111 (convertActivitySummary, convertRouteSummary, convertGear). Note activities.ts:71 has a known-wrong elevation loss calculation flagged with a TODO — pin the intended behaviour with a test.
  • Timezone handling — the highest-risk area given d6a51be Fix timezones. Covers activity.timezone.split(' ')[1] in server/calendar.ts:57 and the local/UTC date pairing in convertActivitySummary.
  • Extract toHms and the description builder out of the .ics route handler and test them directly.
  • TimeRange.cap, and the client utils (midpoint, stats, numberFormat, groupMapItems).

2. Server integration (in-process app + fake Strava). No browser, millisecond-scale.

  • Generated .ics output from GET /calendar/:token.ics — likely the single highest-payoff test in the repo.
  • GET /api/user, DELETE /api/user.
  • The websocket protocol on /api/activities, driven with a plain ws client: assert the handshakestatsactivitiesgear sequence.
  • Error paths: a 401 from Strava should exercise the retry-then-relogin branch at raw-api.ts:294-296.

3. Playwright e2e. Run against the built client served by Express on a single port, rather than the Vite dev server — production-shaped, and it avoids the :8081 client / :8080 SERVER_DOMAIN split where the popup redirect bypasses the Vite proxy.

  • Full login flow. The popup is a real window.open (client/src/stores/ContinueLoginStore.ts:24) resolved via postMessage from server/static/auth.html, so use context.waitForEvent('page').
  • Activities streaming into the sidebar.
  • Calendar subscribe redirect.
  • Block api.mapbox.com with context.route — the map needs a real token and fetches tiles. Assert on sidebar DOM state, not the canvas.
  • Fresh browser context per test so the localforage/IndexedDB caches in client/src/utils/storage.ts don't leak across tests.

Infrastructure

  • Add vitest to shared/server/client and @playwright/test at the root, with a test script alongside the existing lint/prettier ones.
  • Capture real Strava JSON once against a personal account, trim it, and commit as fixtures typed against the interfaces already in server/strava/model.ts.
  • .env.testDOTENV_FILE is already supported by shared/config/dotenv.js, so no new plumbing is needed. Set VALIDATE_USER_BEFORE_CACHE explicitly here.
  • CI workflow running lint + unit + integration; e2e optionally gated.

Optional

  • MSW's WebSocket mocking (MSW 2.x) to unit-test the message switch at client/src/stores/ActivityStore.ts:396-445 with synthetic frames and no server at all. That switch plus checkFinished is the densest logic in the client, and reaching it through Playwright is slow and brittle. Nice to have, not load-bearing.

Suggested order

  1. Fix process.exit(1) and split app.listen.
  2. Vitest + the pure-function tests (no mocking needed, immediate value).
  3. Hoist the Strava URLs, build the fake server, land the .ics integration test as the first slice that proves the seam.
  4. Websocket integration tests.
  5. Playwright.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions