From c37400871a911180b6264a0037ef5760a1db65e5 Mon Sep 17 00:00:00 2001 From: Ben Loe Date: Fri, 21 Jun 2024 14:25:03 -0400 Subject: [PATCH] remove expectContext check and add tests for launchAuthIntegration. Also some refactoring to help with testing --- src/auth/authStorage.ts | 11 +- .../getPartnerPrincipals.test.ts | 3 +- .../launchAuthIntegration.test.ts | 268 +++++++++++++++++- .../launchAuthIntegration.ts | 11 +- src/background/refreshToken.test.ts | 10 +- .../google/sheets/core/sheetsApi.test.ts | 47 ++- src/data/service/apiClient.ts | 11 - src/hooks/auth.ts | 2 +- src/integrations/locator.ts | 5 +- src/integrations/registry.ts | 26 +- .../util/readRawConfigurations.ts | 39 +++ src/testUtils/testAfterEnv.ts | 26 ++ 12 files changed, 377 insertions(+), 82 deletions(-) create mode 100644 src/integrations/util/readRawConfigurations.ts diff --git a/src/auth/authStorage.ts b/src/auth/authStorage.ts index d18d5c7223..7e1ac73e54 100644 --- a/src/auth/authStorage.ts +++ b/src/auth/authStorage.ts @@ -125,6 +125,10 @@ export async function setPartnerAuthData(data: PartnerAuthData): Promise { return partnerTokenStorage.set(data); } +export async function removeOAuth2Token(token: string) { + await chromeP.identity.removeCachedAuthToken({ token }); +} + /** * Clear all partner OAuth2 tokens and reset api query caches */ @@ -137,9 +141,10 @@ export async function clearPartnerAuthData(): Promise { console.debug( "Clearing partner auth for authId: " + partnerAuthData.authId, ); - await chromeP.identity.removeCachedAuthToken({ - token: partnerAuthData.token, - }); + await removeOAuth2Token(partnerAuthData.token); + // await chromeP.identity.removeCachedAuthToken({ + // token: partnerAuthData.token, + // }); } await partnerTokenStorage.remove(); diff --git a/src/background/auth/partnerIntegrations/getPartnerPrincipals.test.ts b/src/background/auth/partnerIntegrations/getPartnerPrincipals.test.ts index d6486d0662..4d860bab92 100644 --- a/src/background/auth/partnerIntegrations/getPartnerPrincipals.test.ts +++ b/src/background/auth/partnerIntegrations/getPartnerPrincipals.test.ts @@ -29,9 +29,9 @@ import { CONTROL_ROOM_OAUTH_INTEGRATION_ID, CONTROL_ROOM_TOKEN_INTEGRATION_ID, } from "@/integrations/constants"; -import { readRawConfigurations } from "@/integrations/registry"; import { registry } from "@/background/messenger/api"; import { type RegistryId } from "@/types/registryTypes"; +import { readRawConfigurations } from "@/integrations/util/readRawConfigurations"; jest.mock("@/integrations/registry", () => { const actual = jest.requireActual("@/integrations/registry"); @@ -57,6 +57,7 @@ jest.mocked(registry.find).mockImplementation(async (id: RegistryId) => { } as any; }); +jest.mock("@/integrations/util/readRawConfigurations"); const readRawConfigurationsMock = jest.mocked(readRawConfigurations); describe("getPartnerPrincipals", () => { diff --git a/src/background/auth/partnerIntegrations/launchAuthIntegration.test.ts b/src/background/auth/partnerIntegrations/launchAuthIntegration.test.ts index 7537a20f33..3a832275fd 100644 --- a/src/background/auth/partnerIntegrations/launchAuthIntegration.test.ts +++ b/src/background/auth/partnerIntegrations/launchAuthIntegration.test.ts @@ -17,19 +17,279 @@ import { registry } from "@/background/messenger/api"; import oauth2IntegrationDefinition from "@contrib/integrations/automation-anywhere-oauth2.yaml"; -import { registryIdFactory } from "@/testUtils/factories/stringFactories"; import { launchAuthIntegration } from "@/background/auth/partnerIntegrations/launchAuthIntegration"; +import { appApiMock } from "@/testUtils/appApiMock"; +import { validateRegistryId } from "@/types/helpers"; +import { readRawConfigurations } from "@/integrations/util/readRawConfigurations"; +import { + integrationConfigFactory, + secretsConfigFactory, +} from "@/testUtils/factories/integrationFactories"; +import launchOAuth2Flow from "@/background/auth/launchOAuth2Flow"; +import { type Metadata } from "@/types/registryTypes"; +import { removeOAuth2Token, setPartnerAuthData } from "@/auth/authStorage"; +import chromeP from "webext-polyfill-kinda"; + +jest.mock("@/integrations/util/readRawConfigurations"); +const readRawConfigurationsMock = jest.mocked(readRawConfigurations); + +const integrationMetaData = oauth2IntegrationDefinition!.metadata as Metadata; +const integrationId = validateRegistryId(integrationMetaData.id); jest.mocked(registry.find).mockResolvedValue({ - id: (oauth2IntegrationDefinition!.metadata as any).id, + id: integrationId, config: oauth2IntegrationDefinition, } as any); +jest.mock("@/background/auth/launchOAuth2Flow"); +const launchOAuth2FlowMock = jest.mocked(launchOAuth2Flow); + +jest.mock("@/auth/authStorage"); +const setPartnerAuthDataMock = jest.mocked(setPartnerAuthData); +const removeOAuth2TokenMock = jest.mocked(removeOAuth2Token); + +// const removeCachedAuthTokenMock = jest.mocked(chrome.identity.removeCachedAuthToken); +// const removeCachedAuthTokenMock = jest.fn(async () => { +// console.log("*** inner mark") +// }); + +// beforeAll(() => { +// chrome.identity = { +// ...chrome.identity, +// removeCachedAuthToken: jest.fn(), +// }; +// chromeP.identity = { +// ...chromeP.identity, +// removeCachedAuthToken: jest.fn(), +// }; +// }); + describe("launchAuthIntegration", () => { + beforeEach(() => { + appApiMock.reset(); + + appApiMock + .onGet("/api/registry/bricks/") + .reply(200, [oauth2IntegrationDefinition]); + + appApiMock.onGet("/api/services/shared/").reply(200, []); + + readRawConfigurationsMock.mockReset(); + launchOAuth2FlowMock.mockReset(); + setPartnerAuthDataMock.mockReset(); + // removeCachedAuthTokenMock.mockReset(); + }); + it("throws error if no local auths are found", async () => { - const integrationId = registryIdFactory(); + readRawConfigurationsMock.mockResolvedValue([]); + + await expect(launchAuthIntegration({ integrationId })).rejects.toThrow( + "No local configurations found for: " + integrationId, + ); + }); + + it("calls launchOAuth2Flow properly for AA partner integration", async () => { + readRawConfigurationsMock.mockResolvedValue([ + integrationConfigFactory({ + integrationId, + config: secretsConfigFactory({ + controlRoomUrl: "https://control-room.example.com", + }), + }), + ]); + + launchOAuth2FlowMock.mockResolvedValue({ + _oauthBrand: null, + access_token: "test_access_token", + refresh_token: "test_refresh_token", + }); + + appApiMock.onGet("/api/me/").reply(200, {}); + + await launchAuthIntegration({ integrationId }); + + expect(launchOAuth2FlowMock).toHaveBeenCalledTimes(1); + expect(launchOAuth2FlowMock).toHaveBeenCalledWith( + // UserDefinedIntegration + expect.objectContaining(integrationMetaData), + // IntegrationConfig + expect.objectContaining({ + config: expect.objectContaining({ + controlRoomUrl: "https://control-room.example.com", + }), + }), + // Interactive option + { interactive: true }, + ); + }); + + it("throws error if controlRoomUrl is missing from the configuration", async () => { + readRawConfigurationsMock.mockResolvedValue([ + integrationConfigFactory({ + integrationId, + config: secretsConfigFactory(), + }), + ]); + + await expect(launchAuthIntegration({ integrationId })).rejects.toThrow( + "controlRoomUrl is missing on configuration", + ); + }); + + it("throws error if controlRoomURl is malformed in the configuration", async () => { + readRawConfigurationsMock.mockResolvedValue([ + integrationConfigFactory({ + integrationId, + config: secretsConfigFactory({ + controlRoomUrl: "malformed-url", + }), + }), + ]); + + await expect(launchAuthIntegration({ integrationId })).rejects.toThrow( + "controlRoomUrl is missing on configuration", + ); + }); + + it("throws error if access_token is missing from launchOAuth2Flow result", async () => { + readRawConfigurationsMock.mockResolvedValue([ + integrationConfigFactory({ + integrationId, + config: secretsConfigFactory({ + controlRoomUrl: "https://control-room.example.com", + }), + }), + ]); + + launchOAuth2FlowMock.mockResolvedValue({ + _oauthBrand: null, + }); + + await expect(launchAuthIntegration({ integrationId })).rejects.toThrow( + "access_token not found in launchOAuth2Flow() result for Control Room login", + ); + }); + + it("on successful launchOAuth2Flow result, makes the correct api call to check the token", async () => { + readRawConfigurationsMock.mockResolvedValue([ + integrationConfigFactory({ + integrationId, + config: secretsConfigFactory({ + controlRoomUrl: "https://control-room.example.com", + }), + }), + ]); + + launchOAuth2FlowMock.mockResolvedValue({ + _oauthBrand: null, + access_token: "test_access_token", + refresh_token: "test_refresh_token", + }); + + appApiMock.onGet("/api/me/").reply(200, {}); + + await launchAuthIntegration({ integrationId }); + + expect(appApiMock.history.get).toBeArrayOfSize(1); + expect(appApiMock.history.get[0].url).toBe("/api/me/"); + }); + + it("when the token check fails with an auth error, clears the oauth2 token and throws rejected error", async () => { + readRawConfigurationsMock.mockResolvedValue([ + integrationConfigFactory({ + integrationId, + config: secretsConfigFactory({ + controlRoomUrl: "https://control-room.example.com", + }), + }), + ]); + + launchOAuth2FlowMock.mockResolvedValue({ + _oauthBrand: null, + access_token: "test_access_token", + refresh_token: "test_refresh_token", + }); + + appApiMock.onGet("/api/me/").reply(401, {}); + await expect(launchAuthIntegration({ integrationId })).rejects.toThrow( - /No local auths found/, + "Control Room rejected login", ); + expect(removeOAuth2TokenMock).toHaveBeenCalledTimes(1); + expect(removeOAuth2TokenMock).toHaveBeenCalledWith("test_access_token"); + }); + + it("sets the partner auth data correctly when refresh token is included", async () => { + readRawConfigurationsMock.mockResolvedValue([ + integrationConfigFactory({ + integrationId, + config: secretsConfigFactory({ + controlRoomUrl: "https://control-room.example.com", + }), + }), + ]); + + launchOAuth2FlowMock.mockResolvedValue({ + _oauthBrand: null, + access_token: "test_access_token", + refresh_token: "test_refresh_token", + }); + + appApiMock.onGet("/api/me/").reply(200, {}); + + await launchAuthIntegration({ integrationId }); + + expect(setPartnerAuthDataMock).toHaveBeenCalledTimes(1); + expect(setPartnerAuthDataMock).toHaveBeenCalledWith({ + authId: expect.toBeString(), // Generated UUID + token: "test_access_token", + refreshToken: "test_refresh_token", + // These values come from automation-anywhere-oauth2.yaml, they were logged by running the test and then copied here + refreshUrl: + "https://oauthconfigapp.automationanywhere.digital/client/oauth/token", + refreshParamPayload: { + client_id: "g2qrB2fvyLYbotkb3zi9wwO5qjmje3eM", + hosturl: "https://control-room.example.com", + }, + refreshExtraHeaders: { + Authorization: "Basic ZzJxckIyZnZ5TFlib3RrYjN6aTl3d081cWptamUzZU0=", + }, + extraHeaders: { + "X-Control-Room": "https://control-room.example.com", + }, + }); + }); + + it("sets the partner auth data correctly without a refresh token", async () => { + readRawConfigurationsMock.mockResolvedValue([ + integrationConfigFactory({ + integrationId, + config: secretsConfigFactory({ + controlRoomUrl: "https://control-room.example.com", + }), + }), + ]); + + launchOAuth2FlowMock.mockResolvedValue({ + _oauthBrand: null, + access_token: "test_access_token", + }); + + appApiMock.onGet("/api/me/").reply(200, {}); + + await launchAuthIntegration({ integrationId }); + + expect(setPartnerAuthDataMock).toHaveBeenCalledTimes(1); + expect(setPartnerAuthDataMock).toHaveBeenCalledWith({ + authId: expect.toBeString(), // Generated UUID + token: "test_access_token", + refreshToken: null, + refreshUrl: null, + refreshParamPayload: null, + refreshExtraHeaders: null, + extraHeaders: { + "X-Control-Room": "https://control-room.example.com", + }, + }); }); }); diff --git a/src/background/auth/partnerIntegrations/launchAuthIntegration.ts b/src/background/auth/partnerIntegrations/launchAuthIntegration.ts index cd55d5d178..9229b40c12 100644 --- a/src/background/auth/partnerIntegrations/launchAuthIntegration.ts +++ b/src/background/auth/partnerIntegrations/launchAuthIntegration.ts @@ -25,7 +25,7 @@ import { CONTROL_ROOM_OAUTH_INTEGRATION_ID } from "@/integrations/constants"; import { canParseUrl } from "@/utils/urlUtils"; import chromeP from "webext-polyfill-kinda"; import { getErrorMessage } from "@/errors/errorHelpers"; -import { setPartnerAuthData } from "@/auth/authStorage"; +import { removeOAuth2Token, setPartnerAuthData } from "@/auth/authStorage"; import { stringToBase64 } from "uint8array-extras"; import { getApiClient } from "@/data/service/apiClient"; import { selectAxiosError } from "@/data/service/requestErrorUtils"; @@ -116,7 +116,8 @@ export async function launchAuthIntegration({ } // Clear the token to allow the user re-login with the SAML/SSO provider - await chromeP.identity.removeCachedAuthToken({ token }); + // await chromeP.identity.removeCachedAuthToken({ token }); + await removeOAuth2Token(token); throw new Error( `Control Room rejected login. Verify you are a user in the Control Room, and/or verify the Control Room SAML and AuthConfig App configuration. @@ -156,6 +157,12 @@ export async function launchAuthIntegration({ "X-Control-Room": controlRoomUrl, }, }); + + // Refactor - TODO: At some point, this whole thing should probably be a + // switch statement that calls separate helper functions for each supported + // integration id, to de-couple the general auth integration logic from + // any partner-specific code. + return; } throw new Error( diff --git a/src/background/refreshToken.test.ts b/src/background/refreshToken.test.ts index b1f80ee134..3f916851e3 100644 --- a/src/background/refreshToken.test.ts +++ b/src/background/refreshToken.test.ts @@ -19,7 +19,6 @@ import refreshPKCEToken from "@/background/refreshToken"; import { appApiMock } from "@/testUtils/appApiMock"; import { sanitizedIntegrationConfigFactory } from "@/testUtils/factories/integrationFactories"; import { type IntegrationConfig } from "@/integrations/integrationTypes"; -import { readRawConfigurations } from "@/integrations/registry"; import { fromJS } from "@/integrations/UserDefinedIntegration"; import { locator } from "@/background/locator"; import aaDefinition from "@contrib/integrations/automation-anywhere-oauth2.yaml"; @@ -31,6 +30,7 @@ import { setCachedAuthData, } from "@/background/auth/authStorage"; import { CONTROL_ROOM_OAUTH_INTEGRATION_ID } from "@/integrations/constants"; +import { readRawConfigurations } from "@/integrations/util/readRawConfigurations"; const aaIntegration = fromJS(aaDefinition as any); const googleIntegration = fromJS(googleDefinition as any); @@ -42,13 +42,7 @@ jest.mock("@/background/auth/authStorage", () => ({ setCachedAuthData: jest.fn(), })); -jest.mock("@/integrations/registry", () => { - const actual = jest.requireActual("@/integrations/registry"); - return { - ...actual, - readRawConfigurations: jest.fn(), - }; -}); +jest.mock("@/integrations/util/readRawConfigurations"); const getCachedAuthDataMock = jest.mocked(getCachedAuthData); const setCachedAuthDataMock = jest.mocked(setCachedAuthData); diff --git a/src/contrib/google/sheets/core/sheetsApi.test.ts b/src/contrib/google/sheets/core/sheetsApi.test.ts index aa9aa6ba0a..1398161429 100644 --- a/src/contrib/google/sheets/core/sheetsApi.test.ts +++ b/src/contrib/google/sheets/core/sheetsApi.test.ts @@ -27,41 +27,40 @@ import { integrationConfigFactory } from "@/testUtils/factories/integrationFacto import { locator } from "@/background/locator"; import googleDefinition from "@contrib/integrations/google-oauth2-pkce.yaml"; import { fromJS } from "@/integrations/UserDefinedIntegration"; -import { readRawConfigurations } from "@/integrations/registry"; import { type IntegrationConfig } from "@/integrations/integrationTypes"; - -import { - deleteCachedAuthData, - getCachedAuthData, - setCachedAuthData, -} from "@/background/auth/authStorage"; +import * as backgroundAuthStorage from "@/background/auth/authStorage"; import { setPlatform } from "@/platform/platformContext"; import backgroundPlatform from "@/background/backgroundPlatform"; +import { readRawConfigurations } from "@/integrations/util/readRawConfigurations"; const axiosMock = new MockAdapter(axios); const googleIntegration = fromJS(googleDefinition as any); -jest.mock("@/background/auth/authStorage", () => ({ - ...jest.requireActual("@/background/auth/authStorage"), - deleteCachedAuthData: jest.fn(), -})); - // Wire up proxyService to the real implementation jest.mocked(apiProxyService).mockImplementation(realProxyService); + +jest.mock("@/integrations/util/readRawConfigurations"); const readRawConfigurationsMock = jest.mocked(readRawConfigurations); -const deleteCachedAuthDataMock = jest.mocked(deleteCachedAuthData); + +const { deleteCachedAuthData, getCachedAuthData, setCachedAuthData } = + backgroundAuthStorage; + +const spyContainer = { + deleteCachedAuthData, +}; + +// const deleteCachedAuthDataMock = jest.mocked(deleteCachedAuthData); +const deleteCachedAuthDataSpy = jest.spyOn( + spyContainer, + "deleteCachedAuthData", +); jest.mock("@/integrations/registry", () => { const actual = jest.requireActual("@/integrations/registry"); return { ...actual, - readRawConfigurations: jest - .fn() - .mockRejectedValue( - new Error("Implement readRawConfigurations mock in test"), - ), lookup: jest.fn(async (id: string) => { if (id === googleIntegration.id) { return googleIntegration; @@ -93,7 +92,7 @@ describe("error handling", () => { readRawConfigurationsMock.mockResolvedValue([integrationConfig]); - deleteCachedAuthDataMock.mockReset(); + deleteCachedAuthDataSpy.mockReset(); await locator.refresh(); }); @@ -116,7 +115,7 @@ describe("error handling", () => { ); // Don't clear the token, because the token is valid the user just might not have access - expect(deleteCachedAuthDataMock).not.toHaveBeenCalledOnce(); + expect(deleteCachedAuthDataSpy).not.toHaveBeenCalledOnce(); }); it("Returns bad request error", async () => { @@ -137,7 +136,7 @@ describe("error handling", () => { "Bad Request", ); - expect(deleteCachedAuthDataMock).not.toHaveBeenCalledOnce(); + expect(deleteCachedAuthDataSpy).not.toHaveBeenCalledOnce(); }); it.each([ @@ -173,7 +172,7 @@ describe("error handling", () => { ).resolves.toStrictEqual({ access_token: "NOTAREALTOKEN", }); - expect(deleteCachedAuthDataMock).toHaveBeenCalledOnce(); + expect(deleteCachedAuthDataSpy).toHaveBeenCalledOnce(); expect( axiosMock.history.get!.filter((x) => x.url!.startsWith(DRIVE_BASE_URL)), @@ -218,7 +217,7 @@ describe("error handling", () => { access_token: "NOTAREALTOKEN", refresh_token: "NOTAREALREFRESHTOKEN", }); - expect(deleteCachedAuthDataMock).toHaveBeenCalledOnce(); + expect(deleteCachedAuthDataSpy).toHaveBeenCalledOnce(); expect( axiosMock.history.get!.filter((x) => x.url!.startsWith(DRIVE_BASE_URL)), @@ -256,7 +255,7 @@ describe("error handling", () => { access_token: "NOTAREALTOKEN2", refresh_token: "NOTAREALREFRESHTOKEN2", }); - expect(deleteCachedAuthDataMock).not.toHaveBeenCalled(); + expect(deleteCachedAuthDataSpy).not.toHaveBeenCalled(); const googleGetRequests = axiosMock.history.get!.filter((x) => x.url!.startsWith(DRIVE_BASE_URL), diff --git a/src/data/service/apiClient.ts b/src/data/service/apiClient.ts index faa8421b0f..e1f896a4ca 100644 --- a/src/data/service/apiClient.ts +++ b/src/data/service/apiClient.ts @@ -33,7 +33,6 @@ import createAuthRefreshInterceptor from "axios-auth-refresh"; import refreshPartnerAuthentication from "@/background/auth/partnerIntegrations/refreshPartnerAuthentication"; import { selectAxiosError } from "@/data/service/requestErrorUtils"; import { isAuthenticationAxiosError } from "@/auth/isAuthenticationAxiosError"; -import { expectContext } from "@/utils/expectContext"; /** * Converts `relativeOrAbsoluteURL` to an absolute PixieBrix service URL @@ -119,11 +118,6 @@ async function safeSetupClient(): Promise { * Returns an Axios client for making (optionally) authenticated API requests to PixieBrix. */ export async function getApiClient(): Promise { - expectContext( - "background", - "The api client should only be used in the background context.", - ); - if (apiClientSetupPromise == null) { await safeSetupClient(); } @@ -168,11 +162,6 @@ export async function maybeGetLinkedApiClient(): Promise { } export function initApiClient() { - expectContext( - "background", - "The api client should only be used in the background context.", - ); - if (apiClientInstance != null) { console.warn( "initApiClient() called, but the client instance already exists.", diff --git a/src/hooks/auth.ts b/src/hooks/auth.ts index 5a4c3de502..841e23becc 100644 --- a/src/hooks/auth.ts +++ b/src/hooks/auth.ts @@ -16,7 +16,6 @@ */ import { type AuthOption, type AuthSharing } from "@/auth/authTypes"; -import { readRawConfigurations } from "@/integrations/registry"; import { sortBy } from "lodash"; import { type RemoteIntegrationConfig } from "@/types/contract"; import { type IntegrationConfig } from "@/integrations/integrationTypes"; @@ -28,6 +27,7 @@ import useMergeAsyncState from "@/hooks/useMergeAsyncState"; import { useGetIntegrationAuthsQuery } from "@/data/service/api"; import getModDefinitionIntegrationIds from "@/integrations/util/getModDefinitionIntegrationIds"; import { type Nullishable } from "@/utils/nullishUtils"; +import { readRawConfigurations } from "@/integrations/util/readRawConfigurations"; function defaultLabel(label: Nullishable): string { const normalized = (label ?? "").trim(); diff --git a/src/integrations/locator.ts b/src/integrations/locator.ts index b216ed4535..5c2c218134 100644 --- a/src/integrations/locator.ts +++ b/src/integrations/locator.ts @@ -17,9 +17,7 @@ import { type RemoteIntegrationConfig } from "@/types/contract"; import { isEmpty, sortBy } from "lodash"; -import servicesRegistry, { - readRawConfigurations, -} from "@/integrations/registry"; +import servicesRegistry from "@/integrations/registry"; import { validateRegistryId } from "@/types/helpers"; import { expectContext, forbidContext } from "@/utils/expectContext"; import { ExtensionNotLinkedError } from "@/errors/genericErrors"; @@ -39,6 +37,7 @@ import { getLinkedApiClient } from "@/data/service/apiClient"; import { memoizeUntilSettled } from "@/utils/promiseUtils"; import { type SetRequired } from "type-fest"; import { pixiebrixConfigurationFactory } from "@/integrations/util/pixiebrixConfigurationFactory"; +import { readRawConfigurations } from "@/integrations/util/readRawConfigurations"; enum Visibility { Private = 0, diff --git a/src/integrations/registry.ts b/src/integrations/registry.ts index 8008f2adca..1519779ea7 100644 --- a/src/integrations/registry.ts +++ b/src/integrations/registry.ts @@ -17,23 +17,8 @@ import BaseRegistry from "@/registry/memoryRegistry"; import { fromJS } from "@/integrations/UserDefinedIntegration"; -import { - type IntegrationConfig, - type IntegrationABC, -} from "@/integrations/integrationTypes"; +import { type IntegrationABC } from "@/integrations/integrationTypes"; import { type RegistryId } from "@/types/registryTypes"; -import { - readReduxStorage, - validateReduxStorageKey, -} from "@/utils/storageUtils"; -import { migrations } from "@/integrations/store/integrationsMigrations"; -import { initialState } from "@/integrations/store/integrationsSlice"; -import { selectIntegrationConfigs } from "@/integrations/store/integrationsSelectors"; - -// @See persistIntegrationsConfig in integrationsSlice.ts -const INTEGRATIONS_STORAGE_KEY = validateReduxStorageKey( - "persist:servicesOptions", -); // eslint-disable-next-line local-rules/persistBackgroundData -- Static const registry = new BaseRegistry( @@ -41,13 +26,4 @@ const registry = new BaseRegistry( fromJS, ); -export async function readRawConfigurations(): Promise { - const integrations = await readReduxStorage( - INTEGRATIONS_STORAGE_KEY, - migrations, - initialState, - ); - return selectIntegrationConfigs({ integrations }); -} - export default registry; diff --git a/src/integrations/util/readRawConfigurations.ts b/src/integrations/util/readRawConfigurations.ts new file mode 100644 index 0000000000..6cdcf2cfa0 --- /dev/null +++ b/src/integrations/util/readRawConfigurations.ts @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2024 PixieBrix, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +// @See persistIntegrationsConfig in integrationsSlice.ts +import type { IntegrationConfig } from "@/integrations/integrationTypes"; +import { + readReduxStorage, + validateReduxStorageKey, +} from "@/utils/storageUtils"; +import { migrations } from "@/integrations/store/integrationsMigrations"; +import { initialState } from "@/integrations/store/integrationsSlice"; +import { selectIntegrationConfigs } from "@/integrations/store/integrationsSelectors"; + +const INTEGRATIONS_STORAGE_KEY = validateReduxStorageKey( + "persist:servicesOptions", +); + +export async function readRawConfigurations(): Promise { + const integrations = await readReduxStorage( + INTEGRATIONS_STORAGE_KEY, + migrations, + initialState, + ); + return selectIntegrationConfigs({ integrations }); +} diff --git a/src/testUtils/testAfterEnv.ts b/src/testUtils/testAfterEnv.ts index 99483e84d4..e53fca6c2e 100644 --- a/src/testUtils/testAfterEnv.ts +++ b/src/testUtils/testAfterEnv.ts @@ -60,3 +60,29 @@ chrome.storage.onChanged = { removeRules: jest.fn(), addRules: jest.fn(), }; + +enum AccountStatus { + SYNC = "SYNC", + ANY = "ANY", +} + +// `jest-webextension-mock` is missing mocks for identity +chrome.identity = { + AccountStatus, + clearAllCachedAuthTokens: jest.fn(), + getAccounts: jest.fn(), + getAuthToken: jest.fn(), + getProfileUserInfo: jest.fn(), + getRedirectURL: jest.fn(), + launchWebAuthFlow: jest.fn(), + onSignInChanged: { + addListener: jest.fn(), + removeListener: jest.fn(), + hasListener: jest.fn(), + hasListeners: jest.fn(), + getRules: jest.fn(), + removeRules: jest.fn(), + addRules: jest.fn(), + }, + removeCachedAuthToken: jest.fn(), +};