log-vault
Version:
A generator of Winston logger instance with pre-defined configurable transports and formats and extra functionality.
347 lines (346 loc) • 13.3 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const body_parser_1 = __importDefault(require("body-parser"));
const express_1 = __importDefault(require("express"));
const bullmq_1 = require("bullmq");
const _1 = require(".");
const defaults_1 = require("./defaults");
const waitForProcess_1 = require("./test-files/util/waitForProcess");
const wait_1 = require("./test-files/util/wait");
const testToken = "testToken";
const testChatId = 1;
describe("e2e tests: LogVault with Notificator", () => {
let tgRequestBody;
let tgShouldFail = false;
let mockServer;
const mockPort = 7625;
let notificator;
let logger;
let logVault;
let timestamp;
beforeAll(async () => {
// Clean queues from any state left by other test files or previous runs
const projectQueue = new bullmq_1.Queue("log-vault", {
connection: defaults_1.defaultRedisConnection
});
const channelQueue = new bullmq_1.Queue(`${testToken}.${testChatId}`, {
connection: defaults_1.defaultRedisConnection
});
await Promise.all([
projectQueue.obliterate({ force: true }),
channelQueue.obliterate({ force: true })
]);
await Promise.all([projectQueue.close(), channelQueue.close()]);
await startMockServer();
});
afterAll(async () => {
await mockServer.close();
});
beforeEach(() => {
// channel queue is cleaned by afterEach → channel.stop(); no per-test obliterate
// to avoid disrupting the BullMQ events stream that QueueEvents subscribes to
});
afterEach(async () => {
tgRequestBody = undefined;
tgShouldFail = false;
await notificator.stop();
jest.clearAllMocks();
});
it("e2e:send notification to Telegram, matched by level", async () => {
initTest({ matchPatterns: [{ level: "http" }] });
await setupQueueListener();
logger.http("something");
await waitForProcessTest();
expect(tgRequestBody).toEqual({
chat_id: 1,
text: `🔵 *http log message* ⏱ _${timestamp}_\n` +
"\n" +
"`[project]: log\\-vault`\n" +
"`[process]: log\\-vault`\n" +
"`[environment]: test`\n" +
"\n" +
"```json\n" +
'"something"\n' +
"```",
parse_mode: "MarkdownV2"
});
});
it("e2e:send notification to Telegram, mismatch by level", async () => {
initTest({ matchPatterns: [{ level: "http" }] });
await setupQueueListener();
logger.info("should not be notified");
await (0, wait_1.wait)(350);
expect(tgRequestBody).toBe(undefined);
});
it("e2e:send notification to Telegram, matched by message, nested", async () => {
initTest({ matchPatterns: [{ match: { message: /error/gi } }] });
await setupQueueListener();
logger.http({
message: "Request failed",
details: {
request: {
headers: {
header1: "header data"
},
body: {
some: "data"
}
},
response: {
responseData: "Error! Wrong request"
}
}
});
await waitForProcessTest();
expect(tgRequestBody).toEqual({
chat_id: 1,
text: `🔵 *http log message* ⏱ _${timestamp}_\n` +
"\n" +
"`[project]: log\\-vault`\n" +
"`[process]: log\\-vault`\n" +
"`[environment]: test`\n" +
"\n" +
"```json\n" +
"\\[\n" +
' "Request failed",\n' +
" \\{\n" +
' "details": \\{\n' +
' "request": \\{\n' +
' "headers": \\{\n' +
' "header1": "header data"\n' +
" \\},\n" +
' "body": \\{\n' +
' "some": "data"\n' +
" \\}\n" +
" \\},\n" +
' "response": \\{\n' +
' "responseData": "Error\\! Wrong request"\n' +
" \\}\n" +
" \\}\n" +
" \\}\n" +
"\\]\n" +
"```",
parse_mode: "MarkdownV2"
});
});
it("e2e:send notification to Telegram, matched by message, nested, with exclusion pattern", async () => {
initTest({
matchPatterns: [
{
level: "http",
match: { message: /error/gi },
exclude: { message: /bot\sping/gi }
}
]
});
await setupQueueListener();
logger.http({
message: "Request failed",
details: {
request: {
headers: {
header1: "header data"
},
body: {
some: "data"
}
},
response: {
responseData: "Error! Wrong request"
}
}
});
await waitForProcessTest();
expect(tgRequestBody).toEqual({
chat_id: 1,
text: `🔵 *http log message* ⏱ _${timestamp}_\n` +
"\n" +
"`[project]: log\\-vault`\n" +
"`[process]: log\\-vault`\n" +
"`[environment]: test`\n" +
"\n" +
"```json\n" +
"\\[\n" +
' "Request failed",\n' +
" \\{\n" +
' "details": \\{\n' +
' "request": \\{\n' +
' "headers": \\{\n' +
' "header1": "header data"\n' +
" \\},\n" +
' "body": \\{\n' +
' "some": "data"\n' +
" \\}\n" +
" \\},\n" +
' "response": \\{\n' +
' "responseData": "Error\\! Wrong request"\n' +
" \\}\n" +
" \\}\n" +
" \\}\n" +
"\\]\n" +
"```",
parse_mode: "MarkdownV2"
});
});
it("e2e: does not loop with circular reference in log data", async () => {
initTest({ matchPatterns: [{ level: "http" }] });
await setupQueueListener();
const circular = { message: "circular test" };
circular.self = circular;
logger.http(circular);
const processed = await completedPromise;
expect(processed).toBeDefined(); // job completed, no infinite loop
});
it("e2e: does not loop with BigInt in log data", async () => {
initTest({ matchPatterns: [{ level: "http" }] });
logger.http({ message: "bigint test", value: BigInt(42) });
await (0, wait_1.wait)(500);
expect(tgRequestBody).toBeUndefined();
});
it("e2e: does not loop when message is undefined", async () => {
initTest({ matchPatterns: [{ level: "http" }] });
await setupQueueListener();
logger.http({ message: undefined });
const processed = await completedPromise;
expect(processed).toBeDefined(); // job completed, no infinite loop
});
it("e2e: captureConsole does not loop when notification processing fails with undefined message", async () => {
const originalConsoleError = console.error;
try {
initTest({ matchPatterns: [{ level: "http" }] });
logVault.captureConsole();
await setupQueueListener();
logger.http({ message: undefined });
const processed = await completedPromise;
expect(processed).toBeDefined(); // job completed, no infinite loop
}
finally {
console.error = originalConsoleError;
}
});
it("e2e: handles Telegram API failure gracefully", async () => {
initTest({ matchPatterns: [{ level: "error" }], jobOptions: { attempts: 1 } });
tgShouldFail = true;
const { failed } = await (0, waitForProcess_1.waitForProcess)(`${testToken}.${testChatId}`);
logger.error("test error - telegram will fail");
const failedReason = await failed;
expect(failedReason).toBeDefined();
expect(tgRequestBody).toBeUndefined();
});
it("e2e: does not loop when Telegram API fails with captureConsole active", async () => {
const originalConsoleError = console.error;
try {
initTest({ matchPatterns: [{ level: "error" }] });
tgShouldFail = true;
logVault.captureConsole();
const errorSpy = jest.spyOn(logger, "error");
logger.error("test error - should not loop");
await (0, wait_1.wait)(1000);
expect(errorSpy).toHaveBeenCalledTimes(1);
}
finally {
console.error = originalConsoleError;
}
});
it("e2e: withNotificator logs final failure through logger without looping", async () => {
initTest({ matchPatterns: [{ level: "error" }], jobOptions: { attempts: 1 } });
logVault.withNotificator(notificator);
tgShouldFail = true;
const errorSpy = jest.spyOn(logger, "error");
const { failed } = await (0, waitForProcess_1.waitForProcess)(`${testToken}.${testChatId}`);
logger.error("test error");
await failed;
await (0, wait_1.wait)(100);
expect(errorSpy).toHaveBeenCalledTimes(2);
expect(errorSpy).toHaveBeenLastCalledWith("Notification delivery failed", expect.objectContaining({ payload: expect.any(Object) }));
});
it("e2e: processes normal data correctly after non-serializable data", async () => {
initTest({ matchPatterns: [{ level: "http" }] });
const circular = {};
circular.self = circular;
logger.http(circular);
await (0, wait_1.wait)(100);
await setupQueueListener();
logger.http("recovery message");
await waitForProcessTest();
expect(tgRequestBody).toEqual(expect.objectContaining({ chat_id: 1, parse_mode: "MarkdownV2" }));
});
it("e2e:send notification to Telegram, matched by message, nested, with exclusion pattern, exclusion matched", async () => {
initTest({
matchPatterns: [
{ match: { message: /error/gi }, exclude: { message: /bot\sping/gi } }
]
});
logger.http({
message: "Request failed",
details: {
request: {
headers: {
header1: "header data",
identification: "Bot ping"
},
body: {
some: "data"
}
},
response: {
responseData: "Error! Wrong request"
}
}
});
await (0, wait_1.wait)(350);
expect(tgRequestBody).toBe(undefined);
});
let completedPromise;
async function setupQueueListener() {
const { completed } = await (0, waitForProcess_1.waitForProcess)(`${testToken}.${testChatId}`);
completedPromise = completed;
}
function initTest(opts) {
const { matchPatterns = [], telegramHost = `http://localhost:${mockPort}`, jobOptions = {} } = opts;
notificator = new _1.Notificator({
workerOpts: {
limiter: {
max: 1,
duration: 30
}
}
});
notificator.add(new _1.TelegramNotificationChannel({
host: telegramHost,
token: testToken,
chatId: testChatId,
matchPatterns,
jobOptions,
workerOptions: {
limiter: {
max: 1,
duration: 10
}
}
}));
logVault = new _1.LogVault().withNotifications();
logger = logVault.logger;
}
function startMockServer() {
return new Promise((resolve) => {
const app = (0, express_1.default)();
app.use(body_parser_1.default.json());
app.post("/*/sendMessage", (req, res) => {
if (tgShouldFail)
return res.status(500).end();
tgRequestBody = req.body;
return res.status(200).end();
});
mockServer = app.listen(mockPort, () => resolve(true));
});
}
async function waitForProcessTest() {
const processed = await completedPromise;
timestamp = processed.timestamp.replace(/([|{[\]*_~}+)(#>!=\-.])/gm, "\\$1");
return processed;
}
});