Files
basic-erp/apps/web/src/app/features/admin/audit.page.spec.ts
T

165 lines
7.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { provideHttpClient } from '@angular/common/http';
import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing';
import { type ComponentFixture, TestBed } from '@angular/core/testing';
import type { AuditRow } from '@basic-erp/sap-types';
import { beforeEach, describe, expect, it } from 'vitest';
import { AUDIT_ACTIONS, AuditPage, describeDetail, formatTimestamp } from './audit.page';
const row = (over: Partial<AuditRow> = {}): AuditRow => ({
id: 1,
ts: '2026-10-16T15:04:05.000Z',
webUser: 'ana',
action: 'sl_update',
resource: 'Orders',
key: '316',
outcome: 'ok',
ip: '10.0.0.5',
detail: { fields: ['Comments'] },
...over,
});
describe('descripción del detalle', () => {
it('lista los campos tocados y las filas de cada tabla, nunca valores', () => {
expect(describeDetail(row({ detail: { fields: ['CardCode', 'Comments'], collections: { DocumentLines: 3, BPAddresses: 0 } } }))).toBe(
'Campos: CardCode, Comments · Tablas: DocumentLines (3), BPAddresses (0)',
);
});
it('muestra el motivo de un login fallido y el error de SAP', () => {
expect(describeDetail(row({ detail: { reason: 'credenciales' } }))).toBe('credenciales');
expect(describeDetail(row({ detail: { fields: ['CardCode'], error: '1250000110 - RUT is missing' } }))).toBe('1250000110 - RUT is missing · Campos: CardCode');
});
it('muestra los permisos asignados y el método denegado', () => {
expect(describeDetail(row({ detail: { paths: ['/ventas/*', '/compras/ordenes'] } }))).toBe('Permisos: /ventas/*, /compras/ordenes');
expect(describeDetail(row({ detail: { paths: [] } }))).toBe('Permisos: (ninguno)');
expect(describeDetail(row({ detail: { method: 'POST' } }))).toBe('POST');
});
it('sin detalle, vacío', () => {
expect(describeDetail(row({ detail: null }))).toBe('');
});
it('la fecha se muestra local, y vacía si no hay', () => {
expect(formatTimestamp('2026-10-16T15:04:05.000Z')).toMatch(/\d/);
expect(formatTimestamp(null)).toBe('');
});
it('cada acción del servidor tiene su nombre en español', () => {
for (const a of ['login', 'login_failed', 'logout', 'sl_create', 'sl_update', 'sl_action', 'sl_denied', 'permissions_set', 'permissions_remove']) {
expect(AUDIT_ACTIONS[a], a).toBeTruthy();
}
});
});
describe('AuditPage', () => {
let fixture: ComponentFixture<AuditPage>;
let ctrl: HttpTestingController;
const el = () => fixture.nativeElement as HTMLElement;
const tick = async () => {
fixture.detectChanges();
await new Promise((r) => setTimeout(r));
fixture.detectChanges();
};
const reply = (rows: AuditRow[], total = rows.length) => ctrl.expectOne((r) => r.url === '/api/audit').flush({ total, rows });
beforeEach(async () => {
TestBed.resetTestingModule();
await TestBed.configureTestingModule({ imports: [AuditPage], providers: [provideHttpClient(), provideHttpClientTesting()] }).compileComponents();
ctrl = TestBed.inject(HttpTestingController);
fixture = TestBed.createComponent(AuditPage);
await tick();
});
it('pide lo más reciente, 50 por página, sin filtros', async () => {
const req = ctrl.expectOne((r) => r.url === '/api/audit');
expect(req.request.params.get('limit')).toBe('50');
expect(req.request.params.get('offset')).toBe('0');
expect(req.request.params.keys().sort()).toEqual(['limit', 'offset']);
req.flush({ total: 0, rows: [] });
await tick();
expect(el().textContent).toContain('Sin registros');
});
it('muestra cada hecho con su acción, resultado y detalle', async () => {
reply([
row(),
row({ id: 2, action: 'login_failed', webUser: 'x', outcome: 'error', resource: null, key: null, detail: { reason: 'credenciales' } }),
row({ id: 3, action: 'sl_denied', outcome: 'denied', detail: { method: 'POST' } }),
]);
await tick();
const rows = [...el().querySelectorAll('tbody tr')].map((r) => r.textContent ?? '');
expect(rows).toHaveLength(3);
expect(rows[0]).toContain('Modificación');
expect(rows[0]).toContain('Orders');
expect(rows[0]).toContain('Campos: Comments');
expect(rows[1]).toContain('Inicio de sesión fallido');
expect(rows[2]).toContain('Operación denegada');
expect([...el().querySelectorAll('tbody .badge')].map((b) => b.textContent?.trim())).toEqual(['Correcto', 'Error', 'Denegado']);
expect(el().querySelector('.pager')?.textContent).toContain('1–3 de 3');
});
it('Filtrar envía solo los filtros escritos y vuelve a la primera página', async () => {
reply([row()], 120);
await tick();
const input = (label: string) => [...el().querySelectorAll('label')].find((l) => l.textContent?.trim().startsWith(label))!.querySelector('input, select') as HTMLInputElement;
input('Usuario').value = 'ana';
input('Usuario').dispatchEvent(new Event('input'));
input('Acción').value = 'sl_update';
input('Acción').dispatchEvent(new Event('change'));
input('Desde').value = '2026-10-01';
input('Desde').dispatchEvent(new Event('input'));
(el().querySelector('button[type=submit]') as HTMLElement).click();
await tick();
const req = ctrl.expectOne((r) => r.url === '/api/audit');
expect(req.request.params.get('webUser')).toBe('ana');
expect(req.request.params.get('action')).toBe('sl_update');
expect(req.request.params.get('from')).toBe('2026-10-01');
expect(req.request.params.has('resource')).toBe(false);
expect(req.request.params.get('offset')).toBe('0');
req.flush({ total: 1, rows: [row()] });
});
it('pagina: Siguiente pide la página que sigue y Anterior vuelve', async () => {
reply([row()], 120);
await tick();
expect(el().querySelector('.pager')?.textContent).toContain('1–1 de 120');
const nextBtn = [...el().querySelectorAll<HTMLButtonElement>('.pager button')].find((b) => b.textContent?.includes('Siguiente'))!;
expect(nextBtn.disabled).toBe(false);
nextBtn.click();
await tick();
const req = ctrl.expectOne((r) => r.url === '/api/audit');
expect(req.request.params.get('offset')).toBe('50');
req.flush({ total: 120, rows: [row({ id: 51 })] });
await tick();
const prev = [...el().querySelectorAll<HTMLButtonElement>('.pager button')].find((b) => b.textContent?.includes('Anterior'))!;
expect(prev.disabled).toBe(false);
prev.click();
await tick();
expect(ctrl.expectOne((r) => r.url === '/api/audit').request.params.get('offset')).toBe('0');
});
it('Limpiar quita los filtros', async () => {
reply([row()]);
await tick();
const user = [...el().querySelectorAll('label')].find((l) => l.textContent?.trim().startsWith('Usuario'))!.querySelector('input') as HTMLInputElement;
user.value = 'ana';
user.dispatchEvent(new Event('input'));
(el().querySelector('button[type=submit]') as HTMLElement).click();
await tick();
reply([row()]);
await tick();
([...el().querySelectorAll('button')].find((b) => b.textContent?.trim() === 'Limpiar') as HTMLElement).click();
await tick();
const req = ctrl.expectOne((r) => r.url === '/api/audit');
expect(req.request.params.has('webUser')).toBe(false);
req.flush({ total: 0, rows: [] });
});
it('un error del servidor se avisa con Reintentar', async () => {
ctrl.expectOne((r) => r.url === '/api/audit').flush({ message: 'Requiere permisos de administrador' }, { status: 403, statusText: 'Forbidden' });
await tick();
expect(el().querySelector('.error')?.textContent).toContain('Requiere permisos de administrador');
});
});