Referencia

SDK cliente

Referencia del SDK cliente, recursos de prueba, comparadores, informes con datos sensibles ocultos, fallos SMTP y mensajes capturados.

Importa el cliente de pruebas desde inboxtap/client. Se comunica con la API HTTP local y crea direcciones de destinatario únicas en el proceso de pruebas.

Crear un cliente

import { InboxTapClient } from "inboxtap/client";

const inboxTap = new InboxTapClient({
  baseUrl: "http://localhost:8025",
});

baseUrl tiene como valor predeterminado http://localhost:8025. Un valor domain opcional evita la petición inicial de estado cuando la prueba ya conoce el dominio de destinatarios configurado.

Crear un buzón

const inbox = await inboxTap.createInbox({ alias: "password-reset" });
console.log(inbox.address);

El alias se normaliza a letras minúsculas, dígitos y guiones, y después se añade un sufijo aleatorio de 12 caracteres. El alias por defecto es test.

Puntos de entrada para recursos de prueba

Los recursos de los ejecutores de pruebas se publican mediante rutas secundarias aisladas, para que el servidor raíz y el SDK cliente no carguen dependencias de prueba opcionales.

ImportaciónExportación principalCiclo de vida
inboxtap/fixturesstartInboxTapFixture()Inicio explícito y close() idempotente
inboxtap/fixtures/bunsetupInboxTap()Preparación y limpieza del archivo mediante las funciones del ciclo de vida de Bun
inboxtap/fixtures/vitestextendInboxTap()Servidor por archivo, buzón por prueba
inboxtap/fixtures/playwrightextendInboxTap()Servidor por proceso de trabajo, buzón por prueba

startInboxTapFixture() usa 0 en ambos puertos de forma predeterminada y devuelve server, client, un transporte verificado de Nodemailer, los datos de conexión smtp, createInbox() y close(). Todos los adaptadores usan el mismo objeto de recursos. Instala Nodemailer 9 y, además, Vitest 4.1 o Playwright 1.61 cuando uses sus rutas secundarias específicas. El valor smtp contiene { host, port, secure: false, ignoreTLS: true } para configurar la aplicación que se está probando.

Crea un buzón nuevo para cada prueba. Un recurso puede compartir el servidor de captura y el transporte dentro de su alcance documentado, pero un buzón global para todo el conjunto permitiría que las pruebas paralelas leyeran el mismo destinatario.

Puntos de entrada para comparadores

Los comparadores de aserción se publican por separado del servidor, el cliente y los recursos de prueba:

ImportaciónExportaciones principalesComportamiento
inboxtap/matchersinboxTapMatchers, createInboxTapMatchers()Implementaciones sin dependencias opcionales y tipos públicos de observación
inboxtap/matchers/bunextendInboxTapExpect(expect)Amplía en el mismo lugar el expect inyectado por Bun
inboxtap/matchers/vitestextendInboxTapExpect(expect)Amplía en el mismo lugar el expect inyectado por Vitest
inboxtap/matchers/playwrightextendInboxTapExpect(expect)Devuelve un nuevo expect tipado de Playwright

Importar inboxtap, inboxtap/client o inboxtap/matchers no requiere Nodemailer, Vitest ni Playwright. Solo un adaptador específico de un ejecutor necesita la dependencia correspondiente.

import { expect } from "vitest";
import { extendInboxTapExpect } from "inboxtap/matchers/vitest";

extendInboxTapExpect(expect);

await expect(inbox).toHaveDeliveredOnce({
  subject: /password reset/i,
  quietMs: 100,
});

const email = await inbox.waitForMessage({ subject: /password reset/i });
expect(email).toHaveRecipient(inbox.address);
expect(email).toContainLink("/reset-password");
expect(email).toHaveUnsubscribeHeader({ oneClick: true });
ComparadorSemántica
toHaveDeliveredOnce({ subject?, quietMs? })Toma una instantánea inmediata del buzón y exige exactamente un mensaje coincidente. Un valor subject de tipo cadena se interpreta como una subcadena sin distinguir mayúsculas; una expresión regular se prueba sin cambiar su lastIndex. quietMs es un entero de 0 a 60000 y su valor predeterminado es 0.
toHaveRecipient(address)Compara de forma exacta y sin distinguir mayúsculas los destinatarios del sobre SMTP; no usa el campo de dirección de presentación analizado.
toContainLink(stringOrRegex)Comprueba los enlaces HTTP(S) extraídos. Una cadena se interpreta como una subcadena; una expresión regular clonada mantiene intacto el estado del llamador.
toHaveUnsubscribeHeader({ oneClick? })Analiza solo las cabeceras RFC en bruto de nivel superior, incluidos nombres con distintas mayúsculas y valores plegados. Por defecto exige un List-Unsubscribe no vacío; con oneClick: true, también exige un destino HTTPS entre corchetes angulares y el valor exacto, sin distinguir mayúsculas, List-Unsubscribe-Post: List-Unsubscribe=One-Click.

toHaveDeliveredOnce() no espera a que llegue un mensaje inicial. Cuando su primera instantánea es válida, quietMs observa solo el intervalo siguiente; no demuestra que sea imposible un reintento posterior. El comparador de un solo clic solo comprueba la forma de las cabeceras RFC 8058. No verifica firmas DKIM, la cobertura de la firma ni el comportamiento del punto de acceso para cancelar la suscripción.

Los resultados de los comparadores omiten deliberadamente actual, expected, los cuerpos del mensaje, los valores de destinatario y enlace, los patrones con tokens y las cabeceras en bruto. Los fallos solo exponen conteos y estados booleanos seguros.

Para recopilar un informe, pasa un recopilador síncrono a la función de creación:

import {
  createInboxTapMatchers,
  type InboxTapMatcherObservation,
} from "inboxtap/matchers";

const observations: InboxTapMatcherObservation[] = [];
const matchers = createInboxTapMatchers({
  recorder: {
    recordMatcherObservation(observation) {
      observations.push(observation);
    },
  },
});

Cada observación tiene una versión de esquema y registra el nombre del comparador, la negación y el estado de éxito, el ID opcional del mensaje capturado y conteos o booleanos sin contenido sensible. Nunca contiene el destinatario esperado, el asunto, el patrón del enlace, el valor de una cabecera ni el cuerpo capturado. Pasa matchers al método expect.extend() de un ejecutor compatible; los adaptadores específicos de cada ejecutor llaman a la misma función de creación.

Informes de prueba

Importa InboxTapReport desde la ruta secundaria inboxtap/reports, que no requiere dependencias opcionales. El recopilador implementa InboxTapMatcherRecorder, por lo que puedes pasarlo directamente a un adaptador de ejecutor o a createInboxTapMatchers({ recorder }). Añade explícitamente los mensajes capturados y las aserciones de la aplicación:

import { test as base } from "vitest";
import { extendInboxTap } from "inboxtap/fixtures/vitest";
import { extendInboxTapExpect } from "inboxtap/matchers/vitest";
import { InboxTapReport } from "inboxtap/reports";

const test = extendInboxTap(base);

test("writes redacted evidence", async ({ expect, inboxTap, inbox }) => {
  const report = new InboxTapReport({ title: "Signup email" });
  extendInboxTapExpect(expect, { recorder: report });

  try {
    await inboxTap.transport.sendMail({
      from: "app@local.test",
      to: inbox.address,
      subject: "Verify your account",
      text: "Open https://app.local.test/verify/id-example?next=private",
    });
    await expect(inbox).toHaveDeliveredOnce({ subject: /verify/i });
    const email = await inbox.waitForMessage({ subject: /verify/i });

    report.addAssertion({
      name: "verification email exposes one link",
      passed: email.links.length === 1,
      messageId: email.id,
    });
  } finally {
    for (const email of await inbox.messages()) report.addMessage(email);
    await report.write("artifacts/signup-email.json");
    await report.write("artifacts/signup-email.html");
  }
});

Escribir desde finally conserva la evidencia más reciente cuando falla un comparador o una aserción de la aplicación.

MétodoComportamiento
addMessage(message)Añade un CapturedEmail, elimina duplicados por ID y devuelve el recopilador para encadenar llamadas.
addAssertion({ name, passed, message?, messageId?, details? })Añade una aserción de la aplicación y devuelve el recopilador. Las cadenas opcionales y los valores estructurados de details pasan por el mismo proceso acotado para ocultar datos sensibles.
recordMatcherObservation(observation)Implementa InboxTapMatcherRecorder; los adaptadores de los ejecutores lo llaman por cada comparador completado.
render({ format: "json" | "html" })Devuelve el artefacto determinista como cadena.
write(filePath, { format? })Escribe el artefacto de forma asíncrona e infiere el formato desde la extensión cuando se omite.

render({ format }) devuelve JSON determinista o HTML estático y autónomo para las mismas entradas ordenadas. El JSON incluye una versión de esquema. write(filePath, { format? }) infiere el formato a partir de una extensión .json o .html cuando se omite y crea los directorios padre que falten. Ninguna de las dos operaciones llama al servidor de InboxTap.

El documento JSON de versión 1 expone title, protection, summary, messages, assertions y truncation. El truncamiento registra los conteos de mensajes y aserciones omitidos, los campos acortados, los bytes UTF-8 omitidos y el límite de bytes del artefacto. utf8BytesOmittedExact vale true cuando utf8BytesOmitted es exacto. Vale false cuando un recorrido acotado termina antes de inspeccionar todo el valor; en ese caso, el conteo es un límite inferior medido; el HTML distingue ambos casos mediante los valores exactly y at least.

El ámbito del recopilador sigue al objeto expect ampliado. El ejemplo usa el objeto expect de Vitest vinculado a la prueba, de modo que cada informe recibe solo las observaciones de los comparadores de esa prueba. No conectes recopiladores distintos por prueba a un mismo expect compartido de Bun o Vitest mientras las pruebas se ejecutan en paralelo. En ese caso, usa llamadas explícitas a addMessage() y addAssertion(), o recopila deliberadamente un único informe para el conjunto de pruebas. El adaptador de Playwright devuelve un nuevo expect ampliado que puede limitarse a un informe.

El proceso predeterminado para ocultar datos sensibles:

  • excluye la fuente RFC en bruto;
  • asigna a cada dirección de correo un seudónimo coherente dentro del informe;
  • elimina las credenciales de las URL y oculta todos los valores de consulta, los fragmentos, los valores de ruta similares a secretos, Authorization, Cookie, Proxy-Authorization, Set-Cookie, X-API-Key, X-Auth-Token y los valores similares a tokens en texto y HTML; y
  • escapa el HTML capturado en vez de representarlo y nunca ejecuta scripts capturados ni carga imágenes, estilos o píxeles de seguimiento capturados o remotos.

Usa redaction.patterns y redaction.additionalSensitiveHeaders para secretos específicos del proyecto. Si un patrón personalizado se solapa con una URL, InboxTap sustituye la URL completa para que una mutación mediante expresiones regulares no exponga valores adyacentes de consulta o fragmento. La recopilación está limitada a 100 mensajes y 1.000 aserciones, y cada artefacto generado tiene un máximo de 10 MiB. Cuando se alcanza un límite, la salida registra marcadores de truncamiento explícitos en vez de crecer sin límite.

La ocultación de datos sensibles reduce el riesgo, pero no puede garantizar que detecte cualquier dato personal o secreto. Revisa el artefacto antes de compartirlo. includeRaw: true conserva una copia protegida de la fuente RFC en bruto y solo debe usarse cuando el valor diagnóstico adicional justifique un riesgo de divulgación sustancialmente mayor.

Controlador de fallos SMTP

Cada servidor programático expone un SmtpFaultController como server.faults; quienes usan recursos de prueba acceden a él mediante inboxTap.server.faults. Los tipos públicos del controlador, las opciones, la compuerta y el estado se exportan desde inboxtap. No hace falta ninguna opción de activación.

interface SmtpFaultController {
  failNext(options: SmtpFailNextOptions): void;
  delayNext(options: SmtpDelayNextOptions): void;
  disconnectNext(options: SmtpDisconnectNextOptions): void;
  pauseNext(options?: SmtpPauseNextOptions): SmtpPauseGate;
  reset(): void;
}

Registra una regla antes de activar la aplicación. Por orden de registro, una regla se vincula a la siguiente transacción SMTP coincidente que alcance DATA, y solo se aplica una regla a cada transacción. Si se omite to, coincide cualquier destinatario; en caso contrario, la comparación no distingue mayúsculas en el sobre SMTP. Si cualquier destinatario coincide en una transacción con varios destinatarios, el fallo se aplica a toda la transacción. Los procesos de escucha SMTP de IPv4 e IPv6 consumen la misma cola del controlador.

MétodoOpciones y límitesResultado
failNextcode entero de 400 a 599; message opcional sin CR/LF, con valor predeterminado Injected SMTP failure; to y times opcionalesDevuelve el fallo SMTP configurado en vez de capturar el mensaje
delayNextdurationMs entero de 0 a 60000; to y times opcionalesRetiene la transacción antes de completar la entrega
disconnectNextafterBytes entero seguro no negativo; to y times opcionalesCierra la conexión SMTP después de observar el umbral
pauseNextto opcional; timeoutMs entero de 0 a 60000, con valor predeterminado 60000; sin opción timesDevuelve un SmtpPauseGate controlado de forma independiente
resetSin opcionesAborta las esperas en cola y activas, y limpia sus temporizadores

times debe ser un entero de 1 a 100 y tiene por defecto 1. Las aplicaciones expandidas de las reglas comparten un límite de 100, contando las que están en cola y las activas. Un to proporcionado debe ser una cadena no vacía; se ignoran los espacios alrededor. Los umbrales de desconexión se observan en los límites de los fragmentos del flujo, en vez de byte por byte. InboxTap cierra al final de DATA cuando un mensaje es más corto que afterBytes, para que la transacción inyectada falle de todos modos. Las entregas fallidas o desconectadas nunca entran en el almacén. Las entregas retrasadas o pausadas solo aparecen después de completar correctamente la entrega SMTP.

SmtpPauseGate.state es un SmtpPauseState de solo lectura:

EstadoSignificado
pendingLa regla está en cola y no ha alcanzado un comando DATA coincidente
pausedUna transacción SMTP coincidente está retenida
releasedrelease() canceló una regla pendiente o reanudó una transacción pausada
expiredSe agotó el tiempo de espera absoluto; una transacción activa recibe SMTP 451 y no se captura
abortedreset() o la parada del servidor canceló la compuerta

El tiempo de espera empieza al llamar a pauseNext(), por lo que acota tanto el tiempo en cola como el de pausa activa. La compuerta pasa a paused después de que el cuerpo del mensaje supere el análisis y la validación de tamaño y entre en la espera. waitUntilPaused() se resuelve entonces y permanece resuelta aunque la compuerta termine después. Rechaza si la compuerta alcanza un estado terminal antes de pausarse. release() es idempotente y puede llamarse durante pending; al hacerlo, elimina la regla en cola para que una transacción posterior no resulte afectada.

const gate = server.faults.pauseNext({
  to: inbox.address,
  timeoutMs: 10_000,
});
const delivery = triggerEmail(inbox.address);

await gate.waitUntilPaused();
expect(gate.state).toBe("paused");

gate.release();
await delivery;

reset() no borra los mensajes capturados. La parada del servidor proporciona la misma limpieza acotada de las compuertas: las que están en cola o activas pasan a aborted y se limpian sus temporizadores. Los controles de fallos son solo programáticos: no existen rutas HTTP, opciones de CLI, historial ni estadísticas de fallos.

Métodos de TestInbox

MétodoSe resuelve aPropósito
messages()CapturedEmail[]Lista los mensajes de esta dirección
clear()numberBorra los mensajes de esta dirección
waitForMessage(options)CapturedEmailEspera el mensaje coincidente completo
waitForLink(options)stringEspera el primer enlace, opcionalmente filtrado por texto contenido
waitForCode(options)stringEspera un código que coincida con un patrón; por defecto seis dígitos
waitForMatch(options)stringEspera una coincidencia obligatoria de cadena o expresión regular

Las opciones de espera aceptan subject, timeoutMs y afterId. subject puede ser una subcadena sin distinguir mayúsculas o una expresión regular. Las esperas de extracción tienen un valor por defecto de 10 segundos.

const code = await inbox.waitForCode({
  pattern: /\b\d{4}\b/,
  subject: /sign in/i,
  timeoutMs: 20_000,
});

Métodos de bajo nivel

InboxTapClient también expone health, listEmails, latestEmail, waitForEmail, clearEmails y el método genérico request. Prefiere TestInbox dentro de pruebas en paralelo porque aplica automáticamente el filtro de destinatario único.

CapturedEmail

interface CapturedEmail {
  id: string;
  receivedAt: string;
  envelope: { from: string | null; to: string[] };
  from: string;
  to: string[];
  subject: string;
  headers: Record<string, string>;
  text: string;
  html: string;
  links: string[];
  codes: string[];
  raw: string;
}

links y codes son listas. InboxTap no asigna nombres semánticos como magic o verification a los valores extraídos.

Errores

Las peticiones fallidas y los tiempos de espera agotados durante la extracción lanzan InboxTapError. Su propiedad status contiene el estado al estilo HTTP, incluido 408 cuando se agota el tiempo de espera y 404 cuando el mensaje no existe.