Skip to content

Commit

Permalink
Added e2e tests for enterprise SSO setup link (#2728)
Browse files Browse the repository at this point in the history
* Added e2e tests for enterprise SSO setup link
generic SAML 2.0 workflow

* Replaced useSWR with fetch api in setuplinks

* Addressed review comment
 - Refactored code to remove localor.all
 - removed extract awaits like page repload

* Refactored code
 - used fixtures to create setup link
 - removed unnecessary code
  • Loading branch information
nitendra-new committed May 27, 2024
1 parent 654fdd4 commit 73b1337
Show file tree
Hide file tree
Showing 5 changed files with 176 additions and 11 deletions.
1 change: 1 addition & 0 deletions e2e/support/fixtures/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './portal';
export * from './sso-page';
export * from './dsync-page';
export * from './setuplink-page';
82 changes: 82 additions & 0 deletions e2e/support/fixtures/setuplink-page.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { Page, expect } from '@playwright/test';

const TEST_SETUPLINK_REDIRECT_URL = 'http://localhost:3366';
const TEST_SETUPLINK_DEFAULT_REDIRECT_URL = 'http://localhost:3366/login/saml';
const TEST_SETUPLINK_ADMIN_URL = '/admin/sso-connection/setup-link';
const TEST_SETUPLINK_URL_LABEL_SELECTOR =
'Share this link with your customers to allow them to set up the integrationClose';

export class SetupLinkPage {
setupLinkUrl: string;
constructor(
public readonly page: Page,
public readonly product: string,
public readonly tenant: string,
public readonly adminPage: string = TEST_SETUPLINK_ADMIN_URL,
public readonly redirectUrl: string = TEST_SETUPLINK_REDIRECT_URL,
public readonly defaultRedirectUrl: string = TEST_SETUPLINK_DEFAULT_REDIRECT_URL
) {
this.page = page;
this.product = product;
this.tenant = tenant;
this.adminPage = adminPage;
this.redirectUrl = redirectUrl;
this.defaultRedirectUrl = defaultRedirectUrl;
this.setupLinkUrl = '';
}

async createSetupLink() {
// Go to admin/sso-connection/setup-link page and create setup link
await this.page.goto(this.adminPage);
await this.page.getByRole('button', { name: 'New Setup Link' }).click();
await this.page.getByPlaceholder('Acme SSO').fill('acme-test');
await this.page.getByLabel('Description (Optional)').fill('acme test');
await this.page.getByPlaceholder('acme', { exact: true }).fill(this.tenant);
await this.page.getByPlaceholder('MyApp').fill(this.product);
await this.page.getByPlaceholder('http://localhost:3366', { exact: true }).fill(this.redirectUrl);
await this.page.getByPlaceholder('http://localhost:3366/login/').fill(this.defaultRedirectUrl);

await this.page.getByRole('button', { name: 'Create Setup Link' }).click();

// Extract generated setup link
this.setupLinkUrl = await this.page
.getByText(TEST_SETUPLINK_URL_LABEL_SELECTOR)
.locator('input[type="text"]')
.first()
.inputValue();
}

async getSetupLinkUrl(): Promise<string> {
return this.setupLinkUrl;
}

async isSetupLinkCreated() {
// Go back to new connections page
await this.page.goto(TEST_SETUPLINK_ADMIN_URL);

// Await for rows loaded
await expect(this.page.getByRole('table')).toBeVisible();

// Check if setup link is created
await expect(
this.page.getByText(this.tenant, { exact: true }),
'Failed to create setup link'
).toBeVisible();
await expect(
this.page.getByText(this.product, { exact: true }),
'Failed to create setup link'
).toBeVisible();
}

async removeSetupLink() {
// Go back to setup link admin url
await this.page.goto(TEST_SETUPLINK_ADMIN_URL);

// Await for rows loaded
await expect(this.page.getByRole('table')).toBeVisible();

// Delete the created setuplink
await this.page.getByRole('button').nth(5).click();
await this.page.getByRole('button', { name: 'Delete' }).click();
}
}
78 changes: 78 additions & 0 deletions e2e/ui/Enterprise SSO/setup_link.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { expect, test as baseTest } from '@playwright/test';
import { Portal, SetupLinkPage } from 'e2e/support/fixtures';

const TEST_SETUPLINK_MOCKSAML_ORIGIN = process.env.MOCKSAML_ORIGIN || 'https://mocksaml.com';
const TEST_SETUPLINK_MOCK_METADATA_URL = `${TEST_SETUPLINK_MOCKSAML_ORIGIN}/api/saml/metadata`;

const TEST_SETUPLINK_ADMIN_CONNECTION = '/admin/sso-connection';
const TENANT = 'acme-setuplink-test.com';
const PRODUCT = 'acme-setuplink-test';

type MyFixtures = {
portal: Portal;
setuplinkPage: SetupLinkPage;
};

export const test = baseTest.extend<MyFixtures>({
portal: async ({ page }, use) => {
const portal = new Portal(page);
await portal.doCredentialsLogin();
await use(portal);
},
setuplinkPage: async ({ page }, use) => {
const setuplinkPage = new SetupLinkPage(page, PRODUCT, TENANT);
await use(setuplinkPage);
},
});

test.describe('Admin Portal Enterprise SSO SetupLink using generic SAML 2.0', () => {
test('should be able to create setup link and sso connection using generic SAML 2.0', async ({
page,
setuplinkPage,
}) => {
// Create setup link
await setuplinkPage.createSetupLink();

// get setuplink url
const linkContent = await setuplinkPage.getSetupLinkUrl();

// Open new tab and go to setup link page
const context = page.context();
const setupLinkPage = await context.newPage();
await setupLinkPage.goto(linkContent);

// Create SSO connection using generic SAML 2.0 workflow
await setupLinkPage.getByRole('button', { name: 'Generic SAML 2.0' }).click();
await setupLinkPage.getByRole('button', { name: 'Next Step' }).click();
await setupLinkPage.getByRole('button', { name: 'Next Step' }).click();
await setupLinkPage
.getByPlaceholder('Paste the Metadata URL here')
.fill(TEST_SETUPLINK_MOCK_METADATA_URL);
await setupLinkPage.getByRole('button', { name: 'Save' }).click();

await setupLinkPage.waitForURL(/\/setup\/.+\/sso-connection$/);
await expect(setupLinkPage.getByRole('cell', { name: 'saml.example.com' })).toBeVisible();
await setupLinkPage.close();

// Go to connections page
await page.goto(TEST_SETUPLINK_ADMIN_CONNECTION);

// Check if new SSO connection is created
await expect(
page.getByText(TENANT, { exact: true }),
'Failed to create new sso connection from setup-link'
).toBeVisible();
await expect(
page.getByText(PRODUCT, { exact: true }),
'Failed to create new sso connection from setup-link'
).toBeVisible();

// Delete the SSO connection
await page.getByLabel('Edit').click();
await page.getByRole('button', { name: 'Delete' }).click();
await page.getByRole('button', { name: 'Confirm' }).click();

// remove setup link
await setuplinkPage.removeSetupLink();
});
});
10 changes: 8 additions & 2 deletions internal-ui/src/hooks/useFetch.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { useEffect, useState } from 'react';

type RefetchFunction = () => void;

async function parseResponseContent(response: Response) {
const responseText = await response.text();

Expand All @@ -14,10 +16,14 @@ export function useFetch<T>({ url }: { url: string }): {
data?: T;
isLoading: boolean;
error: any;
refetch: RefetchFunction;
} {
const [data, setData] = useState<T>();
const [isLoading, setIsLoading] = useState<boolean>(false);
const [error, setError] = useState<any>(null);
const [refetchIndex, setRefetchIndex] = useState<number>(0);

const refetch = () => setRefetchIndex((prevRefetchIndex) => prevRefetchIndex + 1);

useEffect(() => {
async function fetchData() {
Expand All @@ -38,7 +44,7 @@ export function useFetch<T>({ url }: { url: string }): {
}
}
fetchData();
}, [url]);
}, [url, refetchIndex]);

return { data, isLoading, error };
return { data, isLoading, error, refetch };
}
16 changes: 7 additions & 9 deletions internal-ui/src/setup-link/SetupLinks.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
import useSWR from 'swr';
import { useEffect, useState } from 'react';
import { useTranslation } from 'next-i18next';
import EyeIcon from '@heroicons/react/24/outline/EyeIcon';
import TrashIcon from '@heroicons/react/24/outline/TrashIcon';
import ArrowPathIcon from '@heroicons/react/24/outline/ArrowPathIcon';
import ClipboardDocumentIcon from '@heroicons/react/24/outline/ClipboardDocumentIcon';

import { addQueryParamsToPath, copyToClipboard, fetcher } from '../utils';
import { addQueryParamsToPath, copyToClipboard } from '../utils';
import { TableBodyType } from '../shared/Table';
import { pageLimit } from '../shared/Pagination';
import { usePaginate, useRouter } from '../hooks';
import { usePaginate, useRouter, useFetch } from '../hooks';
import type { IdentityFederationApp, SetupLink, SetupLinkService } from '../types';
import {
Loading,
Expand Down Expand Up @@ -65,10 +64,9 @@ export const SetupLinks = ({
}

const getLinksUrl = addQueryParamsToPath(urls.getLinks, params);
const { data, isLoading, error, mutate } = useSWR<{ data: SetupLink[]; pageToken?: string }>(
getLinksUrl,
fetcher
);
const { data, isLoading, error, refetch } = useFetch<{ data: SetupLink[]; pageToken?: string }>({
url: getLinksUrl,
});

const nextPageToken = data?.pageToken;

Expand Down Expand Up @@ -201,7 +199,7 @@ export const SetupLinks = ({
setDelModal(false);
setSetupLink(null);
onDelete(setupLink);
await mutate();
refetch();
} else {
onError(response.error);
}
Expand Down Expand Up @@ -229,7 +227,7 @@ export const SetupLinks = ({
if (rawResponse.ok) {
onRegenerate(response.data);
setShowRegenModal(false);
await mutate();
refetch();
setSetupLink(response.data);
setShowSetupLink(true);
} else {
Expand Down

0 comments on commit 73b1337

Please sign in to comment.