diff --git a/ogWebconsole/.gitignore b/ogWebconsole/.gitignore index 141af17..16b9345 100644 --- a/ogWebconsole/.gitignore +++ b/ogWebconsole/.gitignore @@ -43,3 +43,5 @@ Thumbs.db test-results/ +.e2e-state.json +playwright-report/ diff --git a/ogWebconsole/e2e/global-setup.ts b/ogWebconsole/e2e/global-setup.ts new file mode 100644 index 0000000..bc80819 --- /dev/null +++ b/ogWebconsole/e2e/global-setup.ts @@ -0,0 +1,7 @@ +import { setupTestUsers } from './helpers/api'; + +export default async function globalSetup() { + console.log('\n▶ E2E — creando usuarios de test...'); + await setupTestUsers(); + console.log(' ✓ Setup completado\n'); +} diff --git a/ogWebconsole/e2e/global-teardown.ts b/ogWebconsole/e2e/global-teardown.ts new file mode 100644 index 0000000..90abe9d --- /dev/null +++ b/ogWebconsole/e2e/global-teardown.ts @@ -0,0 +1,7 @@ +import { teardownTestUsers } from './helpers/api'; + +export default async function globalTeardown() { + console.log('\n▶ E2E — eliminando usuarios de test...'); + await teardownTestUsers(); + console.log(' ✓ Teardown completado\n'); +} diff --git a/ogWebconsole/e2e/helpers/api.ts b/ogWebconsole/e2e/helpers/api.ts new file mode 100644 index 0000000..c47c375 --- /dev/null +++ b/ogWebconsole/e2e/helpers/api.ts @@ -0,0 +1,139 @@ +/** + * Helpers para crear y borrar usuarios de test vía la API de ogCore. + * Se usan en globalSetup / globalTeardown de Playwright. + */ +import { Agent } from 'undici'; +import * as fs from 'fs'; +import * as path from 'path'; + +export const API_URL = process.env['API_URL'] || 'https://ogserver.local/api'; +export const APP_URL = process.env['APP_URL'] || 'https://localhost:4200/oggui'; +export const ADMIN_USER = process.env['ADMIN_USER'] || 'ogadmin'; +export const ADMIN_PASS = process.env['ADMIN_PASS'] || '12345678'; +export const TEST_PASS = 'Test1234!'; + +// Nombres fijos — sin timestamp para que sean consistentes entre procesos +export const TEST_USERS = { + superAdmin: { username: 'e2e-superadmin', role: 'ROLE_SUPER_ADMIN' }, + ouAdmin: { username: 'e2e-ouadmin', role: 'ROLE_ORGANIZATIONAL_UNIT_ADMIN' }, + operator: { username: 'e2e-operator', role: 'ROLE_ORGANIZATIONAL_UNIT_OPERATOR' }, + minimal: { username: 'e2e-minimal', role: 'ROLE_ORGANIZATIONAL_UNIT_MINIMAL' }, +}; + +// Fichero temporal para pasar IRIs del setup al teardown +const STATE_FILE = path.join(__dirname, '../../.e2e-state.json'); + +// undici Agent que ignora certificados autofirmados (equivalente a curl -k) +const insecureAgent = new Agent({ connect: { rejectUnauthorized: false } }); + +async function fetchJson(url: string, init: RequestInit): Promise { + const res = await fetch(url, { + ...init, + headers: { 'Content-Type': 'application/ld+json', ...(init.headers as any) }, + dispatcher: insecureAgent, + } as any); + const text = await res.text(); + try { return JSON.parse(text); } catch { return text; } +} + +export async function getAdminToken(): Promise { + const data = await fetchJson(`${API_URL}/auth/login`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ username: ADMIN_USER, password: ADMIN_PASS }), + }); + if (!data.token) throw new Error(`Login fallido: ${JSON.stringify(data)}`); + return data.token; +} + +async function findUserByUsername(token: string, username: string): Promise { + const data = await fetchJson(`${API_URL}/users?username=${encodeURIComponent(username)}`, { + method: 'GET', + headers: { Authorization: `Bearer ${token}` }, + }); + const members = data['hydra:member'] ?? []; + return members.length > 0 ? members[0]['@id'] : null; +} + +async function findUserGroupByName(token: string, name: string): Promise { + const data = await fetchJson(`${API_URL}/user-groups?name=${encodeURIComponent(name)}`, { + method: 'GET', + headers: { Authorization: `Bearer ${token}` }, + }); + const members = data['hydra:member'] ?? []; + return members.length > 0 ? members[0]['@id'] : null; +} + +async function deleteResource(token: string, iri: string): Promise { + await fetch(`${API_URL.replace('/api', '')}${iri}`, { + method: 'DELETE', + headers: { Authorization: `Bearer ${token}` }, + dispatcher: insecureAgent, + } as any); +} + +async function createUserGroup(token: string, role: string, name: string): Promise { + const data = await fetchJson(`${API_URL}/user-groups`, { + method: 'POST', + headers: { Authorization: `Bearer ${token}` }, + body: JSON.stringify({ name, permissions: [role], enabled: true }), + }); + if (!data['@id']) throw new Error(`No se pudo crear UserGroup ${name}: ${JSON.stringify(data)}`); + return data['@id']; +} + +async function createUser(token: string, username: string, ugIri: string): Promise { + const data = await fetchJson(`${API_URL}/users`, { + method: 'POST', + headers: { Authorization: `Bearer ${token}` }, + body: JSON.stringify({ username, password: TEST_PASS, enabled: true, userGroups: [ugIri] }), + }); + if (!data['@id']) throw new Error(`No se pudo crear usuario ${username}: ${JSON.stringify(data)}`); + return data['@id']; +} + +export async function setupTestUsers(): Promise { + const token = await getAdminToken(); + const userIris: string[] = []; + const ugIris: string[] = []; + + for (const { username, role } of Object.values(TEST_USERS)) { + const ugName = `e2e-ug-${username}`; + + // Eliminar usuario previo si existe (limpia runs anteriores) + const existingUser = await findUserByUsername(token, username); + if (existingUser) { + await deleteResource(token, existingUser); + console.log(` ↺ Eliminado usuario previo: ${username}`); + } + + // Eliminar UserGroup previo si existe + const existingUg = await findUserGroupByName(token, ugName); + if (existingUg) { + await deleteResource(token, existingUg); + } + + const ugIri = await createUserGroup(token, role, ugName); + const userIri = await createUser(token, username, ugIri); + ugIris.push(ugIri); + userIris.push(userIri); + console.log(` ✓ ${username} [${role}]`); + } + + // Guarda IRIs en fichero para que teardown pueda leerlos (env vars no siempre pasan entre procesos) + fs.writeFileSync(STATE_FILE, JSON.stringify({ userIris, ugIris }, null, 2)); +} + +export async function teardownTestUsers(): Promise { + if (!fs.existsSync(STATE_FILE)) return; + + const { userIris, ugIris } = JSON.parse(fs.readFileSync(STATE_FILE, 'utf-8')); + const token = await getAdminToken(); + + for (const iri of [...userIris, ...ugIris]) { + await deleteResource(token, iri); + console.log(` ✓ Eliminado: ${iri}`); + } + + fs.unlinkSync(STATE_FILE); +} diff --git a/ogWebconsole/e2e/helpers/auth.ts b/ogWebconsole/e2e/helpers/auth.ts new file mode 100644 index 0000000..a63d154 --- /dev/null +++ b/ogWebconsole/e2e/helpers/auth.ts @@ -0,0 +1,39 @@ +import { Page, expect } from '@playwright/test'; +import { APP_URL, TEST_PASS, TEST_USERS } from './api'; + +export type Role = keyof typeof TEST_USERS; + +/** + * Hace login en la GUI con el usuario de test del rol indicado. + * Espera a que la redirección post-login termine (URL cambia a /groups). + */ +export async function loginAs(page: Page, role: Role): Promise { + const { username } = TEST_USERS[role]; + await page.goto(`${APP_URL}/auth/login`); + await page.locator('input[name="username"]').fill(username); + await page.locator('input[name="password"]').fill(TEST_PASS); + await page.locator('button[type="submit"]').click(); + await expect(page).toHaveURL(/\/groups/, { timeout: 10_000 }); +} + +/** + * Navega a una ruta y devuelve la URL final (útil para comprobar redirecciones). + */ +export async function navigateTo(page: Page, path: string): Promise { + await page.goto(`${APP_URL}${path}`); + await page.waitForLoadState('networkidle'); + return page.url(); +} + +/** Comprueba que un enlace del sidebar ES visible */ +export async function expectSidebarLink(page: Page, routerLink: string, visible: true): Promise; +/** Comprueba que un enlace del sidebar NO está en el DOM */ +export async function expectSidebarLink(page: Page, routerLink: string, visible: false): Promise; +export async function expectSidebarLink(page: Page, routerLink: string, visible: boolean): Promise { + const locator = page.locator(`[routerLink="${routerLink}"], a[href="${routerLink}"]`).first(); + if (visible) { + await expect(locator).toBeVisible({ timeout: 5_000 }); + } else { + await expect(locator).not.toBeAttached({ timeout: 5_000 }); + } +} diff --git a/ogWebconsole/e2e/permissions/ou-admin.spec.ts b/ogWebconsole/e2e/permissions/ou-admin.spec.ts new file mode 100644 index 0000000..b595b48 --- /dev/null +++ b/ogWebconsole/e2e/permissions/ou-admin.spec.ts @@ -0,0 +1,75 @@ +import { test, expect } from '@playwright/test'; +import { loginAs, navigateTo, expectSidebarLink } from '../helpers/auth'; +import { APP_URL } from '../helpers/api'; + +test.describe('OU Admin — acceso a gestión sin infraestructura', () => { + + test.beforeEach(async ({ page }) => { + await loginAs(page, 'ouAdmin'); + }); + + // ── Sidebar ───────────────────────────────────────────────────────────────── + test.describe('Sidebar', () => { + test('ve /groups', async ({ page }) => expectSidebarLink(page, '/groups', true)); + test('ve /dhcp-servers', async ({ page }) => expectSidebarLink(page, '/dhcp-servers', true)); + test('ve /boot-servers', async ({ page }) => expectSidebarLink(page, '/boot-servers', true)); + test('ve /calendars', async ({ page }) => expectSidebarLink(page, '/calendars', true)); + test('ve /repositories', async ({ page }) => expectSidebarLink(page, '/repositories', true)); + test('ve /menus', async ({ page }) => expectSidebarLink(page, '/menus', true)); + }); + + // ── Rutas permitidas ───────────────────────────────────────────────────────── + test.describe('Rutas — puede acceder', () => { + const allowed = [ + '/groups', '/commands', '/dhcp-servers', '/boot-servers', + '/calendars', '/repositories', '/pxe', + ]; + for (const route of allowed) { + test(`accede a ${route}`, async ({ page }) => { + const finalUrl = await navigateTo(page, route); + expect(finalUrl).toContain(route); + }); + } + }); + + // ── Rutas bloqueadas (solo super-admin) ────────────────────────────────────── + test.describe('Rutas — redirige a /groups cuando accede a rutas de super-admin', () => { + const blocked = ['/users', '/roles', '/env-vars', '/import-legacy']; + for (const route of blocked) { + test(`${route} → redirige a /groups`, async ({ page }) => { + const finalUrl = await navigateTo(page, route); + expect(finalUrl).toContain('/groups'); + }); + } + }); + + // ── GUI: botones de infraestructura OCULTOS ────────────────────────────────── + test('NO ve el botón "Añadir OU" en /groups (solo ou-admin no puede crear desde aquí... wait, sí puede)', async ({ page }) => { + await page.goto(`${APP_URL}/groups`); + // ou-admin SÍ ve el botón de añadir OU (no es ou-operator ni ou-minimal) + const addOuBtn = page.locator('button.action-button').first(); + await expect(addOuBtn).toBeVisible({ timeout: 5_000 }); + }); + + test('NO ve botón crear en /dhcp-servers (solo super-admin puede crear servidores DHCP)', async ({ page }) => { + await page.goto(`${APP_URL}/dhcp-servers`); + await page.waitForLoadState('networkidle'); + // El botón de añadir dhcp tiene *ngIf="auth.isSuperAdmin" → no debe aparecer + const addBtn = page.locator('button[color="primary"]').filter({ hasText: /add|añadir/i }); + await expect(addBtn).not.toBeAttached({ timeout: 5_000 }); + }); + + test('NO ve botón crear en /boot-servers (solo super-admin)', async ({ page }) => { + await page.goto(`${APP_URL}/boot-servers`); + await page.waitForLoadState('networkidle'); + const addBtn = page.locator('button').filter({ hasText: /add|añadir/i }).first(); + await expect(addBtn).not.toBeAttached({ timeout: 5_000 }); + }); + + test('NO ve botón crear en /commands (solo super-admin)', async ({ page }) => { + await page.goto(`${APP_URL}/commands`); + await page.waitForLoadState('networkidle'); + const addBtn = page.locator('button.action-button'); + await expect(addBtn).not.toBeAttached({ timeout: 5_000 }); + }); +}); diff --git a/ogWebconsole/e2e/permissions/ou-minimal.spec.ts b/ogWebconsole/e2e/permissions/ou-minimal.spec.ts new file mode 100644 index 0000000..ad2a292 --- /dev/null +++ b/ogWebconsole/e2e/permissions/ou-minimal.spec.ts @@ -0,0 +1,60 @@ +import { test, expect } from '@playwright/test'; +import { loginAs, navigateTo, expectSidebarLink } from '../helpers/auth'; +import { APP_URL } from '../helpers/api'; + +test.describe('OU Minimal — solo vista de grupos', () => { + + test.beforeEach(async ({ page }) => { + await loginAs(page, 'minimal'); + }); + + // ── Login ──────────────────────────────────────────────────────────────────── + test('tras login aterriza en /groups', async ({ page }) => { + await expect(page).toHaveURL(/\/groups/); + }); + + // ── Sidebar: solo /groups ──────────────────────────────────────────────────── + test.describe('Sidebar — solo ve /groups', () => { + test('ve /groups', async ({ page }) => expectSidebarLink(page, '/groups', true)); + test('NO ve /dhcp-servers', async ({ page }) => expectSidebarLink(page, '/dhcp-servers', false)); + test('NO ve /boot-servers', async ({ page }) => expectSidebarLink(page, '/boot-servers', false)); + test('NO ve /calendars', async ({ page }) => expectSidebarLink(page, '/calendars', false)); + test('NO ve /repositories', async ({ page }) => expectSidebarLink(page, '/repositories', false)); + test('NO ve /menus', async ({ page }) => expectSidebarLink(page, '/menus', false)); + }); + + // ── Rutas bloqueadas ───────────────────────────────────────────────────────── + test.describe('Todas las rutas protegidas redirigen a /groups', () => { + const blocked = [ + '/users', '/roles', '/env-vars', + '/commands', '/dhcp-servers', '/boot-servers', + '/calendars', '/repositories', '/pxe', + '/clients/deploy-image', '/clients/partition-assistant', '/clients/run-script', + ]; + for (const route of blocked) { + test(`${route} → redirige a /groups`, async ({ page }) => { + const finalUrl = await navigateTo(page, route); + expect(finalUrl).toContain('/groups'); + }); + } + }); + + // ── GUI en /groups: sin botones de acción ──────────────────────────────────── + test.describe('/groups — sin botones de creación ni edición', () => { + test('NO ve botones "Añadir OU" ni "Añadir cliente"', async ({ page }) => { + await page.goto(`${APP_URL}/groups`); + await page.waitForLoadState('networkidle'); + const actionBtns = page.locator('button.action-button'); + await expect(actionBtns).not.toBeAttached({ timeout: 5_000 }); + }); + + test('NO ve botones de editar ni eliminar en el árbol de grupos', async ({ page }) => { + await page.goto(`${APP_URL}/groups`); + await page.waitForLoadState('networkidle'); + // Los botones de editar/borrar nodo tienen *ngIf="auth.userCategory !== 'ou-minimal'" + // Se verifican a través del menú contextual que no debería aparecer con opciones de edición + const editBtns = page.locator('button').filter({ hasText: /editar|edit|modificar/i }); + await expect(editBtns).not.toBeAttached({ timeout: 5_000 }); + }); + }); +}); diff --git a/ogWebconsole/e2e/permissions/ou-operator.spec.ts b/ogWebconsole/e2e/permissions/ou-operator.spec.ts new file mode 100644 index 0000000..75ced9d --- /dev/null +++ b/ogWebconsole/e2e/permissions/ou-operator.spec.ts @@ -0,0 +1,77 @@ +import { test, expect } from '@playwright/test'; +import { loginAs, navigateTo, expectSidebarLink } from '../helpers/auth'; +import { APP_URL } from '../helpers/api'; + +test.describe('OU Operator — solo grupos y operaciones de equipos', () => { + + test.beforeEach(async ({ page }) => { + await loginAs(page, 'operator'); + }); + + // ── Sidebar: solo /groups visible ─────────────────────────────────────────── + test.describe('Sidebar — solo ve /groups', () => { + test('ve /groups', async ({ page }) => expectSidebarLink(page, '/groups', true)); + test('NO ve /dhcp-servers', async ({ page }) => expectSidebarLink(page, '/dhcp-servers', false)); + test('NO ve /boot-servers', async ({ page }) => expectSidebarLink(page, '/boot-servers', false)); + test('NO ve /calendars', async ({ page }) => expectSidebarLink(page, '/calendars', false)); + test('NO ve /repositories', async ({ page }) => expectSidebarLink(page, '/repositories', false)); + test('NO ve /menus', async ({ page }) => expectSidebarLink(page, '/menus', false)); + }); + + // ── Rutas bloqueadas: redirige a /groups ───────────────────────────────────── + test.describe('Rutas de admin → redirigen a /groups', () => { + const blocked = [ + '/users', '/roles', '/env-vars', + '/commands', '/dhcp-servers', '/boot-servers', + '/calendars', '/repositories', '/pxe', + ]; + for (const route of blocked) { + test(`${route} → redirige a /groups`, async ({ page }) => { + const finalUrl = await navigateTo(page, route); + expect(finalUrl).toContain('/groups'); + }); + } + }); + + // ── Rutas permitidas: operaciones de equipos ───────────────────────────────── + test.describe('Rutas de operación — puede acceder', () => { + test('accede a /groups', async ({ page }) => { + const finalUrl = await navigateTo(page, '/groups'); + expect(finalUrl).toContain('/groups'); + }); + + test('accede a /clients/deploy-image', async ({ page }) => { + const finalUrl = await navigateTo(page, '/clients/deploy-image'); + expect(finalUrl).toContain('/clients/deploy-image'); + }); + + test('accede a /clients/partition-assistant', async ({ page }) => { + const finalUrl = await navigateTo(page, '/clients/partition-assistant'); + expect(finalUrl).toContain('/clients/partition-assistant'); + }); + + test('accede a /clients/run-script', async ({ page }) => { + const finalUrl = await navigateTo(page, '/clients/run-script'); + expect(finalUrl).toContain('/clients/run-script'); + }); + }); + + // ── GUI en /groups: botones de crear OU/cliente ocultos ───────────────────── + test.describe('/groups — botones de creación ocultos', () => { + test('NO ve el botón "Añadir OU"', async ({ page }) => { + await page.goto(`${APP_URL}/groups`); + await page.waitForLoadState('networkidle'); + // *ngIf="auth.userCategory !== 'ou-operator' && ..." + const addOuBtn = page.locator('button.action-button').filter({ hasText: /ou|unidad/i }); + await expect(addOuBtn).not.toBeAttached({ timeout: 5_000 }); + }); + + test('NO ve el botón de menú "Añadir cliente"', async ({ page }) => { + await page.goto(`${APP_URL}/groups`); + await page.waitForLoadState('networkidle'); + // Los botones de acción principales no deben aparecer para operator + const actionBtns = page.locator('button.action-button'); + await expect(actionBtns).not.toBeAttached({ timeout: 5_000 }); + }); + }); +}); diff --git a/ogWebconsole/e2e/permissions/super-admin.spec.ts b/ogWebconsole/e2e/permissions/super-admin.spec.ts new file mode 100644 index 0000000..8f41cd9 --- /dev/null +++ b/ogWebconsole/e2e/permissions/super-admin.spec.ts @@ -0,0 +1,66 @@ +import { test, expect } from '@playwright/test'; +import { loginAs, navigateTo, expectSidebarLink } from '../helpers/auth'; +import { APP_URL } from '../helpers/api'; + +test.describe('Super Admin — acceso total', () => { + + test.beforeEach(async ({ page }) => { + await loginAs(page, 'superAdmin'); + }); + + // ── Sidebar: todos los elementos visibles ─────────────────────────────────── + test.describe('Sidebar — ve todos los enlaces', () => { + const links = [ + '/groups', '/dhcp-servers', '/boot-servers', + '/calendars', '/repositories', '/menus', + ]; + for (const link of links) { + test(`ve el enlace a ${link}`, async ({ page }) => { + await expectSidebarLink(page, link, true); + }); + } + }); + + // ── Rutas: acceso permitido ───────────────────────────────────────────────── + test.describe('Rutas — puede acceder a todas', () => { + const routes = [ + '/users', '/roles', '/env-vars', + '/commands', '/pxe', '/dhcp-servers', '/boot-servers', + '/calendars', '/repositories', + ]; + for (const route of routes) { + test(`accede a ${route} sin ser redirigido`, async ({ page }) => { + const finalUrl = await navigateTo(page, route); + expect(finalUrl).toContain(route); + }); + } + }); + + // ── GUI: botones de infraestructura visibles ──────────────────────────────── + test('ve el botón "Añadir OU" en /groups', async ({ page }) => { + await page.goto(`${APP_URL}/groups`); + await expect(page.locator('button.action-button').first()).toBeVisible(); + }); + + test('ve botones de crear/editar en /dhcp-servers', async ({ page }) => { + await page.goto(`${APP_URL}/dhcp-servers`); + await page.waitForLoadState('networkidle'); + // Botón de añadir servidor (solo visible para super-admin) + const addBtn = page.locator('button').filter({ hasText: /add|añadir|nuevo/i }).first(); + await expect(addBtn).toBeVisible({ timeout: 8_000 }); + }); + + test('ve botones de crear en /commands', async ({ page }) => { + await page.goto(`${APP_URL}/commands`); + await page.waitForLoadState('networkidle'); + const addBtn = page.locator('button.action-button').first(); + await expect(addBtn).toBeVisible({ timeout: 8_000 }); + }); + + test('ve botones de crear en /boot-servers', async ({ page }) => { + await page.goto(`${APP_URL}/boot-servers`); + await page.waitForLoadState('networkidle'); + const addBtn = page.locator('button').filter({ hasText: /add|añadir|nuevo/i }).first(); + await expect(addBtn).toBeVisible({ timeout: 8_000 }); + }); +}); diff --git a/ogWebconsole/package-lock.json b/ogWebconsole/package-lock.json index 461ff5a..c7ff158 100644 --- a/ogWebconsole/package-lock.json +++ b/ogWebconsole/package-lock.json @@ -35,6 +35,7 @@ "@angular/compiler-cli": "^18.0.0", "@angular/localize": "^18.1.0", "@ngx-env/builder": "^18.0.1", + "@playwright/test": "^1.60.0", "@types/jasmine": "~5.1.0", "@types/papaparse": "^5.3.15", "jasmine-core": "~5.1.0", @@ -272,6 +273,7 @@ "version": "18.1.0", "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-18.1.0.tgz", "integrity": "sha512-K0BhvZ/SIVoGXZVuh1KOJDdgcGlHfFGMGrs58utndndAb+gYXReMfz4GR5cQs2OObH6TKmIOY2EH7Og1CY2tsw==", + "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -356,6 +358,7 @@ "version": "18.0.6", "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-18.0.6.tgz", "integrity": "sha512-9CmlQ8uZ0cHuj+1jDdabbFw90QpjrJzh1/cgeHN/8vAGE4ranxX3l6cqpVxuoWRLkB2ykTHt2xeoTM8y0d9ERA==", + "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -405,6 +408,7 @@ "version": "18.1.0", "resolved": "https://registry.npmjs.org/@angular/common/-/common-18.1.0.tgz", "integrity": "sha512-noHDLarQSCZZh7hyNd0HR61Fut+q4QCVq9qc/jKPglfbV/6nPujQSmSpT+rNJlNuBOrCLuvH/CNBNbiqii+x3g==", + "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -420,6 +424,7 @@ "version": "18.1.0", "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-18.1.0.tgz", "integrity": "sha512-JRQzVTeJGSfRLY+dx+gwu/hPQVB8K+5pW12Z42M9x/HBgGW4in0cO2zHkeQPvImqm0nak82Us1Hyf5C+qTlMMQ==", + "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -440,6 +445,7 @@ "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-18.1.0.tgz", "integrity": "sha512-BBsogLPJwxkPh7f8RVHsxyyqNE8XpHbAanjB5fAwnU4W6Sw1kR5rFzkeZM3xaRm2MDiC8DovIl6hlf+s/mKYOw==", "dev": true, + "peer": true, "dependencies": { "@babel/core": "7.24.7", "@jridgewell/sourcemap-codec": "^1.4.14", @@ -467,6 +473,7 @@ "version": "18.1.0", "resolved": "https://registry.npmjs.org/@angular/core/-/core-18.1.0.tgz", "integrity": "sha512-/57/s7CD/0CwlN+3FlhVmx7ypCWXjKi5UKtnlBAUg0D1denIf6ADxwTHFZABYZcYBqOTJgeQUtUw9u/A+0CIlg==", + "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -482,6 +489,7 @@ "version": "18.1.0", "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-18.1.0.tgz", "integrity": "sha512-m+7m9wa+n5dEacd458eSZsZTz0B+HbOtr7/uqM0YTMQaPrhwl1epG5Y103mB6yr00JiJcLNlPLjP888cHFjldQ==", + "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -500,6 +508,7 @@ "resolved": "https://registry.npmjs.org/@angular/localize/-/localize-18.1.0.tgz", "integrity": "sha512-84D06p2Th5NxoJZzsSIn4FkTJGImj7rtNnvyTrHvHdomzzUKwiBOXDB2FiCLDstND0DsCtgjD/uBJivg77z9tg==", "dev": true, + "peer": true, "dependencies": { "@babel/core": "7.24.7", "@types/babel__core": "7.20.5", @@ -588,6 +597,7 @@ "version": "18.1.0", "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-18.1.0.tgz", "integrity": "sha512-jCmxthiI4Zef54crckNht60xwfIsuciGeyZvb7SsXna2maLW9fA4uz1VhZqIWTiBnHwNynVlyfBX3/jBD7S9+g==", + "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -609,6 +619,7 @@ "version": "18.1.0", "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-18.1.0.tgz", "integrity": "sha512-D/wuOQf+gULld9DVEzn2Lw3WbTyAYf/sp3DC5k83O+DQsG3eAIsVkt0zdE+U3DrDYsiWg8M3X+ioi3ouqK0mNg==", + "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -666,6 +677,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.7.tgz", "integrity": "sha512-nykK+LEK86ahTkX/3TgauT0ikKoNCfKHEaZYTUVupJdTLzGNvrblu4u6fa7DhZONAltdf8e662t/abY8idrd/g==", "dev": true, + "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.24.7", @@ -3217,6 +3229,7 @@ "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-5.0.7.tgz", "integrity": "sha512-GFcigCxJTKCH3aECzMIu4FhgLJWnFvMXzpI4CCSoELWFtkOOU2P+goYA61+OKpGrB8fPE7q6n8zAXBSlZRrHjQ==", "dev": true, + "peer": true, "dependencies": { "@inquirer/checkbox": "^2.3.7", "@inquirer/confirm": "^3.1.11", @@ -4518,7 +4531,6 @@ "os": [ "aix" ], - "peer": true, "engines": { "node": ">=12" } @@ -4535,7 +4547,6 @@ "os": [ "android" ], - "peer": true, "engines": { "node": ">=12" } @@ -4552,7 +4563,6 @@ "os": [ "android" ], - "peer": true, "engines": { "node": ">=12" } @@ -4569,7 +4579,6 @@ "os": [ "android" ], - "peer": true, "engines": { "node": ">=12" } @@ -4586,7 +4595,6 @@ "os": [ "darwin" ], - "peer": true, "engines": { "node": ">=12" } @@ -4603,7 +4611,6 @@ "os": [ "darwin" ], - "peer": true, "engines": { "node": ">=12" } @@ -4620,7 +4627,6 @@ "os": [ "freebsd" ], - "peer": true, "engines": { "node": ">=12" } @@ -4637,7 +4643,6 @@ "os": [ "freebsd" ], - "peer": true, "engines": { "node": ">=12" } @@ -4654,7 +4659,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=12" } @@ -4671,7 +4675,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=12" } @@ -4688,7 +4691,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=12" } @@ -4705,7 +4707,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=12" } @@ -4722,7 +4723,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=12" } @@ -4739,7 +4739,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=12" } @@ -4756,7 +4755,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=12" } @@ -4773,7 +4771,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=12" } @@ -4790,7 +4787,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=12" } @@ -4807,7 +4803,6 @@ "os": [ "netbsd" ], - "peer": true, "engines": { "node": ">=12" } @@ -4824,7 +4819,6 @@ "os": [ "openbsd" ], - "peer": true, "engines": { "node": ">=12" } @@ -4841,7 +4835,6 @@ "os": [ "sunos" ], - "peer": true, "engines": { "node": ">=12" } @@ -4858,7 +4851,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": ">=12" } @@ -4875,7 +4867,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": ">=12" } @@ -4892,7 +4883,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": ">=12" } @@ -5306,6 +5296,22 @@ "node": ">=14" } }, + "node_modules/@playwright/test": { + "version": "1.60.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.60.0.tgz", + "integrity": "sha512-O71yZIbAh/PxDMNGns37GHBIfrVkEVyn+AXyIa5dOTfb4/xNvRWV+Vv/NMbNCtODB/pO7vLlF2OTmMVLhmr7Ag==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.60.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.18.0", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.18.0.tgz", @@ -5891,6 +5897,7 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.10.tgz", "integrity": "sha512-MdiXf+nDuMvY0gJKxyfZ7/6UFsETO7mGKF54MVD/ekJS6HdFtpZFBgrh6Pseu64XTb2MLyFPlbW6hj8HYRQNOQ==", "dev": true, + "peer": true, "dependencies": { "undici-types": "~5.26.4" } @@ -6188,6 +6195,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", "dev": true, + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -6261,6 +6269,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.16.0.tgz", "integrity": "sha512-F0twR8U1ZU67JIEtekUcLkXkoO5mMMmgGD8sK/xUFzJ805jxHQl92hImFAqqXMyMYjSPOyUPAwHYhB72g5sTXw==", "dev": true, + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "json-schema-traverse": "^1.0.0", @@ -6666,6 +6675,7 @@ "url": "https://github.com/sponsors/ai" } ], + "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001640", "electron-to-chromium": "^1.4.820", @@ -7725,6 +7735,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "peer": true, "engines": { "node": ">=12" } @@ -9810,7 +9821,8 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-5.1.2.tgz", "integrity": "sha512-2oIUMGn00FdUiqz6epiiJr7xcFyNYj3rDcfmnzfkBnHyBQ3cBQUs4mmyGsOb7TTLb9kxk7dBcmEmqhDKkBoDyA==", - "dev": true + "dev": true, + "peer": true }, "node_modules/jest-worker": { "version": "27.5.1", @@ -9959,6 +9971,7 @@ "resolved": "https://registry.npmjs.org/karma/-/karma-6.4.3.tgz", "integrity": "sha512-LuucC/RE92tJ8mlCwqEoRWXP38UMAqpnq98vktmS9SznSoUPPUJQbc91dHcxcunROvfQjdORVA/YFviH+Xci9Q==", "dev": true, + "peer": true, "dependencies": { "@colors/colors": "1.5.0", "body-parser": "^1.19.0", @@ -10259,6 +10272,7 @@ "resolved": "https://registry.npmjs.org/less/-/less-4.2.0.tgz", "integrity": "sha512-P3b3HJDBtSzsXUl0im2L7gTO5Ubg8mEN6G8qoTS77iXxXX4Hvu4Qj540PZDvQ8V6DmX6iXo98k7Md0Cm1PrLaA==", "dev": true, + "peer": true, "dependencies": { "copy-anything": "^2.0.1", "parse-node-version": "^1.0.1", @@ -12188,6 +12202,53 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/playwright": { + "version": "1.60.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.60.0.tgz", + "integrity": "sha512-hheHdokM8cdqCb0lcE3s+zT4t4W+vvjpGxsZlDnikarzx8tSzMebh3UiFtgqwFwnTnjYQcsyMF8ei2mCO/tpeA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.60.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.60.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.60.0.tgz", + "integrity": "sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/postcss": { "version": "8.4.38", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz", @@ -12207,6 +12268,7 @@ "url": "https://github.com/sponsors/ai" } ], + "peer": true, "dependencies": { "nanoid": "^3.3.7", "picocolors": "^1.0.0", @@ -12823,6 +12885,7 @@ "version": "7.8.1", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "peer": true, "dependencies": { "tslib": "^2.1.0" } @@ -13803,6 +13866,7 @@ "resolved": "https://registry.npmjs.org/terser/-/terser-5.29.2.tgz", "integrity": "sha512-ZiGkhUBIM+7LwkNjXYJq8svgkd+QK3UUr0wJqY4MieaezBSAIPgbSPZyIx0idM6XWK5CMzSWa8MJIzmRcB8Caw==", "dev": true, + "peer": true, "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.8.2", @@ -13855,6 +13919,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -13987,7 +14052,8 @@ "node_modules/tslib": { "version": "2.6.3", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", - "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==" + "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==", + "peer": true }, "node_modules/tuf-js": { "version": "2.2.1", @@ -14039,6 +14105,7 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", "dev": true, + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -14284,6 +14351,7 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-5.3.2.tgz", "integrity": "sha512-6lA7OBHBlXUxiJxbO5aAY2fsHHzDr1q7DvXYnyZycRs2Dz+dXBWuhpWHvmljTRTpQC2uvGmUFFkSHF2vGo90MA==", "dev": true, + "peer": true, "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.38", @@ -14385,6 +14453,7 @@ "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.92.1.tgz", "integrity": "sha512-JECQ7IwJb+7fgUFBlrJzbyu3GEuNBcdqr1LD7IbSzwkSmIevTm8PF+wej3Oxuz/JFBUZ6O1o43zsPkwm1C4TmA==", "dev": true, + "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.3", "@types/estree": "^1.0.5", @@ -14461,6 +14530,7 @@ "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-5.0.4.tgz", "integrity": "sha512-dljXhUgx3HqKP2d8J/fUMvhxGhzjeNVarDLcbO/EWMSgRizDkxHQDZQaLFL5VJY9tRBj2Gz+rvCEYYvhbqPHNA==", "dev": true, + "peer": true, "dependencies": { "@types/bonjour": "^3.5.13", "@types/connect-history-api-fallback": "^1.5.4", @@ -14650,6 +14720,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -15031,7 +15102,8 @@ "node_modules/zone.js": { "version": "0.14.7", "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.14.7.tgz", - "integrity": "sha512-0w6DGkX2BPuiK/NLf+4A8FLE43QwBfuqz2dVgi/40Rj1WmqUskCqj329O/pwrqFJLG5X8wkeG2RhIAro441xtg==" + "integrity": "sha512-0w6DGkX2BPuiK/NLf+4A8FLE43QwBfuqz2dVgi/40Rj1WmqUskCqj329O/pwrqFJLG5X8wkeG2RhIAro441xtg==", + "peer": true } } } diff --git a/ogWebconsole/package.json b/ogWebconsole/package.json index a2a5595..53a23b1 100644 --- a/ogWebconsole/package.json +++ b/ogWebconsole/package.json @@ -6,7 +6,11 @@ "start": "ng serve", "build": "ng build", "watch": "ng build --watch --configuration development", - "test": "ng test" + "test": "ng test", + "e2e": "playwright test", + "e2e:headed": "playwright test --headed --slow-mo=400", + "e2e:ui": "playwright test --ui", + "e2e:report": "playwright show-report" }, "private": true, "dependencies": { @@ -37,6 +41,7 @@ "@angular/compiler-cli": "^18.0.0", "@angular/localize": "^18.1.0", "@ngx-env/builder": "^18.0.1", + "@playwright/test": "^1.60.0", "@types/jasmine": "~5.1.0", "@types/papaparse": "^5.3.15", "jasmine-core": "~5.1.0", diff --git a/ogWebconsole/playwright.config.ts b/ogWebconsole/playwright.config.ts new file mode 100644 index 0000000..8c4e502 --- /dev/null +++ b/ogWebconsole/playwright.config.ts @@ -0,0 +1,33 @@ +import { defineConfig, devices } from '@playwright/test'; + +export default defineConfig({ + testDir: './e2e', + globalSetup: './e2e/global-setup.ts', + globalTeardown: './e2e/global-teardown.ts', + timeout: 30_000, + retries: 0, + workers: 1, // secuencial para no crear conflictos entre usuarios de test + reporter: [['list'], ['html', { outputFolder: 'playwright-report', open: 'never' }]], + + use: { + baseURL: 'https://localhost:4200/oggui', + ignoreHTTPSErrors: true, + screenshot: 'only-on-failure', + video: 'retain-on-failure', + }, + + webServer: { + command: 'npm start', + url: 'https://localhost:4200/oggui', + reuseExistingServer: true, + ignoreHTTPSErrors: true, + timeout: 120_000, + }, + + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], +}); diff --git a/ogWebconsole/src/app/app-routing.module.ts b/ogWebconsole/src/app/app-routing.module.ts index 6aeffb9..a4fe9fe 100644 --- a/ogWebconsole/src/app/app-routing.module.ts +++ b/ogWebconsole/src/app/app-routing.module.ts @@ -71,10 +71,10 @@ const routes: Routes = [ { path: 'commands-task', component: CommandsTaskComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } }, { path: 'commands-logs', component: TaskLogsComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } }, { path: 'calendars', component: CalendarComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } }, - { path: 'clients/deploy-image', component: DeployImageComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } }, - { path: 'clients/partition-assistant', component: PartitionAssistantComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } }, - { path: 'clients/run-script', component: RunScriptAssistantComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } }, - { path: 'clients/:id/create-image', component: CreateClientImageComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } }, + { path: 'clients/deploy-image', component: DeployImageComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin', 'ou-operator'] } }, + { path: 'clients/partition-assistant', component: PartitionAssistantComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin', 'ou-operator'] } }, + { path: 'clients/run-script', component: RunScriptAssistantComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin', 'ou-operator'] } }, + { path: 'clients/:id/create-image', component: CreateClientImageComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin', 'ou-operator'] } }, { path: 'repositories', component: RepositoriesComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } }, { path: 'repository/:id', component: MainRepositoryViewComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } }, { path: 'software', component: SoftwareComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } }, diff --git a/ogWebconsole/src/app/components/commands/main-commands/commands.component.html b/ogWebconsole/src/app/components/commands/main-commands/commands.component.html index f1ccfa2..029ef60 100644 --- a/ogWebconsole/src/app/components/commands/main-commands/commands.component.html +++ b/ogWebconsole/src/app/components/commands/main-commands/commands.component.html @@ -6,7 +6,7 @@

{{ 'CommandsTitle' | translate }}

- +
@@ -44,8 +44,8 @@ {{ 'actions' | translate }} - - + + diff --git a/ogWebconsole/src/app/components/commands/main-commands/commands.component.ts b/ogWebconsole/src/app/components/commands/main-commands/commands.component.ts index d600cba..68e2ef6 100644 --- a/ogWebconsole/src/app/components/commands/main-commands/commands.component.ts +++ b/ogWebconsole/src/app/components/commands/main-commands/commands.component.ts @@ -10,6 +10,7 @@ import { DeleteModalComponent } from '../../../shared/delete_modal/delete-modal/ import { MatTableDataSource } from '@angular/material/table'; import { DatePipe } from '@angular/common'; import { ConfigService } from '@services/config.service'; +import { AuthService } from '@services/auth.service'; import { JoyrideService } from 'ngx-joyride'; @Component({ @@ -19,6 +20,7 @@ import { JoyrideService } from 'ngx-joyride'; }) export class CommandsComponent implements OnInit { public translate = inject(TranslateService); + public auth = inject(AuthService); baseUrl: string; private apiUrl: string; dataSource = new MatTableDataSource(); diff --git a/ogWebconsole/src/app/components/global-status/ogboot-status/ogboot-status.component.spec.ts b/ogWebconsole/src/app/components/global-status/ogboot-status/ogboot-status.component.spec.ts index 145205d..cf5d44f 100644 --- a/ogWebconsole/src/app/components/global-status/ogboot-status/ogboot-status.component.spec.ts +++ b/ogWebconsole/src/app/components/global-status/ogboot-status/ogboot-status.component.spec.ts @@ -1,6 +1,6 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { OgbootStatusComponent } from './ogboot-status.component'; +import { GlobalOgbootStatusComponent as OgbootStatusComponent } from './ogboot-status.component'; describe('OgbootStatusComponent', () => { let component: OgbootStatusComponent; diff --git a/ogWebconsole/src/app/components/groups/groups.component.html b/ogWebconsole/src/app/components/groups/groups.component.html index da09f00..c937b4d 100644 --- a/ogWebconsole/src/app/components/groups/groups.component.html +++ b/ogWebconsole/src/app/components/groups/groups.component.html @@ -16,11 +16,11 @@
- - - - - diff --git a/ogWebconsole/src/app/components/groups/groups.component.ts b/ogWebconsole/src/app/components/groups/groups.component.ts index c77826e..15e6fe5 100644 --- a/ogWebconsole/src/app/components/groups/groups.component.ts +++ b/ogWebconsole/src/app/components/groups/groups.component.ts @@ -111,6 +111,8 @@ export class GroupsComponent implements OnInit, OnDestroy { // Tipos de firmware disponibles firmwareTypes: string[] = []; + private fetchClientsSubscription?: Subscription; + protected status = [ { value: 'off', name: 'Apagado' }, { value: 'initializing', name: 'Inicializando' }, @@ -418,11 +420,9 @@ export class GroupsComponent implements OnInit, OnDestroy { event.stopPropagation(); this.selectedNode = node; const selectedClientsBeforeEdit = this.selection.selected.map(client => client.uuid); - this.fetchClientsForNode(node, selectedClientsBeforeEdit); - - if (node.type === NodeType.Classroom || node.type === NodeType.ClientsGroup) { - this.getStatusForAllClientsInNode(node); - } + + const checkStatusAfterLoad = node.type === NodeType.Classroom || node.type === NodeType.ClientsGroup; + this.fetchClientsForNode(node, selectedClientsBeforeEdit, checkStatusAfterLoad); } onMenuClick(event: Event, node: any): void { @@ -433,7 +433,9 @@ export class GroupsComponent implements OnInit, OnDestroy { } - public fetchClientsForNode(node: any, selectedClientsBeforeEdit: string[] = []): void { + + + public fetchClientsForNode(node: any, selectedClientsBeforeEdit: string[] = [], checkStatusAfterLoad: boolean = false): void { const params = new HttpParams({ fromObject: this.filters }); // Agregar parámetros de ordenamiento al backend @@ -442,8 +444,12 @@ export class GroupsComponent implements OnInit, OnDestroy { backendParams['order[' + this.sortBy + ']'] = this.sortDirection; } + if (this.fetchClientsSubscription) { + this.fetchClientsSubscription.unsubscribe(); + } + this.isLoadingClients = true; - this.http.get(`${this.baseUrl}/clients?organizationalUnit.id=${node.id}&page=${this.page + 1}&itemsPerPage=${this.itemsPerPage}`, { params: backendParams }).subscribe({ + this.fetchClientsSubscription = this.http.get(`${this.baseUrl}/clients?organizationalUnit.id=${node.id}&page=${this.page + 1}&itemsPerPage=${this.itemsPerPage}`, { params: backendParams }).subscribe({ next: (response: any) => { let members = response['hydra:member']; let total = response['hydra:totalItems']; @@ -476,6 +482,10 @@ export class GroupsComponent implements OnInit, OnDestroy { members.forEach((c: Client) => c.uuid && this.syncingClientIds.add(c.uuid)); } + if (checkStatusAfterLoad) { + this.getStatusForAllClientsInNode(node); + } + // Calcular estadísticas después de cargar los clientes this.calculateLocalStats(); }, @@ -855,14 +865,19 @@ export class GroupsComponent implements OnInit, OnDestroy { } + private checkStatusSubscription?: Subscription; + getStatusForAllClientsInNode(node: any): void { if (!node.uuid) return; + if (this.checkStatusSubscription) { + this.checkStatusSubscription.unsubscribe(); + } + this.isCheckingNodeStatus = true; this.selectedClients.data.forEach(c => c.uuid && this.syncingClientIds.add(c.uuid)); - this.subscriptions.add( - this.http.post<{ results: Array<{ uuid: string; ip: string; status: string; success: boolean }> }>( + this.checkStatusSubscription = this.http.post<{ results: Array<{ uuid: string; ip: string; status: string; success: boolean }> }>( `${this.baseUrl}/organizational-units/${node.uuid}/agent/status`, {} ).subscribe({ @@ -883,8 +898,7 @@ export class GroupsComponent implements OnInit, OnDestroy { this.selectedClients.data.forEach(c => c.uuid && this.syncingClientIds.delete(c.uuid)); this.toastr.error(this.translate.instant('errorCheckingStatus')); }, - }) - ); + }); } diff --git a/ogWebconsole/src/app/components/groups/shared/classroom-view/classroom-view.component.html b/ogWebconsole/src/app/components/groups/shared/classroom-view/classroom-view.component.html index 1b4a412..38d40f0 100644 --- a/ogWebconsole/src/app/components/groups/shared/classroom-view/classroom-view.component.html +++ b/ogWebconsole/src/app/components/groups/shared/classroom-view/classroom-view.component.html @@ -34,5 +34,6 @@ + diff --git a/ogWebconsole/src/app/components/ogboot/boot-servers/boot-servers.component.html b/ogWebconsole/src/app/components/ogboot/boot-servers/boot-servers.component.html index d22dbbc..70735bf 100644 --- a/ogWebconsole/src/app/components/ogboot/boot-servers/boot-servers.component.html +++ b/ogWebconsole/src/app/components/ogboot/boot-servers/boot-servers.component.html @@ -9,7 +9,7 @@ {{ 'ogbootServers' | translate }}
- @@ -51,10 +51,10 @@ - - diff --git a/ogWebconsole/src/app/components/ogboot/boot-servers/boot-servers.component.ts b/ogWebconsole/src/app/components/ogboot/boot-servers/boot-servers.component.ts index 8570ffb..74b0997 100644 --- a/ogWebconsole/src/app/components/ogboot/boot-servers/boot-servers.component.ts +++ b/ogWebconsole/src/app/components/ogboot/boot-servers/boot-servers.component.ts @@ -10,6 +10,7 @@ import {DeleteModalComponent} from "../../../shared/delete_modal/delete-modal/de import { JoyrideService } from 'ngx-joyride'; import { Router } from '@angular/router'; import { ConfigService } from '@services/config.service'; +import { AuthService } from '@services/auth.service'; import {ManageBootServerComponent} from "../manage-boot-server/manage-boot-server.component"; import { forkJoin } from 'rxjs'; @@ -20,6 +21,7 @@ import { forkJoin } from 'rxjs'; }) export class BootServersComponent implements OnInit { public translate = inject(TranslateService); + public auth = inject(AuthService); baseUrl: string; private apiUrl: string; dataSource = new MatTableDataSource(); diff --git a/ogWebconsole/src/app/components/ogdhcp/dhcp-servers/dhcp-servers.component.html b/ogWebconsole/src/app/components/ogdhcp/dhcp-servers/dhcp-servers.component.html index ef1ff2b..c05218f 100644 --- a/ogWebconsole/src/app/components/ogdhcp/dhcp-servers/dhcp-servers.component.html +++ b/ogWebconsole/src/app/components/ogdhcp/dhcp-servers/dhcp-servers.component.html @@ -9,7 +9,7 @@ {{ 'dhcpServers' | translate }}
- @@ -54,9 +54,9 @@ {{ 'actions' | translate }} - - diff --git a/ogWebconsole/src/app/components/ogdhcp/dhcp-servers/dhcp-servers.component.ts b/ogWebconsole/src/app/components/ogdhcp/dhcp-servers/dhcp-servers.component.ts index b34cc55..2ada710 100644 --- a/ogWebconsole/src/app/components/ogdhcp/dhcp-servers/dhcp-servers.component.ts +++ b/ogWebconsole/src/app/components/ogdhcp/dhcp-servers/dhcp-servers.component.ts @@ -10,6 +10,7 @@ import {DeleteModalComponent} from "../../../shared/delete_modal/delete-modal/de import { JoyrideService } from 'ngx-joyride'; import { Router } from '@angular/router'; import { ConfigService } from '@services/config.service'; +import { AuthService } from '@services/auth.service'; import {ManageDhcpServerComponent} from "../manage-dhcp-server/manage-dhcp-server.component"; @Component({ @@ -19,6 +20,7 @@ import {ManageDhcpServerComponent} from "../manage-dhcp-server/manage-dhcp-serve }) export class DhcpServersComponent implements OnInit { public translate = inject(TranslateService); + public auth = inject(AuthService); baseUrl: string; private apiUrl: string; dataSource = new MatTableDataSource(); diff --git a/ogWebconsole/src/app/components/repositories/repositories.component.html b/ogWebconsole/src/app/components/repositories/repositories.component.html index d2a5c9c..e14ccd1 100644 --- a/ogWebconsole/src/app/components/repositories/repositories.component.html +++ b/ogWebconsole/src/app/components/repositories/repositories.component.html @@ -10,7 +10,7 @@
- @@ -68,9 +68,9 @@ {{ 'actions' | translate }} - - diff --git a/ogWebconsole/src/app/components/repositories/repositories.component.ts b/ogWebconsole/src/app/components/repositories/repositories.component.ts index e033315..340e4bd 100644 --- a/ogWebconsole/src/app/components/repositories/repositories.component.ts +++ b/ogWebconsole/src/app/components/repositories/repositories.component.ts @@ -10,6 +10,7 @@ import {DeleteModalComponent} from "../../shared/delete_modal/delete-modal/delet import { JoyrideService } from 'ngx-joyride'; import { Router } from '@angular/router'; import { ConfigService } from '@services/config.service'; +import { AuthService } from '@services/auth.service'; import {Subnet} from "../ogdhcp/og-dhcp-subnets.component"; import {ShowMonoliticImagesComponent} from "./show-monolitic-images/show-monolitic-images.component"; import {ShowGitCommitsComponent} from "./show-git-images/show-git-images.component"; @@ -22,6 +23,7 @@ import {ManageRepositoryComponent} from "./manage-repository/manage-repository.c }) export class RepositoriesComponent implements OnInit { public translate = inject(TranslateService); + public auth = inject(AuthService); baseUrl: string; private apiUrl: string; dataSource = new MatTableDataSource(); diff --git a/ogWebconsole/src/app/guards/role.guard.spec.ts b/ogWebconsole/src/app/guards/role.guard.spec.ts index 30809a2..65e097d 100644 --- a/ogWebconsole/src/app/guards/role.guard.spec.ts +++ b/ogWebconsole/src/app/guards/role.guard.spec.ts @@ -1,17 +1,204 @@ import { TestBed } from '@angular/core/testing'; -import { CanActivateFn } from '@angular/router'; - +import { ActivatedRouteSnapshot, CanActivateFn, Router, RouterStateSnapshot } from '@angular/router'; import { roleGuard } from './role.guard'; +import { AuthService } from '@services/auth.service'; -describe('roleGuard', () => { - const executeGuard: CanActivateFn = (...guardParameters) => - TestBed.runInInjectionContext(() => roleGuard(...guardParameters)); +// ───────────────────────────────────────────────────────────────────────────── +// Helper: ejecuta el guard en el contexto de inyección de Angular +// ───────────────────────────────────────────────────────────────────────────── +function runGuard(allowedRoles: string[], currentRole: string): boolean | any { + const route = { data: { allowedRoles } } as unknown as ActivatedRouteSnapshot; + const state = {} as RouterStateSnapshot; + const execute: CanActivateFn = (...args) => + TestBed.runInInjectionContext(() => roleGuard(...args)); + return execute(route, state); +} + +describe('roleGuard — control de acceso por rol', () => { + let mockRouter: jasmine.SpyObj; + let mockAuth: jasmine.SpyObj; beforeEach(() => { - TestBed.configureTestingModule({}); + mockRouter = jasmine.createSpyObj('Router', ['navigate']); + mockAuth = jasmine.createSpyObj('AuthService', ['getCurrentRole']); + + TestBed.configureTestingModule({ + providers: [ + { provide: Router, useValue: mockRouter }, + { provide: AuthService, useValue: mockAuth }, + ], + }); }); - it('should be created', () => { - expect(executeGuard).toBeTruthy(); + // ── Rutas solo para super-admin ──────────────────────────────────────────── + describe('rutas solo para super-admin [users, roles, env-vars, import-legacy]', () => { + const onlySuperAdmin = ['super-admin']; + + it('super-admin puede acceder', () => { + mockAuth.getCurrentRole.and.returnValue('super-admin'); + expect(runGuard(onlySuperAdmin, 'super-admin')).toBeTrue(); + }); + + it('ou-admin NO puede acceder → redirige a /groups', () => { + mockAuth.getCurrentRole.and.returnValue('ou-admin'); + expect(runGuard(onlySuperAdmin, 'ou-admin')).toBeFalse(); + expect(mockRouter.navigate).toHaveBeenCalledWith(['/groups']); + }); + + it('ou-operator NO puede acceder', () => { + mockAuth.getCurrentRole.and.returnValue('ou-operator'); + expect(runGuard(onlySuperAdmin, 'ou-operator')).toBeFalse(); + }); + + it('ou-minimal NO puede acceder', () => { + mockAuth.getCurrentRole.and.returnValue('ou-minimal'); + expect(runGuard(onlySuperAdmin, 'ou-minimal')).toBeFalse(); + }); + }); + + // ── Rutas para super-admin + ou-admin ────────────────────────────────────── + describe('rutas para super-admin + ou-admin [commands, pxe, repositorios, etc.]', () => { + const adminRoutes = ['super-admin', 'ou-admin']; + + it('super-admin puede acceder', () => { + mockAuth.getCurrentRole.and.returnValue('super-admin'); + expect(runGuard(adminRoutes, 'super-admin')).toBeTrue(); + }); + + it('ou-admin puede acceder', () => { + mockAuth.getCurrentRole.and.returnValue('ou-admin'); + expect(runGuard(adminRoutes, 'ou-admin')).toBeTrue(); + }); + + it('ou-operator NO puede acceder → redirige a /groups', () => { + mockAuth.getCurrentRole.and.returnValue('ou-operator'); + expect(runGuard(adminRoutes, 'ou-operator')).toBeFalse(); + expect(mockRouter.navigate).toHaveBeenCalledWith(['/groups']); + }); + + it('ou-minimal NO puede acceder', () => { + mockAuth.getCurrentRole.and.returnValue('ou-minimal'); + expect(runGuard(adminRoutes, 'ou-minimal')).toBeFalse(); + }); + }); + + // ── Rutas para super-admin + ou-admin + ou-operator ──────────────────────── + describe('rutas para admin + operador [deploy-image, partition-assistant, run-script]', () => { + const operatorRoutes = ['super-admin', 'ou-admin', 'ou-operator']; + + it('super-admin puede acceder', () => { + mockAuth.getCurrentRole.and.returnValue('super-admin'); + expect(runGuard(operatorRoutes, 'super-admin')).toBeTrue(); + }); + + it('ou-admin puede acceder', () => { + mockAuth.getCurrentRole.and.returnValue('ou-admin'); + expect(runGuard(operatorRoutes, 'ou-admin')).toBeTrue(); + }); + + it('ou-operator puede acceder', () => { + mockAuth.getCurrentRole.and.returnValue('ou-operator'); + expect(runGuard(operatorRoutes, 'ou-operator')).toBeTrue(); + }); + + it('ou-minimal NO puede acceder → redirige a /groups', () => { + mockAuth.getCurrentRole.and.returnValue('ou-minimal'); + expect(runGuard(operatorRoutes, 'ou-minimal')).toBeFalse(); + expect(mockRouter.navigate).toHaveBeenCalledWith(['/groups']); + }); + }); + + // ── Verificación de redirección coherente ───────────────────────────────── + describe('redirección al acceder a rutas no permitidas', () => { + it('siempre redirige a /groups cuando el acceso es denegado', () => { + mockAuth.getCurrentRole.and.returnValue('ou-minimal'); + runGuard(['super-admin'], 'ou-minimal'); + expect(mockRouter.navigate).toHaveBeenCalledWith(['/groups']); + }); + + it('NO redirige cuando el acceso es permitido', () => { + mockAuth.getCurrentRole.and.returnValue('super-admin'); + runGuard(['super-admin'], 'super-admin'); + expect(mockRouter.navigate).not.toHaveBeenCalled(); + }); + }); + + // ── Matriz completa de rutas de la aplicación ────────────────────────────── + describe('matriz de acceso — todas las rutas protegidas de la aplicación', () => { + interface RouteCase { + path: string; + allowedRoles: string[]; + } + + const routes: RouteCase[] = [ + // solo super-admin + { path: 'users', allowedRoles: ['super-admin'] }, + { path: 'env-vars', allowedRoles: ['super-admin'] }, + { path: 'roles', allowedRoles: ['super-admin'] }, + { path: 'import-legacy', allowedRoles: ['super-admin'] }, + // super-admin + ou-admin + { path: 'commands', allowedRoles: ['super-admin', 'ou-admin'] }, + { path: 'commands-task', allowedRoles: ['super-admin', 'ou-admin'] }, + { path: 'commands-logs', allowedRoles: ['super-admin', 'ou-admin'] }, + { path: 'pxe-images', allowedRoles: ['super-admin', 'ou-admin'] }, + { path: 'pxe', allowedRoles: ['super-admin', 'ou-admin'] }, + { path: 'pxe-boot-file', allowedRoles: ['super-admin', 'ou-admin'] }, + { path: 'ogboot-status', allowedRoles: ['super-admin', 'ou-admin'] }, + { path: 'dhcp-servers', allowedRoles: ['super-admin', 'ou-admin'] }, + { path: 'boot-servers', allowedRoles: ['super-admin', 'ou-admin'] }, + { path: 'calendars', allowedRoles: ['super-admin', 'ou-admin'] }, + { path: 'repositories', allowedRoles: ['super-admin', 'ou-admin'] }, + { path: 'software', allowedRoles: ['super-admin', 'ou-admin'] }, + { path: 'software-profiles', allowedRoles: ['super-admin', 'ou-admin'] }, + { path: 'operative-systems', allowedRoles: ['super-admin', 'ou-admin'] }, + { path: 'menus', allowedRoles: ['super-admin', 'ou-admin'] }, + { path: 'hardware', allowedRoles: ['super-admin', 'ou-admin'] }, + { path: 'hardware-profiles', allowedRoles: ['super-admin', 'ou-admin'] }, + { path: 'hardware-types', allowedRoles: ['super-admin', 'ou-admin'] }, + // super-admin + ou-admin + ou-operator + { path: 'clients/deploy-image', allowedRoles: ['super-admin', 'ou-admin', 'ou-operator'] }, + { path: 'clients/partition-assistant',allowedRoles: ['super-admin', 'ou-admin', 'ou-operator'] }, + { path: 'clients/run-script', allowedRoles: ['super-admin', 'ou-admin', 'ou-operator'] }, + ]; + + const rolesTested: Array<{ role: string; canAccess: (r: RouteCase) => boolean }> = [ + { + role: 'super-admin', + canAccess: () => true, // puede acceder a todo + }, + { + role: 'ou-admin', + canAccess: r => r.allowedRoles.includes('ou-admin'), + }, + { + role: 'ou-operator', + canAccess: r => r.allowedRoles.includes('ou-operator'), + }, + { + role: 'ou-minimal', + canAccess: () => false, // no puede acceder a ninguna ruta protegida + }, + ]; + + rolesTested.forEach(({ role, canAccess }) => { + routes.forEach(route => { + const shouldAccess = canAccess(route); + const label = shouldAccess + ? `${role} PUEDE acceder a /${route.path}` + : `${role} NO PUEDE acceder a /${route.path}`; + + it(label, () => { + mockAuth.getCurrentRole.and.returnValue(role); + const result = runGuard(route.allowedRoles, role); + if (shouldAccess) { + expect(result).toBeTrue(); + } else { + expect(result).toBeFalse(); + expect(mockRouter.navigate).toHaveBeenCalledWith(['/groups']); + } + mockRouter.navigate.calls.reset(); + }); + }); + }); }); }); diff --git a/ogWebconsole/src/app/layout/sidebar/sidebar.component.spec.ts b/ogWebconsole/src/app/layout/sidebar/sidebar.component.spec.ts new file mode 100644 index 0000000..989262f --- /dev/null +++ b/ogWebconsole/src/app/layout/sidebar/sidebar.component.spec.ts @@ -0,0 +1,172 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; +import { RouterTestingModule } from '@angular/router/testing'; +import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; +import { MatDialogModule } from '@angular/material/dialog'; +import { TranslateModule } from '@ngx-translate/core'; +import { SidebarComponent } from './sidebar.component'; +import { AuthService, UserCategory } from '@services/auth.service'; + +// ───────────────────────────────────────────────────────────────────────────── +// Rutas con condiciones de visibilidad (según sidebar.component.html) +// ───────────────────────────────────────────────────────────────────────────── + +/** Siempre visibles para todos los roles autenticados */ +const ALWAYS_VISIBLE = ['/groups']; + +/** Solo visibles para super-admin y ou-admin (ocultos para operator y minimal) */ +const ADMIN_ONLY_ROUTES = [ + '/dhcp-servers', + '/boot-servers', + '/calendars', + '/repositories', + '/menus', +]; + +// ───────────────────────────────────────────────────────────────────────────── +// Helper: cuenta mat-list-item visibles en el DOM +// ───────────────────────────────────────────────────────────────────────────── +function countVisibleListItems(fixture: ComponentFixture): number { + return fixture.debugElement.queryAll(By.css('mat-list-item')).length; +} + +/** Devuelve true si existe algún mat-list-item con [routerLink] apuntando a la ruta dada */ +function hasRouteLink(fixture: ComponentFixture, route: string): boolean { + const items = fixture.debugElement.queryAll(By.css(`[routerLink="${route}"]`)); + return items.length > 0; +} + +// ───────────────────────────────────────────────────────────────────────────── +// Mock de AuthService +// ───────────────────────────────────────────────────────────────────────────── +function mockAuthService(category: UserCategory) { + return { + userCategory: category, + username: 'test-user', + isSuperAdmin: category === 'super-admin', + isOUAdmin: category === 'ou-admin', + isOUOperator: category === 'ou-operator', + isOUMinimal: category === 'ou-minimal', + getCurrentRole: () => category, + }; +} + +// ───────────────────────────────────────────────────────────────────────────── +// Suite principal +// ───────────────────────────────────────────────────────────────────────────── + +describe('SidebarComponent — visibilidad de elementos por rol', () => { + let fixture: ComponentFixture; + let component: SidebarComponent; + + async function createComponent(category: UserCategory) { + await TestBed.configureTestingModule({ + declarations: [SidebarComponent], + imports: [ + RouterTestingModule, + MatDialogModule, + TranslateModule.forRoot(), + ], + providers: [ + { provide: AuthService, useValue: mockAuthService(category) }, + ], + schemas: [CUSTOM_ELEMENTS_SCHEMA], // ignora mat-* desconocidos + }).compileComponents(); + + fixture = TestBed.createComponent(SidebarComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + } + + // ── Grupos — siempre visible ─────────────────────────────────────────────── + describe('/groups — siempre visible para cualquier rol', () => { + (['super-admin', 'ou-admin', 'ou-operator', 'ou-minimal'] as UserCategory[]).forEach(role => { + it(`${role} ve el enlace a /groups`, async () => { + await createComponent(role); + expect(hasRouteLink(fixture, '/groups')).toBeTrue(); + }); + }); + }); + + // ── Super-admin ve todo ──────────────────────────────────────────────────── + describe('super-admin — ve todos los elementos del sidebar', () => { + beforeEach(async () => createComponent('super-admin')); + + ADMIN_ONLY_ROUTES.forEach(route => { + it(`ve el enlace a ${route}`, () => { + expect(hasRouteLink(fixture, route)).toBeTrue(); + }); + }); + + it('ve el menú de acciones (commands)', () => { + // El parent de commands es un mat-list-item sin routerLink pero con el ngIf + const items = fixture.debugElement.queryAll(By.css('mat-list-item')); + expect(items.length).toBeGreaterThan(1); + }); + }); + + // ── Ou-admin ve todo ─────────────────────────────────────────────────────── + describe('ou-admin — ve todos los elementos del sidebar', () => { + beforeEach(async () => createComponent('ou-admin')); + + ADMIN_ONLY_ROUTES.forEach(route => { + it(`ve el enlace a ${route}`, () => { + expect(hasRouteLink(fixture, route)).toBeTrue(); + }); + }); + }); + + // ── Ou-operator: solo /groups ────────────────────────────────────────────── + describe('ou-operator — solo ve /groups', () => { + beforeEach(async () => createComponent('ou-operator')); + + it('ve /groups', () => { + expect(hasRouteLink(fixture, '/groups')).toBeTrue(); + }); + + ADMIN_ONLY_ROUTES.forEach(route => { + it(`NO ve el enlace a ${route}`, () => { + expect(hasRouteLink(fixture, route)).toBeFalse(); + }); + }); + + it('NO ve el menú de acciones (commands)', () => { + // Solo debe haber 1 mat-list-item visible: /groups + expect(countVisibleListItems(fixture)).toBe(1); + }); + }); + + // ── Ou-minimal: solo /groups ─────────────────────────────────────────────── + describe('ou-minimal — solo ve /groups', () => { + beforeEach(async () => createComponent('ou-minimal')); + + it('ve /groups', () => { + expect(hasRouteLink(fixture, '/groups')).toBeTrue(); + }); + + ADMIN_ONLY_ROUTES.forEach(route => { + it(`NO ve el enlace a ${route}`, () => { + expect(hasRouteLink(fixture, route)).toBeFalse(); + }); + }); + + it('NO ve el menú de acciones (commands)', () => { + expect(countVisibleListItems(fixture)).toBe(1); + }); + }); + + // ── Comparativa admin vs operator ───────────────────────────────────────── + describe('comparativa: ou-admin ve más items que ou-operator', () => { + it('ou-admin tiene más mat-list-items visibles que ou-operator', async () => { + await createComponent('ou-admin'); + const adminCount = countVisibleListItems(fixture); + + TestBed.resetTestingModule(); + + await createComponent('ou-operator'); + const operatorCount = countVisibleListItems(fixture); + + expect(adminCount).toBeGreaterThan(operatorCount); + }); + }); +}); diff --git a/ogWebconsole/src/app/services/auth.service.spec.ts b/ogWebconsole/src/app/services/auth.service.spec.ts new file mode 100644 index 0000000..67c8e29 --- /dev/null +++ b/ogWebconsole/src/app/services/auth.service.spec.ts @@ -0,0 +1,162 @@ +import { TestBed } from '@angular/core/testing'; +import { Router } from '@angular/router'; +import { AuthService } from './auth.service'; +import { TranslateModule } from '@ngx-translate/core'; + +// ───────────────────────────────────────────────────────────────────────────── +// Helpers +// ───────────────────────────────────────────────────────────────────────────── + +/** Genera un JWT falso con los roles indicados (sin firma real, solo para tests) */ +function makeToken(roles: string[], username = 'test-user'): string { + const encode = (obj: object) => + btoa(JSON.stringify(obj)) + .replace(/=/g, '') + .replace(/\+/g, '-') + .replace(/\//g, '_'); + + const header = encode({ alg: 'RS256', typ: 'JWT' }); + const payload = encode({ + roles, + username, + id: 1, + uuid: 'test-uuid', + groupsView: 'card', + clientsPerPage: null, + iat: 1000000, + exp: 9999999999, + }); + return `${header}.${payload}.fakesig`; +} + +const TOKEN_SUPER_ADMIN = makeToken(['ROLE_SUPER_ADMIN', 'ROLE_USER']); +const TOKEN_OU_ADMIN = makeToken(['ROLE_ORGANIZATIONAL_UNIT_ADMIN', 'ROLE_USER']); +const TOKEN_OPERATOR = makeToken(['ROLE_ORGANIZATIONAL_UNIT_OPERATOR', 'ROLE_USER']); +const TOKEN_MINIMAL = makeToken(['ROLE_ORGANIZATIONAL_UNIT_MINIMAL', 'ROLE_USER']); +const TOKEN_USER = makeToken(['ROLE_USER']); + +// ───────────────────────────────────────────────────────────────────────────── +// Helpers de setup +// ───────────────────────────────────────────────────────────────────────────── + +function buildService(token: string | null): AuthService { + if (token) { + localStorage.setItem('loginToken', token); + } else { + localStorage.removeItem('loginToken'); + } + // Fuerza reload del token en la nueva instancia + const svc = TestBed.inject(AuthService); + svc.refresh(); + return svc; +} + +// ───────────────────────────────────────────────────────────────────────────── +// Suite principal +// ───────────────────────────────────────────────────────────────────────────── + +describe('AuthService — detección de roles', () => { + let service: AuthService; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [TranslateModule.forRoot()], + providers: [ + AuthService, + { provide: Router, useValue: { navigate: jasmine.createSpy('navigate'), url: '/groups' } }, + ], + }); + }); + + afterEach(() => { + localStorage.removeItem('loginToken'); + }); + + // ── Sin token ────────────────────────────────────────────────────────────── + describe('sin token almacenado', () => { + beforeEach(() => { service = buildService(null); }); + + it('isSuperAdmin es false', () => expect(service.isSuperAdmin).toBeFalse()); + it('isOUAdmin es false', () => expect(service.isOUAdmin).toBeFalse()); + it('isOUOperator es false', () => expect(service.isOUOperator).toBeFalse()); + it('isOUMinimal es false', () => expect(service.isOUMinimal).toBeFalse()); + it('userCategory es "user"',() => expect(service.userCategory).toBe('user')); + it('getCurrentRole es "user"',()=> expect(service.getCurrentRole()).toBe('user')); + it('username es null', () => expect(service.username).toBeNull()); + }); + + // ── ROLE_SUPER_ADMIN ─────────────────────────────────────────────────────── + describe('token con ROLE_SUPER_ADMIN', () => { + beforeEach(() => { service = buildService(TOKEN_SUPER_ADMIN); }); + + it('isSuperAdmin es true', () => expect(service.isSuperAdmin).toBeTrue()); + it('isOUAdmin es false', () => expect(service.isOUAdmin).toBeFalse()); + it('isOUOperator es false', () => expect(service.isOUOperator).toBeFalse()); + it('isOUMinimal es false', () => expect(service.isOUMinimal).toBeFalse()); + it('userCategory es "super-admin"', () => expect(service.userCategory).toBe('super-admin')); + it('getCurrentRole es "super-admin"',() => expect(service.getCurrentRole()).toBe('super-admin')); + }); + + // ── ROLE_ORGANIZATIONAL_UNIT_ADMIN ───────────────────────────────────────── + describe('token con ROLE_ORGANIZATIONAL_UNIT_ADMIN', () => { + beforeEach(() => { service = buildService(TOKEN_OU_ADMIN); }); + + it('isSuperAdmin es false', () => expect(service.isSuperAdmin).toBeFalse()); + it('isOUAdmin es true', () => expect(service.isOUAdmin).toBeTrue()); + it('isOUOperator es false', () => expect(service.isOUOperator).toBeFalse()); + it('isOUMinimal es false', () => expect(service.isOUMinimal).toBeFalse()); + it('userCategory es "ou-admin"', () => expect(service.userCategory).toBe('ou-admin')); + it('getCurrentRole es "ou-admin"',() => expect(service.getCurrentRole()).toBe('ou-admin')); + }); + + // ── ROLE_ORGANIZATIONAL_UNIT_OPERATOR ───────────────────────────────────── + describe('token con ROLE_ORGANIZATIONAL_UNIT_OPERATOR', () => { + beforeEach(() => { service = buildService(TOKEN_OPERATOR); }); + + it('isSuperAdmin es false', () => expect(service.isSuperAdmin).toBeFalse()); + it('isOUAdmin es false', () => expect(service.isOUAdmin).toBeFalse()); + it('isOUOperator es true', () => expect(service.isOUOperator).toBeTrue()); + it('isOUMinimal es false', () => expect(service.isOUMinimal).toBeFalse()); + it('userCategory es "ou-operator"', () => expect(service.userCategory).toBe('ou-operator')); + it('getCurrentRole es "ou-operator"',() => expect(service.getCurrentRole()).toBe('ou-operator')); + }); + + // ── ROLE_ORGANIZATIONAL_UNIT_MINIMAL ────────────────────────────────────── + describe('token con ROLE_ORGANIZATIONAL_UNIT_MINIMAL', () => { + beforeEach(() => { service = buildService(TOKEN_MINIMAL); }); + + it('isSuperAdmin es false', () => expect(service.isSuperAdmin).toBeFalse()); + it('isOUAdmin es false', () => expect(service.isOUAdmin).toBeFalse()); + it('isOUOperator es false', () => expect(service.isOUOperator).toBeFalse()); + it('isOUMinimal es true', () => expect(service.isOUMinimal).toBeTrue()); + it('userCategory es "ou-minimal"', () => expect(service.userCategory).toBe('ou-minimal')); + it('getCurrentRole es "ou-minimal"',() => expect(service.getCurrentRole()).toBe('ou-minimal')); + }); + + // ── ROLE_USER (sin rol elevado) ──────────────────────────────────────────── + describe('token con solo ROLE_USER', () => { + beforeEach(() => { service = buildService(TOKEN_USER); }); + + it('todos los flags de rol son false', () => { + expect(service.isSuperAdmin).toBeFalse(); + expect(service.isOUAdmin).toBeFalse(); + expect(service.isOUOperator).toBeFalse(); + expect(service.isOUMinimal).toBeFalse(); + }); + it('userCategory es "user"', () => expect(service.userCategory).toBe('user')); + it('getCurrentRole es "user"', () => expect(service.getCurrentRole()).toBe('user')); + }); + + // ── Prioridad de roles ───────────────────────────────────────────────────── + describe('prioridad cuando hay múltiples roles en el token', () => { + it('super-admin tiene prioridad sobre ou-admin', () => { + service = buildService(makeToken(['ROLE_SUPER_ADMIN', 'ROLE_ORGANIZATIONAL_UNIT_ADMIN'])); + expect(service.userCategory).toBe('super-admin'); + }); + + it('ou-admin tiene prioridad sobre ou-operator', () => { + service = buildService(makeToken(['ROLE_ORGANIZATIONAL_UNIT_ADMIN', 'ROLE_ORGANIZATIONAL_UNIT_OPERATOR'])); + expect(service.userCategory).toBe('ou-admin'); + }); + }); +}); diff --git a/ogWebconsole/test-results/ogGui-junit-report.xml b/ogWebconsole/test-results/ogGui-junit-report.xml deleted file mode 100644 index 08f02de..0000000 --- a/ogWebconsole/test-results/ogGui-junit-report.xml +++ /dev/null @@ -1,98 +0,0 @@ -<<<<<<< Updated upstream - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -======= ->>>>>>> Stashed changes