dánwòdánwò

Email and messaging

Twilio

Text messages with status callbacks, texts to your app's numbers, Verify codes and the limits of a trial account.

danwo's Twilio emulator answers api.twilio.com and verify.twilio.com. Messages your app sends land on phones your test reads; texts your test sends reach your app's SMS URL, signed as Twilio signs them.

Set up

Add twilioService and seed the account your app authenticates as. Hand the same credentials to the app:

danwo.config.ts
import { appEnvironment, defineConfig, twilioService } from "@danwo/sdk"

const url = "http://localhost:3000"
const account = { sid: `AC${"0".repeat(32)}`, authToken: "danwo-auth-token" }

export default defineConfig({
  testDir: "tests",
  readiness: { mode: "page+backend", settle: true },
  environment: () =>
    appEnvironment({
      app: {
        command: "npm run dev",
        url,
        env: {
          TWILIO_ACCOUNT_SID: account.sid,
          TWILIO_AUTH_TOKEN: account.authToken,
          TWILIO_PHONE_NUMBER: "+15005550006",
        },
      },
      services: [
        twilioService({
          seed: {
            accounts: [account],
            numbers: [{ number: "+15005550006", smsUrl: `${url}/api/twilio/sms` }],
            verifyServices: [{ friendlyName: "Sign in" }],
          },
        }),
      ],
    }),
})

Wrong credentials get Twilio's 401 with error 20003.

Seed it

FieldWhat it seeds
accountsAccounts by sid and auth token; type: "Trial" with its verified numbers.
apiKeysSK… API keys and their secrets.
numbersNumbers the account owns, and the URL texts to each are POSTed to.
verifyServicesVerify services, with the code length.
unreachablePhones that never get a text: the message ends undelivered with error 30003.

Use it in a test

Sign in with a code sent by Verify, read from the phone:

tests/sms-code.danwo.ts
import { type TwilioFixtures, test as base, twilioInbox } from "@danwo/sdk"

const test = base.extend<TwilioFixtures>()

test("signs in with a texted code", async ({ app, screen, expect, twilio }) => {
  await app.open("/sign-in")
  await screen.getByLabel("Phone").fill("+15551234567")
  await screen.getByRole("button", { name: "Text me a code" }).click()

  const text = await twilioInbox(twilio).waitFor("+15551234567")
  await screen.getByLabel("Code").fill(text.code() ?? "")
  await screen.getByRole("button", { name: "Sign in" }).click()

  await expect(screen.getByText("Welcome back")).toBeVisible()
})
HelperWhat it's for
twilioInbox(twilio)The texts each phone got, with the code in each.
twilioPhone(twilio).send(…)Text your app's number as a person would; any TwiML reply is sent back.
twilioSms(twilio)The texts, as a source for the mail fixture.

Trial accounts

Seed type: "Trial" to test what a trial account refuses, with Twilio's own codes:

danwo.config.ts · a trial account
twilioService({
  seed: {
    accounts: [
      {
        sid: `AC${"0".repeat(32)}`,
        authToken: "danwo-auth-token",
        type: "Trial",
        verified: ["+15551234567"],
      },
    ],
  },
})
A trial account sending…Gets
to a number not in verified422, error 572002
a body that isn't one of Twilio's trial templates, such as sms_2faerror 572006
a parameter only upgraded accounts may useerror 0 or 20003

A template id in the body is sent as the template's text, as on Twilio.

What's covered

AreaCovered
MessagesSend, read, list, update and delete; media; Twilio's magic test numbers.
StatusesFrom queued to delivered or undelivered, on the emulator's clock, with status callbacks.
IncomingTexts to your numbers posted to your SMS URL, and your TwiML replies sent.
Verify v2Services, codes sent by text, and checking them.
AccountsThe account, its numbers and API keys.

Faults

CallFaults the matrix tries
POST …/Messages.json and Verify's POSTs429, 500, 503, a 10-second delay, connection reset, answer lost after Twilio acted
Every status callback and incoming textarrives twice, arrives 10 minutes late, forged signature, 1 hour stale

Webhooks

Status callbacks and incoming texts are form POSTs signed with X-Twilio-Signature under the account's auth token, so twilio.validateRequest accepts them.

Fidelity

Live, in safe mode. danwo fidelity twilio reads the account and checks error answers and trial refusals against a real account, without sending texts. A real send on a trial account runs only when DANWO_FIDELITY_TWILIO_SEND_ON_TRIAL is set.

Not covered yet

  • Voice calls and TwiML for voice
  • WhatsApp and Conversations
  • Lookup

Next

On this page

Run it on every pull request

A GitHub check and one comment per pull request, with the flight records attached.

Set up CI