@pxp-nd/core 1.2.91 → 1.2.92
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -0
- package/dist/interfaces.d.ts +13 -0
- package/dist/lib/pxp/Controller.d.ts +1 -0
- package/dist/lib/pxp/Controller.js +4 -3
- package/dist/lib/pxp/utils/Security.d.ts +5 -2
- package/dist/lib/pxp/utils/Security.js +85 -17
- package/package.json +1 -1
- package/test/stdout-log.test.ts +242 -0
package/README.md
CHANGED
|
@@ -1,6 +1,24 @@
|
|
|
1
1
|
# PXP-ND core
|
|
2
2
|
- Core module for pxp-nd
|
|
3
3
|
|
|
4
|
+
## 1.2.92
|
|
5
|
+
|
|
6
|
+
Destino del audit de request. **Sin configurar nada, el comportamiento es idéntico a 1.2.91**: el default sigue siendo insertar en `tsec_log`.
|
|
7
|
+
|
|
8
|
+
### `logSink` (opcional)
|
|
9
|
+
|
|
10
|
+
```ts
|
|
11
|
+
new PxpApp({
|
|
12
|
+
logSink: 'stdout', // 'db' | 'stdout' | 'both'
|
|
13
|
+
});
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
- `db` (default): insert en `tsec_log` por `DB_LOG_CONNECTION_NAME`. El `logId` del JSON de error es el id numérico.
|
|
17
|
+
- `stdout`: una línea JSON por request (`pxpLog: true`). El `logId` es un uuid, generado antes de escribir, y no se toca la base. `request`, `response` y `query` se cortan a 8 KB (`truncated: true`). CloudWatch descarta un evento de más de 256 KB.
|
|
18
|
+
- `both`: la línea JSON y, en segundo plano, el insert. El uuid viaja dentro del request guardado como `__logId`. La respuesta no espera a la base.
|
|
19
|
+
|
|
20
|
+
Las máscaras de `@Log` (`cc_mask`, `complete_mask`, campo en null) se aplican en los tres modos. Los decoradores existentes no hay que tocarlos.
|
|
21
|
+
|
|
4
22
|
## 1.2.91
|
|
5
23
|
|
|
6
24
|
Correcciones encontradas en las pruebas de carga con la base de datos caída. Sin cambios de API ni de configuración.
|
package/dist/interfaces.d.ts
CHANGED
|
@@ -32,10 +32,23 @@ declare interface IModules {
|
|
|
32
32
|
declare interface IConfigExpress {
|
|
33
33
|
[name: string]: any;
|
|
34
34
|
}
|
|
35
|
+
/**
|
|
36
|
+
* Where a request audit line is written.
|
|
37
|
+
* `db` (default) inserts into tsec_log and returns the numeric id.
|
|
38
|
+
* `stdout` writes one JSON line and returns a uuid. No database.
|
|
39
|
+
* `both` writes the line and inserts in the background; the uuid is stored
|
|
40
|
+
* inside the row's request JSON as `__logId`.
|
|
41
|
+
*/
|
|
42
|
+
export declare type LogSink = 'db' | 'stdout' | 'both';
|
|
35
43
|
export interface IConfigPxpApp {
|
|
36
44
|
defaultDbSettings?: string;
|
|
37
45
|
apiPrefix?: string;
|
|
38
46
|
logDuration?: boolean;
|
|
47
|
+
/**
|
|
48
|
+
* Audit destination. Omitted or `db` matches <=1.2.91 (insert into tsec_log).
|
|
49
|
+
* `stdout` is one JSON line per request. `both` is the line plus a background insert.
|
|
50
|
+
*/
|
|
51
|
+
logSink?: LogSink;
|
|
39
52
|
showRoutes?: boolean;
|
|
40
53
|
controllers?: any[];
|
|
41
54
|
entities?: any;
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
* Last modified : 2026-10-08 - Favio Figueroa - EF-23 per-request controller clone (from 1.2.89), startTransaction inside try
|
|
16
16
|
* Last modified : 2026-10-08 - Favio Figueroa - EF-23 acquire timeout + @Timeout decorator
|
|
17
17
|
* Last modified : 2026-10-08 - Favio Figueroa - EF-23 log failures never block or crash the response (1.2.91)
|
|
18
|
+
* Last modified : 2026-10-21 - logSink db | stdout | both (1.2.92)
|
|
18
19
|
*/
|
|
19
20
|
import { EntityManager } from 'typeorm';
|
|
20
21
|
import { Request, Response, NextFunction } from 'express';
|
|
@@ -40,6 +40,7 @@ exports.Controller = void 0;
|
|
|
40
40
|
* Last modified : 2026-10-08 - Favio Figueroa - EF-23 per-request controller clone (from 1.2.89), startTransaction inside try
|
|
41
41
|
* Last modified : 2026-10-08 - Favio Figueroa - EF-23 acquire timeout + @Timeout decorator
|
|
42
42
|
* Last modified : 2026-10-08 - Favio Figueroa - EF-23 log failures never block or crash the response (1.2.91)
|
|
43
|
+
* Last modified : 2026-10-21 - logSink db | stdout | both (1.2.92)
|
|
43
44
|
*/
|
|
44
45
|
const typeorm_1 = require("typeorm");
|
|
45
46
|
const class_validator_1 = require("class-validator");
|
|
@@ -192,7 +193,7 @@ class Controller {
|
|
|
192
193
|
// A failing error log (e.g. database down) must never prevent the response.
|
|
193
194
|
try {
|
|
194
195
|
res.logId = await index_1.__(index_1.insertLog('nouser', 'mac', req.ip, 'error', ex.tecMessage, controllerForRequest.module, controllerForRequest.transactionCode, '', // query
|
|
195
|
-
params, ex.stack, ex.statusCode, endsAt, logValue));
|
|
196
|
+
params, ex.stack, ex.statusCode, endsAt, logValue, controllerForRequest.config.logSink));
|
|
196
197
|
}
|
|
197
198
|
catch (logErr) {
|
|
198
199
|
// __ wraps driver errors in PxpError(500) with a generic message; the real cause is tecMessage.
|
|
@@ -233,7 +234,7 @@ class Controller {
|
|
|
233
234
|
// A failing error log (e.g. database down) must never prevent the response.
|
|
234
235
|
try {
|
|
235
236
|
res.logId = (await index_1.__(index_1.insertLog(controllerForRequest.user && controllerForRequest.user.username ? controllerForRequest.user.username : 'nouser', 'mac', req.ip, 'error', ex.tecMessage, controllerForRequest.module, controllerForRequest.transactionCode, '', // query
|
|
236
|
-
params, ex.stack, ex.statusCode, endsAt, logValue)));
|
|
237
|
+
params, ex.stack, ex.statusCode, endsAt, logValue, controllerForRequest.config.logSink)));
|
|
237
238
|
}
|
|
238
239
|
catch (logErr) {
|
|
239
240
|
// __ wraps driver errors in PxpError(500) with a generic message; the real cause is tecMessage.
|
|
@@ -329,7 +330,7 @@ class Controller {
|
|
|
329
330
|
const iniAt = req.start;
|
|
330
331
|
const endsAt = now.valueOf() - iniAt.valueOf();
|
|
331
332
|
// Fire-and-forget: without the catch a failing log is an unhandledRejection (fatal on Node >= 15).
|
|
332
|
-
index_1.__(index_1.insertLog(this.user && this.user.username ? this.user.username : 'nouser', 'mac', req.ip, 'success', 'successful transaction', this.module, this.transactionCode, '', params, JSON.stringify(metResponse), '200', endsAt, logConfig)).catch(err => console.error('[pxp-core] success log failed', err && (err.tecMessage || err.message)));
|
|
333
|
+
index_1.__(index_1.insertLog(this.user && this.user.username ? this.user.username : 'nouser', 'mac', req.ip, 'success', 'successful transaction', this.module, this.transactionCode, '', params, JSON.stringify(metResponse), '200', endsAt, logConfig, this.config.logSink)).catch(err => console.error('[pxp-core] success log failed', err && (err.tecMessage || err.message)));
|
|
333
334
|
}
|
|
334
335
|
//PARA REPORTES
|
|
335
336
|
if (req.report && req.report.type === 'pdf') {
|
|
@@ -13,11 +13,14 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import 'reflect-metadata';
|
|
15
15
|
import { EntityManager } from 'typeorm';
|
|
16
|
+
import { LogSink } from '../../../interfaces';
|
|
17
|
+
/** Max characters kept for request, response and query on a stdout line. CloudWatch drops an event past 256 KB. */
|
|
18
|
+
declare const STDOUT_LOG_FIELD_MAX = 8192;
|
|
16
19
|
interface ScriptInterface {
|
|
17
20
|
scriptCode: string;
|
|
18
21
|
scriptFunction: (manager: EntityManager) => void;
|
|
19
22
|
}
|
|
20
23
|
declare const userHasPermission: (user: any, transaction: string, permissionFunction: boolean | ((user: any, transaction: string) => Promise<boolean>)) => Promise<boolean>;
|
|
21
|
-
declare const insertLog: (username: string, macaddres: string, ip: string, logType: string, desc: string, proc: string, trans: string, query: string, req: Record<string, unknown>, resp: string, err: string, timeEx: number, logConfig: Record<string, unknown
|
|
22
|
-
export { userHasPermission, insertLog };
|
|
24
|
+
declare const insertLog: (username: string, macaddres: string, ip: string, logType: string, desc: string, proc: string, trans: string, query: string, req: Record<string, unknown>, resp: string, err: string, timeEx: number, logConfig: Record<string, unknown>, logSink?: LogSink) => Promise<number | string>;
|
|
25
|
+
export { userHasPermission, insertLog, STDOUT_LOG_FIELD_MAX };
|
|
23
26
|
export type { ScriptInterface };
|
|
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.insertLog = exports.userHasPermission = void 0;
|
|
6
|
+
exports.STDOUT_LOG_FIELD_MAX = exports.insertLog = exports.userHasPermission = void 0;
|
|
7
7
|
/**
|
|
8
8
|
* Kplian Ltda 2020
|
|
9
9
|
*
|
|
@@ -18,9 +18,13 @@ exports.insertLog = exports.userHasPermission = void 0;
|
|
|
18
18
|
* Last modified : 2020-10-20 17:46:24
|
|
19
19
|
*/
|
|
20
20
|
require("reflect-metadata");
|
|
21
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
21
22
|
const typeorm_1 = require("typeorm");
|
|
22
23
|
// import { Role } from '@pxp-nd/entities';
|
|
23
24
|
const Log_1 = __importDefault(require("../../../entities/Log"));
|
|
25
|
+
/** Max characters kept for request, response and query on a stdout line. CloudWatch drops an event past 256 KB. */
|
|
26
|
+
const STDOUT_LOG_FIELD_MAX = 8192;
|
|
27
|
+
exports.STDOUT_LOG_FIELD_MAX = STDOUT_LOG_FIELD_MAX;
|
|
24
28
|
const userHasPermission = async (user, transaction, permissionFunction) => {
|
|
25
29
|
if (typeof permissionFunction === 'function') {
|
|
26
30
|
return await permissionFunction(user, transaction);
|
|
@@ -42,24 +46,71 @@ const userHasPermission = async (user, transaction, permissionFunction) => {
|
|
|
42
46
|
}
|
|
43
47
|
};
|
|
44
48
|
exports.userHasPermission = userHasPermission;
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
49
|
+
/** uuid v4. @types/node 15 does not declare crypto.randomUUID. */
|
|
50
|
+
const newLogId = () => {
|
|
51
|
+
const bytes = crypto_1.default.randomBytes(16);
|
|
52
|
+
bytes[6] = (bytes[6] & 0x0f) | 0x40;
|
|
53
|
+
bytes[8] = (bytes[8] & 0x3f) | 0x80;
|
|
54
|
+
const hex = bytes.toString('hex');
|
|
55
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
56
|
+
};
|
|
57
|
+
const applyLogMask = (req, logConfig) => {
|
|
58
|
+
if (!logConfig || !req) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
for (const [key, value] of Object.entries(logConfig)) {
|
|
62
|
+
if (key in req) {
|
|
63
|
+
if (!value) {
|
|
64
|
+
delete req[key];
|
|
65
|
+
}
|
|
66
|
+
else if (value === 'cc_mask') {
|
|
67
|
+
const aux = req[key];
|
|
68
|
+
req[key] = '************' + aux.substr(-4);
|
|
69
|
+
}
|
|
70
|
+
else if (value === 'complete_mask') {
|
|
71
|
+
const aux = req[key];
|
|
72
|
+
req[key] = aux.replace(/./g, '*');
|
|
60
73
|
}
|
|
61
74
|
}
|
|
62
75
|
}
|
|
76
|
+
};
|
|
77
|
+
const capField = (value) => {
|
|
78
|
+
const text = value == null ? '' : String(value);
|
|
79
|
+
if (text.length <= STDOUT_LOG_FIELD_MAX) {
|
|
80
|
+
return { text, truncated: false };
|
|
81
|
+
}
|
|
82
|
+
return { text: text.slice(0, STDOUT_LOG_FIELD_MAX), truncated: true };
|
|
83
|
+
};
|
|
84
|
+
const serializeForLog = (value) => {
|
|
85
|
+
try {
|
|
86
|
+
return JSON.stringify(value);
|
|
87
|
+
}
|
|
88
|
+
catch (_a) {
|
|
89
|
+
return '{"error":"unserializable"}';
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
const emitStdoutLog = (logId, username, ip, logType, desc, proc, trans, query, req, resp, err, timeEx) => {
|
|
93
|
+
const request = capField(serializeForLog(req));
|
|
94
|
+
const response = capField(resp);
|
|
95
|
+
const queryField = capField(query);
|
|
96
|
+
console.log(JSON.stringify({
|
|
97
|
+
pxpLog: true,
|
|
98
|
+
logId,
|
|
99
|
+
type: logType,
|
|
100
|
+
user: username,
|
|
101
|
+
ip,
|
|
102
|
+
module: proc,
|
|
103
|
+
transaction: trans,
|
|
104
|
+
status: err,
|
|
105
|
+
ms: timeEx,
|
|
106
|
+
message: desc,
|
|
107
|
+
query: queryField.text,
|
|
108
|
+
request: request.text,
|
|
109
|
+
response: response.text,
|
|
110
|
+
truncated: request.truncated || response.truncated || queryField.truncated,
|
|
111
|
+
}));
|
|
112
|
+
};
|
|
113
|
+
const insertLogRow = async (username, macaddres, ip, logType, desc, proc, trans, query, req, resp, err, timeEx) => {
|
|
63
114
|
const log = new Log_1.default();
|
|
64
115
|
log.username = username;
|
|
65
116
|
log.macaddres = macaddres;
|
|
@@ -74,7 +125,7 @@ const insertLog = async (username, macaddres, ip, logType, desc, proc, trans, qu
|
|
|
74
125
|
log.execTime = timeEx;
|
|
75
126
|
log.errorCode = err;
|
|
76
127
|
log.shortRequest = JSON.stringify(req).substring(0, 150);
|
|
77
|
-
log.shortResponse = resp.substring(0, 150);
|
|
128
|
+
log.shortResponse = (resp || '').substring(0, 150);
|
|
78
129
|
const logIds = await typeorm_1.getManager(process.env.DB_LOG_CONNECTION_NAME)
|
|
79
130
|
.createQueryBuilder()
|
|
80
131
|
.insert()
|
|
@@ -83,4 +134,21 @@ const insertLog = async (username, macaddres, ip, logType, desc, proc, trans, qu
|
|
|
83
134
|
.execute();
|
|
84
135
|
return logIds.identifiers[0].logId;
|
|
85
136
|
};
|
|
137
|
+
const insertLog = async (username, macaddres, ip, logType, desc, proc, trans, query, req, resp, err, timeEx, logConfig, logSink = 'db') => {
|
|
138
|
+
const sink = logSink || 'db';
|
|
139
|
+
if (sink === 'stdout' || sink === 'both') {
|
|
140
|
+
const snapshot = Object.assign({}, (req || {}));
|
|
141
|
+
applyLogMask(snapshot, logConfig);
|
|
142
|
+
const logId = newLogId();
|
|
143
|
+
emitStdoutLog(logId, username, ip, logType, desc, proc, trans, query, snapshot, resp, err, timeEx);
|
|
144
|
+
if (sink === 'both') {
|
|
145
|
+
const dbReq = Object.assign(Object.assign({}, snapshot), { __logId: logId });
|
|
146
|
+
insertLogRow(username, macaddres, ip, logType, desc, proc, trans, query, dbReq, resp, err, timeEx)
|
|
147
|
+
.catch((dbErr) => console.error('[pxp-core] db log failed', dbErr && (dbErr.tecMessage || dbErr.message)));
|
|
148
|
+
}
|
|
149
|
+
return logId;
|
|
150
|
+
}
|
|
151
|
+
applyLogMask(req, logConfig);
|
|
152
|
+
return insertLogRow(username, macaddres, ip, logType, desc, proc, trans, query, req, resp, err, timeEx);
|
|
153
|
+
};
|
|
86
154
|
exports.insertLog = insertLog;
|
package/package.json
CHANGED
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* logSink tests. No database.
|
|
3
|
+
*
|
|
4
|
+
* stdout writes one JSON line and returns a uuid without calling getManager.
|
|
5
|
+
* both writes the line and inserts in the background; the caller is not blocked on the insert.
|
|
6
|
+
* db (the default) still inserts and returns the numeric id.
|
|
7
|
+
*/
|
|
8
|
+
import 'reflect-metadata';
|
|
9
|
+
import http from 'http';
|
|
10
|
+
import { AddressInfo } from 'net';
|
|
11
|
+
import express from 'express';
|
|
12
|
+
import * as typeorm from 'typeorm';
|
|
13
|
+
import { insertLog, STDOUT_LOG_FIELD_MAX } from '../src/lib/pxp/utils/Security';
|
|
14
|
+
import { Controller } from '../src/lib/pxp/Controller';
|
|
15
|
+
import { Authentication, Post, ReadOnly } from '../src/lib/pxp/Decorators';
|
|
16
|
+
import { PxpError } from '../src/lib/pxp/PxpError';
|
|
17
|
+
import { IConfigPxpApp } from '../src/interfaces';
|
|
18
|
+
|
|
19
|
+
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
20
|
+
|
|
21
|
+
const baseArgs = (req: Record<string, unknown>, logConfig: Record<string, unknown> = {}) =>
|
|
22
|
+
['user', 'mac', '127.0.0.1', 'success', 'ok', 'mod', 'mod.tx', '', req, '{"ok":true}', '200', 12, logConfig] as const;
|
|
23
|
+
|
|
24
|
+
const pxpLines = (spy: jest.SpyInstance): any[] =>
|
|
25
|
+
spy.mock.calls
|
|
26
|
+
.map((call) => call[0])
|
|
27
|
+
.filter((line) => typeof line === 'string' && line.includes('"pxpLog":true'))
|
|
28
|
+
.map((line) => JSON.parse(line));
|
|
29
|
+
|
|
30
|
+
describe('insertLog sinks', () => {
|
|
31
|
+
let logSpy: jest.SpyInstance;
|
|
32
|
+
let getManager: jest.SpyInstance;
|
|
33
|
+
let execute: jest.Mock;
|
|
34
|
+
let values: jest.Mock;
|
|
35
|
+
|
|
36
|
+
beforeEach(() => {
|
|
37
|
+
logSpy = jest.spyOn(console, 'log').mockImplementation(() => undefined);
|
|
38
|
+
execute = jest.fn().mockResolvedValue({ identifiers: [{ logId: 7 }] });
|
|
39
|
+
values = jest.fn();
|
|
40
|
+
const qb = {
|
|
41
|
+
insert: jest.fn(),
|
|
42
|
+
into: jest.fn(),
|
|
43
|
+
values,
|
|
44
|
+
execute,
|
|
45
|
+
};
|
|
46
|
+
qb.insert.mockReturnValue(qb);
|
|
47
|
+
qb.into.mockReturnValue(qb);
|
|
48
|
+
values.mockReturnValue(qb);
|
|
49
|
+
getManager = jest.spyOn(typeorm, 'getManager').mockReturnValue({ createQueryBuilder: () => qb } as any);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
afterEach(() => {
|
|
53
|
+
logSpy.mockRestore();
|
|
54
|
+
getManager.mockRestore();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('stdout returns a uuid, writes one line, and does not touch the database', async () => {
|
|
58
|
+
const req = { card: '4111111111111111', keep: 'yes' };
|
|
59
|
+
const id = await insertLog(...baseArgs(req, { card: 'cc_mask' }), 'stdout');
|
|
60
|
+
|
|
61
|
+
expect(id).toMatch(UUID);
|
|
62
|
+
expect(getManager).not.toHaveBeenCalled();
|
|
63
|
+
expect(req).toEqual({ card: '4111111111111111', keep: 'yes' });
|
|
64
|
+
|
|
65
|
+
const [line] = pxpLines(logSpy);
|
|
66
|
+
expect(line.logId).toBe(id);
|
|
67
|
+
expect(line.type).toBe('success');
|
|
68
|
+
expect(line.user).toBe('user');
|
|
69
|
+
expect(line.transaction).toBe('mod.tx');
|
|
70
|
+
expect(line.status).toBe('200');
|
|
71
|
+
expect(line.truncated).toBe(false);
|
|
72
|
+
expect(line.request).toContain('************1111');
|
|
73
|
+
expect(line.request).not.toContain('4111111111111111');
|
|
74
|
+
expect(line.request).toContain('yes');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('stdout drops masked-out fields and caps oversized bodies', async () => {
|
|
78
|
+
const req = { favicon: 'png', note: 'x'.repeat(STDOUT_LOG_FIELD_MAX + 500) };
|
|
79
|
+
await insertLog(...baseArgs(req, { favicon: null }), 'stdout');
|
|
80
|
+
|
|
81
|
+
const [line] = pxpLines(logSpy);
|
|
82
|
+
expect(line.request.startsWith('{"note":')).toBe(true);
|
|
83
|
+
expect(line.request).not.toContain('favicon');
|
|
84
|
+
expect(line.request.length).toBe(STDOUT_LOG_FIELD_MAX);
|
|
85
|
+
expect(line.truncated).toBe(true);
|
|
86
|
+
expect(req.favicon).toBe('png');
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test('both returns the uuid before the insert finishes and stores __logId on the row', async () => {
|
|
90
|
+
let release: (value: unknown) => void = () => undefined;
|
|
91
|
+
execute.mockReturnValue(new Promise((resolve) => {
|
|
92
|
+
release = resolve;
|
|
93
|
+
}));
|
|
94
|
+
|
|
95
|
+
const id = await Promise.race([
|
|
96
|
+
insertLog(...baseArgs({ a: 1 }), 'both'),
|
|
97
|
+
new Promise((_resolve, reject) => setTimeout(() => reject(new Error('waited for the database')), 200)),
|
|
98
|
+
]);
|
|
99
|
+
|
|
100
|
+
expect(id).toMatch(UUID);
|
|
101
|
+
const [line] = pxpLines(logSpy);
|
|
102
|
+
expect(line.logId).toBe(id);
|
|
103
|
+
expect(execute).toHaveBeenCalledTimes(1);
|
|
104
|
+
|
|
105
|
+
const row = values.mock.calls[0][0][0];
|
|
106
|
+
expect(JSON.parse(row.request).__logId).toBe(id);
|
|
107
|
+
|
|
108
|
+
release({ identifiers: [{ logId: 7 }] });
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test('db stays on the numeric id and writes no stdout line', async () => {
|
|
112
|
+
const id = await insertLog(...baseArgs({ a: 1 }));
|
|
113
|
+
|
|
114
|
+
expect(id).toBe(7);
|
|
115
|
+
expect(getManager).toHaveBeenCalledTimes(1);
|
|
116
|
+
expect(pxpLines(logSpy)).toEqual([]);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test('a failing background insert does not reject', async () => {
|
|
120
|
+
execute.mockRejectedValue(new Error('connect ECONNREFUSED'));
|
|
121
|
+
const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => undefined);
|
|
122
|
+
const unhandled: unknown[] = [];
|
|
123
|
+
const onUnhandled = (reason: unknown) => unhandled.push(reason);
|
|
124
|
+
process.on('unhandledRejection', onUnhandled);
|
|
125
|
+
|
|
126
|
+
const id = await insertLog(...baseArgs({ a: 1 }), 'both');
|
|
127
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
128
|
+
|
|
129
|
+
expect(id).toMatch(UUID);
|
|
130
|
+
expect(errorSpy).toHaveBeenCalledWith('[pxp-core] db log failed', 'connect ECONNREFUSED');
|
|
131
|
+
expect(unhandled).toEqual([]);
|
|
132
|
+
|
|
133
|
+
process.removeListener('unhandledRejection', onUnhandled);
|
|
134
|
+
errorSpy.mockRestore();
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
class Audited extends Controller {
|
|
139
|
+
@Post()
|
|
140
|
+
@ReadOnly(true)
|
|
141
|
+
@Authentication(false)
|
|
142
|
+
async fail() {
|
|
143
|
+
throw new PxpError(400, 'audit me');
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
@Post()
|
|
147
|
+
@ReadOnly(true)
|
|
148
|
+
@Authentication(false)
|
|
149
|
+
async ok() {
|
|
150
|
+
return { ok: true };
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const call = (url: string) =>
|
|
155
|
+
new Promise<{ status: number; json: any }>((resolve, reject) => {
|
|
156
|
+
const payload = '{}';
|
|
157
|
+
const req = http.request(
|
|
158
|
+
url,
|
|
159
|
+
{ method: 'POST', headers: { 'content-type': 'application/json', 'content-length': Buffer.byteLength(payload) } },
|
|
160
|
+
(res) => {
|
|
161
|
+
let raw = '';
|
|
162
|
+
res.setEncoding('utf8');
|
|
163
|
+
res.on('data', (chunk) => (raw += chunk));
|
|
164
|
+
res.on('end', () => resolve({ status: res.statusCode as number, json: JSON.parse(raw) }));
|
|
165
|
+
}
|
|
166
|
+
);
|
|
167
|
+
req.on('error', reject);
|
|
168
|
+
req.end(payload);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
describe('controller with logSink stdout', () => {
|
|
172
|
+
let server: http.Server;
|
|
173
|
+
let base: string;
|
|
174
|
+
let logSpy: jest.SpyInstance;
|
|
175
|
+
let getManager: jest.SpyInstance;
|
|
176
|
+
|
|
177
|
+
beforeAll(async () => {
|
|
178
|
+
const config: IConfigPxpApp = {
|
|
179
|
+
apiPrefix: '/api',
|
|
180
|
+
defaultDbSettings: 'Orm',
|
|
181
|
+
middlewares: [],
|
|
182
|
+
entities: {},
|
|
183
|
+
logSink: 'stdout',
|
|
184
|
+
};
|
|
185
|
+
const app = express();
|
|
186
|
+
app.use(express.json());
|
|
187
|
+
app.use((req: any, _res, next) => {
|
|
188
|
+
req.start = new Date();
|
|
189
|
+
next();
|
|
190
|
+
});
|
|
191
|
+
app.use('/', new Audited('test', null, config).router);
|
|
192
|
+
server = http.createServer(app);
|
|
193
|
+
await new Promise<void>((resolve) => server.listen(0, '127.0.0.1', resolve));
|
|
194
|
+
const { port } = server.address() as AddressInfo;
|
|
195
|
+
base = `http://127.0.0.1:${port}/api/test/Audited`;
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
beforeEach(() => {
|
|
199
|
+
logSpy = jest.spyOn(console, 'log').mockImplementation(() => undefined);
|
|
200
|
+
getManager = jest.spyOn(typeorm, 'getManager').mockImplementation(() => {
|
|
201
|
+
throw new Error('stdout sink must not ask for a connection');
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
afterEach(() => {
|
|
206
|
+
logSpy.mockRestore();
|
|
207
|
+
getManager.mockRestore();
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
afterAll(async () => {
|
|
211
|
+
await new Promise((resolve) => server.close(resolve));
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
test('an error response carries the same uuid that was written to stdout', async () => {
|
|
215
|
+
const r = await call(`${base}/fail`);
|
|
216
|
+
|
|
217
|
+
expect(r.status).toBe(400);
|
|
218
|
+
expect(r.json.error.message).toBe('audit me');
|
|
219
|
+
expect(r.json.error.logId).toMatch(UUID);
|
|
220
|
+
expect(getManager).not.toHaveBeenCalled();
|
|
221
|
+
|
|
222
|
+
const [line] = pxpLines(logSpy);
|
|
223
|
+
expect(line.logId).toBe(r.json.error.logId);
|
|
224
|
+
expect(line.type).toBe('error');
|
|
225
|
+
expect(line.status).toBe(400);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
test('a successful request is logged and still answers 200', async () => {
|
|
229
|
+
const r = await call(`${base}/ok`);
|
|
230
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
231
|
+
|
|
232
|
+
expect(r.status).toBe(200);
|
|
233
|
+
expect(r.json).toEqual({ ok: true });
|
|
234
|
+
expect(r.json.logId).toBeUndefined();
|
|
235
|
+
expect(getManager).not.toHaveBeenCalled();
|
|
236
|
+
|
|
237
|
+
const [line] = pxpLines(logSpy);
|
|
238
|
+
expect(line.logId).toMatch(UUID);
|
|
239
|
+
expect(line.type).toBe('success');
|
|
240
|
+
expect(line.status).toBe('200');
|
|
241
|
+
});
|
|
242
|
+
});
|