Files
cms/tests/unit/log-error.test.ts
T
libretech ab071cd222 fix(logging): write log fields without throwing and without credentials
formatLogLine wrote the fields of a log call with JSON.stringify. A
circular reference threw, so Strapi's error middleware lost the error
it was logging: the axios error of a failed PayPal token request holds
its request object, which refers to itself. An Error was written as {},
and the config.headers of an axios error carried "Authorization: Basic"
with the base64 of the PayPal client id and secret into the line.

The fields are now copied before they are written:
- a field named like a credential (authorization, cookie, password,
  secret, token, api key) is written as "[redacted]", at any depth;
- an Error is written as { name, message }, with the first line of the
  message as describeError has it, and its stack only with the stack
  option;
- an object that contains itself is written once, the reference to it
  as "[Circular]";
- an instance of a class is written as its toJSON(), or as its class
  name without one: a request object keeps its headers in _header;
- a bigint is written as its digits, an unreadable field as
  "[unreadable]".

The three todo tests of the formatter pass and are regular tests now.

Refs libretech/mp#71
Refs libretech/mp#67
2026-10-09 01:41:34 +02:00

68 lines
3.4 KiB
TypeScript

import { test } from "node:test";
import assert from "node:assert/strict";
import { describeError } from "../../src/logging/error.ts";
import { formatLogLine } from "../../src/logging/format.ts";
const TIMESTAMP = "2026-10-08 21:29:40.426";
const CREDENTIALS = "RFVNTVlfQ0xJRU5UX0lEOkRVTU1ZX0NMSUVOVF9TRUNSRVQ="; // base64 of the dummy "DUMMY_CLIENT_ID:DUMMY_CLIENT_SECRET"
// What axios throws when PayPal cannot be reached: the request config with its headers.
const networkError = () =>
Object.assign(new Error("getaddrinfo EAI_AGAIN api-m.paypal.com"), {
name: "AxiosError",
code: "EAI_AGAIN",
config: { method: "post", url: "https://api-m.paypal.com/v1/oauth2/token", headers: { Authorization: `Basic ${CREDENTIALS}` } }
});
test("keeps the name, message and status code of a PayPal SDK error, not its request, headers or body", () => {
// @apimatic/core's ApiError, which the SDK's CustomError and OAuthProviderError extend
const error = Object.assign(new Error("OAuth 2 Authorization endpoint returned error."), {
request: { method: "POST", url: "https://api-m.paypal.com/v1/oauth2/token", headers: { Authorization: `Basic ${CREDENTIALS}` } },
statusCode: 401,
headers: { "paypal-debug-id": "f1a2b3c4d5e6f" },
body: '{"error":"invalid_client","error_description":"Client Authentication failed"}'
});
assert.deepEqual(describeError(error), { name: "Error", message: "OAuth 2 Authorization endpoint returned error.", statusCode: 401 });
});
test("keeps the name and message of an axios network error, not its config", () => {
assert.deepEqual(describeError(networkError()), { name: "AxiosError", message: "getaddrinfo EAI_AGAIN api-m.paypal.com", statusCode: undefined });
});
test("keeps the first line of a message: the SDK's response validation error quotes the response after it", () => {
const error = new Error(
'The response did not match the response schema.\n\n> Issue #1\n\nExpected value to be of type \'string\'.\n\nGiven value: {"email_address":"erika.buyer@example.org"}'
);
assert.deepEqual(describeError(error), { name: "Error", message: "The response did not match the response schema.", statusCode: undefined });
});
test("cuts a long message", () => {
assert.equal(describeError(new Error("x".repeat(500))).message, `${"x".repeat(200)}…`);
});
test("describes a thrown value that is not an error by its type alone", () => {
assert.deepEqual(describeError("/result/error"), { name: "string", message: "", statusCode: undefined });
assert.deepEqual(describeError(undefined), { name: "undefined", message: "", statusCode: undefined });
assert.deepEqual(describeError(null), { name: "object", message: "", statusCode: undefined });
assert.deepEqual(describeError({ statusCode: "500" }), { name: "object", message: "", statusCode: undefined });
});
test("the log line of the description holds no credentials", () => {
// The formatter now also keeps them out of the line of the error object itself, see log-format.test.ts.
const line = formatLogLine({
timestamp: TIMESTAMP,
level: "error",
message: "app:e:order-controller: PayPal did not capture the payment",
order: 4711,
error: describeError(networkError())
});
assert.equal(
line,
'2026-10-08 21:29:40.426Z error: app:e:order-controller: PayPal did not capture the payment {\n "order": 4711,\n "error": {\n "name": "AxiosError",\n "message": "getaddrinfo EAI_AGAIN api-m.paypal.com"\n }\n}'
);
assert.ok(!line.includes(CREDENTIALS));
});