diff --git a/playwright.config.js b/playwright.config.js index 103fe2b..b1fac3b 100644 --- a/playwright.config.js +++ b/playwright.config.js @@ -21,7 +21,7 @@ module.exports = defineConfig({ /* Fail the build on CI if you accidentally left test.only in the source code. */ forbidOnly: !!process.env.CI, /* Retry on CI only */ - retries: process.env.CI ? 2 : 0, + retries: process.env.CI ? 2 : 1, /* Opt out of parallel tests on CI. */ workers: process.env.CI ? 1 : undefined, /* Reporter to use. See https://playwright.dev/docs/test-reporters */ @@ -49,7 +49,8 @@ module.exports = defineConfig({ portal: process.env.PORTAL, environment: process.env.ENVIRONMENT, baseURL: `https://${NGINX_SERVER_NAME}` - } + }, + teardown: 'teardown' }, { name: 'default', @@ -81,6 +82,14 @@ module.exports = defineConfig({ environment: process.env.ENVIRONMENT, }, dependencies: ['setup'], + }, + { + name: 'teardown', + testMatch: 'teardown/*.teardown.js', + use: { + storageState: 'playwright/.auth/user.json', + baseURL: `https://${NGINX_SERVER_NAME}` + } } // { diff --git a/tests/data-files/data-files-shared-workspace.spec.js b/tests/data-files/data-files-shared-workspace.spec.js new file mode 100644 index 0000000..57f77ee --- /dev/null +++ b/tests/data-files/data-files-shared-workspace.spec.js @@ -0,0 +1,85 @@ +import { expect } from '@playwright/test'; +import { test } from '../../fixtures/baseFixture'; +import { PORTAL_DATAFILES_STORAGE_SYSTEMS } from '../../settings/custom_portal_settings.json'; + +const portalStorageSystems = PORTAL_DATAFILES_STORAGE_SYSTEMS + +test.describe.configure({ mode: 'serial' }) + +test.describe('Shared Workspaces tests', () => { + + // Skip the tests if portal does not have Shared Workspaces + test.skip(!portalStorageSystems.some(system => (system.scheme === 'projects'))) + + test.beforeEach(async ({ page, baseURL }) => { + await page.goto(baseURL); + await page.locator('#navbarDropdown').click(); + await page.getByRole('link', { name: 'Dashboard' }).click(); + await page.getByRole('link', { name: 'Data Files' }).click(); + await page.getByRole('main').getByRole('link', { name: 'Shared Workspaces' }).click(); + }) + + test('Add Shared Workspace', async ({ page }) => { + test.setTimeout(100000) + await page.getByRole('button', { name: '+ Add' }).click(); + await page.getByRole('menuitem', { name: 'Shared Workspace' }).click(); + + await expect(page.locator('.modal-dialog')).toBeVisible(); + await page.getByRole('textbox').click(); + await page.getByRole('textbox').fill('Test Shared Workspace'); + + await expect(page.locator('.project-members__cell').nth(0)).toContainText('WMA Test User') + + await page.getByRole('button', { name: 'Add Workspace' }).click(); + + await expect(page.locator('.modal-dialog')).not.toBeVisible({timeout: 20000}); + + await expect(page.locator('.listing-placeholder')).toBeVisible(); + + await expect(page.getByRole('heading', {level: 3})).toHaveText('Test Shared Workspace') + + await page.getByRole('main').getByRole('link', { name: 'Shared Workspaces' }).click(); + + const table = page.getByRole('table').and(page.locator('.projects-listing')) + const rows = await table.locator('tbody').locator('tr').all() + + expect(rows.length).toBeGreaterThanOrEqual(1); + + }) + + test('Shared Workspace Search', async ({ page }) => { + const input = page.getByRole('form', { name: 'Workspace Search' }).locator('input') + const searchButton = page.getByRole('form', { name: 'Workspace Search' }).getByRole('button', { name: 'Search', exact: true }) + const table = page.getByRole('table').and(page.locator('.projects-listing')) + + await input.fill('Test Shared Workspace') + await searchButton.click(); + const rows = await table.locator('tbody').locator('tr').all() + expect(rows.length).toBe(1); + + await input.fill('random string') + await searchButton.click(); + await expect(table).toContainText("No Shared Workspaces match your search term.") + }) + + test('Edit Shared Workspace Name and Description', async ({ page }) => { + await page.getByRole('link', { name: 'Test Shared Workspace' }).click(); + await page.getByRole('button', { name: 'Edit Descriptions' }).click(); + + await expect(page.locator('.modal-dialog')).toBeVisible(); + + await page.getByLabel('title').click(); + await page.getByLabel('title').fill(''); + await page.getByLabel('title').fill('Test Shared Workspace Rename'); + + await page.getByLabel('description').click(); + await page.getByLabel('description').fill('Workspace description'); + + await page.getByRole('button', { name: 'Update Changes' }).click(); + + await expect(page.locator('.modal-dialog')).not.toBeVisible(); + + await expect(page.getByRole('heading', {level: 3})).toHaveText('Test Shared Workspace Rename') + await expect(page.getByText('Workspace description')).toBeVisible() + }) +}) \ No newline at end of file diff --git a/tests/teardown/data-files-shared-workspaces.teardown.js b/tests/teardown/data-files-shared-workspaces.teardown.js new file mode 100644 index 0000000..7e5cf79 --- /dev/null +++ b/tests/teardown/data-files-shared-workspaces.teardown.js @@ -0,0 +1,97 @@ +import { expect } from '@playwright/test'; +import { test } from '../../fixtures/baseFixture'; +import { PORTAL_PROJECTS_SYSTEM_PREFIX } from '../../settings/custom_portal_settings.json' + +test('Cleanup shared workspaces', async ({ page, baseURL }) => { + + const tenant = 'https://portals.tapis.io'; + const projectPrefix = PORTAL_PROJECTS_SYSTEM_PREFIX; + + let systems = [] + + try { + const accessToken = await getAccessToken(page, baseURL) + + systems = await getSystems(page, tenant, projectPrefix, accessToken) + + console.log(`Teardown: Found ${systems.length} shared workspaces to delete ${systems}`) + + for (const system of systems) { + await deleteSystem(page, system.id, tenant, accessToken) + console.info(`Teardown: Shared workspace with id ${system.id} deleted`) + } + } catch (e) { + console.error(`An error occured when deleting shared workspaces: ${e.message}`) + } + + // Ensure there are no shared workspaces left over + await page.goto(baseURL); + await page.locator('#navbarDropdown').click(); + await page.getByRole('link', { name: 'Dashboard' }).click(); + await page.getByRole('link', { name: 'Data Files' }).click(); + await page.getByRole('main').getByRole('link', { name: 'Shared Workspaces' }).click(); + + const table = page.getByRole('table').and(page.locator('.projects-listing')) + const rows = await table.locator('tbody').locator('tr').all() + + if (rows.length > 0) { + const links = await page.locator('.data-files-nav-link').all(); + + for (const system of systems) { + for (const link of links) { + const href = await link.getAttribute('href'); + expect(href).not.toContain(system.id); + } + } + } +}) + +async function getAccessToken(page, baseURL) { + const url = `${baseURL}/api/auth/tapis`; + + const cookies = await page.context().cookies() + + const headers = { + 'Cookie': cookies.map(cookie => `${cookie.name}=${cookie.value}`).join('; ') + }; + + const response = await page.request.get(url, {headers: headers}); + const jsonResponse = await response.json(); + return jsonResponse.token; +} + + +async function getSystems(page, tenant, projectPrefix, accessToken) { + + console.log('Getting systems with prefix: ', projectPrefix) + // get systems that match the prefix of the current portal + const url = `${tenant}/v3/systems?search=(id.like.${projectPrefix}*)`; + + return await page.evaluate(async ({url, accessToken}) => { + const response = await fetch(url, { + headers: { + "X-Tapis-Token": accessToken + } + }); + + const jsonResponse = await response.json(); + return jsonResponse.result; + + }, {url, accessToken}) +} + +async function deleteSystem(page, systemId, tenant, accessToken) { + + const url = `${tenant}/v3/systems/${systemId}/delete`; + + return await page.evaluate(async ({url, accessToken}) => { + const response = await fetch(url, { + method: 'POST', + headers: { + "X-Tapis-Token": accessToken + } + }); + + return await response.json(); + }, {url, accessToken}) +} \ No newline at end of file diff --git a/utils/pythonHelper.py b/utils/pythonHelper.py index bae45dc..413f69b 100644 --- a/utils/pythonHelper.py +++ b/utils/pythonHelper.py @@ -30,7 +30,8 @@ "PORTAL_DATAFILES_STORAGE_SYSTEMS": custom_portal_settings._PORTAL_DATAFILES_STORAGE_SYSTEMS or [], "SYSTEM_MONITOR_DISPLAY_LIST": custom_portal_settings._SYSTEM_MONITOR_DISPLAY_LIST or [], "NGINX_SERVER_NAME": os.getenv('NGINX_SERVER_NAME'), - "WORKBENCH_SETTINGS":custom_portal_settings._WORKBENCH_SETTINGS or [] + "WORKBENCH_SETTINGS":custom_portal_settings._WORKBENCH_SETTINGS or [], + "PORTAL_PROJECTS_SYSTEM_PREFIX": custom_portal_settings._PORTAL_PROJECTS_SYSTEM_PREFIX or '' } with open(output_path, 'w') as json_file: