Aplicación web de Devolución a Proveedores para SAP Business One
Reemplaza al addon .NET Framework (SolucionLibros). Genera Devoluciones de mercancías enlazadas a sus Entradas por compra a partir de un listado pegado desde Excel. - libs/shared (@dp/shared): modelos y lógica de dominio pura (FIFO, consolidación por proveedor, parseo del portapapeles), portada del addon. - apps/api (NestJS): cliente del Service Layer con cola por sesión, login con la sesión de SAP, metadatos (UDT/UDF/UDO) y botón Reconstruir, catálogos, planificaciones (pegar, procesar, reprocesar, marcar líneas, crear Devoluciones, cerrar), listado paginado con filtros y descarga de PDF de la Guía de despacho desde BES. Sirve también la web compilada, con HTTPS opcional. - apps/web (Angular): login, Configuración → Metadatos y Planificaciones (listado, nueva, detalle). - deploy/windows y scripts/publicar.mjs: despliegue como servicio de Windows con WinSW. Verificado contra el Service Layer de SQL Server. Sin probar aún: SAP HANA y la instalación del servicio en Windows (ver CLAUDE.md, sección "Sin verificar"). Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
287b187c14
126 files changed
+20033
No files matched your search
@@ -0,0 +1,43 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { INestApplication } from '@nestjs/common';
|
||||
import cookieParser from 'cookie-parser';
|
||||
import request from 'supertest';
|
||||
import { App } from 'supertest/types.js';
|
||||
import { AppModule } from './../src/app.module.js';
|
||||
|
||||
describe('API (e2e)', () => {
|
||||
let app: INestApplication<App>;
|
||||
|
||||
beforeAll(() => {
|
||||
process.env.SL_URL = 'https://localhost:50000/b1s/v1';
|
||||
process.env.SL_COMPANY_DB = 'TEST';
|
||||
process.env.DB_TYPE = 'MSSQL';
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
const moduleFixture: TestingModule = await Test.createTestingModule({
|
||||
imports: [AppModule],
|
||||
}).compile();
|
||||
|
||||
app = moduleFixture.createNestApplication();
|
||||
app.setGlobalPrefix('api');
|
||||
app.use(cookieParser());
|
||||
await app.init();
|
||||
});
|
||||
|
||||
it('/api/health es público', () => {
|
||||
return request(app.getHttpServer()).get('/api/health').expect(200).expect({ status: 'ok' });
|
||||
});
|
||||
|
||||
it('/api/auth/info devuelve la sociedad configurada', () => {
|
||||
return request(app.getHttpServer()).get('/api/auth/info').expect(200).expect({ companyDb: 'TEST', dbType: 'MSSQL' });
|
||||
});
|
||||
|
||||
it('/api/metadata/status exige sesión', () => {
|
||||
return request(app.getHttpServer()).get('/api/metadata/status').expect(401);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await app.close();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,76 @@
|
||||
import { INestApplication } from '@nestjs/common';
|
||||
import { Test } from '@nestjs/testing';
|
||||
import cookieParser from 'cookie-parser';
|
||||
import request from 'supertest';
|
||||
import { App } from 'supertest/types.js';
|
||||
import { AppModule } from './../src/app.module.js';
|
||||
import { ServiceLayerClient } from './../src/service-layer/service-layer.client.js';
|
||||
import { SessionStore } from './../src/service-layer/session.store.js';
|
||||
|
||||
// El Service Layer se reemplaza por uno falso que acepta cualquier login (y registra la sesión en el
|
||||
// almacén real): lo que se comprueba es la cookie de sesión que la API entrega al navegador, no SAP.
|
||||
|
||||
describe('Cookie de sesión (e2e)', () => {
|
||||
let app: INestApplication<App>;
|
||||
|
||||
async function crear(cookieSecure: boolean) {
|
||||
process.env.SL_URL = 'https://localhost:50000/b1s/v1';
|
||||
process.env.SL_COMPANY_DB = 'TEST';
|
||||
process.env.DB_TYPE = 'MSSQL';
|
||||
process.env.COOKIE_SECURE = String(cookieSecure);
|
||||
|
||||
const modulo = await Test.createTestingModule({ imports: [AppModule] })
|
||||
.overrideProvider(ServiceLayerClient)
|
||||
.useFactory({
|
||||
inject: [SessionStore],
|
||||
factory: (sesiones: SessionStore) => ({
|
||||
login: async () => sesiones.crear({ b1Session: 'b', userName: 'u', companyDb: 'X', version: '1', timeoutMinutes: 30 }),
|
||||
logout: async () => undefined,
|
||||
}),
|
||||
})
|
||||
.compile();
|
||||
|
||||
app = modulo.createNestApplication();
|
||||
app.setGlobalPrefix('api');
|
||||
app.use(cookieParser());
|
||||
await app.init();
|
||||
}
|
||||
|
||||
const loguear = async () => {
|
||||
const r = await request(app.getHttpServer()).post('/api/auth/login').send({ userName: 'u', password: 'p' }).expect(200);
|
||||
return (r.headers['set-cookie'] as unknown as string[]).find((c) => c.startsWith('dp_sid=')) ?? '';
|
||||
};
|
||||
|
||||
afterEach(async () => {
|
||||
await app.close();
|
||||
delete process.env.COOKIE_SECURE;
|
||||
});
|
||||
|
||||
it('con COOKIE_SECURE=true la cookie es HttpOnly, Secure, SameSite=Strict y de toda la aplicación', async () => {
|
||||
await crear(true);
|
||||
const cookie = await loguear();
|
||||
|
||||
expect(cookie).toMatch(/^dp_sid=[0-9a-f]{64};/);
|
||||
expect(cookie).toContain('HttpOnly');
|
||||
expect(cookie).toContain('Secure');
|
||||
expect(cookie).toContain('SameSite=Strict');
|
||||
expect(cookie).toContain('Path=/');
|
||||
});
|
||||
|
||||
it('con COOKIE_SECURE=false (desarrollo por HTTP) no lleva Secure, pero sigue HttpOnly y Strict', async () => {
|
||||
await crear(false);
|
||||
const cookie = await loguear();
|
||||
|
||||
expect(cookie).toContain('HttpOnly');
|
||||
expect(cookie).toContain('SameSite=Strict');
|
||||
expect(cookie).not.toContain('Secure');
|
||||
});
|
||||
|
||||
it('con esa cookie el navegador queda autenticado; sin ella, no', async () => {
|
||||
await crear(true);
|
||||
const cookie = await loguear();
|
||||
|
||||
await request(app.getHttpServer()).get('/api/auth/me').expect(401);
|
||||
await request(app.getHttpServer()).get('/api/auth/me').set('Cookie', cookie.split(';')[0]).expect(200);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user