# Testing your integration (https://dodomain.io/docs/testing-your-integration)

Drive the hosted connect flow from your own Playwright suite: the supported automation path, what needs real DNS, and how to assert the result without one.

Your DoDomain integration has two halves worth covering in your own end-to-end suite: the **server half** (you mint a session, you receive a webhook, you activate the domain) and the **browser half** (your user gets to the connect flow and comes back). Both are testable against the real API; this page describes the path that works.

## Drive the hosted page, not the widget [#drive-the-hosted-page-not-the-widget]

The `@dodomain/connect` widget renders the hosted flow inside a **cross-origin iframe** on `app.dodomain.io`, wrapped in modal chrome the widget draws. Automating that means reaching through a frame boundary and pinning assertions to our sheet's internals: brittle, and none of it is your integration.

The supported automation path is the one the hosted link already gives you: **open `connectUrl` as a top-level page**. `https://app.dodomain.io/connect/<token>` is a plain server-rendered page with no embed requirement: the widget loads that same URL with a few query parameters appended (`embed`, the host page's `origin`, and optionally `theme`). Everything the user sees in the sheet, they see at the top level.

```ts
// playwright: the shape of an integration test against the real API
import { expect, test } from "@playwright/test";

test("a customer can take a domain through the DoDomain connect flow", async ({
  page,
  request,
}) => {
  // 1. Mint a session the way YOUR server does, with your secret key.
  const res = await request.post("https://app.dodomain.io/api/v1/sessions", {
    headers: { Authorization: `Bearer ${process.env.DODOMAIN_SECRET_KEY}` },
    data: {
      domain: "customer.example.com",
      records: [{ type: "CNAME", host: "app", value: "edge.yourproduct.com" }],
      returnUrl: "https://yourproduct.test/settings/domains",
    },
  });
  expect(res.ok(), `POST /api/v1/sessions answered HTTP ${res.status()}`).toBe(true);
  const session = await res.json();

  // 2. Open the hosted flow top-level. No iframe, no widget.
  await page.goto(session.connectUrl);

  // 3. The manual flow shows the exact records the user must create.
  await expect(page.getByRole("heading", { name: "customer.example.com", level: 1 })).toBeVisible();
  const row = page.getByRole("listitem").filter({ hasText: "edge.yourproduct.com" });
  await expect(row.getByText("CNAME", { exact: true })).toBeVisible();

  // 4. Drive verification.
  await page.getByRole("button", { name: "Verify records" }).click();
});
```

<Callout title="Useful selectors on the connect page">
  Each requested record is a list item carrying its type, the fully-qualified name, and the value.
  The primary button reads **Verify records** before the first check and **Check again** after it. A
  record that isn't live yet shows **not found yet**; a check that couldn't run shows **couldn't
  check**. Success replaces the button with a **Domain connected** panel. An unknown or deleted
  token renders **Connection not found** instead of a flow, a good negative test.
</Callout>

After the first manual click the flow re-checks on its own every 15 seconds (up to 20 automatic checks) and shows a `Re-checking in Ns` countdown. It never polls before that first click, so a test that opens the page and asserts without clicking generates no DNS traffic.

## The part that needs real DNS [#the-part-that-needs-real-dns]

Verification is not simulated. `POST /api/v1/sessions/:token/verify` queries the domain's **authoritative nameservers** for each record and is fail-closed: a lookup that cannot be completed can never report the record as present. There is no test mode, no fixture resolver, and no way to make a record verify without the record existing.

So a test that asserts all the way to `connection.verified` has to put a real record in real DNS. Two patterns work:

### Pattern A: a dedicated test zone [#pattern-a-a-dedicated-test-zone]

Register a cheap throwaway domain (or delegate a subzone you already own) purely for CI, and give your test suite API credentials for its DNS provider. Each test writes the record the session asked for, then drives the flow to green. This is the only pattern that exercises the whole chain: records live, session `verified`, `connection.verified` webhook delivered, your product activating the domain.

Keep the zone's records clean between runs: use a unique host per run (`run-${Date.now()}`) rather than reusing one name, so a leftover record from a previous test can never make a broken run look green.

### Pattern B: stop at pending, and test the rest directly [#pattern-b-stop-at-pending-and-test-the-rest-directly]

Most integration bugs are not in DNS. You can cover almost everything without a test zone:

- Mint a session against a domain you don't control and assert the honest miss: the flow reports `not found yet`, the counter reads `0/1 found`, and the button stays available as **Check again**. That proves your session shape, your record values, and your return URL, and it proves DoDomain isn't faking success.
- Assert `GET /api/v1/sessions/:token` echoes exactly the `domain` and `records` you meant to send. It is authorized by the session token alone, so a test can read it without your secret key.
- Assert `records[].fqdn` in the **create** response. That is the name verification will actually look up, so a wrong `host`/`domain` combination fails your test at session creation instead of silently failing in production. (See [host is relative to domain](https://dodomain.io/docs/api#creating-a-session).)
- Test your webhook handler against a body you construct yourself, signed with your own `whsec_...` secret. `verifyWebhook(secret, rawBody, signatureHeader)` from `@dodomain/node` is the same function that runs in production; you don't need DoDomain to send the request to test what you do with it.

## Asserting the outcome [#asserting-the-outcome]

Once records are live, there are two independent signals and it is worth checking both:

- **The webhook.** `connection.verified` fires the moment everything matches. In CI, point the app's webhook endpoint at a tunnel or a small collector and wait for the delivery; dedupe on `id`, because delivery is at-least-once.
- **The API.** `GET /api/v1/connections?domain=customer.example.com` lists what DoDomain believes is connected. Each entry carries `status`, `recordFqdns` (the names DoDomain monitors, one per record), `verifiedAt`, `lastCheckedAt`, and `disconnectedAt`, a clean poll-and-assert that needs no inbound network in your test environment. (`fqdn` is also present but is the session's `domain` repeated, kept for wire compatibility; assert on `recordFqdns`.)

Clean up with `DELETE /api/v1/connections/:connectionId`. It is idempotent: a repeat call returns the original `disconnectedAt` with `alreadyDisconnected: true` and fires no second webhook, so a retrying teardown can't corrupt the next run.

<Callout type="warn" title="Sessions expire; connections count">
  Session tokens expire 24 hours after creation, so mint them inside the test rather than checking
  one into a fixture; after expiry every session endpoint answers `410 expired`. And a finalized
  connection counts one unit against your monthly quota the first time it verifies (re-verifying and
  reconnecting add nothing). A suite that verifies real domains on every commit will consume quota;
  Pattern B does not.
</Callout>
