feat(api): capa SAP con sesión compartida, mutex de login y cola de peticiones
- SapSessionService: una sesión para todos; un solo login aunque lleguen peticiones concurrentes o 401 simultáneos (loginInFlight); logout al apagar. - SapClientService: proxy con reintento único ante 401, errores SAP traducidos (SapHttpException) y fallos de red/TLS mapeados a 502/504. Un login rechazado se informa como 502, nunca 401 (el frontend lo toma como sesión web expirada). - Confianza TLS: SAPB1_SL_CA_FILE opcional (autofirmado de SAP o CA de firewall) sumado a las CA del sistema; VERIFY_SSL=true por defecto y no se puede apagar en producción. - SAPB1_SL_MAX_CONCURRENCY (default 1): medido en SAP10, más de ~2 peticiones en paralelo retrasan ~7 s cada una extra; en serie ~60 ms. - GET /api/health/sap, tests unitarios (MockAgent) y e2e opcional contra SAP real (SAP_LIVE=1). apps/api/certs documentado; *.pem/*.cer ignorados. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
445efc998d
commit
bb4acb5510
19 files changed
+629
-22
No files matched your search
@@ -1,20 +1,53 @@
|
||||
import { validateEnv } from './env.schema.js';
|
||||
|
||||
const sap = {
|
||||
SAPB1_SL_URL: 'https://sap.local:50000/b1s/v1/',
|
||||
SAPB1_SL_COMPANYDB: 'DB',
|
||||
SAPB1_SL_USER: 'u',
|
||||
SAPB1_SL_PASSWORD: 'p',
|
||||
};
|
||||
|
||||
describe('validateEnv', () => {
|
||||
it('aplica valores por defecto', () => {
|
||||
const env = validateEnv({});
|
||||
const env = validateEnv(sap);
|
||||
expect(env.PORT).toBe(3000);
|
||||
expect(env.CORS_ORIGINS).toEqual([]);
|
||||
expect(env.LOG_LEVEL).toBe('log');
|
||||
expect(env.SAPB1_SL_VERIFY_SSL).toBe(true);
|
||||
expect(env.SAPB1_SL_CA_FILE).toBeUndefined();
|
||||
expect(env.SAPB1_SL_MAX_CONCURRENCY).toBe(1);
|
||||
});
|
||||
|
||||
it('quita la barra final de la URL de SAP', () => {
|
||||
expect(validateEnv(sap).SAPB1_SL_URL).toBe('https://sap.local:50000/b1s/v1');
|
||||
});
|
||||
|
||||
it('convierte PORT y separa CORS_ORIGINS', () => {
|
||||
const env = validateEnv({ PORT: '8080', CORS_ORIGINS: 'https://a.local, https://b.local' });
|
||||
const env = validateEnv({ ...sap, PORT: '8080', CORS_ORIGINS: 'https://a.local, https://b.local' });
|
||||
expect(env.PORT).toBe(8080);
|
||||
expect(env.CORS_ORIGINS).toEqual(['https://a.local', 'https://b.local']);
|
||||
});
|
||||
|
||||
it('rechaza valores inválidos con un mensaje claro', () => {
|
||||
expect(() => validateEnv({ PORT: 'abc' })).toThrow(/PORT/);
|
||||
expect(() => validateEnv({ ...sap, PORT: 'abc' })).toThrow(/PORT/);
|
||||
});
|
||||
|
||||
it('exige las credenciales de SAP', () => {
|
||||
expect(() => validateEnv({})).toThrow(/SAPB1_SL_URL/);
|
||||
});
|
||||
|
||||
it('trata SAPB1_SL_CA_FILE vacío como no configurado', () => {
|
||||
expect(validateEnv({ ...sap, SAPB1_SL_CA_FILE: '' }).SAPB1_SL_CA_FILE).toBeUndefined();
|
||||
});
|
||||
|
||||
it('rechaza un SAPB1_SL_CA_FILE que no existe', () => {
|
||||
expect(() => validateEnv({ ...sap, SAPB1_SL_CA_FILE: './no-existe.pem' })).toThrow(/CA_FILE/);
|
||||
});
|
||||
|
||||
it('no permite desactivar la validación SSL en producción', () => {
|
||||
expect(() =>
|
||||
validateEnv({ ...sap, NODE_ENV: 'production', SAPB1_SL_VERIFY_SSL: 'false' }),
|
||||
).toThrow(/producción/);
|
||||
expect(validateEnv({ ...sap, SAPB1_SL_VERIFY_SSL: 'false' }).SAPB1_SL_VERIFY_SSL).toBe(false);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user