batchCooking/apps/api/test/logger.service.test.ts
Nicolas 22582536a6 feat(api): ajoute un log service pour les logs de fonctionnement côté serveur
Jusqu'ici, rien ne journalisait quoi que ce soit côté serveur : aucune
trace au démarrage à part un console.log ad hoc, et surtout aucune trace
des requêtes ni des erreurs gérées par ErrorHandlerService — un 500 en
production n'aurait laissé aucune trace exploitable.

- LoggerService (apps/api/src/lib/logger.service.ts) — classe (public
  debug/info/warn/error, private emit), même convention que
  ErrorHandlerService (packages/error-tools) : instance unique partagée
  exportée (`export const logger = new LoggerService()`). Émet une ligne
  JSON structurée par appel (timestamp/level/message + meta), filtrée par
  seuil selon NODE_ENV (debug complet en dev, warn+ pendant les tests
  pour ne pas alourdir la sortie de Mocha, info+ en production). Seul
  endroit du code autorisé à toucher `console` directement (biome-ignore
  justifié), toujours via une méthode nommée — jamais un console.log nu.
- requestLogger (middlewares/request-logger.ts) — une ligne par requête
  terminée (méthode/chemin/statut/durée), montée en tout premier dans
  app.ts, avant même setupCore (CORS/JSON/cookies), pour englober tout le
  pipeline. Niveau déduit du statut (info/warn/error).
- errorLogger (middlewares/error-logger.ts) — monté juste avant
  createErrorMiddleware : réutilise errorHandlerService.handle() (pur/
  sans effet de bord) pour classifier l'erreur avant que la vraie réponse
  ne soit construite, log en warn les 4xx routiniers (validation, 404,
  401...) et en error les 5xx/exceptions non prévues (avec la stack).
- error-handler.service.ts : retire le `console.error(error)` ad hoc de
  fromUnknownError — errorLogger voit désormais chaque erreur avant que
  ce service ne la mappe, donc ce console.error faisait doublon (et
  loggait en texte brut, pas en JSON structuré).
- server.ts : le console.log de démarrage passe par logger.info.

Vérifié : pnpm --filter api test (303/303, dont 8 nouveaux tests sur
LoggerService), pnpm lint/build clean, testé en live (pnpm dev:api +
curl) — logs JSON corrects pour un 200, un 404, un 401.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-21 10:19:27 +02:00

105 lines
3.8 KiB
TypeScript

import { expect } from "chai";
import { LoggerService, logger, minLevelFor } from "../src/lib/logger.service.js";
/**
* Stubs one `console` method for one test, capturing every call instead of
* actually writing to stdout/stderr — same "stub the one thing that
* touches the outside world" approach `the-meal-db.test.ts` uses for
* `fetch`. Restored by the caller (`afterEach` below) regardless of which
* test used it.
*/
function stubConsoleMethod(method: "debug" | "info" | "warn" | "error") {
const calls: unknown[][] = [];
// biome-ignore lint/suspicious/noConsole: this *is* the console-stubbing helper — it has to read the real method to be able to restore it. Never calls it for real.
const original = console[method];
console[method] = (...args: unknown[]) => {
calls.push(args);
};
return {
calls,
restore: () => (console[method] = original),
};
}
describe("logger.service", () => {
describe("minLevelFor", () => {
it("only lets warn/error through in test — Mocha's own output shouldn't get any noisier", () => {
expect(minLevelFor("test")).to.equal("warn");
});
it("only lets info/warn/error through in production — debug is too verbose to ship", () => {
expect(minLevelFor("production")).to.equal("info");
});
it("lets everything through, including debug, in development", () => {
expect(minLevelFor("development")).to.equal("debug");
});
});
describe("logger", () => {
let stub: ReturnType<typeof stubConsoleMethod>;
afterEach(() => {
stub?.restore();
});
it("emits a warn line as a single JSON object via console.warn, with a timestamp/level/message and any extra meta merged in", () => {
stub = stubConsoleMethod("warn");
logger.warn("Something routine failed", { status: 404, code: 4049 });
expect(stub.calls).to.have.length(1);
const [line] = stub.calls[0];
const parsed = JSON.parse(line as string);
expect(parsed.level).to.equal("warn");
expect(parsed.message).to.equal("Something routine failed");
expect(parsed.status).to.equal(404);
expect(parsed.code).to.equal(4049);
expect(new Date(parsed.timestamp).toString()).to.not.equal("Invalid Date");
});
it("emits an error line via console.error", () => {
stub = stubConsoleMethod("error");
logger.error("Something broke");
expect(stub.calls).to.have.length(1);
const parsed = JSON.parse(stub.calls[0][0] as string);
expect(parsed.level).to.equal("error");
});
it('drops debug/info under the test suite\'s own NODE_ENV=test (minLevelFor("test") === "warn")', () => {
const debugStub = stubConsoleMethod("debug");
const infoStub = stubConsoleMethod("info");
logger.debug("Should not appear");
logger.info("Should not appear either");
expect(debugStub.calls).to.have.length(0);
expect(infoStub.calls).to.have.length(0);
debugStub.restore();
infoStub.restore();
});
it("never lets meta override the line's own timestamp/level/message keys", () => {
stub = stubConsoleMethod("warn");
logger.warn("Real message", { message: "spoofed", level: "spoofed", timestamp: "spoofed" });
const parsed = JSON.parse(stub.calls[0][0] as string);
expect(parsed.message).to.equal("Real message");
expect(parsed.level).to.equal("warn");
expect(new Date(parsed.timestamp).toString()).to.not.equal("Invalid Date");
});
it('a separate instance constructed with nodeEnv="development" lets debug through, independently of the shared logger\'s own NODE_ENV=test threshold', () => {
const debugStub = stubConsoleMethod("debug");
const devLogger = new LoggerService("development");
devLogger.debug("Visible in dev");
expect(debugStub.calls).to.have.length(1);
debugStub.restore();
});
});
});