Files
cms/tests/unit/log-error.test.ts
T
libretech f599b2c84b feat(logging): describe an error by its name, message and status code
describeError keeps an error's name, the first line of its message, cut
to 200 characters, and its HTTP status code. An error of the PayPal SDK
or of axios carries its request, whose Authorization header holds the
PayPal client credentials or an access token, so the error object itself
must not reach the log. The SDK's response validation error quotes the
response values on the lines after the first.

Refs libretech/mp#71
2026-10-09 01:12:20 +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", () => {
// Compare log-format.test.ts: the line of the error object itself holds them, a known defect of the formatter.
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));
});