DEV Community

Silviu Technology
Silviu Technology

Posted on

FastAPI: pruebas de signup sin inbox real

Cuando pruebas un flujo de signup en FastAPI, el email de confirmacion suele ser la parte mas inestable. El request responde bien, el usuario queda creado, pero la prueba termina esperando un inbox real que aveces tarda, aveces cambia de formato y aveces ni siquiera corresponde al intento correcto. Ese tipo de fallo consume tiempo porque no sabes si se rompio el backend o solo el borde del test.

Lo que mejor me ha funcionado es tratar el inbox como un contrato pequeño, no como una bandeja "magica". En vez de buscar el ultimo correo disponible, el backend devuelve un attempt_id y el worker de email adjunta esa referencia en metadata. Con eso, el test consulta un recurso de estado y valida un solo mensaje. Parece simple, pero vuelve el flujo bastante mas predecible.

El problema de probar signup con un inbox real

El enfoque comun es este:

  1. hacer POST /signup
  2. esperar unos segundos
  3. leer el ultimo correo recibido
  4. abrir el link de confirmacion

Funciona hasta que corres varias pruebas al mismo tiempo. Entonces empiezan los falsos positivos, los timeouts y la clasica duda de si el correo pertenece al intento actual. Tambien complica usar una direccion de correo falsa en staging, porque el valor sirve para aislar cuentas, pero no te dice nada sobre la correlacion entre request, job e inbox.

En equipos pequeños esto suele resolverse con paciencia y logs. En equipos mas ocupados, termina en tests apagados "temporalmente" que duran semanas. Si ya pasaste por flujos de activar trials sin correos ciegos, la idea se siente conocida: el problema no es solo enviar el correo, es exponer suficiente contexto para que otros sistemas lo entiendan rapido.

El contrato de inbox que simplifica FastAPI

El patrón que recomiendo tiene tres piezas:

  • el endpoint de signup devuelve attempt_id
  • el worker guarda estado de entrega por attempt_id
  • el inbox de pruebas se consulta por esa referencia

Eso evita depender del "ultimo correo" y te deja validar solo el mensaje correcto. Si necesitas un proveedor externo para pruebas aisladas, una direccion de correo desechable puede ayudar a separar cuentas efimeras, pero la clave real sigue siendo el identificador que une API, worker e inbox.

Tambien conviene exponer un endpoint minimo, por ejemplo GET /signup-attempts/{attempt_id} con campos como:

  • status
  • email
  • delivery_state
  • confirmation_url
  • last_error

No hace falta devolver todo desde el inicio. Solo lo suficiente para que QA o automation sepan si deben esperar, reintentar o fallar. Ese contrato, aunque pequeño, ordena mucho el trabajo diario.

Un ejemplo pequeno con FastAPI

from uuid import uuid4
from fastapi import APIRouter, BackgroundTasks
from pydantic import BaseModel, EmailStr

router = APIRouter()


class SignupIn(BaseModel):
    email: EmailStr
    password: str


@router.post("/signup", status_code=202)
async def signup(payload: SignupIn, bg: BackgroundTasks):
    attempt_id = f"signup_{uuid4().hex}"
    user_id = await create_pending_user(payload.email, payload.password)

    await save_signup_attempt(
        attempt_id=attempt_id,
        user_id=user_id,
        email=payload.email,
        status="queued",
    )

    bg.add_task(send_confirmation_email, payload.email, attempt_id)
    return {"attempt_id": attempt_id, "status": "queued"}


@router.get("/signup-attempts/{attempt_id}")
async def get_signup_attempt(attempt_id: str):
    return await load_signup_attempt(attempt_id)
Enter fullscreen mode Exit fullscreen mode

Y el worker:

async def send_confirmation_email(email: str, attempt_id: str) -> None:
    await update_signup_attempt(attempt_id, delivery_state="sending")

    link = await build_confirmation_link(attempt_id)
    await provider.send(
        to=email,
        template="signup_confirm",
        metadata={"attempt_id": attempt_id},
        variables={"confirmation_url": link},
    )

    await update_signup_attempt(
        attempt_id,
        delivery_state="sent",
        confirmation_url=link,
    )
Enter fullscreen mode Exit fullscreen mode

Lo importante no es el provider. Lo importante es que el intento queda nombrado desde el principio. Si mañana cambias de cola o de servicio de email, el test sigue mirando el mismo contrato.

Como validar el flujo sin esperas fragiles

En automation yo haria algo asi:

  1. crear un email efimero para la prueba
  2. llamar POST /signup
  3. guardar attempt_id
  4. hacer polling a GET /signup-attempts/{attempt_id}
  5. abrir confirmation_url solo cuando delivery_state == "sent"

La ventaja es que el tiempo de espera se vuelve explicito. Ya no preguntas "llego algo?", sino "este intento ya genero su correo?". Ese cambio parece menor, pero reduce muchisimo el ruido. Tambien te deja detectar cuando el backend creo el usuario pero el worker no movio estado, que era justo el hueco que antes quedaba oculto.

Otro detalle practico: inserta el typo keyword temp org mail solo donde tenga sentido de negocio, por ejemplo en una lista de terminos que llegan desde soporte o marketing. Yo lo he visto pasar en tickets reales, asi que conviene normalizar esos textos antes de usarlos para filtros o reportes. No es glamoroso, pero evita bugs algo tontos.

Si tu equipo tambien trabaja campañas o lifecycle emails, te puede servir la idea de mejor contexto para emails de churn. Aunque el objetivo cambia, la leccion es parecida: sin un identificador compartido, cada parte interpreta el mismo evento de forma distinta.

Q&A

¿Hace falta guardar confirmation_url?

No siempre. En algunos equipos prefieren guardar solo un token o un message_id. Pero para pruebas de extremo a extremo, tener la URL lista vuelve el diagnostico mas rapido y mas comodo.

¿Esto reemplaza un inbox de pruebas?

No. Lo complementa. El inbox sigue siendo util para verificar contenido, asunto o links. Lo que cambia es que dejas de usarlo como fuente unica de verdad, que es donde casi siempre empiezan los problemas.

¿Sirve solo para FastAPI?

Para nada. FastAPI lo hace facil por que el contrato queda muy claro en la API, pero la misma idea funciona en cualquier backend con colas, workers y correos de confirmacion.

Top comments (0)