Explorar el Código

inject logger to controller, increase test coverage

Richard Köhl hace 2 años
padre
commit
651c506f1b

+ 1 - 0
.gitignore

@@ -1 +1,2 @@
 .env
+coverage/

+ 54 - 1
src/infrastructure/logger.ts

@@ -28,7 +28,60 @@ log.setup({
       level: getLogLevel(logLevel),
       handlers: ['console'],
     },
+    test: {
+      level: 'DEBUG',
+      handlers: ['console'],
+    },
   },
 });
 
-export const logger = log.getLogger();
+export class Logger {
+  private messages: string[] = [];
+  private logger;
+
+  constructor(private loggerName = 'default') {
+    this.logger = log.getLogger(loggerName);
+  }
+
+  public static getInstance(loggerName?: string): Logger {
+    return new Logger(loggerName);
+  }
+
+  private isTest(): boolean {
+    return this.loggerName === 'test';
+  }
+
+  public debug(message: string) {
+    this.logger.debug(message);
+    if (this.isTest()) {
+      this.messages.push('[DEBUG] ' + message);
+    }
+  }
+
+  public info(message: string) {
+    this.logger.info(message);
+    if (this.isTest()) {
+      this.messages.push('[INFO] ' + message);
+    }
+  }
+
+  public warn(message: string) {
+    this.logger.warning(message);
+    if (this.isTest()) {
+      this.messages.push('[WARNING] ' + message);
+    }
+  }
+
+  public error(message: string) {
+    this.logger.error(message);
+    if (this.isTest()) {
+      this.messages.push('[ERROR] ' + message);
+    }
+  }
+
+  public getMessages(): string[] {
+    return this.messages;
+  }
+}
+
+export const logger = Logger.getInstance();

+ 48 - 21
src/interfaces/controllers/controller.class.ts

@@ -1,41 +1,68 @@
 import { HTTPStatus } from 'deps';
 import { isSuccessfulStatus } from 'deps';
 
-import { logger } from 'infra/logger.ts';
+import { Logger, logger as defaultLogger } from 'infra/logger.ts';
 
-interface Options {
+export interface ControllerOptions {
   status?: HTTPStatus;
   json?: boolean;
 }
+
+export type ControllerHandler = (
+  req: Request,
+  error?: string,
+  headers?: Record<string, string>,
+) => Response | Promise<Response>;
+
+export type ControllerHandlers = Record<string, ControllerHandler>;
+
 export default class Controller {
   constructor(
-    public handlers: Record<
-      string,
-      (
-        req: Request,
-        error?: string,
-        headers?: Record<string, string>,
-      ) => Response | Promise<Response>
-    >,
+    private handlers: ControllerHandlers = {},
+    private logger: Logger = defaultLogger,
   ) {}
 
-  public static response = (
+  public setHandlers(handlers: ControllerHandlers): void {
+    this.handlers = handlers;
+  }
+
+  public setHandler(
+    path: string,
+    method: string,
+    handler: ControllerHandler,
+  ): void {
+    this.handlers[`${path}_${method}`] = handler;
+  }
+
+  public getHandlers(): ControllerHandlers {
+    return this.handlers;
+  }
+
+  public getHandler(path: string, method: string): ControllerHandler {
+    return this.handlers[`${path}_${method}`];
+  }
+
+  public hasHandler(path: string, method: string): boolean {
+    return `${path}_${method}` in this.handlers;
+  }
+
+  public response = (
     req: Request,
     body: string,
-    options: Options = {},
+    options: ControllerOptions = {},
   ) => {
     const url = new URL(req.url);
     const output = new TextEncoder().encode(body);
     const response = new Response(output, {
       status: options.status ?? HTTPStatus.OK,
     });
-    const userName = this.getUser(req);
+    const userName = Controller.getUser(req);
 
     if (options.json) {
       response.headers.set('Content-Type', 'application/json');
     }
     const userAgent = req.headers.get('User-Agent') ?? '-';
-    const logMessage = this.getLogMessage(
+    const logMessage = Controller.getLogMessage(
       req.method,
       url.pathname,
       response.status,
@@ -44,22 +71,22 @@ export default class Controller {
       userName,
     );
     if (isSuccessfulStatus(response.status)) {
-      if (this.isHealthCheck(userAgent)) {
-        logger.debug(logMessage);
+      if (Controller.isHealthCheck(userAgent)) {
+        this.logger.debug(logMessage);
       } else {
-        logger.info(logMessage);
+        this.logger.info(logMessage);
       }
     } else {
-      logger.error(logMessage);
+      this.logger.error(logMessage);
     }
 
     return response;
   };
 
-  public static responseJSON = (
+  public responseJSON = (
     req: Request,
-    body: unknown,
-    options: Options = {},
+    body: Record<string, unknown>,
+    options: ControllerOptions = {},
   ) => {
     options.json = true;
 

+ 29 - 14
src/interfaces/controllers/errors.controller.ts

@@ -3,25 +3,40 @@ import { HTTPStatus } from 'deps';
 import { logger } from 'infra/logger.ts';
 import Controller from 'if/controllers/controller.class.ts';
 
-const NotFoundController = new Controller({
-  '*_*': (req: Request) => {
-    return Controller.response(req, 'not found', {
+function notFoundController(): Controller {
+  const controller = new Controller();
+
+  controller.setHandler('*', '*', (req: Request) => {
+    return controller.response(req, 'not found', {
       status: HTTPStatus.NotFound,
     });
-  },
-});
+  });
+
+  return controller;
+}
+
+function internalServerErrorController(): Controller {
+  const controller = new Controller();
 
-const InternalServerErrorController = new Controller({
-  '*_*': (req: Request, error?: string) => {
-    logger.error(error);
-    return Controller.response(req, 'internal server error', {
+  controller.setHandler('*', '*', (req: Request, error?: string) => {
+    if (error) {
+      logger.error(error);
+    }
+
+    return controller.response(req, 'internal server error', {
       status: HTTPStatus.InternalServerError,
     });
-  },
-});
+  });
+
+  return controller;
+}
+
+function handle(errorController: Controller, req: Request, error?: string) {
+  return errorController.getHandler('*', '*')(req, error);
+}
 
 export const ControllerErrors = {
-  NotFound: (req: Request) => NotFoundController.handlers['*_*'](req),
-  InternalServerError: (req: Request, error?: string) =>
-    InternalServerErrorController.handlers['*_*'](req, error),
+  NotFound: (req: Request) => handle(notFoundController(), req),
+  InternalServerError: (req: Request, error: string) =>
+    handle(internalServerErrorController(), req, error),
 };

+ 11 - 5
src/interfaces/controllers/health.controller.ts

@@ -1,7 +1,9 @@
 import Controller from 'if/controllers/controller.class.ts';
 
-export default new Controller({
-  '/health_GET': (req: Request) => {
+function HealthController(): Controller {
+  const controller = new Controller();
+
+  controller.setHandler('/health', 'GET', (req: Request) => {
     const url = new URL(req.url);
     const started = Deno.env.get('STARTED');
     let uptime = 0;
@@ -19,6 +21,10 @@ export default new Controller({
       status: 'healthy',
     };
 
-    return Controller.responseJSON(req, body);
-  },
-});
+    return controller.responseJSON(req, body);
+  });
+
+  return controller;
+}
+
+export default HealthController();

+ 11 - 6
src/interfaces/controllers/metrics.controller.ts

@@ -1,9 +1,14 @@
 import Controller from 'if/controllers/controller.class.ts';
 
-export default new Controller({
-  '/metrics_GET': (req: Request) => {
-    const body = Deno.metrics();
+function MetricsController(): Controller {
+  const controller = new Controller();
 
-    return Controller.responseJSON(req, body);
-  },
-});
+  controller.setHandler('/metrics', 'GET', (req: Request) => {
+    const body = { ...Deno.metrics() };
+    return controller.responseJSON(req, body);
+  });
+
+  return controller;
+}
+
+export default MetricsController();

+ 11 - 5
src/interfaces/controllers/root.controller.ts

@@ -1,7 +1,13 @@
 import Controller from 'if/controllers/controller.class.ts';
 
-export default new Controller({
-  '/_GET': (req: Request) => {
-    return Controller.response(req, Deno.env.get('GREETING') ?? '');
-  },
-});
+function MetricsController(): Controller {
+  const controller = new Controller();
+
+  controller.setHandler('/', 'GET', (req: Request) => {
+    return controller.response(req, Deno.env.get('GREETING') ?? '');
+  });
+
+  return controller;
+}
+
+export default MetricsController();

+ 1 - 1
src/interfaces/router.ts

@@ -4,7 +4,7 @@ import { ControllerErrors } from 'if/controllers/errors.controller.ts';
 // Preprocess controllers into a map
 const controllerMap = new Map();
 for (const controller of controllers) {
-  for (const [key, handler] of Object.entries(controller.handlers)) {
+  for (const [key, handler] of Object.entries(controller.getHandlers())) {
     controllerMap.set(key, handler);
   }
 }

+ 74 - 5
test/interfaces/controllers/controller.class.test.ts

@@ -1,5 +1,10 @@
-import { assertEquals } from 'test-deps';
-import Controller from 'if/controllers/controller.class.ts';
+import { assert, assertEquals } from 'test-deps';
+import { HTTPStatus } from 'deps';
+
+import Controller, {
+  ControllerHandler,
+} from 'if/controllers/controller.class.ts';
+import { Logger } from 'infra/logger.ts';
 
 Deno.test('Controller class', async () => {
   const mockHandler = (req: Request) => new Response('Hello, world!');
@@ -8,11 +13,11 @@ Deno.test('Controller class', async () => {
   });
 
   // Test that the handler was stored correctly
-  assertEquals(controller.handlers['/_GET'], mockHandler);
+  assert(controller.hasHandler('/', 'GET'));
 
   // Test the response method
   const req = new Request('http://localhost/');
-  const res = Controller.response(req, 'Hello, world!');
+  const res = controller.response(req, 'Hello, world!');
   assertEquals(res.status, 200);
   assertEquals(
     new TextDecoder().decode(new Uint8Array(await res.arrayBuffer())),
@@ -20,7 +25,71 @@ Deno.test('Controller class', async () => {
   );
 
   // Test the responseJSON method
-  const jsonRes = Controller.responseJSON(req, { message: 'Hello, world!' });
+  const jsonRes = controller.responseJSON(req, { message: 'Hello, world!' });
   assertEquals(jsonRes.status, 200);
   assertEquals(await jsonRes.json(), { message: 'Hello, world!' });
 });
+
+Deno.test('Controller class error handling', async () => {
+  const error = new Error('Test error');
+  const errorHandler = (
+    req: Request,
+    error?: string,
+    headers?: Record<string, string>,
+  ): Response => {
+    return controller.response(req, error ?? 'Unknown error', {
+      status: HTTPStatus.InternalServerError,
+    });
+  };
+
+  const controller = new Controller({
+    '/_GET': errorHandler,
+  });
+
+  // Test that the handler was stored correctly
+  assert(controller.hasHandler('/', 'GET'));
+
+  // Test the error response method
+  const req = new Request('http://localhost/');
+  const handler = controller.getHandler('/', 'GET');
+  if (handler) {
+    const res = await handler(req, error.message);
+    assertEquals(res.status, HTTPStatus.InternalServerError);
+    assertEquals(
+      new TextDecoder().decode(new Uint8Array(await res.arrayBuffer())),
+      error.message,
+    );
+  } else {
+    assert(false, 'Handler not found');
+  }
+});
+
+Deno.test({
+  name: 'Controller should log debug message for health check user agent',
+  fn: async () => {
+    let logger = Logger.getInstance('test');
+
+    const userAgent = Deno.env.get('DOCKER_USER_AGENT') ?? '';
+    console.log(userAgent);
+    const req = new Request('http://localhost', {
+      method: 'GET',
+      headers: new Headers({
+        'User-Agent': userAgent,
+      }),
+    });
+    const handler: ControllerHandler = (req: Request) => {
+      return controller.response(req, 'Hello, World!');
+    };
+    const controller = new Controller({ '/_GET': handler }, logger);
+    const response = await controller.getHandler('/', 'GET')(req);
+
+    // Check if the expected debug message was logged
+    const debugMessage = logger.getMessages().find((message) =>
+      message.startsWith('[DEBUG] ') && message.endsWith(`(${userAgent})`)
+    );
+
+    assert(debugMessage !== undefined, 'Expected debug message was not logged');
+
+    assertEquals(response.status, 200);
+  },
+});