diff --git a/e2e/package-lock.json b/e2e/package-lock.json index 2a07571dd..e31dc2f06 100644 --- a/e2e/package-lock.json +++ b/e2e/package-lock.json @@ -5,6 +5,9 @@ "packages": { "": { "name": "tests_end_to_end", + "dependencies": { + "dotenv": "^16.4.5" + }, "devDependencies": { "@playwright/test": "^1.47.0", "@types/node": "^22.5.4", @@ -129,6 +132,17 @@ "node": ">=0.4.0" } }, + "node_modules/dotenv": { + "version": "16.4.5", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", + "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/follow-redirects": { "version": "1.15.6", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", diff --git a/e2e/package.json b/e2e/package.json index 168c34ae3..1b8a4b53b 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -7,11 +7,15 @@ "e2e": "playwright test", "e2e:ui": "playwright test --ui", "e2e:report": "playwright show-report", + "record": "playwright codegen", "test": "npm run start && npm run e2e && npm run stop" }, "devDependencies": { "@playwright/test": "^1.47.0", "@types/node": "^22.5.4", "wait-on": "^7.2.0" + }, + "dependencies": { + "dotenv": "^16.4.5" } } diff --git a/e2e/playwright.config.js b/e2e/playwright.config.js index a97fe1573..b557a7709 100644 --- a/e2e/playwright.config.js +++ b/e2e/playwright.config.js @@ -1,5 +1,6 @@ // @ts-check -const { defineConfig, devices } = require('@playwright/test'); +import { defineConfig, devices } from '@playwright/test'; +import fs from 'node:fs'; /** * Read environment variables from file. @@ -7,6 +8,30 @@ const { defineConfig, devices } = require('@playwright/test'); */ // require('dotenv').config({ path: path.resolve(__dirname, '.env') }); +const BROWSERS = ['Chrome', 'Firefox']; +const files = fs.readdirSync('tests', { withFileTypes: true }); +const users = files.filter(file => file.isDirectory()).map(file => file.name); + +// setup project +let projects = [{ + name: 'setup', + testMatch: 'auth.setup.js', +}]; + +for (const browser of BROWSERS) { + for (const user of users) { + projects.push({ + name: `${browser}:${user}`, + use: { + ...devices[`Desktop ${browser}`], + storageState: `playwright/.auth/${user}.json`, + }, + testDir: `./tests/${user}`, + dependencies: ['setup'], + }) + } +} + /** * @see https://playwright.dev/docs/test-configuration */ @@ -33,56 +58,7 @@ module.exports = defineConfig({ }, /* Configure projects for major browsers */ - projects: [ - // Setup project - { - name: 'setup', - testMatch: /.*\.setup\.js/, - }, - { - name: 'chromium', - use: { - ...devices['Desktop Chrome'], - // Use prepared auth state. - storageState: 'playwright/.auth/user.json', - }, - dependencies: ['setup'], - }, - { - name: 'firefox', - use: { - ...devices['Desktop Firefox'], - // Use prepared auth state. - storageState: 'playwright/.auth/user.json', - }, - dependencies: ['setup'], - }, - - // { - // name: 'webkit', - // use: { ...devices['Desktop Safari'] }, - // }, - - /* Test against mobile viewports. */ - // { - // name: 'Mobile Chrome', - // use: { ...devices['Pixel 5'] }, - // }, - // { - // name: 'Mobile Safari', - // use: { ...devices['iPhone 12'] }, - // }, - - /* Test against branded browsers. */ - // { - // name: 'Microsoft Edge', - // use: { ...devices['Desktop Edge'], channel: 'msedge' }, - // }, - // { - // name: 'Google Chrome', - // use: { ...devices['Desktop Chrome'], channel: 'chrome' }, - // }, - ], + projects: projects, /* Run your local dev server before starting the tests */ // webServer: { diff --git a/e2e/tests/home_page.spec.js b/e2e/tests/administrator/dashboard.spec.js similarity index 84% rename from e2e/tests/home_page.spec.js rename to e2e/tests/administrator/dashboard.spec.js index c4544825d..3c2d31b35 100644 --- a/e2e/tests/home_page.spec.js +++ b/e2e/tests/administrator/dashboard.spec.js @@ -1,6 +1,6 @@ import { test, expect } from '@playwright/test'; -test('successfully loads', async ({ page }) => { +test('create case with empty name should present error', async ({ page }) => { await page.goto('/dashboard'); // FIXME: Should be a button instead of a link @@ -10,4 +10,3 @@ test('successfully loads', async ({ page }) => { // FIXME: Locator should be: page.getByRole('alert', { name: 'Invalid data type' }); await expect(page.getByText('Invalid data type')).toBeVisible(); }); - diff --git a/e2e/tests/administrator/manage/customers.spec.js b/e2e/tests/administrator/manage/customers.spec.js new file mode 100644 index 000000000..8806b5999 --- /dev/null +++ b/e2e/tests/administrator/manage/customers.spec.js @@ -0,0 +1,7 @@ +import { test, expect } from '@playwright/test'; + +test('should be able to open "Add customer" modal', async ({ page }) => { + await page.goto('/manage/customers'); + await page.getByRole('button', { name: 'Add customer' }).click(); + await expect(page.getByRole('heading', { name: 'Add customer' })).toBeVisible() +}); diff --git a/e2e/tests/auth.setup.js b/e2e/tests/auth.setup.js index 3e9c79c9e..d65d35bea 100644 --- a/e2e/tests/auth.setup.js +++ b/e2e/tests/auth.setup.js @@ -1,19 +1,85 @@ -import { test as setup } from '@playwright/test'; +import { test as setup, expect } from '@playwright/test'; import path from 'path'; +import dotenv from 'dotenv'; +import fs from 'node:fs'; -const authFile = path.join(__dirname, '../playwright/.auth/user.json'); +const _API_URL = 'http://127.0.0.1:8000'; -const username = "administrator"; -const password = "MySuperAdminPassword!"; +const _PERMISSION_CUSTOMERS_READ = 0x40; +const _ADMINISTRATOR_USERNAME = 'administrator'; -setup('authenticate', async ({ page }) => { +let apiContext; +let administrator_password; + +setup.beforeAll(async ({ playwright }) => { + const envFile = fs.readFileSync('../.env'); + const env = dotenv.parse(envFile); + + administrator_password = env.IRIS_ADM_PASSWORD + + apiContext = await playwright.request.newContext({ + baseURL: _API_URL, + extraHTTPHeaders: { + 'Authorization': `Bearer ${env.IRIS_ADM_API_KEY}`, + 'Content-Type': 'application/json' + }, + }); +}); + +async function authenticate(page, login, password) { await page.goto('/'); - await page.getByRole('textbox', { name: 'Username' }).fill(username); + await page.getByRole('textbox', { name: 'Username' }).fill(login); await page.getByRole('textbox', { name: 'Password' }).fill(password); await page.getByRole('button', { name: 'Sign in' }).click(); // FIXME: It should be: await page.waitForURL('/dashboard'); No wildcard. // Wait until the page receives the cookies. await page.waitForURL('/dashboard*'); + const authFile = path.join(__dirname, `../playwright/.auth/${login}.json`); await page.context().storageState({ path: authFile }); -}); \ No newline at end of file +} + +setup('authenticate as administrator', async ({ page }) => { + await authenticate(page, _ADMINISTRATOR_USERNAME, administrator_password); +}); + +setup('authenticate as user with customers read rights', async ({ page }) => { + // TODO when this method is called a second time, all these request will fail + // think about a better ways of doing things, some possible strategies + // - find a way to create a new valid database before and empty the database after + // - find a way to remove elements from the database to roughly get back to the initial state + // - code so that these requests are robust (check the group exists, user exists, link between the two is set...) + // - global setup and teardown? https://playwright.dev/docs/test-global-setup-teardown + let response = await apiContext.post('/manage/groups/add', { + data: { + group_name: 'group_customers_r', + group_description: 'Group with rights: customers_read', + group_permissions: [_PERMISSION_CUSTOMERS_READ] + } + }); + const groupIdentifier = (await response.json()).data.group_id; + const login = 'user_customers_r'; + const password = 'aA.1234567890'; + response = await apiContext.post('/manage/users/add', { + data: { + user_name: login, + user_login: login, + user_email: `${login}@eu`, + user_password: password + } + }); + const userIdentifier = (await response.json()).data.id; + response = await apiContext.post(`/manage/users/${userIdentifier}/groups/update`, { + data: { + groups_membership: [groupIdentifier] + } + }); + + await authenticate(page, login, password); +}); + +setup.afterAll(async ({ }) => { + // Dispose all responses. + await apiContext.dispose(); +}); + diff --git a/e2e/tests/user_customers_r/manage/customers.spec.js b/e2e/tests/user_customers_r/manage/customers.spec.js new file mode 100644 index 000000000..8806b5999 --- /dev/null +++ b/e2e/tests/user_customers_r/manage/customers.spec.js @@ -0,0 +1,7 @@ +import { test, expect } from '@playwright/test'; + +test('should be able to open "Add customer" modal', async ({ page }) => { + await page.goto('/manage/customers'); + await page.getByRole('button', { name: 'Add customer' }).click(); + await expect(page.getByRole('heading', { name: 'Add customer' })).toBeVisible() +}); diff --git a/source/app/blueprints/pages/manage/manage_customers_routes.py b/source/app/blueprints/pages/manage/manage_customers_routes.py index 69748030e..3800c82a7 100644 --- a/source/app/blueprints/pages/manage/manage_customers_routes.py +++ b/source/app/blueprints/pages/manage/manage_customers_routes.py @@ -129,7 +129,6 @@ def view_customer_modal(client_id, caseid, url_redir): @manage_customers_blueprint.route('/manage/customers/add/modal', methods=['GET']) @ac_requires(Permissions.customers_read, no_cid_required=True) -@ac_requires_client_access() def add_customers_modal(caseid, url_redir): if url_redir: return redirect(url_for('manage_customers.manage_customers', cid=caseid)) diff --git a/source/app/blueprints/rest/manage/manage_customers_routes.py b/source/app/blueprints/rest/manage/manage_customers_routes.py index 1d250cb5a..bd5d24df7 100644 --- a/source/app/blueprints/rest/manage/manage_customers_routes.py +++ b/source/app/blueprints/rest/manage/manage_customers_routes.py @@ -240,7 +240,6 @@ def view_customers(client_id): @manage_customers_rest_blueprint.route('/manage/customers/add', methods=['POST']) @ac_api_requires(Permissions.customers_write) -@ac_api_requires_client_access() def add_customers(): if not request.is_json: return response_error("Invalid request") diff --git a/source/app/blueprints/rest/manage/manage_groups.py b/source/app/blueprints/rest/manage/manage_groups.py index 3f7ef7137..4f1e7b1ab 100644 --- a/source/app/blueprints/rest/manage/manage_groups.py +++ b/source/app/blueprints/rest/manage/manage_groups.py @@ -68,11 +68,11 @@ def manage_groups_index(): def manage_groups_add(): if not request.is_json: - return response_error("Invalid request, expecting JSON") + return response_error('Invalid request, expecting JSON') data = request.get_json() if not data: - return response_error("Invalid request, expecting JSON") + return response_error('Invalid request, expecting JSON') ags = AuthorizationGroupSchema() @@ -85,9 +85,9 @@ def manage_groups_add(): db.session.commit() except marshmallow.exceptions.ValidationError as e: - return response_error(msg="Data error", data=e.messages) + return response_error(msg='Data error', data=e.messages) - track_activity(message=f"added group {ags_c.group_name}", ctx_less=True) + track_activity(message=f'added group {ags_c.group_name}', ctx_less=True) return response_success('', data=ags.dump(ags_c)) diff --git a/tests/iris.py b/tests/iris.py index 368389025..e35dee9ef 100644 --- a/tests/iris.py +++ b/tests/iris.py @@ -25,17 +25,20 @@ from user import User API_URL = 'http://127.0.0.1:8000' +# TODO SSOT: this could be directly read from the .env file _API_KEY = 'B8BA5D730210B50F41C06941582D7965D57319D5685440587F98DFDC45A01594' _IRIS_PATH = Path('..') _TEST_DATA_PATH = Path('./data') +_ADMINISTRATOR_USER_IDENTIFIER = 1 class Iris: def __init__(self): self._docker_compose = DockerCompose(_IRIS_PATH, 'docker-compose.dev.yml') + # TODO remove this field and use _administrator instead self._api = RestApi(API_URL, _API_KEY) - self._administrator = User(API_URL, _API_KEY, 1) + self._administrator = User(API_URL, _API_KEY, _ADMINISTRATOR_USER_IDENTIFIER) self._user_count = 0 def _wait(self, condition, attempts, sleep_duration=1): diff --git a/tests/tests_rest.py b/tests/tests_rest.py index 1d108c9cf..c6d82b6f5 100644 --- a/tests/tests_rest.py +++ b/tests/tests_rest.py @@ -22,6 +22,7 @@ _INITIAL_DEMO_CASE_IDENTIFIER = 1 _GROUP_ANALYSTS_IDENTIFIER = 2 _CASE_ACCESS_LEVEL_FULL_ACCESS = 4 +_PERMISSION_CUSTOMERS_WRITE = 0x80 # TODO should change None into 123456789 and maybe fix... _IDENTIFIER_FOR_NONEXISTENT_OBJECT = None @@ -571,3 +572,20 @@ def test_delete_task_should_return_403_when_user_has_insufficient_rights(self): response = user.delete(f'/api/v2/tasks/{task_identifier}') self.assertEqual(403, response.status_code) + + def test_create_customer_should_return_200_when_user_has_customer_write_right(self): + body = { + 'group_name': 'Customer create', + 'group_description': 'Group with customers_write right', + 'group_permissions': [_PERMISSION_CUSTOMERS_WRITE] + } + response = self._subject.create('/manage/groups/add', body).json() + group_identifier = response['data']['group_id'] + user = self._subject.create_dummy_user() + body = {'groups_membership': [group_identifier]} + self._subject.create(f'/manage/users/{user.get_identifier()}/groups/update', body) + + body = {'custom_attributes': {}, 'customer_description': '', 'customer_name': 'Customer', 'customer_sla': ''} + response = user.create('/manage/customers/add', body) + + self.assertEqual(200, response.status_code) diff --git a/tests/user.py b/tests/user.py index ff669aac7..7bd9870ed 100644 --- a/tests/user.py +++ b/tests/user.py @@ -42,4 +42,4 @@ def get(self, path): return self._api.get(path) def delete(self, path): - return self._api.delete(path) \ No newline at end of file + return self._api.delete(path)