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

164 lines
7.5 KiB
TypeScript
Raw Normal View History

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');
});
});