]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-18429 Migrate permission templates app tests to RTL
authorWouter Admiraal <wouter.admiraal@sonarsource.com>
Thu, 23 Feb 2023 14:27:41 +0000 (15:27 +0100)
committersonartech <sonartech@sonarsource.com>
Tue, 28 Feb 2023 20:03:06 +0000 (20:03 +0000)
24 files changed:
server/sonar-web/src/main/js/api/mocks/PermissionsServiceMock.ts
server/sonar-web/src/main/js/api/permissions.ts
server/sonar-web/src/main/js/apps/permission-templates/components/ActionsCell.tsx
server/sonar-web/src/main/js/apps/permission-templates/components/DeleteForm.tsx
server/sonar-web/src/main/js/apps/permission-templates/components/Form.tsx
server/sonar-web/src/main/js/apps/permission-templates/components/Header.tsx
server/sonar-web/src/main/js/apps/permission-templates/components/Home.tsx
server/sonar-web/src/main/js/apps/permission-templates/components/PermissionTemplatesApp.tsx
server/sonar-web/src/main/js/apps/permission-templates/components/Template.tsx
server/sonar-web/src/main/js/apps/permission-templates/components/TemplateHeader.tsx
server/sonar-web/src/main/js/apps/permission-templates/components/__tests__/ActionsCell-test.tsx [deleted file]
server/sonar-web/src/main/js/apps/permission-templates/components/__tests__/Defaults-test.tsx [deleted file]
server/sonar-web/src/main/js/apps/permission-templates/components/__tests__/Form-test.tsx [deleted file]
server/sonar-web/src/main/js/apps/permission-templates/components/__tests__/ListItem-test.tsx [deleted file]
server/sonar-web/src/main/js/apps/permission-templates/components/__tests__/NameCell-test.tsx [deleted file]
server/sonar-web/src/main/js/apps/permission-templates/components/__tests__/PermissionTemplatesApp-it.tsx
server/sonar-web/src/main/js/apps/permission-templates/components/__tests__/__snapshots__/Defaults-test.tsx.snap [deleted file]
server/sonar-web/src/main/js/apps/permission-templates/components/__tests__/__snapshots__/Form-test.tsx.snap [deleted file]
server/sonar-web/src/main/js/apps/permission-templates/components/__tests__/__snapshots__/ListItem-test.tsx.snap [deleted file]
server/sonar-web/src/main/js/apps/permission-templates/components/__tests__/__snapshots__/NameCell-test.tsx.snap [deleted file]
server/sonar-web/src/main/js/apps/permission-templates/components/__tests__/__snapshots__/PermissionTemplatesApp-it.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/permissions/project/components/__tests__/PermissionsProject-it.tsx
server/sonar-web/src/main/js/helpers/mocks/permissions.ts
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index c8edbe64903a1c6bc276048b12af5a36e3958891..4d7b322fd669a13d5c8e5fd5030294aefe422cce 100644 (file)
@@ -22,10 +22,10 @@ import {
   mockPermission,
   mockPermissionGroup,
   mockPermissionTemplate,
+  mockPermissionTemplateGroup,
   mockPermissionUser,
-  mockTemplateGroup,
-  mockTemplateUser,
 } from '../../helpers/mocks/permissions';
+import { PERMISSIONS_ORDER_FOR_PROJECT_TEMPLATE } from '../../helpers/permissions';
 import { ComponentQualifier, Visibility } from '../../types/component';
 import { Permissions } from '../../types/permissions';
 import { Permission, PermissionGroup, PermissionTemplate, PermissionUser } from '../../types/types';
@@ -35,6 +35,8 @@ import {
   applyTemplateToProject,
   bulkApplyTemplate,
   changeProjectVisibility,
+  createPermissionTemplate,
+  deletePermissionTemplate,
   getGlobalPermissionsGroups,
   getGlobalPermissionsUsers,
   getPermissionsGroupsForComponent,
@@ -51,56 +53,43 @@ import {
   revokePermissionFromUser,
   revokeTemplatePermissionFromGroup,
   revokeTemplatePermissionFromUser,
+  setDefaultPermissionTemplate,
+  updatePermissionTemplate,
 } from '../permissions';
 
 const MAX_PROJECTS_TO_APPLY_PERMISSION_TEMPLATE = 10;
 
-const defaultPermissionTemplates: PermissionTemplate[] = [
-  mockPermissionTemplate(),
-  mockPermissionTemplate({
-    id: 'template2',
-    name: 'Permission Template 2',
-  }),
-];
-
-const templateUsers = [
-  mockTemplateUser(),
-  mockTemplateUser({
+const defaultUsers = [
+  mockPermissionUser(),
+  mockPermissionUser({
     login: 'gooduser1',
     name: 'John',
-    permissions: ['issueadmin', 'securityhotspotadmin', 'user'],
+    permissions: [Permissions.IssueAdmin, Permissions.SecurityHotspotAdmin, Permissions.Browse],
   }),
-  mockTemplateUser({
+  mockPermissionUser({
     login: 'gooduser2',
     name: 'Alexa',
-    permissions: ['issueadmin', 'user'],
+    permissions: [Permissions.IssueAdmin, Permissions.Browse],
   }),
-  mockTemplateUser({
+  mockPermissionUser({
     name: 'Siri',
     login: 'gooduser3',
   }),
-  mockTemplateUser({
+  mockPermissionUser({
     login: 'gooduser4',
     name: 'Cool',
-    permissions: ['user'],
+    permissions: [Permissions.Browse],
   }),
-  mockTemplateUser({
+  mockPermissionUser({
     name: 'White',
     login: 'baduser1',
   }),
-  mockTemplateUser({
+  mockPermissionUser({
     name: 'Green',
     login: 'baduser2',
   }),
 ];
 
-const templateGroups = [
-  mockTemplateGroup(),
-  mockTemplateGroup({ id: 'admins', name: 'admins', permissions: [] }),
-];
-
-const defaultUsers = [mockPermissionUser()];
-
 const defaultGroups = [
   mockPermissionGroup({ name: 'sonar-users', permissions: [Permissions.Browse] }),
   mockPermissionGroup({
@@ -110,6 +99,39 @@ const defaultGroups = [
   mockPermissionGroup({ name: 'sonar-losers', permissions: [] }),
 ];
 
+const defaultTemplates: PermissionTemplate[] = [
+  mockPermissionTemplate({
+    id: 'template1',
+    name: 'Permission Template 1',
+    description: 'This is permission template 1',
+    defaultFor: [
+      ComponentQualifier.Project,
+      ComponentQualifier.Application,
+      ComponentQualifier.Portfolio,
+    ],
+    permissions: PERMISSIONS_ORDER_FOR_PROJECT_TEMPLATE.map((key) =>
+      mockPermissionTemplateGroup({
+        key,
+        groupsCount: defaultGroups.filter((g) => g.permissions.includes(key)).length,
+        usersCount: defaultUsers.filter((g) => g.permissions.includes(key)).length,
+        withProjectCreator: false,
+      })
+    ),
+  }),
+  mockPermissionTemplate({
+    id: 'template2',
+    name: 'Permission Template 2',
+    permissions: PERMISSIONS_ORDER_FOR_PROJECT_TEMPLATE.map((key) =>
+      mockPermissionTemplateGroup({
+        key,
+        groupsCount: 0,
+        usersCount: 0,
+        withProjectCreator: [Permissions.Browse, Permissions.CodeViewer].includes(key),
+      })
+    ),
+  }),
+];
+
 const PAGE_SIZE = 5;
 const MIN_QUERY_LENGTH = 3;
 const DEFAULT_PAGE = 1;
@@ -119,28 +141,19 @@ jest.mock('../permissions');
 export default class PermissionsServiceMock {
   #permissionTemplates: PermissionTemplate[] = [];
   #permissions: Permission[];
-  #defaultTemplates: Array<{ templateId: string; qualifier: string }>;
+  #defaultTemplates: Array<{ templateId: string; qualifier: string }> = [];
   #groups: PermissionGroup[];
   #users: PermissionUser[];
   #isAllowedToChangePermissions = true;
 
   constructor() {
-    this.#permissionTemplates = cloneDeep(defaultPermissionTemplates);
-    this.#defaultTemplates = [
-      ComponentQualifier.Project,
-      ComponentQualifier.Application,
-      ComponentQualifier.Portfolio,
-    ].map((qualifier) => ({ templateId: this.#permissionTemplates[0].id, qualifier }));
-    this.#permissions = [
-      Permissions.Admin,
-      Permissions.CodeViewer,
-      Permissions.IssueAdmin,
-      Permissions.SecurityHotspotAdmin,
-      Permissions.Scan,
-      Permissions.Browse,
-    ].map((key) => mockPermission({ key, name: key }));
+    this.#permissionTemplates = cloneDeep(defaultTemplates);
+    this.#permissions = PERMISSIONS_ORDER_FOR_PROJECT_TEMPLATE.map((key) =>
+      mockPermission({ key, name: key })
+    );
     this.#groups = cloneDeep(defaultGroups);
     this.#users = cloneDeep(defaultUsers);
+    this.updateDefaults();
 
     jest.mocked(getPermissionTemplates).mockImplementation(this.handleGetPermissionTemplates);
     jest.mocked(bulkApplyTemplate).mockImplementation(this.handleBulkApplyTemplate);
@@ -157,6 +170,12 @@ export default class PermissionsServiceMock {
     jest.mocked(revokeTemplatePermissionFromGroup).mockImplementation(this.handlePermissionChange);
     jest.mocked(grantTemplatePermissionToUser).mockImplementation(this.handlePermissionChange);
     jest.mocked(revokeTemplatePermissionFromUser).mockImplementation(this.handlePermissionChange);
+    jest.mocked(createPermissionTemplate).mockImplementation(this.handleCreatePermissionTemplate);
+    jest.mocked(updatePermissionTemplate).mockImplementation(this.handleUpdatePermissionTemplate);
+    jest.mocked(deletePermissionTemplate).mockImplementation(this.handleDeletePermissionTemplate);
+    jest
+      .mocked(setDefaultPermissionTemplate)
+      .mockImplementation(this.handleSetDefaultPermissionTemplate);
     jest.mocked(changeProjectVisibility).mockImplementation(this.handleChangeProjectVisibility);
     jest.mocked(getGlobalPermissionsUsers).mockImplementation(this.handleGetPermissionUsers);
     jest.mocked(getGlobalPermissionsGroups).mockImplementation(this.handleGetPermissionGroups);
@@ -198,44 +217,24 @@ export default class PermissionsServiceMock {
     return Promise.resolve();
   };
 
-  handleGetPermissionTemplateUsers = (data: { q?: string | null; p?: number; ps?: number }) => {
-    const { ps = PAGE_SIZE, p = DEFAULT_PAGE, q } = data;
-
-    const users =
-      q && q.length >= MIN_QUERY_LENGTH
-        ? templateUsers.filter((user) =>
-            [user.login, user.name].some((key) => key.toLowerCase().includes(q.toLowerCase()))
-          )
-        : templateUsers;
-
-    const usersChunks = chunk(users, ps);
-
-    return this.reply({
-      paging: { pageSize: ps, total: users.length, pageIndex: p },
-      users: usersChunks[p - 1] ?? [],
-    });
+  handleGetPermissionTemplateUsers = (data: {
+    templateId: string;
+    q?: string;
+    permission?: string;
+    p?: number;
+    ps?: number;
+  }) => {
+    return this.handleGetPermissionUsers(data);
   };
 
   handleGetPermissionTemplateGroups = (data: {
     templateId: string;
-    q?: string | null;
+    q?: string;
     permission?: string;
     p?: number;
     ps?: number;
   }) => {
-    const { ps = PAGE_SIZE, p = DEFAULT_PAGE, q } = data;
-
-    const groups =
-      q && q.length >= MIN_QUERY_LENGTH
-        ? templateGroups.filter((group) => group.name.toLowerCase().includes(q.toLowerCase()))
-        : templateGroups;
-
-    const groupsChunks = chunk(groups, ps);
-
-    return this.reply({
-      paging: { pageSize: ps, total: groups.length, pageIndex: p },
-      groups: groupsChunks[p - 1] ?? [],
-    });
+    return this.handleGetPermissionGroups(data);
   };
 
   handleChangeProjectVisibility = (_project: string, _visibility: Visibility) => {
@@ -386,6 +385,58 @@ export default class PermissionsServiceMock {
     return this.#isAllowedToChangePermissions ? Promise.resolve() : Promise.reject();
   };
 
+  handleCreatePermissionTemplate = (data: {
+    name: string;
+    description?: string;
+    projectKeyPattern?: string;
+  }) => {
+    const newTemplate = mockPermissionTemplate({
+      id: `template-${this.#permissionTemplates.length + 1}`,
+      ...data,
+    });
+    this.#permissionTemplates.push(newTemplate);
+    return this.reply({ permissionTemplate: newTemplate });
+  };
+
+  handleUpdatePermissionTemplate = (data: {
+    id: string;
+    description?: string;
+    name?: string;
+    projectKeyPattern?: string;
+  }) => {
+    const { id } = data;
+    const template = this.#permissionTemplates.find((t) => t.id === id);
+    if (template === undefined) {
+      throw new Error(`Couldn't find template with id ${id}`);
+    }
+    Object.assign(template, data);
+
+    return this.reply(undefined);
+  };
+
+  handleDeletePermissionTemplate = (data: { templateId?: string; templateName?: string }) => {
+    const { templateId } = data;
+    this.#permissionTemplates = this.#permissionTemplates.filter((t) => t.id !== templateId);
+    return this.reply(undefined);
+  };
+
+  handleSetDefaultPermissionTemplate = (templateId: string, qualifier: ComponentQualifier) => {
+    this.#permissionTemplates = this.#permissionTemplates.map((t) => ({
+      ...t,
+      defaultFor: t.defaultFor.filter((q) => q !== qualifier),
+    }));
+
+    const template = this.#permissionTemplates.find((t) => t.id === templateId);
+    if (template === undefined) {
+      throw new Error(`Couldn't find template with id ${templateId}`);
+    }
+    template.defaultFor = uniq([...template.defaultFor, qualifier]);
+
+    this.updateDefaults();
+
+    return this.reply(undefined);
+  };
+
   setIsAllowedToChangePermissions = (val: boolean) => {
     this.#isAllowedToChangePermissions = val;
   };
@@ -398,8 +449,25 @@ export default class PermissionsServiceMock {
     this.#users = users;
   };
 
+  getTemplates = () => {
+    return this.#permissionTemplates;
+  };
+
+  updateDefaults = () => {
+    this.#defaultTemplates = [
+      ComponentQualifier.Project,
+      ComponentQualifier.Application,
+      ComponentQualifier.Portfolio,
+    ].map((qualifier) => ({
+      templateId:
+        this.#permissionTemplates.find((t) => t.defaultFor.includes(qualifier))?.id ??
+        this.#permissionTemplates[0].id,
+      qualifier,
+    }));
+  };
+
   reset = () => {
-    this.#permissionTemplates = cloneDeep(defaultPermissionTemplates);
+    this.#permissionTemplates = cloneDeep(defaultTemplates);
     this.#groups = cloneDeep(defaultGroups);
     this.#users = cloneDeep(defaultUsers);
     this.setIsAllowedToChangePermissions(true);
index 8e65bd6ead781baffecb1baeaa892b9287705b8b..dfde7efcc220437d411cc9181564d9db9023d3de 100644 (file)
@@ -74,15 +74,24 @@ export function getPermissionTemplates(): Promise<GetPermissionTemplatesResponse
   return getJSON(url);
 }
 
-export function createPermissionTemplate(data: RequestData) {
+export function createPermissionTemplate(data: {
+  name: string;
+  description?: string;
+  projectKeyPattern?: string;
+}): Promise<{ permissionTemplate: Omit<PermissionTemplate, 'defaultFor'> }> {
   return postJSON('/api/permissions/create_template', data);
 }
 
-export function updatePermissionTemplate(data: RequestData): Promise<void> {
+export function updatePermissionTemplate(data: {
+  id: string;
+  description?: string;
+  name?: string;
+  projectKeyPattern?: string;
+}): Promise<void> {
   return post('/api/permissions/update_template', data);
 }
 
-export function deletePermissionTemplate(data: RequestData) {
+export function deletePermissionTemplate(data: { templateId?: string; templateName?: string }) {
   return post('/api/permissions/delete_template', data).catch(throwGlobalError);
 }
 
@@ -188,7 +197,7 @@ export function getGlobalPermissionsGroups(data: {
 
 export function getPermissionTemplateUsers(data: {
   templateId: string;
-  q?: string | null;
+  q?: string;
   permission?: string;
   p?: number;
   ps?: number;
@@ -201,7 +210,7 @@ export function getPermissionTemplateUsers(data: {
 
 export function getPermissionTemplateGroups(data: {
   templateId: string;
-  q?: string | null;
+  q?: string;
   permission?: string;
   p?: number;
   ps?: number;
index 3423924685fd827d21a8824c5b37b6d677da001e..17bc11e53eafb7b9c6800173ec78af8b8250b073 100644 (file)
@@ -27,7 +27,7 @@ import {
 import ActionsDropdown, { ActionsDropdownItem } from '../../../components/controls/ActionsDropdown';
 import { Router, withRouter } from '../../../components/hoc/withRouter';
 import QualifierIcon from '../../../components/icons/QualifierIcon';
-import { translate } from '../../../helpers/l10n';
+import { translate, translateWithParameters } from '../../../helpers/l10n';
 import { queryToSearch } from '../../../helpers/urls';
 import { PermissionTemplate } from '../../../types/types';
 import { PERMISSION_TEMPLATES_PATH } from '../utils';
@@ -47,7 +47,7 @@ interface State {
   updateModal: boolean;
 }
 
-export class ActionsCell extends React.PureComponent<Props, State> {
+class ActionsCell extends React.PureComponent<Props, State> {
   mounted = false;
   state: State = { deleteForm: false, updateModal: false };
 
@@ -158,7 +158,9 @@ export class ActionsCell extends React.PureComponent<Props, State> {
 
     return (
       <>
-        <ActionsDropdown>
+        <ActionsDropdown
+          label={translateWithParameters('permission_templates.show_actions_for_x', t.name)}
+        >
           {this.renderSetDefaultsControl()}
 
           {!this.props.fromDetails && (
index 1429bed942e9dc99069becb69c9df819a843a1c8..fbf1ffe6ca560388e65b38770458693c6db985ee 100644 (file)
@@ -37,9 +37,9 @@ export default function DeleteForm({ onClose, onSubmit, permissionTemplate: t }:
     <SimpleModal header={header} onClose={onClose} onSubmit={onSubmit}>
       {({ onCloseClick, onFormSubmit, submitting }) => (
         <form onSubmit={onFormSubmit}>
-          <header className="modal-head">
+          <div className="modal-head">
             <h2>{header}</h2>
-          </header>
+          </div>
 
           <div className="modal-body">
             {translateWithParameters(
@@ -48,7 +48,7 @@ export default function DeleteForm({ onClose, onSubmit, permissionTemplate: t }:
             )}
           </div>
 
-          <footer className="modal-foot">
+          <div className="modal-foot">
             <DeferredSpinner className="spacer-right" loading={submitting} />
             <SubmitButton className="button-red" disabled={submitting}>
               {translate('delete')}
@@ -56,7 +56,7 @@ export default function DeleteForm({ onClose, onSubmit, permissionTemplate: t }:
             <ResetButtonLink disabled={submitting} onClick={onCloseClick}>
               {translate('cancel')}
             </ResetButtonLink>
-          </footer>
+          </div>
         </form>
       )}
     </SimpleModal>
index f09bb51d361fc6be467d3775c17c470837830080..6ad3b0bc7c39c7f5f0b0f79c484d3514eae33073 100644 (file)
@@ -88,9 +88,9 @@ export default class Form extends React.PureComponent<Props, State> {
       >
         {({ onCloseClick, onFormSubmit, submitting }) => (
           <form id="permission-template-form" onSubmit={onFormSubmit}>
-            <header className="modal-head">
+            <div className="modal-head">
               <h2>{this.props.header}</h2>
-            </header>
+            </div>
 
             <div className="modal-body">
               <MandatoryFieldsExplanation className="modal-field" />
@@ -140,7 +140,7 @@ export default class Form extends React.PureComponent<Props, State> {
               </div>
             </div>
 
-            <footer className="modal-foot">
+            <div className="modal-foot">
               <DeferredSpinner className="spacer-right" loading={submitting} />
               <SubmitButton disabled={submitting} id="permission-template-submit">
                 {this.props.confirmButtonText}
@@ -152,7 +152,7 @@ export default class Form extends React.PureComponent<Props, State> {
               >
                 {translate('cancel')}
               </ResetButtonLink>
-            </footer>
+            </div>
           </form>
         )}
       </SimpleModal>
index ffae2347077931fa96c3378b13e399b3988d6709..86da435b83da110e50a3d8267b3516a42203c6eb 100644 (file)
@@ -21,6 +21,7 @@ import * as React from 'react';
 import { createPermissionTemplate } from '../../../api/permissions';
 import { Button } from '../../../components/controls/buttons';
 import { Router, withRouter } from '../../../components/hoc/withRouter';
+import DeferredSpinner from '../../../components/ui/DeferredSpinner';
 import { translate } from '../../../helpers/l10n';
 import { PERMISSION_TEMPLATES_PATH } from '../utils';
 import Form from './Form';
@@ -77,7 +78,7 @@ class Header extends React.PureComponent<Props, State> {
       <header className="page-header" id="project-permissions-header">
         <h1 className="page-title">{translate('permission_templates.page')}</h1>
 
-        {!this.props.ready && <i className="spinner" />}
+        <DeferredSpinner loading={!this.props.ready} />
 
         <div className="page-actions">
           <Button onClick={this.handleCreateClick}>{translate('create')}</Button>
index f7bbc5bbd8a7009540b25de61ebc2cb3ee9c2a5b..f7591ffbd9c39b3af6081056ed7608ee5e04e92b 100644 (file)
@@ -39,12 +39,14 @@ export default function Home(props: Props) {
 
       <Header ready={props.ready} refresh={props.refresh} />
 
-      <List
-        permissionTemplates={props.permissionTemplates}
-        permissions={props.permissions}
-        refresh={props.refresh}
-        topQualifiers={props.topQualifiers}
-      />
+      <main>
+        <List
+          permissionTemplates={props.permissionTemplates}
+          permissions={props.permissions}
+          refresh={props.refresh}
+          topQualifiers={props.topQualifiers}
+        />
+      </main>
     </div>
   );
 }
index 0a613050b6f3475ee7c0a758dea9c14b978de02a..0806b4325c0edae924fe32a4f186574ae69077a3 100644 (file)
@@ -42,7 +42,7 @@ interface State {
   permissionTemplates: PermissionTemplate[];
 }
 
-export class PermissionTemplatesApp extends React.PureComponent<Props, State> {
+class PermissionTemplatesApp extends React.PureComponent<Props, State> {
   mounted = false;
   state: State = {
     ready: false,
@@ -52,16 +52,15 @@ export class PermissionTemplatesApp extends React.PureComponent<Props, State> {
 
   componentDidMount() {
     this.mounted = true;
-    this.requestPermissions();
+    this.handleRefresh();
   }
 
   componentWillUnmount() {
     this.mounted = false;
   }
 
-  requestPermissions = async () => {
+  handleRefresh = async () => {
     const { permissions, defaultTemplates, permissionTemplates } = await getPermissionTemplates();
-
     if (this.mounted) {
       const sortedPerm = sortPermissions(permissions);
       const permissionTemplatesMerged = mergeDefaultsToTemplates(
@@ -77,46 +76,46 @@ export class PermissionTemplatesApp extends React.PureComponent<Props, State> {
   };
 
   renderTemplate(id: string) {
-    if (!this.state.ready) {
+    const { permissionTemplates, ready } = this.state;
+    if (!ready) {
       return null;
     }
 
-    const template = this.state.permissionTemplates.find((t) => t.id === id);
+    const template = permissionTemplates.find((t) => t.id === id);
     if (!template) {
       return null;
     }
 
     return (
       <Template
-        refresh={this.requestPermissions}
+        refresh={this.handleRefresh}
         template={template}
         topQualifiers={this.props.appState.qualifiers}
       />
     );
   }
 
-  renderHome() {
-    return (
-      <Home
-        permissionTemplates={this.state.permissionTemplates}
-        permissions={this.state.permissions}
-        ready={this.state.ready}
-        refresh={this.requestPermissions}
-        topQualifiers={this.props.appState.qualifiers}
-      />
-    );
-  }
-
   render() {
-    const { id } = this.props.location.query;
+    const { appState, location } = this.props;
+    const { id } = location.query;
+    const { permissionTemplates, permissions, ready } = this.state;
     return (
-      <main>
+      <>
         <Suggestions suggestions="permission_templates" />
         <Helmet defer={false} title={translate('permission_templates.page')} />
 
-        {id && this.renderTemplate(id)}
-        {!id && this.renderHome()}
-      </main>
+        {id === undefined ? (
+          <Home
+            permissionTemplates={permissionTemplates}
+            permissions={permissions}
+            ready={ready}
+            refresh={this.handleRefresh}
+            topQualifiers={appState.qualifiers}
+          />
+        ) : (
+          this.renderTemplate(id)
+        )}
+      </>
     );
   }
 }
index 3a12d1dd84c7c994fb9a2169c9e8ab30dcfa2515..57fa927316d6c8a38db2ef7d9ede6447e1195464 100644 (file)
@@ -78,7 +78,7 @@ export default class Template extends React.PureComponent<Props, State> {
       filter !== 'groups'
         ? api.getPermissionTemplateUsers({
             templateId: template.id,
-            q: query || null,
+            q: query || undefined,
             permission: selectedPermission,
             p: usersPage,
           })
@@ -88,7 +88,7 @@ export default class Template extends React.PureComponent<Props, State> {
       filter !== 'users'
         ? api.getPermissionTemplateGroups({
             templateId: template.id,
-            q: query || null,
+            q: query || undefined,
             permission: selectedPermission,
             p: groupsPage,
           })
@@ -311,28 +311,29 @@ export default class Template extends React.PureComponent<Props, State> {
           template={template}
           topQualifiers={topQualifiers}
         />
-
-        <TemplateDetails template={template} />
-
-        <AllHoldersList
-          filter={filter}
-          onGrantPermissionToGroup={this.grantPermissionToGroup}
-          onGrantPermissionToUser={this.grantPermissionToUser}
-          groups={groups}
-          groupsPaging={groupsPaging}
-          loading={loading}
-          onFilter={this.handleFilter}
-          onLoadMore={this.onLoadMore}
-          onQuery={this.handleSearch}
-          query={query}
-          onRevokePermissionFromGroup={this.revokePermissionFromGroup}
-          onRevokePermissionFromUser={this.revokePermissionFromUser}
-          users={allUsers}
-          usersPaging={usersPagingWithCreator}
-          permissions={permissions}
-          selectedPermission={selectedPermission}
-          onSelectPermission={this.handleSelectPermission}
-        />
+        <main>
+          <TemplateDetails template={template} />
+
+          <AllHoldersList
+            filter={filter}
+            onGrantPermissionToGroup={this.grantPermissionToGroup}
+            onGrantPermissionToUser={this.grantPermissionToUser}
+            groups={groups}
+            groupsPaging={groupsPaging}
+            loading={loading}
+            onFilter={this.handleFilter}
+            onLoadMore={this.onLoadMore}
+            onQuery={this.handleSearch}
+            query={query}
+            onRevokePermissionFromGroup={this.revokePermissionFromGroup}
+            onRevokePermissionFromUser={this.revokePermissionFromUser}
+            users={allUsers}
+            usersPaging={usersPagingWithCreator}
+            permissions={permissions}
+            selectedPermission={selectedPermission}
+            onSelectPermission={this.handleSelectPermission}
+          />
+        </main>
       </div>
     );
   }
index 6b4e3c5f4addf50b839bedf7433f2c0432775dcd..f391f3567ba247f2bcd4cfe21455d946870a885e 100644 (file)
@@ -19,6 +19,7 @@
  */
 import * as React from 'react';
 import Link from '../../../components/common/Link';
+import DeferredSpinner from '../../../components/ui/DeferredSpinner';
 import { translate } from '../../../helpers/l10n';
 import { PermissionTemplate } from '../../../types/types';
 import { PERMISSION_TEMPLATES_PATH } from '../utils';
@@ -32,7 +33,7 @@ interface Props {
 }
 
 export default function TemplateHeader(props: Props) {
-  const { template } = props;
+  const { template, loading } = props;
   return (
     <header className="page-header" id="project-permissions-header">
       <div className="note spacer-bottom">
@@ -41,7 +42,7 @@ export default function TemplateHeader(props: Props) {
 
       <h1 className="page-title">{template.name}</h1>
 
-      {props.loading && <i className="spinner" />}
+      <DeferredSpinner loading={loading} />
 
       <div className="pull-right">
         <ActionsCell
diff --git a/server/sonar-web/src/main/js/apps/permission-templates/components/__tests__/ActionsCell-test.tsx b/server/sonar-web/src/main/js/apps/permission-templates/components/__tests__/ActionsCell-test.tsx
deleted file mode 100644 (file)
index 15c292d..0000000
+++ /dev/null
@@ -1,56 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2023 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-import { shallow } from 'enzyme';
-import * as React from 'react';
-import { mockRouter } from '../../../../helpers/testMocks';
-import { ActionsCell } from '../ActionsCell';
-
-const SAMPLE = {
-  createdAt: '2018-01-01',
-  id: 'id',
-  name: 'name',
-  permissions: [],
-  defaultFor: [],
-};
-
-function renderActionsCell(props?: Partial<ActionsCell['props']>) {
-  return shallow(
-    <ActionsCell
-      permissionTemplate={SAMPLE}
-      refresh={() => true}
-      router={mockRouter()}
-      topQualifiers={['TRK', 'VW']}
-      {...props}
-    />
-  );
-}
-
-it('should set default', () => {
-  const setDefault = renderActionsCell().find('.js-set-default');
-  expect(setDefault.length).toBe(2);
-  expect(setDefault.at(0).prop('data-qualifier')).toBe('TRK');
-  expect(setDefault.at(1).prop('data-qualifier')).toBe('VW');
-});
-
-it('should not set default', () => {
-  const permissionTemplate = { ...SAMPLE, defaultFor: ['TRK', 'VW'] };
-  const setDefault = renderActionsCell({ permissionTemplate }).find('.js-set-default');
-  expect(setDefault.length).toBe(0);
-});
diff --git a/server/sonar-web/src/main/js/apps/permission-templates/components/__tests__/Defaults-test.tsx b/server/sonar-web/src/main/js/apps/permission-templates/components/__tests__/Defaults-test.tsx
deleted file mode 100644 (file)
index 0fb6bd8..0000000
+++ /dev/null
@@ -1,43 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2023 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-import { shallow } from 'enzyme';
-import * as React from 'react';
-import { PermissionTemplate } from '../../../../types/types';
-import Defaults from '../Defaults';
-
-const SAMPLE: PermissionTemplate = {
-  createdAt: '2018-01-01',
-  defaultFor: [],
-  id: 'id',
-  name: 'name',
-  permissions: [],
-};
-
-it('should render one qualifier', () => {
-  const sample = { ...SAMPLE, defaultFor: ['DEV'] };
-  const output = shallow(<Defaults template={sample} />);
-  expect(output).toMatchSnapshot();
-});
-
-it('should render several qualifiers', () => {
-  const sample = { ...SAMPLE, defaultFor: ['TRK', 'VW'] };
-  const output = shallow(<Defaults template={sample} />);
-  expect(output).toMatchSnapshot();
-});
diff --git a/server/sonar-web/src/main/js/apps/permission-templates/components/__tests__/Form-test.tsx b/server/sonar-web/src/main/js/apps/permission-templates/components/__tests__/Form-test.tsx
deleted file mode 100644 (file)
index 251d815..0000000
+++ /dev/null
@@ -1,30 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2023 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-import { shallow } from 'enzyme';
-import * as React from 'react';
-import Form from '../Form';
-
-it('render correctly', () => {
-  expect(
-    shallow(
-      <Form confirmButtonText="confirm" header="title" onClose={jest.fn()} onSubmit={jest.fn()} />
-    )
-  ).toMatchSnapshot();
-});
diff --git a/server/sonar-web/src/main/js/apps/permission-templates/components/__tests__/ListItem-test.tsx b/server/sonar-web/src/main/js/apps/permission-templates/components/__tests__/ListItem-test.tsx
deleted file mode 100644 (file)
index 9905f13..0000000
+++ /dev/null
@@ -1,43 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2023 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-import { shallow } from 'enzyme';
-import * as React from 'react';
-import ListItem from '../ListItem';
-
-it('render correctly', () => {
-  expect(shallowRender()).toMatchSnapshot();
-});
-
-function shallowRender() {
-  return shallow(
-    <ListItem
-      key="1"
-      refresh={async () => {}}
-      template={{
-        id: '1',
-        createdAt: '2020-01-01',
-        name: 'test',
-        defaultFor: [],
-        permissions: [],
-      }}
-      topQualifiers={[]}
-    />
-  );
-}
diff --git a/server/sonar-web/src/main/js/apps/permission-templates/components/__tests__/NameCell-test.tsx b/server/sonar-web/src/main/js/apps/permission-templates/components/__tests__/NameCell-test.tsx
deleted file mode 100644 (file)
index fb15f09..0000000
+++ /dev/null
@@ -1,40 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2023 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-import { shallow } from 'enzyme';
-import * as React from 'react';
-import NameCell from '../NameCell';
-
-it('render correctly', () => {
-  expect(shallowRender()).toMatchSnapshot();
-});
-
-function shallowRender() {
-  return shallow(
-    <NameCell
-      template={{
-        id: '1',
-        createdAt: '2020-01-01',
-        name: 'test',
-        defaultFor: ['user'],
-        permissions: [],
-      }}
-    />
-  );
-}
index 799cb16ee55c209cbf3a6ccfeac2df02b5b6adb5..97b58d1a82beb71846174f9870422818f08d7b74 100644 (file)
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
+import { act, screen, waitFor, within } from '@testing-library/react';
 import userEvent from '@testing-library/user-event';
-import React from 'react';
-import { byRole } from 'testing-library-selector';
+import { UserEvent } from '@testing-library/user-event/dist/types/setup/setup';
+import { uniq } from 'lodash';
+import { byLabelText, byRole } from 'testing-library-selector';
 import PermissionsServiceMock from '../../../../api/mocks/PermissionsServiceMock';
+import { mockPermissionGroup, mockPermissionUser } from '../../../../helpers/mocks/permissions';
+import { PERMISSIONS_ORDER_FOR_PROJECT_TEMPLATE } from '../../../../helpers/permissions';
 import { mockAppState } from '../../../../helpers/testMocks';
-import { renderApp } from '../../../../helpers/testReactTestingUtils';
+import { renderAppWithAdminContext } from '../../../../helpers/testReactTestingUtils';
 import { ComponentQualifier } from '../../../../types/component';
 import { Permissions } from '../../../../types/permissions';
-import PermissionTemplatesApp from '../PermissionTemplatesApp';
+import { PermissionGroup, PermissionUser } from '../../../../types/types';
+import routes from '../../routes';
 
 const serviceMock = new PermissionsServiceMock();
 
@@ -33,64 +38,492 @@ beforeEach(() => {
   serviceMock.reset();
 });
 
-const ui = {
-  templateLink1: byRole('link', { name: 'Permission Template 1' }),
-  permissionCheckbox: (target: string, permission: Permissions) =>
-    byRole('checkbox', {
-      name: `permission.assign_x_to_y.projects_role.${permission}.${target}`,
-    }),
-  showMoreButton: byRole('button', { name: 'show_more' }),
-};
+describe('rendering', () => {
+  it('should render the list of templates', async () => {
+    const user = userEvent.setup();
+    const ui = getPageObject(user);
+    renderPermissionTemplatesApp();
+    await ui.appLoaded();
 
-it('grants/revokes permission from users or groups', async () => {
-  const user = userEvent.setup();
-  renderPermissionTemplatesApp();
+    // Lists all templates.
+    expect(ui.templateLink('Permission Template 1').get()).toBeInTheDocument();
+    expect(ui.templateLink('Permission Template 2').get()).toBeInTheDocument();
+
+    // Shows all permission table headers.
+    PERMISSIONS_ORDER_FOR_PROJECT_TEMPLATE.forEach((permission, i) => {
+      expect(ui.getTableHeaderHelpTooltip(i + 1)).toHaveTextContent(
+        `projects_role.${permission}.desc`
+      );
+    });
+
+    // Shows warning for browse and code viewer permissions.
+    [Permissions.Browse, Permissions.CodeViewer].forEach((_permission, i) => {
+      expect(ui.getTableHeaderHelpTooltip(i + 1)).toHaveTextContent(
+        'projects_role.public_projects_warning'
+      );
+    });
+
+    // Check summaries.
+    // Note: because of the intricacies of these table cells, and the verbosity
+    // this would introduce in this test, I went ahead and relied on snapshots.
+    // The snapshots only focus on the text content, so any updates in styling
+    // or DOM structure should not alter the snapshots.
+    const row1 = within(screen.getByRole('row', { name: /Permission Template 1/ }));
+    PERMISSIONS_ORDER_FOR_PROJECT_TEMPLATE.forEach((permission, i) => {
+      expect(row1.getAllByRole('cell').at(i + 1)?.textContent).toMatchSnapshot(
+        `Permission Template 1: ${permission}`
+      );
+    });
+    const row2 = within(screen.getByRole('row', { name: /Permission Template 2/ }));
+    PERMISSIONS_ORDER_FOR_PROJECT_TEMPLATE.forEach((permission, i) => {
+      expect(row2.getAllByRole('cell').at(i + 1)?.textContent).toMatchSnapshot(
+        `Permission Template 2: ${permission}`
+      );
+    });
+  });
+
+  it('should render the correct template', async () => {
+    const user = userEvent.setup();
+    const ui = getPageObject(user);
+    renderPermissionTemplatesApp();
+    await ui.appLoaded();
+
+    await ui.openTemplateDetails('Permission Template 1');
+    await ui.appLoaded();
+
+    expect(screen.getByText('This is permission template 1')).toBeInTheDocument();
+    PERMISSIONS_ORDER_FOR_PROJECT_TEMPLATE.forEach((permission, i) => {
+      expect(ui.permissionCheckbox('johndoe', permission).get()).toBeInTheDocument();
+      expect(ui.getTableHeaderHelpTooltip(i)).toHaveTextContent(`projects_role.${permission}.desc`);
+    });
+  });
+});
+
+describe('CRUD', () => {
+  it('should allow the creation of new templates', async () => {
+    const user = userEvent.setup();
+    const ui = getPageObject(user);
+    renderPermissionTemplatesApp();
+    await ui.appLoaded();
+
+    await act(async () => {
+      await ui.createNewTemplate('New Permission Template', 'New template description');
+    });
+    await ui.appLoaded();
+
+    expect(screen.getByRole('heading', { name: 'New Permission Template' })).toBeInTheDocument();
+    expect(screen.getByText('New template description')).toBeInTheDocument();
+  });
+
+  it('should allow the create modal to be opened and closed', async () => {
+    const user = userEvent.setup();
+    const ui = getPageObject(user);
+    renderPermissionTemplatesApp();
+    await ui.appLoaded();
+
+    await ui.openCreateModal();
+    await ui.closeModal();
+
+    expect(ui.modal.query()).not.toBeInTheDocument();
+  });
+
+  it('should allow template details to be updated from the list', async () => {
+    const user = userEvent.setup();
+    const ui = getPageObject(user);
+    renderPermissionTemplatesApp();
+    await ui.appLoaded();
+
+    await ui.updateTemplate(
+      'Permission Template 2',
+      'Updated name',
+      'Updated description',
+      '/new pattern/'
+    );
 
-  await user.click(await ui.templateLink1.find());
+    expect(ui.templateLink('Updated name').get()).toBeInTheDocument();
+    expect(screen.getByText('Updated description')).toBeInTheDocument();
+    expect(screen.getByText('/new pattern/')).toBeInTheDocument();
+  });
+
+  it('should allow template details to be updated from the template page directly', async () => {
+    const user = userEvent.setup();
+    const ui = getPageObject(user);
+    renderPermissionTemplatesApp();
+    await ui.appLoaded();
+
+    await ui.openTemplateDetails('Permission Template 2');
+    await ui.appLoaded();
+
+    await ui.updateTemplate(
+      'Permission Template 2',
+      'Updated name',
+      'Updated description',
+      '/new pattern/'
+    );
+
+    expect(screen.getByText('Updated name')).toBeInTheDocument();
+    expect(screen.getByText('Updated description')).toBeInTheDocument();
+    expect(screen.getByText('/new pattern/')).toBeInTheDocument();
+  });
 
-  // User
-  expect(ui.permissionCheckbox('Admin Admin', Permissions.Browse).get()).not.toBeChecked();
-  await user.click(ui.permissionCheckbox('Admin Admin', Permissions.Browse).get());
-  expect(ui.permissionCheckbox('Admin Admin', Permissions.Browse).get()).toBeChecked();
+  it('should allow the update modal to be opened and closed', async () => {
+    const user = userEvent.setup();
+    const ui = getPageObject(user);
+    renderPermissionTemplatesApp();
+    await ui.appLoaded();
 
-  expect(ui.permissionCheckbox('Admin Admin', Permissions.Admin).get()).toBeChecked();
-  await user.click(ui.permissionCheckbox('Admin Admin', Permissions.Admin).get());
-  expect(ui.permissionCheckbox('Admin Admin', Permissions.Admin).get()).not.toBeChecked();
+    await ui.openUpdateModal('Permission Template 2');
+    await ui.closeModal();
 
-  // Group
-  expect(ui.permissionCheckbox('Anyone', Permissions.Browse).get()).not.toBeChecked();
-  await user.click(ui.permissionCheckbox('Anyone', Permissions.Browse).get());
-  expect(ui.permissionCheckbox('Anyone', Permissions.Browse).get()).toBeChecked();
+    expect(ui.modal.query()).not.toBeInTheDocument();
+  });
+
+  it('should allow templates to be deleted from the list', async () => {
+    const user = userEvent.setup();
+    const ui = getPageObject(user);
+    renderPermissionTemplatesApp();
+    await ui.appLoaded();
+
+    await act(async () => {
+      await ui.deleteTemplate('Permission Template 2');
+    });
+    await ui.appLoaded();
+
+    expect(ui.templateLink('Permission Template 1').get()).toBeInTheDocument();
+    expect(ui.templateLink('Permission Template 2').query()).not.toBeInTheDocument();
+  });
 
-  expect(ui.permissionCheckbox('Anyone', Permissions.CodeViewer).get()).toBeChecked();
-  await user.click(ui.permissionCheckbox('Anyone', Permissions.CodeViewer).get());
-  expect(ui.permissionCheckbox('Anyone', Permissions.CodeViewer).get()).not.toBeChecked();
+  it('should allow templates to be deleted from the template page directly', async () => {
+    const user = userEvent.setup();
+    const ui = getPageObject(user);
+    renderPermissionTemplatesApp();
+    await ui.appLoaded();
 
-  // Handles error on permission change
-  serviceMock.setIsAllowedToChangePermissions(false);
-  await user.click(ui.permissionCheckbox('Admin Admin', Permissions.Browse).get());
-  expect(ui.permissionCheckbox('Admin Admin', Permissions.Browse).get()).toBeChecked();
+    await ui.openTemplateDetails('Permission Template 2');
+    await ui.appLoaded();
 
-  await user.click(ui.permissionCheckbox('Anyone', Permissions.CodeViewer).get());
-  expect(ui.permissionCheckbox('Anyone', Permissions.CodeViewer).get()).not.toBeChecked();
+    await act(async () => {
+      await ui.deleteTemplate('Permission Template 2');
+    });
+    await ui.appLoaded();
 
-  await user.click(ui.permissionCheckbox('Admin Admin', Permissions.Admin).get());
-  expect(ui.permissionCheckbox('Admin Admin', Permissions.Admin).get()).not.toBeChecked();
+    expect(ui.templateLink('Permission Template 1').get()).toBeInTheDocument();
+    expect(ui.templateLink('Permission Template 2').query()).not.toBeInTheDocument();
+  });
+
+  it('should allow the delete modal to be opened and closed', async () => {
+    const user = userEvent.setup();
+    const ui = getPageObject(user);
+    renderPermissionTemplatesApp();
+    await ui.appLoaded();
+
+    await ui.openDeleteModal('Permission Template 2');
+    await ui.closeModal();
+
+    expect(ui.modal.query()).not.toBeInTheDocument();
+  });
+
+  it('should not allow a default template to be deleted', async () => {
+    const user = userEvent.setup();
+    const ui = getPageObject(user);
+    renderPermissionTemplatesApp();
+    await ui.appLoaded();
+
+    await user.click(ui.cogMenuBtn('Permission Template 1').get());
+
+    expect(ui.deleteBtn.query()).not.toBeInTheDocument();
+  });
 });
 
-it('loads more items on Show More', async () => {
+describe('filtering', () => {
+  it('should allow to filter permission holders', async () => {
+    const user = userEvent.setup();
+    const ui = getPageObject(user);
+    renderPermissionTemplatesApp();
+    await ui.appLoaded();
+
+    await ui.openTemplateDetails('Permission Template 1');
+    await ui.appLoaded();
+
+    expect(screen.getByText('sonar-users')).toBeInTheDocument();
+    expect(screen.getByText('johndoe')).toBeInTheDocument();
+
+    await ui.showOnlyUsers();
+    expect(screen.queryByText('sonar-users')).not.toBeInTheDocument();
+    expect(screen.getByText('johndoe')).toBeInTheDocument();
+
+    await ui.showOnlyGroups();
+    expect(screen.getByText('sonar-users')).toBeInTheDocument();
+    expect(screen.queryByText('johndoe')).not.toBeInTheDocument();
+
+    await ui.showAll();
+    expect(screen.getByText('sonar-users')).toBeInTheDocument();
+    expect(screen.getByText('johndoe')).toBeInTheDocument();
+
+    await ui.searchFor('sonar-adm');
+    expect(screen.getByText('sonar-admins')).toBeInTheDocument();
+    expect(screen.queryByText('sonar-users')).not.toBeInTheDocument();
+    expect(screen.queryByText('johndoe')).not.toBeInTheDocument();
+
+    await ui.clearSearch();
+    expect(screen.getByText('sonar-users')).toBeInTheDocument();
+    expect(screen.getByText('johndoe')).toBeInTheDocument();
+  });
+
+  it('should allow to show only permission holders with a specific permission', async () => {
+    const user = userEvent.setup();
+    const ui = getPageObject(user);
+    renderPermissionTemplatesApp();
+    await ui.appLoaded();
+
+    await ui.openTemplateDetails('Permission Template 1');
+    await ui.appLoaded();
+
+    expect(screen.getAllByRole('row').length).toBe(12);
+    await ui.toggleFilterByPermission(Permissions.Admin);
+    expect(screen.getAllByRole('row').length).toBe(2);
+    await ui.toggleFilterByPermission(Permissions.Admin);
+    expect(screen.getAllByRole('row').length).toBe(12);
+  });
+});
+
+describe('assigning/revoking permissions', () => {
+  it('should add and remove permissions to/from a group', async () => {
+    const user = userEvent.setup();
+    const ui = getPageObject(user);
+    renderPermissionTemplatesApp();
+    await ui.appLoaded();
+
+    await ui.openTemplateDetails('Permission Template 1');
+    await ui.appLoaded();
+
+    expect(ui.permissionCheckbox('sonar-users', Permissions.Admin).get()).not.toBeChecked();
+
+    await ui.togglePermission('sonar-users', Permissions.Admin);
+    await ui.appLoaded();
+    expect(ui.permissionCheckbox('sonar-users', Permissions.Admin).get()).toBeChecked();
+
+    await ui.togglePermission('sonar-users', Permissions.Admin);
+    await ui.appLoaded();
+    expect(ui.permissionCheckbox('sonar-users', Permissions.Admin).get()).not.toBeChecked();
+  });
+
+  it('should add and remove permissions to/from a user', async () => {
+    const user = userEvent.setup();
+    const ui = getPageObject(user);
+    renderPermissionTemplatesApp();
+    await ui.appLoaded();
+
+    await ui.openTemplateDetails('Permission Template 1');
+    await ui.appLoaded();
+
+    expect(ui.permissionCheckbox('johndoe', Permissions.Scan).get()).not.toBeChecked();
+
+    await ui.togglePermission('johndoe', Permissions.Scan);
+    await ui.appLoaded();
+    expect(ui.permissionCheckbox('johndoe', Permissions.Scan).get()).toBeChecked();
+
+    await ui.togglePermission('johndoe', Permissions.Scan);
+    await ui.appLoaded();
+    expect(ui.permissionCheckbox('johndoe', Permissions.Scan).get()).not.toBeChecked();
+  });
+
+  it('should handle errors correctly', async () => {
+    serviceMock.setIsAllowedToChangePermissions(false);
+    const user = userEvent.setup();
+    const ui = getPageObject(user);
+    renderPermissionTemplatesApp();
+    await ui.appLoaded();
+
+    await ui.openTemplateDetails('Permission Template 1');
+    await ui.appLoaded();
+
+    expect(ui.permissionCheckbox('johndoe', Permissions.Scan).get()).not.toBeChecked();
+    await ui.togglePermission('johndoe', Permissions.Scan);
+    await ui.appLoaded();
+    expect(ui.permissionCheckbox('johndoe', Permissions.Scan).get()).not.toBeChecked();
+  });
+});
+
+it('should correctly handle pagination', async () => {
+  const groups: PermissionGroup[] = [];
+  const users: PermissionUser[] = [];
+  Array.from(Array(20).keys()).forEach((i) => {
+    groups.push(mockPermissionGroup({ name: `Group ${i}` }));
+    users.push(mockPermissionUser({ login: `user-${i}` }));
+  });
+  serviceMock.setGroups(groups);
+  serviceMock.setUsers(users);
+
   const user = userEvent.setup();
+  const ui = getPageObject(user);
   renderPermissionTemplatesApp();
+  await ui.appLoaded();
 
-  await user.click(await ui.templateLink1.find());
+  await ui.openTemplateDetails('Permission Template 1');
+  await ui.appLoaded();
 
-  expect(ui.permissionCheckbox('White', Permissions.Browse).query()).not.toBeInTheDocument();
-  await user.click(ui.showMoreButton.get());
-  expect(ui.permissionCheckbox('White', Permissions.Browse).get()).toBeInTheDocument();
+  expect(screen.getAllByRole('row').length).toBe(14);
+  await ui.clickLoadMore();
+  expect(screen.getAllByRole('row').length).toBe(24);
 });
 
-function renderPermissionTemplatesApp() {
-  renderApp('admin/permission_templates', <PermissionTemplatesApp />, {
-    appState: mockAppState({ qualifiers: [ComponentQualifier.Project] }),
+it.each([ComponentQualifier.Project, ComponentQualifier.Application, ComponentQualifier.Portfolio])(
+  'should correctly be assignable by default to %s',
+  async (qualifier) => {
+    const user = userEvent.setup();
+    const ui = getPageObject(user);
+    renderPermissionTemplatesApp(uniq([ComponentQualifier.Project, qualifier]));
+    await ui.appLoaded();
+
+    await ui.setTemplateAsDefaultFor('Permission Template 2', qualifier);
+
+    const row1 = within(screen.getByRole('row', { name: /Permission Template 1/ }));
+    const row2 = within(screen.getByRole('row', { name: /Permission Template 2/ }));
+    const regex = new RegExp(`permission_template\\.default_for\\.(.*)qualifiers.${qualifier}`);
+    expect(row2.getByText(regex)).toBeInTheDocument();
+    expect(row1.queryByText(regex)).not.toBeInTheDocument();
+  }
+);
+
+function getPageObject(user: UserEvent) {
+  const ui = {
+    loading: byLabelText('loading'),
+    templateLink: (name: string) => byRole('link', { name }),
+    permissionCheckbox: (target: string, permission: Permissions) =>
+      byRole('checkbox', {
+        name: `permission.assign_x_to_y.projects_role.${permission}.${target}`,
+      }),
+    tableHeaderFilter: (permission: Permissions) =>
+      byRole('link', { name: `projects_role.${permission}` }),
+    onlyUsersBtn: byRole('button', { name: 'users.page' }),
+    onlyGroupsBtn: byRole('button', { name: 'user_groups.page' }),
+    showAllBtn: byRole('button', { name: 'all' }),
+    searchInput: byRole('searchbox', { name: 'search.search_for_users_or_groups' }),
+    loadMoreBtn: byRole('button', { name: 'show_more' }),
+    createNewTemplateBtn: byRole('button', { name: 'create' }),
+    modal: byRole('dialog'),
+    cogMenuBtn: (name: string) =>
+      byRole('button', { name: `permission_templates.show_actions_for_x.${name}` }),
+    deleteBtn: byRole('button', { name: 'delete' }),
+    updateDetailsBtn: byRole('button', { name: 'update_details' }),
+    setDefaultBtn: (qualifier: ComponentQualifier) =>
+      byRole('button', {
+        name:
+          qualifier === ComponentQualifier.Project
+            ? 'permission_templates.set_default'
+            : `permission_templates.set_default_for qualifier.${qualifier} qualifiers.${qualifier}`,
+      }),
+  };
+
+  return {
+    ...ui,
+    async appLoaded() {
+      await waitFor(() => {
+        expect(ui.loading.query()).not.toBeInTheDocument();
+      });
+    },
+    async openTemplateDetails(name: string) {
+      await user.click(ui.templateLink(name).get());
+    },
+    async toggleFilterByPermission(permission: Permissions) {
+      await user.click(ui.tableHeaderFilter(permission).get());
+    },
+    async showOnlyUsers() {
+      await user.click(ui.onlyUsersBtn.get());
+    },
+    async showOnlyGroups() {
+      await user.click(ui.onlyGroupsBtn.get());
+    },
+    async showAll() {
+      await user.click(ui.showAllBtn.get());
+    },
+    async searchFor(name: string) {
+      await user.type(ui.searchInput.get(), name);
+    },
+    async clearSearch() {
+      await user.clear(ui.searchInput.get());
+    },
+    async clickLoadMore() {
+      await user.click(ui.loadMoreBtn.get());
+    },
+    async togglePermission(target: string, permission: Permissions) {
+      await user.click(ui.permissionCheckbox(target, permission).get());
+    },
+    async openCreateModal() {
+      await user.click(ui.createNewTemplateBtn.get());
+    },
+    async createNewTemplate(name: string, description: string, pattern?: string) {
+      await user.click(ui.createNewTemplateBtn.get());
+      const modal = within(ui.modal.get());
+      await user.type(modal.getByRole('textbox', { name: /name/ }), name);
+      await user.type(modal.getByRole('textbox', { name: 'description' }), description);
+      if (pattern) {
+        await user.type(
+          modal.getByRole('textbox', { name: 'permission_template.key_pattern' }),
+          pattern
+        );
+      }
+      await user.click(modal.getByRole('button', { name: 'create' }));
+    },
+    async openDeleteModal(name: string) {
+      await user.click(ui.cogMenuBtn(name).get());
+      await user.click(ui.deleteBtn.get());
+    },
+    async deleteTemplate(name: string) {
+      await user.click(ui.cogMenuBtn(name).get());
+      await user.click(ui.deleteBtn.get());
+      const modal = within(ui.modal.get());
+      await user.click(modal.getByRole('button', { name: 'delete' }));
+    },
+    async openUpdateModal(name: string) {
+      await user.click(ui.cogMenuBtn(name).get());
+      await user.click(ui.updateDetailsBtn.get());
+    },
+    async updateTemplate(
+      name: string,
+      newName: string,
+      newDescription: string,
+      newPattern: string
+    ) {
+      await user.click(ui.cogMenuBtn(name).get());
+      await user.click(ui.updateDetailsBtn.get());
+
+      const modal = within(ui.modal.get());
+      const nameInput = modal.getByRole('textbox', { name: /name/ });
+      const descriptionInput = modal.getByRole('textbox', { name: 'description' });
+      const patternInput = modal.getByRole('textbox', { name: 'permission_template.key_pattern' });
+
+      await user.clear(nameInput);
+      await user.type(nameInput, newName);
+      await user.clear(descriptionInput);
+      await user.type(descriptionInput, newDescription);
+      await user.clear(patternInput);
+      await user.type(patternInput, newPattern);
+
+      await user.click(modal.getByRole('button', { name: 'update_verb' }));
+    },
+    async closeModal() {
+      const modal = within(ui.modal.get());
+      await user.click(modal.getByRole('button', { name: 'cancel' }));
+    },
+    async setTemplateAsDefaultFor(name: string, qualifier: ComponentQualifier) {
+      await user.click(ui.cogMenuBtn(name).get());
+      await user.click(ui.setDefaultBtn(qualifier).get());
+    },
+    getTableHeaderHelpTooltip(i: number) {
+      const th = byRole('columnheader').getAll().at(i);
+      if (th === undefined) {
+        throw new Error(`Couldn't locate the <th> at index ${i}`);
+      }
+      within(th).getByTestId('help-tooltip-activator').focus();
+      return screen.getByRole('tooltip');
+    },
+  };
+}
+
+function renderPermissionTemplatesApp(qualifiers = [ComponentQualifier.Project]) {
+  renderAppWithAdminContext('admin/permission_templates', routes, {
+    appState: mockAppState({ qualifiers }),
   });
 }
diff --git a/server/sonar-web/src/main/js/apps/permission-templates/components/__tests__/__snapshots__/Defaults-test.tsx.snap b/server/sonar-web/src/main/js/apps/permission-templates/components/__tests__/__snapshots__/Defaults-test.tsx.snap
deleted file mode 100644 (file)
index 2713b36..0000000
+++ /dev/null
@@ -1,21 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should render one qualifier 1`] = `
-<div>
-  <span
-    className="badge spacer-right"
-  >
-    permission_template.default_for.qualifiers.DEV
-  </span>
-</div>
-`;
-
-exports[`should render several qualifiers 1`] = `
-<div>
-  <span
-    className="badge spacer-right"
-  >
-    permission_template.default_for.qualifiers.TRK, qualifiers.VW
-  </span>
-</div>
-`;
diff --git a/server/sonar-web/src/main/js/apps/permission-templates/components/__tests__/__snapshots__/Form-test.tsx.snap b/server/sonar-web/src/main/js/apps/permission-templates/components/__tests__/__snapshots__/Form-test.tsx.snap
deleted file mode 100644 (file)
index 81c1df3..0000000
+++ /dev/null
@@ -1,12 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`render correctly 1`] = `
-<SimpleModal
-  header="title"
-  onClose={[MockFunction]}
-  onSubmit={[Function]}
-  size="small"
->
-  <Component />
-</SimpleModal>
-`;
diff --git a/server/sonar-web/src/main/js/apps/permission-templates/components/__tests__/__snapshots__/ListItem-test.tsx.snap b/server/sonar-web/src/main/js/apps/permission-templates/components/__tests__/__snapshots__/ListItem-test.tsx.snap
deleted file mode 100644 (file)
index 44b252c..0000000
+++ /dev/null
@@ -1,37 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`render correctly 1`] = `
-<tr
-  data-id="1"
-  data-name="test"
->
-  <NameCell
-    template={
-      {
-        "createdAt": "2020-01-01",
-        "defaultFor": [],
-        "id": "1",
-        "name": "test",
-        "permissions": [],
-      }
-    }
-  />
-  <td
-    className="nowrap thin text-right text-top little-padded-left little-padded-right"
-  >
-    <withRouter(ActionsCell)
-      permissionTemplate={
-        {
-          "createdAt": "2020-01-01",
-          "defaultFor": [],
-          "id": "1",
-          "name": "test",
-          "permissions": [],
-        }
-      }
-      refresh={[Function]}
-      topQualifiers={[]}
-    />
-  </td>
-</tr>
-`;
diff --git a/server/sonar-web/src/main/js/apps/permission-templates/components/__tests__/__snapshots__/NameCell-test.tsx.snap b/server/sonar-web/src/main/js/apps/permission-templates/components/__tests__/__snapshots__/NameCell-test.tsx.snap
deleted file mode 100644 (file)
index acb52f5..0000000
+++ /dev/null
@@ -1,39 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`render correctly 1`] = `
-<td
-  className="little-padded-left little-padded-right"
->
-  <ForwardRef(Link)
-    to={
-      {
-        "pathname": "/admin/permission_templates",
-        "search": "?id=1",
-      }
-    }
-  >
-    <strong
-      className="js-name"
-    >
-      test
-    </strong>
-  </ForwardRef(Link)>
-  <div
-    className="spacer-top js-defaults"
-  >
-    <Defaults
-      template={
-        {
-          "createdAt": "2020-01-01",
-          "defaultFor": [
-            "user",
-          ],
-          "id": "1",
-          "name": "test",
-          "permissions": [],
-        }
-      }
-    />
-  </div>
-</td>
-`;
diff --git a/server/sonar-web/src/main/js/apps/permission-templates/components/__tests__/__snapshots__/PermissionTemplatesApp-it.tsx.snap b/server/sonar-web/src/main/js/apps/permission-templates/components/__tests__/__snapshots__/PermissionTemplatesApp-it.tsx.snap
new file mode 100644 (file)
index 0000000..e05b703
--- /dev/null
@@ -0,0 +1,25 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`rendering should render the list of templates: Permission Template 1: admin 1`] = `"0  user(s)1 group(s)"`;
+
+exports[`rendering should render the list of templates: Permission Template 1: codeviewer 1`] = `"0  user(s)0 group(s)"`;
+
+exports[`rendering should render the list of templates: Permission Template 1: issueadmin 1`] = `"2  user(s)0 group(s)"`;
+
+exports[`rendering should render the list of templates: Permission Template 1: scan 1`] = `"0  user(s)0 group(s)"`;
+
+exports[`rendering should render the list of templates: Permission Template 1: securityhotspotadmin 1`] = `"1  user(s)0 group(s)"`;
+
+exports[`rendering should render the list of templates: Permission Template 1: user 1`] = `"3  user(s)2 group(s)"`;
+
+exports[`rendering should render the list of templates: Permission Template 2: admin 1`] = `"0  user(s)0 group(s)"`;
+
+exports[`rendering should render the list of templates: Permission Template 2: codeviewer 1`] = `"permission_templates.project_creatorspermission_templates.project_creators.explanation0  user(s)0 group(s)"`;
+
+exports[`rendering should render the list of templates: Permission Template 2: issueadmin 1`] = `"0  user(s)0 group(s)"`;
+
+exports[`rendering should render the list of templates: Permission Template 2: scan 1`] = `"0  user(s)0 group(s)"`;
+
+exports[`rendering should render the list of templates: Permission Template 2: securityhotspotadmin 1`] = `"0  user(s)0 group(s)"`;
+
+exports[`rendering should render the list of templates: Permission Template 2: user 1`] = `"permission_templates.project_creatorspermission_templates.project_creators.explanation0  user(s)0 group(s)"`;
index f73dbcac56a0c19ef4403ea8ec17ec5e45fbb9d1..568fa687c7b3aa2413e39d6739b1d58b39ac23b7 100644 (file)
@@ -103,11 +103,11 @@ describe('filtering', () => {
     renderPermissionsProjectApp();
     await ui.appLoaded();
 
-    expect(screen.getAllByRole('row').length).toBe(7);
+    expect(screen.getAllByRole('row').length).toBe(11);
     await ui.toggleFilterByPermission(Permissions.Admin);
     expect(screen.getAllByRole('row').length).toBe(2);
     await ui.toggleFilterByPermission(Permissions.Admin);
-    expect(screen.getAllByRole('row').length).toBe(7);
+    expect(screen.getAllByRole('row').length).toBe(11);
   });
 });
 
index d7850a004fe4a92738ec227c14cd63bd3ba957e5..a3ff2ac1307f1da7345fe16400182542e1f06654 100644 (file)
@@ -73,22 +73,3 @@ export function mockPermissionTemplate(override: Partial<PermissionTemplate> = {
     ...override,
   };
 }
-
-export function mockTemplateUser(override: Partial<PermissionUser> = {}) {
-  return {
-    login: 'admin',
-    name: 'Admin Admin',
-    permissions: ['admin', 'codeviewer'],
-    ...override,
-  };
-}
-
-export function mockTemplateGroup(override: Partial<PermissionGroup> = {}) {
-  return {
-    id: 'Anyone',
-    name: 'Anyone',
-    description: 'everyone',
-    permissions: ['admin', 'codeviewer'],
-    ...override,
-  };
-}
index b55aa2e688119c944d373d6c64692e8eb8cb02c3..62bb421d3a666b69882e1dc22a86ebfa7463da79 100644 (file)
@@ -2834,6 +2834,7 @@ permission_templates.bulk_apply_permission_template.apply_to_selected=You're abo
 permission_templates.bulk_apply_permission_template.apply_to_all=You're about to apply the selected permission template to {0} item(s).
 permission_templates.select_to_delete=You must select at least one item
 permission_templates.delete_selected=Delete all selected items
+permission_templates.show_actions_for_x=Show actions for template {0} 
 
 
 #------------------------------------------------------------------------------