import type { INestApplication } from '@nestjs/common'; import request from 'supertest'; import { createTestApp, employee, sapCalls } from './support/create-test-app.js'; describe('Proxy del Service Layer (e2e)', () => { let app: INestApplication; let admin: { Authorization: string }; let luis: { Authorization: string }; const http = () => request(app.getHttpServer()); const loginAs = async (username: string) => { const res = await http().post('/api/auth/login').send({ username, password: 'secreto' }).expect(200); return { Authorization: `Bearer ${res.body.token}` }; }; beforeEach(async () => { app = await createTestApp([employee('ana', 1), employee('luis', 2)], 'ana', (req) => req.method === 'GET' ? { value: [{ ItemCode: 'A' }], 'odata.count': '1' } : { DocEntry: 77, DocNum: 1001 }, ); admin = await loginAs('ana'); luis = await loginAs('luis'); }); afterEach(async () => { await app.close(); }); describe('listar', () => { it('arma la consulta OData con total y tamaño de página', async () => { const res = await http() .get('/api/sl/Items') .query({ $select: 'ItemCode, ItemName', $filter: "contains(ItemName,'TUBO')", $orderby: 'ItemCode', $top: 20, $skip: 40 }) .set(luis) .expect(200); expect(res.body).toEqual({ value: [{ ItemCode: 'A' }], 'odata.count': '1' }); expect(sapCalls.at(-1)).toMatchObject({ method: 'GET', path: 'Items', query: { $select: 'ItemCode,ItemName', $filter: "contains(ItemName,'TUBO')", $orderby: 'ItemCode', $top: 20, $skip: 40, $inlinecount: 'allpages', }, headers: { 'B1S-PageSize': '20', 'B1S-CaseInsensitive': 'true' }, }); }); it('sin $top usa 50 y sin $filter no fuerza mayúsculas/minúsculas', async () => { await http().get('/api/sl/Items').set(luis).expect(200); const call = sapCalls.at(-1)!; expect(call.query).toMatchObject({ $top: 50 }); expect(call.headers).toEqual({ 'B1S-PageSize': '50' }); }); it('rechaza parámetros no permitidos y $expand no declarado', async () => { await http().get('/api/sl/Items').query({ $apply: 'x' }).set(luis).expect(400); await http().get('/api/sl/Items').query({ $top: 100000 }).set(luis).expect(400); await http().get('/api/sl/Items').query({ $expand: 'ItemPrices' }).set(luis).expect(400); expect(sapCalls).toHaveLength(0); }); }); describe('por clave', () => { it('clave numérica y de texto', async () => { await http().get('/api/sl/Orders/15').set(admin).expect(200); expect(sapCalls.at(-1)!.path).toBe('Orders(15)'); await http().get("/api/sl/Items/O'NEIL").set(luis).expect(200); expect(sapCalls.at(-1)!.path).toBe("Items('O''NEIL')"); }); it('no deja colar rutas ni acciones en la clave', async () => { await http().get('/api/sl/Orders/1)%2FCancel').set(admin).expect(400); await http().get('/api/sl/Orders/..%2FLogout').set(admin).expect(400); expect(sapCalls).toHaveLength(0); }); }); describe('recursos', () => { it('lo no registrado es 404 (EmployeesInfo incluido)', async () => { await http().get('/api/sl/EmployeesInfo').set(admin).expect(404); await http().get('/api/sl/Logout').set(admin).expect(404); await http().get('/api/sl/items').set(admin).expect(404); expect(sapCalls).toHaveLength(0); }); it('sin sesión es 401', async () => { await http().get('/api/sl/Items').expect(401); }); it('método no permitido: los catálogos de solo lectura no aceptan POST', async () => { await http().post('/api/sl/Warehouses').set(admin).send({ WarehouseCode: 'X' }).expect(405); await http().delete('/api/sl/Orders/1').set(admin).expect(404); // no hay ruta DELETE }); }); describe('permisos de menú', () => { it('un documento exige la ruta de menú; el catálogo de consulta no', async () => { await http().get('/api/sl/Orders').set(luis).expect(403); await http().get('/api/sl/Warehouses').set(luis).expect(200); await http().put('/api/permissions/luis').set(admin).send({ paths: ['/ventas/*'] }).expect(200); await http().get('/api/sl/Orders').set(luis).expect(200); await http().get('/api/sl/PurchaseOrders').set(luis).expect(403); }); it('escribir en un maestro exige el módulo correspondiente', async () => { await http().put('/api/permissions/luis').set(admin).send({ paths: ['/socios/proveedores'] }).expect(200); await http().post('/api/sl/BusinessPartners').set(luis).send({ CardCode: 'P1' }).expect(201); await http().post('/api/sl/Items').set(luis).send({ ItemCode: 'X' }).expect(403); }); }); describe('crear y modificar', () => { it('POST devuelve lo que responde SAP y reenvía el cuerpo', async () => { const body = { CardCode: 'C1', DocumentLines: [{ ItemCode: 'A', Quantity: 2 }] }; const res = await http().post('/api/sl/Orders').set(admin).send(body).expect(201); expect(res.body).toEqual({ DocEntry: 77, DocNum: 1001 }); expect(sapCalls.at(-1)).toMatchObject({ method: 'POST', path: 'Orders', body }); }); it('PATCH responde 204 y reenvía B1S-ReplaceCollectionsOnPatch decidido por el frontend', async () => { await http() .patch('/api/sl/Orders/15') .set(admin) .set('B1S-ReplaceCollectionsOnPatch', 'true') .send({ Comments: 'x' }) .expect(204); expect(sapCalls.at(-1)).toMatchObject({ method: 'PATCH', path: 'Orders(15)', body: { Comments: 'x' }, headers: { 'B1S-ReplaceCollectionsOnPatch': 'true' }, }); }); it('rechaza cuerpos que no son objetos y cabeceras B1S-* no permitidas', async () => { await http().post('/api/sl/Orders').set(admin).send([1, 2]).expect(400); await http().post('/api/sl/Orders').set(admin).set('B1S-Atomic', 'x').send({}).expect(400); await http().patch('/api/sl/Orders/1').set(admin).set('B1S-PageSize', '5').send({}).expect(400); expect(sapCalls).toHaveLength(0); }); it('un cuerpo grande (documento con muchas líneas) no se corta por tamaño', async () => { const lines = Array.from({ length: 800 }, (_, i) => ({ ItemCode: `ITEM-${i}`, Quantity: 1, ItemDescription: 'x'.repeat(100) })); await http().post('/api/sl/Orders').set(admin).send({ DocumentLines: lines }).expect(201); }); }); });