diff --git a/src/behaviours/resolve-system-proxy/resolve-system-proxy.test.ts b/src/behaviours/resolve-system-proxy/resolve-system-proxy.test.ts new file mode 100644 index 0000000000..a78aa0dd68 --- /dev/null +++ b/src/behaviours/resolve-system-proxy/resolve-system-proxy.test.ts @@ -0,0 +1,85 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import type { AsyncFnMock } from "@async-fn/jest"; +import asyncFn from "@async-fn/jest"; +import type { ApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder"; +import { getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder"; +import type { ResolveSystemProxy } from "../../common/utils/resolve-system-proxy/resolve-system-proxy-injection-token"; +import { resolveSystemProxyInjectionToken } from "../../common/utils/resolve-system-proxy/resolve-system-proxy-injection-token"; +import resolveSystemProxyFromElectronInjectable from "../../main/utils/resolve-system-proxy/resolve-system-proxy-from-electron.injectable"; +import { getPromiseStatus } from "../../common/test-utils/get-promise-status"; + +describe("resolve-system-proxy", () => { + let applicationBuilder: ApplicationBuilder; + let actualPromise: Promise; + let resolveSystemProxyFromElectronMock: AsyncFnMock; + + beforeEach(async () => { + applicationBuilder = getApplicationBuilder(); + + resolveSystemProxyFromElectronMock = asyncFn(); + + applicationBuilder.beforeApplicationStart(({ mainDi }) => { + mainDi.override( + resolveSystemProxyFromElectronInjectable, + () => resolveSystemProxyFromElectronMock, + ); + }); + + await applicationBuilder.render(); + }); + + describe("given in main, when called with URL", () => { + beforeEach(async () => { + const resolveSystemProxyInMain = applicationBuilder.dis.mainDi.inject( + resolveSystemProxyInjectionToken, + ); + + actualPromise = resolveSystemProxyInMain("some-url"); + }); + + it("calls for proxy of the URL from Electron", () => { + expect(resolveSystemProxyFromElectronMock).toHaveBeenCalledWith("some-url"); + }); + + it("does not resolve yet", async () => { + const promiseStatus = await getPromiseStatus(actualPromise); + + expect(promiseStatus.fulfilled).toBe(false); + }); + + it("when the call for proxy resolves, resolves with the proxy", async () => { + resolveSystemProxyFromElectronMock.resolve("some-proxy"); + + expect(await actualPromise).toBe("some-proxy"); + }); + }); + + describe("given in renderer, when called with URL", () => { + beforeEach(async () => { + const resolveSystemProxyInRenderer = applicationBuilder.dis.rendererDi.inject( + resolveSystemProxyInjectionToken, + ); + + actualPromise = resolveSystemProxyInRenderer("some-url"); + }); + + it("calls for proxy of the URL from Electron", () => { + expect(resolveSystemProxyFromElectronMock).toHaveBeenCalledWith("some-url"); + }); + + it("does not resolve yet", async () => { + const promiseStatus = await getPromiseStatus(actualPromise); + + expect(promiseStatus.fulfilled).toBe(false); + }); + + it("when the call for proxy resolves, resolves with the proxy", async () => { + resolveSystemProxyFromElectronMock.resolve("some-proxy"); + + expect(await actualPromise).toBe("some-proxy"); + }); + }); +}); diff --git a/src/common/log-error.injectable.ts b/src/common/log-error.injectable.ts new file mode 100644 index 0000000000..4fab2cd546 --- /dev/null +++ b/src/common/log-error.injectable.ts @@ -0,0 +1,13 @@ +/** + * 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 logErrorInjectable = getInjectable({ + id: "log-error", + instantiate: (di) => di.inject(loggerInjectable).error, +}); + +export default logErrorInjectable; diff --git a/src/common/utils/resolve-system-proxy/resolve-system-proxy-channel.injectable.ts b/src/common/utils/resolve-system-proxy/resolve-system-proxy-channel.injectable.ts new file mode 100644 index 0000000000..5dd17ddc6a --- /dev/null +++ b/src/common/utils/resolve-system-proxy/resolve-system-proxy-channel.injectable.ts @@ -0,0 +1,21 @@ +/** + * 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 type { RequestChannel } from "../channel/request-channel-injection-token"; +import { requestChannelInjectionToken } from "../channel/request-channel-injection-token"; + +export type ResolveSystemProxyChannel = RequestChannel; + +const resolveSystemProxyChannelInjectable = getInjectable({ + id: "resolve-system-proxy-channel", + + instantiate: (): ResolveSystemProxyChannel => ({ + id: "resolve-system-proxy-channel", + }), + + injectionToken: requestChannelInjectionToken, +}); + +export default resolveSystemProxyChannelInjectable; diff --git a/src/common/utils/resolve-system-proxy/resolve-system-proxy-injection-token.ts b/src/common/utils/resolve-system-proxy/resolve-system-proxy-injection-token.ts new file mode 100644 index 0000000000..616718ea98 --- /dev/null +++ b/src/common/utils/resolve-system-proxy/resolve-system-proxy-injection-token.ts @@ -0,0 +1,12 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import { getInjectionToken } from "@ogre-tools/injectable"; + +export type ResolveSystemProxy = (url: string) => Promise; + +export const resolveSystemProxyInjectionToken = getInjectionToken({ + id: "resolve-system-proxy", +}); diff --git a/src/common/utils/with-error-logging/with-error-logging.injectable.ts b/src/common/utils/with-error-logging/with-error-logging.injectable.ts index 12b48c6204..0aaca9f97d 100644 --- a/src/common/utils/with-error-logging/with-error-logging.injectable.ts +++ b/src/common/utils/with-error-logging/with-error-logging.injectable.ts @@ -3,7 +3,7 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { getInjectable } from "@ogre-tools/injectable"; -import loggerInjectable from "../../logger.injectable"; +import logErrorInjectable from "../../log-error.injectable"; import { isPromise } from "../is-promise/is-promise"; export type WithErrorLoggingFor = ( @@ -16,30 +16,34 @@ const withErrorLoggingInjectable = getInjectable({ id: "with-error-logging", instantiate: (di): WithErrorLoggingFor => { - const logger = di.inject(loggerInjectable); + const logError = di.inject(logErrorInjectable); return (getErrorMessage) => (toBeDecorated) => (...args) => { + let returnValue: ReturnType; + try { - const returnValue = toBeDecorated(...args); - - if (isPromise(returnValue)) { - returnValue.catch((e) => { - const errorMessage = getErrorMessage(e); - - logger.error(errorMessage, e); - }); - } - - return returnValue; + returnValue = toBeDecorated(...args); } catch (e) { const errorMessage = getErrorMessage(e); - logger.error(errorMessage, e); + logError(errorMessage, e); throw e; } + + if (isPromise(returnValue)) { + return returnValue.catch((e: unknown) => { + const errorMessage = getErrorMessage(e); + + logError(errorMessage, e); + + throw e; + }); + } + + return returnValue; }; }, }); diff --git a/src/common/utils/with-error-logging/with-error-logging.test.ts b/src/common/utils/with-error-logging/with-error-logging.test.ts index 533374d9ad..05526c64c9 100644 --- a/src/common/utils/with-error-logging/with-error-logging.test.ts +++ b/src/common/utils/with-error-logging/with-error-logging.test.ts @@ -163,23 +163,24 @@ describe("with-error-logging", () => { }); describe("when call rejects with error instance", () => { - let error: Error; - - beforeEach(async () => { - try { - await toBeDecorated.reject(new Error("some-error")); - await returnValuePromise; - } catch (e) { - error = e as Error; - } + beforeEach(() => { + toBeDecorated.reject(new Error("some-error")); }); - it("logs the error", () => { + it("logs the error", async () => { + let error: unknown; + + try { + await returnValuePromise; + } catch (e) { + error = e; + } + expect(loggerStub.error).toHaveBeenCalledWith("some-error-message-for-some-error", error); }); it("rejects", () => { - return expect(() => returnValuePromise).rejects.toThrow("some-error"); + return expect(returnValuePromise).rejects.toThrow("some-error"); }); }); @@ -187,8 +188,9 @@ describe("with-error-logging", () => { let error: unknown; beforeEach(async () => { + toBeDecorated.reject({ someProperty: "some-rejection" }); + try { - await toBeDecorated.reject({ someProperty: "some-rejection" }); await returnValuePromise; } catch (e) { error = e; @@ -203,7 +205,7 @@ describe("with-error-logging", () => { }); it("rejects", () => { - return expect(() => returnValuePromise).rejects.toEqual({ someProperty: "some-rejection" }); + return expect(returnValuePromise).rejects.toEqual({ someProperty: "some-rejection" }); }); }); diff --git a/src/extensions/common-api/index.ts b/src/extensions/common-api/index.ts index e3c789c864..0d07220d1d 100644 --- a/src/extensions/common-api/index.ts +++ b/src/extensions/common-api/index.ts @@ -10,6 +10,8 @@ import * as Store from "./stores"; import * as Util from "./utils"; import * as Catalog from "./catalog"; import * as Types from "./types"; +import * as Proxy from "./proxy"; + import logger from "../../common/logger"; export { @@ -20,4 +22,5 @@ export { Types, Util, logger, + Proxy, }; diff --git a/src/extensions/common-api/proxy.ts b/src/extensions/common-api/proxy.ts new file mode 100644 index 0000000000..bf0cd8e626 --- /dev/null +++ b/src/extensions/common-api/proxy.ts @@ -0,0 +1,14 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import { asLegacyGlobalFunctionForExtensionApi } from "../as-legacy-globals-for-extension-api/as-legacy-global-function-for-extension-api"; +import { resolveSystemProxyInjectionToken } from "../../common/utils/resolve-system-proxy/resolve-system-proxy-injection-token"; + +/** + * Resolves URL-specific proxy information from system. See more here: https://www.electronjs.org/docs/latest/api/session#sesresolveproxyurl + * @param url - The URL for proxy information + * @returns Promise for proxy information as string + */ +export const resolveSystemProxy = asLegacyGlobalFunctionForExtensionApi(resolveSystemProxyInjectionToken); diff --git a/src/main/getDiForUnitTesting.ts b/src/main/getDiForUnitTesting.ts index f17a4e6217..36713d852d 100644 --- a/src/main/getDiForUnitTesting.ts +++ b/src/main/getDiForUnitTesting.ts @@ -99,6 +99,7 @@ import rollbackHelmReleaseInjectable from "./helm/helm-service/rollback-helm-rel import updateHelmReleaseInjectable from "./helm/helm-service/update-helm-release.injectable"; import waitUntilBundledExtensionsAreLoadedInjectable from "./start-main-application/lens-window/application-window/wait-until-bundled-extensions-are-loaded.injectable"; import { registerMobX } from "@ogre-tools/injectable-extension-for-mobx"; +import electronInjectable from "./utils/resolve-system-proxy/electron.injectable"; export function getDiForUnitTesting(opts: { doGeneralOverrides?: boolean } = {}) { const { @@ -124,6 +125,7 @@ export function getDiForUnitTesting(opts: { doGeneralOverrides?: boolean } = {}) di.preventSideEffects(); if (doGeneralOverrides) { + di.override(electronInjectable, () => ({})); di.override(waitUntilBundledExtensionsAreLoadedInjectable, () => async () => {}); di.override(getRandomIdInjectable, () => () => "some-irrelevant-random-id"); di.override(hotbarStoreInjectable, () => ({ load: () => {} })); diff --git a/src/main/utils/resolve-system-proxy/electron.injectable.ts b/src/main/utils/resolve-system-proxy/electron.injectable.ts new file mode 100644 index 0000000000..a5999c9e59 --- /dev/null +++ b/src/main/utils/resolve-system-proxy/electron.injectable.ts @@ -0,0 +1,14 @@ +/** + * 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 electron from "electron"; + +const electronInjectable = getInjectable({ + id: "electron", + instantiate: () => electron, + causesSideEffects: true, +}); + +export default electronInjectable; diff --git a/src/main/utils/resolve-system-proxy/resolve-system-proxy-channel-responder.injectable.ts b/src/main/utils/resolve-system-proxy/resolve-system-proxy-channel-responder.injectable.ts new file mode 100644 index 0000000000..c7a1747770 --- /dev/null +++ b/src/main/utils/resolve-system-proxy/resolve-system-proxy-channel-responder.injectable.ts @@ -0,0 +1,21 @@ +/** + * 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 resolveSystemProxyChannelInjectable from "../../../common/utils/resolve-system-proxy/resolve-system-proxy-channel.injectable"; +import resolveSystemProxyInjectable from "./resolve-system-proxy.injectable"; +import { requestChannelListenerInjectionToken } from "../../../common/utils/channel/request-channel-listener-injection-token"; + +const resolveSystemProxyChannelResponderInjectable = getInjectable({ + id: "resolve-system-proxy-channel-responder", + + instantiate: (di) => ({ + channel: di.inject(resolveSystemProxyChannelInjectable), + handler: di.inject(resolveSystemProxyInjectable), + }), + + injectionToken: requestChannelListenerInjectionToken, +}); + +export default resolveSystemProxyChannelResponderInjectable; diff --git a/src/main/utils/resolve-system-proxy/resolve-system-proxy-from-electron.injectable.ts b/src/main/utils/resolve-system-proxy/resolve-system-proxy-from-electron.injectable.ts new file mode 100644 index 0000000000..c663d59fde --- /dev/null +++ b/src/main/utils/resolve-system-proxy/resolve-system-proxy-from-electron.injectable.ts @@ -0,0 +1,32 @@ +/** + * 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 electronInjectable from "./electron.injectable"; +import withErrorLoggingInjectable from "../../../common/utils/with-error-logging/with-error-logging.injectable"; + +const resolveSystemProxyFromElectronInjectable = getInjectable({ + id: "resolve-system-proxy-from-electron", + + instantiate: (di) => { + const electron = di.inject(electronInjectable); + const withErrorLoggingFor = di.inject(withErrorLoggingInjectable); + + const withErrorLogging = withErrorLoggingFor(() => "Error resolving proxy"); + + return withErrorLogging(async (url: string) => { + const webContent = electron.webContents + .getAllWebContents() + .find((x) => !x.isDestroyed()); + + if (!webContent) { + throw new Error(`Tried to resolve proxy for "${url}", but no browser window was available`); + } + + return await webContent.session.resolveProxy(url); + }); + }, +}); + +export default resolveSystemProxyFromElectronInjectable; diff --git a/src/main/utils/resolve-system-proxy/resolve-system-proxy-from-electron.test.ts b/src/main/utils/resolve-system-proxy/resolve-system-proxy-from-electron.test.ts new file mode 100644 index 0000000000..9c395dca5b --- /dev/null +++ b/src/main/utils/resolve-system-proxy/resolve-system-proxy-from-electron.test.ts @@ -0,0 +1,138 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import { getDiForUnitTesting } from "../../getDiForUnitTesting"; +import resolveSystemProxyFromElectronInjectable from "./resolve-system-proxy-from-electron.injectable"; +import electronInjectable from "./electron.injectable"; +import type { AsyncFnMock } from "@async-fn/jest"; +import asyncFn from "@async-fn/jest"; +import type electron from "electron"; +import { getPromiseStatus } from "../../../common/test-utils/get-promise-status"; +import logErrorInjectable from "../../../common/log-error.injectable"; +import type { DiContainer } from "@ogre-tools/injectable"; + +describe("technical: resolve-system-proxy-from-electron", () => { + let resolveSystemProxyMock: AsyncFnMock<(url: string) => Promise>; + let logErrorMock: jest.Mock; + let di: DiContainer; + let actualPromise: Promise; + + beforeEach(() => { + di = getDiForUnitTesting(); + + logErrorMock = jest.fn(); + di.override(logErrorInjectable, () => logErrorMock); + }); + + describe("given there are non-destroyed Lens windows, when called with URL", () => { + beforeEach(() => { + resolveSystemProxyMock = asyncFn(); + + di.override( + electronInjectable, + + () => + ({ + webContents: { + getAllWebContents: () => [ + { + isDestroyed: () => true, + + session: { + resolveProxy: () => { + throw new Error("should never come here"); + }, + }, + }, + + { + isDestroyed: () => false, + session: { resolveProxy: resolveSystemProxyMock }, + }, + + { + isDestroyed: () => false, + + session: { + resolveProxy: () => { + throw new Error("should never come here"); + }, + }, + }, + ], + }, + } as unknown as typeof electron), + ); + + const resolveSystemProxyFromElectron = di.inject( + resolveSystemProxyFromElectronInjectable, + ); + + actualPromise = resolveSystemProxyFromElectron("some-url"); + }); + + it("calls to resolve proxy from the first window", () => { + expect(resolveSystemProxyMock).toHaveBeenCalledWith("some-url"); + }); + + it("does not resolve yet", async () => { + const promiseStatus = await getPromiseStatus(actualPromise); + + expect(promiseStatus.fulfilled).toBe(false); + }); + + it("when call for proxy, resolves with the proxy", async () => { + resolveSystemProxyMock.resolve("some-proxy"); + + expect(await actualPromise).toBe("some-proxy"); + }); + }); + + describe("given there are only destroyed Lens windows, when called with URL", () => { + let error: any; + + beforeEach(async () => { + di.override( + electronInjectable, + () => + ({ + webContents: { + getAllWebContents: () => [ + { + isDestroyed: () => true, + + session: { + resolveProxy: () => { + throw new Error("should never come here"); + }, + }, + }, + ], + }, + } as unknown as typeof electron), + ); + + resolveSystemProxyMock = asyncFn(); + + const resolveSystemProxyFromElectron = di.inject( + resolveSystemProxyFromElectronInjectable, + ); + + try { + await resolveSystemProxyFromElectron("some-url"); + } catch (e) { + error = e; + } + }); + + it("throws error", () => { + expect(error.message).toBe('Tried to resolve proxy for "some-url", but no browser window was available'); + }); + + it("logs error", () => { + expect(logErrorMock).toHaveBeenCalledWith("Error resolving proxy", error); + }); + }); +}); diff --git a/src/main/utils/resolve-system-proxy/resolve-system-proxy.injectable.ts b/src/main/utils/resolve-system-proxy/resolve-system-proxy.injectable.ts new file mode 100644 index 0000000000..c683217916 --- /dev/null +++ b/src/main/utils/resolve-system-proxy/resolve-system-proxy.injectable.ts @@ -0,0 +1,21 @@ +/** + * 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 { resolveSystemProxyInjectionToken } from "../../../common/utils/resolve-system-proxy/resolve-system-proxy-injection-token"; +import resolveSystemProxyFromElectronInjectable from "./resolve-system-proxy-from-electron.injectable"; + +const resolveSystemProxyInjectable = getInjectable({ + id: "resolve-system-proxy-for-main", + + instantiate: (di) => { + const resolveSystemProxyFromElectron = di.inject(resolveSystemProxyFromElectronInjectable); + + return (url) => resolveSystemProxyFromElectron(url); + }, + + injectionToken: resolveSystemProxyInjectionToken, +}); + +export default resolveSystemProxyInjectable; diff --git a/src/renderer/utils/resolve-proxy/resolve-system-proxy.injectable.ts b/src/renderer/utils/resolve-proxy/resolve-system-proxy.injectable.ts new file mode 100644 index 0000000000..8fcb9063d0 --- /dev/null +++ b/src/renderer/utils/resolve-proxy/resolve-system-proxy.injectable.ts @@ -0,0 +1,23 @@ +/** + * 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 { resolveSystemProxyInjectionToken } from "../../../common/utils/resolve-system-proxy/resolve-system-proxy-injection-token"; +import requestFromChannelInjectable from "../channel/request-from-channel.injectable"; +import resolveSystemProxyChannelInjectable from "../../../common/utils/resolve-system-proxy/resolve-system-proxy-channel.injectable"; + +const resolveSystemProxyInjectable = getInjectable({ + id: "resolve-system-proxy-for-renderer", + + instantiate: (di) => { + const requestFromChannel = di.inject(requestFromChannelInjectable); + const resolveSystemProxyChannel = di.inject(resolveSystemProxyChannelInjectable); + + return async (url) => requestFromChannel(resolveSystemProxyChannel, url); + }, + + injectionToken: resolveSystemProxyInjectionToken, +}); + +export default resolveSystemProxyInjectable;