feat(oggui): sistema de permisos por rol + mejoras de UX y tests #95
|
|
@ -43,3 +43,5 @@ Thumbs.db
|
|||
|
||||
test-results/
|
||||
|
||||
.e2e-state.json
|
||||
playwright-report/
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
}
|
||||
|
|
@ -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');
|
||||
}
|
||||
|
|
@ -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<any> {
|
||||
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<string> {
|
||||
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<string | null> {
|
||||
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<string | null> {
|
||||
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<void> {
|
||||
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<string> {
|
||||
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<string> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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);
|
||||
}
|
||||
|
|
@ -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<void> {
|
||||
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<string> {
|
||||
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<void>;
|
||||
/** Comprueba que un enlace del sidebar NO está en el DOM */
|
||||
export async function expectSidebarLink(page: Page, routerLink: string, visible: false): Promise<void>;
|
||||
export async function expectSidebarLink(page: Page, routerLink: string, visible: boolean): Promise<void> {
|
||||
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 });
|
||||
}
|
||||
}
|
||||
|
|
@ -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 });
|
||||
});
|
||||
});
|
||||
|
|
@ -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 });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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 });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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 });
|
||||
});
|
||||
});
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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'] },
|
||||
},
|
||||
],
|
||||
});
|
||||
|
|
@ -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'] } },
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
<h2 joyrideStep="titleStep" text="{{ 'titleStepText' | translate }}">{{ 'CommandsTitle' | translate }}</h2>
|
||||
</div>
|
||||
<div class="command-button-row" joyrideStep="addCommandStep" text="{{ 'addCommandStepText' | translate }}">
|
||||
<button class="action-button" (click)="openCreateCommandModal()">{{ 'addCommand' | translate }}</button>
|
||||
<button *ngIf="auth.isSuperAdmin" class="action-button" (click)="openCreateCommandModal()">{{ 'addCommand' | translate }}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -44,8 +44,8 @@
|
|||
<th mat-header-cell *matHeaderCellDef style="text-align: center;">{{ 'actions' | translate }}</th>
|
||||
<td mat-cell *matCellDef="let command" style="text-align: center;" joyrideStep="actionsStep" text="{{ 'actionsStepText' | translate }}">
|
||||
<button mat-icon-button color="info" (click)="viewDetails($event, command)"><mat-icon>visibility</mat-icon></button>
|
||||
<button mat-icon-button color="primary" [disabled]="command.readOnly" (click)="editCommand($event, command)"><mat-icon>edit</mat-icon></button>
|
||||
<button mat-icon-button color="warn" [disabled]="command.readOnly" (click)="deleteCommand($event, command)"><mat-icon>delete</mat-icon></button>
|
||||
<button *ngIf="auth.isSuperAdmin" mat-icon-button color="primary" [disabled]="command.readOnly" (click)="editCommand($event, command)"><mat-icon>edit</mat-icon></button>
|
||||
<button *ngIf="auth.isSuperAdmin" mat-icon-button color="warn" [disabled]="command.readOnly" (click)="deleteCommand($event, command)"><mat-icon>delete</mat-icon></button>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
|
|
|
|||
|
|
@ -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<any>();
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -16,11 +16,11 @@
|
|||
</div>
|
||||
<div class="groups-button-row">
|
||||
<div joyrideStep="addStep" text="{{ 'groupsAddStepText' | translate }}" style="display: flex; gap: 8px;">
|
||||
<button class="action-button" (click)="addOU($event)" *ngIf="auth.userCategory !== 'ou-minimal'"
|
||||
<button class="action-button" (click)="addOU($event)" *ngIf="auth.userCategory !== 'ou-minimal' && auth.userCategory !== 'ou-operator'"
|
||||
matTooltip="{{ 'newOrganizationalUnitTooltip' | translate }}" matTooltipShowDelay="1000">
|
||||
{{ 'newOrganizationalUnitButton' | translate }}
|
||||
</button>
|
||||
<button class="action-button" [matMenuTriggerFor]="menuClients" *ngIf="auth.userCategory !== 'ou-minimal'"
|
||||
<button class="action-button" [matMenuTriggerFor]="menuClients" *ngIf="auth.userCategory !== 'ou-minimal' && auth.userCategory !== 'ou-operator'"
|
||||
[disabled]="!hasOrganizationalUnits()"
|
||||
matTooltip="{{ hasOrganizationalUnits() ? ('newClientButtonTooltip' | translate) : ('noOrganizationalUnitsTooltip' | translate) }}"
|
||||
matTooltipShowDelay="1000">{{
|
||||
|
|
@ -256,15 +256,15 @@
|
|||
<mat-icon>map</mat-icon>
|
||||
<span>{{ 'roomMap' | translate }}</span>
|
||||
</button>
|
||||
<button mat-menu-item (click)="addClient($event, selectedNode)" *ngIf="auth.userCategory !== 'ou-minimal'" [disabled]="!hasOrganizationalUnits()">
|
||||
<button mat-menu-item (click)="addClient($event, selectedNode)" *ngIf="auth.userCategory !== 'ou-minimal' && auth.userCategory !== 'ou-operator'" [disabled]="!hasOrganizationalUnits()">
|
||||
<mat-icon>add</mat-icon>
|
||||
<span>{{ 'newSingleClientButton' | translate }}</span>
|
||||
</button>
|
||||
<button mat-menu-item (click)="addMultipleClients($event, selectedNode)" *ngIf="auth.userCategory !== 'ou-minimal'" [disabled]="!hasOrganizationalUnits()">
|
||||
<button mat-menu-item (click)="addMultipleClients($event, selectedNode)" *ngIf="auth.userCategory !== 'ou-minimal' && auth.userCategory !== 'ou-operator'" [disabled]="!hasOrganizationalUnits()">
|
||||
<mat-icon>playlist_add</mat-icon>
|
||||
<span>{{ 'newMultipleClientButton' | translate }}</span>
|
||||
</button>
|
||||
<button mat-menu-item (click)="addOU($event, selectedNode)" *ngIf="auth.userCategory !== 'ou-minimal'">
|
||||
<button mat-menu-item (click)="addOU($event, selectedNode)" *ngIf="auth.userCategory !== 'ou-minimal' && auth.userCategory !== 'ou-operator'">
|
||||
<mat-icon>account_tree</mat-icon>
|
||||
<span>{{ 'addOrganizationalUnit' | translate }}</span>
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -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<any>(`${this.baseUrl}/clients?organizationalUnit.id=${node.id}&page=${this.page + 1}&itemsPerPage=${this.itemsPerPage}`, { params: backendParams }).subscribe({
|
||||
this.fetchClientsSubscription = this.http.get<any>(`${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'));
|
||||
},
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -34,5 +34,6 @@
|
|||
</button>
|
||||
<app-loading *ngIf="syncingUds" [isLoading]="syncingUds"></app-loading>
|
||||
</ng-container>
|
||||
<button class="ordinary-button" mat-dialog-close>{{ 'buttonCancel' | translate }}</button>
|
||||
<button class="submit-button" (click)="saveDisposition()">{{ 'saveDispositionButton' | translate }}</button>
|
||||
</mat-dialog-actions>
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
{{ 'ogbootServers' | translate }}</h2>
|
||||
</div>
|
||||
<div class="images-button-row">
|
||||
<button
|
||||
<button *ngIf="auth.isSuperAdmin"
|
||||
class="action-button" (click)="addServer()"
|
||||
joyrideStep="addStep"
|
||||
text="Añadir Servidor Boot">{{ 'addBootServer' | translate }}</button>
|
||||
|
|
@ -51,10 +51,10 @@
|
|||
<button mat-icon-button color="primary" (click)="syncServer(server)" [matTooltip]="'syncServer' | translate">
|
||||
<mat-icon>sync</mat-icon>
|
||||
</button>
|
||||
<button mat-icon-button color="primary" (click)="editServer($event, server)" [matTooltip]="'editServerTitle' | translate">
|
||||
<button *ngIf="auth.isSuperAdmin" mat-icon-button color="primary" (click)="editServer($event, server)" [matTooltip]="'editServerTitle' | translate">
|
||||
<mat-icon>edit</mat-icon>
|
||||
</button>
|
||||
<button mat-icon-button color="warn" (click)="deleteServer($event, server)" [matTooltip]="'buttonDeleteServer' | translate">
|
||||
<button *ngIf="auth.isSuperAdmin" mat-icon-button color="warn" (click)="deleteServer($event, server)" [matTooltip]="'buttonDeleteServer' | translate">
|
||||
<mat-icon>delete</mat-icon>
|
||||
</button>
|
||||
</td>
|
||||
|
|
|
|||
|
|
@ -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<any>();
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
{{ 'dhcpServers' | translate }}</h2>
|
||||
</div>
|
||||
<div class="images-button-row">
|
||||
<button
|
||||
<button *ngIf="auth.isSuperAdmin"
|
||||
class="action-button" (click)="addServer()"
|
||||
joyrideStep="addStep"
|
||||
text="Añadir Servidor DHCP">{{ 'addDhcpServer' | translate }}</button>
|
||||
|
|
@ -54,9 +54,9 @@
|
|||
<ng-container matColumnDef="actions">
|
||||
<th mat-header-cell *matHeaderCellDef style="text-align: center;">{{ 'actions' | translate }}</th>
|
||||
<td mat-cell *matCellDef="let server" style="text-align: center;" joyrideStep="actionsStep">
|
||||
<button mat-icon-button color="primary" (click)="editServer($event, server)" [matTooltip]="'editServerTitle' | translate">
|
||||
<button *ngIf="auth.isSuperAdmin" mat-icon-button color="primary" (click)="editServer($event, server)" [matTooltip]="'editServerTitle' | translate">
|
||||
<mat-icon>edit</mat-icon></button>
|
||||
<button mat-icon-button color="warn" (click)="deleteServer($event, server)" [matTooltip]="'buttonDeleteServer' | translate">
|
||||
<button *ngIf="auth.isSuperAdmin" mat-icon-button color="warn" (click)="deleteServer($event, server)" [matTooltip]="'buttonDeleteServer' | translate">
|
||||
<mat-icon>delete</mat-icon>
|
||||
</button>
|
||||
</td>
|
||||
|
|
|
|||
|
|
@ -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<any>();
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@
|
|||
</h2>
|
||||
</div>
|
||||
<div class="images-button-row">
|
||||
<button
|
||||
<button *ngIf="auth.isSuperAdmin"
|
||||
class="action-button" (click)="addImage()"
|
||||
joyrideStep="addStep"
|
||||
text="{{ 'addRepositoryStepText' | translate }}">{{ 'addRepository' | translate }}</button>
|
||||
|
|
@ -68,9 +68,9 @@
|
|||
<th mat-header-cell *matHeaderCellDef i18n="@@columnActions" style="text-align: center;">{{ 'actions' | translate }}</th>
|
||||
<td mat-cell *matCellDef="let repository" style="text-align: center;" joyrideStep="actionsStep"
|
||||
[text]="'actionsDescription' | translate">
|
||||
<button mat-icon-button color="primary" (click)="editRepository($event, repository)" i18n="@@editImage">
|
||||
<button *ngIf="auth.isSuperAdmin" mat-icon-button color="primary" (click)="editRepository($event, repository)" i18n="@@editImage">
|
||||
<mat-icon>edit</mat-icon></button>
|
||||
<button mat-icon-button color="warn" (click)="deleteRepository($event, repository)">
|
||||
<button *ngIf="auth.isSuperAdmin" mat-icon-button color="warn" (click)="deleteRepository($event, repository)">
|
||||
<mat-icon i18n="@@deleteElementTooltip">delete</mat-icon>
|
||||
</button>
|
||||
</td>
|
||||
|
|
|
|||
|
|
@ -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<any>();
|
||||
|
|
|
|||
|
|
@ -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<Router>;
|
||||
let mockAuth: jasmine.SpyObj<AuthService>;
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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<SidebarComponent>): 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<SidebarComponent>, 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<SidebarComponent>;
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,98 +0,0 @@
|
|||
<<<<<<< Updated upstream
|
||||
<?xml version="1.0"?>
|
||||
<testsuite name="Chrome Headless 131.0.0.0 (Linux x86_64)" package="" timestamp="2024-12-09T10:14:38" id="0" hostname="alvaro-Latitude-3420" tests="80" errors="0" failures="0" time="2.257">
|
||||
<properties>
|
||||
<property name="browser.fullName" value="Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/131.0.0.0 Safari/537.36"/>
|
||||
</properties>
|
||||
<testcase name="CreateSoftwareComponent should create" time="0.12" classname="CreateSoftwareComponent"/>
|
||||
<testcase name="CommandsComponent should create" time="0.104" classname="CommandsComponent"/>
|
||||
<testcase name="OgbootStatusComponent should create the component" time="0.054" classname="OgbootStatusComponent"/>
|
||||
<testcase name="PxeComponent should have a defined currentPage" time="0.069" classname="PxeComponent"/>
|
||||
<testcase name="PxeComponent should have a defined itemsPerPage" time="0.041" classname="PxeComponent"/>
|
||||
<testcase name="PxeComponent should have a defined component" time="0.035" classname="PxeComponent"/>
|
||||
<testcase name="PxeComponent should have a defined loading" time="0.037" classname="PxeComponent"/>
|
||||
<testcase name="PxeComponent should have a defined page" time="0.036" classname="PxeComponent"/>
|
||||
<testcase name="PxeComponent should have a defined selectedElements" time="0.032" classname="PxeComponent"/>
|
||||
<testcase name="PxeComponent should have a defined pageSizeOptions" time="0.034" classname="PxeComponent"/>
|
||||
<testcase name="PxeComponent should have a defined selectedItem" time="0.031" classname="PxeComponent"/>
|
||||
<testcase name="PxeComponent should have a defined length" time="0.029" classname="PxeComponent"/>
|
||||
<testcase name="PxeComponent should have a defined datePipe" time="0.028" classname="PxeComponent"/>
|
||||
<testcase name="PxeComponent should have a defined dataSource" time="0.029" classname="PxeComponent"/>
|
||||
<testcase name="PxeComponent should have a defined baseUrl" time="0.029" classname="PxeComponent"/>
|
||||
<testcase name="PxeComponent should have a defined alertMessage" time="0.025" classname="PxeComponent"/>
|
||||
<testcase name="PxeComponent should have a defined pxeTemplates" time="0.027" classname="PxeComponent"/>
|
||||
<testcase name="PxeComponent should have a defined filters" time="0.029" classname="PxeComponent"/>
|
||||
<testcase name="PxeComponent should create the component" time="0.025" classname="PxeComponent"/>
|
||||
<testcase name="AdminComponent debería renderizar dos botones" time="0.012" classname="AdminComponent"/>
|
||||
<testcase name="AdminComponent debería tener un botón con routerLink a "/users"" time="0.005" classname="AdminComponent"/>
|
||||
<testcase name="AdminComponent debería aplicar la clase "fab-button" a ambos botones" time="0.005" classname="AdminComponent"/>
|
||||
<testcase name="AdminComponent debería crear el componente" time="0.006" classname="AdminComponent"/>
|
||||
<testcase name="CreateOperativeSystemComponent should create" time="0.017" classname="CreateOperativeSystemComponent"/>
|
||||
<testcase name="PxeBootFilesComponent should create" time="0.048" classname="PxeBootFilesComponent"/>
|
||||
<testcase name="CalendarComponent should create" time="0.05" classname="CalendarComponent"/>
|
||||
<testcase name="CreateRepositoryComponent should create" time="0.018" classname="CreateRepositoryComponent"/>
|
||||
<testcase name="PXEimagesComponent should create" time="0.077" classname="PXEimagesComponent"/>
|
||||
<testcase name="SoftwareComponent should create" time="0.055" classname="SoftwareComponent"/>
|
||||
<testcase name="CreateCommandComponent should create" time="0.033" classname="CreateCommandComponent"/>
|
||||
<testcase name="AppComponent should default to Spanish if no language is saved in localStorage" time="0.01" classname="AppComponent"/>
|
||||
<testcase name="AppComponent should create the app" time="0.003" classname="AppComponent"/>
|
||||
<testcase name="AppComponent should set language to Spanish in sessionStorage on ngOnInit" time="0.002" classname="AppComponent"/>
|
||||
<testcase name="AppComponent should have as title 'ogWebconsole'" time="0.003" classname="AppComponent"/>
|
||||
<testcase name="AppComponent should set the language from localStorage on creation" time="0.003" classname="AppComponent"/>
|
||||
<testcase name="DeployImageComponent should create" time="0.08" classname="DeployImageComponent"/>
|
||||
<testcase name="CreateSoftwareProfileComponent should create" time="0.085" classname="CreateSoftwareProfileComponent"/>
|
||||
<testcase name="OgDhcpSubnetsComponent should create" time="0.078" classname="OgDhcpSubnetsComponent"/>
|
||||
<testcase name="OgDhcpSubnetsComponent should call syncSubnets and handle success" time="0.044" classname="OgDhcpSubnetsComponent"/>
|
||||
<testcase name="SoftwareProfileComponent should create" time="0.051" classname="SoftwareProfileComponent"/>
|
||||
<testcase name="RolesComponent should create" time="0.021" classname="RolesComponent"/>
|
||||
<testcase name="RolesComponent should initialize the dataSource" time="0.005" classname="RolesComponent"/>
|
||||
<testcase name="RolesComponent should have a defined columns array" time="0.005" classname="RolesComponent"/>
|
||||
<testcase name="RolesComponent should have a default itemsPerPage value" time="0.007" classname="RolesComponent"/>
|
||||
<testcase name="EnvVarsComponent should create" time="0.019" classname="EnvVarsComponent"/>
|
||||
<testcase name="MainRepositoryViewComponent should create" time="0.071" classname="MainRepositoryViewComponent"/>
|
||||
<testcase name="StatusComponent should create" time="0.022" classname="StatusComponent"/>
|
||||
<testcase name="CommandsTaskComponent should create" time="0.039" classname="CommandsTaskComponent"/>
|
||||
<testcase name="LoginComponent should add rotating class to the logo when loading" time="0.025" classname="LoginComponent"/>
|
||||
<testcase name="LoginComponent should create" time="0.01" classname="LoginComponent"/>
|
||||
<testcase name="LoginComponent should add "invalid" class to username input if it is invalid and touched" time="0.011" classname="LoginComponent"/>
|
||||
<testcase name="LoginComponent should show a success toast message" time="0.01" classname="LoginComponent"/>
|
||||
<testcase name="LoginComponent should not add rotating class to the logo when not loading" time="0.01" classname="LoginComponent"/>
|
||||
<testcase name="LoginComponent should show error message on login failure" time="0.01" classname="LoginComponent"/>
|
||||
<testcase name="LoginComponent should toggle password visibility when clicking the button" time="0.01" classname="LoginComponent"/>
|
||||
<testcase name="LoginComponent should disable the login button if username or password is missing" time="0.01" classname="LoginComponent"/>
|
||||
<testcase name="LoginComponent should change language to English and save to localStorage" time="0.009" classname="LoginComponent"/>
|
||||
<testcase name="LoginComponent should call onLogin and navigate on successful login" time="0.012" classname="LoginComponent"/>
|
||||
<testcase name="LoginComponent should enable the login button if username and password are present" time="0.01" classname="LoginComponent"/>
|
||||
<testcase name="LoginComponent should change language to Spanish and save to localStorage" time="0.01" classname="LoginComponent"/>
|
||||
<testcase name="LoginComponent should show an error toast message" time="0.01" classname="LoginComponent"/>
|
||||
<testcase name="ExecuteCommandComponent should create" time="0.025" classname="ExecuteCommandComponent"/>
|
||||
<testcase name="CreateImageComponent should create" time="0.025" classname="CreateImageComponent"/>
|
||||
<testcase name="OgdhcpComponent should create" time="0.005" classname="OgdhcpComponent"/>
|
||||
<testcase name="DashboardComponent should create the component" time="0.006" classname="DashboardComponent"/>
|
||||
<testcase name="GroupsComponent should call getFilters on ngOnInit" time="0.141" classname="GroupsComponent"/>
|
||||
<testcase name="GroupsComponent should call search on ngOnInit" time="0.011" classname="GroupsComponent"/>
|
||||
<testcase name="GroupsComponent should call getFilters method" time="0.014" classname="GroupsComponent"/>
|
||||
<testcase name="GroupsComponent should call onSelectUnidad method" time="0.009" classname="GroupsComponent"/>
|
||||
<testcase name="GroupsComponent should call onTabChange method" time="0.011" classname="GroupsComponent"/>
|
||||
<testcase name="GroupsComponent should create" time="0.013" classname="GroupsComponent"/>
|
||||
<testcase name="GroupsComponent should call search method" time="0.011" classname="GroupsComponent"/>
|
||||
<testcase name="ServerInfoDialogComponent should create" time="0.013" classname="ServerInfoDialogComponent"/>
|
||||
<testcase name="UsersComponent should call search on init" time="0.032" classname="UsersComponent"/>
|
||||
<testcase name="UsersComponent should have default values for pagination" time="0.006" classname="UsersComponent"/>
|
||||
<testcase name="UsersComponent should create" time="0.008" classname="UsersComponent"/>
|
||||
<testcase name="UsersComponent should define displayedColumns" time="0.007" classname="UsersComponent"/>
|
||||
<testcase name="UsersComponent should initialize the dataSource" time="0.007" classname="UsersComponent"/>
|
||||
<testcase name="OperativeSystemComponent should create" time="0.041" classname="OperativeSystemComponent"/>
|
||||
<testcase name="RepositoriesComponent should create" time="0.037" classname="RepositoriesComponent"/>
|
||||
<system-out>
|
||||
<![CDATA[Chrome Headless 131.0.0.0 (Linux x86_64) ERROR: 'Error fetching organizational units:', HttpErrorResponse{headers: HttpHeaders{normalizedNames: Map{}, lazyUpdate: null, headers: Map{}}, status: 0, statusText: 'Unknown Error', url: 'https://127.0.0.1:8443/organizational-units?type=classroom&page=1&itemsPerPage=50', ok: false, name: 'HttpErrorResponse', message: 'Http failure response for https://127.0.0.1:8443/organizational-units?type=classroom&page=1&itemsPerPage=50: 0 Unknown Error', error: ProgressEvent{isTrusted: true}}
|
||||
,Chrome Headless 131.0.0.0 (Linux x86_64) ERROR: 'Error al sincronizar', HttpErrorResponse{headers: HttpHeaders{normalizedNames: Map{}, lazyUpdate: null, headers: Map{}}, status: 0, statusText: 'Unknown Error', url: 'https://127.0.0.1:8443/og-lives/sync', ok: false, name: 'HttpErrorResponse', message: 'Http failure response for https://127.0.0.1:8443/og-lives/sync: 0 Unknown Error', error: ProgressEvent{isTrusted: true}}
|
||||
,Chrome Headless 131.0.0.0 (Linux x86_64) ERROR: 'Error fetching images', HttpErrorResponse{headers: HttpHeaders{normalizedNames: Map{}, lazyUpdate: null, headers: Map{}}, status: 0, statusText: 'Unknown Error', url: 'https://127.0.0.1:8443/og-lives?page=1&itemsPerPage=1000', ok: false, name: 'HttpErrorResponse', message: 'Http failure response for https://127.0.0.1:8443/og-lives?page=1&itemsPerPage=1000: 0 Unknown Error', error: ProgressEvent{isTrusted: true}}
|
||||
,Chrome Headless 131.0.0.0 (Linux x86_64) ERROR: 'Error fetching og lives', HttpErrorResponse{headers: HttpHeaders{normalizedNames: Map{}, lazyUpdate: null, headers: Map{}}, status: 0, statusText: 'Unknown Error', url: 'https://127.0.0.1:8443/og-lives?page=1&itemsPerPage=1000', ok: false, name: 'HttpErrorResponse', message: 'Http failure response for https://127.0.0.1:8443/og-lives?page=1&itemsPerPage=1000: 0 Unknown Error', error: ProgressEvent{isTrusted: true}}
|
||||
|
||||
]]>
|
||||
</system-out>
|
||||
<system-err/>
|
||||
</testsuite>
|
||||
=======
|
||||
>>>>>>> Stashed changes
|
||||
Loading…
Reference in New Issue