Skip to content

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.humae NO existe en users

Pasos:

  1. Ir a /register
  2. Llenar: nombre Ana QA, email qa-cand-001@test.humae, password Password123, confirm Password123
  3. Marcar checkbox de términos
  4. Clic en "Crear cuenta"

Resultado esperado:

  • Redirección a /verify-email con toast "Te enviamos un correo para verificar"
  • En DB: users tiene nuevo registro con status=pending_verification, email_verified_at=null
  • Rol candidate asignado en model_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

Severidad: 🔴 Crítica

Precondiciones: TC-CAND-001 ejecutado, correo en MailHog

Pasos:

  1. Abrir correo en MailHog
  2. Copiar link "Verificar correo"
  3. Abrir en browser

Resultado esperado:

  • Redirección a /login con 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:

  1. Ir a /verify-email
  2. Ingresar email del user pendiente
  3. 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:

  1. /login
  2. Credenciales correctas
  3. Submit

Resultado esperado:

  • Redirección a /dashboard
  • Cookie humae_session seteada (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:

  1. Login
  2. Cerrar browser completamente
  3. 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:

  1. User logueado → menú → "Cerrar sesión"

Resultado esperado:

  • POST /auth/logout → 204
  • Token revocado en DB (personal_access_tokens borrado)
  • 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:

  1. /forgot-password
  2. Email de user activo
  3. Submit

Resultado esperado:

  • Toast "Te enviamos un correo"
  • Registro en password_reset_tokens con token hash
  • Correo en MailHog con link /reset-password?token=...&email=...

TC-CAND-008 · Reset password con token válido

Severidad: 🔴 Crítica

Pasos:

  1. Clic en link del correo (TC-CAND-007)
  2. Ingresar nueva password + confirmar
  3. Submit

Resultado esperado:

  • Redirección a /login con 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_SECRET configurado
  • Stripe CLI corriendo con stripe listen si es local

Pasos:

  1. Dashboard → "Contratar membresía"
  2. Redirige a Stripe Checkout
  3. Tarjeta 4242 4242 4242 4242, fecha futura, CVC 123
  4. "Pagar"

Resultado esperado:

  • Redirección a /membership/success
  • Webhook checkout.session.completed llega al backend
  • Payment.status = succeeded
  • Membership.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:

  1. Dashboard → "Contratar membresía"
  2. Tarjeta 4000 0000 0000 0002 (decline)
  3. "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:

  1. Copiar el event_id del webhook original desde Stripe Dashboard
  2. 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:

  1. Login como candidato
  2. 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:

  1. /me/profile
  2. Cambiar headline, summary, años de experiencia, salario esperado
  3. 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:

  1. Buscar la sección "¿Cómo te quieres postular?" en el form.
  2. Seleccionar el RadioGroup Practicante.
  3. Guardar cambios.
  4. Salir y volver a entrar al perfil.

Resultado esperado:

  • Toast "Perfil actualizado".
  • En DB: candidate_profiles.candidate_kind = 'intern'.
  • En GET /me/profile el campo data.candidate_kind viene "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 con errors.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:

  1. En /me/profile, ir a la sección "Áreas en las que te gustaría trabajar".
  2. En el buscador escribir "Pro" → marcar Producción.
  3. Buscar y marcar Calidad y Mantenimiento.
  4. Click en el icono de estrella vacía de Producción → ahora es la principal (estrella rellena).
  5. Quitar Mantenimiento (botón ✕ del chip).
  6. Escribir "Bioingeniería" en el input "Otra (texto libre)".
  7. Guardar.

Resultado esperado:

  • Toast "Perfil actualizado".
  • DB:
    • 2 filas en candidate_functional_areas (Producción con is_primary=true, Calidad con is_primary=false).
    • candidate_profiles.functional_area_id = id de Producción (sincronizado).
    • candidate_profiles.other_area_text = 'Bioingeniería'.
  • GET /me/profile devuelve functional_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_areas array 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_id queda null.
  • Enviar área con id inexistente → 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:

  1. /me/profile
  2. Clic "Cambiar foto"
  3. Seleccionar JPG ≤ 4MB
  4. Upload

Resultado esperado:

  • Foto visible inmediatamente
  • User.avatar_url apunta 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:

  1. Perfil → "Agregar experiencia"
  2. Empresa, puesto, fechas, descripción
  3. Guardar
  4. Repetir para educación
  5. Agregar 3 skills con niveles distintos
  6. 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=trueend_date se ignora
  • Intentar agregar misma skill 2 veces → 422 "Skill ya agregada"

TC-CAND-017 · Subir documento adjunto

Severidad: 🟡 Media

Pasos:

  1. Perfil → "Documentos" → "Subir"
  2. PDF ≤ 10MB, tipo cv_personal
  3. 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:

  1. /me/psicometricos
  2. Clic "Big Five"
  3. Responder las 25 preguntas (escala 1-5)
  4. Submit final

Resultado esperado:

  • PsychometricAttempt.status = completed
  • PsychometricResult creado con:
    • dimension_scores (5 dimensiones)
    • total_score > 0
    • grade A/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:

  1. 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:

  1. Login como candidato
  2. /me/entrevistas
  3. 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 a propuesta
  • Cancelar → requiere motivo, state → cancelada (terminal), notifica a todos
  • Click en entrevista ya realizada → botones deshabilitados

Siguiente

Flujo del reclutador →

Manual de usuario HUMAE · Uso interno