Files
basic-erp/apps/api/test/sl-proxy.e2e-spec.ts
T

151 lines
6.4 KiB
TypeScript
Raw Normal View History

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);
});
});
});