Skip to content

Programa de Instructores

Módulos API: instructor-applications, creators, path-instructors Páginas web: InstructorProgramPage, AdminApplicationsPage, CreatorProfileSettingsPage, CreatorDashboardPage, CreatorProfilePage Spec Playwright: apps/e2e/tests/creator/instructor-program.spec.ts — 📋 pendiente

EndpointMóduloRolesDescripción
POST /instructor-applicationsinstructor-applicationsPúblico (sin auth)Enviar solicitud de instructor
GET /admin/applicationsinstructor-applicationsSUPER_ADMINListar solicitudes con filtro por estado
GET /admin/applications/statsinstructor-applicationsSUPER_ADMINKPIs de solicitudes
PATCH /admin/applications/:idinstructor-applicationsSUPER_ADMINMarcar en revisión / actualizar notas
POST /admin/applications/:id/approveinstructor-applicationsSUPER_ADMINAprobar solicitud y crear CreatorProfile
POST /admin/applications/:id/rejectinstructor-applicationsSUPER_ADMINRechazar solicitud con motivo
POST /admin/applications/:id/request-infoinstructor-applicationsSUPER_ADMINSolicitar información adicional
GET /creator/me/profilecreatorsCONTENT_ADMIN, SUPER_ADMINObtener perfil del creador autenticado
PATCH /creator/me/profilecreatorsCONTENT_ADMIN, SUPER_ADMINActualizar bio, especialidades, redes
POST /creator/me/avatarcreatorsCONTENT_ADMIN, SUPER_ADMINSubir avatar a S3
POST /creator/me/bannercreatorsCONTENT_ADMIN, SUPER_ADMINSubir banner a S3
GET /creator/me/statscreatorsCONTENT_ADMIN, SUPER_ADMINKPIs del dashboard (estudiantes, rating, rutas)
GET /creator/me/badgescreatorsCONTENT_ADMIN, SUPER_ADMINProgreso de badges de instructor
GET /creators/:usernamecreatorsPúblicoPerfil público de un creador
GET /paths/:pathId/instructorspath-instructorsCONTENT_ADMIN, SUPER_ADMINListar co-instructores de una ruta
POST /paths/:pathId/instructors/invitepath-instructorsCONTENT_ADMIN, SUPER_ADMINInvitar co-instructor a una ruta
PATCH /paths/:pathId/instructors/:userId/permissionspath-instructorsCONTENT_ADMIN, SUPER_ADMINActualizar rol/permisos de co-instructor
DELETE /paths/:pathId/instructors/:userIdpath-instructorsCONTENT_ADMIN, SUPER_ADMINRevocar acceso de co-instructor
POST /path-instructors/invitations/:token/acceptpath-instructorsAutenticadoAceptar invitación de co-instructor
POST /path-instructors/invitations/:token/declinepath-instructorsAutenticadoRechazar invitación de co-instructor
GET /path-instructors/my-collaborationspath-instructorsAutenticadoMis colaboraciones activas como co-instructor

PENDING → IN_REVIEW → APPROVED
→ REJECTED
→ WITHDRAWN (retirada por el solicitante)

Los motivos de rechazo válidos son: no_experience, generic, covered, no_sector, linkedin, other.


US-INST-001: Solicitar convertirme en instructor

Section titled “US-INST-001: Solicitar convertirme en instructor”

US-INST-001: Solicitud pública de instructor

Section titled “US-INST-001: Solicitud pública de instructor”

Como visitante o usuario registrado quiero completar un formulario de solicitud para unirme al programa de instructores de AcademIA para que el equipo evalúe mi perfil y propuesta de contenido.

Módulo: instructor-applications · Endpoint: POST /instructor-applications Página: /instructorsInstructorProgramPage Estado: ✅ Implementado Prioridad: Alta

AC-INST-001.1 — Solicitud enviada con datos válidos

Section titled “AC-INST-001.1 — Solicitud enviada con datos válidos”
Given que accedo a /instructors y relleno el formulario con
| fullName | Ana García López |
| email | ana@empresa.com |
| linkedinUrl | linkedin.com/in/anagarcia |
| currentRole | Data Scientist Senior en Banco Santander |
| courseProposal | Curso de 10 lecciones sobre LLMs aplicados a análisis de riesgo crediticio… (>100 chars) |
When envío el formulario
Then recibo HTTP 201
And el body contiene { id: string, message: "Solicitud recibida…" }
And la solicitud queda con status PENDING en BD
And el historial statusHistory contiene un evento { type: "RECEIVED" }

AC-INST-001.2 — Duplicado bloqueado para email con solicitud activa

Section titled “AC-INST-001.2 — Duplicado bloqueado para email con solicitud activa”
Given que ya existe una solicitud con status PENDING para "ana@empresa.com"
When envío una nueva solicitud con el mismo email
Then recibo HTTP 409
And el mensaje de error contiene "Ya existe una solicitud activa"

AC-INST-001.3 — Validación de campos obligatorios

Section titled “AC-INST-001.3 — Validación de campos obligatorios”
Given que omito el campo "courseProposal" en el formulario
When intento enviar la solicitud
Then recibo HTTP 400
And la respuesta lista los campos con error de validación

US-INST-002: Revisión de solicitudes por el SUPER_ADMIN

Section titled “US-INST-002: Revisión de solicitudes por el SUPER_ADMIN”

Como SUPER_ADMIN quiero ver la lista de solicitudes de instructores con su estado y filtrarlas para gestionar el pipeline de incorporación de nuevos creadores.

Módulo: instructor-applications · Endpoints: GET /admin/applications, GET /admin/applications/stats Página: /admin/applicationsAdminApplicationsPage Estado: ✅ Implementado Prioridad: Alta

AC-INST-002.1 — Listar todas las solicitudes

Section titled “AC-INST-002.1 — Listar todas las solicitudes”
Given que estoy autenticado como SUPER_ADMIN
When hago GET /admin/applications
Then recibo HTTP 200 con un array de solicitudes ordenadas por createdAt desc
And cada item contiene fullName, email, linkedinUrl, currentRole, courseProposal, status
Given que hay solicitudes en estados PENDING, IN_REVIEW y APPROVED
When hago GET /admin/applications?status=PENDING
Then recibo solo las solicitudes con status PENDING
Given que hay solicitudes en distintos estados
When hago GET /admin/applications/stats
Then recibo { total, pending, inReview, approved, rejected, withdrawn, weeklyNew }

AC-INST-002.4 — Acceso denegado para roles no-admin

Section titled “AC-INST-002.4 — Acceso denegado para roles no-admin”
Given que estoy autenticado como CONTENT_ADMIN
When hago GET /admin/applications
Then recibo HTTP 403

US-INST-003: Marcar solicitud en revisión

Section titled “US-INST-003: Marcar solicitud en revisión”

Como SUPER_ADMIN quiero marcar una solicitud como IN_REVIEW para señalar que estoy evaluándola activamente.

Módulo: instructor-applications · Endpoint: PATCH /admin/applications/:id Estado: ✅ Implementado Prioridad: Media

AC-INST-003.1 — Transición PENDING → IN_REVIEW

Section titled “AC-INST-003.1 — Transición PENDING → IN_REVIEW”
Given que existe una solicitud con status PENDING (id = "app-1")
When hago PATCH /admin/applications/app-1 con body { status: "IN_REVIEW" }
Then recibo HTTP 200
And la solicitud tiene status IN_REVIEW
And statusHistory contiene un evento { type: "IN_REVIEW", actorEmail: <admin_id> }

AC-INST-003.2 — No se puede marcar IN_REVIEW desde APPROVED

Section titled “AC-INST-003.2 — No se puede marcar IN_REVIEW desde APPROVED”
Given que existe una solicitud con status APPROVED
When hago PATCH /admin/applications/:id con body { status: "IN_REVIEW" }
Then recibo HTTP 400 con mensaje "Solo se puede marcar en revisión desde PENDING"

US-INST-004: Aprobar solicitud y activar acceso Creator Studio

Section titled “US-INST-004: Aprobar solicitud y activar acceso Creator Studio”

Como SUPER_ADMIN quiero aprobar una solicitud de instructor y vincularla con un usuario existente de la plataforma para que ese usuario reciba el rol CONTENT_ADMIN y acceda al Creator Studio.

Módulo: instructor-applications · Endpoint: POST /admin/applications/:id/approve Estado: ✅ Implementado Prioridad: Alta

AC-INST-004.1 — Aprobación con userId vinculado

Section titled “AC-INST-004.1 — Aprobación con userId vinculado”
Given que existe una solicitud en estado IN_REVIEW
And existe un usuario con id "user-42" en la plataforma
When hago POST /admin/applications/:id/approve con body { userId: "user-42", reviewerEmail: "admin@nappai.com" }
Then recibo HTTP 201
And el usuario "user-42" tiene role CONTENT_ADMIN
And se crea un CreatorProfile para "user-42" con isPublic=false
And se dispara la notificación CREATOR_APPLICATION_APPROVED al usuario
And el audit log registra la acción CREATOR_APPLICATION_APPROVED

AC-INST-004.2 — Aprobación sin userId (solicitante sin cuenta)

Section titled “AC-INST-004.2 — Aprobación sin userId (solicitante sin cuenta)”
Given que existe una solicitud en estado PENDING
When hago POST /admin/applications/:id/approve sin body (o con userId omitido)
Then recibo HTTP 201 y la solicitud queda APPROVED
And no se crea CreatorProfile ni se modifica ningún usuario

AC-INST-004.3 — No se puede aprobar una solicitud ya REJECTED

Section titled “AC-INST-004.3 — No se puede aprobar una solicitud ya REJECTED”
Given que existe una solicitud con status REJECTED
When hago POST /admin/applications/:id/approve
Then recibo HTTP 400 con "Solo se puede aprobar desde PENDING o IN_REVIEW"

US-INST-005: Rechazar solicitud con motivo

Section titled “US-INST-005: Rechazar solicitud con motivo”

Como SUPER_ADMIN quiero rechazar una solicitud indicando un motivo estandarizado para mantener coherencia en la comunicación y posibilitar re-aplicaciones futuras.

Módulo: instructor-applications · Endpoint: POST /admin/applications/:id/reject Estado: ✅ Implementado Prioridad: Alta

AC-INST-005.1 — Rechazo con motivo válido

Section titled “AC-INST-005.1 — Rechazo con motivo válido”
Given que existe una solicitud en estado IN_REVIEW
When hago POST /admin/applications/:id/reject con body
{ reason: "no_experience", message: "Necesitas experiencia real en empresa", internalNotes: "Perfil junior" }
Then recibo HTTP 201
And la solicitud tiene status REJECTED
And si la solicitud tiene un userId → se dispara notificación CREATOR_APPLICATION_REJECTED
And el audit log registra CREATOR_APPLICATION_REJECTED

AC-INST-005.2 — Motivo de rechazo inválido rechazado

Section titled “AC-INST-005.2 — Motivo de rechazo inválido rechazado”
Given que existe una solicitud en estado PENDING
When hago POST /admin/applications/:id/reject con { reason: "motivo_inventado" }
Then recibo HTTP 400 con "Motivo de rechazo no válido"

US-INST-006: Completar perfil de instructor tras aprobación

Section titled “US-INST-006: Completar perfil de instructor tras aprobación”

Como instructor aprobado (CONTENT_ADMIN) quiero completar mi perfil público con bio, foto, especialidades y redes sociales para que los estudiantes puedan conocerme antes de inscribirse a mis rutas.

Módulo: creators · Endpoints: GET /creator/me/profile, PATCH /creator/me/profile, POST /creator/me/avatar Página: /creator/profileCreatorProfileSettingsPage Estado: ✅ Implementado Prioridad: Alta

AC-INST-006.1 — Guardar cambios de perfil

Section titled “AC-INST-006.1 — Guardar cambios de perfil”
Given que estoy autenticado como CONTENT_ADMIN con CreatorProfile existente
When hago PATCH /creator/me/profile con
{ displayName: "Ana García", headline: "LLM Expert", bio: "...", specialties: ["LLMs", "NLP"], yearsExperience: 8 }
Then recibo HTTP 200 con el perfil actualizado
And los campos se persisten en CreatorProfile

AC-INST-006.2 — Subida de avatar (imagen cuadrada)

Section titled “AC-INST-006.2 — Subida de avatar (imagen cuadrada)”
Given que estoy autenticado como CONTENT_ADMIN
When hago POST /creator/me/avatar con un archivo JPEG de 200×200px vía multipart/form-data
Then recibo HTTP 201 con { ok: true, avatarUrl: "https://..." }
And el campo avatarUrl del CreatorProfile apunta a la URL S3

AC-INST-006.3 — Rechazo de archivos de avatar demasiado grandes

Section titled “AC-INST-006.3 — Rechazo de archivos de avatar demasiado grandes”
Given que intento subir un avatar de 6 MB (> límite de 5 MB)
When hago POST /creator/me/avatar
Then recibo HTTP 413 o error de validación de multer

AC-INST-006.4 — Especialidades máximo 6

Section titled “AC-INST-006.4 — Especialidades máximo 6”
Given que mi perfil ya tiene 6 especialidades seleccionadas
When intento seleccionar una séptima especialidad en el frontend
Then el botón está deshabilitado y no se envía la mutación

US-INST-007: Ver estadísticas de instructor en el dashboard

Section titled “US-INST-007: Ver estadísticas de instructor en el dashboard”

Como instructor aprobado quiero ver mis KPIs principales (estudiantes, rating, rutas publicadas, tasa de completado) para evaluar el impacto de mi contenido.

Módulo: creators · Endpoint: GET /creator/me/stats Página: /creator/dashboardCreatorDashboardPage Estado: ✅ Implementado Prioridad: Alta

AC-INST-007.1 — Dashboard devuelve métricas reales

Section titled “AC-INST-007.1 — Dashboard devuelve métricas reales”
Given que estoy autenticado como CONTENT_ADMIN
And tengo 3 rutas publicadas con 150 estudiantes en total
When hago GET /creator/me/stats
Then recibo HTTP 200 con
{ totalStudents: 150, avgRating: number, publishedPaths: 3, draftPaths: number,
completionRate: number, studentsThisMonth: number, activeStudents30d: number }

AC-INST-007.2 — Dashboard vacío para instructor sin rutas

Section titled “AC-INST-007.2 — Dashboard vacío para instructor sin rutas”
Given que soy CONTENT_ADMIN con CreatorProfile pero sin rutas
When hago GET /creator/me/stats
Then recibo HTTP 200 con todos los contadores a 0
And no se produce error 500

US-INST-008: Progreso de badges de instructor

Section titled “US-INST-008: Progreso de badges de instructor”

Como instructor quiero ver mis badges obtenidos y el progreso hacia los siguientes para entender cómo avanzar en el programa de reconocimiento de AcademIA.

Módulo: creators · Endpoint: GET /creator/me/badges Página: /creator/badgesCreatorBadgesPage Estado: ✅ Implementado Prioridad: Media

AC-INST-008.1 — Obtener progreso de badges

Section titled “AC-INST-008.1 — Obtener progreso de badges”
Given que estoy autenticado como CONTENT_ADMIN
When hago GET /creator/me/badges
Then recibo un array donde cada item contiene
{ badge: { slug, name, tier, iconEmoji }, granted: boolean, progress: number (0-100), currentValue, targetValue }

AC-INST-008.2 — Auto-otorgamiento de badge al superar criterio

Section titled “AC-INST-008.2 — Auto-otorgamiento de badge al superar criterio”
Given que tengo 100 estudiantes (criterio de "primeros-estudiantes" = 50 estudiantes)
When hago GET /creator/me/badges
Then el badge "primeros-estudiantes" tiene granted=true
And se ha creado el registro CreatorBadgeGrant en BD automáticamente

US-INST-009: Perfil público del instructor

Section titled “US-INST-009: Perfil público del instructor”

Como visitante o estudiante quiero ver el perfil público de un instructor para conocer su experiencia, especialidades y rutas disponibles antes de inscribirme.

Módulo: creators · Endpoint: GET /creators/:username Página: /creators/:usernameCreatorProfilePage Estado: ✅ Implementado Prioridad: Media

AC-INST-009.1 — Perfil público accesible sin autenticación

Section titled “AC-INST-009.1 — Perfil público accesible sin autenticación”
Given que existe un CreatorProfile con username "ana-garcia" e isPublic=true
When hago GET /creators/ana-garcia (sin JWT)
Then recibo HTTP 200 con displayName, bio, specialties, badges, paths publicados y stats reales

AC-INST-009.2 — Perfil privado no visible al público

Section titled “AC-INST-009.2 — Perfil privado no visible al público”
Given que existe un CreatorProfile con isPublic=false (recién aprobado)
When hago GET /creators/ana-garcia (sin JWT)
Then recibo HTTP 404

US-INST-010: Invitar co-instructor a una ruta

Section titled “US-INST-010: Invitar co-instructor a una ruta”

Como instructor coordinador (CONTENT_ADMIN) quiero invitar a otro instructor a colaborar en una de mis rutas asignándole un rol y permisos específicos para distribuir el trabajo de creación de contenido.

Módulo: path-instructors · Endpoints: POST /paths/:pathId/instructors/invite, GET /paths/:pathId/instructors Estado: ✅ Implementado Prioridad: Media

AC-INST-010.1 — Invitar co-instructor con email y rol

Section titled “AC-INST-010.1 — Invitar co-instructor con email y rol”
Given que soy coordinador de la ruta "path-ia-banca"
When hago POST /paths/path-ia-banca/instructors/invite con
{ email: "colega@empresa.com", role: "CONTRIBUTOR", permissions: { canEditContent: true } }
Then recibo HTTP 201 con { ok: true }
And se crea un registro PathInstructor con inviteStatus=PENDING
And se envía notificación al destinatario (PATH_INSTRUCTOR_INVITED)
And el audit log registra PATH_INSTRUCTOR_INVITED

AC-INST-010.2 — Listar co-instructores de una ruta

Section titled “AC-INST-010.2 — Listar co-instructores de una ruta”
Given que la ruta "path-ia-banca" tiene 2 co-instructores
When hago GET /paths/path-ia-banca/instructors
Then recibo un array con id, userId, role, inviteStatus, permissions y user.fullName de cada uno

US-INST-011: Aceptar o rechazar invitación de co-instructor

Section titled “US-INST-011: Aceptar o rechazar invitación de co-instructor”

Como instructor invitado quiero aceptar o rechazar una invitación para colaborar en una ruta para tener control sobre las colaboraciones en las que participo.

Módulo: path-instructors · Endpoints: POST /path-instructors/invitations/:token/accept, POST /path-instructors/invitations/:token/decline Estado: ✅ Implementado Prioridad: Media

AC-INST-011.1 — Aceptar invitación con token válido

Section titled “AC-INST-011.1 — Aceptar invitación con token válido”
Given que tengo un token de invitación válido y no expirado
And estoy autenticado como CONTENT_ADMIN
When hago POST /path-instructors/invitations/:token/accept
Then recibo HTTP 201 con { ok: true, pathSlug: "path-ia-banca" }
And el PathInstructor tiene inviteStatus=ACCEPTED y acceptedAt != null
And el audit log registra PATH_INSTRUCTOR_ACCEPTED
Given que tengo un token de invitación válido
When hago POST /path-instructors/invitations/:token/decline
Then recibo HTTP 201 con { ok: true }
And el PathInstructor tiene inviteStatus=DECLINED
And el audit log registra PATH_INSTRUCTOR_DECLINED

AC-INST-011.3 — Token expirado no procesable

Section titled “AC-INST-011.3 — Token expirado no procesable”
Given que el token de invitación ha expirado
When hago POST /path-instructors/invitations/:token/accept
Then recibo HTTP 400 o 404 indicando que la invitación no es válida

TC-INST-001 — Flujo completo de solicitud pública

Section titled “TC-INST-001 — Flujo completo de solicitud pública”

Cubre: AC-INST-001.1, AC-INST-001.2 Tipo: E2E (API)

test('TC-INST-001: submit instructor application — success and duplicate blocked', async ({ request }) => {
const payload = {
fullName: 'Ana García López',
email: `ana+${Date.now()}@test.com`,
linkedinUrl: 'linkedin.com/in/anagarcia',
currentRole: 'Data Scientist Senior en empresa financiera con 8 años de experiencia',
courseProposal: 'Propongo un curso de 10 lecciones prácticas sobre aplicación de LLMs en análisis de riesgo crediticio, con casos reales del sector bancario español y ejercicios hands-on con Python y LangChain.',
}
// Primera solicitud — debe funcionar
const res1 = await request.post('/instructor-applications', { data: payload })
expect(res1.status()).toBe(201)
const body1 = await res1.json()
expect(body1).toHaveProperty('id')
expect(body1.message).toContain('Solicitud recibida')
// Segunda solicitud con el mismo email — debe bloquearse
const res2 = await request.post('/instructor-applications', { data: payload })
expect(res2.status()).toBe(409)
const body2 = await res2.json()
expect(body2.message).toContain('Ya existe una solicitud activa')
})

TC-INST-002 — Validación de campos del formulario

Section titled “TC-INST-002 — Validación de campos del formulario”

Cubre: AC-INST-001.3 Tipo: E2E (API)

test('TC-INST-002: reject application with missing required fields', async ({ request }) => {
// courseProposal muy corto (< 100 chars)
const res = await request.post('/instructor-applications', {
data: {
fullName: 'Test User',
email: 'test@example.com',
linkedinUrl: 'linkedin.com/in/test',
currentRole: 'Developer with 5 years of AI experience in multiple projects',
courseProposal: 'Demasiado corto',
},
})
expect(res.status()).toBe(400)
// fullName vacío
const res2 = await request.post('/instructor-applications', {
data: {
fullName: 'A',
email: 'test2@example.com',
linkedinUrl: 'linkedin.com/in/test',
currentRole: 'Descripción válida del rol con suficiente contenido',
courseProposal: 'x'.repeat(110),
},
})
expect(res2.status()).toBe(400)
})

TC-INST-003 — Listar y filtrar solicitudes como SUPER_ADMIN

Section titled “TC-INST-003 — Listar y filtrar solicitudes como SUPER_ADMIN”

Cubre: AC-INST-002.1, AC-INST-002.2, AC-INST-002.4 Tipo: E2E (API)

test('TC-INST-003: admin can list and filter applications', async ({ request }) => {
// Autenticar como SUPER_ADMIN
const loginRes = await request.post('/auth/login', {
data: { email: process.env.SUPER_ADMIN_EMAIL, password: process.env.SUPER_ADMIN_PASSWORD },
})
const { accessToken } = await loginRes.json()
const authHeaders = { Authorization: `Bearer ${accessToken}` }
// Listar todas
const listRes = await request.get('/admin/applications', { headers: authHeaders })
expect(listRes.status()).toBe(200)
const apps = await listRes.json()
expect(Array.isArray(apps)).toBe(true)
// Filtrar por PENDING
const pendingRes = await request.get('/admin/applications?status=PENDING', { headers: authHeaders })
expect(pendingRes.status()).toBe(200)
const pending = await pendingRes.json()
pending.forEach((a: any) => expect(a.status).toBe('PENDING'))
// Acceso denegado sin auth
const noAuthRes = await request.get('/admin/applications')
expect(noAuthRes.status()).toBe(401)
})

TC-INST-004 — Aprobación completa de solicitud con usuario vinculado

Section titled “TC-INST-004 — Aprobación completa de solicitud con usuario vinculado”

Cubre: AC-INST-004.1 Tipo: E2E (API)

test('TC-INST-004: approve application links user as CONTENT_ADMIN and creates CreatorProfile', async ({ request }) => {
const adminLogin = await request.post('/auth/login', {
data: { email: process.env.SUPER_ADMIN_EMAIL, password: process.env.SUPER_ADMIN_PASSWORD },
})
const { accessToken: adminToken } = await adminLogin.json()
const auth = { Authorization: `Bearer ${adminToken}` }
// Crear solicitud
const appRes = await request.post('/instructor-applications', {
data: {
fullName: 'Carlos Instructor',
email: `carlos+${Date.now()}@test.com`,
linkedinUrl: 'linkedin.com/in/carlos',
currentRole: 'Senior ML Engineer con amplia trayectoria en banca y fintech',
courseProposal: 'Propongo un itinerario completo sobre MLOps aplicado a modelos de scoring crediticio, incluyendo monitorización, drift detection y compliance regulatorio en el sector financiero europeo.',
},
})
const { id: appId } = await appRes.json()
// Aprobar sin userId (no requiere usuario existente para el test básico)
const approveRes = await request.post(`/admin/applications/${appId}/approve`, {
headers: auth,
data: { reviewerEmail: process.env.SUPER_ADMIN_EMAIL },
})
expect(approveRes.status()).toBe(201)
const approvedApp = await approveRes.json()
expect(approvedApp.status).toBe('APPROVED')
})

TC-INST-005 — Rechazo de solicitud con motivo válido e inválido

Section titled “TC-INST-005 — Rechazo de solicitud con motivo válido e inválido”

Cubre: AC-INST-005.1, AC-INST-005.2 Tipo: E2E (API)

test('TC-INST-005: reject application with valid and invalid reasons', async ({ request }) => {
const adminLogin = await request.post('/auth/login', {
data: { email: process.env.SUPER_ADMIN_EMAIL, password: process.env.SUPER_ADMIN_PASSWORD },
})
const { accessToken } = await adminLogin.json()
const auth = { Authorization: `Bearer ${accessToken}` }
// Crear solicitud para rechazar
const appRes = await request.post('/instructor-applications', {
data: {
fullName: 'Luis Candidato',
email: `luis+${Date.now()}@test.com`,
linkedinUrl: 'linkedin.com/in/luis',
currentRole: 'Junior Developer sin experiencia real en proyectos de producción',
courseProposal: 'Quiero enseñar Python básico para principiantes sin conocimiento previo de programación, cubriendo variables y funciones desde cero con ejemplos sencillos y ejercicios fáciles.',
},
})
const { id: appId } = await appRes.json()
// Rechazar con motivo válido
const rejectRes = await request.post(`/admin/applications/${appId}/reject`, {
headers: auth,
data: { reason: 'no_experience', message: 'Perfil sin experiencia real demostrable', internalNotes: 'Junior sin proyectos relevantes' },
})
expect(rejectRes.status()).toBe(201)
// Crear segunda solicitud para probar motivo inválido
const appRes2 = await request.post('/instructor-applications', {
data: {
fullName: 'María Test',
email: `maria+${Date.now()}@test.com`,
linkedinUrl: 'linkedin.com/in/maria',
currentRole: 'Product Manager con experiencia en startups de IA y equipos de datos multidisciplinares',
courseProposal: 'Propongo un curso sobre product management para productos de IA, incluyendo cómo definir métricas, gestionar equipos de datos y hacer roadmaps orientados al impacto del negocio.',
},
})
const { id: appId2 } = await appRes2.json()
const badRejectRes = await request.post(`/admin/applications/${appId2}/reject`, {
headers: auth,
data: { reason: 'motivo_inventado', message: 'Mensaje de prueba' },
})
expect(badRejectRes.status()).toBe(400)
})

TC-INST-006 — Actualización de perfil de instructor

Section titled “TC-INST-006 — Actualización de perfil de instructor”

Cubre: AC-INST-006.1, AC-INST-006.4 Tipo: E2E (API)

test('TC-INST-006: CONTENT_ADMIN can update creator profile', async ({ request }) => {
// Login como CONTENT_ADMIN
const loginRes = await request.post('/auth/login', {
data: { email: process.env.CONTENT_ADMIN_EMAIL, password: process.env.CONTENT_ADMIN_PASSWORD },
})
const { accessToken } = await loginRes.json()
const auth = { Authorization: `Bearer ${accessToken}` }
const updateRes = await request.patch('/creator/me/profile', {
headers: auth,
data: {
displayName: 'Ana García López',
headline: 'LLM Expert & AI Educator',
bio: 'Especialista en aplicaciones prácticas de LLMs en el sector bancario con 8 años de experiencia.',
specialties: ['LLMs', 'NLP', 'Python', 'Data Science', 'Machine Learning', 'MLOps'],
yearsExperience: 8,
linkedinUrl: 'https://linkedin.com/in/anagarcia',
},
})
expect(updateRes.status()).toBe(200)
const profile = await updateRes.json()
expect(profile.displayName).toBe('Ana García López')
expect(profile.specialties).toHaveLength(6)
// Verificar que el GET devuelve los cambios
const getRes = await request.get('/creator/me/profile', { headers: auth })
expect(getRes.status()).toBe(200)
const fetched = await getRes.json()
expect(fetched.headline).toBe('LLM Expert & AI Educator')
expect(fetched.yearsExperience).toBe(8)
})

TC-INST-007 — Perfil público inaccesible si isPublic=false

Section titled “TC-INST-007 — Perfil público inaccesible si isPublic=false”

Cubre: AC-INST-009.2 Tipo: E2E (API)

test('TC-INST-007: private creator profile returns 404 to public', async ({ request }) => {
// Este test asume que existe un CreatorProfile con isPublic=false y username conocido
// En entorno de CI usar un fixture de BD o crear el perfil con un helper
const res = await request.get('/creators/instructor-draft-profile')
expect(res.status()).toBe(404)
})

TC-INST-008 — Dashboard de stats del instructor

Section titled “TC-INST-008 — Dashboard de stats del instructor”

Cubre: AC-INST-007.1, AC-INST-007.2 Tipo: E2E (API)

test('TC-INST-008: creator dashboard stats endpoint', async ({ request }) => {
const loginRes = await request.post('/auth/login', {
data: { email: process.env.CONTENT_ADMIN_EMAIL, password: process.env.CONTENT_ADMIN_PASSWORD },
})
const { accessToken } = await loginRes.json()
const auth = { Authorization: `Bearer ${accessToken}` }
const statsRes = await request.get('/creator/me/stats', { headers: auth })
expect(statsRes.status()).toBe(200)
const stats = await statsRes.json()
expect(stats).toHaveProperty('totalStudents')
expect(stats).toHaveProperty('avgRating')
expect(stats).toHaveProperty('publishedPaths')
expect(stats).toHaveProperty('draftPaths')
expect(stats).toHaveProperty('completionRate')
expect(stats).toHaveProperty('activeStudents30d')
expect(typeof stats.totalStudents).toBe('number')
expect(typeof stats.avgRating).toBe('number')
})

TC-INST-009 — Invitar co-instructor a una ruta

Section titled “TC-INST-009 — Invitar co-instructor a una ruta”

Cubre: AC-INST-010.1, AC-INST-010.2 Tipo: E2E (API)

test('TC-INST-009: coordinator can invite co-instructor to a path', async ({ request }) => {
const coordinatorLogin = await request.post('/auth/login', {
data: { email: process.env.CONTENT_ADMIN_EMAIL, password: process.env.CONTENT_ADMIN_PASSWORD },
})
const { accessToken } = await coordinatorLogin.json()
const auth = { Authorization: `Bearer ${accessToken}` }
// Obtener pathId de una de las rutas del coordinador
const pathsRes = await request.get('/creator/me/paths', { headers: auth })
expect(pathsRes.status()).toBe(200)
const paths = await pathsRes.json()
expect(paths.length).toBeGreaterThan(0)
const pathId = paths[0].id
// Invitar co-instructor
const inviteRes = await request.post(`/paths/${pathId}/instructors/invite`, {
headers: auth,
data: {
email: process.env.COLLABORATOR_EMAIL,
role: 'CONTRIBUTOR',
permissions: { canEditContent: true, canViewStudents: false },
},
})
expect(inviteRes.status()).toBe(201)
// Listar co-instructores
const listRes = await request.get(`/paths/${pathId}/instructors`, { headers: auth })
expect(listRes.status()).toBe(200)
const instructors = await listRes.json()
const invited = instructors.find((i: any) => i.user.email === process.env.COLLABORATOR_EMAIL)
expect(invited).toBeDefined()
expect(invited.role).toBe('CONTRIBUTOR')
expect(invited.inviteStatus).toBe('PENDING')
})

TC-INST-010 — KPIs de solicitudes disponibles para SUPER_ADMIN

Section titled “TC-INST-010 — KPIs de solicitudes disponibles para SUPER_ADMIN”

Cubre: AC-INST-002.3 Tipo: E2E (API)

test('TC-INST-010: application stats endpoint returns all counters', async ({ request }) => {
const loginRes = await request.post('/auth/login', {
data: { email: process.env.SUPER_ADMIN_EMAIL, password: process.env.SUPER_ADMIN_PASSWORD },
})
const { accessToken } = await loginRes.json()
const auth = { Authorization: `Bearer ${accessToken}` }
const statsRes = await request.get('/admin/applications/stats', { headers: auth })
expect(statsRes.status()).toBe(200)
const stats = await statsRes.json()
expect(stats).toHaveProperty('total')
expect(stats).toHaveProperty('pending')
expect(stats).toHaveProperty('inReview')
expect(stats).toHaveProperty('approved')
expect(stats).toHaveProperty('rejected')
expect(stats).toHaveProperty('withdrawn')
expect(stats).toHaveProperty('weeklyNew')
// Todos los valores deben ser enteros no negativos
Object.values(stats).forEach((v) => {
expect(typeof v).toBe('number')
expect(v as number).toBeGreaterThanOrEqual(0)
})
})

flowchart TD
V[Visitante / Usuario] -->|POST /instructor-applications| A[Solicitud PENDING]
A -->|PATCH status=IN_REVIEW| B[Solicitud IN_REVIEW]
A -->|POST /approve| C[Solicitud APPROVED]
B -->|POST /approve| C
B -->|POST /reject| D[Solicitud REJECTED]
B -->|POST /request-info| B
A -->|POST /reject| D
C -->|userId vinculado| E[User.role = CONTENT_ADMIN]
E --> F[CreatorProfile isPublic=false]
F -->|PATCH /creator/me/profile| G[Perfil completado]
G -->|SUPER_ADMIN activa isPublic| H[Perfil público visible]
E --> I[Acceso Creator Studio /creator/*]