Apariencia
Flujo del candidato
20 casos de prueba que cubren todo el ciclo del candidato: registro → verificación → login → membresía → perfil → psicométricos → CV → entrevistas.
Duración estimada: 60 minutos ejecutándolos en orden.
Registro y verificación
TC-CAND-001 · Registro exitoso con email nuevo
Severidad: 🔴 Crítica
Precondiciones:
- Base de datos con catálogos seeds
- Email
qa-cand-001@test.humaeNO existe enusers
Pasos:
- Ir a
/register - Llenar: nombre
Ana QA, emailqa-cand-001@test.humae, passwordPassword123, confirmPassword123 - Marcar checkbox de términos
- Clic en "Crear cuenta"
Resultado esperado:
- Redirección a
/verify-emailcon toast "Te enviamos un correo para verificar" - En DB:
userstiene nuevo registro constatus=pending_verification,email_verified_at=null - Rol
candidateasignado enmodel_has_roles - Correo recibido en MailHog con subject "Verifica tu correo…"
Variaciones:
- Email ya registrado → 422 con
errors.email: ["Este correo ya está registrado"] - Password sin número → 422 con error en
password - Passwords no coinciden → 422 con error en
password_confirmation - Sin aceptar términos → validación client-side bloquea submit
TC-CAND-002 · Verificación de email con link válido
Severidad: 🔴 Crítica
Precondiciones: TC-CAND-001 ejecutado, correo en MailHog
Pasos:
- Abrir correo en MailHog
- Copiar link "Verificar correo"
- Abrir en browser
Resultado esperado:
- Redirección a
/logincon toast "Correo verificado" - En DB:
users.email_verified_at= fecha reciente,status=active
Variaciones:
- Link con signature inválida (modificar la URL) → 403 "Link inválido"
- Link expirado (> 60 min) → Pantalla con botón "Reenviar verificación"
TC-CAND-003 · Reenvío de correo de verificación
Severidad: 🟠 Alta
Precondiciones: User pending_verification existe
Pasos:
- Ir a
/verify-email - Ingresar email del user pendiente
- Clic "Reenviar"
Resultado esperado:
- Toast "Enviamos un nuevo correo"
- Nuevo email en MailHog (distinto signature)
Variaciones:
- Rate limit (4° intento en 1 min) → 429 "Demasiados intentos, espera 60 segundos"
- Email no existe → 200 con mensaje genérico (no leakeamos existencia)
- Email ya verificado → toast "Este correo ya está verificado"
Login y sesión
TC-CAND-004 · Login exitoso
Severidad: 🔴 Crítica
Precondiciones: User active con email verificado
Pasos:
/login- Credenciales correctas
- Submit
Resultado esperado:
- Redirección a
/dashboard - Cookie
humae_sessionseteada (inspeccionar en DevTools) - Token guardado en
localStorage.humae.auth.token - Header muestra avatar + nombre
Variaciones:
- Password incorrecto → 422 "Credenciales no coinciden"
- Usuario pendiente verificación → 403 "Tu correo aún no está verificado" + botón reenviar
- Usuario suspendido → 403 "Cuenta suspendida, contacta soporte"
- Hit rate limit (6° intento en 1 min) → 429
TC-CAND-005 · Sesión persiste tras cerrar browser
Severidad: 🟠 Alta
Precondiciones: TC-CAND-004
Pasos:
- Login
- Cerrar browser completamente
- Abrir de nuevo y visitar
/dashboard
Resultado esperado: Sigue autenticado (token persistido)
Variaciones:
- Borrar cookies y localStorage → redirige a
/login - Invalidar token manualmente en DB (
DELETE FROM personal_access_tokens WHERE id = X) → próxima request devuelve 401, frontend redirige a login
TC-CAND-006 · Logout
Severidad: 🟡 Media
Pasos:
- User logueado → menú → "Cerrar sesión"
Resultado esperado:
POST /auth/logout→ 204- Token revocado en DB (
personal_access_tokensborrado) - Redirección a
/login - Intentar acceder a
/dashboard→ redirige a/login
Recuperación de password
TC-CAND-007 · Forgot password happy path
Severidad: 🔴 Crítica
Pasos:
/forgot-password- Email de user activo
- Submit
Resultado esperado:
- Toast "Te enviamos un correo"
- Registro en
password_reset_tokenscon token hash - Correo en MailHog con link
/reset-password?token=...&email=...
TC-CAND-008 · Reset password con token válido
Severidad: 🔴 Crítica
Pasos:
- Clic en link del correo (TC-CAND-007)
- Ingresar nueva password + confirmar
- Submit
Resultado esperado:
- Redirección a
/logincon toast "Contraseña actualizada" - Login con nueva password funciona
- Login con password vieja → falla
- Todos los tokens Sanctum anteriores revocados (seguridad)
Variaciones:
- Token expirado (> 60 min) → 422 "Token inválido o expirado"
- Token ya usado → 422
- Email no coincide con el token → 422
Membresía
TC-CAND-009 · Checkout exitoso con Stripe test card
Severidad: 🔴 Crítica
Precondiciones:
- Candidato logueado, sin membresía activa
- Stripe en modo test
STRIPE_WEBHOOK_SECRETconfigurado- Stripe CLI corriendo con
stripe listensi es local
Pasos:
- Dashboard → "Contratar membresía"
- Redirige a Stripe Checkout
- Tarjeta
4242 4242 4242 4242, fecha futura, CVC123 - "Pagar"
Resultado esperado:
- Redirección a
/membership/success - Webhook
checkout.session.completedllega al backend Payment.status = succeededMembership.status = active,expires_at = now + 180 days- Correo "Tu membresía HUMAE está activa" en inbox
- Notificación in-app visible en la campana
- Dashboard muestra tarjeta verde "Activa hasta DD/MM/YYYY"
Variaciones:
- Webhook no configurado → UI no sabe que el pago fue exitoso hasta refresh manual. Ver logs para confirmar fallback.
TC-CAND-010 · Checkout con tarjeta rechazada
Severidad: 🟠 Alta
Pasos:
- Dashboard → "Contratar membresía"
- Tarjeta
4000 0000 0000 0002(decline) - "Pagar"
Resultado esperado:
- Stripe muestra error inline
- Candidato regresa manualmente o redirige a
/membership/cancel Payment.status = pending(sin éxito)- NO se crea
Membership - Candidato puede reintentar con otra tarjeta
TC-CAND-011 · Webhook duplicado (idempotencia)
Severidad: 🔴 Crítica
Precondiciones: TC-CAND-009 completado
Pasos:
- Copiar el
event_iddel webhook original desde Stripe Dashboard - En Stripe Dashboard → el evento → "Resend event"
Resultado esperado:
- Backend devuelve 200 (idempotente)
- NO se crea otra Membership (solo hay 1 para ese user)
- NO se envía otro correo de activación
TC-CAND-012 · Membresía expiring soon (alerta)
Severidad: 🟡 Media
Precondiciones: Membresía con expires_at en los próximos 15 días
bash
php artisan tinker
>>> $m = \App\Models\Membership::first();
>>> $m->update(['expires_at' => now()->addDays(10)]);Pasos:
- Login como candidato
- Dashboard
Resultado esperado: Banner amarillo "Tu membresía expira en 10 días. Renueva…"
TC-CAND-013 · Ejecutar ExpireMembershipsJob manualmente
Severidad: 🟠 Alta
Precondiciones: Membresía con expires_at en el pasado
Pasos:
bash
php artisan tinker
>>> $m = \App\Models\Membership::first();
>>> $m->update(['expires_at' => now()->subDays(1)]);
>>> \App\Jobs\ExpireMembershipsJob::dispatchSync();
>>> $m->fresh()->status;Resultado esperado:
$m->status=MembershipStatus::Expired- Candidato desaparece del directorio
- Log:
[ExpireMembershipsJob] expired 1 memberships
Perfil profesional
TC-CAND-014 · Editar datos básicos del perfil
Severidad: 🟠 Alta
Pasos:
/me/profile- Cambiar headline, summary, años de experiencia, salario esperado
- Guardar
Resultado esperado: 200, toast "Perfil actualizado", DB actualizada
Variaciones:
- Headline > 200 chars → 422
- linkedin_url no es URL → 422
- years_of_experience negativo o > 70 → 422
TC-CAND-014b · Marcar categoría empleado o practicante
Severidad: 🔴 Crítica (PDF cosasfaltanteshumae punto 2)
Precondiciones: User candidato logueado en /me/profile con perfil ya creado.
Pasos:
- Buscar la sección "¿Cómo te quieres postular?" en el form.
- Seleccionar el RadioGroup
Practicante. - Guardar cambios.
- Salir y volver a entrar al perfil.
Resultado esperado:
- Toast "Perfil actualizado".
- En DB:
candidate_profiles.candidate_kind = 'intern'. - En
GET /me/profileel campodata.candidate_kindviene"intern". - Al volver a entrar, el RadioGroup queda preseleccionado en Practicante.
- Cuando un recruiter abre el directorio, este perfil muestra badge ámbar Practicante.
Variaciones:
- Cambiar de Practicante a Empleado → se persiste el nuevo valor (no acumulativo).
- Enviar
"banana"por API (manualmente) → 422 conerrors.candidate_kind. - Dejar sin marcar → el campo queda
null(no es obligatorio para guardar el perfil, pero el cliente lo recomienda antes de pagar membresía).
TC-CAND-014c · Seleccionar áreas de interés multi-select con principal
Severidad: 🔴 Crítica (PDF cosasfaltanteshumae punto 1)
Precondiciones: Catálogo functional_areas cargado (al menos las 15 del PDF).
Pasos:
- En
/me/profile, ir a la sección "Áreas en las que te gustaría trabajar". - En el buscador escribir "Pro" → marcar Producción.
- Buscar y marcar Calidad y Mantenimiento.
- Click en el icono de estrella vacía de Producción → ahora es la principal (estrella rellena).
- Quitar Mantenimiento (botón ✕ del chip).
- Escribir "Bioingeniería" en el input "Otra (texto libre)".
- Guardar.
Resultado esperado:
- Toast "Perfil actualizado".
- DB:
- 2 filas en
candidate_functional_areas(Producción conis_primary=true, Calidad conis_primary=false). candidate_profiles.functional_area_id= id de Producción (sincronizado).candidate_profiles.other_area_text = 'Bioingeniería'.
- 2 filas en
GET /me/profiledevuelvefunctional_areas: [{id, code, name, is_primary, sort_order}, ...].- En el directorio del recruiter, el detalle del candidato muestra una tarjeta "Áreas de interés" con badge primario para Producción.
Variaciones:
- Marcar 11 áreas → 422 (
functional_areasarray max 10). - Enviar dos áreas con
is_primary=true→ el backend respeta solo la primera marcada como primaria. - Quitar todas las áreas y guardar → pivote vacío,
functional_area_idquedanull. - Enviar área con
idinexistente → 422 (exists:functional_areas,id). - Click en la estrella rellena de Producción cuando ya es principal → no efecto (ya está marcada).
TC-CAND-015 · Subir avatar (imagen válida)
Severidad: 🟠 Alta
Pasos:
/me/profile- Clic "Cambiar foto"
- Seleccionar JPG ≤ 4MB
- Upload
Resultado esperado:
- Foto visible inmediatamente
User.avatar_urlapunta a{APP_URL}/storage/avatars/{user_id}/{hash}.webp- Archivo físico en
humae_backend/storage/app/public/avatars/{user_id}/(400×400 webp)
Variaciones:
- Archivo > 4MB → 422 "Archivo demasiado grande"
- Formato no permitido (PDF, SVG, GIF) → 422 "Formato inválido"
- Hit rate limit (11° upload en 1 min) → 429
TC-CAND-016 · Agregar experiencia, educación, skills
Severidad: 🟠 Alta
Pasos:
- Perfil → "Agregar experiencia"
- Empresa, puesto, fechas, descripción
- Guardar
- Repetir para educación
- Agregar 3 skills con niveles distintos
- Agregar 2 idiomas
Resultado esperado:
- Cada item aparece en el perfil
- Orden cronológico inverso para experiencias y educación
- Skills e idiomas con unique constraint (no duplicados)
Variaciones:
- Experiencia sin
start_date→ 422 end_date < start_date→ 422- Marcar
is_current=true→end_datese ignora - Intentar agregar misma skill 2 veces → 422 "Skill ya agregada"
TC-CAND-017 · Subir documento adjunto
Severidad: 🟡 Media
Pasos:
- Perfil → "Documentos" → "Subir"
- PDF ≤ 10MB, tipo
cv_personal - Upload
Resultado esperado: archivo en storage/app/private/documents/{profile_id}/ + registro en candidate_documents con file_provider='local' y file_url apuntando a /api/v1/me/profile/documents/{id}/download
Psicométricos
TC-CAND-018 · Completar Big Five test
Severidad: 🟠 Alta
Precondiciones: Seed BigFiveQuestionsSeeder corrido (25 preguntas)
Pasos:
/me/psicometricos- Clic "Big Five"
- Responder las 25 preguntas (escala 1-5)
- Submit final
Resultado esperado:
PsychometricAttempt.status = completedPsychometricResultcreado con:dimension_scores(5 dimensiones)total_score> 0gradeA/B/C/D
- Redirección a
/me/psicometricos/resultados/{id} - Radar chart renderiza con las 5 dimensiones
Variaciones:
- Submit parcial (sin responder todas) → 422 "Faltan preguntas"
- Submit dos veces → segundo submit devuelve el mismo result (idempotente)
- Pausar y volver mañana → attempt se guarda, puede continuar
CV PDF
TC-CAND-019 · Descargar CV
Severidad: 🟠 Alta
Precondiciones: Perfil con datos mínimos (nombre, ≥ 1 experiencia)
Pasos:
- Dashboard → "Descargar CV"
Resultado esperado:
- Response
Content-Type: application/pdf - Filename
CV_Nombre_Apellido.pdf(transliterado, sin acentos ni ñ) - PDF abre correctamente con:
- Logo HUMAE en header
- Foto de perfil (si existe)
- Datos + experiencias + educación + skills
- Tamaño < 500KB aprox
Variaciones:
- Sin experiencias → PDF se genera pero sección vacía
- Hit rate limit (31 en 1 min) → 429
Entrevistas del candidato
TC-CAND-020 · Confirmar entrevista agendada
Severidad: 🔴 Crítica
Precondiciones: Recruiter agendó una entrevista (ver TC-REC-010)
Pasos:
- Login como candidato
/me/entrevistas- Clic "Confirmar" en entrevista
propuesta
Resultado esperado:
Interview.state=confirmada- Notificación a recruiter + company_user (email + in-app)
- El botón desaparece, ahora hay "Reprogramar" y "Cancelar"
Variaciones:
- Reprogramar → pide nueva fecha, crea row en
interview_reschedules, state vuelve apropuesta - Cancelar → requiere motivo, state →
cancelada(terminal), notifica a todos - Click en entrevista ya
realizada→ botones deshabilitados

