mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Introduce higher order function to log errors in decorated functions
Co-authored-by: Janne Savolainen <janne.savolainen@live.fi> Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>
This commit is contained in:
parent
ec2d2056cb
commit
06851d9961
@ -0,0 +1,39 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||||
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
|
*/
|
||||||
|
import { getInjectable } from "@ogre-tools/injectable";
|
||||||
|
import loggerInjectable from "../../logger.injectable";
|
||||||
|
|
||||||
|
const withErrorLoggingInjectable = getInjectable({
|
||||||
|
id: "with-error-logging",
|
||||||
|
|
||||||
|
instantiate: (di) => {
|
||||||
|
const logger = di.inject(loggerInjectable);
|
||||||
|
|
||||||
|
return (getErrorMessage: (error: Error) => string) =>
|
||||||
|
<T extends (...args: any[]) => any>(toBeDecorated: T) =>
|
||||||
|
(...args: Parameters<T>): ReturnType<T> => {
|
||||||
|
try {
|
||||||
|
const returnValue = toBeDecorated(...args);
|
||||||
|
|
||||||
|
if (isPromise(returnValue)) {
|
||||||
|
returnValue.catch((e: Error) => {
|
||||||
|
logger.error(getErrorMessage(e as Error), e);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return returnValue;
|
||||||
|
} catch (e) {
|
||||||
|
logger.error(getErrorMessage(e as Error), e);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default withErrorLoggingInjectable;
|
||||||
|
|
||||||
|
function isPromise(reference: any): reference is Promise<any> {
|
||||||
|
return !!reference?.then;
|
||||||
|
}
|
||||||
215
src/common/utils/with-error-logging/with-error-logging.test.ts
Normal file
215
src/common/utils/with-error-logging/with-error-logging.test.ts
Normal file
@ -0,0 +1,215 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||||
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { getDiForUnitTesting } from "../../../main/getDiForUnitTesting";
|
||||||
|
import loggerInjectable from "../../logger.injectable";
|
||||||
|
import type { Logger } from "../../logger";
|
||||||
|
import withErrorLoggingInjectable from "./with-error-logging.injectable";
|
||||||
|
import { pipeline } from "@ogre-tools/fp";
|
||||||
|
import type { AsyncFnMock } from "@async-fn/jest";
|
||||||
|
import asyncFn from "@async-fn/jest";
|
||||||
|
import { getPromiseStatus } from "../../test-utils/get-promise-status";
|
||||||
|
|
||||||
|
describe("with-error-logging", () => {
|
||||||
|
describe("given decorated sync function", () => {
|
||||||
|
let loggerStub: Logger;
|
||||||
|
let toBeDecorated: jest.Mock<number | undefined, [string, string]>;
|
||||||
|
let decorated: (a: string, b: string) => number | undefined;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
const di = getDiForUnitTesting();
|
||||||
|
|
||||||
|
loggerStub = {
|
||||||
|
error: jest.fn(),
|
||||||
|
} as unknown as Logger;
|
||||||
|
|
||||||
|
di.override(loggerInjectable, () => loggerStub);
|
||||||
|
|
||||||
|
const withErrorLoggingFor = di.inject(withErrorLoggingInjectable);
|
||||||
|
|
||||||
|
toBeDecorated = jest.fn();
|
||||||
|
|
||||||
|
decorated = pipeline(
|
||||||
|
toBeDecorated,
|
||||||
|
withErrorLoggingFor((error: Error) => `some-error-message-for-${error.message}`),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when function does not throw and returns value", () => {
|
||||||
|
let returnValue: number | undefined;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
// eslint-disable-next-line unused-imports/no-unused-vars-ts
|
||||||
|
toBeDecorated.mockImplementation((_, __) => 42);
|
||||||
|
|
||||||
|
returnValue = decorated("some-parameter", "some-other-parameter");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("passes arguments to decorated function", () => {
|
||||||
|
expect(toBeDecorated).toHaveBeenCalledWith("some-parameter", "some-other-parameter");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not log error", () => {
|
||||||
|
expect(loggerStub.error).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns the value", () => {
|
||||||
|
expect(returnValue).toBe(42);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when function does not throw and returns no value", () => {
|
||||||
|
let returnValue: number | undefined;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
// eslint-disable-next-line unused-imports/no-unused-vars-ts
|
||||||
|
toBeDecorated.mockImplementation((_, __) => undefined);
|
||||||
|
|
||||||
|
returnValue = decorated("some-parameter", "some-other-parameter");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("passes arguments to decorated function", () => {
|
||||||
|
expect(toBeDecorated).toHaveBeenCalledWith("some-parameter", "some-other-parameter");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not log error", () => {
|
||||||
|
expect(loggerStub.error).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns nothing", () => {
|
||||||
|
expect(returnValue).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when function throws", () => {
|
||||||
|
let error: Error;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
// eslint-disable-next-line unused-imports/no-unused-vars-ts
|
||||||
|
toBeDecorated.mockImplementation((_, __) => {
|
||||||
|
throw new Error("some-error");
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
decorated("some-parameter", "some-other-parameter");
|
||||||
|
} catch (e: any) {
|
||||||
|
error = e;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("passes arguments to decorated function", () => {
|
||||||
|
expect(toBeDecorated).toHaveBeenCalledWith("some-parameter", "some-other-parameter");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("logs the error", () => {
|
||||||
|
expect(loggerStub.error).toHaveBeenCalledWith("some-error-message-for-some-error", error);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("throws", () => {
|
||||||
|
expect(error.message).toBe("some-error");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("given decorated async function", () => {
|
||||||
|
let loggerStub: Logger;
|
||||||
|
let decorated: (a: string, b: string) => Promise<number | undefined>;
|
||||||
|
let toBeDecorated: AsyncFnMock<typeof decorated>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
const di = getDiForUnitTesting();
|
||||||
|
|
||||||
|
loggerStub = {
|
||||||
|
error: jest.fn(),
|
||||||
|
} as unknown as Logger;
|
||||||
|
|
||||||
|
di.override(loggerInjectable, () => loggerStub);
|
||||||
|
|
||||||
|
const withErrorLoggingFor = di.inject(withErrorLoggingInjectable);
|
||||||
|
|
||||||
|
toBeDecorated = asyncFn();
|
||||||
|
|
||||||
|
decorated = pipeline(
|
||||||
|
toBeDecorated,
|
||||||
|
withErrorLoggingFor((error: Error) => `some-error-message-for-${error.message}`),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when called", () => {
|
||||||
|
let returnValuePromise: Promise<number | undefined>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
returnValuePromise = decorated("some-parameter", "some-other-parameter");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("passes arguments to decorated function", () => {
|
||||||
|
expect(toBeDecorated).toHaveBeenCalledWith("some-parameter", "some-other-parameter");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not log error yet", () => {
|
||||||
|
expect(loggerStub.error).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not resolve yet", async () => {
|
||||||
|
const promiseStatus = await getPromiseStatus(returnValuePromise);
|
||||||
|
|
||||||
|
expect(promiseStatus.fulfilled).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when call rejects", () => {
|
||||||
|
let error: Error;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
try {
|
||||||
|
await toBeDecorated.reject(new Error("some-error"));
|
||||||
|
await returnValuePromise;
|
||||||
|
} catch (e) {
|
||||||
|
error = e as Error;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("logs the error", () => {
|
||||||
|
expect(loggerStub.error).toHaveBeenCalledWith("some-error-message-for-some-error", error);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects", () => {
|
||||||
|
return expect(() => returnValuePromise).rejects.toThrow("some-error");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when call resolves with value", () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await toBeDecorated.resolve(42);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not log error", () => {
|
||||||
|
expect(loggerStub.error).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("resolves with the value", async () => {
|
||||||
|
const returnValue = await returnValuePromise;
|
||||||
|
|
||||||
|
expect(returnValue).toBe(42);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when call resolves without value", () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await toBeDecorated.resolve(undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not log error", () => {
|
||||||
|
expect(loggerStub.error).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("resolves with the value", async () => {
|
||||||
|
const returnValue = await returnValuePromise;
|
||||||
|
|
||||||
|
expect(returnValue).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue
Block a user