DEV Community

Silviu Technology
Silviu Technology

Posted on

FastAPI: contratos para emails de prueba confiables

Cuando una aplicación envía un correo de verificación, muchas pruebas empiezan con algo parecido a esto: crear un usuario, poner una dirección inventada y esperar. Funciona el primer día. Despues, el test falla por una razón dificil de ver: un proveedor lento, una bandeja compartida o un reintento que creó dos usuarios.

En proyectos de Python y FastAPI me resulta más práctico tratar el email de prueba como un contrato pequeño. El objetivo no es simular cada detalle de un buzón, sino dejar claro qué se genera, qué se espera y qué evidencia guardamos. Un servicio como tempmailso puede servir como un temp email generator para escenarios controlados, pero la arquitectura debe seguir siendo útil si mañana cambia el proveedor.

El problema: un email de prueba no es solo un string

Una dirección temporal tiene tres responsabilidades distintas:

  1. Identificar de forma segura el flujo que estamos probando.
  2. Recibir o consultar el mensaje esperado.
  3. Permitir que el test explique por qué pasó o falló.

Si mezclamos esas tareas dentro del endpoint, cada test acaba con esperas arbitrarias y mensajes poco claros. También es facil confundir una dirección válida con una cuenta que realmente puede recibir el correo.

Para pruebas de signup, prefiero generar un identificador único por ejecución y guardar el proveedor, el alias y la fecha de expiración. No hace falta almacenar el contenido completo del email para cada caso: normalmente bastan el message_id, el asunto esperado y el enlace de verificación reducido.

Un contrato pequeño para el endpoint

El endpoint puede aceptar una dirección de prueba, pero debe validar el propósito de la petición. Pydantic ayuda a que el límite sea visible:

from pydantic import BaseModel, EmailStr

class VerificationRequest(BaseModel):
    email: EmailStr
    run_id: str
    purpose: str = "signup"
Enter fullscreen mode Exit fullscreen mode

La respuesta tambien debe tener forma estable. Por ejemplo, verification_id, status y expires_at. Así, un cliente de pruebas no necesita conocer si el backend usa una cola, una API externa o un buzón local.

En el servicio, separo la interfaz del proveedor:

from typing import Protocol

class Mailbox(Protocol):
    async def wait_for_subject(self, address: str, subject: str) -> str:
        ...
Enter fullscreen mode Exit fullscreen mode

Este límite es sencillo, pero evita que FastAPI dependa de llamadas HTTP concretas. Para ver otro enfoque sobre estados claros para correos async, conviene pensar primero en estados y luego en tiempos de espera.

Separar proveedor, dominio y evidencia

El proveedor de correo no debería decidir el resultado del test. El test debe preguntar algo como: “¿el usuario recibió un enlace válido dentro del límite?”. La integración traduce su respuesta a un evento interno:

{
    "run_id": "signup-1842",
    "status": "received",
    "subject": "Confirma tu cuenta",
    "message_id": "msg-91a",
}
Enter fullscreen mode Exit fullscreen mode

Con este formato, cambiar de buzón no obliga a reescribir todos los casos. También puedes conectar emails aislados por branch a la misma idea: cada ejecución tiene su propio espacio y sus propios recibos.

Evita registrar tokens completos, enlaces de acceso o cuerpos que contengan datos reales. En el recibo basta un hash del mensaje, el código de estado y una marca de tiempo. Es menos tentador copiar información sensible a los logs, y el diagnóstico sigue siendo util.

Reintentos sin duplicar usuarios

Los tests de integración suelen reintentarse. Si cada intento llama a POST /signup sin una clave idempotente, el resultado puede ser dos cuentas y un email ambiguo. Usa un Idempotency-Key derivado del run_id y del caso:

key = f"{run_id}:signup"
headers = {"Idempotency-Key": key}
Enter fullscreen mode Exit fullscreen mode

El servidor debe devolver la misma decisión para la misma clave durante la vida del test. El polling del buzón necesita la misma disciplina: un timeout global, pausas progresivas y un último recibo con timed_out, no una excepción generica que borra el contexto.

Checklist para automatización

  • ¿Cada ejecución genera una dirección o alias aislado?
  • ¿El endpoint devuelve un contrato versionado y pequeño?
  • ¿El proveedor se puede sustituir detrás de una interfaz?
  • ¿Hay una clave idempotente para signup y verificación?
  • ¿El timeout queda visible en segundos y no escondido en un sleep?
  • ¿El recibo evita tokens y contenido sensible?
  • ¿Los casos de fake e mail com y temp gamil com quedan como datos de prueba, no como reglas de validación?

Una dirección temporal no demuestra identidad. Solo ayuda a probar un flujo. Si el producto necesita seguridad, hay que comprobar ownership, límites de abuso y expiración por separado.

Cierre

El patrón es pequeño: contrato en el borde, proveedor intercambiable, aislamiento por ejecución y evidencia mínima. Eso hace que una prueba de email falle de forma explicable, que es mucho más valioso que tener una prueba “verde” que nadie se atreve a tocar.

Top comments (0)