Files
basic-erp/apps/api/src/gateway/gateway.service.spec.ts
T

127 lines
6.7 KiB
TypeScript

import { BadRequestException, ForbiddenException, NotFoundException } from '@nestjs/common';
import { type GatewayParam } from '@basic-erp/sap-types';
import { vi } from 'vitest';
import type { AuditService } from '../audit/audit.service.js';
import type { AuthUser } from '../auth/auth.types.js';
import type { PermissionsService } from '../permissions/permissions.service.js';
import type { GatewayClientService } from './gateway-client.service.js';
import type { GatewayReportEntity } from './gateway-report.entity.js';
import { GatewayService, decodePdf, gatewayMessage } from './gateway.service.js';
const user: AuthUser = { webUser: 'ana', empId: 7, firstName: 'Ana', lastName: 'P' };
const PDF = Buffer.from('%PDF-1.4 contenido');
const params: GatewayParam[] = [
{ name: 'Dockey@', type: 'xsd:decimal', label: 'Documento', kind: 'lookup', required: true, lookup: 'doc:DeliveryNotes' },
{ name: 'CANTIDAD', type: 'xsd:decimal', label: 'Cantidad', kind: 'number', required: false },
];
describe('decodePdf', () => {
it('acepta el base64 tal cual y entre comillas', () => {
expect(decodePdf(PDF.toString('base64')).equals(PDF)).toBe(true);
expect(decodePdf(JSON.stringify(PDF.toString('base64'))).equals(PDF)).toBe(true);
});
});
describe('gatewayMessage', () => {
it('muestra el texto de error del gateway o el «(---)» de un reporte sin resultado', () => {
expect(gatewayMessage('Error code name: Unparseable date: "31/01/2026"')).toContain('Unparseable date');
expect(gatewayMessage('(---)')).toBe('(---)');
expect(gatewayMessage(Buffer.from('mensaje en base64').toString('base64'))).toBe('mensaje en base64');
});
});
describe('GatewayService', () => {
let rows: Map<string, GatewayReportEntity>;
let call: ReturnType<typeof vi.fn>;
let record: ReturnType<typeof vi.fn>;
let paths: string[];
let service: GatewayService;
beforeEach(() => {
rows = new Map();
call = vi.fn(async (_m: string, path: string) => {
if (path.endsWith('LoadAuthorizedCRList')) return { status: 200, text: JSON.stringify({ resultSet: [{ code: 'DLN20015', name: 'Etiquetas', root_name: '#1#4', root_guid: '44' }] }) };
if (path.endsWith('LoadCR')) return { status: 200, text: JSON.stringify({ error: false, resultSet: [{ name: 'Dockey@', type: 'xsd:decimal', description: 'Documento', isOptionalPrompt: 'false' }] }) };
return { status: 200, text: PDF.toString('base64') };
});
record = vi.fn(async () => undefined);
paths = ['*'];
const repo = {
find: async (opts?: { where?: { enabled?: boolean } }) => [...rows.values()].filter((r) => opts?.where?.enabled === undefined || r.enabled === opts.where.enabled),
findOneBy: async ({ code }: { code: string }) => rows.get(code) ?? null,
save: async (e: GatewayReportEntity) => void rows.set(e.code, e),
remove: async (e: GatewayReportEntity) => void rows.delete(e.code),
};
service = new GatewayService(
{ call } as unknown as GatewayClientService,
repo as never,
{ getPaths: async () => paths } as unknown as PermissionsService,
{ record } as unknown as AuditService,
);
});
const register = () => service.save('DLN20015', { title: 'Etiquetas', category: 'inventario', enabled: true, params }, user, '10.0.0.5');
it('lista lo que el gateway autoriza y arma los parámetros de un reporte desde LoadCR', async () => {
expect((await service.authorized()).map((r) => r.code)).toEqual(['DLN20015']);
expect(await service.parameters('DLN20015')).toEqual([{ name: 'Dockey@', type: 'xsd:decimal', label: 'Documento', kind: 'number', required: true }]);
});
it('solo se matricula un reporte que el gateway autoriza, y queda registrado en la auditoría', async () => {
await expect(service.save('NOEXISTE', { title: 'x', category: 'otros', enabled: true, params: [] }, user, '10.0.0.5')).rejects.toThrow(BadRequestException);
await register();
expect((await service.registry())[0]).toMatchObject({ code: 'DLN20015', category: 'inventario', params });
expect(record).toHaveBeenCalledWith(expect.objectContaining({ action: 'report_register', resource: 'DLN20015' }));
// volver a guardar uno ya matriculado no consulta al gateway
call.mockClear();
await register();
expect(call).not.toHaveBeenCalled();
});
it('desmatricular quita el reporte y avisa si no estaba', async () => {
await register();
await service.remove('DLN20015', user, '10.0.0.5');
expect(await service.registry()).toEqual([]);
await expect(service.remove('DLN20015', user, '10.0.0.5')).rejects.toThrow(NotFoundException);
});
it('ejecutar exige el permiso de menú y que el reporte esté matriculado y habilitado', async () => {
await register();
paths = ['/ventas/*'];
await expect(service.export(user, 'DLN20015', { 'Dockey@': '1' }, '10.0.0.5')).rejects.toThrow(ForbiddenException);
paths = ['/reportes/ejecutar'];
await expect(service.export(user, 'OTRO', {}, '10.0.0.5')).rejects.toThrow(NotFoundException);
await service.save('DLN20015', { title: 'Etiquetas', category: 'inventario', enabled: false, params }, user, '10.0.0.5');
await expect(service.export(user, 'DLN20015', { 'Dockey@': '1' }, '10.0.0.5')).rejects.toThrow(NotFoundException);
});
it('genera el PDF con el cuerpo que arman los parámetros matriculados y audita solo sus nombres', async () => {
await register();
const pdf = await service.export(user, 'DLN20015', { 'Dockey@': '137138', CANTIDAD: '5', Intruso: 'x' }, '10.0.0.5');
expect(pdf.equals(PDF)).toBe(true);
expect(call).toHaveBeenLastCalledWith('POST', 'rs/v1/ExportPDFData', {
query: { DocCode: 'DLN20015' },
body: [
{ name: 'Dockey@', type: 'xsd:decimal', value: [['137138']] },
{ name: 'CANTIDAD', type: 'xsd:decimal', value: [['5']] },
],
});
expect(record).toHaveBeenLastCalledWith(expect.objectContaining({ action: 'report_export', detail: { params: ['Dockey@', 'CANTIDAD'] } }));
expect(JSON.stringify(record.mock.calls)).not.toContain('137138');
});
it('un parámetro obligatorio sin valor no llega al gateway', async () => {
await register();
call.mockClear();
await expect(service.export(user, 'DLN20015', { CANTIDAD: '5' }, '10.0.0.5')).rejects.toThrow(/Documento/);
expect(call).not.toHaveBeenCalled();
});
it('si el gateway no devuelve un PDF lo rechaza y lo deja en la auditoría', async () => {
await register();
call.mockResolvedValueOnce({ status: 200, text: Buffer.from('no es pdf').toString('base64') });
await expect(service.export(user, 'DLN20015', { 'Dockey@': '1' }, '10.0.0.5')).rejects.toThrow(/no devolvió un PDF: no es pdf/);
expect(record).toHaveBeenLastCalledWith(expect.objectContaining({ action: 'report_export', outcome: 'error' }));
});
});