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

Added e2e tests for enterprise SSO setup link #2728

Merged
merged 4 commits into from
May 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@
}

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 All @@ -76,7 +74,7 @@
if (nextPageToken) {
setPageTokenMap((tokenMap) => ({ ...tokenMap, [paginate.offset]: nextPageToken }));
}
}, [nextPageToken, paginate.offset]);

Check warning on line 77 in internal-ui/src/setup-link/SetupLinks.tsx

View workflow job for this annotation

GitHub Actions / ci (20)

React Hook useEffect has a missing dependency: 'setPageTokenMap'. Either include it or remove the dependency array

if (isLoading) {
return <Loading />;
Expand Down Expand Up @@ -201,7 +199,7 @@
setDelModal(false);
setSetupLink(null);
onDelete(setupLink);
await mutate();
refetch();
} else {
onError(response.error);
}
Expand Down Expand Up @@ -229,7 +227,7 @@
if (rawResponse.ok) {
onRegenerate(response.data);
setShowRegenModal(false);
await mutate();
refetch();
setSetupLink(response.data);
setShowSetupLink(true);
} else {
Expand Down
Loading