> ## Documentation Index
> Fetch the complete documentation index at: https://docs.mirobody.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Provider Testing

> Verify a C++ vendor client with the vendor debug CLI and against a running server.

## Overview

Vendor clients call live third-party APIs, so they aren't part of the unit suite. You verify them two ways: the **`vendor` debug CLI** (exercises one client's operations in isolation, no server) and **the HTTP routes** on a running server (the ownership-gated bind → verify → fetch path).

<Info>
  Metadata (`VendorInfo`) needs no credentials; transport operations need real credentials against the vendor.
</Info>

## The `vendor` debug CLI

The `cli/vendor.cpp` tool (built under `cli/` when `MIROBODY_BUILD_TOOLS` is ON — the default) drives one vendor's contract operations directly. Subcommands:

```text theme={null}
vendor list                                          # all registered vendors
vendor <id> info                                     # full metadata for one vendor
vendor <id> auth-url  --redirect URL [--state S]     # build the consent URL
vendor <id> providers [--user U]                     # connectable data sources
vendor <id> fetch     --user U --domain sleep [--start ISO] [--end ISO]
vendor <id> webhook   --file payload.json            # body from --file/--data/stdin
vendor <id> revoke    --user U [--provider slug]
```

The standalone CLI reads its credentials from the legacy `MIROBODY_VENDOR_<ID>_*` env vars (its own `from_env()` convention); the server itself is configured with the clean `<ID>_*` keys (`FITBIT_CLIENT_ID`, …) — see [Using Providers](/en/providers/using-providers).

<Steps>
  <Step title="Metadata (no credentials)">
    Confirm your `VendorInfo` is complete and the id is registered:

    ```bash theme={null}
    ./build/cli/vendor list
    ./build/cli/vendor fitbit info
    ```
  </Step>

  <Step title="Authorize URL">
    ```bash theme={null}
    MIROBODY_VENDOR_FITBIT_CLIENT_ID=... \
      ./build/cli/vendor fitbit auth-url --redirect https://localhost/cb --state abc
    ```

    Check the URL is well-formed and carries your scopes / `state`.
  </Step>

  <Step title="Fetch">
    With a real access token, fetch a domain over a window:

    ```bash theme={null}
    MIROBODY_VENDOR_FITBIT_API_KEY=... \
      ./build/cli/vendor fitbit fetch --user - --domain heart_rate --start 2026-06-01 --end 2026-06-07
    ```

    Verify the returned JSON matches the vendor's documented shape, and that **unsupported domains throw** with a clear message.
  </Step>

  <Step title="Webhook (signed vendors)">
    Feed a captured payload and confirm signature verification:

    ```bash theme={null}
    ./build/cli/vendor fitbit webhook --file sample_notification.json
    ```

    Pass a tampered body and confirm it's rejected.
  </Step>

  <Step title="Revoke">
    ```bash theme={null}
    MIROBODY_VENDOR_FITBIT_API_KEY=... MIROBODY_VENDOR_FITBIT_CLIENT_ID=... \
    MIROBODY_VENDOR_FITBIT_CLIENT_SECRET=... ./build/cli/vendor fitbit revoke --user -
    ```
  </Step>
</Steps>

<Note>
  For stub operations, the CLI prints the vendor's "not implemented" reason — that's expected and is itself worth asserting (the message should name *why* it's a stub).
</Note>

## Against a running server

Exercise the ownership-gated HTTP path (all routes need a bearer JWT; the server listens on `HTTP_PORT`, default `8080`):

```bash theme={null}
# 1. bind the external account id (PENDING)
curl -X POST "http://localhost:8080/vendors/fitbit/bind" \
  -H "Authorization: Bearer $JWT" -H "Content-Type: application/json" \
  -d '{"external_user_id": "FITBIT_USER_ID"}'

# 2. verify ownership (-> VERIFIED)
curl -X POST "http://localhost:8080/vendors/fitbit/bind/verify" -H "Authorization: Bearer $JWT"

# 3. fetch a verified link's data
curl "http://localhost:8080/vendors/fitbit/data?domain=heart_rate&start=2026-06-01&end=2026-06-07" \
  -H "Authorization: Bearer $JWT"
```

Confirm that fetching **before** verifying is refused, and that a second user can't bind the same `external_user_id` (the `UNIQUE(vendor_id, external_user_id)` backstop).

For an EHR (SMART on FHIR), walk the browser flow: `/health/ehr/providers` → `/health/ehr/authorize` → `/health/ehr/callback` → `/health/ehr/sync`, then read the persisted `Observation`s back from `/fhir/Observation`.

## Local HTTPS for OAuth callbacks

Many vendors reject non-HTTPS redirect URIs, so a plain `http://localhost:8080` callback won't work for a real consent flow. Options for local development:

* A local reverse proxy on port 443 terminating TLS (nginx / Caddy) with a trusted cert from **`mkcert`**, forwarding to `localhost:8080`, and a hosts-file entry mapping your production callback domain to `127.0.0.1` — so the **same** redirect URI works locally and in production.
* A tunneling service such as **ngrok** (also handy for inbound **webhooks** during local testing).

The point is to present the vendor with the exact HTTPS callback URL it expects without deploying for every change.

## What to cover

<AccordionGroup>
  <Accordion title="Happy path" icon="check">
    Valid credentials → `auth-url` builds, `fetch` returns documented JSON for each supported domain, signed `webhook` verifies, `revoke` succeeds.
  </Accordion>

  <Accordion title="Token lifecycle (exchange_code / refresh)" icon="key">
    With a fresh authorization code, `exchange_code(code, redirect_uri)` returns a `TokenSet` (access token, refresh token where the vendor issues one, `expires_in`); `refresh(refresh_token)` returns a fresh set. A bad or replayed code → a clear `VendorError` with the vendor's error body. Vendors without a refresh grant (Polar — its tokens don't expire) should throw their documented reason. End-to-end, this pair is exercised by `POST /vendors/{id}/bind/verify` (exchanges the code and stores the user's tokens encrypted) and by `fetch` refreshing an expired token.
  </Accordion>

  <Accordion title="Missing / wrong config" icon="triangle-exclamation">
    Empty `api_key` / `client_id` → a clear `VendorError` naming the missing env var. Required `base_url` (self-hosted / EHR) empty → refusal, not a guessed host.
  </Accordion>

  <Accordion title="Edge cases" icon="code">
    Unsupported `DataDomain` throws; empty date window uses the vendor default; tampered webhook signature is rejected; a stubbed op reports its reason.
  </Accordion>
</AccordionGroup>

## Next steps

<CardGroup cols={2}>
  <Card title="Provider Integration" icon="code" href="/en/development/provider-integration">
    Build the vendor you're testing
  </Card>

  <Card title="Testing Guide" icon="flask" href="/en/development/testing">
    The Catch2 unit suite
  </Card>

  <Card title="OAuth Implementation" icon="key" href="/en/development/oauth-implementation">
    Consent, webhooks, revocation
  </Card>

  <Card title="Using Providers" icon="link" href="/en/providers/using-providers">
    The bind / verify / data routes
  </Card>
</CardGroup>
