165 lines
7.5 KiB
TypeScript
165 lines
7.5 KiB
TypeScript
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');
|
||
});
|
||
});
|