From 4702aaaa2b88f01f11235f5769806498bf4db4fe Mon Sep 17 00:00:00 2001 From: Novak Zaballa <41410593+novakzaballa@users.noreply.github.com> Date: Tue, 21 Jan 2025 12:39:03 -0400 Subject: [PATCH] test: Add permissions and roles e2e tests (#4172) Co-authored-by: kyle-ssg Co-authored-by: Matthew Elwell Co-authored-by: Kim Gustyr --- .../workflows/.reusable-docker-e2e-tests.yml | 7 +- .../platform-docker-build-test-publish.yml | 2 + .github/workflows/platform-pull-request.yml | 24 ++- api/e2etests/e2e_seed_data.py | 4 +- .../end_to_end/test_integration_e2e_tests.py | 4 +- frontend/common/stores/organisation-store.js | 1 + frontend/common/stores/project-store.js | 3 + frontend/e2e/config.ts | 7 + frontend/e2e/helpers.cafe.ts | 107 ++++++++- frontend/e2e/index.cafe.js | 13 +- frontend/e2e/init.cafe.js | 21 +- .../e2e/tests/environment-permission-test.ts | 146 +++++++++++++ frontend/e2e/tests/environment-test.ts | 7 +- .../e2e/tests/organisation-permission-test.ts | 38 ++++ frontend/e2e/tests/project-permission-test.ts | 129 +++++++++++ frontend/e2e/tests/roles-test.ts | 61 ++++++ frontend/env/project_e2e.js | 2 +- frontend/global.d.ts | 1 - frontend/package.json | 1 + frontend/web/components/EditIdentity.tsx | 88 ++++---- frontend/web/components/EditPermissions.tsx | 50 ++--- .../web/components/RolePermissionsList.tsx | 2 +- frontend/web/components/RolesTable.tsx | 2 +- frontend/web/components/UserGroupList.tsx | 2 +- frontend/web/components/modals/CreateFlag.js | 54 +++-- frontend/web/components/modals/CreateRole.tsx | 3 +- frontend/web/components/pages/HomeAside.tsx | 203 ++++++++---------- .../pages/UsersAndPermissionsPage.tsx | 3 +- frontend/web/components/pages/UsersPage.tsx | 45 ++-- frontend/web/project/project-components.js | 2 +- 30 files changed, 785 insertions(+), 247 deletions(-) create mode 100644 frontend/e2e/tests/environment-permission-test.ts create mode 100644 frontend/e2e/tests/organisation-permission-test.ts create mode 100644 frontend/e2e/tests/project-permission-test.ts create mode 100644 frontend/e2e/tests/roles-test.ts diff --git a/.github/workflows/.reusable-docker-e2e-tests.yml b/.github/workflows/.reusable-docker-e2e-tests.yml index 3078a8ceb47e..aab9a7190dc3 100644 --- a/.github/workflows/.reusable-docker-e2e-tests.yml +++ b/.github/workflows/.reusable-docker-e2e-tests.yml @@ -28,9 +28,12 @@ on: required: false default: ubuntu-latest secrets: - gcr-token: + GCR_TOKEN: description: A token to use for logging into Github Container Registry. If not provided, login does not occur. required: false + SLACK_TOKEN: + description: A token to use uploading test failures to slack. + required: false jobs: run-e2e: @@ -43,7 +46,7 @@ jobs: id-token: write env: - GCR_TOKEN: ${{ secrets.gcr-token }} + GCR_TOKEN: ${{ secrets.GCR_TOKEN }} steps: - name: Cloning repo diff --git a/.github/workflows/platform-docker-build-test-publish.yml b/.github/workflows/platform-docker-build-test-publish.yml index 2a0a5f8c14af..909145bbffa7 100644 --- a/.github/workflows/platform-docker-build-test-publish.yml +++ b/.github/workflows/platform-docker-build-test-publish.yml @@ -92,6 +92,8 @@ jobs: concurrency: 2 - tests: versioning concurrency: 1 + - tests: organisation-permission environment-permission project-permission roles + concurrency: 1 docker-publish-api: needs: [docker-build-api, run-e2e-tests] diff --git a/.github/workflows/platform-pull-request.yml b/.github/workflows/platform-pull-request.yml index 13c171bf9c94..b44bbb2eeb59 100644 --- a/.github/workflows/platform-pull-request.yml +++ b/.github/workflows/platform-pull-request.yml @@ -140,7 +140,8 @@ jobs: concurrency: ${{ matrix.args.concurrency }} tests: ${{ matrix.args.tests }} secrets: - gcr-token: ${{ needs.permissions-check.outputs.can-write == 'true' && secrets.GITHUB_TOKEN || '' }} + GCR_TOKEN: ${{ needs.permissions-check.outputs.can-write == 'true' && secrets.GITHUB_TOKEN || '' }} + SLACK_TOKEN: ${{ needs.permissions-check.outputs.can-write == 'true' && secrets.SLACK_TOKEN || '' }} strategy: matrix: @@ -154,3 +155,24 @@ jobs: concurrency: 2 - tests: versioning concurrency: 1 + + run-e2e-tests-private-cloud: + if: needs.permissions-check.outputs.can-write == 'true' && !cancelled() + needs: [permissions-check, docker-build-private-cloud, docker-build-e2e] + uses: ./.github/workflows/.reusable-docker-e2e-tests.yml + with: + runs-on: ${{ matrix.runs-on }} + e2e-image: ${{ needs.docker-build-e2e.outputs.image }} + api-image: ${{ needs.docker-build-private-cloud.outputs.image }} + concurrency: ${{ matrix.args.concurrency }} + tests: ${{ matrix.args.tests }} + secrets: + GCR_TOKEN: ${{ needs.permissions-check.outputs.can-write == 'true' && secrets.GITHUB_TOKEN || '' }} + SLACK_TOKEN: ${{ needs.permissions-check.outputs.can-write == 'true' && secrets.SLACK_TOKEN || '' }} + + strategy: + matrix: + runs-on: [ubuntu-latest, ARM64-2c] + args: + - tests: organisation-permission environment-permission project-permission roles + concurrency: 1 diff --git a/api/e2etests/e2e_seed_data.py b/api/e2etests/e2e_seed_data.py index 37b7605e2a17..4303c5ed2873 100644 --- a/api/e2etests/e2e_seed_data.py +++ b/api/e2etests/e2e_seed_data.py @@ -22,7 +22,7 @@ CREATE_PROJECT, MANAGE_USER_GROUPS, ) -from organisations.subscriptions.constants import SCALE_UP +from organisations.subscriptions.constants import ENTERPRISE from projects.models import Project, UserProjectPermission from users.models import FFAdminUser, UserPermissionGroup @@ -129,7 +129,7 @@ def seed_data() -> None: ] # Upgrade organisation seats Subscription.objects.filter(organisation__in=org_admin.organisations.all()).update( - max_seats=8, plan=SCALE_UP, subscription_id="test_subscription_id" + max_seats=8, plan=ENTERPRISE, subscription_id="test_subscription_id" ) # Create projects and environments diff --git a/api/tests/integration/e2etests/end_to_end/test_integration_e2e_tests.py b/api/tests/integration/e2etests/end_to_end/test_integration_e2e_tests.py index 731ffe28d3a2..f5b56f018891 100644 --- a/api/tests/integration/e2etests/end_to_end/test_integration_e2e_tests.py +++ b/api/tests/integration/e2etests/end_to_end/test_integration_e2e_tests.py @@ -5,7 +5,7 @@ from rest_framework.test import APIClient from organisations.models import Subscription -from organisations.subscriptions.constants import SCALE_UP +from organisations.subscriptions.constants import ENTERPRISE from users.models import FFAdminUser @@ -43,7 +43,7 @@ def test_e2e_teardown(settings, db) -> None: organisation__in=e2e_user.organisations.all() ): assert subscription.max_seats == 8 - assert subscription.plan == SCALE_UP + assert subscription.plan == ENTERPRISE assert subscription.subscription_id == "test_subscription_id" diff --git a/frontend/common/stores/organisation-store.js b/frontend/common/stores/organisation-store.js index 5aad2f9d0707..0d2262e65a9e 100644 --- a/frontend/common/stores/organisation-store.js +++ b/frontend/common/stores/organisation-store.js @@ -377,6 +377,7 @@ store.dispatcherIndex = Dispatcher.register(store, (payload) => { controller.invalidateInviteLink(action.link) break case Actions.LOGOUT: + store.model = null store.id = null break default: diff --git a/frontend/common/stores/project-store.js b/frontend/common/stores/project-store.js index 6ab8f2b12e35..ca5ecd76e96e 100644 --- a/frontend/common/stores/project-store.js +++ b/frontend/common/stores/project-store.js @@ -276,6 +276,9 @@ store.dispatcherIndex = Dispatcher.register(store, (payload) => { case Actions.EDIT_PROJECT: controller.editProject(action.id, action.project) break + case Actions.LOGOUT: + store.model = null + break default: } }) diff --git a/frontend/e2e/config.ts b/frontend/e2e/config.ts index 876692e0c0a7..84c66a17cd6f 100644 --- a/frontend/e2e/config.ts +++ b/frontend/e2e/config.ts @@ -1,5 +1,12 @@ const E2E_EMAIL_DOMAIN = 'flagsmithe2etestdomain.io' export const E2E_SIGN_UP_USER = `e2e_signup_user@${E2E_EMAIL_DOMAIN}` export const E2E_USER = `e2e_user@${E2E_EMAIL_DOMAIN}` +export const E2E_NON_ADMIN_USER = `e2e_user@${E2E_EMAIL_DOMAIN}` +//e2e_non_admin_user_with_project_permissions@flagsmithe2etestdomain.io +export const E2E_NON_ADMIN_USER_WITH_ORG_PERMISSIONS = `e2e_non_admin_user_with_org_permissions@${E2E_EMAIL_DOMAIN}` +export const E2E_NON_ADMIN_USER_WITH_PROJECT_PERMISSIONS = `e2e_non_admin_user_with_project_permissions@${E2E_EMAIL_DOMAIN}` +export const E2E_NON_ADMIN_USER_WITH_PROJECT_READ_PERMISSIONS = `e2e_non_admin_user_with_project_read_permissions@${E2E_EMAIL_DOMAIN}` +export const E2E_NON_ADMIN_USER_WITH_ENV_PERMISSIONS = `e2e_non_admin_user_with_env_permissions@${E2E_EMAIL_DOMAIN}` +export const E2E_NON_ADMIN_USER_WITH_A_ROLE = `e2e_non_admin_user_with_a_role@${E2E_EMAIL_DOMAIN}` export const E2E_CHANGE_MAIL = `e2e_change_email@${E2E_EMAIL_DOMAIN}` export const PASSWORD = 'Str0ngp4ssw0rd!' diff --git a/frontend/e2e/helpers.cafe.ts b/frontend/e2e/helpers.cafe.ts index fb7465b432a6..93d8b386bd80 100644 --- a/frontend/e2e/helpers.cafe.ts +++ b/frontend/e2e/helpers.cafe.ts @@ -35,6 +35,24 @@ export const waitForElementVisible = async (selector: string) => { .ok(`waitForElementVisible(${selector})`, { timeout: LONG_TIMEOUT }) } +export const waitForElementNotClickable = async (selector: string) => { + logUsingLastSection(`Waiting element visible ${selector}`) + await t + .expect(Selector(selector).visible) + .ok(`waitForElementVisible(${selector})`, { timeout: LONG_TIMEOUT }) + await t + .expect(Selector(selector).hasAttribute('disabled')).ok() +} + +export const waitForElementClickable = async (selector: string) => { + logUsingLastSection(`Waiting element visible ${selector}`) + await t + .expect(Selector(selector).visible) + .ok(`waitForElementVisible(${selector})`, { timeout: LONG_TIMEOUT }) + await t + .expect(Selector(selector).hasAttribute('disabled')).notOk() +} + export const logResults = async (requests: LoggedRequest[], t) => { if (!t.testRun?.errs?.length) { log('Finished without errors') @@ -88,6 +106,17 @@ export const click = async (selector: string) => { .click(selector) } +export const clickByText = async (text:string, element = 'button') => { + logUsingLastSection(`Click by text ${text} ${element}`) + const selector = Selector(element).withText(text); + await t + .scrollIntoView(selector) + .expect(Selector(selector).hasAttribute('disabled')) + .notOk('ready for testing', { timeout: 5000 }) + .hover(selector) + .click(selector) +} + export const gotoSegments = async () => { await click('#segments-link') } @@ -102,6 +131,28 @@ export const getLogger = () => stringifyResponseBody: true, }) +export const createRole = async (roleName: string, index: number, users: number[]) => { + await click(byId('tab-item-roles')) + await click(byId('create-role')) + await setText(byId('role-name'), roleName) + await click(byId('save-role')) + await click(byId(`role-${index}`)) + await click(byId('members-tab')) + await click(byId('assigned-users')) + for (const userId of users) { + await click(byId(`assignees-list-item-${userId}`)) + } + await closeModal() +} + + +export const editRoleMembers = async (index:number)=>{ + await click(byId('tab-item-roles')) + await click(byId('create-role')) + await setText(byId('role-name'), roleName) + await click(byId('save-role')) +} + export const gotoTraits = async () => { await click('#features-link') await click('#users-link') @@ -219,6 +270,12 @@ export const saveFeatureSegments = async () => { await waitForElementNotExist('#create-feature-modal') } +export const createEnvironment = async (name:string) => { + await setText('[name="envName"]', name) + await click('#create-env-btn') + await waitForElementVisible(byId(`switch-environment-${name.toLowerCase()}-active`)) +} + export const goToUser = async (index: number) => { await click('#features-link') await click('#users-link') @@ -257,7 +314,7 @@ export const login = async (email: string, password: string) => { await click('#login-btn') await waitForElementVisible('#project-manage-widget') } -export const logout = async (t) => { +export const logout = async () => { await click('#account-settings-link') await click('#logout-link') await waitForElementVisible('#login-page') @@ -413,6 +470,14 @@ export const toggleFeature = async (index: number, toValue: boolean) => { ) } +export const setUserPermissions = async (index: number, toValue: boolean) => { + await click(byId(`feature-switch-${index}${toValue ? '-off' : 'on'}`)) + await click('#confirm-toggle-feature-btn') + await waitForElementVisible( + byId(`feature-switch-${index}${toValue ? '-on' : 'off'}`), + ) +} + export const setSegmentRule = async ( ruleIndex: number, orIndex: number, @@ -478,4 +543,44 @@ export const refreshUntilElementVisible = async (selector: string, maxRetries=20 return t.scrollIntoView(element) } +const permissionsMap = { + 'CREATE_PROJECT': 'organisation', + 'MANAGE_USERS': 'organisation', + 'MANAGE_USER_GROUPS': 'organisation', + 'VIEW_PROJECT': 'project', + 'CREATE_ENVIRONMENT': 'project', + 'DELETE_FEATURE': 'project', + 'CREATE_FEATURE': 'project', + 'MANAGE_SEGMENTS': 'project', + 'VIEW_AUDIT_LOG': 'project', + 'VIEW_ENVIRONMENT': 'environment', + 'UPDATE_FEATURE_STATE': 'environment', + 'MANAGE_IDENTITIES': 'environment', + 'CREATE_CHANGE_REQUEST': 'environment', + 'APPROVE_CHANGE_REQUEST': 'environment', + 'VIEW_IDENTITIES': 'environment', + 'MANAGE_SEGMENT_OVERRIDES': 'environment', + 'MANAGE_TAGS': 'project', +} as const; + + +export const setUserPermission = async (email: string, permission: keyof typeof permissionsMap | 'ADMIN', entityName:string|null, entityLevel?: 'project'|'environment'|'organisation', parentName?: string) => { + await click(byId('users-and-permissions')) + await click(byId(`user-${email}`)) + const level = permissionsMap[permission] || entityLevel + await click(byId(`${level}-permissions-tab`)) + if(parentName) { + await clickByText(parentName, 'a') + } + if(entityName) { + await click(byId(`permissions-${entityName.toLowerCase()}`)) + } + if(permission==='ADMIN') { + await click(byId(`admin-switch-${level}`)) + } else { + await click(byId(`permission-switch-${permission}`)) + } + await closeModal() +} + export default {} diff --git a/frontend/e2e/index.cafe.js b/frontend/e2e/index.cafe.js index 470f4077059e..3ac3261a403b 100644 --- a/frontend/e2e/index.cafe.js +++ b/frontend/e2e/index.cafe.js @@ -21,10 +21,15 @@ createTestCafe() testcafe = tc; await new Promise((resolve) => { process.env.PORT = 3000; - server = fork('./api/index'); - server.on('message', () => { - resolve(); - }); + console.log(process.env.E2E_LOCAL) + if(process.env.E2E_LOCAL) { + resolve() + } else { + server = fork('./api/index'); + server.on('message', () => { + resolve(); + }); + } }); const runner = testcafe.createRunner() const args = process.argv.splice(2).map(value => value.toLowerCase()); diff --git a/frontend/e2e/init.cafe.js b/frontend/e2e/init.cafe.js index d979892d58ea..7c0e6a0cdbdf 100644 --- a/frontend/e2e/init.cafe.js +++ b/frontend/e2e/init.cafe.js @@ -10,7 +10,11 @@ import projectTest from './tests/project-test' import { testSegment1, testSegment2, testSegment3 } from './tests/segment-test' import initialiseTests from './tests/initialise-tests' import flagTests from './tests/flag-tests' -import versioningTests from './tests/versioning-tests'; +import versioningTests from './tests/versioning-tests' +import organisationPermissionTest from './tests/organisation-permission-test' +import projectPermissionTest from './tests/project-permission-test' +import environmentPermissionTest from './tests/environment-permission-test' +import rolesTest from './tests/roles-test' require('dotenv').config() @@ -124,3 +128,18 @@ test('Versioning', async () => { await versioningTests() await logout() }) + +test('Organisation-permission', async () => { + await organisationPermissionTest() + await logout() +}) + +test('Project-permission', async () => { + await projectPermissionTest() + await logout() +}) + +test('Environment-permission', async () => { + await environmentPermissionTest() + await logout() +}) diff --git a/frontend/e2e/tests/environment-permission-test.ts b/frontend/e2e/tests/environment-permission-test.ts new file mode 100644 index 000000000000..ea072faebb72 --- /dev/null +++ b/frontend/e2e/tests/environment-permission-test.ts @@ -0,0 +1,146 @@ +import { + byId, + click, clickByText, closeModal, createEnvironment, + createFeature, editRemoteConfig, + gotoTraits, + log, + login, logout, setUserPermission, + toggleFeature, waitForElementClickable, waitForElementNotClickable, waitForElementNotExist, waitForElementVisible, +} from '../helpers.cafe'; +import { + PASSWORD, + E2E_NON_ADMIN_USER_WITH_ENV_PERMISSIONS, + E2E_USER, +} from '../config'; +import { Selector, t } from 'testcafe' + +export default async function () { + log('Login') + await login(E2E_NON_ADMIN_USER_WITH_ENV_PERMISSIONS, PASSWORD) + log('User can only view project') + await click('#project-select-0') + await t + .expect(Selector('#project-select-1').exists) + .notOk('The element"#project-select-1" should not be present') + await logout() + + log('User with permissions can Handle the Features') + await login(E2E_NON_ADMIN_USER_WITH_ENV_PERMISSIONS, PASSWORD) + await click('#project-select-0') + await createFeature(0, 'test_feature', false) + await toggleFeature(0, true) + await logout() + + log('User without permissions cannot create traits') + await login(E2E_NON_ADMIN_USER_WITH_ENV_PERMISSIONS, PASSWORD) + await click('#project-select-0') + await gotoTraits() + const createTraitBtn = Selector(byId('add-trait')) + await t.expect(createTraitBtn.hasAttribute('disabled')).ok() + await logout() + + log('User without permissions cannot see audit logs') + await login(E2E_NON_ADMIN_USER_WITH_ENV_PERMISSIONS, PASSWORD) + await click('#project-select-0') + await waitForElementNotExist(byId('audit-log-link')) + await logout() + + log('Create new environment') + await login(E2E_USER, PASSWORD) + await clickByText('My Test Project 6 Env Permission') + await click('#create-env-link') + await createEnvironment('Production') + await logout() + log('User without permissions cannot see environment') + await login(E2E_NON_ADMIN_USER_WITH_ENV_PERMISSIONS, PASSWORD) + await click('#project-select-0') + await waitForElementVisible(byId('switch-environment-development')) + await waitForElementNotExist(byId('switch-environment-production')) + await logout() + + log('Grant view environment permission') + await login(E2E_USER, PASSWORD) + await setUserPermission(E2E_NON_ADMIN_USER_WITH_ENV_PERMISSIONS, 'VIEW_ENVIRONMENT', 'Production', 'environment', 'My Test Project 6 Env Permission' ) + await logout() + log('User with permissions can see environment') + await login(E2E_NON_ADMIN_USER_WITH_ENV_PERMISSIONS, PASSWORD) + await click('#project-select-0') + await waitForElementVisible(byId('switch-environment-production')) + await waitForElementVisible(byId('switch-environment-production')) + await logout() + + log('User with permissions can update feature state') + await login(E2E_NON_ADMIN_USER_WITH_ENV_PERMISSIONS, PASSWORD) + await click('#project-select-0') + await createFeature(0,'my_feature',"foo",'A test feature') + await editRemoteConfig(0, 'bar') + await logout() + log('User without permission cannot create a segment override') + await login(E2E_NON_ADMIN_USER_WITH_ENV_PERMISSIONS, PASSWORD) + await click('#project-select-0') + await click(byId('feature-item-0')) + await click(byId('segment_overrides')) + await waitForElementNotClickable('#update-feature-segments-btn') + await closeModal() + await logout() + log('Grant MANAGE_IDENTITIES permission') + await login(E2E_USER, PASSWORD) + await setUserPermission(E2E_NON_ADMIN_USER_WITH_ENV_PERMISSIONS, 'MANAGE_SEGMENT_OVERRIDES', 'Development', 'environment', 'My Test Project 6 Env Permission' ) + await logout() + log('User with permission can create a segment override') + await login(E2E_NON_ADMIN_USER_WITH_ENV_PERMISSIONS, PASSWORD) + await click('#project-select-0') + await click(byId('feature-item-0')) + await click(byId('segment_overrides')) + await waitForElementClickable('#update-feature-segments-btn') + await closeModal() + await logout() + + log('User without permissions cannot update feature state') + await login(E2E_NON_ADMIN_USER_WITH_ENV_PERMISSIONS, PASSWORD) + await click('#project-select-0') + await waitForElementClickable(byId('feature-switch-0-on')) + await click(byId('switch-environment-production')) + await waitForElementNotClickable(byId('feature-switch-0-on')) + await click(byId('feature-item-0')) + await waitForElementNotClickable(byId('update-feature-btn')) + await closeModal() + await logout() + + log('User with permissions can view identities') + await login(E2E_NON_ADMIN_USER_WITH_ENV_PERMISSIONS, PASSWORD) + await click('#project-select-0') + await waitForElementVisible('#users-link') + await logout() + + log('User without permissions cannot add user trait') + await login(E2E_NON_ADMIN_USER_WITH_ENV_PERMISSIONS, PASSWORD) + await click('#project-select-0') + await click('#users-link') + await click(byId('user-item-0')) + await waitForElementNotClickable(byId('add-trait')) + await logout() + + log('Grant MANAGE_IDENTITIES permission') + await login(E2E_USER, PASSWORD) + await setUserPermission(E2E_NON_ADMIN_USER_WITH_ENV_PERMISSIONS, 'MANAGE_IDENTITIES', 'Development', 'environment', 'My Test Project 6 Env Permission' ) + await logout() + log('User with permissions can add user trait') + await login(E2E_NON_ADMIN_USER_WITH_ENV_PERMISSIONS, PASSWORD) + await click('#project-select-0') + await click('#users-link') + await click(byId('user-item-0')) + await waitForElementClickable(byId('add-trait')) + await logout() + + + log('Remove VIEW_IDENTITIES permission') + await login(E2E_USER, PASSWORD) + await setUserPermission(E2E_NON_ADMIN_USER_WITH_ENV_PERMISSIONS, 'VIEW_IDENTITIES', 'Development', 'environment', 'My Test Project 6 Env Permission' ) + await logout() + log('User without permissions cannot view identities') + await login(E2E_NON_ADMIN_USER_WITH_ENV_PERMISSIONS, PASSWORD) + await click('#project-select-0') + await click('#users-link') + await waitForElementVisible(byId('missing-view-identities')) +} diff --git a/frontend/e2e/tests/environment-test.ts b/frontend/e2e/tests/environment-test.ts index bbdcbd236e63..7749d32e0971 100644 --- a/frontend/e2e/tests/environment-test.ts +++ b/frontend/e2e/tests/environment-test.ts @@ -1,11 +1,12 @@ import { byId, click, + createEnvironment, log, login, setText, waitForElementVisible, -} from '../helpers.cafe' +} from '../helpers.cafe'; import { PASSWORD, E2E_USER } from '../config' export default async function () { @@ -14,9 +15,7 @@ export default async function () { await click('#project-select-0') log('Create environment') await click('#create-env-link') - await setText('[name="envName"]', 'Staging') - await click('#create-env-btn') - await waitForElementVisible(byId('switch-environment-staging-active')) + await createEnvironment('Staging') log('Edit Environment') await click('#env-settings-link') await setText("[name='env-name']", 'Internal') diff --git a/frontend/e2e/tests/organisation-permission-test.ts b/frontend/e2e/tests/organisation-permission-test.ts new file mode 100644 index 000000000000..24456a18637d --- /dev/null +++ b/frontend/e2e/tests/organisation-permission-test.ts @@ -0,0 +1,38 @@ +import { + byId, + click, clickByText, + closeModal, + log, + login, logout, + setText, waitForElementClickable, waitForElementNotClickable, + waitForElementVisible, +} from '../helpers.cafe'; +import { + PASSWORD, + E2E_NON_ADMIN_USER_WITH_ORG_PERMISSIONS, + E2E_NON_ADMIN_USER_WITH_PROJECT_PERMISSIONS, +} from '../config'; +import { Selector, t } from 'testcafe' + +export default async function () { + log('Login') + await login(E2E_NON_ADMIN_USER_WITH_ORG_PERMISSIONS, PASSWORD) + log('User without permissions cannot see any Project') + await t + .expect(Selector('#project-select-0').exists) + .notOk('The element"#project-select-0" should not be present') + log('User with permissions can Create a Project') + await waitForElementClickable( byId('create-first-project-btn')) + + log('User can manage groups') + await click(byId('users-and-permissions')) + await clickByText('Groups') + await waitForElementClickable("#btn-invite-groups") + await logout() + log('Login as project user') + await login(E2E_NON_ADMIN_USER_WITH_PROJECT_PERMISSIONS, PASSWORD) + log('User cannot manage users or groups') + await click(byId('users-and-permissions')) + await clickByText('Groups') + await waitForElementNotClickable("#btn-invite-groups") +} diff --git a/frontend/e2e/tests/project-permission-test.ts b/frontend/e2e/tests/project-permission-test.ts new file mode 100644 index 000000000000..8f68d67df317 --- /dev/null +++ b/frontend/e2e/tests/project-permission-test.ts @@ -0,0 +1,129 @@ +import { + byId, + click, + createEnvironment, + createFeature, gotoSegments, + log, + login, + logout, + setUserPermission, + toggleFeature, waitForElementNotClickable, waitForElementNotExist, waitForElementVisible, +} from '../helpers.cafe'; +import { E2E_NON_ADMIN_USER_WITH_PROJECT_PERMISSIONS, E2E_USER, PASSWORD } from '../config'; +import { Selector, t } from 'testcafe'; + +export default async function () { + + log('User with VIEW_PROJECT can only see their project') + await login(E2E_NON_ADMIN_USER_WITH_PROJECT_PERMISSIONS, PASSWORD) + await waitForElementNotExist('#project-select-1') + await logout() + + log('User with CREATE_ENVIRONMENT can create an environment') + await login(E2E_NON_ADMIN_USER_WITH_PROJECT_PERMISSIONS, PASSWORD) + await click('#project-select-0') + await createEnvironment('Staging') + await logout() + + log('User with VIEW_AUDIT_LOG can view the audit log') + await login(E2E_NON_ADMIN_USER_WITH_PROJECT_PERMISSIONS, PASSWORD) + await click('#project-select-0') + await click(byId('audit-log-link')) + await logout() + log('Remove VIEW_AUDIT_LOG permission') + await login(E2E_USER, PASSWORD) + await setUserPermission(E2E_NON_ADMIN_USER_WITH_PROJECT_PERMISSIONS, 'VIEW_AUDIT_LOG', 'My Test Project 5 Project Permission', 'project' ) + await logout() + log('User without VIEW_AUDIT_LOG cannot view the audit log') + await login(E2E_NON_ADMIN_USER_WITH_PROJECT_PERMISSIONS, PASSWORD) + await click('#project-select-0') + await waitForElementNotExist('audit-log-link') + await logout() + + log('User with CREATE_FEATURE can Handle the Features') + await login(E2E_NON_ADMIN_USER_WITH_PROJECT_PERMISSIONS, PASSWORD) + await click('#project-select-0') + await createFeature(0, 'test_feature', false) + await toggleFeature(0, true) + await logout() + log('Remove CREATE_FEATURE permissions') + await login(E2E_USER, PASSWORD) + await setUserPermission(E2E_NON_ADMIN_USER_WITH_PROJECT_PERMISSIONS, 'CREATE_FEATURE', 'My Test Project 5 Project Permission', 'project' ) + await logout() + log('User without CREATE_FEATURE cannot Handle the Features') + await login(E2E_NON_ADMIN_USER_WITH_PROJECT_PERMISSIONS, PASSWORD) + await click('#project-select-0') + await waitForElementNotClickable('#show-create-feature-btn') + await logout() + + log('User without ADMIN permissions cannot set other users project permissions') + await login(E2E_NON_ADMIN_USER_WITH_PROJECT_PERMISSIONS, PASSWORD) + await waitForElementNotExist('#project-settings-link') + await logout() + + log('Set user as project ADMIN') + await login(E2E_USER, PASSWORD) + await setUserPermission(E2E_NON_ADMIN_USER_WITH_PROJECT_PERMISSIONS, 'ADMIN', 'My Test Project 5 Project Permission', 'project' ) + await logout() + log('User with ADMIN permissions can set project settings') + await login(E2E_NON_ADMIN_USER_WITH_PROJECT_PERMISSIONS, PASSWORD) + await click('#project-select-0') + await waitForElementVisible('#project-settings-link') + await logout() + log('Remove user as project ADMIN') + await login(E2E_USER, PASSWORD) + await setUserPermission(E2E_NON_ADMIN_USER_WITH_PROJECT_PERMISSIONS, 'ADMIN', 'My Test Project 5 Project Permission', 'project' ) + await logout() + + log('User without create environment permissions cannot create a new environment') + await login(E2E_USER, PASSWORD) + await setUserPermission(E2E_NON_ADMIN_USER_WITH_PROJECT_PERMISSIONS, 'CREATE_ENVIRONMENT', 'My Test Project 5 Project Permission', 'project' ) + await logout() + await login(E2E_NON_ADMIN_USER_WITH_PROJECT_PERMISSIONS, PASSWORD) + await click('#project-select-0') + await waitForElementNotExist('#create-env-link') + await logout() + + log('User without DELETE_FEATURE permissions cannot Delete any feature') + await login(E2E_NON_ADMIN_USER_WITH_PROJECT_PERMISSIONS, PASSWORD) + await click('#project-select-0') + await click(byId('feature-action-0')) + await waitForElementVisible(byId('remove-feature-btn-0')) + await Selector(byId('remove-feature-btn-0')).hasClass( + 'feature-action__item_disabled', + ) + await logout() + log('Add DELETE_FEATURE permission to user') + await login(E2E_USER, PASSWORD) + await setUserPermission(E2E_NON_ADMIN_USER_WITH_PROJECT_PERMISSIONS, 'DELETE_FEATURE', 'My Test Project 5 Project Permission', 'project' ) + await logout() + log('User with permissions can Delete any feature') + await login(E2E_NON_ADMIN_USER_WITH_PROJECT_PERMISSIONS, PASSWORD) + await click('#project-select-0') + await click(byId('feature-action-0')) + await waitForElementVisible(byId('remove-feature-btn-0')) + await t.expect(Selector(byId('remove-feature-btn-0')).hasClass('feature-action__item_disabled')).notOk(); + await logout() + + log('User without MANAGE_SEGMENTS permissions cannot Manage Segments') + await login(E2E_NON_ADMIN_USER_WITH_PROJECT_PERMISSIONS, PASSWORD) + await click('#project-select-0') + await gotoSegments() + const createSegmentBtn = Selector(byId('show-create-segment-btn')) + await t.expect(createSegmentBtn.hasAttribute('disabled')).ok() + await logout() + log('Add MANAGE_SEGMENTS permission to user') + await login(E2E_USER, PASSWORD) + await setUserPermission( + E2E_NON_ADMIN_USER_WITH_PROJECT_PERMISSIONS, + 'MANAGE_SEGMENTS', + 'My Test Project 5 Project Permission', + 'project' + ) + await logout() + log('User with MANAGE_SEGMENTS permissions can Manage Segments') + await login(E2E_NON_ADMIN_USER_WITH_PROJECT_PERMISSIONS, PASSWORD) + await click('#project-select-0') + await gotoSegments() + await t.expect(createSegmentBtn.hasAttribute('disabled')).notOk() +} diff --git a/frontend/e2e/tests/roles-test.ts b/frontend/e2e/tests/roles-test.ts new file mode 100644 index 000000000000..bc4a06da2ad7 --- /dev/null +++ b/frontend/e2e/tests/roles-test.ts @@ -0,0 +1,61 @@ +import { + byId, + click, + createFeature, + log, + login, + setText, + waitForElementVisible, + closeModal, + logout, + gotoTraits, + deleteFeature, createRole, +} from '../helpers.cafe'; +import { + PASSWORD, + E2E_NON_ADMIN_USER_WITH_A_ROLE, + E2E_USER, +} from '../config' +import { t } from 'testcafe' + +export default async function () { + log('Login') + await login(E2E_USER, PASSWORD) + await click('#project-select-6') + await createFeature(0, 'test_feature', false) + log('Go to Roles') + await click(byId('org-settings-link')) + await click(byId('users-and-permissions')) + await waitForElementVisible(byId('tab-item-roles')) + log('Create Role') + await createRole('test_role', 0, [4]) + log('Add project permissions to the Role') + await click(byId(`role-0`)) + await click(byId('permissions-tab')) + await click(byId('permissions-tab')) + await waitForElementVisible(byId('project-permissions-tab')) + await click(byId('project-permissions-tab')) + await click(byId('permissions-my test project 7 role')) + await click(byId('admin-switch-project')) + log('Add environment permissions to the Role') + await waitForElementVisible(byId('environment-permissions-tab')) + await click(byId('environment-permissions-tab')) + await click(byId('project-select')) + await waitForElementVisible(byId('project-select-option-6')) + await click(byId('project-select-option-6')) + await click(byId('permissions-development')) + await click(byId('admin-switch-environment')) + await closeModal() + await logout(t) + log('Login with the user with a new Role') + await t.eval(() => location.reload()); + await t.wait(2000); + await login(E2E_NON_ADMIN_USER_WITH_A_ROLE, PASSWORD) + await click('#project-select-0') + log('User with permissions can Handle the Features') + const flagName = 'test_feature' + await deleteFeature(0, flagName) + + log('User with permissions can See the Identities') + await gotoTraits() +} diff --git a/frontend/env/project_e2e.js b/frontend/env/project_e2e.js index 5ba0ec03e362..ca94f0c38321 100644 --- a/frontend/env/project_e2e.js +++ b/frontend/env/project_e2e.js @@ -13,7 +13,7 @@ module.exports = global.Project = { flagsmith: 'ENktaJnfLVbLifybz34JmX', - flagsmithClientAPI: 'https://api.bullet-train.io/api/v1/', + flagsmithClientAPI: 'https://edge.api.flagsmith.com/api/v1/', flagsmithClientEdgeAPI: 'https://edge.api.flagsmith.com/api/v1/', // This is used for Sentry tracking diff --git a/frontend/global.d.ts b/frontend/global.d.ts index ab67ef295ae7..b7b2ec2cae16 100644 --- a/frontend/global.d.ts +++ b/frontend/global.d.ts @@ -35,7 +35,6 @@ declare global { const Column: typeof Component const Loader: typeof Component const E2E: boolean - const DYNATRACE_URL: string | undefined const dtrum: undefined | { identifyUser: (id: string) => void } const closeModal: () => void const closeModal2: () => void diff --git a/frontend/package.json b/frontend/package.json index 617d495c819d..2b7f4145507e 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -11,6 +11,7 @@ "husky:install": "cd ../ && husky install", "test:bundle:staging": "cross-env E2E=1 ENV=staging npm run bundle", "test:dev": "cross-env NODE_ENV=production E2E=true E2E_DEV=true ts-node -T ./e2e/index.cafe", + "test:devlocal": "cross-env NODE_ENV=production E2E_LOCAL=true E2E_CONCURRENCY=1 E2E=true E2E_DEV=true ts-node -T ./e2e/index.cafe", "test:devBundle": "npm run test:bundle && npm run test:dev", "test": "npm run test:bundle && cross-env NODE_ENV=production E2E=true ts-node -T ./e2e/index.cafe", "test:staging": "npm run test:bundle:staging && cross-env NODE_ENV=production E2E=true ENV=staging ts-node -T ./e2e/index.cafe", diff --git a/frontend/web/components/EditIdentity.tsx b/frontend/web/components/EditIdentity.tsx index db68c063e06b..5081abe070c6 100644 --- a/frontend/web/components/EditIdentity.tsx +++ b/frontend/web/components/EditIdentity.tsx @@ -3,7 +3,7 @@ import { Identity } from 'common/types/responses' import { useUpdateIdentityMutation } from 'common/services/useIdentity' import Button from './base/forms/Button' import ErrorMessage from './ErrorMessage' -import classNames from 'classnames'; +import classNames from 'classnames' type EditIdentityType = { data: Identity @@ -24,9 +24,9 @@ const EditIdentity: FC = ({ data, environmentId }) => { const handleBlur = () => { if (aliasRef.current) { const updatedAlias = (aliasRef.current.textContent || '') - .replace(/\n/g, ' ') - .trim() - .toLowerCase() + .replace(/\n/g, ' ') + .trim() + .toLowerCase() aliasRef.current.textContent = alias setAlias(updatedAlias) @@ -45,27 +45,26 @@ const EditIdentity: FC = ({ data, environmentId }) => { const handleFocus = () => { if (!alias) { - aliasRef.current.textContent = ''; // Clear the content + aliasRef.current.textContent = '' // Clear the content } // Ensure that aliasRef.current has at least one child node (a text node) if (aliasRef.current && aliasRef.current.childNodes.length === 0) { - aliasRef.current.appendChild(document.createTextNode('')); + aliasRef.current.appendChild(document.createTextNode('')) } if (aliasRef.current) { - const selection = window.getSelection(); - const range = document.createRange(); + const selection = window.getSelection() + const range = document.createRange() - const textLength = aliasRef.current.textContent?.length || 0; - range.setStart(aliasRef.current.childNodes[0], textLength); - range.collapse(true); + const textLength = aliasRef.current.textContent?.length || 0 + range.setStart(aliasRef.current.childNodes[0], textLength) + range.collapse(true) - selection?.removeAllRanges(); - selection?.addRange(range); + selection?.removeAllRanges() + selection?.addRange(range) } - }; - + } const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === 'Enter') { @@ -85,7 +84,10 @@ const EditIdentity: FC = ({ data, environmentId }) => { // Restore cursor position const newRange = document.createRange() - newRange.setStart(aliasRef.current.childNodes[0], Math.min(cursorPosition, lowerCaseText.length)) + newRange.setStart( + aliasRef.current.childNodes[0], + Math.min(cursorPosition, lowerCaseText.length), + ) newRange.collapse(true) selection?.removeAllRanges() @@ -94,33 +96,33 @@ const EditIdentity: FC = ({ data, environmentId }) => { } return ( - <> - - {alias || 'None'} - - - {error} - + <> + + {alias || 'None'} + + + {error} + ) } diff --git a/frontend/web/components/EditPermissions.tsx b/frontend/web/components/EditPermissions.tsx index 68c4ba8afcbc..3d4f7abb446c 100644 --- a/frontend/web/components/EditPermissions.tsx +++ b/frontend/web/components/EditPermissions.tsx @@ -839,15 +839,15 @@ const _EditPermissionsModal: FC = withAdminPermissions( { - toggleAdmin() - setValueChanged(true) - }} - checked={isAdmin} - /> - - - )} + onChange={() => { + toggleAdmin() + setValueChanged(true) + }} + checked={isAdmin} + /> + + + )} { const name = Format.enumeration.get(item.key).toLowerCase() @@ -857,24 +857,24 @@ const _EditPermissionsModal: FC = withAdminPermissions( className='no-pad mb-2 overflow-visible' items={permissions} renderRow={(p: AvailablePermission, index: number) => { - const levelUpperCase = level.toUpperCase() - const disabled = - level !== 'organisation' && - p.key !== `VIEW_${levelUpperCase}` && - !hasPermission(`VIEW_${levelUpperCase}`) + const levelUpperCase = level.toUpperCase() + const disabled = + level !== 'organisation' && + p.key !== `VIEW_${levelUpperCase}` && + !hasPermission(`VIEW_${levelUpperCase}`) const permission = entityPermissions.permissions.find( (v) => v.permission_key === p.key, ) const permissionType = getPermissionType(p.key) - return ( - - - - {Format.enumeration.get(p.key)} + > + + + {Format.enumeration.get(p.key)}
{p.description}
@@ -888,7 +888,7 @@ const _EditPermissionsModal: FC = withAdminPermissions( }} /> )} -
+
{tagBasedPermissions ? (