Skip to content

Catálogo y Enrolamiento

Módulos API: paths Páginas web: CatalogPage (/catalog), AppCatalogPage (/dashboard/catalog), PathDetailPage (/paths/:slug) Spec Playwright: apps/e2e/tests/student/catalog.spec.ts — 📋 pendiente


MétodoEndpointAuthDescripción
GET/pathsNoListado del catálogo (solo isPublished: true, sin organizationId) con filtros y ordenación
GET/paths/:slugNoDetalle público de un path con módulos, instructor y stats en vivo
GET/paths/:slug/previewSí (CONTENT_ADMIN / SUPER_ADMIN)Detalle con contenido no publicado para creators
GET/paths/:slug/preview/structureSí (CONTENT_ADMIN / SUPER_ADMIN)Estructura lateral con lecciones no publicadas
GET/paths/:slug/preview/lessons/:lessonIdSí (CONTENT_ADMIN / SUPER_ADMIN)Lección individual en modo preview
GET/paths/:slug/learnEstructura con progreso del usuario (sidebar del player)
GET/paths/:slug/enrollmentEstado de enrolamiento del usuario autenticado
POST/paths/:slug/enrollEnrolar al usuario; valida límite de inscripciones del plan
GET/paths/:slug/reviewsNoReviews paginadas del path (10 por página)
GET/paths/:slug/reviews/mineMi review para el path (null si no existe)
POST/paths/:slug/reviewsCrear review; requiere progreso >= 50 % y no haber revisado antes
PATCH/paths/:slug/reviews/mineEditar mi review existente
GET/paths/:id/preview-imageNoImagen de preview del path (binaria, con caché 24 h)
GET/paths/:id/banner-imageNoImagen de banner del path (binaria, con caché 24 h)
ParamTipoValores
rolestringrol profesional (ej. Ventas)
industrystringsector (ej. Tecnología)
accessenumfree · paid · all
levelstringPrincipiante · Intermedio · Avanzado
qstringbúsqueda full-text en título y descripción
sortenumpopular (defecto) · az · hrs · new
certenumall · yes

US-CAT-001: Explorar el catálogo público sin autenticarse

Section titled “US-CAT-001: Explorar el catálogo público sin autenticarse”

Como visitante no registrado Quiero ver el catálogo completo de rutas de aprendizaje Para descubrir qué ofrece la plataforma antes de registrarme

Criterios: AC-CAT-001 · Casos: TC-CAT-001, TC-CAT-002 · Prioridad: Alta · ✅ Implementado


Section titled “US-CAT-002: Filtrar y buscar paths en el catálogo”

Como visitante o usuario autenticado Quiero filtrar el catálogo por rol, sector, nivel, duración, acceso y certificado, y buscar por texto libre Para encontrar rutas relevantes para mi perfil profesional sin revisar todo el catálogo

Criterios: AC-CAT-002 · Casos: TC-CAT-003, TC-CAT-004 · Prioridad: Alta · ✅ Implementado


US-CAT-003: Ver el detalle de un path antes de enrolarme

Section titled “US-CAT-003: Ver el detalle de un path antes de enrolarme”

Como visitante o usuario autenticado Quiero ver la descripción completa, módulos, instructor, valoraciones y condiciones de acceso de un path Para tomar una decisión informada antes de inscribirme

Criterios: AC-CAT-003 · Casos: TC-CAT-005, TC-CAT-006 · Prioridad: Alta · ✅ Implementado


US-CAT-004: Enrolarme en un path según mi nivel de acceso

Section titled “US-CAT-004: Enrolarme en un path según mi nivel de acceso”

Como usuario autenticado Quiero inscribirme en un path que esté incluido en mi plan Para comenzar a aprender y acceder al contenido de las lecciones

Criterios: AC-CAT-004 · Casos: TC-CAT-007, TC-CAT-008, TC-CAT-009 · Prioridad: Alta · ✅ Implementado


US-CAT-005: Ver CTA de upgrade cuando el path requiere plan superior

Section titled “US-CAT-005: Ver CTA de upgrade cuando el path requiere plan superior”

Como usuario con plan FREE o STARTER Quiero ver claramente qué plan necesito para acceder a un path bloqueado Para saber qué tengo que contratar si quiero acceder al contenido

Criterios: AC-CAT-005 · Casos: TC-CAT-010 · Prioridad: Alta · ✅ Implementado


US-CAT-006: No poder enrolarme si he alcanzado el límite del plan

Section titled “US-CAT-006: No poder enrolarme si he alcanzado el límite del plan”

Como usuario FREE con 0 inscripciones disponibles restantes Quiero recibir un mensaje claro indicando que he llegado al límite Para entender que debo mejorar mi plan para inscribirme en más rutas

Criterios: AC-CAT-006 · Casos: TC-CAT-011 · Prioridad: Alta · ✅ Implementado


US-CAT-007: Dejar una review en un path con suficiente progreso

Section titled “US-CAT-007: Dejar una review en un path con suficiente progreso”

Como usuario enrolado que ha completado al menos el 50 % del path Quiero dejar una valoración de 1-5 estrellas con comentario opcional Para ayudar a otros usuarios a elegir contenido de calidad

Criterios: AC-CAT-007 · Casos: TC-CAT-012, TC-CAT-013 · Prioridad: Media · ✅ Implementado


Como usuario que ya dejó una review Quiero poder editar mi puntuación o comentario Para reflejar un cambio de opinión después de completar el path

Criterios: AC-CAT-008 · Casos: TC-CAT-014 · Prioridad: Media · ✅ Implementado


US-CAT-009: Ver el modo preview de un path como creator

Section titled “US-CAT-009: Ver el modo preview de un path como creator”

Como CONTENT_ADMIN o SUPER_ADMIN Quiero previsualizar el path con todos sus módulos (incluso no publicados) tal como lo verá el estudiante Para revisar la presentación del contenido antes de publicarlo

Criterios: AC-CAT-009 · Casos: TC-CAT-015, TC-CAT-016 · Prioridad: Media · ✅ Implementado


AC-CAT-001: Catálogo público accesible sin autenticación

Section titled “AC-CAT-001: Catálogo público accesible sin autenticación”

US: US-CAT-001

AC-CAT-001.1 — Catálogo carga sin sesión

Dado que soy un visitante no registrado
Cuando navego a "/catalog"
Entonces veo la lista de rutas publicadas (isPublished: true)
Y no se me solicita autenticación
Y cada tarjeta muestra: título, descripción (2 líneas), nivel, horas, número de módulos, access level badge

AC-CAT-001.2 — Solo paths publicados son visibles

Dado que existe un path con isPublished: false
Cuando accedo a "GET /paths"
Entonces ese path NO aparece en el listado
Y la respuesta HTTP es 200

AC-CAT-001.3 — Paths de organizaciones no aparecen en el catálogo público

Dado que existe un path con organizationId != null
Cuando accedo a "GET /paths" sin autenticación
Entonces ese path NO aparece en el catálogo público

Section titled “AC-CAT-002: Filtrado y búsqueda del catálogo”

US: US-CAT-002

AC-CAT-002.1 — Filtro por acceso (free/paid)

Dado que el catálogo contiene paths con accessLevel PUBLIC, FREE_REGISTERED y PAID_PRO
Cuando aplico el filtro "Gratuito" (access=free)
Entonces solo veo paths con accessLevel PUBLIC o FREE_REGISTERED
Y los paths PAID_PRO no aparecen en los resultados

AC-CAT-002.2 — Búsqueda full-text

Dado que hay un path con título "IA para Ventas"
Cuando escribo "ventas" en el campo de búsqueda
Entonces ese path aparece en los resultados
Y los paths sin "ventas" en título, descripción ni roles no aparecen

AC-CAT-002.3 — Ordenación: sin resultados

Dado que busco un texto que no coincide con ningún path
Entonces veo el estado vacío con el mensaje "Sin resultados"
Y el botón "Limpiar filtros" está visible y funcional

AC-CAT-002.4 — Cambio de filtro reinicia la paginación a página 1

Dado que estoy en la página 2 del catálogo
Cuando cambio cualquier filtro
Entonces vuelvo a la página 1 automáticamente

US: US-CAT-003

AC-CAT-003.1 — Página de detalle accesible sin autenticación

Dado que existe un path publicado con slug "ia-para-ventas"
Cuando accedo a "/paths/ia-para-ventas" sin autenticación
Entonces veo: título, descripción larga, access badge, nivel de dificultad
Y veo el accordion de módulos con conteo de lecciones por módulo
Y veo la información del instructor (si es público)
Y el CTA muestra "Registrarse gratis"

AC-CAT-003.2 — Path no encontrado devuelve 404

Dado que accedo a "GET /paths/slug-inexistente"
Entonces la API responde con status 404
Y en la UI veo el estado de error "Ruta no encontrada"

AC-CAT-003.3 — Path no publicado no es accesible públicamente

Dado que existe un path con isPublished: false y slug "borrador-path"
Cuando accedo a "GET /paths/borrador-path"
Entonces la API responde con status 404

US: US-CAT-004

AC-CAT-004.1 — Enrolamiento exitoso en path gratuito

Dado que soy usuario FREE autenticado
Y existe un path con accessLevel FREE_REGISTERED
Y no estoy inscrito en ese path
Cuando hago POST a "/paths/:slug/enroll"
Entonces la API responde con status 201 y { enrolled: true, pathId, pathTitle }
Y soy redirigido a "/learn/:slug"
Y recibo una notificación de inscripción exitosa

AC-CAT-004.2 — Doble enrolamiento retorna 409

Dado que ya estoy inscrito en el path
Cuando intento hacer POST a "/paths/:slug/enroll" de nuevo
Entonces la API responde con status 409 y código "Already enrolled"
Y en la UI soy redirigido directamente a "/learn/:slug"

AC-CAT-004.3 — Visitante no autenticado redirigido al registro

Dado que no tengo sesión activa
Cuando hago clic en "Registrarse gratis" en la página de detalle de un path
Entonces soy redirigido a "/auth/register"

AC-CAT-005: CTA de upgrade para contenido bloqueado

Section titled “AC-CAT-005: CTA de upgrade para contenido bloqueado”

US: US-CAT-005

AC-CAT-005.1 — Tarjeta muestra plan requerido cuando el usuario no tiene acceso

Dado que soy usuario FREE (nivel numérico 1)
Y existe un path con accessLevel PAID_PRO (nivel numérico 3)
Cuando veo la tarjeta de ese path en el catálogo
Entonces el botón muestra "Requiere Plan Pro" con ícono de candado
Y al hacer clic soy redirigido a "/pricing"

AC-CAT-005.2 — Usuario con nivel suficiente no ve el candado

Dado que soy usuario PRO
Y existe un path con accessLevel PAID_PRO
Cuando veo la tarjeta de ese path
Entonces el botón muestra "Empezar →" sin candado

AC-CAT-006: Límite de inscripciones del plan

Section titled “AC-CAT-006: Límite de inscripciones del plan”

US: US-CAT-006

AC-CAT-006.1 — Enrolamiento rechazado al alcanzar el límite

Dado que soy usuario FREE con maxEnrollments = 3
Y ya tengo 3 paths inscritos
Cuando intento hacer POST a "/paths/:slug/enroll" en un path nuevo
Entonces la API responde con status 403 y código "ENROLLMENT_LIMIT_REACHED"
Y el body incluye { limit: 3, current: 3 }

AC-CAT-006.2 — Banner de límite alcanzado en AppCatalogPage

Dado que soy usuario FREE y he alcanzado mi límite de inscripciones
Cuando visito "/dashboard/catalog"
Entonces veo un banner rojo indicando "Has alcanzado el límite de N inscripciones"
Y aparece el enlace "Mejorar plan →" que apunta a "/pricing"
Y los botones "Empezar" de paths no inscritos aparecen deshabilitados con tooltip "Límite de inscripciones alcanzado"

US: US-CAT-007

AC-CAT-007.1 — Review creada con progreso suficiente

Dado que soy usuario autenticado e inscrito en el path "ia-para-ventas"
Y he completado al menos el 50 % de las lecciones publicadas del path
Cuando hago POST a "/paths/ia-para-ventas/reviews" con { rating: 5, comment: "Excelente" }
Entonces la API responde con status 201 y los datos de la review
Y el avgRating y reviewCount del path se recalculan automáticamente

AC-CAT-007.2 — Review rechazada con menos del 50 % de progreso

Dado que tengo menos del 50 % del path completado
Cuando intento crear una review
Entonces la API responde con status 403 y código "INSUFFICIENT_PROGRESS"
Y el body incluye { currentPct: <número>, message: "Complete at least 50%..." }

AC-CAT-007.3 — Review duplicada retorna 409

Dado que ya existe una review mía para este path
Cuando intento crear otra review
Entonces la API responde con status 409 y código "REVIEW_EXISTS"

US: US-CAT-008

AC-CAT-008.1 — Actualización exitosa de rating y comentario

Dado que tengo una review existente con rating 3 para el path "ia-para-ventas"
Cuando hago PATCH a "/paths/ia-para-ventas/reviews/mine" con { rating: 4, comment: "Mejoró" }
Entonces la API responde con status 200 y los datos actualizados
Y el avgRating del path se recalcula con el nuevo valor

AC-CAT-008.2 — Edición rechazada si no existe review previa

Dado que nunca he dejado review en este path
Cuando intento PATCH "/paths/:slug/reviews/mine"
Entonces la API responde con status 404

AC-CAT-009: Preview de un path como creator

Section titled “AC-CAT-009: Preview de un path como creator”

US: US-CAT-009

AC-CAT-009.1 — CONTENT_ADMIN puede ver preview solo de sus propios paths

Dado que soy CONTENT_ADMIN y el path "ia-para-ventas" me pertenece
Cuando accedo a "GET /paths/ia-para-ventas/preview"
Entonces la API responde con status 200 e incluye módulos no publicados

AC-CAT-009.2 — CONTENT_ADMIN no puede previsualizar paths de otro creator

Dado que soy CONTENT_ADMIN y el path "otro-path" pertenece a otro creator
Cuando accedo a "GET /paths/otro-path/preview"
Entonces la API responde con status 403

AC-CAT-009.3 — Estudiante no puede acceder al endpoint preview

Dado que soy usuario FREE autenticado
Cuando accedo a "GET /paths/:slug/preview"
Entonces la API responde con status 403

TC-CAT-001: Catálogo carga sin autenticación

Section titled “TC-CAT-001: Catálogo carga sin autenticación”

AC: AC-CAT-001.1 · Prioridad: Alta · Tipo: E2E · Estado: 📋 Pendiente Spec: apps/e2e/tests/student/catalog.spec.ts

Precondiciones: Existe al menos un path publicado en la BD de test

Pasos:

  1. Navegar a /catalog sin sesión activa (contexto anónimo)
  2. Esperar que los cards se rendericen

Resultado esperado: La página muestra al menos un PathCard. No hay redirección al login. El contador de resultados muestra al menos “1 ruta”.

// sin fixtures — contexto anónimo de Playwright
const page = await context.newPage()
await page.goto('/catalog')
await expect(page.locator('[data-testid="path-card"]').first()).toBeVisible()
await expect(page).not.toHaveURL(/\/login/)

TC-CAT-002: Path de organización no aparece en catálogo público

Section titled “TC-CAT-002: Path de organización no aparece en catálogo público”

AC: AC-CAT-001.3 · Prioridad: Alta · Tipo: API · Estado: 📋 Pendiente Spec: apps/e2e/tests/student/catalog.spec.ts

// Verificar a nivel API que organizationId != null excluye el path
const res = await apiClient.get('/paths')
expect(res.status).toBe(200)
const corporateLeaked = res.data.some((p: { organizationId: string | null }) => p.organizationId !== null)
expect(corporateLeaked).toBe(false)

TC-CAT-003: Filtrar por acceso — solo gratuitos

Section titled “TC-CAT-003: Filtrar por acceso — solo gratuitos”

AC: AC-CAT-002.1 · Prioridad: Alta · Tipo: API · Estado: 📋 Pendiente Spec: apps/e2e/tests/student/catalog.spec.ts

const res = await apiClient.get('/paths?access=free')
expect(res.status).toBe(200)
const PAID_LEVELS = ['PAID_STARTER', 'PAID_PRO', 'PAID_ELITE', 'CORPORATE']
const hasPaidPath = res.data.some((p: { accessLevel: string }) => PAID_LEVELS.includes(p.accessLevel))
expect(hasPaidPath).toBe(false)

TC-CAT-004: Búsqueda full-text sin resultados

Section titled “TC-CAT-004: Búsqueda full-text sin resultados”

AC: AC-CAT-002.3 · Prioridad: Media · Tipo: E2E · Estado: 📋 Pendiente Spec: apps/e2e/tests/student/catalog.spec.ts

// data-testid: 'catalog-search', 'catalog-empty-state', 'catalog-clear-filters'
await page.getByTestId('catalog-search').fill('xyzw123nonexistent')
await expect(page.getByTestId('catalog-empty-state')).toBeVisible()
await page.getByTestId('catalog-clear-filters').click()
await expect(page.getByTestId('catalog-empty-state')).not.toBeVisible()

TC-CAT-005: Detalle de path accesible sin autenticación

Section titled “TC-CAT-005: Detalle de path accesible sin autenticación”

AC: AC-CAT-003.1 · Prioridad: Alta · Tipo: E2E · Estado: 📋 Pendiente Spec: apps/e2e/tests/student/catalog.spec.ts

Precondiciones: Existe al menos un path publicado con slug conocido en la BD de test

// Obtener slug del primer path del catálogo
const catalogRes = await apiClient.get('/paths')
expect(catalogRes.status).toBe(200)
const slug: string = catalogRes.data[0].slug
// Verificar detalle accesible sin auth
const detailRes = await apiClient.get(`/paths/${slug}`)
expect(detailRes.status).toBe(200)
expect(detailRes.data).toMatchObject({
title: expect.any(String),
slug,
accessLevel: expect.any(String),
modules: expect.any(Array),
})

AC: AC-CAT-003.2 · Prioridad: Alta · Tipo: API · Estado: 📋 Pendiente Spec: apps/e2e/tests/student/catalog.spec.ts

const res = await apiClient.get('/paths/slug-que-no-existe-jamas')
expect(res.status).toBe(404)

TC-CAT-007: Enrolamiento exitoso — path gratuito

Section titled “TC-CAT-007: Enrolamiento exitoso — path gratuito”

AC: AC-CAT-004.1 · Prioridad: Alta · Tipo: E2E · Estado: 📋 Pendiente Spec: apps/e2e/tests/student/catalog.spec.ts

Precondiciones: fixtures.free existe; hay al menos un path con accessLevel: FREE_REGISTERED; el usuario no está inscrito

await loginAs(page, fixtures.free)
// Obtener un path gratuito no inscrito
const res = await apiClient.get('/paths?access=free', { headers: authHeader(fixtures.free) })
const freePath = res.data[0]
await page.goto(`/paths/${freePath.slug}`)
// data-testid: 'enroll-cta-button'
await page.getByTestId('enroll-cta-button').click()
// Debe redirigir al player
await expect(page).toHaveURL(new RegExp(`/learn/${freePath.slug}`))
// Verificar estado en API
const enrollment = await apiClient.get(`/paths/${freePath.slug}/enrollment`, {
headers: authHeader(fixtures.free),
})
expect(enrollment.data.enrolled).toBe(true)

TC-CAT-008: Doble enrolamiento redirige al player

Section titled “TC-CAT-008: Doble enrolamiento redirige al player”

AC: AC-CAT-004.2 · Prioridad: Media · Tipo: API · Estado: 📋 Pendiente Spec: apps/e2e/tests/student/catalog.spec.ts

// El fixture.starter ya está inscrito en el path de prueba
const firstEnroll = await apiClient.post(`/paths/${testPathSlug}/enroll`, {}, {
headers: authHeader(fixtures.starter),
})
// Primera inscripción — puede ser 201 o 409 si ya existía en un run anterior
expect([201, 409]).toContain(firstEnroll.status)
const secondEnroll = await apiClient.post(`/paths/${testPathSlug}/enroll`, {}, {
headers: authHeader(fixtures.starter),
})
expect(secondEnroll.status).toBe(409)

TC-CAT-009: Visitante sin sesión redirigido al registro al intentar enrolarse

Section titled “TC-CAT-009: Visitante sin sesión redirigido al registro al intentar enrolarse”

AC: AC-CAT-004.3 · Prioridad: Alta · Tipo: E2E · Estado: 📋 Pendiente Spec: apps/e2e/tests/student/catalog.spec.ts

// fixtures.anonymous — sin sesión
const page = await context.newPage()
await page.goto(`/paths/${testPathSlug}`)
// data-testid: 'enroll-cta-button' — anónimo ve "Registrarse gratis"
await page.getByTestId('enroll-cta-button').click()
await expect(page).toHaveURL(/\/auth\/register/)

TC-CAT-010: CTA de upgrade en tarjeta bloqueada

Section titled “TC-CAT-010: CTA de upgrade en tarjeta bloqueada”

AC: AC-CAT-005.1 · Prioridad: Alta · Tipo: E2E · Estado: 📋 Pendiente Spec: apps/e2e/tests/student/catalog.spec.ts

Precondiciones: Existe un path con accessLevel: PAID_PRO en el catálogo

await loginAs(page, fixtures.free) // rol FREE, nivel numérico 1
// Buscar un path de pago PRO
const res = await apiClient.get('/paths?access=paid')
const proPaths = res.data.filter((p: { accessLevel: string }) => p.accessLevel === 'PAID_PRO')
expect(proPaths.length).toBeGreaterThan(0)
await page.goto('/catalog')
// data-testid: 'path-card-upgrade-btn' (visible solo cuando locked=true)
const upgradeBtn = page.getByTestId('path-card-upgrade-btn').first()
await expect(upgradeBtn).toBeVisible()
await upgradeBtn.click()
await expect(page).toHaveURL(/\/pricing/)

TC-CAT-011: Enrolamiento rechazado por límite de plan

Section titled “TC-CAT-011: Enrolamiento rechazado por límite de plan”

AC: AC-CAT-006.1 · Prioridad: Alta · Tipo: API · Estado: 📋 Pendiente Spec: apps/e2e/tests/student/catalog.spec.ts

// Asumiendo que fixtures.free tiene el límite agotado en la BD de test
const res = await apiClient.post(`/paths/${anotherPathSlug}/enroll`, {}, {
headers: authHeader(fixtures.free),
})
expect(res.status).toBe(403)
expect(res.data.code).toBe('ENROLLMENT_LIMIT_REACHED')
expect(res.data).toMatchObject({
limit: expect.any(Number),
current: expect.any(Number),
})

TC-CAT-012: Crear review con progreso suficiente

Section titled “TC-CAT-012: Crear review con progreso suficiente”

AC: AC-CAT-007.1 · Prioridad: Media · Tipo: API · Estado: 📋 Pendiente Spec: apps/e2e/tests/student/catalog.spec.ts

// fixtures.pro asumido con >= 50% completado en testPathSlug
const res = await apiClient.post(`/paths/${testPathSlug}/reviews`, {
rating: 4,
comment: 'Muy útil para el día a día',
}, { headers: authHeader(fixtures.pro) })
expect(res.status).toBe(201)
expect(res.data).toMatchObject({
id: expect.any(String),
rating: 4,
comment: 'Muy útil para el día a día',
pathId: expect.any(String),
})

TC-CAT-013: Review rechazada con progreso insuficiente

Section titled “TC-CAT-013: Review rechazada con progreso insuficiente”

AC: AC-CAT-007.2 · Prioridad: Alta · Tipo: API · Estado: 📋 Pendiente Spec: apps/e2e/tests/student/catalog.spec.ts

// fixtures.starter asumido con < 50% completado en testPathSlug
const res = await apiClient.post(`/paths/${testPathSlug}/reviews`, {
rating: 5,
comment: 'Intentando dejar review sin completar',
}, { headers: authHeader(fixtures.starter) })
expect(res.status).toBe(403)
expect(res.data.code).toBe('INSUFFICIENT_PROGRESS')
expect(res.data.currentPct).toBeLessThan(50)

AC: AC-CAT-008.1 · Prioridad: Media · Tipo: API · Estado: 📋 Pendiente Spec: apps/e2e/tests/student/catalog.spec.ts

// fixtures.pro ya tiene review creada (ver TC-CAT-012)
const res = await apiClient.patch(`/paths/${testPathSlug}/reviews/mine`, {
rating: 5,
comment: 'Actualización: aún mejor de lo que pensaba',
}, { headers: authHeader(fixtures.pro) })
expect(res.status).toBe(200)
expect(res.data.rating).toBe(5)
expect(res.data.comment).toBe('Actualización: aún mejor de lo que pensaba')
// Verificar que la media del path se actualizó
const pathRes = await apiClient.get(`/paths/${testPathSlug}`)
expect(pathRes.data.avgRating).toBeGreaterThan(0)

TC-CAT-015: Preview bloqueado para usuario sin rol correcto

Section titled “TC-CAT-015: Preview bloqueado para usuario sin rol correcto”

AC: AC-CAT-009.3 · Prioridad: Alta · Tipo: API · Estado: 📋 Pendiente Spec: apps/e2e/tests/student/catalog.spec.ts

// fixtures.free — rol FREE, no puede acceder al preview de creator
const res = await apiClient.get(`/paths/${testPathSlug}/preview`, {
headers: authHeader(fixtures.free),
})
expect(res.status).toBe(403)

TC-CAT-016: Preview retorna módulos no publicados para SUPER_ADMIN

Section titled “TC-CAT-016: Preview retorna módulos no publicados para SUPER_ADMIN”

AC: AC-CAT-009.1 · Prioridad: Media · Tipo: API · Estado: 📋 Pendiente Spec: apps/e2e/tests/admin/paths-preview.spec.ts

// fixtures.superAdmin — SUPER_ADMIN puede previsualizar cualquier path
const publicRes = await apiClient.get(`/paths/${testPathSlug}`)
const previewRes = await apiClient.get(`/paths/${testPathSlug}/preview`, {
headers: authHeader(fixtures.superAdmin),
})
expect(previewRes.status).toBe(200)
// El preview incluye módulos no publicados — el array puede ser más largo
expect(previewRes.data.modules.length).toBeGreaterThanOrEqual(publicRes.data.modules.length)