2026-10-16 12:35:46 -05:00
import { ForbiddenException } from '@nestjs/common' ;
import { vi } from 'vitest' ;
import type { AuditService } from '../audit/audit.service.js' ;
2026-10-19 12:01:26 -05:00
import type { AppConfigService } from '../config/app-config.service.js' ;
2026-10-16 12:35:46 -05:00
import type { AuthUser } from '../auth/auth.types.js' ;
import type { PermissionsService } from '../permissions/permissions.service.js' ;
import type { SapClientService } from '../sap/sap-client.service.js' ;
import { SlProxyService } from './sl-proxy.service.js' ;
const user : AuthUser = { webUser : 'ana' , empId : 7 , firstName : 'Ana' , lastName : 'P' };
describe ( 'SlProxyService: auditoría' , () => {
let request : ReturnType < typeof vi.fn >;
let record : ReturnType < typeof vi.fn >;
let paths : string [];
let service : SlProxyService ;
beforeEach (() => {
request = vi . fn ( async () => ({ data : { DocEntry : 316 , DocNum : 12 } }));
record = vi . fn ( async () => undefined );
paths = [ '*' ];
service = new SlProxyService (
{ request } as unknown as SapClientService ,
{ getPaths : async () => paths } as unknown as PermissionsService ,
{ record } as unknown as AuditService ,
);
});
it ( 'crear queda registrado con la clave creada y solo los nombres de los campos' , async () => {
await service . create ( user , 'Orders' , { CardCode : 'C1' , Comments : 'privado' , DocumentLines : [{ ItemCode : 'A' }, { ItemCode : 'B' }] }, {}, '10.0.0.5' );
expect ( record ). toHaveBeenCalledWith ({
action : 'sl_create' ,
webUser : 'ana' ,
ip : '10.0.0.5' ,
resource : 'Orders' ,
key : '316' ,
detail : { fields : [ 'CardCode' , 'Comments' ], collections : { DocumentLines : 2 } },
});
expect ( JSON . stringify ( record . mock . calls )). not . toContain ( 'privado' );
});
it ( 'modificar queda registrado con su clave y los campos tocados' , async () => {
request . mockResolvedValueOnce ({ data : undefined });
await service . update ( user , 'BusinessPartners' , 'C100' , { Phone1 : '123' }, {}, '10.0.0.5' );
expect ( record ). toHaveBeenCalledWith ({ action : 'sl_update' , webUser : 'ana' , ip : '10.0.0.5' , resource : 'BusinessPartners' , key : 'C100' , detail : { fields : [ 'Phone1' ] } });
});
it ( 'si SAP rechaza la operación queda registrada como error, con el mensaje, y el error sigue su camino' , async () => {
request . mockRejectedValueOnce ( new Error ( '1250000110 - RUT is missing' ));
await expect ( service . create ( user , 'BusinessPartners' , { CardCode : 'C9' }, {}, '10.0.0.5' )). rejects . toThrow ( 'RUT is missing' );
expect ( record ). toHaveBeenCalledWith ({
action : 'sl_create' ,
webUser : 'ana' ,
ip : '10.0.0.5' ,
resource : 'BusinessPartners' ,
outcome : 'error' ,
detail : { fields : [ 'CardCode' ], error : '1250000110 - RUT is missing' },
});
request . mockRejectedValueOnce ( new Error ( '412' ));
await expect ( service . update ( user , 'Orders' , '5' , { Comments : 'x' }, {}, '10.0.0.5' )). rejects . toThrow ( '412' );
expect ( record ). toHaveBeenLastCalledWith ( expect . objectContaining ({ action : 'sl_update' , key : '5' , outcome : 'error' }));
});
it ( 'un usuario sin permiso de escritura queda registrado como denegado y no llega a SAP' , async () => {
paths = [ '/ventas/ordenes' ];
await expect ( service . create ( user , 'Invoices' , { CardCode : 'C1' }, {}, '10.0.0.5' )). rejects . toBeInstanceOf ( ForbiddenException );
expect ( request ). not . toHaveBeenCalled ();
expect ( record ). toHaveBeenCalledWith ({ action : 'sl_denied' , webUser : 'ana' , ip : '10.0.0.5' , resource : 'Invoices' , outcome : 'denied' , detail : { method : 'POST' } });
});
it ( 'las acciones de solo consulta (series de numeración) no se auditan' , async () => {
request . mockResolvedValueOnce ({ data : { value : [] } });
await service . create ( user , 'SeriesService_GetDocumentSeries' , { DocumentTypeParams : { Document : '17' } }, {}, '10.0.0.5' );
expect ( record ). not . toHaveBeenCalled ();
});
it ( 'las lecturas no se auditan' , async () => {
request . mockResolvedValueOnce ({ data : { value : [] } });
await service . list ( user , 'Orders' , {}, {});
request . mockResolvedValueOnce ({ data : {} });
await service . get ( user , 'Orders' , '5' , {});
expect ( record ). not . toHaveBeenCalled ();
});
2026-10-16 13:23:27 -05:00
describe ( 'acciones sobre un registro (anular un pago)' , () => {
it ( 'anular un pago llama a Entidad(clave)/Cancel y queda registrado' , async () => {
request . mockResolvedValueOnce ({ data : undefined });
await service . action ( user , 'IncomingPayments' , '512' , 'Cancel' , '10.0.0.5' );
expect ( request ). toHaveBeenCalledWith ( expect . objectContaining ({ method : 'POST' , path : 'IncomingPayments(512)/Cancel' }));
expect ( record ). toHaveBeenCalledWith ({ action : 'sl_action' , webUser : 'ana' , ip : '10.0.0.5' , resource : 'IncomingPayments' , key : '512' , detail : { action : 'Cancel' } });
});
it ( 'una acción que no está en la lista blanca del recurso no llega a SAP' , async () => {
await expect ( service . action ( user , 'IncomingPayments' , '512' , 'Delete' , '10.0.0.5' )). rejects . toThrow ( /no disponible/i );
await expect ( service . action ( user , 'Orders' , '5' , 'Cancel' , '10.0.0.5' )). rejects . toThrow ( /no disponible/i );
expect ( request ). not . toHaveBeenCalled ();
});
it ( 'sin permiso de escritura queda denegado y no llega a SAP' , async () => {
paths = [ '/bancos/pagos-efectuados' ];
await expect ( service . action ( user , 'IncomingPayments' , '512' , 'Cancel' , '10.0.0.5' )). rejects . toBeInstanceOf ( ForbiddenException );
expect ( request ). not . toHaveBeenCalled ();
expect ( record ). toHaveBeenCalledWith ( expect . objectContaining ({ action : 'sl_denied' , resource : 'IncomingPayments' , outcome : 'denied' }));
});
it ( 'si SAP rechaza la anulación queda registrada como error y el error sigue su camino' , async () => {
request . mockRejectedValueOnce ( new Error ( 'ya está anulado' ));
await expect ( service . action ( user , 'VendorPayments' , '9' , 'Cancel' , '10.0.0.5' )). rejects . toThrow ( 'ya está anulado' );
expect ( record ). toHaveBeenCalledWith ( expect . objectContaining ({ action : 'sl_action' , resource : 'VendorPayments' , key : '9' , outcome : 'error' , detail : { action : 'Cancel' , error : 'ya está anulado' } }));
});
it ( 'la clave del registro se valida (solo números en los pagos)' , async () => {
await expect ( service . action ( user , 'IncomingPayments' , "1);DROP" , 'Cancel' , '10.0.0.5' )). rejects . toThrow ();
expect ( request ). not . toHaveBeenCalled ();
});
});
2026-10-16 12:35:46 -05:00
});
2026-10-19 12:01:26 -05:00
describe ( 'SlProxyService: modo solo lectura' , () => {
const make = ( readOnly : boolean ) => {
const request = vi . fn ( async () => ({ data : { value : [] } }));
const record = vi . fn ( async () => undefined );
const service = new SlProxyService (
{ request } as unknown as SapClientService ,
{ getPaths : async () => [ '*' ] } as unknown as PermissionsService ,
{ record } as unknown as AuditService ,
{ get : ( k : string ) => ( k === 'SAPB1_SL_READ_ONLY' ? readOnly : undefined ) } as unknown as AppConfigService ,
);
return { service , request , record };
};
it ( 'rechaza crear, modificar y las acciones sin llegar a SAP, y lo audita' , async () => {
const { service , request , record } = make ( true );
await expect ( service . create ( user , 'Orders' , { CardCode : 'C1' }, {}, '10.0.0.5' )). rejects . toThrow ( 'solo lectura' );
await expect ( service . update ( user , 'Orders' , '1' , { Comments : 'x' }, {}, '10.0.0.5' )). rejects . toThrow ( 'solo lectura' );
await expect ( service . action ( user , 'IncomingPayments' , '1' , 'Cancel' , '10.0.0.5' )). rejects . toThrow ( 'solo lectura' );
expect ( request ). not . toHaveBeenCalled ();
expect ( record ). toHaveBeenCalledWith ( expect . objectContaining ({ action : 'sl_denied' , outcome : 'denied' }));
});
it ( 'las consultas y las acciones de solo consulta (series) siguen funcionando' , async () => {
const { service , request } = make ( true );
await service . list ( user , 'Orders' , {}, {});
await service . create ( user , 'SeriesService_GetDocumentSeries' , { DocumentTypeParams : { Document : '30' } }, {}, '10.0.0.5' );
expect ( request ). toHaveBeenCalledTimes ( 2 );
});
it ( 'sin el modo solo lectura todo sigue igual' , async () => {
const { service , request } = make ( false );
await service . create ( user , 'Orders' , { CardCode : 'C1' }, {}, '10.0.0.5' );
expect ( request ). toHaveBeenCalledTimes ( 1 );
});
});