Skip to content

Commit

Permalink
remove expectContext check and add tests for launchAuthIntegration. A…
Browse files Browse the repository at this point in the history
…lso some refactoring to help with testing
  • Loading branch information
Ben Loe committed Jun 21, 2024
1 parent 3d98c2a commit c374008
Show file tree
Hide file tree
Showing 12 changed files with 377 additions and 82 deletions.
11 changes: 8 additions & 3 deletions src/auth/authStorage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,10 @@ export async function setPartnerAuthData(data: PartnerAuthData): Promise<void> {
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
*/
Expand All @@ -137,9 +141,10 @@ export async function clearPartnerAuthData(): Promise<void> {
console.debug(
"Clearing partner auth for authId: " + partnerAuthData.authId,
);
await chromeP.identity.removeCachedAuthToken({
token: partnerAuthData.token,
});
await removeOAuth2Token(partnerAuthData.token);
// await chromeP.identity.removeCachedAuthToken({

Check failure on line 145 in src/auth/authStorage.ts

View workflow job for this annotation

GitHub Actions / lint

Comments should not begin with a lowercase character
// token: partnerAuthData.token,
// });
}

await partnerTokenStorage.remove();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand All @@ -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", () => {
Expand Down
268 changes: 264 additions & 4 deletions src/background/auth/partnerIntegrations/launchAuthIntegration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Check failure on line 51 in src/background/auth/partnerIntegrations/launchAuthIntegration.test.ts

View workflow job for this annotation

GitHub Actions / lint

Comments should not begin with a lowercase character
// 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();

Check failure on line 80 in src/background/auth/partnerIntegrations/launchAuthIntegration.test.ts

View workflow job for this annotation

GitHub Actions / lint

Comments should not begin with a lowercase character
});

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=",

Check failure

Code scanning / CodeQL

Hard-coded credentials Critical

The hard-coded value "Basic ZzJxckIyZnZ5TFlib3RrYjN6aTl3d081cWptamUzZU0=" is used as
authorization header
.
},
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",
},
});
});
});
11 changes: 9 additions & 2 deletions src/background/auth/partnerIntegrations/launchAuthIntegration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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(
Expand Down
10 changes: 2 additions & 8 deletions src/background/refreshToken.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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);
Expand All @@ -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);
Expand Down
Loading

0 comments on commit c374008

Please sign in to comment.