2026-10-16 12:35:46 -05:00
|
|
|
|
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', () => {
|
2026-10-16 13:23:27 -05:00
|
|
|
|
for (const a of ['login', 'login_failed', 'logout', 'sl_create', 'sl_update', 'sl_action', 'sl_denied', 'permissions_set', 'permissions_remove']) {
|
2026-10-16 12:35:46 -05:00
|
|
|
|
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');
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|