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; let call: ReturnType; let record: ReturnType; 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' })); }); });