From cc0920fbdbac326cb88a14af1145e1ca01cc1864 Mon Sep 17 00:00:00 2001 From: Viktor Vorona Date: Wed, 27 Sep 2023 14:11:36 +0200 Subject: [PATCH] SONAR-20532 Wording and tests --- .../js/api/mocks/AuthenticationServiceMock.ts | 34 ++++++ .../authentication/GitHubMappingModal.tsx | 7 +- .../__tests__/Authentication-it.tsx | 103 ++++++++++++++++++ .../src/main/js/queries/identity-provider.ts | 2 +- .../resources/org/sonar/l10n/core.properties | 3 +- 5 files changed, 146 insertions(+), 3 deletions(-) diff --git a/server/sonar-web/src/main/js/api/mocks/AuthenticationServiceMock.ts b/server/sonar-web/src/main/js/api/mocks/AuthenticationServiceMock.ts index 1141cb3f296..b2ddda2275d 100644 --- a/server/sonar-web/src/main/js/api/mocks/AuthenticationServiceMock.ts +++ b/server/sonar-web/src/main/js/api/mocks/AuthenticationServiceMock.ts @@ -28,9 +28,11 @@ import { Task, TaskStatuses, TaskTypes } from '../../types/tasks'; import { activateGithubProvisioning, activateScim, + addGithubRolesMapping, checkConfigurationValidity, deactivateGithubProvisioning, deactivateScim, + deleteGithubRolesMapping, fetchGithubProvisioningStatus, fetchGithubRolesMapping, fetchIsScimEnabled, @@ -154,6 +156,8 @@ export default class AuthenticationServiceMock { .mockImplementation(this.handleCheckConfigurationValidity); jest.mocked(fetchGithubRolesMapping).mockImplementation(this.handleFetchGithubRolesMapping); jest.mocked(updateGithubRolesMapping).mockImplementation(this.handleUpdateGithubRolesMapping); + jest.mocked(addGithubRolesMapping).mockImplementation(this.handleAddGithubRolesMapping); + jest.mocked(deleteGithubRolesMapping).mockImplementation(this.handleDeleteGithubRolesMapping); } addProvisioningTask = (overrides: Partial> = {}) => { @@ -249,6 +253,36 @@ export default class AuthenticationServiceMock { ); }; + handleAddGithubRolesMapping: typeof addGithubRolesMapping = (data) => { + const newRole = { ...data, id: data.githubRole }; + this.githubMapping = [...this.githubMapping, newRole]; + + return Promise.resolve(newRole); + }; + + handleDeleteGithubRolesMapping: typeof deleteGithubRolesMapping = (id) => { + this.githubMapping = this.githubMapping.filter((el) => el.id !== id); + return Promise.resolve(); + }; + + addGitHubCustomRole = (id: string, permissions: (keyof GitHubMapping['permissions'])[]) => { + this.githubMapping = [ + ...this.githubMapping, + { + id, + githubRole: id, + permissions: { + user: permissions.includes('user'), + codeViewer: permissions.includes('codeViewer'), + issueAdmin: permissions.includes('issueAdmin'), + securityHotspotAdmin: permissions.includes('securityHotspotAdmin'), + admin: permissions.includes('admin'), + scan: permissions.includes('scan'), + }, + }, + ]; + }; + reset = () => { this.scimStatus = false; this.githubProvisioningStatus = false; diff --git a/server/sonar-web/src/main/js/apps/settings/components/authentication/GitHubMappingModal.tsx b/server/sonar-web/src/main/js/apps/settings/components/authentication/GitHubMappingModal.tsx index 795d3c419c1..02006c9c4d1 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/authentication/GitHubMappingModal.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/authentication/GitHubMappingModal.tsx @@ -24,7 +24,7 @@ import { DeleteButton, SubmitButton } from '../../../../components/controls/butt import PermissionHeader from '../../../../components/permissions/PermissionHeader'; import { Alert } from '../../../../components/ui/Alert'; import Spinner from '../../../../components/ui/Spinner'; -import { translate } from '../../../../helpers/l10n'; +import { translate, translateWithParameters } from '../../../../helpers/l10n'; import { PERMISSIONS_ORDER_FOR_PROJECT_TEMPLATE, convertToPermissionDefinitions, @@ -69,6 +69,11 @@ function PermissionRow(props: Readonly) { {!mapping.isBaseRole && ( { props.setMapping(list?.filter((r) => r.githubRole !== mapping.githubRole) ?? null); }} diff --git a/server/sonar-web/src/main/js/apps/settings/components/authentication/__tests__/Authentication-it.tsx b/server/sonar-web/src/main/js/apps/settings/components/authentication/__tests__/Authentication-it.tsx index a14e5ac963c..77a94494869 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/authentication/__tests__/Authentication-it.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/authentication/__tests__/Authentication-it.tsx @@ -154,6 +154,18 @@ const ui = { mappingRow: byRole('dialog', { name: 'settings.authentication.github.configuration.roles_mapping.dialog.title', }).byRole('row'), + customRoleInput: byRole('textbox', { + name: 'settings.authentication.github.configuration.roles_mapping.dialog.add_custom_role', + }), + customRoleAddBtn: byRole('dialog', { + name: 'settings.authentication.github.configuration.roles_mapping.dialog.title', + }).byRole('button', { name: 'add_verb' }), + roleExistsError: byRole('dialog', { + name: 'settings.authentication.github.configuration.roles_mapping.dialog.title', + }).byText('settings.authentication.github.configuration.roles_mapping.role_exists'), + deleteCustomRoleCustom2: byRole('button', { + name: 'settings.authentication.github.configuration.roles_mapping.dialog.delete_custom_role.custom2', + }), getMappingRowByRole: (text: string) => ui.github.mappingRow.getAll().find((row) => within(row).queryByText(text) !== null), mappingCheckbox: byRole('checkbox'), @@ -943,6 +955,97 @@ describe('Github tab', () => { expect(readCheckboxes).not.toBeChecked(); await user.click(github.mappingDialogClose.get()); }); + + it('should add/remove/update custom roles', async () => { + const user = userEvent.setup(); + settingsHandler.presetGithubAutoProvisioning(); + handler.enableGithubProvisioning(); + handler.addGitHubCustomRole('custom1', ['user', 'codeViewer', 'scan']); + handler.addGitHubCustomRole('custom2', ['user', 'codeViewer', 'issueAdmin', 'scan']); + renderAuthentication([Feature.GithubProvisioning]); + await user.click(await github.tab.find()); + + expect(await github.saveGithubProvisioning.find()).toBeDisabled(); + await user.click(github.editMappingButton.get()); + + const rows = (await github.mappingRow.findAll()).filter( + (row) => within(row).queryAllByRole('checkbox').length > 0, + ); + + expect(rows).toHaveLength(7); + + let custom1Checkboxes = github.mappingCheckbox.getAll(github.getMappingRowByRole('custom1')); + + expect(custom1Checkboxes[0]).toBeChecked(); + expect(custom1Checkboxes[1]).toBeChecked(); + expect(custom1Checkboxes[2]).not.toBeChecked(); + expect(custom1Checkboxes[3]).not.toBeChecked(); + expect(custom1Checkboxes[4]).not.toBeChecked(); + expect(custom1Checkboxes[5]).toBeChecked(); + + await user.click(custom1Checkboxes[1]); + await user.click(custom1Checkboxes[2]); + + await user.click(github.deleteCustomRoleCustom2.get()); + + expect(github.customRoleInput.get()).toHaveValue(''); + await user.type(github.customRoleInput.get(), 'read'); + await user.click(github.customRoleAddBtn.get()); + expect(await github.roleExistsError.find()).toBeInTheDocument(); + expect(github.customRoleAddBtn.get()).toBeDisabled(); + await user.clear(github.customRoleInput.get()); + expect(github.roleExistsError.query()).not.toBeInTheDocument(); + await user.type(github.customRoleInput.get(), 'custom1'); + await user.click(github.customRoleAddBtn.get()); + expect(await github.roleExistsError.find()).toBeInTheDocument(); + expect(github.customRoleAddBtn.get()).toBeDisabled(); + await user.clear(github.customRoleInput.get()); + await user.type(github.customRoleInput.get(), 'custom3'); + expect(github.roleExistsError.query()).not.toBeInTheDocument(); + expect(github.customRoleAddBtn.get()).toBeEnabled(); + await user.click(github.customRoleAddBtn.get()); + + let custom3Checkboxes = github.mappingCheckbox.getAll(github.getMappingRowByRole('custom3')); + expect(custom3Checkboxes[0]).not.toBeChecked(); + expect(custom3Checkboxes[1]).not.toBeChecked(); + expect(custom3Checkboxes[2]).not.toBeChecked(); + expect(custom3Checkboxes[3]).not.toBeChecked(); + expect(custom3Checkboxes[4]).not.toBeChecked(); + expect(custom3Checkboxes[5]).not.toBeChecked(); + await user.click(custom3Checkboxes[1]); + await user.click(github.mappingDialogClose.get()); + + expect(await github.saveGithubProvisioning.find()).toBeEnabled(); + await act(() => user.click(github.saveGithubProvisioning.get())); + + // Clean local mapping state + await user.click(github.jitProvisioningButton.get()); + await user.click(github.githubProvisioningButton.get()); + + await user.click(github.editMappingButton.get()); + + expect( + (await github.mappingRow.findAll()).filter( + (row) => within(row).queryAllByRole('checkbox').length > 0, + ), + ).toHaveLength(7); + custom1Checkboxes = github.mappingCheckbox.getAll(github.getMappingRowByRole('custom1')); + custom3Checkboxes = github.mappingCheckbox.getAll(github.getMappingRowByRole('custom3')); + expect(github.getMappingRowByRole('custom2')).toBeUndefined(); + expect(custom1Checkboxes[0]).toBeChecked(); + expect(custom1Checkboxes[1]).not.toBeChecked(); + expect(custom1Checkboxes[2]).toBeChecked(); + expect(custom1Checkboxes[3]).not.toBeChecked(); + expect(custom1Checkboxes[4]).not.toBeChecked(); + expect(custom1Checkboxes[5]).toBeChecked(); + expect(custom3Checkboxes[0]).not.toBeChecked(); + expect(custom3Checkboxes[1]).toBeChecked(); + expect(custom3Checkboxes[2]).not.toBeChecked(); + expect(custom3Checkboxes[3]).not.toBeChecked(); + expect(custom3Checkboxes[4]).not.toBeChecked(); + expect(custom3Checkboxes[5]).not.toBeChecked(); + await user.click(github.mappingDialogClose.get()); + }); }); }); diff --git a/server/sonar-web/src/main/js/queries/identity-provider.ts b/server/sonar-web/src/main/js/queries/identity-provider.ts index 6017978687e..85e8d706bae 100644 --- a/server/sonar-web/src/main/js/queries/identity-provider.ts +++ b/server/sonar-web/src/main/js/queries/identity-provider.ts @@ -124,7 +124,7 @@ export function useSyncWithGitHubNow() { }; } -// Order is reversed to put non-existing roles at the end (their index is -1) +// Order is reversed to put custom roles at the end (their index is -1) const defaultRoleOrder = ['admin', 'maintain', 'write', 'triage', 'read']; export function useGithubRolesMappingQuery() { diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties index bccc8c54921..5cf3a651a65 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -1541,7 +1541,8 @@ settings.authentication.github.configuration.roles_mapping.dialog.title=GitHub R settings.authentication.github.configuration.roles_mapping.dialog.roles_column=Roles settings.authentication.github.configuration.roles_mapping.dialog.add_custom_role=Add custom role: settings.authentication.github.configuration.roles_mapping.role_exists=Role already exists -settings.authentication.github.configuration.roles_mapping.dialog.custom_roles_description=Define a custom GitHub role to create a mapping. If the custom role name matches an existing role in any of your organizations, this mapping will apply to all occurrences of that role. If the existing custom role doesn't match any entries in this mapping, it will gracefully fall back to its base role. +settings.authentication.github.configuration.roles_mapping.dialog.custom_roles_description=When a custom role name added here matches an existing GitHub custom role in any of your organizations, the mapping applies to all users with this custom role. If an existing GitHub custom role has no exact match in this list, the permissions of its inherited base role are mapped. +settings.authentication.github.configuration.roles_mapping.dialog.delete_custom_role=Delete custom role {0} settings.authentication.github.configuration.unsaved_changes=You have unsaved changes. # SAML -- 2.39.5