Skip to content

Commit b475e43

Browse files
authored
Added missing integration tests for access-control commmands. (#101)
* Added missing integration tests for access-control commmands. * Updated @oclif to fix 1 high severity vulnerability.
1 parent d6a3589 commit b475e43

7 files changed

Lines changed: 779 additions & 636 deletions

File tree

.github/workflows/ci.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,4 +49,5 @@ jobs:
4949
ITP_ISSUER_URL: ${{ vars.ITP_ISSUER_URL }}
5050
ITP_SERVICE_CLIENT_ID: ${{ vars.ITP_SERVICE_CLIENT_ID }}
5151
ITP_SERVICE_CLIENT_SECRET: ${{ secrets.ITP_SERVICE_CLIENT_SECRET }}
52+
ITP_MAILINATOR_API_KEY: ${{ secrets.ITP_MAILINATOR_API_KEY }}
5253
run: npm run test

.vscode/launch.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@
4242
"--forbid-only",
4343
"--timeout",
4444
"999999",
45-
"integration-tests/**/api.test.ts"
45+
"integration-tests/**/*.test.ts"
4646
],
4747
"console": "integratedTerminal",
4848
"internalConsoleOptions": "neverOpen"

integration-tests/access-control/member/owner.ts

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,13 @@ import { expect } from "chai";
1010
import { groupMember } from "../../../src/services/access-control-client/models/group-members";
1111
import { ownerResponse } from "../../../src/services/access-control-client/models/owner";
1212
import { User } from "../../../src/services/user-client/models/user";
13+
import { fetchEmailsAndGetInvitationLink } from "../../utils/helpers";
1314

1415
const tests = () => {
1516
let iTwinId: string;
17+
const iTwinName: string = `cli-itwin-integration-test-${new Date().toISOString()}`;
1618

1719
before(async () => {
18-
const iTwinName = `cli-itwin-integration-test-${new Date().toISOString()}`;
1920
const iTwin = await runCommand<ITwin>(`itwin create --class Thing --sub-class Asset --name ${iTwinName}`);
2021
expect(iTwin.result?.id).is.not.undefined;
2122
iTwinId = iTwin.result!.id!;
@@ -26,13 +27,27 @@ const tests = () => {
2627
expect(result.stdout).to.contain('deleted');
2728
});
2829

29-
it('Should add new owner to an iTwin', async () => {
30-
const emailToAdd = 'itwin.cli.qa-testaccount@be-mailinator.eastus.cloudapp.azure.com';
31-
const owner = await runCommand<ownerResponse>(`access-control member owner add -i ${iTwinId} --email ${emailToAdd}`);
32-
expect(owner.result).is.not.undefined;
33-
expect(owner.result!.member).is.null;
34-
expect(owner.result!.invitation).is.not.undefined;
35-
expect(owner.result!.invitation.email).to.equal(emailToAdd);
30+
it('Should invite an external member to an iTwin, accept invitation and remove owner member', async () => {
31+
const emailToAdd = 'iTwin.CLI.QA.IntegrationTest@bentley.m8r.co';
32+
const invitedOwner = await runCommand<ownerResponse>(`access-control member owner add -i ${iTwinId} --email ${emailToAdd}`);
33+
expect(invitedOwner.result).is.not.undefined;
34+
expect(invitedOwner.result!.member).is.null;
35+
expect(invitedOwner.result!.invitation).is.not.undefined;
36+
expect(invitedOwner.result!.invitation.email.toLowerCase()).to.equal(emailToAdd.toLowerCase());
37+
38+
const invitationLink = await fetchEmailsAndGetInvitationLink(emailToAdd.split('@')[0], iTwinName);
39+
40+
await fetch(invitationLink);
41+
42+
const usersInfo = await runCommand<groupMember[]>(`access-control member owner list --itwin-id ${iTwinId}`);
43+
expect(usersInfo.result).is.not.undefined;
44+
expect(usersInfo.result!.length).to.be.equal(2);
45+
const joinedUser = usersInfo.result?.filter(user => user.email.toLowerCase() === emailToAdd.toLowerCase())[0];
46+
expect(joinedUser).to.not.be.undefined;
47+
48+
const deletionResult = await runCommand<{result: string}>(`access-control member owner delete --itwin-id ${iTwinId} --member-id ${joinedUser?.id}`);
49+
expect(deletionResult.result).to.not.be.undefined;
50+
expect(deletionResult.result!.result).to.be.equal("deleted");
3651
});
3752

3853
it('Should list owners of an iTwin', async () => {

integration-tests/access-control/member/user.ts

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,16 @@ import { ITwin } from "@itwin/itwins-client";
77
import { runCommand } from "@oclif/test";
88
import { expect } from "chai";
99

10-
import { member } from "../../../src/services/access-control-client/models/members";
10+
import { member, membersResponse } from "../../../src/services/access-control-client/models/members";
1111
import { Role } from "../../../src/services/access-control-client/models/role";
1212
import { User } from "../../../src/services/user-client/models/user";
13+
import { fetchEmailsAndGetInvitationLink } from "../../utils/helpers";
1314

1415
const tests = () => {
1516
let iTwinId: string;
17+
const iTwinName: string = `cli-itwin-integration-test-${new Date().toISOString()}`;
1618

1719
before(async () => {
18-
const iTwinName = `cli-itwin-integration-test-${new Date().toISOString()}`;
1920
const iTwin = await runCommand<ITwin>(`itwin create --class Thing --sub-class Asset --name ${iTwinName}`);
2021
expect(iTwin.result?.id).is.not.undefined;
2122
iTwinId = iTwin.result!.id!;
@@ -26,7 +27,39 @@ const tests = () => {
2627
expect(result.stdout).to.contain('deleted');
2728
});
2829

29-
it('Should dispaly owner info of an iTwin in member info', async () => {
30+
it('Should invite an external member to an iTwin, accept sent invitation and remove user member', async () => {
31+
const newRole = await runCommand<Role>(`access-control role create -i ${iTwinId} -n "Test Role 1" -d "Test Role Description"`);
32+
expect(newRole.result).is.not.undefined;
33+
expect(newRole.result!.id).is.not.undefined;
34+
35+
const emailToAdd = 'iTwin.CLI.QA.IntegrationTest@bentley.m8r.co';
36+
37+
const invitedUser = await runCommand<membersResponse>(`access-control member user add --itwin-id ${iTwinId} --members "[{"email": "${emailToAdd}", "roleIds": ["${newRole.result!.id}"]}]"`);
38+
39+
expect(invitedUser.result).to.not.be.undefined;
40+
expect(invitedUser.result!.invitations.length).to.be.equal(1);
41+
expect(invitedUser.result!.invitations[0].email.toLowerCase()).to.be.equal(emailToAdd.toLowerCase());
42+
expect(invitedUser.result!.invitations[0].roles.length).to.be.equal(1);
43+
expect(invitedUser.result!.invitations[0].roles[0].id).to.be.equal(newRole.result!.id);
44+
45+
const invitationLink = await fetchEmailsAndGetInvitationLink(emailToAdd.split('@')[0], iTwinName);
46+
47+
await fetch(invitationLink);
48+
49+
const usersInfo = await runCommand<member[]>(`access-control member user list --itwin-id ${iTwinId}`);
50+
expect(usersInfo.result).is.not.undefined;
51+
expect(usersInfo.result!.length).to.be.equal(2);
52+
const joinedUser = usersInfo.result?.filter(user => user.email.toLowerCase() === emailToAdd.toLowerCase())[0];
53+
expect(joinedUser).to.not.be.undefined;
54+
expect(joinedUser?.roles.length).to.be.equal(1);
55+
expect(joinedUser?.roles[0].id).to.be.equal(newRole.result!.id);
56+
57+
const deletionResult = await runCommand<{result: string}>(`access-control member user delete --itwin-id ${iTwinId} --member-id ${joinedUser?.id}`);
58+
expect(deletionResult.result).to.not.be.undefined;
59+
expect(deletionResult.result!.result).to.be.equal("deleted");
60+
});
61+
62+
it('Should display owner info of an iTwin in member info', async () => {
3063
const userInfo = await runCommand<User>(`user me`);
3164
expect(userInfo.result).is.not.undefined;
3265

@@ -37,7 +70,7 @@ const tests = () => {
3770
});
3871

3972
it('Should add new member to an iTwin and update the role', async () => {
40-
const newRole = await runCommand<Role>(`access-control role create -i ${iTwinId} -n "Test Role" -d "Test Role Description"`);
73+
const newRole = await runCommand<Role>(`access-control role create -i ${iTwinId} -n "Test Role 2" -d "Test Role Description"`);
4174
expect(newRole.result).is.not.undefined;
4275
expect(newRole.result!.id).is.not.undefined;
4376

integration-tests/utils/helpers.ts

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { IModel } from "@itwin/imodels-client-management"
77
import { ITwin } from "@itwin/itwins-client";
88
import { runCommand } from "@oclif/test";
99
import { expect } from "chai";
10+
import { GetInboxRequest, GetMessageRequest, MailinatorClient } from 'mailinator-client'
1011

1112
import { fileTyped } from "../../src/services/storage-client/models/file-typed.js";
1213
import { fileUpload } from "../../src/services/storage-client/models/file-upload.js";
@@ -100,7 +101,43 @@ export async function deleteIModel(id: string): Promise<void> {
100101
export async function getRootFolderId(iTwinId: string): Promise<string> {
101102
const { result: topFolders } = await runCommand<itemsWithFolderLink>(`storage root-folder --itwin-id ${iTwinId}`);
102103
const rootFolderId = topFolders?._links?.folder?.href?.split('/').pop();
103-
104104
expect(rootFolderId).to.not.be.undefined;
105105
return rootFolderId as string;
106+
}
107+
108+
/**
109+
* Fetches emails from the specified inbox and then finds and returns the invitation link for iTwinName iTwin.
110+
* NOTE: This function only works for `@bentley.m8r.co` email addresses and not `@be-mailinator.eastus.cloudapp.azure.com` email addresses.
111+
* @param inbox Inbox to fetch the invitation email from.
112+
* @param iTwinName Name of the iTwin.
113+
* @returns Invitation link for joining the iTwin.
114+
*/
115+
export async function fetchEmailsAndGetInvitationLink(inbox: string, iTwinName: string): Promise<string> {
116+
await new Promise<void>(resolve => {setTimeout(_ => resolve(), 30 * 1000);});
117+
118+
expect(process.env.ITP_MAILINATOR_API_KEY).to.not.be.undefined;
119+
120+
const client = new MailinatorClient(process.env.ITP_MAILINATOR_API_KEY!);
121+
const inboxResponse = await client.request(new GetInboxRequest("private", inbox, undefined, 10));
122+
expect(inboxResponse.result).to.not.be.null;
123+
124+
for (let i = 0; i < inboxResponse.result!.msgs.length; i++) {
125+
if (inboxResponse.result!.msgs[i].subject !== "You have been invited to collaborate")
126+
continue;
127+
128+
// eslint-disable-next-line no-await-in-loop
129+
const messageResponse = await client.request(new GetMessageRequest("private", inboxResponse.result!.msgs[0].id ));
130+
expect(messageResponse.result).to.not.be.null;
131+
const messageBody = messageResponse.result!.parts[0].body;
132+
if (!messageBody.includes(iTwinName))
133+
continue;
134+
135+
const invitationTokenRegex = /href=".*?invitationToken.*?"/;
136+
const matches = messageBody.match(invitationTokenRegex);
137+
expect(matches).to.not.be.null;
138+
expect(matches!.length).to.be.equal(1);
139+
return matches![0].slice("href=\"".length, -1);
140+
}
141+
142+
throw new Error("Email was not found in inbox.")
106143
}

0 commit comments

Comments
 (0)