From 93bdbdf9e480235a576b97e600319d9c517a9a72 Mon Sep 17 00:00:00 2001 From: Alexis Date: Mon, 12 Feb 2024 12:43:57 +0100 Subject: [PATCH 1/9] functional tests --- frontend/.gitignore | 1 + frontend/package.json | 4 +- .../src/lib/components/ModelTable/Th.svelte | 1 + .../TableRowActions/TableRowActions.svelte | 6 +- .../[model=urlmodel]/[id=uuid]/+page.svelte | 8 +- .../[id=uuid]/+page.svelte | 5 +- .../(app)/evidences/[id=uuid]/+page.svelte | 11 +- .../risk-assessments/[id=uuid]/+page.svelte | 11 +- .../[id=uuid]/edit/+page.svelte | 3 +- frontend/tests/Dockerfile | 12 + frontend/tests/docker-compose.e2e-tests.yml | 35 + frontend/tests/e2e-tests.sh | 64 ++ .../tests/functional/detailed/common.test.ts | 79 ++ .../tests/functional/detailed/folders.test.ts | 28 + .../tests/functional/detailed/login.test.ts | 35 + frontend/tests/functional/nav.test.ts | 133 ++-- frontend/tests/functional/startup.test.ts | 24 +- frontend/tests/functional/user-route.test.ts | 549 +++++++------ frontend/tests/utils/analytics-page.ts | 7 +- frontend/tests/utils/base-page.ts | 17 +- frontend/tests/utils/form-content.ts | 149 ++-- frontend/tests/utils/login-page.ts | 84 +- frontend/tests/utils/page-content.ts | 160 ++-- frontend/tests/utils/page-detail.ts | 91 +++ frontend/tests/utils/sidebar.ts | 6 +- frontend/tests/utils/test-data.ts | 73 ++ frontend/tests/utils/test-utils.ts | 726 ++++++++++++------ frontend/tests/utils/test_file.txt | 1 + 28 files changed, 1507 insertions(+), 816 deletions(-) create mode 100644 frontend/tests/Dockerfile create mode 100644 frontend/tests/docker-compose.e2e-tests.yml create mode 100755 frontend/tests/e2e-tests.sh create mode 100644 frontend/tests/functional/detailed/common.test.ts create mode 100644 frontend/tests/functional/detailed/folders.test.ts create mode 100644 frontend/tests/functional/detailed/login.test.ts create mode 100644 frontend/tests/utils/page-detail.ts create mode 100644 frontend/tests/utils/test-data.ts create mode 100644 frontend/tests/utils/test_file.txt diff --git a/frontend/.gitignore b/frontend/.gitignore index 602437196..f09e87191 100644 --- a/frontend/.gitignore +++ b/frontend/.gitignore @@ -18,5 +18,6 @@ vite.config.js.timestamp-* .turbo .vercel .test-tmp +.testhistory symlink-from coverage/** diff --git a/frontend/package.json b/frontend/package.json index 4384fdbda..2e8f0baa5 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -6,18 +6,18 @@ "dev": "vite dev", "build": "vite build", "preview": "vite preview", - "test:e2e": "playwright test", "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", "test": "vitest", "test:ci": "vitest run", "test:ui": "vitest --ui", + "test:e2e": "ARGS=\"$npm_config_args\" docker compose -f ./tests/docker-compose.e2e-tests.yml up --force-recreate --build -V", "coverage": "vitest run --coverage", "lint": "prettier --plugin-search-dir . --check . && eslint .", "format": "prettier --plugin-search-dir . --write ." }, "devDependencies": { - "@playwright/test": "^1.28.1", + "@playwright/test": "^1.40.1", "@skeletonlabs/skeleton": "^2.3.0", "@skeletonlabs/tw-plugin": "^0.2.2", "@sveltejs/adapter-auto": "^3.0.0", diff --git a/frontend/src/lib/components/ModelTable/Th.svelte b/frontend/src/lib/components/ModelTable/Th.svelte index 4d116418d..febe0cbd6 100644 --- a/frontend/src/lib/components/ModelTable/Th.svelte +++ b/frontend/src/lib/components/ModelTable/Th.svelte @@ -12,6 +12,7 @@ on:click={() => handler.sort(orderBy)} class:active={$sorted.identifier === identifier} class={_class} + data-testid="tableheader" >
diff --git a/frontend/src/lib/components/TableRowActions/TableRowActions.svelte b/frontend/src/lib/components/TableRowActions/TableRowActions.svelte index bbd2a0c72..8aaf51b04 100644 --- a/frontend/src/lib/components/TableRowActions/TableRowActions.svelte +++ b/frontend/src/lib/components/TableRowActions/TableRowActions.svelte @@ -62,7 +62,7 @@ {#if !hasBody} {#if displayDetail} - {/if} @@ -70,7 +70,7 @@ {/if} @@ -81,7 +81,7 @@ stopPropagation(_); }} on:keydown={(_) => modalConfirmDelete(row.meta.id, row.name ?? Object.values(row)[0])} - class="cursor-pointer hover:text-primary-500"> {/if} {/if} diff --git a/frontend/src/routes/(app)/[model=urlmodel]/[id=uuid]/+page.svelte b/frontend/src/routes/(app)/[model=urlmodel]/[id=uuid]/+page.svelte index 0b75bb73d..ebc1c5ab0 100644 --- a/frontend/src/routes/(app)/[model=urlmodel]/[id=uuid]/+page.svelte +++ b/frontend/src/routes/(app)/[model=urlmodel]/[id=uuid]/+page.svelte @@ -172,16 +172,17 @@
{#each Object.entries(data.data).filter(([key, _]) => !['id', 'is_published'].includes(key)) as [key, value]}
-
+
{key.replace('_', ' ')}
    -
  • +
  • {#if value} {#if Array.isArray(value)}
      {#each value as val} -
    • +
    • {#if val.str && val.id} {@const itemHref = `/${ URL_MODEL_MAP[data.urlModel]['foreignKeyFields']?.find( @@ -219,6 +220,7 @@ Edit {/if} diff --git a/frontend/src/routes/(app)/compliance-assessments/[id=uuid]/+page.svelte b/frontend/src/routes/(app)/compliance-assessments/[id=uuid]/+page.svelte index 9503b8f77..9c1570480 100644 --- a/frontend/src/routes/(app)/compliance-assessments/[id=uuid]/+page.svelte +++ b/frontend/src/routes/(app)/compliance-assessments/[id=uuid]/+page.svelte @@ -96,7 +96,7 @@
      {#each Object.entries(data.compliance_assessment).filter( ([key, _]) => ['name', 'description', 'project', 'framework'].includes(key) ) as [key, value]}
      -
      +
      {#if key === 'urn'} URN {:else} @@ -104,7 +104,7 @@ {/if}
        -
      • +
      • {#if value} {#if Array.isArray(value)}
          @@ -155,6 +155,7 @@ Edit {/if} diff --git a/frontend/src/routes/(app)/evidences/[id=uuid]/+page.svelte b/frontend/src/routes/(app)/evidences/[id=uuid]/+page.svelte index 4396d4c3d..16b8e8eda 100644 --- a/frontend/src/routes/(app)/evidences/[id=uuid]/+page.svelte +++ b/frontend/src/routes/(app)/evidences/[id=uuid]/+page.svelte @@ -62,7 +62,8 @@
          {#each Object.entries(data.evidence).filter( ([key, _]) => ['name', 'description', 'folder', 'security_measures', 'requirement_assessments', 'attachment', 'link', 'comment'].includes(key) ) as [key, value]}
          -
          +
          {#if key === 'urn'} URN {:else} @@ -70,7 +71,7 @@ {/if}
            -
          • +
          • {#if value} {#if Array.isArray(value)}
              @@ -118,6 +119,7 @@ Edit {/if} @@ -126,10 +128,11 @@ {#if data.evidence.attachment}
              -

              {data.evidence.attachment}

              +

              {data.evidence.attachment}

              Download Download - +
              diff --git a/frontend/tests/Dockerfile b/frontend/tests/Dockerfile new file mode 100644 index 000000000..6a899f209 --- /dev/null +++ b/frontend/tests/Dockerfile @@ -0,0 +1,12 @@ +# The image version should match the playwright version in the package.json +FROM mcr.microsoft.com/playwright:v1.40.1 +WORKDIR /app + +ARG PUBLIC_BACKEND_API_URL +ENV PUBLIC_BACKEND_API_URL=$PUBLIC_BACKEND_API_URL + +COPY ../package*.json . +RUN npm ci +RUN npx playwright install --with-deps +COPY .. . +EXPOSE 9323 \ No newline at end of file diff --git a/frontend/tests/docker-compose.e2e-tests.yml b/frontend/tests/docker-compose.e2e-tests.yml new file mode 100644 index 000000000..9e7e75a81 --- /dev/null +++ b/frontend/tests/docker-compose.e2e-tests.yml @@ -0,0 +1,35 @@ +version: "3.9" + +services: + backend: + container_name: test-backend + build: + context: ../../backend/ + restart: always + environment: + - ALLOWED_HOSTS=backend,localhost + - DJANGO_SUPERUSER_EMAIL=${DJANGO_SUPERUSER_EMAIL} + - DJANGO_SUPERUSER_PASSWORD=${DJANGO_SUPERUSER_PASSWORD} + - CISO_ASSISTANT_URL=http://localhost:4173 + - DJANGO_DEBUG=True + ports: + - 8080:8000 + + tests: + container_name: functional-tests + environment: + - ORIGIN=http://localhost:4173 + - DOCKER=True + build: + context: ../../frontend/ + dockerfile: ./tests/Dockerfile + args: + - PUBLIC_BACKEND_API_URL=http://backend:8000/api + depends_on: + - backend + volumes: + - ./reports:/app/tests/reports + - ./results:/app/tests/results + ports: + - 9323:9323 # playwright reporter port + command: 'npx playwright test ${ARGS}' diff --git a/frontend/tests/e2e-tests.sh b/frontend/tests/e2e-tests.sh new file mode 100755 index 000000000..d161d5977 --- /dev/null +++ b/frontend/tests/e2e-tests.sh @@ -0,0 +1,64 @@ +#! /usr/bin/env bash +APP_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +DB_DIR=$APP_DIR/backend/db +DATABASE_BACKUP_NAME=ciso-assistant-backup.sqlite3 + +cleanup() { + echo -e "\nCleaning up..." + if [ -n "$BACKEND_PID" ] ; then + kill $BACKEND_PID + echo "| backend server stopped" + fi + if [ -f $DB_DIR/$DATABASE_BACKUP_NAME ] ; then + mv $DB_DIR/$DATABASE_BACKUP_NAME $DB_DIR/ciso-assistant.sqlite3 + else + rm $DB_DIR/ciso-assistant.sqlite3 + fi + echo "| database restored" + if [ -d $APP_DIR/frontend/tests/utils/.testhistory ] ; then + rm -rf $APP_DIR/frontend/tests/utils/.testhistory + echo "| test data history removed" + fi + trap - SIGINT SIGTERM EXIT + echo "Cleanup done" + exit 0 +} + +interrupt() { + echo "Test interrupted" + cleanup +} + +trap cleanup SIGINT SIGTERM +trap interrupt EXIT + +if [ -f $DB_DIR/ciso-assistant.sqlite3 ] ; then + echo "an existing database is already created" + echo "backup of the existing database..." + + mv $DB_DIR/ciso-assistant.sqlite3 $DB_DIR/$DATABASE_BACKUP_NAME + echo "backup completed" +fi + +echo "starting backend server..." +unset POSTGRES_NAME POSTGRES_USER POSTGRES_PASSWORD +CISO_ASSISTANT_URL=http://localhost:4173 +ALLOWED_HOSTS=localhost +DJANGO_DEBUG=True +DJANGO_SUPERUSER_EMAIL=admin@tests.com +DJANGO_SUPERUSER_PASSWORD=1234 + +cd $APP_DIR/backend/ +python manage.py makemigrations +python manage.py migrate +python manage.py createsuperuser --noinput +nohup python manage.py runserver 8080 > /dev/null 2>&1 & +BACKEND_PID=$! +echo "test backend server started on port 8080 (PID: $BACKEND_PID)" + +echo "starting playwright tests" +export ORIGIN=http://localhost:4173 +export PUBLIC_BACKEND_API_URL=http://localhost:8080/api + +cd $APP_DIR/frontend/ +npx playwright test ./tests/functional/$1 $2 $3 $4 $5 diff --git a/frontend/tests/functional/detailed/common.test.ts b/frontend/tests/functional/detailed/common.test.ts new file mode 100644 index 000000000..3f2cfd167 --- /dev/null +++ b/frontend/tests/functional/detailed/common.test.ts @@ -0,0 +1,79 @@ +import { test, expect, setHttpResponsesListener, TestContent, replaceValues } from '../../utils/test-utils.js'; +import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs'; +import { dirname } from 'path'; + +let items: { [k: string]: any } = TestContent.itemBuilder(); +let history: any = {}; + +function setFilePath(projectName: string, retry: number) { + file_path = `./tests/utils/.testhistory/${projectName}/hist${retry}.json`; + mkdirSync(dirname(file_path), { recursive: true }); + return file_path; +} + +let file_path = ""; +const testPages = Object.keys(items); + +test.describe.configure({mode: 'serial'}); +for (const key of testPages) { + test.describe(`Tests on ${items[key].displayName.toLowerCase()} item`, () => { + test.beforeAll(async ({}, testInfo) => { + setFilePath(testInfo.project.name, testInfo.retry); + existsSync(file_path) ? history = JSON.parse(readFileSync(file_path, 'utf8')) : writeFileSync(file_path, JSON.stringify(history)); + }); + + test.describe(`Tests on ${items[key].displayName.toLowerCase()} item details`, () => { + test.beforeEach(async ({ logedPage, pages, page }, testInfo) => { + await pages[key].goto(); + await expect(page).toHaveURL(pages[key].url); + + if (testInfo.line in history) { + items = history[testInfo.line] + } + else { + items = TestContent.itemBuilder(); + history[testInfo.line] = items; + } + + setHttpResponsesListener(page); + + await pages[key].createItem(items[key].build, "dependency" in items[key] ? items[key].dependency : null); + + if (await pages[key].getRow(items[key].build.name).isHidden()) { + //filter the item to the top of the list + await pages[key].collumnHeader('Name').click(); + } + + await pages[key].viewItemDetail(items[key].build.name); + await pages[key].itemDetail.hasTitle(); + }); + + test(`${items[key].displayName} item details are showing properly`, async ({ pages, page }) => { + await pages[key].itemDetail.verifyItem(items[key].build); + //wait fore the file to load to prevent crashing + page.url().includes('evidences') ? await pages[key].page.getByTestId("attachment-name-title").waitFor({state: 'visible'}) : null; + }); + + test(`user can edit ${items[key].displayName.toLowerCase()} item`, async ({ pages, page }, testInfo) => { + const editedValues = await pages[key].itemDetail.editItem(items[key].build, items[key].editParams); + replaceValues(history[testInfo.line], items[key].build.name, items[key].build.name + ' edited'); + //wait fore the file to load to prevent crashing + page.url().includes('evidences') ? await pages[key].page.getByTestId("attachment-name-title").waitFor({state: 'visible'}) : null; + + await pages[key].itemDetail.verifyItem(editedValues); + }); + }); + + test.afterAll(async () => { + writeFileSync(file_path, JSON.stringify(history)); + }); + + // test.afterEach('cleanup', async ({ pages, page }) => { + // await pages[key].goto() + // await page.waitForURL(pages[key].url); + // await pages[key].deleteItemButton(vars.folderName).click(); + // await pages[key].deleteModalConfirmButton.click(); + // await expect(pages[key].getRow(vars.folderName)).not.toBeVisible(); + // }); + }); +} diff --git a/frontend/tests/functional/detailed/folders.test.ts b/frontend/tests/functional/detailed/folders.test.ts new file mode 100644 index 000000000..16854e96a --- /dev/null +++ b/frontend/tests/functional/detailed/folders.test.ts @@ -0,0 +1,28 @@ +import { test, expect, setHttpResponsesListener, TestContent } from '../../utils/test-utils.js'; + +let vars: {[key: string]: any}; + +test.beforeEach(async ({ logedPage, foldersPage, sideBar, page }) => { + await sideBar.click("General", foldersPage.url); + await expect(page).toHaveURL(foldersPage.url); + + setHttpResponsesListener(page); + vars = TestContent.itemBuilder(); + + await foldersPage.createItem({ + name: vars.folderName, + description: vars.description + }); +}); + +test.describe('Tests on folder page', () => { + +}); + +test.afterEach('cleanup', async ({ foldersPage, page }) => { + await foldersPage.goto() + await page.waitForURL(foldersPage.url); + await foldersPage.deleteItemButton(vars.folderName).click(); + await foldersPage.deleteModalConfirmButton.click(); + await expect(foldersPage.getRow(vars.folderName)).not.toBeVisible(); +}); \ No newline at end of file diff --git a/frontend/tests/functional/detailed/login.test.ts b/frontend/tests/functional/detailed/login.test.ts new file mode 100644 index 000000000..492b59073 --- /dev/null +++ b/frontend/tests/functional/detailed/login.test.ts @@ -0,0 +1,35 @@ +import { test, baseTest, expect} from '../../utils/test-utils.js'; + +baseTest.beforeEach(async ({ page }) => { + await page.goto('/'); +}); + +baseTest.skip('login page as expected title', async ({ page }) => { + await expect.soft(page.getByRole('heading', { name: 'Hello there 👋' })).toBeVisible(); +}); + +test('login / logout process is working properly', async ({ loginPage, overviewPage, sideBar, page }) => { + await loginPage.hasUrl(1); + await expect.soft(page.getByRole('heading', { name: 'Login into your account' })).toBeVisible(); + await loginPage.login(); + await overviewPage.hasUrl(); + sideBar.moreButton.click(); + sideBar.logoutButton.click(); + await loginPage.hasUrl(0); +}); + +test('redirect to the right page after login', async ({ loginPage, page }) => { + await page.goto('/login?next=/calendar'); + await loginPage.hasUrl(1); + await loginPage.login(); + await expect(page).toHaveURL('/calendar'); +}); + +test('login invalid message is showing properly', async ({ loginPage, page }) => { + await loginPage.hasUrl(); + await loginPage.login('invalid@tests.com', '123456'); + await expect.soft(page.getByText('Unable to log in with provided credentials.')).toBeVisible(); + await loginPage.hasUrl(); +}); + +//TODO add test for the "forgot password" link \ No newline at end of file diff --git a/frontend/tests/functional/nav.test.ts b/frontend/tests/functional/nav.test.ts index eb1c396b2..75ea6b7df 100644 --- a/frontend/tests/functional/nav.test.ts +++ b/frontend/tests/functional/nav.test.ts @@ -1,76 +1,77 @@ -import { test, expect, httpResponsesListener } from '../utils/test-utils'; +import { test, expect, setHttpResponsesListener} from '../utils/test-utils.js'; type StringMap = { - [key: string]: string; + [key: string]: string; }; -test('sidebar navigation tests', async ({ logedPage, analyticsPage: analyticsPage, layout, sideBar, page }) => { - await test.step('proper redirection to the analytics page after login', async () => { - await analyticsPage.hasUrl(); - await analyticsPage.hasTitle(); - httpResponsesListener(page); - }); - - await test.step('navigation link are working properly', async () => { - //TODO delete this when page titles are fixed - const temporaryPageTitle: StringMap = { - 'Risk matrices': 'Risk matrices', - 'X-Rays': 'X rays', - 'Backup & restore': 'Backup restore' +test('sidebar navigation tests', async ({ logedPage, overviewPage, sideBar, page }) => { + test.slow(); + + await test.step('proper redirection to the overview page after login', async () => { + await overviewPage.hasUrl(); + await overviewPage.hasTitle(); + setHttpResponsesListener(page); + }); + + await test.step('navigation link are working properly', async () => { + //TODO delete this when page titles are fixed + const temporaryPageTitle: StringMap = { + 'X-Rays': "X rays", + 'Backup & restore': "Backup restore" }; + + for await (const [key, value] of sideBar.items) { + for await (const item of value) { + if (item.href !== '/role-assignments') { + await sideBar.click(key, item.href); + await expect(page).toHaveURL(item.href); + if (item.name in temporaryPageTitle) { + await expect.soft(logedPage.pageTitle).toHaveText(temporaryPageTitle[item.name]); + } else { + await expect.soft(logedPage.pageTitle).toHaveText(item.name); + } + } + } + } + }); - for await (const [key, value] of sideBar.items) { - for await (const item of value) { - if (item.href !== '/role-assignments') { - await sideBar.click(key, item.href); - await expect(page).toHaveURL(item.href); - if (item.name in temporaryPageTitle) { - await expect.soft(layout.pageTitle).toHaveText(temporaryPageTitle[item.name]); - } else { - await expect.soft(layout.pageTitle).toHaveText(item.name); - } - } - } - } - }); - - await test.step('user email is showing properly', async () => { - await expect(page.getByTestId('sidebar-user-account-display')).toHaveText(logedPage.email); - //TODO test also that user name and first name are displayed instead of the email when sets - }); - - await test.step('more panel links are working properly', async () => { - await sideBar.moreButton.click(); - await expect(sideBar.morePanel).not.toHaveAttribute('inert'); - await expect(sideBar.profileButton).toBeVisible(); - await sideBar.profileButton.click(); - await expect(sideBar.morePanel).toHaveAttribute('inert'); - await expect(page).toHaveURL('/profile'); - await expect.soft(layout.pageTitle).toHaveText('Profile'); - - await sideBar.moreButton.click(); - await expect(sideBar.morePanel).not.toHaveAttribute('inert'); - await expect(sideBar.aboutButton).toBeVisible(); - await sideBar.aboutButton.click(); - await expect(sideBar.morePanel).toHaveAttribute('inert'); - await expect(layout.modalTitle).toBeVisible(); - await expect.soft(layout.modalTitle).toHaveText('About CISO Assistant'); - await page.mouse.click(20, 20); // click outside the modal to close it - await expect(layout.modalTitle).not.toBeVisible(); - - await sideBar.moreButton.click(); - await expect(sideBar.morePanel).not.toHaveAttribute('inert'); - await expect(sideBar.logoutButton).toBeVisible(); - await sideBar.logoutButton.click(); - await logedPage.hasUrl(0); - }); + await test.step('user email is showing properly', async () => { + await expect(page.getByTestId('sidebar-user-account-display')).toHaveText(logedPage.email); + //TODO test also that user name and first name are displayed instead of the email when sets + }); + + await test.step('more panel links are working properly', async () => { + await sideBar.moreButton.click(); + await expect(sideBar.morePanel).not.toHaveAttribute('inert') + await expect(sideBar.profileButton).toBeVisible(); + await sideBar.profileButton.click(); + await expect(sideBar.morePanel).toHaveAttribute('inert') + await expect(page).toHaveURL('/profile'); + await expect.soft(logedPage.pageTitle).toHaveText('Profile'); + + await sideBar.moreButton.click(); + await expect(sideBar.morePanel).not.toHaveAttribute('inert') + await expect(sideBar.aboutButton).toBeVisible(); + await sideBar.aboutButton.click(); + await expect(sideBar.morePanel).toHaveAttribute('inert') + await expect(logedPage.modalTitle).toBeVisible(); + await expect.soft(logedPage.modalTitle).toHaveText('About CISO Assistant'); + await page.mouse.click(20, 20); // click outside the modal to close it + await expect(logedPage.modalTitle).not.toBeVisible(); + + await sideBar.moreButton.click(); + await expect(sideBar.morePanel).not.toHaveAttribute('inert') + await expect(sideBar.logoutButton).toBeVisible(); + await sideBar.logoutButton.click(); + await logedPage.hasUrl(0); + }); }); test('sidebar component tests', async ({ logedPage, sideBar, page }) => { - await test.step('sidebar can be collapsed and expanded', async () => { - sideBar.toggleButton.click(); - await expect(sideBar.toggleButton).toHaveClass(/rotate-180/); - sideBar.toggleButton.click(); - await expect(sideBar.toggleButton).not.toHaveClass(/rotate-180/); - }); + await test.step('sidebar can be collapsed and expanded', async () => { + sideBar.toggleButton.click(); + await expect(sideBar.toggleButton).toHaveClass(/rotate-180/); + sideBar.toggleButton.click(); + await expect(sideBar.toggleButton).not.toHaveClass(/rotate-180/); + }); }); diff --git a/frontend/tests/functional/startup.test.ts b/frontend/tests/functional/startup.test.ts index 5275a5dc3..254b61dff 100644 --- a/frontend/tests/functional/startup.test.ts +++ b/frontend/tests/functional/startup.test.ts @@ -1,15 +1,15 @@ -import { test } from '../utils/test-utils'; +import { test } from '../utils/test-utils.js'; -test('startup tests', async ({ loginPage, analyticsPage: analyticsPage, page }) => { - await test.step('proper redirection to the login page', async () => { - await page.goto('/'); - await loginPage.hasUrl(1); - await loginPage.login(); - await analyticsPage.hasUrl(); - }); +test('startup tests', async ({ loginPage, overviewPage, page }) => { + await test.step('proper redirection to the login page', async () => { + await page.goto('/'); + await loginPage.hasUrl(1); + await loginPage.login(); + await overviewPage.hasUrl(); + }); - await test.step('proper redirection to the analytics page after login', async () => { - await analyticsPage.hasUrl(); - await analyticsPage.hasTitle(); - }); + await test.step('proper redirection to the overview page after login', async () => { + await overviewPage.hasUrl(); + await overviewPage.hasTitle(); + }); }); diff --git a/frontend/tests/functional/user-route.test.ts b/frontend/tests/functional/user-route.test.ts index 66a7ce5b8..62c97381f 100644 --- a/frontend/tests/functional/user-route.test.ts +++ b/frontend/tests/functional/user-route.test.ts @@ -1,288 +1,263 @@ -import { dirname, join } from 'path'; -import { test, expect, httpResponsesListener, getUniqueValue as _ } from '../utils/test-utils'; -import { fileURLToPath } from 'url'; - -const testVars = { - assessmentName: _('Test assessment'), - assetName: _('Test asset'), - evidenceName: _('Test evidence'), - folderName: _('Test folder'), - projectName: _('Test project'), - riskAcceptanceName: _('Test risk acceptance'), - riskAnalysisName: _('Test risk analysis'), - riskScenarioName: _('Test risk scenario'), - securityFunctionName: _('Test security function'), - securityMeasureName: _('Test security measure'), - threatName: _('Test threat'), - description: 'Test description', - file: new URL('../utils/test_image.jpg', import.meta.url).pathname, - framework: { - name: 'NIS 2 requirements', - urn: 'urn:intuitem:risk:library:nis2' - }, - riskMatrix: { - name: 'Critical risk matrix 5x5', - displayName: 'default_5x5', - urn: 'urn:intuitem:risk:library:critical_risk_matrix_5x5' - }, - securityFunction: { - name: 'Physical security policy', - urn: 'urn:intuitem:risk:function:POL.PHYSICAL' - }, - threat: { - name: 'T1052 - Exfiltration Over Physical Medium', - urn: 'urn:intuitem:risk:threat:T1052' - } -}; - -test('user usual routine actions are working correctly', async ({ - logedPage, - pages, - analyticsPage: analyticsPage, - sideBar, - page -}) => { - test.slow(); - - await test.step('proper redirection to the analytics page after login', async () => { - await analyticsPage.hasUrl(); - await analyticsPage.hasTitle(); - httpResponsesListener(page); - }); - - await test.step('user can create a domain', async () => { - await sideBar.click('General', pages.foldersPage.url); - await expect(page).toHaveURL(pages.foldersPage.url); - await pages.foldersPage.hasTitle(); - - await pages.foldersPage.createItem({ - name: testVars.folderName, - description: testVars.description - }); - - //TODO assert that the folder data are displayed in the table - }); - - await test.step('user can create a project', async () => { - await sideBar.click('General', pages.projectsPage.url); - await expect(page).toHaveURL(pages.projectsPage.url); - await pages.projectsPage.hasTitle(); - - await pages.projectsPage.createItem({ - name: testVars.projectName, - description: testVars.description, - folder: testVars.folderName, - internal_reference: 'Test internal reference', - lc_status: 'Production' - }); - - //TODO assert that the project data are displayed in the table - }); - - await test.step('user can create an asset', async () => { - await sideBar.click('General', pages.assetsPage.url); - await expect(page).toHaveURL(pages.assetsPage.url); - await pages.assetsPage.hasTitle(); - - await pages.assetsPage.createItem({ - name: testVars.assetName, - description: testVars.description, - business_value: 'Test value', - folder: testVars.folderName, - type: 'Primary' - }); - - //TODO assert that the asset data are displayed in the table - }); - - await test.step('user can import a framework', async () => { - await sideBar.click('Compliance management', pages.frameworksPage.url); - await expect(page).toHaveURL(pages.frameworksPage.url); - await pages.frameworksPage.hasTitle(); - - await pages.frameworksPage.addButton.click(); - await expect(page).toHaveURL(pages.librariesPage.url); - await pages.librariesPage.hasTitle(); - - await pages.librariesPage.importLibrary(testVars.framework.name, testVars.framework.urn); - - await sideBar.click('Compliance management', pages.frameworksPage.url); - await expect(page).toHaveURL(pages.frameworksPage.url); - await expect(page.getByRole('row', { name: testVars.framework.name })).toBeVisible(); - }); - - await test.step('user can create a security function', async () => { - await sideBar.click('General', pages.securityFunctionsPage.url); - await expect(page).toHaveURL(pages.securityFunctionsPage.url); - await pages.securityFunctionsPage.hasTitle(); - - await pages.securityFunctionsPage.createItem({ - name: testVars.securityFunctionName, - description: testVars.description, - provider: 'Test provider', - folder: testVars.folderName - }); - - //TODO assert that the security function data are displayed in the table - }); - - await test.step('user can create a security measure', async () => { - await sideBar.click('General', pages.securityMeasuresPage.url); - await expect(page).toHaveURL(pages.securityMeasuresPage.url); - await pages.securityMeasuresPage.hasTitle(); - - await pages.securityMeasuresPage.createItem({ - name: testVars.securityMeasureName, - description: testVars.description, - type: 'Technical', - status: 'In progress', - eta: '2025-01-01', - link: 'https://intuitem.com/', - effort: 'Large', - folder: testVars.folderName, - security_function: testVars.securityFunction.name - }); - - //TODO assert that the security measure data are displayed in the table - }); - - await test.step('user can create an assessment', async () => { - await sideBar.click('Compliance management', pages.assessmentsPage.url); - await expect(page).toHaveURL(pages.assessmentsPage.url); - await pages.assessmentsPage.hasTitle(); - - await pages.assessmentsPage.createItem({ - name: testVars.assessmentName, - description: testVars.description, - project: testVars.projectName, - framework: testVars.framework.name, - version: '1.4.2', - is_draft: 'false', - is_obsolete: 'true' - }); - - //TODO assert that the assessment data are displayed in the table - }); - - await test.step('user can create an evidence', async () => { - await sideBar.click('Compliance management', pages.evidencesPage.url); - await expect(page).toHaveURL(pages.evidencesPage.url); - await pages.evidencesPage.hasTitle(); - - await pages.evidencesPage.createItem({ - name: testVars.evidenceName, - description: testVars.description, - attachment: testVars.file, - security_measure: testVars.securityMeasureName, - comment: 'Test comment' - }); - - //TODO assert that the evidence data are displayed in the table - }); - - await test.step('user can import a risk matrix', async () => { - await sideBar.click('Risk management', pages.riskMatricesPage.url); - await expect(page).toHaveURL(pages.riskMatricesPage.url); - await pages.riskMatricesPage.hasTitle(); - - await pages.riskMatricesPage.addButton.click(); - await expect(page).toHaveURL(pages.librariesPage.url); - await pages.librariesPage.hasTitle(); - - await pages.librariesPage.importLibrary(testVars.matrix.name, testVars.matrix.urn); - - await sideBar.click('Risk management', pages.riskMatricesPage.url); - await expect(page).toHaveURL(pages.riskMatricesPage.url); - await expect(page.getByRole('row', { name: testVars.matrix.displayName })).toBeVisible(); - // await expect(page.getByRole('row', { name: testVars.matrix.name })).toBeVisible(); - }); - - await test.step('user can create a risk analysis', async () => { - await sideBar.click('Risk management', pages.riskAnalysesPage.url); - await expect(page).toHaveURL(pages.riskAnalysesPage.url); - await pages.riskAnalysesPage.hasTitle(); - - await pages.riskAnalysesPage.createItem({ - name: testVars.riskAnalysisName, - description: testVars.description, - project: testVars.projectName, - version: '1.4.2', - is_draft: 'false', - auditor: logedPage.email, - risk_matrix: testVars.matrix.displayName - }); - - //TODO assert that the risk analysis data are displayed in the table - }); - - await test.step('user can create a threat', async () => { - await sideBar.click('General', pages.threatsPage.url); - await expect(page).toHaveURL(pages.threatsPage.url); - await pages.threatsPage.hasTitle(); - - await pages.threatsPage.createItem({ - name: testVars.threatName, - description: testVars.description, - folder: testVars.folderName, - provider: 'Test provider' - }); - - //TODO assert that the threat data are displayed in the table - }); - - await test.step('user can create a risk scenario', async () => { - await sideBar.click('Risk management', pages.riskScenariosPage.url); - await expect(page).toHaveURL(pages.riskScenariosPage.url); - await pages.riskScenariosPage.hasTitle(); - - await pages.riskScenariosPage.createItem({ - name: testVars.riskScenarioName, - description: testVars.description, - analysis: testVars.riskAnalysisName, - threat: testVars.threat.name - }); - - //TODO assert that the risk scenario data are displayed in the table - }); - - await test.step('user can create a risk acceptance', async () => { - await sideBar.click('Risk management', pages.riskAcceptancesPage.url); - await expect(page).toHaveURL(pages.riskAcceptancesPage.url); - await pages.riskAcceptancesPage.hasTitle(); - - await pages.riskAcceptancesPage.createItem({ - name: testVars.riskAcceptanceName, - description: testVars.description, - expiry_date: '2025-01-01', - justification: 'Test comment', - folder: testVars.folderName, - approver: logedPage.email, - risk_scenarios: testVars.riskScenarioName - }); - - //TODO assert that the risk acceptance data are displayed in the table - }); - - await test.step('cleanup', async () => { - //clean up test folder and associated objects - await sideBar.click('General', pages.foldersPage.url); - await expect(pages.foldersPage.deleteItemButton(testVars.folderName)).toBeVisible(); - await pages.foldersPage.deleteItemButton(testVars.folderName).click(); - await pages.foldersPage.deleteModalConfirmButton.click(); - await expect(pages.foldersPage.deleteModalTitle).not.toBeVisible(); - - // //clean up test framework - // await sideBar.click("Compliance management", pages.frameworksPage.url); - // await expect(pages.frameworksPage.deleteItemButton(testVars.framework.name)).toBeVisible(); - // await pages.frameworksPage.deleteItemButton(testVars.framework.name).click(); - // await pages.frameworksPage.deleteModalConfirmButton.click(); - // await expect(pages.frameworksPage.deleteModalTitle).not.toBeVisible(); - - // //clean up test risk matrix - // await sideBar.click("Risk management", pages.riskMatricesPage.url); - // await expect(pages.riskMatricesPage.deleteItemButton(testVars.matrix.displayName)).toBeVisible(); - // await pages.riskMatricesPage.deleteItemButton(testVars.matrix.displayName).click(); - // await pages.riskMatricesPage.deleteModalConfirmButton.click(); - // await expect(pages.riskMatricesPage.deleteModalTitle).not.toBeVisible(); - }); +import { LoginPage } from '../utils/login-page.js'; +import { test, expect, setHttpResponsesListener, TestContent } from '../utils/test-utils.js'; + +let vars = TestContent.generateTestVars(); + +test('user usual routine actions are working correctly', async ({ logedPage, pages, overviewPage, sideBar, page }) => { + test.slow(); + + await test.step('proper redirection to the overview page after login', async () => { + await overviewPage.hasUrl(); + await overviewPage.hasTitle(); + setHttpResponsesListener(page); + }); + + await test.step('user can create a domain', async () => { + await sideBar.click("General", pages.foldersPage.url); + await expect(page).toHaveURL(pages.foldersPage.url); + await pages.foldersPage.hasTitle(); + + await pages.foldersPage.createItem({ + name: vars.folderName, + description: vars.description + }); + + //TODO assert that the domain data are displayed in the table + }); + + await test.step('user can create a project', async () => { + await sideBar.click("General", pages.projectsPage.url); + await expect(page).toHaveURL(pages.projectsPage.url); + await pages.projectsPage.hasTitle(); + + await pages.projectsPage.createItem({ + name: vars.projectName, + description: vars.description, + folder: vars.folderName, + internal_reference: "Test internal reference", + lc_status: "Production" + }); + + //TODO assert that the project data are displayed in the table + }); + + await test.step('user can create an asset', async () => { + await sideBar.click("General", pages.assetsPage.url); + await expect(page).toHaveURL(pages.assetsPage.url); + await pages.assetsPage.hasTitle(); + + await pages.assetsPage.createItem({ + name: vars.assetName, + description: vars.description, + business_value: "Test value", + folder: vars.folderName, + type: "Primary" + }); + + //TODO assert that the asset data are displayed in the table + }); + + await test.step('user can import a framework', async () => { + await sideBar.click("Compliance management", pages.frameworksPage.url); + await expect(page).toHaveURL(pages.frameworksPage.url); + await pages.frameworksPage.hasTitle(); + + await pages.frameworksPage.addButton.click(); + await expect(page).toHaveURL(pages.librariesPage.url); + await pages.librariesPage.hasTitle(); + + await pages.librariesPage.importLibrary(vars.framework.name, vars.framework.urn); + + await sideBar.click("Compliance management", pages.frameworksPage.url); + await expect(page).toHaveURL(pages.frameworksPage.url); + await expect(page.getByRole('row', { name: vars.framework.name })).toBeVisible(); + }); + + await test.step('user can create a security function', async () => { + await sideBar.click("General", pages.securityFunctionsPage.url); + await expect(page).toHaveURL(pages.securityFunctionsPage.url); + await pages.securityFunctionsPage.hasTitle(); + + await pages.securityFunctionsPage.createItem({ + name: vars.securityFunctionName, + description: vars.description, + category: "Physical", + provider: "Test provider", + folder: vars.folderName + }); + + //TODO assert that the security function data are displayed in the table + }); + + await test.step('user can create a security measure', async () => { + await sideBar.click("General", pages.securityMeasuresPage.url); + await expect(page).toHaveURL(pages.securityMeasuresPage.url); + await pages.securityMeasuresPage.hasTitle(); + + await pages.securityMeasuresPage.createItem({ + name: vars.securityMeasureName, + description: vars.description, + category: "Technical", + status: "Planned", + eta: "2025-01-01", + link: "https://intuitem.com/", + effort: "Large", + folder: vars.folderName, + security_function: vars.securityFunctionName + }); + + //TODO assert that the security measure data are displayed in the table + }); + + await test.step('user can create a compliance assessment', async () => { + await sideBar.click("Compliance management", pages.complianceAssessmentsPage.url); + await expect(page).toHaveURL(pages.complianceAssessmentsPage.url); + await pages.complianceAssessmentsPage.hasTitle(); + + await pages.complianceAssessmentsPage.createItem({ + name: vars.assessmentName, + description: vars.description, + project: vars.projectName, + version: "1.4.2", + framework: vars.framework.name, + eta: "2025-01-01", + due_date: "2025-05-01" + }); + + //TODO assert that the compliance assessment data are displayed in the table + }); + + await test.step('user can create an evidence', async () => { + await sideBar.click("Compliance management", pages.evidencesPage.url); + await expect(page).toHaveURL(pages.evidencesPage.url); + await pages.evidencesPage.hasTitle(); + + await pages.evidencesPage.createItem({ + name: vars.evidenceName, + description: vars.description, + attachment: vars.file, + folder: vars.folderName, + security_measures: [vars.securityMeasureName], + requirement_assessments: [ + vars.assessmentName + ' - ' + vars.requirement_assessment.name, + vars.assessmentName + ' - ' + vars.requirement_assessment2.name + ], + link: "https://intuitem.com/" + }); + + //TODO assert that the evidence data are displayed in the table + }); + + await test.step('user can import a risk matrix', async () => { + await sideBar.click("Risk management", pages.riskMatricesPage.url); + await expect(page).toHaveURL(pages.riskMatricesPage.url); + await pages.riskMatricesPage.hasTitle(); + + await pages.riskMatricesPage.addButton.click(); + await expect(page).toHaveURL(pages.librariesPage.url); + await pages.librariesPage.hasTitle(); + + await pages.librariesPage.importLibrary(vars.matrix.name, vars.matrix.urn); + + await sideBar.click("Risk management", pages.riskMatricesPage.url); + await expect(page).toHaveURL(pages.riskMatricesPage.url); + await expect(page.getByRole('row', { name: vars.matrix.displayName })).toBeVisible(); + // await expect(page.getByRole('row', { name: testVars.matrix.name })).toBeVisible(); + }); + + await test.step('user can create a risk assessment', async () => { + await sideBar.click("Risk management", pages.riskAssessmentsPage.url); + await expect(page).toHaveURL(pages.riskAssessmentsPage.url); + await pages.riskAssessmentsPage.hasTitle(); + + await pages.riskAssessmentsPage.createItem({ + name: vars.riskAssessmentName, + description: vars.description, + project: vars.projectName, + version: "1.4.2", + risk_matrix: vars.matrix.displayName, + eta: "2025-01-01", + due_date: "2025-05-01" + }); + + //TODO assert that the risk assessment data are displayed in the table + }); + + await test.step('user can create a threat', async () => { + await sideBar.click("General", pages.threatsPage.url); + await expect(page).toHaveURL(pages.threatsPage.url); + await pages.threatsPage.hasTitle(); + + await pages.threatsPage.createItem({ + name: vars.threatName, + description: vars.description, + folder: vars.folderName, + provider: "Test provider" + }); + + //TODO assert that the threat data are displayed in the table + }); + + await test.step('user can create a risk scenario', async () => { + await sideBar.click("Risk management", pages.riskScenariosPage.url); + await expect(page).toHaveURL(pages.riskScenariosPage.url); + await pages.riskScenariosPage.hasTitle(); + + await pages.riskScenariosPage.createItem({ + name: vars.riskScenarioName, + description: vars.description, + risk_assessment: vars.riskAssessmentName, + threats: [vars.threat.name] + }); + + //TODO assert that the risk scenario data are displayed in the table + }); + + await test.step('user can create a risk acceptance', async () => { + await sideBar.click("Risk management", pages.riskAcceptancesPage.url); + await expect(page).toHaveURL(pages.riskAcceptancesPage.url); + await pages.riskAcceptancesPage.hasTitle(); + + await pages.riskAcceptancesPage.createItem({ + name: vars.riskAcceptanceName, + description: vars.description, + expiry_date: "2025-01-01", + folder: vars.folderName, + approver: LoginPage.defaultEmail, + risk_scenarios: [vars.riskScenarioName] + }); + + //TODO assert that the risk acceptance data are displayed in the table + }); + + // await test.step('cleanup', async () => { + // //clean up test folder and associated objects + // await sideBar.click("General", pages.foldersPage.url); + // await expect(pages.foldersPage.deleteItemButton(testVars.folderName)).toBeVisible(); + // await pages.foldersPage.deleteItemButton(testVars.folderName).click(); + // await pages.foldersPage.deleteModalConfirmButton.click(); + // await expect(pages.foldersPage.deleteModalTitle).not.toBeVisible(); + + // // //clean up test framework + // // await sideBar.click("Compliance management", pages.frameworksPage.url); + // // await expect(pages.frameworksPage.deleteItemButton(testVars.framework.name)).toBeVisible(); + // // await pages.frameworksPage.deleteItemButton(testVars.framework.name).click(); + // // await pages.frameworksPage.deleteModalConfirmButton.click(); + // // await expect(pages.frameworksPage.deleteModalTitle).not.toBeVisible(); + + // // //clean up test matrix + // // await sideBar.click("Risk management", pages.riskMatricesPage.url); + // // await expect(pages.riskMatricesPage.deleteItemButton(testVars.matrix.displayName)).toBeVisible(); + // // await pages.riskMatricesPage.deleteItemButton(testVars.matrix.displayName).click(); + // // await pages.riskMatricesPage.deleteModalConfirmButton.click(); + // // await expect(pages.riskMatricesPage.deleteModalTitle).not.toBeVisible(); + // }); }); + +test.afterEach('cleanup', async ({ foldersPage, page }) => { + await foldersPage.goto() + await page.waitForURL(foldersPage.url); + await foldersPage.deleteItemButton(vars.folderName).click(); + await foldersPage.deleteModalConfirmButton.click(); + await expect(foldersPage.getRow(vars.folderName)).not.toBeVisible(); +}); \ No newline at end of file diff --git a/frontend/tests/utils/analytics-page.ts b/frontend/tests/utils/analytics-page.ts index 5ffbb3f32..33a9c7d86 100644 --- a/frontend/tests/utils/analytics-page.ts +++ b/frontend/tests/utils/analytics-page.ts @@ -1,6 +1,5 @@ -import { expect, type Page } from './test-utils'; -import { PageContent } from './page-content'; -import { BasePage } from './base-page'; +import { expect, type Page } from './test-utils.js'; +import { BasePage } from './base-page.js'; export class AnalyticsPage extends BasePage { constructor(public readonly page: Page) { @@ -9,6 +8,6 @@ export class AnalyticsPage extends BasePage { } async hasTitle() { - await expect.soft(this.page.locator('#page-title')).toHaveText('Analytics'); + await expect.soft(this.pageTitle).toHaveText('Analytics'); } } diff --git a/frontend/tests/utils/base-page.ts b/frontend/tests/utils/base-page.ts index 1c5080ab4..2f934b54a 100644 --- a/frontend/tests/utils/base-page.ts +++ b/frontend/tests/utils/base-page.ts @@ -1,19 +1,34 @@ -import { expect, type Locator, type Page } from './test-utils'; +import { expect, type Locator, type Page } from './test-utils.js'; export abstract class BasePage { readonly url: string; readonly name: string | RegExp; + readonly pageTitle: Locator; + readonly modalTitle: Locator; constructor(public readonly page: Page, url: string, name: string | RegExp) { this.url = url; this.name = name; + this.pageTitle = this.page.locator('#page-title'); + this.modalTitle = this.page.getByTestId('modal-title'); } async goto() { await this.page.goto(this.url); + await this.page.waitForURL(this.url); } async hasUrl() { await expect(this.page).toHaveURL(this.url); } + + //TODO function to assert breadcrumb path is accurate + + async isToastVisible(value: string, flags?: string | undefined, options?: {} | undefined) { + const toast = this.page.getByTestId("toast").filter({hasText: new RegExp(value, flags)}); + await expect(toast).toBeVisible(options); + await toast.getByLabel('Dismiss toast').click(); + await expect(toast).toBeHidden(); + return toast; + } } diff --git a/frontend/tests/utils/form-content.ts b/frontend/tests/utils/form-content.ts index fce8d8b66..f48599ec6 100644 --- a/frontend/tests/utils/form-content.ts +++ b/frontend/tests/utils/form-content.ts @@ -1,76 +1,95 @@ -import { expect, type Locator, type Page } from './test-utils'; +import { expect, type Locator, type Page } from './test-utils.js'; export enum FormFieldType { - CHECKBOX = 'checkbox', - FILE = 'file', - SELECT = 'select', - SELECT_AUTOCOMPLETE = 'select-auto-complete', - TEXT = 'text' + CHECKBOX = "checkbox", + DATE = "date", + FILE = "file", + SELECT = "select", + SELECT_AUTOCOMPLETE = "select-autocomplete", + SELECT_MULTIPLE_AUTOCOMPLETE = "select-multi-autocomplete", + TEXT = "text", } type FormField = { - locator: Locator; - type: FormFieldType; + locator: Locator; + type: FormFieldType; }; export class FormContent { - readonly formTitle: Locator; - readonly saveButton: Locator; - readonly cancelButton: Locator; - readonly fields: Map; - name: string | RegExp; + readonly formTitle: Locator; + readonly saveButton: Locator; + readonly cancelButton: Locator; + readonly fields: Map; + name: string | RegExp; - constructor( - public readonly page: Page, - name: string | RegExp, - fields: { name: string; type: FormFieldType }[] - ) { - this.formTitle = this.page.getByTestId('modal-title'); - this.saveButton = this.page.getByTestId('save-button'); - this.cancelButton = this.page.getByTestId('cancel-button'); - this.name = name; - this.fields = new Map( - fields.map((field) => [ - field.name, - { - locator: this.page.getByTestId('form-input-' + field.name.replace('_', '-')), - type: field.type - } - ]) - ); - } + constructor(public readonly page: Page, name: string | RegExp, fields: {name: string, type: FormFieldType}[]) { + this.formTitle = this.page.getByTestId("modal-title"); + this.saveButton = this.page.getByTestId("save-button"); + this.cancelButton = this.page.getByTestId("cancel-button"); + this.name = name; + this.fields = new Map(fields.map(field => [field.name, {locator: this.page.getByTestId("form-input-" + field.name.replace('_', '-')), type: field.type}])); + } - async fill(values: { [k: string]: string }) { - for (const key in values) { - const field = this.fields.get(key); - switch (field?.type) { - case FormFieldType.CHECKBOX: - if (values[key] === 'true') { - await field.locator.check(); - } else if (values[key] === 'false') { - await field.locator.uncheck(); - } - break; - case FormFieldType.FILE: - await field.locator.setInputFiles(values[key]); - break; - case FormFieldType.SELECT: - await field.locator.selectOption(values[key]); - break; - case FormFieldType.SELECT_AUTOCOMPLETE: - await field.locator.click(); - await expect(this.page.locator('li', { hasText: values[key] })).toBeVisible(); - await this.page.locator('li', { hasText: values[key] }).click(); - break; - default: - await field?.locator.fill(values[key]); - break; - } - } - } + async fill(values: { [k: string]: any }) { + let temp = {}; - async hasTitle() { - await expect(this.formTitle).toBeVisible(); - await expect(this.formTitle).toHaveText(this.name); - } -} + for (const key in values) { + const field = this.fields.get(key); + + if (field?.locator.innerText !== values[key]) { + switch (field?.type) { + case FormFieldType.CHECKBOX: + if (values[key] === "true") { + await field.locator.check(); + } + else if (values[key] === "false") { + await field.locator.uncheck(); + } + break; + case FormFieldType.FILE: + await field.locator.setInputFiles(values[key]); + break; + case FormFieldType.SELECT: + await field.locator.selectOption(values[key]); + break; + case FormFieldType.SELECT_AUTOCOMPLETE: + await field.locator.click(); + + if (typeof values[key] === "object" && 'request' in values[key]) { + const responsePromise = this.page.waitForResponse(resp => resp.url().includes(values[key].request.url) && resp.status() === 200); + await expect(this.page.getByRole("option", {name: values[key].value, exact: true})).toBeVisible(); + await this.page.getByRole("option", {name: values[key].value, exact: true}).click(); + + const response = await responsePromise; + expect((await response.json()).category).toBe(values[key].category); + } else { + await expect(this.page.getByRole("option", {name: values[key], exact: true})).toBeVisible(); + await this.page.getByRole("option", {name: values[key], exact: true}).click(); + } + break; + case FormFieldType.SELECT_MULTIPLE_AUTOCOMPLETE: + await field.locator.click(); + for (const val of values[key]) { + await expect(this.page.getByRole("option", {name: val, exact: true})).toBeVisible(); + await this.page.getByRole("option", {name: val, exact: true}).click(); + } + if (await field.locator.isEnabled()) { + await field.locator.press("Escape"); + } + break; + case FormFieldType.DATE: + await field.locator.clear(); + default: + await field?.locator.fill(values[key]); + break; + } + } + // await this.page.waitForTimeout(20); + } + } + + async hasTitle() { + await expect(this.formTitle).toBeVisible(); + await expect(this.formTitle).toHaveText(this.name); + } +} \ No newline at end of file diff --git a/frontend/tests/utils/login-page.ts b/frontend/tests/utils/login-page.ts index 502d4083b..a3d642dd3 100644 --- a/frontend/tests/utils/login-page.ts +++ b/frontend/tests/utils/login-page.ts @@ -1,46 +1,52 @@ -import { expect, type Page } from './test-utils'; -import { BasePage } from './base-page'; +import { expect, type Page } from './test-utils.js'; +import { BasePage } from './base-page.js'; enum State { - Unset = -1, - False = 0, - True = 1 + Unset = -1, + False = 0, + True = 1 } -export class LoginPage extends BasePage { - readonly email: string = 'admin@tests.com'; - readonly password: string = '1234'; +export class LoginPage extends BasePage { + static readonly defaultEmail: string = 'admin@tests.com'; + static readonly defaultPassword: string = '1234'; + email: string; + password: string; - constructor(public readonly page: Page) { - super(page, '/login', 'Login'); - } + constructor(public readonly page: Page) { + super(page, '/login', 'Login'); + this.email = LoginPage.defaultEmail; + this.password = LoginPage.defaultPassword; + } - async login(email: string = this.email, password: string = this.password) { - await this.page.locator('input[name="username"]').fill(email); - await this.page.locator('input[name="password"]').fill(password); - await this.page.getByRole('button', { name: 'Log in' }).click(); - if (email === this.email && password === this.password) { - // await this.page.waitForURL('/[!login]*', { timeout: 10000 }); - await this.page.waitForURL(/^.*\/((?!login).)*$/, { timeout: 10000 }); - } else { - await this.page.waitForURL(/^.*\/login(\?next=\/.*)?$/); - } - } + async login(email: string=LoginPage.defaultEmail, password: string=LoginPage.defaultPassword) { + this.email = email; + this.password = password; + await this.page.locator('input[name="username"]').fill(email); + await this.page.locator('input[name="password"]').fill(password); + await this.page.getByRole('button', { name: 'Log in' }).click(); + if (email === LoginPage.defaultEmail && password === LoginPage.defaultPassword) { + await this.page.waitForURL(/^.*\/((?!login).)*$/, { timeout: 10000 }); + } + else { + await this.page.waitForURL(/^.*\/login(\?next=\/.*)?$/); + } + } - async hasUrl(redirect: State = State.Unset) { - switch (redirect) { - case State.Unset: - // url can be /login or /login?next=/ - await expect(this.page).toHaveURL(/^.*\/login(\?next=\/.*)?$/); - break; - case State.False: - // url must be /login - await expect(this.page).toHaveURL(/^.*\/login$/); - break; - case State.True: - //url must be /login?next=/ - await expect(this.page).toHaveURL(/^.*\/login\?next=\/.*$/); - break; - } - } -} + async hasUrl(redirect: State=State.Unset) { + switch (redirect) { + case State.Unset: + // url can be /login or /login?next=/ + await expect(this.page).toHaveURL(/^.*\/login(\?next=\/.*)?$/); + break; + case State.False: + // url must be /login + await expect(this.page).toHaveURL(/^.*\/login$/); + break; + case State.True: + //url must be /login?next=/ + await expect(this.page).toHaveURL(/^.*\/login\?next=\/.*$/); + break; + } + } +} \ No newline at end of file diff --git a/frontend/tests/utils/page-content.ts b/frontend/tests/utils/page-content.ts index 4a83f56cb..d9119bcce 100644 --- a/frontend/tests/utils/page-content.ts +++ b/frontend/tests/utils/page-content.ts @@ -1,81 +1,99 @@ -import { expect, type Locator, type Page } from './test-utils'; -import { FormContent, FormFieldType } from './form-content'; -import { BasePage } from './base-page'; +import { expect, type Locator, type Page } from './test-utils.js'; +import { FormContent, FormFieldType } from './form-content.js'; +import { BasePage } from './base-page.js'; +import { PageDetail } from './page-detail.js'; export class PageContent extends BasePage { - readonly form: FormContent; - readonly addButton: Locator; - readonly deleteModalTitle: Locator; - readonly deleteModalConfirmButton: Locator; - readonly deleteModalCancelButton: Locator; + readonly form: FormContent; + readonly itemDetail: PageDetail; + readonly addButton: Locator; + readonly editButton: Locator; + readonly deleteModalTitle: Locator; + readonly deleteModalConfirmButton: Locator; + readonly deleteModalCancelButton: Locator; - constructor( - public readonly page: Page, - url: string, - name: string | RegExp, - fields: { name: string; type: FormFieldType }[] = [ - { name: 'name', type: FormFieldType.TEXT }, - { name: 'description', type: FormFieldType.TEXT } - ] - ) { - super(page, url, name); - this.form = - typeof name == 'string' - ? new FormContent(page, 'New ' + name.substring(0, name.length - 1), fields) - : new FormContent(page, new RegExp(/New /.source + name.source), fields); - this.addButton = this.page.getByTestId('add-button'); - this.deleteModalTitle = this.page.getByTestId('modal-title'); - this.deleteModalConfirmButton = this.page.getByTestId('delete-confirm-button'); - this.deleteModalCancelButton = this.page.getByTestId('delete-cancel-button'); - } + constructor(public readonly page: Page, url: string, name: string | RegExp, fields: {name: string, type: FormFieldType}[] = [{name: "name", type: FormFieldType.TEXT}, {name: "description", type: FormFieldType.TEXT}]) { + super(page, url, name); + this.form = typeof name == 'string' ? new FormContent(page, "New " + name.substring(0, name.length - 1), fields) : new FormContent(page, new RegExp(/New /.source + name.source), fields); + this.itemDetail = new PageDetail(page, url, this.form, ""); + this.addButton = this.page.getByTestId("add-button"); + this.editButton = this.page.getByTestId("edit-button"); + this.deleteModalTitle = this.page.getByTestId("modal-title"); + this.deleteModalConfirmButton = this.page.getByTestId("delete-confirm-button"); + this.deleteModalCancelButton = this.page.getByTestId("delete-cancel-button"); + } - async hasTitle() { - await expect.soft(this.page.locator('#page-title')).toHaveText(this.name); - } + async hasTitle() { + await expect.soft(this.pageTitle).toHaveText(this.name); + } - async createItem(values: { [k: string]: string }) { - await this.addButton.click(); - await this.form.hasTitle(); - await this.form.fill(values); - await this.form.saveButton.click(); - await expect(this.form.formTitle).not.toBeVisible(); - if (typeof this.name == 'string') { - await expect( - this.page.getByTestId('toast').filter({ - hasText: new RegExp( - 'Successfully created ' + - this.url.substring(1, this.url.length - 1).replaceAll('-', ' ') + - '.' - ) - }) - ).toBeVisible(); - } else { - await expect( - this.page - .getByTestId('toast') - .filter({ hasText: new RegExp('Successfully created ' + this.name.source + '.', 'i') }) - ).toBeVisible(); + async createItem(values: { [k: string]: any }, dependency?: any) { + if (dependency) { + await this.page.goto('/libraries'); + await this.page.waitForURL('/libraries'); + + await this.importLibrary(dependency.name, dependency.urn); + await this.goto(); } - } - async importLibrary(name: string, urn: string) { - await this.importItemButton(name).click(); - await expect( - this.page - .getByTestId('toast') - .filter({ hasText: new RegExp('Successfully imported library ' + urn + '.') }) - ).toBeVisible({ timeout: 15000 }); - } + await this.addButton.click(); + await this.form.hasTitle(); + await this.form.fill(values); + await this.form.saveButton.click(); + await expect(this.form.formTitle).not.toBeVisible(); + if (typeof this.name == 'string') { + await this.isToastVisible('Successfully created ' + this.name.substring(0, this.name.length - 1).toLowerCase() + /\..+/.source); + } + else { + await this.isToastVisible('Successfully created ' + this.name.source + /\..+/.source, 'i'); + } + } + + async importLibrary(name: string, urn: string, language: string="English") { + if (await this.tab('Imported libraries').isVisible()) { + if (await this.getRow(name).isHidden()) { + await this.tab('Libraries store').click(); + expect(this.tab('Libraries store').getAttribute('aria-selected')).toBeTruthy(); + } else { + return; + } + } + await this.importItemButton(name, language).click(); + await this.isToastVisible('Successfully imported library ' + urn + '.+', undefined, { timeout: 15000}); + } + + async viewItemDetail(value?: string) { + if (value) { + await this.getRow(value).getByTestId("tablerow-detail-button").click(); + this.itemDetail.setItem(value); + } else { + await this.getRow().getByTestId("tablerow-detail-button").click(); + this.itemDetail.setItem(await this.getRow().innerText()); + } + await this.page.waitForURL(new RegExp("^.*\\" + this.url + "\/.+")); + } + + getRow(value?: string, additional?: any) { + return value ? additional ? this.page.getByRole('row', { name: value }).filter({ has: this.page.getByText(additional).first() }) : this.page.getByRole('row', { name: value }) : this.page.getByRole('row').first(); + } + + collumnHeader(value: string) { + return this.page.getByTestId("tableheader").filter({ hasText: value }); + } + + tab(value: string) { + return this.page.getByTestId('tab').filter({ hasText: value }); + } - editItemButton(value: string) { - return this.page.getByRole('row', { name: value }).getByTestId('tablerow-edit-button'); - } + editItemButton(value: string) { + return this.getRow(value).getByTestId("tablerow-edit-button"); + } - deleteItemButton(value: string) { - return this.page.getByRole('row', { name: value }).getByTestId('tablerow-delete-button'); - } + deleteItemButton(value: string) { + return this.getRow(value).getByTestId("tablerow-delete-button"); + } - importItemButton(value: string) { - return this.page.getByRole('row', { name: value }).getByTestId('tablerow-import-button'); - } -} + importItemButton(value: string, language?: string) { + return language ? this.getRow(value, language).getByTestId("tablerow-import-button") : this.getRow(value).getByTestId("tablerow-import-button").first(); + } +} \ No newline at end of file diff --git a/frontend/tests/utils/page-detail.ts b/frontend/tests/utils/page-detail.ts new file mode 100644 index 000000000..daf4b4680 --- /dev/null +++ b/frontend/tests/utils/page-detail.ts @@ -0,0 +1,91 @@ +import { expect, type Locator, type Page } from './test-utils.js'; +import { FormContent, FormFieldType } from './form-content.js'; +import { BasePage } from './base-page.js'; + +export class PageDetail extends BasePage { + readonly form: FormContent; + item: string; + readonly editButton: Locator; + + constructor(public readonly page: Page, url: string, form: FormContent, item: string) { + super(page, url, item); + this.form = form; + this.item = item; + this.editButton = this.page.getByTestId("edit-button"); + } + + async hasTitle() { + await expect.soft(this.pageTitle).toHaveText(this.item.replaceAll('-', ' ')); + } + + setItem(item: string) { + this.item = item; + } + + async editItem(buildParams: { [k: string]: string }, editParams: { [k: string]: string }) { + await this.editButton.click(); + + let editedValues: { [k: string]: string } = {}; + for (const key in editParams) { + editedValues[key] = (editParams[key] === "" ? buildParams[key] + ' edited' : editParams[key]); + } + + await this.form.fill(editedValues); + await this.form.saveButton.click(); + + await this.isToastVisible('.+ successfully saved: ' + {...buildParams, ...editedValues}.name); + return editedValues; + } + + async verifyItem(values: { [k: string]: any }) { + if (this.url.includes('risk-assessments')) { + if ('project' in values) { + await expect.soft(this.page.getByTestId("name-field-value")).toHaveText(`${values.project}/${values.name} - ${values.version}`); + } + else { + await expect.soft(this.page.getByTestId("name-field-value")).toHaveText(new RegExp(`.+/${values.name} - ${values.version}`)); + } + if ('risk_matrix' in values) { + await expect.soft(this.page.getByTestId("risk-matrix-field-title")).toHaveText("Risk matrix:"); + await expect.soft(this.page.getByTestId("risk-matrix-field-value")).toHaveText(values.risk_matrix); + } + + await expect.soft(this.page.getByTestId("description-field-title")).toHaveText("Description:"); + await expect.soft(this.page.getByTestId("description-field-value")).toHaveText(values.description); + } + else { + for (const key in values) { + if (await this.page.getByTestId(key.replaceAll('_', '-') + "-field-title").isVisible()) { + await expect.soft(this.page.getByTestId(key.replaceAll('_', '-') + "-field-title")).toHaveText(new RegExp(key.replaceAll('_', ' '), 'i')); + + if (this.form.fields.get(key)?.type === FormFieldType.DATE) { + const displayedValue = await this.page.getByTestId(key.replaceAll('_', '-') + "-field-value").innerText(); + + const displayedDate = new Date(displayedValue); + const date = new Date(values[key]); + + expect.soft(displayedValue).toMatch(/\d{1,2}\/\d{1,2}\/\d{4},\s\d{1,2}(:\d{1,2}){2} (AM|PM)/); + expect.soft(displayedDate.getFullYear()).toBe(date.getFullYear()); + expect.soft(displayedDate.getMonth()).toBe(date.getMonth()); + expect.soft(displayedDate.getDate()).toBe(date.getDate()); + } + else if (this.form.fields.get(key)?.type === FormFieldType.FILE) { + const displayedValue = await this.page.getByTestId(key.replaceAll('_', '-') + "-field-value").innerText(); + const fileName = values[key]?.split('/')?.pop()?.split('.') ?? []; + + expect.soft(displayedValue).toMatch(new RegExp(fileName[0] + '(_.{7})?' + '.' + fileName[1])); + } + else { + const value = this.page.getByTestId(key.replaceAll('_', '-') + "-field-value"); + if ((await value.allInnerTexts()).length > 1) { + await expect.soft(await value.allInnerTexts()).toHaveTextUnordered(typeof values[key] === 'object' ? values[key].value : values[key]); + } + else { + await expect.soft(value).toContainText(typeof values[key] === 'object' ? values[key].value : values[key], { ignoreCase: true }); + } + } + } + } + } + } +} \ No newline at end of file diff --git a/frontend/tests/utils/sidebar.ts b/frontend/tests/utils/sidebar.ts index 8e73e8102..455340681 100644 --- a/frontend/tests/utils/sidebar.ts +++ b/frontend/tests/utils/sidebar.ts @@ -1,6 +1,6 @@ -import { expect, type Locator, type Page } from './test-utils'; -import { navData } from '../../src/lib/components/SideBar/navData'; -import type { PageContent } from './page-content'; +import { expect, type Locator, type Page } from './test-utils.js'; +import { navData } from '../../src/lib/components/SideBar/navData.js'; +import type { PageContent } from './page-content.js'; type TabContent = { name: string; diff --git a/frontend/tests/utils/test-data.ts b/frontend/tests/utils/test-data.ts new file mode 100644 index 000000000..b7febac96 --- /dev/null +++ b/frontend/tests/utils/test-data.ts @@ -0,0 +1,73 @@ +export default { + assessmentName: "Test assessment", + assetName: "Test asset", + evidenceName: "Test evidence", + folderName: "Test domain", + projectName: "Test project", + riskAcceptanceName: "Test risk acceptance", + riskAssessmentName: "Test risk assessment", + riskScenarioName: "Test risk scenario", + securityFunctionName: "Test security function", + securityMeasureName: "Test security measure", + threatName: "Test threat", + description: "Test description", + file: new URL('../utils/test_image.jpg', import.meta.url).pathname, + file2: new URL('../utils/test_file.txt', import.meta.url).pathname, + framework: { + name: "NIST CSF", + urn: "urn:intuitem:risk:library:nist-csf-1_1" + }, + matrix: { + name: "Critical risk matrix 5x5", + displayName: "default_5x5", + urn: "urn:intuitem:risk:library:5x5_critical_risk_matrix" + }, + securityFunction: { + name: "Physical security policy", + category: "policy", + library: { + name: "Documents and policies", + urn: "urn:intuitem:risk:library:doc-pol" + }, + urn: "urn:intuitem:risk:function:POL.PHYSICAL" + }, + securityFunction2: { + name: "Cryptographic policy", + category: "policy", + library: { + name: "Documents and policies", + urn: "urn:intuitem:risk:library:doc-pol" + }, + urn: "urn:intuitem:risk:function:POL.CRYPTO" + }, + threat: { + name: "T1011 - Exfiltration Over Other Network Medium", + library: { + name: "Mitre ATT&CK v14 - Threats and mitigations", + urn: "urn:intuitem:risk:library:mitre-attack-v14" + }, + urn: "urn:intuitem:risk:threat:mitre-attack:T1011" + }, + threat2: { + name: "T1052 - Exfiltration Over Physical Medium", + library: { + name: "Mitre ATT&CK v14 - Threats and mitigations", + urn: "urn:intuitem:risk:library:mitre-attack-v14" + }, + urn: "urn:intuitem:risk:threat:mitre-attack:T1052" + }, + requirement_assessment: { + name: "RC.RP. Recovery Planning/RC.RP-1", + library: { + name: "NIST CSF", + urn: "urn:intuitem:risk:library:nist-csf-1_1" + }, + }, + requirement_assessment2: { + name: "ID.GV. Governance/ID.GV-4", + library: { + name: "NIST CSF", + urn: "urn:intuitem:risk:library:nist-csf-1_1" + }, + } +} as {[key: string]: any}; \ No newline at end of file diff --git a/frontend/tests/utils/test-utils.ts b/frontend/tests/utils/test-utils.ts index 7f2b988d8..21760d887 100644 --- a/frontend/tests/utils/test-utils.ts +++ b/frontend/tests/utils/test-utils.ts @@ -1,282 +1,512 @@ -import { test as base, expect as baseExpect, type Page } from '@playwright/test'; -import { SideBar } from './sidebar'; -import { Layout } from './layout'; -import { LoginPage } from './login-page'; -import { AnalyticsPage } from './analytics-page'; -import { PageContent } from './page-content'; -import { FormFieldType as type } from './form-content'; +import { test as base, expect as baseExpect, type Page} from '@playwright/test'; +import { SideBar } from './sidebar.js'; +import { LoginPage } from './login-page.js'; +import { OverviewPage } from './overview-page.js'; +import { PageContent } from './page-content.js'; +import { FormFieldType as type } from './form-content.js'; +import { randomBytes } from 'crypto'; +import testData from './test-data.js' type Fixtures = { - layout: Layout; - sideBar: SideBar; - pages: { [page: string]: PageContent }; - assessmentsPage: PageContent; - assetsPage: PageContent; - evidencesPage: PageContent; - foldersPage: PageContent; - frameworksPage: PageContent; - librariesPage: PageContent; - projectsPage: PageContent; - riskAcceptancesPage: PageContent; - riskAnalysesPage: PageContent; - riskMatricesPage: PageContent; - riskScenariosPage: PageContent; - securityFunctionsPage: PageContent; - securityMeasuresPage: PageContent; - threatsPage: PageContent; - analyticsPage: AnalyticsPage; - logedPage: LoginPage; - loginPage: LoginPage; + sideBar: SideBar; + pages: {[page: string]: PageContent} + complianceAssessmentsPage: PageContent; + assetsPage: PageContent; + evidencesPage: PageContent; + foldersPage: PageContent; + frameworksPage: PageContent; + librariesPage: PageContent; + projectsPage: PageContent; + riskAcceptancesPage: PageContent; + riskAssessmentsPage: PageContent; + riskMatricesPage: PageContent; + riskScenariosPage: PageContent; + securityFunctionsPage: PageContent; + securityMeasuresPage: PageContent; + threatsPage: PageContent; + overviewPage: OverviewPage; + logedPage: LoginPage; + loginPage: LoginPage; }; export const test = base.extend({ - layout: async ({ logedPage }, use) => { - await use(new Layout(logedPage.page)); - }, + sideBar: async ({ page }, use) => { + await use(new SideBar(page)); + }, - sideBar: async ({ page }, use) => { - await use(new SideBar(page)); - }, + pages: async ({ page, complianceAssessmentsPage, assetsPage, evidencesPage, foldersPage, frameworksPage, librariesPage, projectsPage, riskAcceptancesPage, riskAssessmentsPage, riskMatricesPage, riskScenariosPage, securityFunctionsPage, securityMeasuresPage, threatsPage }, use) => { + await use({complianceAssessmentsPage, assetsPage, evidencesPage, foldersPage, frameworksPage, librariesPage, projectsPage, riskAcceptancesPage, riskAssessmentsPage, riskMatricesPage, riskScenariosPage, securityFunctionsPage, securityMeasuresPage, threatsPage}); + }, - pages: async ( - { - page, - assessmentsPage, - assetsPage, - evidencesPage, - foldersPage, - frameworksPage, - librariesPage, - projectsPage, - riskAcceptancesPage, - riskAnalysesPage, - riskMatricesPage, - riskScenariosPage, - securityFunctionsPage, - securityMeasuresPage, - threatsPage - }, - use - ) => { - await use({ - assessmentsPage, - assetsPage, - evidencesPage, - foldersPage, - frameworksPage, - librariesPage, - projectsPage, - riskAcceptancesPage, - riskAnalysesPage, - riskMatricesPage, - riskScenariosPage, - securityFunctionsPage, - securityMeasuresPage, - threatsPage - }); - }, + complianceAssessmentsPage: async ({ page }, use) => { + const aPage = new PageContent(page, '/compliance-assessments', 'Compliance assessments', [ + { name: 'name', type: type.TEXT }, + { name: 'description', type: type.TEXT }, + { name: 'project', type: type.SELECT_AUTOCOMPLETE }, + { name: 'version', type: type.TEXT }, + { name: 'framework', type: type.SELECT_AUTOCOMPLETE }, + { name: 'eta', type: type.DATE }, + { name: 'due_date', type: type.DATE }, + ]); + await use(aPage); + }, - assessmentsPage: async ({ page }, use) => { - const aPage = new PageContent(page, '/assessments', 'Assessments', [ - { name: 'name', type: type.TEXT }, - { name: 'description', type: type.TEXT }, - { name: 'project', type: type.SELECT_AUTOCOMPLETE }, - { name: 'framework', type: type.SELECT_AUTOCOMPLETE }, - { name: 'version', type: type.TEXT }, - { name: 'is_draft', type: type.CHECKBOX }, - { name: 'is_obsolete', type: type.CHECKBOX } - ]); - await use(aPage); - }, + assetsPage: async ({ page }, use) => { + const aPage = new PageContent(page, '/assets', 'Assets', [ + { name: 'name', type: type.TEXT }, + { name: 'description', type: type.TEXT }, + { name: 'business_value', type: type.TEXT }, + { name: 'folder', type: type.SELECT_AUTOCOMPLETE }, + { name: 'type', type: type.SELECT }, + { name: 'parent_assets', type: type.SELECT_AUTOCOMPLETE }, + ]); + await use(aPage); + }, - assetsPage: async ({ page }, use) => { - const aPage = new PageContent(page, '/assets', 'Assets', [ - { name: 'name', type: type.TEXT }, - { name: 'description', type: type.TEXT }, - { name: 'business_value', type: type.TEXT }, - { name: 'folder', type: type.SELECT_AUTOCOMPLETE }, - { name: 'type', type: type.SELECT }, - { name: 'parent_assets', type: type.SELECT_AUTOCOMPLETE } - ]); - await use(aPage); - }, + evidencesPage: async ({ page }, use) => { + const ePage = new PageContent(page, '/evidences', 'Evidences', [ + { name: 'name', type: type.TEXT }, + { name: 'description', type: type.TEXT }, + { name: 'attachment', type: type.FILE }, + { name: 'folder', type: type.SELECT_AUTOCOMPLETE }, + { name: 'security_measures', type: type.SELECT_MULTIPLE_AUTOCOMPLETE }, + { name: 'requirement_assessments', type: type.SELECT_MULTIPLE_AUTOCOMPLETE }, + { name: 'link', type: type.TEXT }, + ]); + await use(ePage); + }, - evidencesPage: async ({ page }, use) => { - const ePage = new PageContent(page, '/evidences', 'Evidences', [ - { name: 'name', type: type.TEXT }, - { name: 'description', type: type.TEXT }, - { name: 'attachment', type: type.FILE }, - { name: 'security_measure', type: type.SELECT_AUTOCOMPLETE }, - { name: 'comment', type: type.TEXT } - ]); - await use(ePage); - }, + foldersPage: async ({ page }, use) => { + const fPage = new PageContent(page, '/folders', 'Domains'); + await use(fPage); + }, - foldersPage: async ({ page }, use) => { - const fPage = new PageContent(page, '/folders', 'Domains'); - fPage.form.name = 'New Folder'; - await use(fPage); - }, + frameworksPage: async ({ page }, use) => { + const fPage = new PageContent(page, '/frameworks', 'Frameworks'); + await use(fPage); + }, - frameworksPage: async ({ page }, use) => { - const fPage = new PageContent(page, '/frameworks', 'Frameworks'); - await use(fPage); - }, + librariesPage: async ({ page }, use) => { + const lPage = new PageContent(page, '/libraries', 'Libraries'); + await use(lPage); + }, - librariesPage: async ({ page }, use) => { - const lPage = new PageContent(page, '/libraries', 'Libraries'); - await use(lPage); - }, + projectsPage: async ({ page }, use) => { + const pPage = new PageContent(page, '/projects', 'Projects', [ + { name: 'name', type: type.TEXT }, + { name: 'description', type: type.TEXT }, + { name: 'folder', type: type.SELECT_AUTOCOMPLETE }, + { name: 'internal_reference', type: type.TEXT }, + { name: 'lc_status', type: type.SELECT_AUTOCOMPLETE }, + ]); + await use(pPage); + }, - projectsPage: async ({ page }, use) => { - const pPage = new PageContent(page, '/projects', 'Projects', [ - { name: 'name', type: type.TEXT }, - { name: 'description', type: type.TEXT }, - { name: 'folder', type: type.SELECT_AUTOCOMPLETE }, - { name: 'internal_reference', type: type.TEXT }, - { name: 'lc_status', type: type.SELECT_AUTOCOMPLETE } - ]); - await use(pPage); - }, + riskAcceptancesPage: async ({ page }, use) => { + const rPage = new PageContent(page, '/risk-acceptances', 'Risk acceptances', [ + { name: 'name', type: type.TEXT }, + { name: 'description', type: type.TEXT }, + { name: 'expiry_date', type: type.DATE }, + { name: 'folder', type: type.SELECT_AUTOCOMPLETE }, + { name: 'approver', type: type.SELECT_AUTOCOMPLETE }, + { name: 'risk_scenarios', type: type.SELECT_MULTIPLE_AUTOCOMPLETE }, + ]); + await use(rPage); + }, - riskAcceptancesPage: async ({ page }, use) => { - const rPage = new PageContent(page, '/risk-acceptances', 'Risk acceptances', [ - { name: 'name', type: type.TEXT }, - { name: 'description', type: type.TEXT }, - { name: 'expiry_date', type: type.TEXT }, - { name: 'justification', type: type.TEXT }, - { name: 'folder', type: type.SELECT_AUTOCOMPLETE }, - { name: 'approver', type: type.SELECT_AUTOCOMPLETE }, - { name: 'risk_scenarios', type: type.SELECT_AUTOCOMPLETE } - ]); - await use(rPage); - }, + riskAssessmentsPage: async ({ page }, use) => { + const rPage = new PageContent(page, '/risk-assessments', 'Risk assessments', [ + { name: 'name', type: type.TEXT }, + { name: 'description', type: type.TEXT }, + { name: 'project', type: type.SELECT_AUTOCOMPLETE }, + { name: 'version', type: type.TEXT }, + { name: 'risk_matrix', type: type.SELECT_AUTOCOMPLETE }, + { name: 'eta', type: type.DATE }, + { name: 'due_date', type: type.DATE }, + ]); + await use(rPage); + }, - riskAnalysesPage: async ({ page }, use) => { - const rPage = new PageContent(page, '/risk-analyses', /Risk analys[ie]s/, [ - { name: 'name', type: type.TEXT }, - { name: 'description', type: type.TEXT }, - { name: 'project', type: type.SELECT_AUTOCOMPLETE }, - { name: 'version', type: type.TEXT }, - { name: 'is_draft', type: type.CHECKBOX }, - { name: 'auditor', type: type.SELECT_AUTOCOMPLETE }, - { name: 'risk_matrix', type: type.SELECT_AUTOCOMPLETE } - ]); - await use(rPage); - }, + riskMatricesPage: async ({ page }, use) => { + const rPage = new PageContent(page, '/risk-matrices', 'Risk matrices'); + await use(rPage); + }, - riskMatricesPage: async ({ page }, use) => { - const rPage = new PageContent(page, '/risk-matrices', 'Risk matrices'); - await use(rPage); - }, + riskScenariosPage: async ({ page }, use) => { + const rPage = new PageContent(page, '/risk-scenarios', 'Risk scenarios', [ + { name: 'name', type: type.TEXT }, + { name: 'description', type: type.TEXT }, + { name: 'risk_assessment', type: type.SELECT_AUTOCOMPLETE }, + { name: 'threats', type: type.SELECT_MULTIPLE_AUTOCOMPLETE }, + { name: 'treatment', type: type.SELECT }, + { name: 'assets', type: type.SELECT_MULTIPLE_AUTOCOMPLETE }, + { name: 'existing_measures', type: type.TEXT }, + { name: 'current_proba', type: type.SELECT }, + { name: 'current_impact', type: type.SELECT }, + { name: 'security_measures', type: type.SELECT_MULTIPLE_AUTOCOMPLETE }, + { name: 'residual_proba', type: type.SELECT }, + { name: 'residual_impact', type: type.SELECT }, + { name: 'comments', type: type.TEXT }, + ]); + await use(rPage); + }, - riskScenariosPage: async ({ page }, use) => { - const rPage = new PageContent(page, '/risk-scenarios', 'Risk scenarios', [ - { name: 'name', type: type.TEXT }, - { name: 'description', type: type.TEXT }, - { name: 'analysis', type: type.SELECT_AUTOCOMPLETE }, - { name: 'threat', type: type.SELECT_AUTOCOMPLETE } - ]); - await use(rPage); - }, + securityFunctionsPage: async ({ page }, use) => { + const sPage = new PageContent(page, '/security-functions', 'Security functions', [ + { name: 'name', type: type.TEXT }, + { name: 'description', type: type.TEXT }, + { name: 'category', type: type.SELECT }, + { name: 'provider', type: type.TEXT }, + { name: 'folder', type: type.SELECT_AUTOCOMPLETE }, + ]); + await use(sPage); + }, - securityFunctionsPage: async ({ page }, use) => { - const sPage = new PageContent(page, '/security-functions', 'Security functions', [ - { name: 'name', type: type.TEXT }, - { name: 'description', type: type.TEXT }, - { name: 'provider', type: type.TEXT }, - { name: 'folder', type: type.SELECT_AUTOCOMPLETE } - ]); - await use(sPage); - }, + securityMeasuresPage: async ({ page }, use) => { + const sPage = new PageContent(page, '/security-measures', 'Security measures', [ + { name: 'name', type: type.TEXT }, + { name: 'description', type: type.TEXT }, + { name: 'category', type: type.SELECT }, + { name: 'status', type: type.SELECT }, + { name: 'eta', type: type.DATE }, + { name: 'expiry_date', type: type.DATE }, + { name: 'link', type: type.TEXT }, + { name: 'effort', type: type.SELECT }, + { name: 'folder', type: type.SELECT_AUTOCOMPLETE }, + { name: 'security_function', type: type.SELECT_AUTOCOMPLETE }, + ]); + await use(sPage); + }, - securityMeasuresPage: async ({ page }, use) => { - const sPage = new PageContent(page, '/security-measures', 'Security measures', [ - { name: 'name', type: type.TEXT }, - { name: 'description', type: type.TEXT }, - { name: 'type', type: type.SELECT }, - { name: 'status', type: type.SELECT }, - { name: 'eta', type: type.TEXT }, - { name: 'link', type: type.TEXT }, - { name: 'effort', type: type.SELECT }, - { name: 'folder', type: type.SELECT_AUTOCOMPLETE }, - { name: 'security_function', type: type.SELECT_AUTOCOMPLETE } - ]); - await use(sPage); - }, + threatsPage: async ({ page }, use) => { + const tPage = new PageContent(page, '/threats', 'Threats', [ + { name: 'name', type: type.TEXT }, + { name: 'description', type: type.TEXT }, + { name: 'folder', type: type.SELECT_AUTOCOMPLETE }, + { name: 'provider', type: type.TEXT }, + ]); + await use(tPage); + }, - threatsPage: async ({ page }, use) => { - const tPage = new PageContent(page, '/threats', 'Threats', [ - { name: 'name', type: type.TEXT }, - { name: 'description', type: type.TEXT }, - { name: 'folder', type: type.SELECT_AUTOCOMPLETE }, - { name: 'provider', type: type.TEXT } - ]); - await use(tPage); - }, + logedPage: async ({ page }, use) => { + const loginPage = new LoginPage(page); + await loginPage.goto(); + await loginPage.login(); + await use(loginPage); + }, - logedPage: async ({ page }, use) => { - const loginPage = new LoginPage(page); - await loginPage.goto(); - await loginPage.login(); - await use(loginPage); - }, + loginPage: async ({ page }, use) => { + await use(new LoginPage(page)); + }, - loginPage: async ({ page }, use) => { - await use(new LoginPage(page)); - }, - - analyticsPage: async ({ page }, use) => { - await use(new AnalyticsPage(page)); - } + overviewPage: async ({ page }, use) => { + await use(new OverviewPage(page)); + }, }); export const expect = baseExpect.extend({ - toBeOneofValues(received: number, expected: number[]) { - const pass = expected.includes(received); - if (pass) { - return { - pass: true, - message: () => `passed` - }; - } else { - return { - pass: false, - message: () => `expect(${received}).toBeOneofValues([${expected}])` - }; - } - }, + toBeOneofValues(received: number, expected: number[]) { + const pass = received >= expected[0] && received <= expected[1]; + if (pass) { + return { + pass: true, + message: () => `passed` + }; + } else { + return { + pass: false, + message: () => `expect(${received}).toBeOneofValues([${expected}])` + }; + } + }, + + toBeOneofStrings(received: string, expected: string[]) { + const pass = expected.includes(received); + if (pass) { + return { + pass: true, + message: () => `passed` + }; + } else { + return { + pass: false, + message: () => `expect(${received}).toBeOneofStrings([${expected}])` + }; + } + }, - toBeOneofStrings(received: string, expected: string[]) { - const pass = expected.includes(received); - if (pass) { - return { - pass: true, - message: () => `passed` - }; - } else { - return { - pass: false, - message: () => `expect(${received}).toBeOneofStrings([${expected}])` - }; - } - } + async toHaveTextUnordered(received: string[], expected: string[]) { + const pass = expected.every(value => received.includes(value)); + if (pass) { + return { + pass: true, + message: () => `passed` + }; + } else { + return { + pass: false, + message: () => `expect(${received}).toHaveTextUnordered([${expected}])` + }; + } + } }); -export function httpResponsesListener(page: Page) { - page.on('response', (response) => { - expect( - response.status(), - 'An error with status code ' + response.status() + ' occured when trying to achieve operation' - ).toBeLessThan(400); - }); - page.on('console', (message) => { - expect(message.type()).not.toBe('error'); - }); +export class TestContent { + static itemBuilder(vars: {[key: string]: any} = this.generateTestVars()) { + return { + foldersPage: { + displayName: "Domains", + build: { + name: vars.folderName, + description: vars.description + }, + editParams: { + name: "", + description: "" + } + }, + projectsPage: { + displayName: "Projects", + build: { + name: vars.projectName, + description: vars.description, + folder: vars.folderName, + internal_reference: "Test internal reference", + lc_status: "Production" + }, + editParams: { + name: "", + description: "", + internal_reference: "", + lc_status: "End Of Life" + } + }, + assetsPage: { + displayName: "Assets", + build: { + name: vars.assetName, + description: vars.description, + business_value: "Test value", + folder: vars.folderName, + type: "Primary" + }, + editParams: { + name: "", + description: "", + business_value: "", + type: "Support" + //TODO add parent_assets + } + }, + threatsPage: { + displayName: "Threats", + build : { + name: vars.threatName, + description: vars.description, + folder: vars.folderName, + provider: "Test provider" + }, + editParams: { + name: "", + description: "", + provider: "" + } + }, + securityFunctionsPage: { + displayName: "Security functions", + build : { + name: vars.securityFunctionName, + description: vars.description, + category: "Technical", + provider: "Test provider", + folder: vars.folderName + }, + editParams: { + name: "", + description: "", + category: "Physical", + provider: "" + } + }, + securityMeasuresPage: { + displayName: "Security measures", + dependency: vars.securityFunction.library, + build: { + security_function: { + value: vars.securityFunction.name, + category: vars.securityFunction.category, + request: { + url: "security-functions" + } + }, + name: vars.securityMeasureName, + description: vars.description, + status: "Planned", + eta: "2025-01-01", + expiry_date: "2025-05-01", + link: "https://intuitem.com/", + effort: "Large", + folder: vars.folderName, + category: vars.securityFunction.category + }, + editParams: { + security_function: { + value: vars.securityFunction2.name, + category: vars.securityFunction2.category, + request: { + url: "security-functions" + } + }, + name: "", + description: "", + status: "Active", + eta: "2025-12-31", + expiry_date: "2026-02-25", + link: "https://intuitem.com/community/", + effort: "Medium", + category: "Process" + } + }, + complianceAssessmentsPage: { + displayName: "Compliance assessments", + dependency: vars.framework, + build: { + name: vars.assessmentName, + description: vars.description, + project: vars.projectName, + // version: "1.4.2", + framework: vars.framework.name, + // eta: "2025-01-01", + // due_date: "2025-05-01" + }, + editParams: { + name: "", + description: "", + // version: "1.4.3", + //TODO add framework + // eta: "2025-12-31", + // due_date: "2026-02-25" + } + }, + evidencesPage: { + displayName: "Evidences", + dependency: vars.framework, + build : { + name: vars.evidenceName, + description: vars.description, + attachment: vars.file, + folder: vars.folderName, + security_measures: [vars.securityMeasureName], + requirement_assessments: [ + vars.assessmentName + ' - ' + vars.requirement_assessment.name, + vars.assessmentName + ' - ' + vars.requirement_assessment2.name + ], + link: "https://intuitem.com/", + }, + editParams: { + name: "", + description: "", + attachment: vars.file2, + link: "https://intuitem.com/community/", + } + }, + riskAssessmentsPage: { + displayName: "Risk assessments", + dependency: vars.matrix, + build : { + name: vars.riskAssessmentName, + description: vars.description, + project: vars.projectName, + version: "1.4.2", + risk_matrix: vars.matrix.displayName, + // eta: "2025-01-01", + // due_date: "2025-05-01" + }, + editParams: { + name: "", + description: "", + version: "1.4.3", + //TODO add risk_matrix + // eta: "2025-12-31", + // due_date: "2026-02-25" + } + }, + riskScenariosPage: { + displayName: "Risk scenarios", + dependency: vars.threat.library, + build : { + name: vars.riskScenarioName, + description: vars.description, + risk_assessment: vars.riskAssessmentName, + threats: [vars.threat.name, vars.threat2.name], + }, + editParams: { + name: "", + description: "", + treatment: "Accepted", + //TODO add risk_assessment & threats + assets: [vars.assetName], + existing_measures: "Test existing measures", + current_proba: "High", + current_impact: "Medium", + security_measures: [vars.securityMeasureName], + residual_proba: "Medium", + residual_impact: "Low", + comments: "Test comments" + } + }, + riskAcceptancesPage: { + displayName: "Risk acceptances", + build : { + name: vars.riskAcceptanceName, + description: vars.description, + expiry_date: "2025-01-01", + folder: vars.folderName, + approver: LoginPage.defaultEmail, + risk_scenarios: [vars.riskScenarioName] + }, + editParams: { + name: "", + description: "", + expiry_date: "2025-12-31", + //TODO add approver & risk_scenarios + } + } + } + } + + static generateTestVars() { + const vars = {...testData}; + for (const key in testData) { + vars[key] = key.match(/.*Name/) ? getUniqueValue(testData[key]) : testData[key]; + } + return vars + } +} + +export function setHttpResponsesListener(page: Page) { + page.on('response', response => { + // expect.soft(response.ok()).toBeTruthy(); + expect.soft(response.status()).toBeOneofValues([100, 399]); + // expect.soft(response.ok(), 'An error with status code ' + response.status() + ' occured when trying to achieve operation').toBeTruthy(); + }); + page.on('console', message => { + expect.soft(message.type()).not.toBe('error'); + }); } export function getUniqueValue(value: string) { - return process.env.TEST_WORKER_INDEX + '-' + value; + return process.env.TEST_WORKER_INDEX + '-' + value + '-' + randomBytes(2).toString('hex'); +} + +export function replaceValues(obj: any, searchValue: string, replaceValue: string) { + for (let key in obj) { + if (typeof obj[key] === 'object') { + replaceValues(obj[key], searchValue, replaceValue); + } else if (typeof obj[key] === 'string') { + obj[key] = obj[key].replace(searchValue, replaceValue); + } + } } -export { test as baseTest, type Page, type Locator } from '@playwright/test'; +export { test as baseTest, type Page, type Locator } from '@playwright/test'; \ No newline at end of file diff --git a/frontend/tests/utils/test_file.txt b/frontend/tests/utils/test_file.txt new file mode 100644 index 000000000..9944a9f24 --- /dev/null +++ b/frontend/tests/utils/test_file.txt @@ -0,0 +1 @@ +This is a test file \ No newline at end of file From 84437222288b161250fc6dbc085817e1aff258d3 Mon Sep 17 00:00:00 2001 From: Alexis Date: Mon, 12 Feb 2024 16:50:41 +0100 Subject: [PATCH 2/9] rename ex overview page to analytics + config update --- frontend/playwright.config.ts | 33 ++++++----- .../tests/functional/detailed/folders.test.ts | 28 ---------- .../tests/functional/detailed/login.test.ts | 2 +- frontend/tests/functional/login.test.ts | 40 ------------- frontend/tests/functional/nav.test.ts | 2 +- frontend/tests/functional/startup.test.ts | 2 +- frontend/tests/functional/user-route.test.ts | 56 ++++++------------- frontend/tests/utils/test-utils.ts | 8 +-- 8 files changed, 43 insertions(+), 128 deletions(-) delete mode 100644 frontend/tests/functional/detailed/folders.test.ts delete mode 100644 frontend/tests/functional/login.test.ts diff --git a/frontend/playwright.config.ts b/frontend/playwright.config.ts index 46d833396..a61b39f30 100644 --- a/frontend/playwright.config.ts +++ b/frontend/playwright.config.ts @@ -4,43 +4,50 @@ import { devices } from '@playwright/test'; const config: PlaywrightTestConfig = { webServer: { command: 'npm run build && npm run preview', - port: 4173 + port: 4173, + reuseExistingServer: !process.env.CI, }, testDir: 'tests', outputDir: 'tests/results', fullyParallel: true, forbidOnly: !!process.env.CI, retries: process.env.CI ? 2 : 1, - workers: process.env.CI ? 2 : 2, + workers: process.env.CI ? 1 : 2, globalTimeout: 60 * 60 * 1000, - timeout: 60 * 1000, + timeout: 50 * 1000, + expect : { + timeout: 7 * 1000 + }, reporter: [ [process.env.CI ? 'github' : 'list'], - ['html', { open: 'never', outputFolder: 'tests/reports' }] + ['html', { + open: process.env.CI ? 'never' : process.env.DOCKER ? 'always' : 'on-failure', + outputFolder: 'tests/reports', + host: process.env.DOCKER ? '0.0.0.0' : 'localhost' + }] ], use: { - // launchOptions: { - // slowMo: 1000, - // }, screenshot: 'only-on-failure', - video: 'retain-on-failure', - trace: 'retain-on-failure', + video: process.env.CI ? 'retain-on-failure' : 'on', + trace: process.env.CI ? 'retain-on-failure' : 'on', contextOptions: { - recordVideo: { dir: 'tests/results/videos' } + recordVideo: { dir: "tests/results/videos"} } }, projects: [ { name: 'chromium', - use: { ...devices['Desktop Chrome'] } + use: { ...devices['Desktop Chrome'] }, }, { name: 'firefox', - use: { ...devices['Desktop Firefox'] } - } + use: { ...devices['Desktop Firefox'] }, + }, // { // name: 'webkit', // use: { ...devices['Desktop Safari'] }, + // name: 'webkit', + // use: { ...devices['Desktop Safari'] }, // } ] }; diff --git a/frontend/tests/functional/detailed/folders.test.ts b/frontend/tests/functional/detailed/folders.test.ts deleted file mode 100644 index 16854e96a..000000000 --- a/frontend/tests/functional/detailed/folders.test.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { test, expect, setHttpResponsesListener, TestContent } from '../../utils/test-utils.js'; - -let vars: {[key: string]: any}; - -test.beforeEach(async ({ logedPage, foldersPage, sideBar, page }) => { - await sideBar.click("General", foldersPage.url); - await expect(page).toHaveURL(foldersPage.url); - - setHttpResponsesListener(page); - vars = TestContent.itemBuilder(); - - await foldersPage.createItem({ - name: vars.folderName, - description: vars.description - }); -}); - -test.describe('Tests on folder page', () => { - -}); - -test.afterEach('cleanup', async ({ foldersPage, page }) => { - await foldersPage.goto() - await page.waitForURL(foldersPage.url); - await foldersPage.deleteItemButton(vars.folderName).click(); - await foldersPage.deleteModalConfirmButton.click(); - await expect(foldersPage.getRow(vars.folderName)).not.toBeVisible(); -}); \ No newline at end of file diff --git a/frontend/tests/functional/detailed/login.test.ts b/frontend/tests/functional/detailed/login.test.ts index 492b59073..78dd9a77c 100644 --- a/frontend/tests/functional/detailed/login.test.ts +++ b/frontend/tests/functional/detailed/login.test.ts @@ -8,7 +8,7 @@ baseTest.skip('login page as expected title', async ({ page }) => { await expect.soft(page.getByRole('heading', { name: 'Hello there 👋' })).toBeVisible(); }); -test('login / logout process is working properly', async ({ loginPage, overviewPage, sideBar, page }) => { +test('login / logout process is working properly', async ({ loginPage, analyticsPage: overviewPage, sideBar, page }) => { await loginPage.hasUrl(1); await expect.soft(page.getByRole('heading', { name: 'Login into your account' })).toBeVisible(); await loginPage.login(); diff --git a/frontend/tests/functional/login.test.ts b/frontend/tests/functional/login.test.ts deleted file mode 100644 index f0151701f..000000000 --- a/frontend/tests/functional/login.test.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { test, baseTest, expect } from '../utils/test-utils'; - -baseTest.beforeEach(async ({ page }) => { - await page.goto('/'); -}); - -baseTest('login page as expected title', async ({ page }) => { - await expect.soft(page.getByRole('heading', { name: 'Hello there 👋' })).toBeVisible(); -}); - -test('login / logout process is working properly', async ({ - loginPage, - analyticsPage: analyticsPage, - sideBar, - page -}) => { - await loginPage.hasUrl(1); - await expect.soft(page.getByRole('heading', { name: 'Login into your account' })).toBeVisible(); - await loginPage.login(); - await analyticsPage.hasUrl(); - sideBar.moreButton.click(); - sideBar.logoutButton.click(); - await loginPage.hasUrl(0); -}); - -test('redirect to the right page after login', async ({ loginPage, page }) => { - await page.goto('/login?next=/calendar'); - await loginPage.hasUrl(1); - await loginPage.login(); - await expect(page).toHaveURL('/calendar'); -}); - -test('login invalid message is showing properly', async ({ loginPage, page }) => { - await loginPage.hasUrl(); - await loginPage.login('invalid@tests.com', '123456'); - await expect.soft(page.getByText('Unable to log in with provided credentials.')).toBeVisible(); - await loginPage.hasUrl(); -}); - -//TODO add test for the "forgot password" link diff --git a/frontend/tests/functional/nav.test.ts b/frontend/tests/functional/nav.test.ts index 75ea6b7df..491d1513a 100644 --- a/frontend/tests/functional/nav.test.ts +++ b/frontend/tests/functional/nav.test.ts @@ -4,7 +4,7 @@ type StringMap = { [key: string]: string; }; -test('sidebar navigation tests', async ({ logedPage, overviewPage, sideBar, page }) => { +test('sidebar navigation tests', async ({ logedPage, analyticsPage: overviewPage, sideBar, page }) => { test.slow(); await test.step('proper redirection to the overview page after login', async () => { diff --git a/frontend/tests/functional/startup.test.ts b/frontend/tests/functional/startup.test.ts index 254b61dff..828afcec9 100644 --- a/frontend/tests/functional/startup.test.ts +++ b/frontend/tests/functional/startup.test.ts @@ -1,6 +1,6 @@ import { test } from '../utils/test-utils.js'; -test('startup tests', async ({ loginPage, overviewPage, page }) => { +test('startup tests', async ({ loginPage, analyticsPage: overviewPage, page }) => { await test.step('proper redirection to the login page', async () => { await page.goto('/'); await loginPage.hasUrl(1); diff --git a/frontend/tests/functional/user-route.test.ts b/frontend/tests/functional/user-route.test.ts index 62c97381f..be60f4b23 100644 --- a/frontend/tests/functional/user-route.test.ts +++ b/frontend/tests/functional/user-route.test.ts @@ -3,7 +3,7 @@ import { test, expect, setHttpResponsesListener, TestContent } from '../utils/te let vars = TestContent.generateTestVars(); -test('user usual routine actions are working correctly', async ({ logedPage, pages, overviewPage, sideBar, page }) => { +test('user usual routine actions are working correctly', async ({ logedPage, pages, analyticsPage: overviewPage, sideBar, page }) => { test.slow(); await test.step('proper redirection to the overview page after login', async () => { @@ -13,7 +13,7 @@ test('user usual routine actions are working correctly', async ({ logedPage, pag }); await test.step('user can create a domain', async () => { - await sideBar.click("General", pages.foldersPage.url); + await sideBar.click("Organisation", pages.foldersPage.url); await expect(page).toHaveURL(pages.foldersPage.url); await pages.foldersPage.hasTitle(); @@ -26,7 +26,7 @@ test('user usual routine actions are working correctly', async ({ logedPage, pag }); await test.step('user can create a project', async () => { - await sideBar.click("General", pages.projectsPage.url); + await sideBar.click("Organisation", pages.projectsPage.url); await expect(page).toHaveURL(pages.projectsPage.url); await pages.projectsPage.hasTitle(); @@ -42,7 +42,7 @@ test('user usual routine actions are working correctly', async ({ logedPage, pag }); await test.step('user can create an asset', async () => { - await sideBar.click("General", pages.assetsPage.url); + await sideBar.click("Context", pages.assetsPage.url); await expect(page).toHaveURL(pages.assetsPage.url); await pages.assetsPage.hasTitle(); @@ -58,7 +58,7 @@ test('user usual routine actions are working correctly', async ({ logedPage, pag }); await test.step('user can import a framework', async () => { - await sideBar.click("Compliance management", pages.frameworksPage.url); + await sideBar.click("Compliance", pages.frameworksPage.url); await expect(page).toHaveURL(pages.frameworksPage.url); await pages.frameworksPage.hasTitle(); @@ -68,13 +68,13 @@ test('user usual routine actions are working correctly', async ({ logedPage, pag await pages.librariesPage.importLibrary(vars.framework.name, vars.framework.urn); - await sideBar.click("Compliance management", pages.frameworksPage.url); + await sideBar.click("Compliance", pages.frameworksPage.url); await expect(page).toHaveURL(pages.frameworksPage.url); await expect(page.getByRole('row', { name: vars.framework.name })).toBeVisible(); }); await test.step('user can create a security function', async () => { - await sideBar.click("General", pages.securityFunctionsPage.url); + await sideBar.click("Context", pages.securityFunctionsPage.url); await expect(page).toHaveURL(pages.securityFunctionsPage.url); await pages.securityFunctionsPage.hasTitle(); @@ -90,7 +90,7 @@ test('user usual routine actions are working correctly', async ({ logedPage, pag }); await test.step('user can create a security measure', async () => { - await sideBar.click("General", pages.securityMeasuresPage.url); + await sideBar.click("Context", pages.securityMeasuresPage.url); await expect(page).toHaveURL(pages.securityMeasuresPage.url); await pages.securityMeasuresPage.hasTitle(); @@ -110,7 +110,7 @@ test('user usual routine actions are working correctly', async ({ logedPage, pag }); await test.step('user can create a compliance assessment', async () => { - await sideBar.click("Compliance management", pages.complianceAssessmentsPage.url); + await sideBar.click("Compliance", pages.complianceAssessmentsPage.url); await expect(page).toHaveURL(pages.complianceAssessmentsPage.url); await pages.complianceAssessmentsPage.hasTitle(); @@ -128,7 +128,7 @@ test('user usual routine actions are working correctly', async ({ logedPage, pag }); await test.step('user can create an evidence', async () => { - await sideBar.click("Compliance management", pages.evidencesPage.url); + await sideBar.click("Compliance", pages.evidencesPage.url); await expect(page).toHaveURL(pages.evidencesPage.url); await pages.evidencesPage.hasTitle(); @@ -149,7 +149,7 @@ test('user usual routine actions are working correctly', async ({ logedPage, pag }); await test.step('user can import a risk matrix', async () => { - await sideBar.click("Risk management", pages.riskMatricesPage.url); + await sideBar.click("Governance", pages.riskMatricesPage.url); await expect(page).toHaveURL(pages.riskMatricesPage.url); await pages.riskMatricesPage.hasTitle(); @@ -159,14 +159,13 @@ test('user usual routine actions are working correctly', async ({ logedPage, pag await pages.librariesPage.importLibrary(vars.matrix.name, vars.matrix.urn); - await sideBar.click("Risk management", pages.riskMatricesPage.url); + await sideBar.click("Governance", pages.riskMatricesPage.url); await expect(page).toHaveURL(pages.riskMatricesPage.url); await expect(page.getByRole('row', { name: vars.matrix.displayName })).toBeVisible(); - // await expect(page.getByRole('row', { name: testVars.matrix.name })).toBeVisible(); }); await test.step('user can create a risk assessment', async () => { - await sideBar.click("Risk management", pages.riskAssessmentsPage.url); + await sideBar.click("Risk", pages.riskAssessmentsPage.url); await expect(page).toHaveURL(pages.riskAssessmentsPage.url); await pages.riskAssessmentsPage.hasTitle(); @@ -184,7 +183,7 @@ test('user usual routine actions are working correctly', async ({ logedPage, pag }); await test.step('user can create a threat', async () => { - await sideBar.click("General", pages.threatsPage.url); + await sideBar.click("Context", pages.threatsPage.url); await expect(page).toHaveURL(pages.threatsPage.url); await pages.threatsPage.hasTitle(); @@ -199,7 +198,7 @@ test('user usual routine actions are working correctly', async ({ logedPage, pag }); await test.step('user can create a risk scenario', async () => { - await sideBar.click("Risk management", pages.riskScenariosPage.url); + await sideBar.click("Risk", pages.riskScenariosPage.url); await expect(page).toHaveURL(pages.riskScenariosPage.url); await pages.riskScenariosPage.hasTitle(); @@ -214,7 +213,7 @@ test('user usual routine actions are working correctly', async ({ logedPage, pag }); await test.step('user can create a risk acceptance', async () => { - await sideBar.click("Risk management", pages.riskAcceptancesPage.url); + await sideBar.click("Risk", pages.riskAcceptancesPage.url); await expect(page).toHaveURL(pages.riskAcceptancesPage.url); await pages.riskAcceptancesPage.hasTitle(); @@ -229,29 +228,6 @@ test('user usual routine actions are working correctly', async ({ logedPage, pag //TODO assert that the risk acceptance data are displayed in the table }); - - // await test.step('cleanup', async () => { - // //clean up test folder and associated objects - // await sideBar.click("General", pages.foldersPage.url); - // await expect(pages.foldersPage.deleteItemButton(testVars.folderName)).toBeVisible(); - // await pages.foldersPage.deleteItemButton(testVars.folderName).click(); - // await pages.foldersPage.deleteModalConfirmButton.click(); - // await expect(pages.foldersPage.deleteModalTitle).not.toBeVisible(); - - // // //clean up test framework - // // await sideBar.click("Compliance management", pages.frameworksPage.url); - // // await expect(pages.frameworksPage.deleteItemButton(testVars.framework.name)).toBeVisible(); - // // await pages.frameworksPage.deleteItemButton(testVars.framework.name).click(); - // // await pages.frameworksPage.deleteModalConfirmButton.click(); - // // await expect(pages.frameworksPage.deleteModalTitle).not.toBeVisible(); - - // // //clean up test matrix - // // await sideBar.click("Risk management", pages.riskMatricesPage.url); - // // await expect(pages.riskMatricesPage.deleteItemButton(testVars.matrix.displayName)).toBeVisible(); - // // await pages.riskMatricesPage.deleteItemButton(testVars.matrix.displayName).click(); - // // await pages.riskMatricesPage.deleteModalConfirmButton.click(); - // // await expect(pages.riskMatricesPage.deleteModalTitle).not.toBeVisible(); - // }); }); test.afterEach('cleanup', async ({ foldersPage, page }) => { diff --git a/frontend/tests/utils/test-utils.ts b/frontend/tests/utils/test-utils.ts index 21760d887..f1dbeba5a 100644 --- a/frontend/tests/utils/test-utils.ts +++ b/frontend/tests/utils/test-utils.ts @@ -1,7 +1,7 @@ import { test as base, expect as baseExpect, type Page} from '@playwright/test'; import { SideBar } from './sidebar.js'; import { LoginPage } from './login-page.js'; -import { OverviewPage } from './overview-page.js'; +import { AnalyticsPage } from './analytics-page.js'; import { PageContent } from './page-content.js'; import { FormFieldType as type } from './form-content.js'; import { randomBytes } from 'crypto'; @@ -24,7 +24,7 @@ type Fixtures = { securityFunctionsPage: PageContent; securityMeasuresPage: PageContent; threatsPage: PageContent; - overviewPage: OverviewPage; + analyticsPage: AnalyticsPage; logedPage: LoginPage; loginPage: LoginPage; }; @@ -199,8 +199,8 @@ export const test = base.extend({ await use(new LoginPage(page)); }, - overviewPage: async ({ page }, use) => { - await use(new OverviewPage(page)); + analyticsPage: async ({ page }, use) => { + await use(new AnalyticsPage(page)); }, }); From 22f0d03dee010bf8c4b6a35eccf491bb294aa5ab Mon Sep 17 00:00:00 2001 From: Alexis Date: Mon, 12 Feb 2024 18:19:27 +0100 Subject: [PATCH 3/9] fix item retrieval issue in item lists during tests --- frontend/src/lib/components/ModelTable/Search.svelte | 1 + frontend/tests/e2e-tests.sh | 4 ++-- frontend/tests/functional/detailed/common.test.ts | 3 +-- frontend/tests/utils/page-content.ts | 5 +++++ 4 files changed, 9 insertions(+), 4 deletions(-) diff --git a/frontend/src/lib/components/ModelTable/Search.svelte b/frontend/src/lib/components/ModelTable/Search.svelte index 2a0b1bc9a..9744f686a 100644 --- a/frontend/src/lib/components/ModelTable/Search.svelte +++ b/frontend/src/lib/components/ModelTable/Search.svelte @@ -6,6 +6,7 @@ handler.search(value)} diff --git a/frontend/tests/e2e-tests.sh b/frontend/tests/e2e-tests.sh index d161d5977..1a52ca6c8 100755 --- a/frontend/tests/e2e-tests.sh +++ b/frontend/tests/e2e-tests.sh @@ -5,8 +5,8 @@ DATABASE_BACKUP_NAME=ciso-assistant-backup.sqlite3 cleanup() { echo -e "\nCleaning up..." - if [ -n "$BACKEND_PID" ] ; then - kill $BACKEND_PID + if [ -n $BACKEND_PID ] ; then + kill $BACKEND_PID > /dev/null 2>&1 echo "| backend server stopped" fi if [ -f $DB_DIR/$DATABASE_BACKUP_NAME ] ; then diff --git a/frontend/tests/functional/detailed/common.test.ts b/frontend/tests/functional/detailed/common.test.ts index 3f2cfd167..097005a24 100644 --- a/frontend/tests/functional/detailed/common.test.ts +++ b/frontend/tests/functional/detailed/common.test.ts @@ -40,8 +40,7 @@ for (const key of testPages) { await pages[key].createItem(items[key].build, "dependency" in items[key] ? items[key].dependency : null); if (await pages[key].getRow(items[key].build.name).isHidden()) { - //filter the item to the top of the list - await pages[key].collumnHeader('Name').click(); + await pages[key].searchInput.fill(items[key].build.name); } await pages[key].viewItemDetail(items[key].build.name); diff --git a/frontend/tests/utils/page-content.ts b/frontend/tests/utils/page-content.ts index d9119bcce..d7d81c481 100644 --- a/frontend/tests/utils/page-content.ts +++ b/frontend/tests/utils/page-content.ts @@ -8,6 +8,7 @@ export class PageContent extends BasePage { readonly itemDetail: PageDetail; readonly addButton: Locator; readonly editButton: Locator; + readonly searchInput: Locator; readonly deleteModalTitle: Locator; readonly deleteModalConfirmButton: Locator; readonly deleteModalCancelButton: Locator; @@ -18,6 +19,7 @@ export class PageContent extends BasePage { this.itemDetail = new PageDetail(page, url, this.form, ""); this.addButton = this.page.getByTestId("add-button"); this.editButton = this.page.getByTestId("edit-button"); + this.searchInput = this.page.getByTestId("search-input"); this.deleteModalTitle = this.page.getByTestId("modal-title"); this.deleteModalConfirmButton = this.page.getByTestId("delete-confirm-button"); this.deleteModalCancelButton = this.page.getByTestId("delete-cancel-button"); @@ -60,6 +62,9 @@ export class PageContent extends BasePage { } await this.importItemButton(name, language).click(); await this.isToastVisible('Successfully imported library ' + urn + '.+', undefined, { timeout: 15000}); + await this.tab('Imported libraries').click(); + expect(this.tab('Imported libraries').getAttribute('aria-selected')).toBeTruthy(); + expect(this.getRow(name)).toBeVisible(); } async viewItemDetail(value?: string) { From 6aed0aedc5b6a56d41bc78e612bcc0226db09bc6 Mon Sep 17 00:00:00 2001 From: Alexis Date: Mon, 12 Feb 2024 18:54:06 +0100 Subject: [PATCH 4/9] fix missing exports in e2e-tests.sh --- frontend/tests/e2e-tests.sh | 10 +++++----- frontend/tests/utils/test-data.ts | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/frontend/tests/e2e-tests.sh b/frontend/tests/e2e-tests.sh index 1a52ca6c8..cf0366d0a 100755 --- a/frontend/tests/e2e-tests.sh +++ b/frontend/tests/e2e-tests.sh @@ -42,11 +42,11 @@ fi echo "starting backend server..." unset POSTGRES_NAME POSTGRES_USER POSTGRES_PASSWORD -CISO_ASSISTANT_URL=http://localhost:4173 -ALLOWED_HOSTS=localhost -DJANGO_DEBUG=True -DJANGO_SUPERUSER_EMAIL=admin@tests.com -DJANGO_SUPERUSER_PASSWORD=1234 +export CISO_ASSISTANT_URL=http://localhost:4173 +export ALLOWED_HOSTS=localhost +export DJANGO_DEBUG=True +export DJANGO_SUPERUSER_EMAIL=admin@tests.com +export DJANGO_SUPERUSER_PASSWORD=1234 cd $APP_DIR/backend/ python manage.py makemigrations diff --git a/frontend/tests/utils/test-data.ts b/frontend/tests/utils/test-data.ts index b7febac96..4fd830b7f 100644 --- a/frontend/tests/utils/test-data.ts +++ b/frontend/tests/utils/test-data.ts @@ -15,7 +15,7 @@ export default { file2: new URL('../utils/test_file.txt', import.meta.url).pathname, framework: { name: "NIST CSF", - urn: "urn:intuitem:risk:library:nist-csf-1_1" + urn: "urn:intuitem:risk:library:nist-csf-1.1" }, matrix: { name: "Critical risk matrix 5x5", @@ -60,14 +60,14 @@ export default { name: "RC.RP. Recovery Planning/RC.RP-1", library: { name: "NIST CSF", - urn: "urn:intuitem:risk:library:nist-csf-1_1" + urn: "urn:intuitem:risk:library:nist-csf-1.1" }, }, requirement_assessment2: { name: "ID.GV. Governance/ID.GV-4", library: { name: "NIST CSF", - urn: "urn:intuitem:risk:library:nist-csf-1_1" + urn: "urn:intuitem:risk:library:nist-csf-1.1" }, } } as {[key: string]: any}; \ No newline at end of file From 50847fa1264e81f97da6934689be68fa8de90148 Mon Sep 17 00:00:00 2001 From: Alexis Date: Tue, 13 Feb 2024 21:46:38 +0100 Subject: [PATCH 5/9] fix all functional tests --- frontend/.gitignore | 1 + .../(app)/evidences/[id=uuid]/+page.svelte | 4 +- .../risk-assessments/[id=uuid]/+page.svelte | 4 +- frontend/tests/e2e-tests.sh | 47 +++++++++-- frontend/tests/functional/nav.test.ts | 3 +- frontend/tests/functional/user-route.test.ts | 6 +- frontend/tests/utils/form-content.ts | 82 +++++++++---------- frontend/tests/utils/page-detail.ts | 4 +- frontend/tests/utils/test-data.ts | 18 ++-- frontend/tests/utils/test-utils.ts | 12 +-- 10 files changed, 110 insertions(+), 71 deletions(-) diff --git a/frontend/.gitignore b/frontend/.gitignore index f09e87191..b05bcd4a7 100644 --- a/frontend/.gitignore +++ b/frontend/.gitignore @@ -19,5 +19,6 @@ vite.config.js.timestamp-* .vercel .test-tmp .testhistory +.testbackendoutput.out symlink-from coverage/** diff --git a/frontend/src/routes/(app)/evidences/[id=uuid]/+page.svelte b/frontend/src/routes/(app)/evidences/[id=uuid]/+page.svelte index 16b8e8eda..622395834 100644 --- a/frontend/src/routes/(app)/evidences/[id=uuid]/+page.svelte +++ b/frontend/src/routes/(app)/evidences/[id=uuid]/+page.svelte @@ -71,13 +71,13 @@ {/if}
                -
              • +
              • {#if value} {#if Array.isArray(value)}
                  {#if value.length > 0} {#each value as val} -
                • +
                • {#if val.str && val.id} {@const itemHref = `/${ URL_MODEL_MAP[data.URLModel]['foreignKeyFields']?.find( diff --git a/frontend/src/routes/(app)/risk-assessments/[id=uuid]/+page.svelte b/frontend/src/routes/(app)/risk-assessments/[id=uuid]/+page.svelte index b2b95acb6..45b0bbe92 100644 --- a/frontend/src/routes/(app)/risk-assessments/[id=uuid]/+page.svelte +++ b/frontend/src/routes/(app)/risk-assessments/[id=uuid]/+page.svelte @@ -154,7 +154,7 @@
                  - Remediation plan