Jobs y Plataforma
Módulos API: scheduled-jobs · apps/api/src/modules/scheduled-jobs/
Páginas web: AdminScheduledJobsPage · apps/web/src/pages/admin/platform/AdminScheduledJobsPage.tsx
RTK Query: adminJobsApi · apps/web/src/store/api/adminJobsApi.ts
Spec Playwright: apps/e2e/tests/admin/jobs.spec.ts — 📋 pendiente
Endpoints cubiertos
Section titled “Endpoints cubiertos”| Método | Endpoint | Descripción |
|---|---|---|
GET | /admin/jobs | Listado de todos los jobs con las últimas 5 ejecuciones cada uno |
GET | /admin/jobs/:slug | Detalle de un job con las últimas 10 ejecuciones |
GET | /admin/jobs/:slug/status | Estado en tiempo real: isRunning, lastStatus, lastRunAt, nextRunAt |
GET | /admin/jobs/:slug/executions?limit=N | Historial de ejecuciones paginado (máx. 100) |
PATCH | /admin/jobs/:slug | Actualizar cronExpr, timezone, isEnabled, retentionDays, config, notes |
POST | /admin/jobs/:slug/run | Lanzar el job manualmente (fire-and-forget, crea JobExecution(RUNNING)) |
GET | /admin/jobs/export-executions?slug=X | Exportar ejecuciones de un job como JSON |
Todos los endpoints requieren rol SUPER_ADMIN. Cualquier otro rol recibe 403 Forbidden.
User Stories
Section titled “User Stories”US-ADM-JOB-001: Ver todos los cron jobs registrados
Section titled “US-ADM-JOB-001: Ver todos los cron jobs registrados”Como SUPER_ADMIN Quiero ver en una sola pantalla todos los cron jobs registrados en la plataforma con su estado, schedule y último resultado Para tener visibilidad centralizada del estado operacional de las tareas automatizadas
Criterios: AC-ADM-JOB-001 · Casos: TC-ADM-JOB-001, TC-ADM-JOB-002 · Prioridad: Alta · ✅ Implementado
US-ADM-JOB-002: Ver historial de ejecuciones de un job
Section titled “US-ADM-JOB-002: Ver historial de ejecuciones de un job”Como SUPER_ADMIN Quiero expandir un job para ver el historial detallado de sus ejecuciones: estado, duración, items procesados y mensajes de error Para diagnosticar fallos recurrentes o verificar que un job se ejecutó correctamente
Criterios: AC-ADM-JOB-002 · Casos: TC-ADM-JOB-003 · Prioridad: Alta · ✅ Implementado
US-ADM-JOB-003: Desactivar un job sin tocar código
Section titled “US-ADM-JOB-003: Desactivar un job sin tocar código”Como SUPER_ADMIN Quiero desactivar un cron job desde la UI usando el toggle de la tarjeta Para suspender temporalmente una tarea automatizada sin necesidad de redeploy ni modificación de código
Criterios: AC-ADM-JOB-003 · Casos: TC-ADM-JOB-004, TC-ADM-JOB-005 · Prioridad: Alta · ✅ Implementado
US-ADM-JOB-004: Reactivar un job desactivado
Section titled “US-ADM-JOB-004: Reactivar un job desactivado”Como SUPER_ADMIN Quiero reactivar un job previamente desactivado usando el mismo toggle Para restaurar la ejecución automática de la tarea sin redeploy
Criterios: AC-ADM-JOB-004 · Casos: TC-ADM-JOB-006 · Prioridad: Alta · ✅ Implementado
US-ADM-JOB-005: Ejecutar un job manualmente fuera de su schedule
Section titled “US-ADM-JOB-005: Ejecutar un job manualmente fuera de su schedule”Como SUPER_ADMIN Quiero pulsar “Run” en un job para lanzarlo inmediatamente fuera de su horario programado Para probar que el job funciona correctamente o ejecutarlo ante una necesidad puntual
Criterios: AC-ADM-JOB-005 · Casos: TC-ADM-JOB-007, TC-ADM-JOB-008 · Prioridad: Alta · ✅ Implementado
US-ADM-JOB-006: Configurar la expresión cron y parámetros de un job
Section titled “US-ADM-JOB-006: Configurar la expresión cron y parámetros de un job”Como SUPER_ADMIN Quiero editar la expresión cron, zona horaria, retención de logs y notas de un job desde un modal Para ajustar la frecuencia de ejecución o añadir contexto sin modificar el código fuente
Criterios: AC-ADM-JOB-006 · Casos: TC-ADM-JOB-009 · Prioridad: Media · ✅ Implementado
US-ADM-JOB-007: Filtrar y buscar jobs por categoría o nombre
Section titled “US-ADM-JOB-007: Filtrar y buscar jobs por categoría o nombre”Como SUPER_ADMIN Quiero filtrar la lista de jobs por categoría (engagement, billing, ai, etc.) y buscar por nombre o slug Para localizar rápidamente un job específico en una lista extensa
Criterios: AC-ADM-JOB-007 · Casos: TC-ADM-JOB-010 · Prioridad: Media · ✅ Implementado
US-ADM-JOB-008: Acceso denegado a roles no autorizados
Section titled “US-ADM-JOB-008: Acceso denegado a roles no autorizados”Como sistema de seguridad
Quiero que cualquier rol que no sea SUPER_ADMIN reciba 403 Forbidden al intentar acceder a los endpoints de jobs
Para garantizar que la gestión de tareas automatizadas queda reservada al administrador del sistema
Criterios: AC-ADM-JOB-008 · Casos: TC-ADM-JOB-011 · Prioridad: Alta · ✅ Implementado
Acceptance Criteria
Section titled “Acceptance Criteria”AC-ADM-JOB-001: Ver todos los cron jobs registrados
Section titled “AC-ADM-JOB-001: Ver todos los cron jobs registrados”US: US-ADM-JOB-001
AC-ADM-JOB-001.1 — Listado completo con estado y schedule
Dado que soy SUPER_ADMIN autenticadoCuando navego a /admin/platform/jobsEntonces veo una tarjeta por cada job registrado en la plataformaY cada tarjeta muestra el nombre, slug, categoría y expresión cronY cada tarjeta muestra el estado actual: OK, Error, En curso, Deshabilitado o OmitidoY el panel KPI superior muestra el total de jobs, activos/OK, con errores y en ejecuciónAC-ADM-JOB-001.2 — El listado se actualiza automáticamente
Dado que estoy en /admin/platform/jobsCuando han pasado 30 segundos desde la última cargaEntonces el panel refresca el listado automáticamente sin recargar la páginaAC-ADM-JOB-001.3 — Estado visual correcto por resultado
Dado que un job tiene lastStatus = "FAILED"Cuando veo su tarjeta en el listadoEntonces el indicador de estado es rojo y el label muestra "Error"
Dado que un job tiene isEnabled = falseCuando veo su tarjeta en el listadoEntonces la tarjeta aparece con opacidad reducida y el label muestra "Deshabilitado"
Dado que un job está actualmente en ejecuciónCuando veo su tarjeta en el listadoEntonces el indicador pulsa en azul y el label muestra "Ejecutando ahora"AC-ADM-JOB-002: Ver historial de ejecuciones
Section titled “AC-ADM-JOB-002: Ver historial de ejecuciones”US: US-ADM-JOB-002
AC-ADM-JOB-002.1 — Tabla de ejecuciones al expandir la tarjeta
Dado que estoy en /admin/platform/jobsCuando hago clic en la tarjeta de un jobEntonces la tarjeta se expande y muestra la tabla de últimas ejecucionesY cada fila de la tabla contiene: fecha de inicio, estado, items procesados, duración y notas/errorY la tabla muestra las 5 ejecuciones más recientes ordenadas por fecha descendenteAC-ADM-JOB-002.2 — Ejecución fallida muestra mensaje de error
Dado que un job tiene una ejecución con status = "FAILED"Cuando expando la tarjeta del jobEntonces la fila de esa ejecución muestra el badge rojo "Error"Y la columna "Notas" muestra el errorMessage registrado por failExecution()AC-ADM-JOB-002.3 — Ejecución manual se identifica en triggeredBy
Dado que un job fue lanzado manualmente por un adminCuando expando la tarjeta del jobEntonces la columna "Notas" de esa ejecución muestra "▶ manual:<adminId>"AC-ADM-JOB-002.4 — Panel de estadísticas muestra última duración y resultado
Dado que un job tiene ejecuciones previas registradasCuando expando su tarjetaEntonces el panel "Estadísticas" muestra la duración de la última ejecuciónY muestra el badge de color correspondiente al último estado (COMPLETED/FAILED/SKIPPED)AC-ADM-JOB-003: Desactivar un job desde la UI
Section titled “AC-ADM-JOB-003: Desactivar un job desde la UI”US: US-ADM-JOB-003
AC-ADM-JOB-003.1 — Toggle desactiva el job inmediatamente
Dado que un job tiene isEnabled = trueCuando deslizo el toggle de la tarjeta hacia la posición apagadaEntonces la API recibe PATCH /admin/jobs/:slug con { isEnabled: false }Y la tarjeta refleja el estado deshabilitado (opacidad reducida, label "Deshabilitado")Y aparece un toast de confirmación "Job X deshabilitado"AC-ADM-JOB-003.2 — Job desactivado no se ejecuta en el siguiente cron tick
Dado que un job tiene isEnabled = false en BDCuando llega el horario programado del job (cron tick)Entonces el job no ejecuta ninguna lógica de negocioY no se crea ningún registro en JobExecutionY isEnabled() en JobExecutionTrackerService retorna falseAC-ADM-JOB-003.3 — Botón “Run” deshabilitado cuando el job está inactivo
Dado que un job tiene isEnabled = falseCuando veo la tarjeta del job en el listadoEntonces el botón "▶ Run" está deshabilitado y no se puede pulsarAC-ADM-JOB-004: Reactivar un job desactivado
Section titled “AC-ADM-JOB-004: Reactivar un job desactivado”US: US-ADM-JOB-004
AC-ADM-JOB-004.1 — Toggle reactiva el job
Dado que un job tiene isEnabled = falseCuando deslizo el toggle hacia la posición encendidaEntonces la API recibe PATCH /admin/jobs/:slug con { isEnabled: true }Y la tarjeta vuelve a mostrarse activa (opacidad normal, estado "OK")Y aparece un toast "Job X habilitado ✓"AC-ADM-JOB-004.2 — Cache invalidado al cambiar estado
Dado que el tracker tiene el estado cached como disabled para un jobCuando el admin reactiva el job desde la UIEntonces el PATCH a la API invalida el cache del tracker para ese slugY el próximo cron tick del job llamará a isEnabled() que consultará la BD y obtendrá trueAC-ADM-JOB-005: Ejecutar un job manualmente
Section titled “AC-ADM-JOB-005: Ejecutar un job manualmente”US: US-ADM-JOB-005
AC-ADM-JOB-005.1 — Ejecución manual lanza el job y retorna execution id
Dado que soy SUPER_ADMIN y el job está habilitado y no está ejecutándoseCuando pulso el botón "▶ Run" en la tarjeta del jobEntonces la API recibe POST /admin/jobs/:slug/runY la API crea una JobExecution con status = "RUNNING" y triggeredBy = "manual:<adminId>"Y la respuesta contiene { executionId, status: "RUNNING", triggeredBy }Y el botón cambia a "⟳ Corriendo" y se deshabilita mientras el job está activoAC-ADM-JOB-005.2 — Toast de finalización cuando el job completa
Dado que lancé manualmente un job que está ejecutándoseCuando el job finaliza (status cambia de RUNNING a COMPLETED o FAILED)Entonces el frontend detecta el cambio vía polling de /admin/jobs/:slug/statusY aparece el toast '✓ "NombreJob" completado' si fue COMPLETEDY el listado de jobs se refresca automáticamenteAC-ADM-JOB-005.3 — Conflicto si el job ya está ejecutándose
Dado que el job ya tiene una ejecución con status = "RUNNING"Cuando el admin intenta lanzarlo manualmente de nuevoEntonces la API retorna 409 Conflict con mensaje indicando que el job ya está en ejecuciónY el frontend muestra un toast de errorAC-ADM-JOB-005.4 — Handler no registrado retorna 404
Dado que el handler del job no está registrado en ScheduledJobsHandlerRegistry(por ejemplo, el servidor acaba de reiniciarse y el módulo aún no inicializó)Cuando el admin pulsa "▶ Run"Entonces la API retorna 404 con el mensaje correspondienteAC-ADM-JOB-006: Configurar expresión cron y parámetros
Section titled “AC-ADM-JOB-006: Configurar expresión cron y parámetros”US: US-ADM-JOB-006
AC-ADM-JOB-006.1 — Modal de edición muestra valores actuales del job
Dado que estoy en /admin/platform/jobsCuando pulso el botón ⚙ de la tarjeta de un jobEntonces se abre el modal de configuración con los valores actuales pre-cargados: la expresión cron, zona horaria, días de retención y notas del jobAC-ADM-JOB-006.2 — Guardar cambios persiste en BD e invalida cache
Dado que he modificado la expresión cron a "0 3 * * *" en el modalCuando pulso "Guardar cambios"Entonces la API recibe PATCH /admin/jobs/:slug con los nuevos valoresY el tracker invalida el cache para ese slugY aparece el toast '✓ Configuración de "slug" guardada'Y la tarjeta refleja la nueva expresión cronAC-ADM-JOB-006.3 — Hint muestra descripción legible del cron
Dado que estoy en el modal de ediciónCuando escribo "0 2 * * *" en el campo de expresión cronEntonces el hint debajo del campo muestra "todos los días a las 02:00 UTC"AC-ADM-JOB-007: Filtrar y buscar jobs
Section titled “AC-ADM-JOB-007: Filtrar y buscar jobs”US: US-ADM-JOB-007
AC-ADM-JOB-007.1 — Filtro por categoría
Dado que estoy en /admin/platform/jobs y hay jobs de múltiples categoríasCuando hago clic en el chip "💳 Facturación"Entonces solo se muestran los jobs cuya category = "billing"Y el chip queda resaltado con color primarioAC-ADM-JOB-007.2 — Búsqueda por nombre o slug
Dado que escribo "notif" en el campo de búsquedaEntonces el listado se filtra en tiempo real mostrando solo los jobs cuyo slug, nombre o descripción contienen "notif" (insensible a mayúsculas)AC-ADM-JOB-007.3 — Sin resultados muestra mensaje vacío
Dado que aplico un filtro que no coincide con ningún jobEntonces veo el mensaje "No hay jobs que coincidan con el filtro."Y no hay tarjetas vacías ni errores de consolaAC-ADM-JOB-008: Control de acceso — solo SUPER_ADMIN
Section titled “AC-ADM-JOB-008: Control de acceso — solo SUPER_ADMIN”US: US-ADM-JOB-008
AC-ADM-JOB-008.1 — Roles no autorizados reciben 403
Dado que soy un usuario con rol FREE, STARTER, PRO, ELITE, CORPORATE_USER, CORPORATE_ADMIN o CONTENT_ADMINCuando realizo cualquier request a /admin/jobs (GET, PATCH, POST)Entonces la API responde con 403 ForbiddenY el cuerpo de la respuesta no revela información del sistema de jobsAC-ADM-JOB-008.2 — Request sin autenticar recibe 401
Dado que realizo una request a /admin/jobs sin incluir el header AuthorizationEntonces la API responde con 401 UnauthorizedAC-ADM-JOB-008.3 — SUPER_ADMIN puede acceder a todos los endpoints
Dado que soy SUPER_ADMIN autenticado con JWT válidoCuando accedo a GET /admin/jobs, PATCH /admin/jobs/:slug o POST /admin/jobs/:slug/runEntonces la API responde con 200/201 y los datos correspondientesTest Cases
Section titled “Test Cases”TC-ADM-JOB-001: Listado de jobs carga correctamente
Section titled “TC-ADM-JOB-001: Listado de jobs carga correctamente”AC: AC-ADM-JOB-001.1 · Prioridad: Alta · Tipo: API · Estado: 📋 Pendiente
Spec: apps/e2e/tests/admin/jobs.spec.ts
Precondiciones: Al menos un ScheduledJob existe en BD (seed aplicado).
import { fixtures } from '../../fixtures/users.fixture'import { apiClient } from '../../utils/api-client'
// Nota: fixtures no incluye superAdmin — se debe añadir o usar token directotest('SUPER_ADMIN puede listar todos los jobs', async () => { const res = await apiClient.get('/admin/jobs', superAdminToken) expect(res.status).toBe(200) expect(Array.isArray(res.data)).toBe(true) expect(res.data.length).toBeGreaterThan(0) // Cada job tiene los campos obligatorios const job = res.data[0] expect(job).toHaveProperty('slug') expect(job).toHaveProperty('name') expect(job).toHaveProperty('cronExpr') expect(job).toHaveProperty('isEnabled') expect(job).toHaveProperty('lastStatus') expect(job).toHaveProperty('executions') expect(Array.isArray(job.executions)).toBe(true)})TC-ADM-JOB-002: KPIs del panel reflejan el estado real
Section titled “TC-ADM-JOB-002: KPIs del panel reflejan el estado real”AC: AC-ADM-JOB-001.1 · Prioridad: Media · Tipo: E2E · Estado: 📋 Pendiente
Spec: apps/e2e/tests/admin/jobs.spec.ts
test('El panel KPI muestra el conteo correcto de jobs activos', async ({ page }) => { await loginAsSuperAdmin(page) await page.goto('/admin/platform/jobs') // Verificar que las tarjetas KPI están presentes await expect(page.getByText('Total jobs')).toBeVisible() await expect(page.getByText('Activos / OK')).toBeVisible() await expect(page.getByText('Con errores')).toBeVisible() await expect(page.getByText('En ejecución')).toBeVisible()})TC-ADM-JOB-003: Historial de ejecuciones — tabla y campos
Section titled “TC-ADM-JOB-003: Historial de ejecuciones — tabla y campos”AC: AC-ADM-JOB-002.1, AC-ADM-JOB-002.2 · Prioridad: Alta · Tipo: API · Estado: 📋 Pendiente
Spec: apps/e2e/tests/admin/jobs.spec.ts
Precondiciones: El job test-slug tiene al menos una ejecución registrada en BD.
test('Historial de ejecuciones retorna estructura correcta', async () => { const res = await apiClient.get('/admin/jobs/test-slug/executions?limit=10', superAdminToken) expect(res.status).toBe(200) expect(Array.isArray(res.data)).toBe(true) if (res.data.length > 0) { const exec = res.data[0] expect(exec).toHaveProperty('id') expect(exec).toHaveProperty('status') expect(exec).toHaveProperty('startedAt') expect(exec).toHaveProperty('durationMs') expect(exec).toHaveProperty('triggeredBy') expect(['RUNNING', 'COMPLETED', 'FAILED', 'SKIPPED']).toContain(exec.status) }})
test('Historial con limit respeta el máximo de 100', async () => { const res = await apiClient.get('/admin/jobs/test-slug/executions?limit=200', superAdminToken) expect(res.status).toBe(200) expect(res.data.length).toBeLessThanOrEqual(100)})TC-ADM-JOB-004: Desactivar job — API y estado persistido
Section titled “TC-ADM-JOB-004: Desactivar job — API y estado persistido”AC: AC-ADM-JOB-003.1 · Prioridad: Alta · Tipo: API · Estado: 📋 Pendiente
Spec: apps/e2e/tests/admin/jobs.spec.ts
test('PATCH isEnabled=false desactiva el job en BD', async () => { // Setup: asegurar que el job está habilitado await apiClient.patch('/admin/jobs/test-slug', { isEnabled: true }, superAdminToken)
// Desactivar const res = await apiClient.patch('/admin/jobs/test-slug', { isEnabled: false }, superAdminToken) expect(res.status).toBe(200) expect(res.data.isEnabled).toBe(false)
// Verificar que la BD refleja el cambio const status = await apiClient.get('/admin/jobs/test-slug/status', superAdminToken) // isEnabled no está en JobStatusDto — verificar via getJob const job = await apiClient.get('/admin/jobs/test-slug', superAdminToken) expect(job.data.isEnabled).toBe(false)
// Teardown: restaurar estado await apiClient.patch('/admin/jobs/test-slug', { isEnabled: true }, superAdminToken)})TC-ADM-JOB-005: Job desactivado — botón Run deshabilitado en UI
Section titled “TC-ADM-JOB-005: Job desactivado — botón Run deshabilitado en UI”AC: AC-ADM-JOB-003.3 · Prioridad: Media · Tipo: E2E · Estado: 📋 Pendiente
Spec: apps/e2e/tests/admin/jobs.spec.ts
Precondiciones: Existe un job con isEnabled = false.
test('Botón Run está deshabilitado para job inactivo', async ({ page }) => { await loginAsSuperAdmin(page) await page.goto('/admin/platform/jobs') // Encontrar la tarjeta del job deshabilitado const disabledCard = page.locator('.ajs-card.disabled').first() await expect(disabledCard).toBeVisible() // El botón Run debe estar deshabilitado const runBtn = disabledCard.locator('button', { hasText: 'Run' }) await expect(runBtn).toBeDisabled()})TC-ADM-JOB-006: Reactivar job desactivado
Section titled “TC-ADM-JOB-006: Reactivar job desactivado”AC: AC-ADM-JOB-004.1 · Prioridad: Alta · Tipo: API · Estado: 📋 Pendiente
Spec: apps/e2e/tests/admin/jobs.spec.ts
test('PATCH isEnabled=true reactiva el job', async () => { // Setup: desactivar primero await apiClient.patch('/admin/jobs/test-slug', { isEnabled: false }, superAdminToken)
// Reactivar const res = await apiClient.patch('/admin/jobs/test-slug', { isEnabled: true }, superAdminToken) expect(res.status).toBe(200) expect(res.data.isEnabled).toBe(true)})TC-ADM-JOB-007: Ejecución manual — retorna execution con status RUNNING
Section titled “TC-ADM-JOB-007: Ejecución manual — retorna execution con status RUNNING”AC: AC-ADM-JOB-005.1 · Prioridad: Alta · Tipo: API · Estado: 📋 Pendiente
Spec: apps/e2e/tests/admin/jobs.spec.ts
Precondiciones: El handler del job test-slug está registrado (aplicación corriendo).
test('POST /run crea JobExecution con status RUNNING', async () => { const res = await apiClient.post('/admin/jobs/test-slug/run', {}, superAdminToken) expect(res.status).toBe(201) expect(res.data).toHaveProperty('executionId') expect(res.data.status).toBe('RUNNING') expect(res.data.triggeredBy).toMatch(/^manual:/)})TC-ADM-JOB-008: Conflicto al lanzar job ya en ejecución
Section titled “TC-ADM-JOB-008: Conflicto al lanzar job ya en ejecución”AC: AC-ADM-JOB-005.3 · Prioridad: Alta · Tipo: API · Estado: 📋 Pendiente
Spec: apps/e2e/tests/admin/jobs.spec.ts
test('Segundo POST /run retorna 409 si el job ya está RUNNING', async () => { // Lanzar el job — puede completarse muy rápido; este test requiere un job con latencia // En tests, usar un job de mantenimiento con duración conocida // Opción alternativa: crear un registro RUNNING en BD directamente via seed de test const res = await apiClient.post('/admin/jobs/test-long-running-slug/run', {}, superAdminToken) if (res.status === 201) { // Si el primero fue exitoso, intentar un segundo inmediatamente const res2 = await apiClient.post('/admin/jobs/test-long-running-slug/run', {}, superAdminToken) // Puede ser 409 si aún está RUNNING, o 201 si ya terminó (job muy rápido) expect([201, 409]).toContain(res2.status) if (res2.status === 409) { expect(res2.data.message).toMatch(/already running/i) } }})TC-ADM-JOB-009: Actualizar expresión cron del job
Section titled “TC-ADM-JOB-009: Actualizar expresión cron del job”AC: AC-ADM-JOB-006.2 · Prioridad: Media · Tipo: API · Estado: 📋 Pendiente
Spec: apps/e2e/tests/admin/jobs.spec.ts
test('PATCH cronExpr actualiza la configuración del job', async () => { const originalJob = await apiClient.get('/admin/jobs/test-slug', superAdminToken) const originalCron = originalJob.data.cronExpr
const newCron = '0 3 * * *' const res = await apiClient.patch( '/admin/jobs/test-slug', { cronExpr: newCron, timezone: 'UTC', retentionDays: 30 }, superAdminToken ) expect(res.status).toBe(200) expect(res.data.cronExpr).toBe(newCron)
// Teardown: restaurar cron original await apiClient.patch('/admin/jobs/test-slug', { cronExpr: originalCron }, superAdminToken)})TC-ADM-JOB-010: Filtrado por categoría en UI
Section titled “TC-ADM-JOB-010: Filtrado por categoría en UI”AC: AC-ADM-JOB-007.1 · Prioridad: Baja · Tipo: E2E · Estado: 📋 Pendiente
Spec: apps/e2e/tests/admin/jobs.spec.ts
test('Chip de categoría filtra la lista de jobs visible', async ({ page }) => { await loginAsSuperAdmin(page) await page.goto('/admin/platform/jobs') // Activar filtro "Facturación" await page.locator('.ajs-chip', { hasText: 'Facturación' }).click() // Verificar que los chips de otras categorías no aparecen en las tarjetas visibles const visibleCards = page.locator('.ajs-card') const count = await visibleCards.count() for (let i = 0; i < count; i++) { await expect(visibleCards.nth(i).locator('.ajs-badge-pr')).toContainText('Facturación') }})TC-ADM-JOB-011: Control de acceso — roles no autorizados reciben 403
Section titled “TC-ADM-JOB-011: Control de acceso — roles no autorizados reciben 403”AC: AC-ADM-JOB-008.1, AC-ADM-JOB-008.2 · Prioridad: Alta · Tipo: API · Estado: 📋 Pendiente
Spec: apps/e2e/tests/admin/jobs.spec.ts
import { fixtures } from '../../fixtures/users.fixture'import { apiClient } from '../../utils/api-client'
const unauthorizedFixtures = [ fixtures.free, fixtures.starter, fixtures.pro, fixtures.elite, fixtures.corpUser, fixtures.corpAdmin, fixtures.contentAdmin,]
test.describe('GET /admin/jobs — acceso denegado a roles no autorizados', () => { for (const fixture of unauthorizedFixtures) { test(`role ${fixture.role} recibe 403`, async () => { const res = await apiClient.get('/admin/jobs', fixture.token) expect(res.status).toBe(403) }) }})
test('Request sin token recibe 401', async () => { const res = await apiClient.get('/admin/jobs', null) expect(res.status).toBe(401)})
test('PATCH sin token recibe 401', async () => { const res = await apiClient.patch('/admin/jobs/any-slug', { isEnabled: false }, null) expect(res.status).toBe(401)})
test('POST /run sin token recibe 401', async () => { const res = await apiClient.post('/admin/jobs/any-slug/run', {}, null) expect(res.status).toBe(401)})Notas de implementación
Section titled “Notas de implementación”Estructura de datos clave
Section titled “Estructura de datos clave”| Modelo Prisma | Campos relevantes para tests |
|---|---|
ScheduledJob | slug (único), isEnabled, lastStatus, lastRunAt, cronExpr, timezone |
JobExecution | status (RUNNING/COMPLETED/FAILED/SKIPPED), durationMs, itemsProcessed, errorMessage, triggeredBy |
Comportamiento del tracker en condiciones extremas
Section titled “Comportamiento del tracker en condiciones extremas”| Condición | Comportamiento esperado |
|---|---|
BD no disponible en isEnabled() | Fail-open: el job ejecuta de todas formas |
BD no disponible en startExecution() | Retorna null; el job ejecuta sin registrar en BD |
Cache TTL de isEnabled | 60 segundos — el toggle del admin invalida el cache inmediatamente via invalidateCache(slug) |
| Job sin handler registrado (cold start) | POST /run retorna 404 con mensaje descriptivo |
Fixture superAdmin requerido
Section titled “Fixture superAdmin requerido”Los tests de esta área requieren un fixture superAdmin que no existe actualmente en apps/e2e/fixtures/users.fixture.ts. Antes de implementar la suite, añadir:
superAdmin: { email: 'super.admin@test.fluency.com', password: 'TestPassword123!', role: 'SUPER_ADMIN', token: '',},Y crear el usuario correspondiente en el seed de test (apps/e2e/fixtures/auth.setup.ts).