Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add tests for ConnectorAuthenticationManager to get coverage into the green #127

Merged
merged 7 commits into from
Jun 28, 2024
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,6 @@ manifest.itsyn.yml

# documentation
documentation/file.json
documentation/index.ts
documentation/index.ts

.configStore/
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"test:trimming": "npm run build && nyc mocha --grep trimming",
"test:unmap": "npm run build && nyc mocha --grep unmap",
"test:larger-source-set":"npm run build && nyc mocha --grep larger-source-set",
"test:authclient":"npm run build && nyc mocha --grep AuthClient",
"test:connector": "node lib/test/TestConnector/Main.js test/assets/TestArgs.json",
"documentation": "cross-env RUSHSTACK_FILE_ERROR_BASE_FOLDER=$npm_config_local_prefix betools docs --source=./src --out=./documentation --json=./documentation/file.json --tsIndexFile=./connector-framework.ts --onlyJson"
},
Expand Down
44 changes: 40 additions & 4 deletions src/ConnectorAuthenticationManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,14 +51,15 @@ abstract class CachedTokenClient implements AuthorizationClient {
*/
export class CallbackUrlClient extends CachedTokenClient {
private _callbackUrl: AccessTokenCallbackUrl;

constructor(callbackUrl: AccessTokenCallbackUrl) {
super();
this._callbackUrl = callbackUrl;
}
public override async getAccessToken(): Promise<string> {
return this.getCachedTokenIfNotExpired (async () => {

const response = await fetch(this._callbackUrl);
const response = await this.fetch();
const responseJSON = await response.json();
const tokenStr = responseJSON.access_token;
const expiresIn = await responseJSON.expires_in;
Expand All @@ -67,6 +68,34 @@ export class CallbackUrlClient extends CachedTokenClient {
return tePair;
});
}

protected async fetch(): Promise<Response> {
const response =await fetch(this._callbackUrl);
return response;
}
}

export interface DummyCallbackUrlParams {
callbackUrl: AccessTokenCallbackUrl; // dummy callback URL - for show only
token: string; // the token to return
expiration: number; // in seconds
}

export class DummyCallbackUrlClient extends CallbackUrlClient {
private _dummyToken: string;
private _dummyExpiration: number;

constructor(dummyParams: DummyCallbackUrlParams) {
super(dummyParams.callbackUrl);
this._dummyToken = dummyParams.token;
this._dummyExpiration = dummyParams.expiration;

}

protected override async fetch(): Promise<Response> {
// eslint-disable-next-line @typescript-eslint/naming-convention
return new Response(JSON.stringify({access_token: this._dummyToken, expires_in: this._dummyExpiration}));
};
}

/**
Expand Down Expand Up @@ -95,6 +124,7 @@ interface ConnectorAuthenticationManagerParams {
callback?: AccessTokenGetter;
callbackUrl?: AccessTokenCallbackUrl;
authClientConfig?: NodeCliAuthorizationConfiguration;
dummyParams?: DummyCallbackUrlParams;
}

/**
Expand Down Expand Up @@ -131,28 +161,34 @@ export class ConnectorAuthenticationManager {

}

public initializeCallbackClient(callback: AccessTokenGetter): CallbackClient {
private initializeCallbackClient(callback: AccessTokenGetter): CallbackClient {
return new CallbackClient (callback);
}

public initializeCallbackUrlClient(authClient: AccessTokenCallbackUrl){
private initializeCallbackUrlClient(authClient: AccessTokenCallbackUrl){
return new CallbackUrlClient(authClient);
}

public async initializeInteractiveClient(authClient: NodeCliAuthorizationConfiguration){
private async initializeInteractiveClient(authClient: NodeCliAuthorizationConfiguration){
const ncliClient = new NodeCliAuthorizationClient(authClient);
// From docs... If signIn hasn't been called, the AccessToken will remain empty.
await ncliClient.signIn();
return ncliClient;
}

private async initializeDummyCallbackUrlClient(dummyParams: DummyCallbackUrlParams){
return new DummyCallbackUrlClient(dummyParams);
}

public async initialize() {
if (this._cAMParams.callback)
this._authClient = this.initializeCallbackClient (this._cAMParams.callback);
else if (this._cAMParams.callbackUrl)
this._authClient = this.initializeCallbackUrlClient(this._cAMParams.callbackUrl);
else if (this._cAMParams.authClientConfig)
this._authClient = await this.initializeInteractiveClient(this._cAMParams.authClientConfig);
else if (this._cAMParams.dummyParams)
this._authClient = await this.initializeDummyCallbackUrlClient(this._cAMParams.dummyParams);
else
throw new Error(`Must pass callback, callbackUrl or an auth client!`);
}
Expand Down
2 changes: 1 addition & 1 deletion test/ConnectorTestUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export function setupLoggingWithAPIMRateTrap() {
/* eslint-disable no-console */

/** Loads the provided `.env` file into process.env */
function loadEnv(envFile: string) {
export function loadEnv(envFile: string) {
if (!fs.existsSync(envFile))
return;

Expand Down
6 changes: 2 additions & 4 deletions test/TestConnector/TestConnector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,8 +176,8 @@ export default class TestConnector extends BaseConnector {
deleteElementTree(this.synchronizer.imodel, this.jobSubject.id);
this.synchronizer.imodel.elements.deleteElement(this.repositoryLinkId);
// bf: ADO# 1387737 - Also delete the ExternalSource
if (this._externalSourceId != undefined)
this.synchronizer.imodel.elements.deleteElement(this._externalSourceId);
if (this._externalSourceId !== undefined)
this.synchronizer.imodel.elements.deleteElement(this._externalSourceId);
}

public getApplicationVersion(): string {
Expand Down Expand Up @@ -223,8 +223,6 @@ export default class TestConnector extends BaseConnector {
const partitionId = this.synchronizer.imodel.elements.insertElement(partitionProps);

return this.synchronizer.imodel.models.insertModel({ classFullName: TestConnectorGroupModel.classFullName, modeledElement: { id: partitionId } });


}

private queryGroupModel(): Id64String | undefined {
Expand Down
29 changes: 13 additions & 16 deletions test/integration/ConnectorRunner.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,12 @@
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
import type { AccessToken, Id64String} from "@itwin/core-bentley";
import { BentleyStatus } from "@itwin/core-bentley";
import { AccessToken, BentleyStatus, Id64String} from "@itwin/core-bentley";
import { BriefcaseDb, BriefcaseManager, IModelHost, IModelJsFs } from "@itwin/core-backend";
import type { TestBrowserAuthorizationClientConfiguration} from "@itwin/oidc-signin-tool";
import { TestUtility} from "@itwin/oidc-signin-tool";
import { TestBrowserAuthorizationClientConfiguration, TestUtility} from "@itwin/oidc-signin-tool";
import { expect } from "chai";
import { ConnectorRunner } from "../../src/ConnectorRunner";
import type { HubArgsProps} from "../../src/Args";
import { HubArgs, JobArgs } from "../../src/Args";
import { HubArgs, HubArgsProps, JobArgs } from "../../src/Args";
import { KnownTestLocations } from "../KnownTestLocations";
import { IModelsClient } from "@itwin/imodels-client-authoring";
import { BackendIModelsAccess } from "@itwin/imodels-access-backend";
Expand All @@ -21,13 +18,13 @@ import { TestIModelManager } from "./TestIModelManager";
describe("iTwin Connector Fwk (#integration)", () => {

let testProjectId: Id64String;
let newImodelName = process.env.test_new_imodel_name ? process.env.test_new_imodel_name : "ConnectorFramework";
let updateImodelName = process.env.test_existing_imodel_name? process.env.test_existing_imodel_name: newImodelName + "Update";
let unmapImodelName = process.env.test_unmap_imodel_name? process.env.test_unmap_imodel_name: newImodelName + "Unmap";
const newImodelName = process.env.test_new_imodel_name ? process.env.test_new_imodel_name : "ConnectorFramework";
const updateImodelName = process.env.test_existing_imodel_name? process.env.test_existing_imodel_name: `${newImodelName }Update`;
const unmapImodelName = process.env.test_unmap_imodel_name? process.env.test_unmap_imodel_name: `${newImodelName }Unmap`;

let testClientConfig: TestBrowserAuthorizationClientConfiguration;
let token: AccessToken| undefined;
let callbackUrl : string|undefined;
let callbackUrl: string|undefined;

const testConnector = path.join("..", "lib", "test", "TestConnector", "TestConnector.js");

Expand All @@ -44,10 +41,10 @@ describe("iTwin Connector Fwk (#integration)", () => {
clientId: process.env.test_client_id!,
redirectUri: process.env.test_redirect_uri!,
scope: process.env.test_scopes!,
authority: `https://${process.env.imjs_url_prefix}ims.bentley.com`
authority: `https://${process.env.imjs_url_prefix}ims.bentley.com`,
};

callbackUrl = process.env.test_callbackUrl!
callbackUrl = process.env.test_callbackUrl!;

const userCred = {
email: process.env.test_user_name!,
Expand Down Expand Up @@ -128,7 +125,7 @@ describe("iTwin Connector Fwk (#integration)", () => {
} as HubArgsProps);

if (callbackUrl) {
hubArgs.tokenCallbackUrl = callbackUrl;
hubArgs.tokenCallbackUrl = callbackUrl;
}
else {
hubArgs.clientConfig = testClientConfig;
Expand Down Expand Up @@ -162,7 +159,7 @@ describe("iTwin Connector Fwk (#integration)", () => {
} as HubArgsProps);

if (callbackUrl) {
hubArgs.tokenCallbackUrl = callbackUrl;
hubArgs.tokenCallbackUrl = callbackUrl;
}
else {
hubArgs.clientConfig = testClientConfig;
Expand Down Expand Up @@ -200,7 +197,7 @@ describe("iTwin Connector Fwk (#integration)", () => {
} as HubArgsProps);

if (callbackUrl) {
hubArgs.tokenCallbackUrl = callbackUrl;
hubArgs.tokenCallbackUrl = callbackUrl;
}
else {
hubArgs.clientConfig = testClientConfig;
Expand Down
103 changes: 103 additions & 0 deletions test/standalone/AccessToken.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { assert } from "chai";
import { ConnectorAuthenticationManager, DummyCallbackUrlParams } from "../../src/ConnectorAuthenticationManager";
import {loadEnv} from "../../test/ConnectorTestUtils";
import * as path from "path";
import { NodeCliAuthorizationConfiguration } from "@itwin/node-cli-authorization";
import { TestBrowserAuthorizationClient } from "@itwin/oidc-signin-tool";
import { AccessToken } from "@itwin/core-bentley";
describe("AuthClient - using Node Cli Client", async () => {
let authManager: ConnectorAuthenticationManager;
loadEnv(path.join(__dirname, "../../", ".env"));

const testClientConfig: NodeCliAuthorizationConfiguration = {
clientId: process.env.desktop_client_id!,
redirectUri: process.env.desktop_redirect_uri!,
scope: process.env.desktop_scopes!,
issuerUrl: `https://${process.env.imjs_url_prefix || ""}ims.bentley.com`,
};

beforeEach(async () => {

});

it("getAccessToken should return a token", async () => {
authManager = new ConnectorAuthenticationManager({authClientConfig: testClientConfig});
await authManager.initialize();
assert.isDefined(await authManager.getAccessToken());
});
});

describe("AuthClient - using callback", async () => {
let authManager: ConnectorAuthenticationManager;
let token;
loadEnv(path.join(__dirname, "../../", ".env"));

beforeEach(async () => {
const testClientConfig = {
clientId: process.env.test_client_id!,
redirectUri: process.env.test_redirect_uri!,
scope: process.env.test_scopes!,
authority: `https://${process.env.imjs_url_prefix}ims.bentley.com`,
};

const userCred = {
email: process.env.test_user_name!,
password: process.env.test_user_password!,
};
const client = new TestBrowserAuthorizationClient(testClientConfig, userCred);
token = await client.getAccessToken();

if (!token) {
throw new Error("Token not defined");
}
});

it("getAccessToken should return a token", async () => {
const tokenCallback = async (): Promise<AccessToken> => {
return token!;
};
authManager = new ConnectorAuthenticationManager({callback: tokenCallback});
await authManager.initialize();
assert.isDefined(await authManager.getAccessToken());
});
});

describe("AuthClient (#standalone) - using (dummy) callback URL", async () => {
let authManager: ConnectorAuthenticationManager;
const dummyParams: DummyCallbackUrlParams = {
callbackUrl: "http://localhost:3000",
token: "dummy",
expiration: 3600,
};
// beforeEach(async () => {});

it("getAccessToken should return a token", async () => {

authManager = new ConnectorAuthenticationManager({dummyParams});
await authManager.initialize();
assert.isDefined(await authManager.getAccessToken());
});

it("getAccessToken should return a token after exceeding expiration", async () => {
const shortExpiration = 5;
dummyParams.expiration = shortExpiration;
authManager = new ConnectorAuthenticationManager({dummyParams});
await authManager.initialize();

// Token should be fresh
assert.isDefined(await authManager.getAccessToken());

// Token should be cached
assert.isDefined(await authManager.getAccessToken());

// Wait for expiration
await new Promise((resolve) => setTimeout(resolve, (shortExpiration + 1)*1E3));

// Token should be expired
// This is admittedly not the greatest test b/c we can't be certain we're not getting a cached token.
// Since the cached token is tested with the integration tests and b/c we are already at 84% coverage, ...
// we won't take this farther at this time. We could make the CachedTokenClient and CachedToken public
// through accessor methods and test the expiration directly.
assert.isDefined(await authManager.getAccessToken());
});
});
Loading