Quickstart — validar la 007 de punta a punta

Guía runnable (sin código de implementación; este se genera en /speckit.tasks + /speckit.implement). Referencias: contracts/identidad.md, data-model.md.

Requisitos

Node 22, Docker + Compose, Postgres 16. Variables: DATABASE_URL, SESSION_SECRET, TZ=Europe/Madrid.

Arranque

cd tema5/citaclara
cp .env.example .env
docker compose up -d db
npm install
npx prisma migrate dev   # 007 no añade migración (usa Paciente de la 001)
npm run seed             # Eleva con telefono
npm run dev

Validación por historia (trazable a spec)

ID Escenario Comando / pasos Esperado
US1/matriz Variantes Ana.Garcia.Lopez@correo.es, ANA.GARCIA.LOPEZ@CORREO.ES, +34 612 345 678, 0034-612-345-678, (612) 345-678 sobre la ficha ana.garcia.lopez@correo.es + 612345678 npm test -- identidad (matriz unit + resolución portal/recordatorio) todas resuelven a la misma ficha en ambos canales (SC-001)
US1/incorrecto Variante que tras normalizar no valida (p. ej. teléfono de 9 dígitos que no empieza por 6/7/8/9) POST /api/portal/sesion con esa variante 401 «No hemos encontrado esos datos…» + teléfono, sin citas, sin revelar si existe
US2/portal Dos fichas gemelas (mismo correo + teléfono normalizados), entrar al portal POST /api/portal/sesion con esas credenciales 409 «Tienes varias fichas…» + teléfono, cero citas mostradas
US2/recordatorio Mismas gemelas evaluadas por recordatorios proceso de recordatorios sobre ese destinatario cero envíos + incidencia duplicado
US2/sin-fugas Cualquier denegación por duplicado leer el aviso no enumera ni distingue fichas (SC-002)
US3/caducidad Entrar, 30 min sin actividad, siguiente petición POST /api/portal/sesion → esperar 30 min (reloj controlado en test) → GET citas exige reidentificación (correo + teléfono)
US3/cierre Entrar, «Cerrar sesión», reutilizar token DELETE /api/portal/sesion → reintentar con el token anterior 401, cookie limpiada, nada en localStorage
US4/vigente Enlace vigente de cita reservada, sin sesión de portal GET confirmación con token → PATCH /api/citas/[id]/cancelar { origen: email, token } muestra solo su cita; 200 aplicada vía cancelarCita; reutilizar para ver vale, reconfirmar da YA_CANCELADA
US4/caducado Enlace pasado el inicio o 72 h del envío mismo PATCH tras caducidad rechazo sin mutar
US4/flood >10 intentos/hora sobre el mismo enlace 11 GET/PATCH en 1 h bloqueo temporal con aviso claro, sin mutar
US4/ajeno Token de la cita A usado contra la cita B PATCH /api/citas/B/cancelar { origen: email, token: tokenA } ENLACE_INVALIDO, sin mutar B (SC-004)
SC-005 grep de normalización/sesión/duplicados rg -i "toLowerCase|normaliz|merge|fusion" lib app única definición en lib/identidad.ts (+ firmas en lib/sesion-paciente.ts y lib/enlace-verificado.ts); cero merge/fusión

Suites

npm test                    # Vitest unit + integración — todo verde para merge
npm run comprobar:es        # ningún string visible fuera de es-ES; avisos únicos con teléfono

Puerta de merge (constitución): suite verde + cero fugas entre fichas (gemelas 100 % denegadas sin mostrar ni mutar) + textos es-ES + grep de unicidad de normalización/sesión/duplicados (SC-005) + revisión de simplicidad (sin tablas/migraciones/endpoints fuera de data-model.md/contracts/).