From c45d6ea4526e3b87d18b7bbbd79612376317dfb8 Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Tue, 30 Sep 2025 21:51:25 -0400 Subject: [PATCH] implement MockLoggerService.ts testing utility --- packages/backend/src/logger.ts | 16 +++- .../backend/test/misc/MockLoggerService.ts | 74 +++++++++++++++++++ 2 files changed, 87 insertions(+), 3 deletions(-) create mode 100644 packages/backend/test/misc/MockLoggerService.ts diff --git a/packages/backend/src/logger.ts b/packages/backend/src/logger.ts index 4bf45fc76b..0799ec3e94 100644 --- a/packages/backend/src/logger.ts +++ b/packages/backend/src/logger.ts @@ -23,13 +23,16 @@ export type DataElement = DataObject | Error | string | null; // https://stackoverflow.com/questions/61148466/typescript-type-that-matches-any-object-but-not-arrays export type DataObject = Record | (object & { length?: never; }); +export type Console = Pick; +export const nativeConsole: Console = global.console; + const levelFuncs = { error: 'error', warning: 'warn', success: 'info', info: 'log', debug: 'debug', -} as const satisfies Record; +} as const satisfies Record; // eslint-disable-next-line import/no-default-export export default class Logger { @@ -37,12 +40,19 @@ export default class Logger { private parentLogger: Logger | null = null; public readonly verbose: boolean; - constructor(context: string, color?: KEYWORD, verbose?: boolean) { + /** + * Where to send the actual log strings. + * Defaults to the native global.console instance. + */ + private readonly console: Console; + + constructor(context: string, color?: KEYWORD, verbose?: boolean, console?: Console) { this.context = { name: context, color: color, }; this.verbose = verbose ?? envOption.verbose; + this.console = console ?? nativeConsole; } @bindThis @@ -94,7 +104,7 @@ export default class Logger { } else if (data != null) { args.push(data); } - console[levelFuncs[level]](...args); + this.console[levelFuncs[level]](...args); } @bindThis diff --git a/packages/backend/test/misc/MockLoggerService.ts b/packages/backend/test/misc/MockLoggerService.ts new file mode 100644 index 0000000000..fe4af8d659 --- /dev/null +++ b/packages/backend/test/misc/MockLoggerService.ts @@ -0,0 +1,74 @@ +/* + * SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { jest } from '@jest/globals'; +import { Injectable } from '@nestjs/common'; +import type { KEYWORD } from 'color-convert/conversions.js'; +import type { Config } from '@/config.js'; +import Logger, { type Console } from '@/logger.js'; +import { LoggerService } from '@/core/LoggerService.js'; +import { bindThis } from '@/decorators.js'; + +/** + * Mocked implementation of LoggerService. + * Suppresses all log output to prevent console spam, and records calls for assertions. + */ +@Injectable() +export class MockLoggerService extends LoggerService { + /** + * Mocked Console implementation. + * All logs from all logger instances will be sent here. + */ + public readonly console: jest.Mocked = { + error: jest.fn(), + warn: jest.fn(), + info: jest.fn(), + log: jest.fn(), + debug: jest.fn(), + }; + + /** + * Controls the verbose flag for logger instances. + * Defaults to false (not verbose). + */ + public verbose: boolean; + + constructor(config?: Config) { + config ??= { logging: { verbose: false } } as Config; + super(config); + } + + /** + * Resets the instance to initial state. + * Mocks are reset, and verbose flag is cleared. + */ + @bindThis + public reset() { + this.console.error.mockReset(); + this.console.warn.mockReset(); + this.console.info.mockReset(); + this.console.log.mockReset(); + this.console.debug.mockReset(); + + this.verbose = false; + } + + /** + * Asserts that no errors and/or warnings have been logged. + */ + @bindThis + public assertNoErrors(opts?: { orWarnings?: boolean }): void { + expect(this.console.error).not.toHaveBeenCalled(); + + if (opts?.orWarnings) { + expect(this.console.warn).not.toHaveBeenCalled(); + } + } + + @bindThis + getLogger(domain: string, color?: KEYWORD | undefined): Logger { + return new Logger(domain, color, this.verbose, this.console); + } +}