Ctrl K
ComunidadGuías…

server/onerror.test.ts

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
// @vitest-isolate true
import zlib from "node:zlib";
import { errors as formidableErrors } from "formidable";
import type Koa from "koa";
import { DatabaseError } from "sequelize";
import type { Mock } from "vitest";
import { requestErrorHandler } from "@server/logging/sentry";
import { InternalError, ValidationError, NotFoundError } from "./errors";
import onerror from "./onerror";

// Mock the requestErrorHandler from Sentry
vi.mock("@server/logging/sentry", () => ({
  requestErrorHandler: vi.fn(),
}));

type MockCtx = {
  headers: Record<string, string>;
  headerSent: boolean;
  writable: boolean;
  accepts: Mock;
  set: Mock;
  res: { end: Mock; statusCode: number };
  status: number | undefined;
  type: string | undefined;
  body: unknown;
};

type ReportableError = Error & {
  status?: number;
  isReportable?: boolean;
};

describe("onerror", () => {
  let app: Koa;
  let ctx: MockCtx;

  beforeEach(() => {
    // Create a mock Koa app
    app = {
      context: {},
    } as unknown as Koa;

    // Apply the onerror middleware
    onerror(app);

    // Create a mock context
    ctx = {
      headers: {},
      headerSent: false,
      writable: true,
      accepts: vi.fn(() => "json"),
      set: vi.fn(),
      res: {
        end: vi.fn(),
        statusCode: 404,
      },
      status: undefined,
      type: undefined,
      body: undefined,
    };

    // Clear mock calls
    (requestErrorHandler as Mock).mockClear();
  });

  it("should report InternalError to Sentry", () => {
    const error = InternalError("Test internal error");

    app.context.onerror.call(ctx, error);

    expect(requestErrorHandler).toHaveBeenCalledWith(error, ctx);
    expect(ctx.status).toBe(500);
  });

  it("should not report ValidationError to Sentry", () => {
    const error = ValidationError("Test validation error");

    app.context.onerror.call(ctx, error);

    expect(requestErrorHandler).not.toHaveBeenCalled();
    expect(ctx.status).toBe(400);
  });

  it("should not report NotFoundError to Sentry", () => {
    const error = NotFoundError("Test not found error");

    app.context.onerror.call(ctx, error);

    expect(requestErrorHandler).not.toHaveBeenCalled();
    expect(ctx.status).toBe(404);
  });

  it("should report unknown errors without isReportable property to Sentry", () => {
    const error = new Error("Unknown error") as ReportableError;
    error.status = 500;

    app.context.onerror.call(ctx, error);

    expect(requestErrorHandler).toHaveBeenCalledWith(error, ctx);
  });

  it("should report errors with invalid status codes to Sentry", () => {
    const error = new Error("Invalid status error") as ReportableError;
    error.status = 999;

    app.context.onerror.call(ctx, error);

    expect(requestErrorHandler).toHaveBeenCalledWith(error, ctx);
  });

  it("should not report errors explicitly marked with isReportable: false", () => {
    const error = new Error("Custom error") as ReportableError;
    error.status = 500;
    error.isReportable = false;

    app.context.onerror.call(ctx, error);

    expect(requestErrorHandler).not.toHaveBeenCalled();
  });

  it("should convert a canceled query into a service unavailable error", () => {
    const parent = new Error("canceling statement due to statement timeout");
    Object.assign(parent, { code: "57014", sql: "SELECT 1" });
    const error = new DatabaseError(parent as never);

    app.context.onerror.call(ctx, error);

    expect(requestErrorHandler).toHaveBeenCalled();
    expect(ctx.status).toBe(503);
    expect(ctx.body).toContain("request_timeout");
  });

  it("should convert an unreadable request stream into a client closed request", () => {
    const error = new Error("stream is not readable") as ReportableError & {
      type?: string;
    };
    error.status = 500;
    error.type = "stream.not.readable";

    app.context.onerror.call(ctx, error);

    expect(requestErrorHandler).not.toHaveBeenCalled();
    expect(ctx.status).toBe(499);
    expect(ctx.body).toContain("client_closed_request");
  });

  it.each([
    ["br", () => zlib.brotliDecompressSync(Buffer.from("not brotli"))],
    ["gzip", () => zlib.gunzipSync(Buffer.from("not gzip"))],
    ["gzip", () => zlib.gunzipSync(zlib.gzipSync("truncated").subarray(0, 10))],
  ])(
    "should convert a %s decompression error into an invalid request",
    (_encoding, decompress) => {
      app.context.onerror.call(ctx, captureError(decompress));

      expect(requestErrorHandler).not.toHaveBeenCalled();
      expect(ctx.status).toBe(400);
      expect(ctx.body).toContain("invalid_request");
    }
  );

  it("should convert an aborted multipart upload into a client closed request", () => {
    const error = new formidableErrors.default(
      "Request aborted",
      formidableErrors.aborted
    );

    app.context.onerror.call(ctx, error);

    expect(requestErrorHandler).not.toHaveBeenCalled();
    expect(ctx.status).toBe(499);
    expect(ctx.body).toContain("client_closed_request");
  });

  it("should set the response status code when the response is not writable", () => {
    ctx.writable = false;

    app.context.onerror.call(ctx, InternalError("Test internal error"));

    expect(ctx.res.statusCode).toBe(500);
    expect(ctx.res.end).not.toHaveBeenCalled();
  });

  it("should not throw when the response is unavailable", () => {
    ctx.writable = false;
    Reflect.deleteProperty(ctx, "res");

    expect(() =>
      app.context.onerror.call(ctx, InternalError("Test internal error"))
    ).not.toThrow();
  });

  it("should report errors explicitly marked with isReportable: true", () => {
    const error = new Error("Custom error") as ReportableError;
    error.status = 400;
    error.isReportable = true;

    app.context.onerror.call(ctx, error);

    expect(requestErrorHandler).toHaveBeenCalledWith(error, ctx);
  });
});

function captureError(fn: () => unknown): Error {
  try {
    fn();
  } catch (err) {
    if (err instanceof Error) {
      return err;
    }
  }
  throw new Error("Expected function to throw");
}