@@ -20,7 +20,12 @@ | |||
import { throwGlobalError } from '../helpers/error'; | |||
import { getJSON, post, postJSON, RequestData } from '../helpers/request'; | |||
import { BranchParameters } from '../types/branch-like'; | |||
import { ComponentQualifier, TreeComponent, TreeComponentWithPath } from '../types/component'; | |||
import { | |||
ComponentQualifier, | |||
TreeComponent, | |||
TreeComponentWithPath, | |||
Visibility, | |||
} from '../types/component'; | |||
import { | |||
ComponentMeasure, | |||
Dict, | |||
@@ -31,7 +36,6 @@ import { | |||
Paging, | |||
SourceLine, | |||
SourceViewerFile, | |||
Visibility, | |||
} from '../types/types'; | |||
export interface BaseSearchProjectsParameters { |
@@ -1,191 +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 { chunk, cloneDeep } from 'lodash'; | |||
import { | |||
mockPermissionTemplate, | |||
mockTemplateGroup, | |||
mockTemplateUser, | |||
} from '../../helpers/testMocks'; | |||
import { PermissionTemplate } from '../../types/types'; | |||
import { BaseSearchProjectsParameters } from '../components'; | |||
import { | |||
addProjectCreatorToTemplate, | |||
bulkApplyTemplate, | |||
getPermissionTemplateGroups, | |||
getPermissionTemplates, | |||
getPermissionTemplateUsers, | |||
grantTemplatePermissionToGroup, | |||
grantTemplatePermissionToUser, | |||
removeProjectCreatorFromTemplate, | |||
revokeTemplatePermissionFromGroup, | |||
revokeTemplatePermissionFromUser, | |||
} 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({ | |||
login: 'gooduser1', | |||
name: 'John', | |||
permissions: ['issueadmin', 'securityhotspotadmin', 'user'], | |||
}), | |||
mockTemplateUser({ | |||
login: 'gooduser2', | |||
name: 'Alexa', | |||
permissions: ['issueadmin', 'user'], | |||
}), | |||
mockTemplateUser({ | |||
name: 'Siri', | |||
login: 'gooduser3', | |||
}), | |||
mockTemplateUser({ | |||
login: 'gooduser4', | |||
name: 'Cool', | |||
permissions: ['user'], | |||
}), | |||
mockTemplateUser({ | |||
name: 'White', | |||
login: 'baduser1', | |||
}), | |||
mockTemplateUser({ | |||
name: 'Green', | |||
login: 'baduser2', | |||
}), | |||
]; | |||
const templateGroups = [ | |||
mockTemplateGroup(), | |||
mockTemplateGroup({ id: 'admins', name: 'admins', permissions: [] }), | |||
]; | |||
const PAGE_SIZE = 5; | |||
const MIN_QUERY_LENGTH = 3; | |||
const DEFAULT_PAGE = 1; | |||
jest.mock('../permissions'); | |||
export default class PermissionTemplateServiceMock { | |||
permissionTemplates: PermissionTemplate[] = []; | |||
isAllowedPermissionChange = true; | |||
constructor() { | |||
this.permissionTemplates = cloneDeep(defaultPermissionTemplates); | |||
(getPermissionTemplates as jest.Mock).mockImplementation(this.handleGetPermissionTemplates); | |||
(bulkApplyTemplate as jest.Mock).mockImplementation(this.handleBulkApplyTemplate); | |||
(getPermissionTemplateUsers as jest.Mock).mockImplementation( | |||
this.handleGetPermissionTemplateUsers | |||
); | |||
(getPermissionTemplateGroups as jest.Mock).mockImplementation( | |||
this.handleGetPermissionTemplateGroups | |||
); | |||
(addProjectCreatorToTemplate as jest.Mock).mockImplementation(this.handlePermissionChange); | |||
(removeProjectCreatorFromTemplate as jest.Mock).mockImplementation(this.handlePermissionChange); | |||
(grantTemplatePermissionToGroup as jest.Mock).mockImplementation(this.handlePermissionChange); | |||
(revokeTemplatePermissionFromGroup as jest.Mock).mockImplementation( | |||
this.handlePermissionChange | |||
); | |||
(grantTemplatePermissionToUser as jest.Mock).mockImplementation(this.handlePermissionChange); | |||
(revokeTemplatePermissionFromUser as jest.Mock).mockImplementation(this.handlePermissionChange); | |||
} | |||
handleGetPermissionTemplates = () => { | |||
return this.reply({ permissionTemplates: this.permissionTemplates }); | |||
}; | |||
handleBulkApplyTemplate = (params: BaseSearchProjectsParameters) => { | |||
if ( | |||
params.projects && | |||
params.projects.split(',').length > MAX_PROJECTS_TO_APPLY_PERMISSION_TEMPLATE | |||
) { | |||
const response = new Response( | |||
JSON.stringify({ errors: [{ msg: 'bulk apply permission template error message' }] }) | |||
); | |||
return Promise.reject(response); | |||
} | |||
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] ?? [], | |||
}); | |||
}; | |||
handleGetPermissionTemplateGroups = (data: { | |||
templateId: string; | |||
q?: string | null; | |||
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] ?? [], | |||
}); | |||
}; | |||
handlePermissionChange = () => { | |||
return this.isAllowedPermissionChange ? Promise.resolve() : Promise.reject(); | |||
}; | |||
updatePermissionChangeAllowance = (val: boolean) => { | |||
this.isAllowedPermissionChange = val; | |||
}; | |||
reset = () => { | |||
this.permissionTemplates = cloneDeep(defaultPermissionTemplates); | |||
this.updatePermissionChangeAllowance(true); | |||
}; | |||
reply<T>(response: T): Promise<T> { | |||
return Promise.resolve(cloneDeep(response)); | |||
} | |||
} |
@@ -0,0 +1,389 @@ | |||
/* | |||
* 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 { chunk, cloneDeep, remove, uniq } from 'lodash'; | |||
import { | |||
mockPermission, | |||
mockPermissionGroup, | |||
mockPermissionTemplate, | |||
mockPermissionUser, | |||
mockTemplateGroup, | |||
mockTemplateUser, | |||
} from '../../helpers/mocks/permissions'; | |||
import { ComponentQualifier, Visibility } from '../../types/component'; | |||
import { Permissions } from '../../types/permissions'; | |||
import { Permission, PermissionGroup, PermissionTemplate, PermissionUser } from '../../types/types'; | |||
import { BaseSearchProjectsParameters } from '../components'; | |||
import { | |||
addProjectCreatorToTemplate, | |||
applyTemplateToProject, | |||
bulkApplyTemplate, | |||
changeProjectVisibility, | |||
getPermissionsGroupsForComponent, | |||
getPermissionsUsersForComponent, | |||
getPermissionTemplateGroups, | |||
getPermissionTemplates, | |||
getPermissionTemplateUsers, | |||
grantPermissionToGroup, | |||
grantPermissionToUser, | |||
grantTemplatePermissionToGroup, | |||
grantTemplatePermissionToUser, | |||
removeProjectCreatorFromTemplate, | |||
revokePermissionFromGroup, | |||
revokePermissionFromUser, | |||
revokeTemplatePermissionFromGroup, | |||
revokeTemplatePermissionFromUser, | |||
} 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({ | |||
login: 'gooduser1', | |||
name: 'John', | |||
permissions: ['issueadmin', 'securityhotspotadmin', 'user'], | |||
}), | |||
mockTemplateUser({ | |||
login: 'gooduser2', | |||
name: 'Alexa', | |||
permissions: ['issueadmin', 'user'], | |||
}), | |||
mockTemplateUser({ | |||
name: 'Siri', | |||
login: 'gooduser3', | |||
}), | |||
mockTemplateUser({ | |||
login: 'gooduser4', | |||
name: 'Cool', | |||
permissions: ['user'], | |||
}), | |||
mockTemplateUser({ | |||
name: 'White', | |||
login: 'baduser1', | |||
}), | |||
mockTemplateUser({ | |||
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({ | |||
name: 'sonar-admins', | |||
permissions: [Permissions.Admin, Permissions.Browse], | |||
}), | |||
mockPermissionGroup({ name: 'sonar-losers', permissions: [] }), | |||
]; | |||
const PAGE_SIZE = 5; | |||
const MIN_QUERY_LENGTH = 3; | |||
const DEFAULT_PAGE = 1; | |||
jest.mock('../permissions'); | |||
export default class PermissionsServiceMock { | |||
permissionTemplates: PermissionTemplate[] = []; | |||
permissions: Permission[]; | |||
defaultTemplates: Array<{ templateId: string; qualifier: string }>; | |||
groups: PermissionGroup[]; | |||
users: PermissionUser[]; | |||
isAllowedPermissionChange = 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.groups = cloneDeep(defaultGroups); | |||
this.users = cloneDeep(defaultUsers); | |||
jest.mocked(getPermissionTemplates).mockImplementation(this.handleGetPermissionTemplates); | |||
jest.mocked(bulkApplyTemplate).mockImplementation(this.handleBulkApplyTemplate); | |||
jest.mocked(applyTemplateToProject).mockImplementation(this.handleApplyTemplateToProject); | |||
jest | |||
.mocked(getPermissionTemplateUsers) | |||
.mockImplementation(this.handleGetPermissionTemplateUsers); | |||
jest | |||
.mocked(getPermissionTemplateGroups) | |||
.mockImplementation(this.handleGetPermissionTemplateGroups); | |||
jest.mocked(addProjectCreatorToTemplate).mockImplementation(this.handlePermissionChange); | |||
jest.mocked(removeProjectCreatorFromTemplate).mockImplementation(this.handlePermissionChange); | |||
jest.mocked(grantTemplatePermissionToGroup).mockImplementation(this.handlePermissionChange); | |||
jest.mocked(revokeTemplatePermissionFromGroup).mockImplementation(this.handlePermissionChange); | |||
jest.mocked(grantTemplatePermissionToUser).mockImplementation(this.handlePermissionChange); | |||
jest.mocked(revokeTemplatePermissionFromUser).mockImplementation(this.handlePermissionChange); | |||
jest.mocked(changeProjectVisibility).mockImplementation(this.handleChangeProjectVisibility); | |||
jest | |||
.mocked(getPermissionsGroupsForComponent) | |||
.mockImplementation(this.handleGetPermissionGroupsForComponent); | |||
jest | |||
.mocked(getPermissionsUsersForComponent) | |||
.mockImplementation(this.handleGetPermissionUsersForComponent); | |||
jest.mocked(grantPermissionToGroup).mockImplementation(this.handleGrantPermissionToGroup); | |||
jest.mocked(revokePermissionFromGroup).mockImplementation(this.handleRevokePermissionFromGroup); | |||
jest.mocked(grantPermissionToUser).mockImplementation(this.handleGrantPermissionToUser); | |||
jest.mocked(revokePermissionFromUser).mockImplementation(this.handleRevokePermissionFromUser); | |||
} | |||
handleGetPermissionTemplates = () => { | |||
return this.reply({ | |||
permissionTemplates: this.permissionTemplates, | |||
defaultTemplates: this.defaultTemplates, | |||
permissions: this.permissions, | |||
}); | |||
}; | |||
handleApplyTemplateToProject = (_data: { projectKey: string; templateId: string }) => { | |||
return this.reply(undefined); | |||
}; | |||
handleBulkApplyTemplate = (params: BaseSearchProjectsParameters) => { | |||
if ( | |||
params.projects && | |||
params.projects.split(',').length > MAX_PROJECTS_TO_APPLY_PERMISSION_TEMPLATE | |||
) { | |||
const response = new Response( | |||
JSON.stringify({ errors: [{ msg: 'bulk apply permission template error message' }] }) | |||
); | |||
return Promise.reject(response); | |||
} | |||
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] ?? [], | |||
}); | |||
}; | |||
handleGetPermissionTemplateGroups = (data: { | |||
templateId: string; | |||
q?: string | null; | |||
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] ?? [], | |||
}); | |||
}; | |||
handleChangeProjectVisibility = (_project: string, _visibility: Visibility) => { | |||
return this.reply(undefined); | |||
}; | |||
handleGetPermissionGroupsForComponent = (data: { | |||
projectKey: string; | |||
q?: string; | |||
permission?: string; | |||
p?: number; | |||
ps?: number; | |||
}) => { | |||
const { ps = PAGE_SIZE, p = DEFAULT_PAGE, q, permission } = data; | |||
const groups = | |||
q && q.length >= MIN_QUERY_LENGTH | |||
? this.groups.filter((group) => group.name.toLowerCase().includes(q.toLowerCase())) | |||
: this.groups; | |||
const groupsChunked = chunk( | |||
permission ? groups.filter((g) => g.permissions.includes(permission)) : groups, | |||
ps | |||
); | |||
return this.reply({ | |||
paging: { pageSize: ps, total: groups.length, pageIndex: p }, | |||
groups: groupsChunked[p - 1] ?? [], | |||
}); | |||
}; | |||
handleGetPermissionUsersForComponent = (data: { | |||
projectKey: string; | |||
q?: string; | |||
permission?: string; | |||
p?: number; | |||
ps?: number; | |||
}) => { | |||
const { ps = PAGE_SIZE, p = DEFAULT_PAGE, q, permission } = data; | |||
const users = | |||
q && q.length >= MIN_QUERY_LENGTH | |||
? this.users.filter((user) => user.name.toLowerCase().includes(q.toLowerCase())) | |||
: this.users; | |||
const usersChunked = chunk( | |||
permission ? users.filter((u) => u.permissions.includes(permission)) : users, | |||
ps | |||
); | |||
return this.reply({ | |||
paging: { pageSize: ps, total: users.length, pageIndex: p }, | |||
users: usersChunked[p - 1] ?? [], | |||
}); | |||
}; | |||
handleGrantPermissionToGroup = (data: { | |||
projectKey?: string; | |||
groupName: string; | |||
permission: string; | |||
}) => { | |||
if (!this.isAllowedPermissionChange) { | |||
return Promise.reject(); | |||
} | |||
const { groupName, permission } = data; | |||
const group = this.groups.find((g) => g.name === groupName); | |||
if (group === undefined) { | |||
throw new Error(`Could not find group with name ${groupName}`); | |||
} | |||
group.permissions = uniq([...group.permissions, permission]); | |||
return this.reply(undefined); | |||
}; | |||
handleRevokePermissionFromGroup = (data: { | |||
projectKey?: string; | |||
groupName: string; | |||
permission: string; | |||
}) => { | |||
if (!this.isAllowedPermissionChange) { | |||
return Promise.reject(); | |||
} | |||
const { groupName, permission } = data; | |||
const group = this.groups.find((g) => g.name === groupName); | |||
if (group === undefined) { | |||
throw new Error(`Could not find group with name ${groupName}`); | |||
} | |||
group.permissions = remove(group.permissions, permission); | |||
return this.reply(undefined); | |||
}; | |||
handleGrantPermissionToUser = (data: { | |||
projectKey?: string; | |||
login: string; | |||
permission: string; | |||
}) => { | |||
if (!this.isAllowedPermissionChange) { | |||
return Promise.reject(); | |||
} | |||
const { login, permission } = data; | |||
const user = this.users.find((u) => u.login === login); | |||
if (user === undefined) { | |||
throw new Error(`Could not find user with login ${login}`); | |||
} | |||
user.permissions = uniq([...user.permissions, permission]); | |||
return this.reply(undefined); | |||
}; | |||
handleRevokePermissionFromUser = (data: { | |||
projectKey?: string; | |||
login: string; | |||
permission: string; | |||
}) => { | |||
if (!this.isAllowedPermissionChange) { | |||
return Promise.reject(); | |||
} | |||
const { login, permission } = data; | |||
const user = this.users.find((u) => u.login === login); | |||
if (user === undefined) { | |||
throw new Error(`Could not find user with name ${login}`); | |||
} | |||
user.permissions = remove(user.permissions, permission); | |||
return this.reply(undefined); | |||
}; | |||
handlePermissionChange = () => { | |||
return this.isAllowedPermissionChange ? Promise.resolve() : Promise.reject(); | |||
}; | |||
updatePermissionChangeAllowance = (val: boolean) => { | |||
this.isAllowedPermissionChange = val; | |||
}; | |||
setGroups = (groups: PermissionGroup[]) => { | |||
this.groups = groups; | |||
}; | |||
setUsers = (users: PermissionUser[]) => { | |||
this.users = users; | |||
}; | |||
reset = () => { | |||
this.permissionTemplates = cloneDeep(defaultPermissionTemplates); | |||
this.groups = cloneDeep(defaultGroups); | |||
this.users = cloneDeep(defaultUsers); | |||
this.updatePermissionChangeAllowance(true); | |||
}; | |||
reply<T>(response: T): Promise<T> { | |||
return Promise.resolve(cloneDeep(response)); | |||
} | |||
} |
@@ -19,13 +19,13 @@ | |||
*/ | |||
import { throwGlobalError } from '../helpers/error'; | |||
import { getJSON, post, postJSON, RequestData } from '../helpers/request'; | |||
import { Visibility } from '../types/component'; | |||
import { | |||
Paging, | |||
Permission, | |||
PermissionGroup, | |||
PermissionTemplate, | |||
PermissionUser, | |||
Visibility, | |||
} from '../types/types'; | |||
import { BaseSearchProjectsParameters } from './components'; | |||
@@ -93,7 +93,7 @@ export function setDefaultPermissionTemplate(templateId: string, qualifier: stri | |||
return post('/api/permissions/set_default_template', { templateId, qualifier }); | |||
} | |||
export function applyTemplateToProject(data: RequestData) { | |||
export function applyTemplateToProject(data: { projectKey: string; templateId: string }) { | |||
return post('/api/permissions/apply_template', data).catch(throwGlobalError); | |||
} | |||
@@ -32,7 +32,7 @@ import { HttpStatus } from '../../../helpers/request'; | |||
import { mockLocation, mockRouter } from '../../../helpers/testMocks'; | |||
import { waitAndUpdate } from '../../../helpers/testUtils'; | |||
import { AlmKeys } from '../../../types/alm-settings'; | |||
import { ComponentQualifier } from '../../../types/component'; | |||
import { ComponentQualifier, Visibility } from '../../../types/component'; | |||
import { TaskStatuses, TaskTypes } from '../../../types/tasks'; | |||
import { Component } from '../../../types/types'; | |||
import handleRequiredAuthorization from '../../utils/handleRequiredAuthorization'; | |||
@@ -98,12 +98,18 @@ it('changes component', () => { | |||
const wrapper = shallowRender(); | |||
wrapper.setState({ | |||
branchLikes: [mockMainBranch()], | |||
component: { qualifier: 'TRK', visibility: 'public' } as Component, | |||
component: { | |||
qualifier: ComponentQualifier.Project, | |||
visibility: Visibility.Public, | |||
} as Component, | |||
loading: false, | |||
}); | |||
wrapper.instance().handleComponentChange({ visibility: 'private' }); | |||
expect(wrapper.state().component).toEqual({ qualifier: 'TRK', visibility: 'private' }); | |||
wrapper.instance().handleComponentChange({ visibility: Visibility.Private }); | |||
expect(wrapper.state().component).toEqual({ | |||
qualifier: ComponentQualifier.Project, | |||
visibility: Visibility.Private, | |||
}); | |||
}); | |||
it('loads the project binding, if any', async () => { | |||
@@ -151,7 +157,7 @@ it('updates branches on change', async () => { | |||
wrapper.setState({ | |||
branchLikes: [mockMainBranch()], | |||
component: mockComponent({ | |||
breadcrumbs: [{ key: 'projectKey', name: 'project', qualifier: 'TRK' }], | |||
breadcrumbs: [{ key: 'projectKey', name: 'project', qualifier: ComponentQualifier.Project }], | |||
}), | |||
loading: false, | |||
}); |
@@ -22,7 +22,7 @@ import * as React from 'react'; | |||
import { mockComponent } from '../../../../../../helpers/mocks/component'; | |||
import { mockCurrentUser, mockLoggedInUser, mockMetric } from '../../../../../../helpers/testMocks'; | |||
import { waitAndUpdate } from '../../../../../../helpers/testUtils'; | |||
import { ComponentQualifier } from '../../../../../../types/component'; | |||
import { ComponentQualifier, Visibility } from '../../../../../../types/component'; | |||
import ProjectBadges from '../badges/ProjectBadges'; | |||
import { ProjectInformation } from '../ProjectInformation'; | |||
import { ProjectInformationPages } from '../ProjectInformationPages'; | |||
@@ -37,9 +37,9 @@ jest.mock('../../../../../../api/measures', () => { | |||
it('should render correctly', async () => { | |||
expect(shallowRender()).toMatchSnapshot('default'); | |||
expect(shallowRender({ currentUser: mockLoggedInUser() })).toMatchSnapshot('logged in user'); | |||
expect(shallowRender({ component: mockComponent({ visibility: 'private' }) })).toMatchSnapshot( | |||
'private' | |||
); | |||
expect( | |||
shallowRender({ component: mockComponent({ visibility: Visibility.Private }) }) | |||
).toMatchSnapshot('private'); | |||
const wrapper = shallowRender(); | |||
await waitAndUpdate(wrapper); | |||
expect(wrapper).toMatchSnapshot('measures loaded'); |
@@ -20,6 +20,7 @@ | |||
import { shallow } from 'enzyme'; | |||
import * as React from 'react'; | |||
import { mockComponent } from '../../../../../../helpers/mocks/component'; | |||
import { ComponentQualifier, Visibility } from '../../../../../../types/component'; | |||
import { | |||
ProjectInformationRenderer, | |||
ProjectInformationRendererProps, | |||
@@ -43,7 +44,9 @@ it('should render correctly', () => { | |||
}); | |||
it('should render a private project correctly', () => { | |||
expect(shallowRender({ component: mockComponent({ visibility: 'private' }) })).toMatchSnapshot(); | |||
expect( | |||
shallowRender({ component: mockComponent({ visibility: Visibility.Private }) }) | |||
).toMatchSnapshot(); | |||
}); | |||
it('should render an app correctly', () => { | |||
@@ -87,7 +90,10 @@ function shallowRender(props: Partial<ProjectInformationRendererProps> = {}) { | |||
hasFeature={jest.fn().mockReturnValue(true)} | |||
canConfigureNotifications={true} | |||
canUseBadges={true} | |||
component={mockComponent({ qualifier: 'TRK', visibility: 'public' })} | |||
component={mockComponent({ | |||
qualifier: ComponentQualifier.Project, | |||
visibility: Visibility.Public, | |||
})} | |||
onComponentChange={jest.fn()} | |||
onPageChange={jest.fn()} | |||
{...props} |
@@ -20,12 +20,14 @@ | |||
import userEvent from '@testing-library/user-event'; | |||
import React from 'react'; | |||
import { byRole } from 'testing-library-selector'; | |||
import PermissionTemplateServiceMock from '../../../../api/mocks/PermissionTemplateServiceMock'; | |||
import PermissionsServiceMock from '../../../../api/mocks/PermissionsServiceMock'; | |||
import { mockAppState } from '../../../../helpers/testMocks'; | |||
import { renderApp } from '../../../../helpers/testReactTestingUtils'; | |||
import { ComponentQualifier } from '../../../../types/component'; | |||
import { Permissions } from '../../../../types/permissions'; | |||
import PermissionTemplatesApp from '../PermissionTemplatesApp'; | |||
const serviceMock = new PermissionTemplateServiceMock(); | |||
const serviceMock = new PermissionsServiceMock(); | |||
beforeEach(() => { | |||
serviceMock.reset(); | |||
@@ -33,37 +35,11 @@ beforeEach(() => { | |||
const ui = { | |||
templateLink1: byRole('link', { name: 'Permission Template 1' }), | |||
adminUserBrowseCheckboxChecked: byRole('checkbox', { | |||
name: `checked permission 'projects_role.user' for user 'Admin Admin'`, | |||
}), | |||
adminUserBrowseCheckboxUnchecked: byRole('checkbox', { | |||
name: `unchecked permission 'projects_role.user' for user 'Admin Admin'`, | |||
}), | |||
adminUserAdministerCheckboxChecked: byRole('checkbox', { | |||
name: `checked permission 'projects_role.admin' for user 'Admin Admin'`, | |||
}), | |||
adminUserAdministerCheckboxUnchecked: byRole('checkbox', { | |||
name: `unchecked permission 'projects_role.admin' for user 'Admin Admin'`, | |||
}), | |||
anyoneGroupBrowseCheckboxChecked: byRole('checkbox', { | |||
name: `checked permission 'projects_role.user' for group 'Anyone'`, | |||
}), | |||
anyoneGroupBrowseCheckboxUnchecked: byRole('checkbox', { | |||
name: `unchecked permission 'projects_role.user' for group 'Anyone'`, | |||
}), | |||
anyoneGroupCodeviewCheckboxChecked: byRole('checkbox', { | |||
name: `checked permission 'projects_role.codeviewer' for group 'Anyone'`, | |||
}), | |||
anyoneGroupCodeviewCheckboxUnchecked: byRole('checkbox', { | |||
name: `unchecked permission 'projects_role.codeviewer' for group 'Anyone'`, | |||
}), | |||
permissionCheckbox: (target: string, permission: Permissions) => | |||
byRole('checkbox', { | |||
name: `permission.assign_x_to_y.projects_role.${permission}.${target}`, | |||
}), | |||
showMoreButton: byRole('button', { name: 'show_more' }), | |||
whiteUserBrowseCheckbox: byRole('checkbox', { | |||
name: `unchecked permission 'projects_role.user' for user 'White'`, | |||
}), | |||
}; | |||
it('grants/revokes permission from users or groups', async () => { | |||
@@ -73,36 +49,33 @@ it('grants/revokes permission from users or groups', async () => { | |||
await user.click(await ui.templateLink1.find()); | |||
// User | |||
expect(ui.adminUserBrowseCheckboxUnchecked.get()).not.toBeChecked(); | |||
await user.click(ui.adminUserBrowseCheckboxUnchecked.get()); | |||
expect(ui.adminUserBrowseCheckboxChecked.get()).toBeChecked(); | |||
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(); | |||
expect(ui.adminUserAdministerCheckboxChecked.get()).toBeChecked(); | |||
await user.click(ui.adminUserAdministerCheckboxChecked.get()); | |||
expect(ui.adminUserAdministerCheckboxUnchecked.get()).not.toBeChecked(); | |||
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(); | |||
// Group | |||
expect(ui.anyoneGroupBrowseCheckboxUnchecked.get()).not.toBeChecked(); | |||
await user.click(ui.anyoneGroupBrowseCheckboxUnchecked.get()); | |||
expect(ui.anyoneGroupBrowseCheckboxChecked.get()).toBeChecked(); | |||
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.anyoneGroupCodeviewCheckboxChecked.get()).toBeChecked(); | |||
await user.click(ui.anyoneGroupCodeviewCheckboxChecked.get()); | |||
expect(ui.anyoneGroupCodeviewCheckboxUnchecked.get()).not.toBeChecked(); | |||
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(); | |||
// Handles error on permission change | |||
serviceMock.updatePermissionChangeAllowance(false); | |||
await user.click(ui.adminUserBrowseCheckboxChecked.get()); | |||
expect(ui.adminUserBrowseCheckboxChecked.get()).toBeChecked(); | |||
await user.click(ui.anyoneGroupCodeviewCheckboxUnchecked.get()); | |||
expect(ui.anyoneGroupCodeviewCheckboxUnchecked.get()).not.toBeChecked(); | |||
await user.click(ui.permissionCheckbox('Admin Admin', Permissions.Browse).get()); | |||
expect(ui.permissionCheckbox('Admin Admin', Permissions.Browse).get()).toBeChecked(); | |||
await user.click(ui.adminUserBrowseCheckboxChecked.get()); | |||
expect(ui.adminUserBrowseCheckboxChecked.get()).toBeChecked(); | |||
await user.click(ui.permissionCheckbox('Anyone', Permissions.CodeViewer).get()); | |||
expect(ui.permissionCheckbox('Anyone', Permissions.CodeViewer).get()).not.toBeChecked(); | |||
await user.click(ui.adminUserAdministerCheckboxUnchecked.get()); | |||
expect(ui.adminUserAdministerCheckboxUnchecked.get()).not.toBeChecked(); | |||
await user.click(ui.permissionCheckbox('Admin Admin', Permissions.Admin).get()); | |||
expect(ui.permissionCheckbox('Admin Admin', Permissions.Admin).get()).not.toBeChecked(); | |||
}); | |||
it('loads more items on Show More', async () => { | |||
@@ -111,13 +84,13 @@ it('loads more items on Show More', async () => { | |||
await user.click(await ui.templateLink1.find()); | |||
expect(ui.whiteUserBrowseCheckbox.query()).not.toBeInTheDocument(); | |||
expect(ui.permissionCheckbox('White', Permissions.Browse).query()).not.toBeInTheDocument(); | |||
await user.click(ui.showMoreButton.get()); | |||
expect(ui.whiteUserBrowseCheckbox.get()).toBeInTheDocument(); | |||
expect(ui.permissionCheckbox('White', Permissions.Browse).get()).toBeInTheDocument(); | |||
}); | |||
function renderPermissionTemplatesApp() { | |||
renderApp('admin/permission_templates', <PermissionTemplatesApp />, { | |||
appState: mockAppState({ qualifiers: ['TRK'] }), | |||
appState: mockAppState({ qualifiers: [ComponentQualifier.Project] }), | |||
}); | |||
} |
@@ -83,8 +83,6 @@ export default class ApplyTemplate extends React.PureComponent<Props, State> { | |||
this.setState({ done: true }); | |||
} | |||
}); | |||
} else { | |||
return Promise.reject(undefined); | |||
} | |||
}; | |||
@@ -94,7 +92,7 @@ export default class ApplyTemplate extends React.PureComponent<Props, State> { | |||
render() { | |||
const header = translateWithParameters( | |||
'projects_role.apply_template_to_xxx', | |||
'projects_role.apply_template_to_x', | |||
this.props.project.name | |||
); | |||
@@ -19,8 +19,9 @@ | |||
*/ | |||
import * as React from 'react'; | |||
import { Button } from '../../../../components/controls/buttons'; | |||
import DeferredSpinner from '../../../../components/ui/DeferredSpinner'; | |||
import { translate } from '../../../../helpers/l10n'; | |||
import { isApplication, isPortfolioLike } from '../../../../types/component'; | |||
import { ComponentQualifier, isApplication, isPortfolioLike } from '../../../../types/component'; | |||
import { Component } from '../../../../types/types'; | |||
import ApplyTemplate from './ApplyTemplate'; | |||
@@ -70,7 +71,7 @@ export default class PageHeader extends React.PureComponent<Props, State> { | |||
} | |||
const visibilityDescription = | |||
component.qualifier === 'TRK' && component.visibility | |||
component.qualifier === ComponentQualifier.Project && component.visibility | |||
? translate('visibility', component.visibility, 'description', component.qualifier) | |||
: undefined; | |||
@@ -78,7 +79,7 @@ export default class PageHeader extends React.PureComponent<Props, State> { | |||
<header className="page-header"> | |||
<h1 className="page-title">{translate('permissions.page')}</h1> | |||
{this.props.loading && <i className="spinner" />} | |||
<DeferredSpinner loading={this.props.loading} /> | |||
{canApplyPermissionTemplate && ( | |||
<div className="page-actions"> |
@@ -24,6 +24,8 @@ import * as api from '../../../../api/permissions'; | |||
import withComponentContext from '../../../../app/components/componentContext/withComponentContext'; | |||
import VisibilitySelector from '../../../../components/common/VisibilitySelector'; | |||
import { translate } from '../../../../helpers/l10n'; | |||
import { Visibility } from '../../../../types/component'; | |||
import { Permissions } from '../../../../types/permissions'; | |||
import { Component, Paging, PermissionGroup, PermissionUser } from '../../../../types/types'; | |||
import AllHoldersList from '../../shared/components/AllHoldersList'; | |||
import { FilterOption } from '../../shared/components/SearchForm'; | |||
@@ -49,7 +51,7 @@ interface State { | |||
usersPaging?: Paging; | |||
} | |||
export class App extends React.PureComponent<Props, State> { | |||
export class PermissionsProjectApp extends React.PureComponent<Props, State> { | |||
mounted = false; | |||
constructor(props: Props) { | |||
@@ -73,12 +75,6 @@ export class App extends React.PureComponent<Props, State> { | |||
this.mounted = false; | |||
} | |||
stopLoading = () => { | |||
if (this.mounted) { | |||
this.setState({ loading: false }); | |||
} | |||
}; | |||
loadUsersAndGroups = (userPage?: number, groupsPage?: number) => { | |||
const { component } = this.props; | |||
const { filter, query, selectedPermission } = this.state; | |||
@@ -198,102 +194,90 @@ export class App extends React.PureComponent<Props, State> { | |||
grantPermissionToGroup = (group: string, permission: string) => { | |||
if (this.mounted) { | |||
this.setState({ | |||
loading: true, | |||
groups: this.addPermissionToGroup(group, permission), | |||
}); | |||
this.setState({ loading: true }); | |||
return api | |||
.grantPermissionToGroup({ | |||
projectKey: this.props.component.key, | |||
groupName: group, | |||
permission, | |||
}) | |||
.then(this.stopLoading, () => { | |||
.then(() => { | |||
if (this.mounted) { | |||
this.setState({ | |||
loading: false, | |||
groups: this.removePermissionFromGroup(group, permission), | |||
groups: this.addPermissionToGroup(group, permission), | |||
}); | |||
} | |||
}); | |||
}, this.stopLoading); | |||
} | |||
return Promise.resolve(); | |||
}; | |||
grantPermissionToUser = (user: string, permission: string) => { | |||
if (this.mounted) { | |||
this.setState({ | |||
loading: true, | |||
users: this.addPermissionToUser(user, permission), | |||
}); | |||
this.setState({ loading: true }); | |||
return api | |||
.grantPermissionToUser({ | |||
projectKey: this.props.component.key, | |||
login: user, | |||
permission, | |||
}) | |||
.then(this.stopLoading, () => { | |||
.then(() => { | |||
if (this.mounted) { | |||
this.setState({ | |||
loading: false, | |||
users: this.removePermissionFromUser(user, permission), | |||
users: this.addPermissionToUser(user, permission), | |||
}); | |||
} | |||
}); | |||
}, this.stopLoading); | |||
} | |||
return Promise.resolve(); | |||
}; | |||
revokePermissionFromGroup = (group: string, permission: string) => { | |||
if (this.mounted) { | |||
this.setState({ | |||
loading: true, | |||
groups: this.removePermissionFromGroup(group, permission), | |||
}); | |||
this.setState({ loading: true }); | |||
return api | |||
.revokePermissionFromGroup({ | |||
projectKey: this.props.component.key, | |||
groupName: group, | |||
permission, | |||
}) | |||
.then(this.stopLoading, () => { | |||
.then(() => { | |||
if (this.mounted) { | |||
this.setState({ | |||
loading: false, | |||
groups: this.addPermissionToGroup(group, permission), | |||
groups: this.removePermissionFromGroup(group, permission), | |||
}); | |||
} | |||
}); | |||
}, this.stopLoading); | |||
} | |||
return Promise.resolve(); | |||
}; | |||
revokePermissionFromUser = (user: string, permission: string) => { | |||
if (this.mounted) { | |||
this.setState({ | |||
loading: true, | |||
users: this.removePermissionFromUser(user, permission), | |||
}); | |||
this.setState({ loading: true }); | |||
return api | |||
.revokePermissionFromUser({ | |||
projectKey: this.props.component.key, | |||
login: user, | |||
permission, | |||
}) | |||
.then(this.stopLoading, () => { | |||
.then(() => { | |||
if (this.mounted) { | |||
this.setState({ | |||
loading: false, | |||
users: this.addPermissionToUser(user, permission), | |||
users: this.removePermissionFromUser(user, permission), | |||
}); | |||
} | |||
}); | |||
}, this.stopLoading); | |||
} | |||
return Promise.resolve(); | |||
}; | |||
handleVisibilityChange = (visibility: string) => { | |||
if (visibility === 'public') { | |||
if (visibility === Visibility.Public) { | |||
this.openDisclaimer(); | |||
} else { | |||
this.turnProjectToPrivate(); | |||
@@ -301,31 +285,19 @@ export class App extends React.PureComponent<Props, State> { | |||
}; | |||
turnProjectToPublic = () => { | |||
this.props.onComponentChange({ visibility: 'public' }); | |||
api.changeProjectVisibility(this.props.component.key, 'public').then( | |||
() => { | |||
this.loadHolders(); | |||
}, | |||
() => { | |||
this.props.onComponentChange({ | |||
visibility: 'private', | |||
}); | |||
} | |||
); | |||
this.setState({ loading: true }); | |||
return api.changeProjectVisibility(this.props.component.key, Visibility.Public).then(() => { | |||
this.props.onComponentChange({ visibility: Visibility.Public }); | |||
this.loadHolders(); | |||
}); | |||
}; | |||
turnProjectToPrivate = () => { | |||
this.props.onComponentChange({ visibility: 'private' }); | |||
api.changeProjectVisibility(this.props.component.key, 'private').then( | |||
() => { | |||
this.loadHolders(); | |||
}, | |||
() => { | |||
this.props.onComponentChange({ | |||
visibility: 'public', | |||
}); | |||
} | |||
); | |||
this.setState({ loading: true }); | |||
return api.changeProjectVisibility(this.props.component.key, Visibility.Private).then(() => { | |||
this.props.onComponentChange({ visibility: Visibility.Private }); | |||
this.loadHolders(); | |||
}); | |||
}; | |||
openDisclaimer = () => { | |||
@@ -340,6 +312,12 @@ export class App extends React.PureComponent<Props, State> { | |||
} | |||
}; | |||
stopLoading = () => { | |||
if (this.mounted) { | |||
this.setState({ loading: false }); | |||
} | |||
}; | |||
render() { | |||
const { component } = this.props; | |||
const { | |||
@@ -357,8 +335,8 @@ export class App extends React.PureComponent<Props, State> { | |||
component.configuration && component.configuration.canUpdateProjectVisibilityToPrivate; | |||
let order = PERMISSIONS_ORDER_BY_QUALIFIER[component.qualifier]; | |||
if (component.visibility === 'public') { | |||
order = without(order, 'user', 'codeviewer'); | |||
if (component.visibility === Visibility.Public) { | |||
order = without(order, Permissions.Browse, Permissions.CodeViewer); | |||
} | |||
const permissions = convertToPermissionDefinitions(order, 'projects_role'); | |||
@@ -372,6 +350,7 @@ export class App extends React.PureComponent<Props, State> { | |||
canTurnToPrivate={canTurnToPrivate} | |||
className="big-spacer-top big-spacer-bottom" | |||
onChange={this.handleVisibilityChange} | |||
loading={loading} | |||
visibility={component.visibility} | |||
/> | |||
{disclaimer && ( | |||
@@ -405,4 +384,4 @@ export class App extends React.PureComponent<Props, State> { | |||
} | |||
} | |||
export default withComponentContext(App); | |||
export default withComponentContext(PermissionsProjectApp); |
@@ -1,157 +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 { | |||
grantPermissionToGroup, | |||
grantPermissionToUser, | |||
revokePermissionFromGroup, | |||
revokePermissionFromUser, | |||
} from '../../../../../api/permissions'; | |||
import { mockComponent } from '../../../../../helpers/mocks/component'; | |||
import { waitAndUpdate } from '../../../../../helpers/testUtils'; | |||
import { App } from '../App'; | |||
jest.mock('../../../../../api/permissions', () => ({ | |||
getPermissionsGroupsForComponent: jest.fn().mockResolvedValue({ | |||
paging: { pageIndex: 1, pageSize: 100, total: 2 }, | |||
groups: [ | |||
{ | |||
id: '1', | |||
name: 'SonarSource', | |||
description: 'SonarSource team', | |||
permissions: ['admin', 'codeviewer', 'issueadmin'], | |||
}, | |||
{ name: 'Anyone', permissions: [] }, | |||
], | |||
}), | |||
getPermissionsUsersForComponent: jest.fn().mockResolvedValue({ | |||
paging: { pageIndex: 1, pageSize: 100, total: 3 }, | |||
users: [ | |||
{ | |||
avatar: 'admin-avatar', | |||
email: 'admin@gmail.com', | |||
login: 'admin', | |||
name: 'Admin Admin', | |||
permissions: ['admin'], | |||
}, | |||
{ | |||
avatar: 'user-avatar-1', | |||
email: 'user1@gmail.com', | |||
login: 'user1', | |||
name: 'User Number 1', | |||
permissions: [], | |||
}, | |||
{ | |||
avatar: 'user-avatar-2', | |||
email: 'user2@gmail.com', | |||
login: 'user2', | |||
name: 'User Number 2', | |||
permissions: [], | |||
}, | |||
], | |||
}), | |||
grantPermissionToGroup: jest.fn().mockResolvedValue({}), | |||
grantPermissionToUser: jest.fn().mockResolvedValue({}), | |||
revokePermissionFromGroup: jest.fn().mockResolvedValue({}), | |||
revokePermissionFromUser: jest.fn().mockResolvedValue({}), | |||
})); | |||
beforeEach(() => { | |||
jest.clearAllMocks(); | |||
}); | |||
it('should render correctly', async () => { | |||
const wrapper = shallowRender(); | |||
expect(wrapper).toMatchSnapshot(); | |||
await waitAndUpdate(wrapper); | |||
expect(wrapper).toMatchSnapshot(); | |||
}); | |||
describe('should manage state correctly', () => { | |||
it('should handle permission select', async () => { | |||
const wrapper = shallowRender(); | |||
await waitAndUpdate(wrapper); | |||
const instance = wrapper.instance(); | |||
instance.handlePermissionSelect('foo'); | |||
expect(wrapper.state('selectedPermission')).toBe('foo'); | |||
instance.handlePermissionSelect('foo'); | |||
expect(wrapper.state('selectedPermission')).toBeUndefined(); | |||
}); | |||
it('should add and remove permission to a group', async () => { | |||
const wrapper = shallowRender(); | |||
await waitAndUpdate(wrapper); | |||
const instance = wrapper.instance(); | |||
const apiPayload = { | |||
projectKey: 'my-project', | |||
groupName: 'SonarSource', | |||
permission: 'foo', | |||
}; | |||
instance.grantPermissionToGroup('SonarSource', 'foo'); | |||
const groupState = wrapper.state('groups'); | |||
expect(groupState[0].permissions).toHaveLength(4); | |||
expect(groupState[0].permissions).toContain('foo'); | |||
await waitAndUpdate(wrapper); | |||
expect(grantPermissionToGroup).toHaveBeenCalledWith(apiPayload); | |||
expect(wrapper.state('groups')).toBe(groupState); | |||
(grantPermissionToGroup as jest.Mock).mockRejectedValueOnce({}); | |||
instance.grantPermissionToGroup('SonarSource', 'bar'); | |||
expect(wrapper.state('groups')[0].permissions).toHaveLength(5); | |||
expect(wrapper.state('groups')[0].permissions).toContain('bar'); | |||
await waitAndUpdate(wrapper); | |||
expect(wrapper.state('groups')[0].permissions).toHaveLength(4); | |||
expect(wrapper.state('groups')[0].permissions).not.toContain('bar'); | |||
instance.revokePermissionFromGroup('SonarSource', 'foo'); | |||
expect(wrapper.state('groups')[0].permissions).toHaveLength(3); | |||
expect(wrapper.state('groups')[0].permissions).not.toContain('foo'); | |||
await waitAndUpdate(wrapper); | |||
expect(revokePermissionFromGroup).toHaveBeenCalledWith(apiPayload); | |||
}); | |||
it('should add and remove permission to a user', async () => { | |||
const wrapper = shallowRender(); | |||
await waitAndUpdate(wrapper); | |||
const instance = wrapper.instance(); | |||
const apiPayload = { | |||
projectKey: 'my-project', | |||
login: 'user1', | |||
permission: 'foo', | |||
}; | |||
instance.grantPermissionToUser('user1', 'foo'); | |||
expect(wrapper.state('users')[1].permissions).toHaveLength(1); | |||
expect(wrapper.state('users')[1].permissions).toContain('foo'); | |||
await waitAndUpdate(wrapper); | |||
expect(grantPermissionToUser).toHaveBeenCalledWith(apiPayload); | |||
instance.revokePermissionFromUser('user1', 'foo'); | |||
expect(wrapper.state('users')[1].permissions).toHaveLength(0); | |||
await waitAndUpdate(wrapper); | |||
expect(revokePermissionFromUser).toHaveBeenCalledWith(apiPayload); | |||
}); | |||
}); | |||
function shallowRender(props: Partial<App['props']> = {}) { | |||
return shallow<App>(<App component={mockComponent()} onComponentChange={jest.fn()} {...props} />); | |||
} |
@@ -1,53 +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 { waitAndUpdate } from '../../../../../helpers/testUtils'; | |||
import ApplyTemplate from '../ApplyTemplate'; | |||
jest.mock('../../../../../api/permissions', () => ({ | |||
getPermissionTemplates: jest.fn().mockResolvedValue({ | |||
permissionTemplates: [ | |||
{ | |||
id: 'tmp1', | |||
name: 'SonarSource projects', | |||
createdAt: '2015-11-27T15:20:32+0100', | |||
permissions: [ | |||
{ key: 'admin', usersCount: 0, groupsCount: 3 }, | |||
{ key: 'codeviewer', usersCount: 0, groupsCount: 6 }, | |||
], | |||
}, | |||
], | |||
defaultTemplates: [{ templateId: 'tmp1', qualifier: 'TRK' }], | |||
permissions: [ | |||
{ key: 'admin', name: 'Administer', description: 'Administer access' }, | |||
{ key: 'codeviewer', name: 'See Source Code', description: 'View code' }, | |||
], | |||
}), | |||
})); | |||
it('render correctly', async () => { | |||
const wrapper = shallow( | |||
<ApplyTemplate onClose={jest.fn()} project={{ key: 'foo', name: 'Foo' }} /> | |||
); | |||
expect(wrapper).toMatchSnapshot(); | |||
await waitAndUpdate(wrapper); | |||
expect(wrapper.dive()).toMatchSnapshot(); | |||
}); |
@@ -0,0 +1,287 @@ | |||
/* | |||
* 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 { act, screen, waitFor } from '@testing-library/react'; | |||
import userEvent from '@testing-library/user-event'; | |||
import { UserEvent } from '@testing-library/user-event/dist/types/setup/setup'; | |||
import * as React from 'react'; | |||
import selectEvent from 'react-select-event'; | |||
import { byLabelText, byRole, byText } from 'testing-library-selector'; | |||
import PermissionsServiceMock from '../../../../../api/mocks/PermissionsServiceMock'; | |||
import { mockComponent } from '../../../../../helpers/mocks/component'; | |||
import { renderApp } from '../../../../../helpers/testReactTestingUtils'; | |||
import { ComponentQualifier, Visibility } from '../../../../../types/component'; | |||
import { Permissions } from '../../../../../types/permissions'; | |||
import { Component } from '../../../../../types/types'; | |||
import { PERMISSIONS_ORDER_FOR_PROJECT_TEMPLATE, PERMISSIONS_ORDER_FOR_VIEW } from '../../../utils'; | |||
import { PermissionsProjectApp } from '../PermissionsProjectApp'; | |||
let serviceMock: PermissionsServiceMock; | |||
beforeAll(() => { | |||
serviceMock = new PermissionsServiceMock(); | |||
}); | |||
afterEach(() => { | |||
serviceMock.reset(); | |||
}); | |||
describe('rendering', () => { | |||
it.each([ | |||
[ComponentQualifier.Project, 'roles.page.description2', PERMISSIONS_ORDER_FOR_PROJECT_TEMPLATE], | |||
[ComponentQualifier.Portfolio, 'roles.page.description_portfolio', PERMISSIONS_ORDER_FOR_VIEW], | |||
[ | |||
ComponentQualifier.Application, | |||
'roles.page.description_application', | |||
PERMISSIONS_ORDER_FOR_VIEW, | |||
], | |||
])('should render correctly for %s', async (qualifier, description, permissions) => { | |||
const user = userEvent.setup(); | |||
const ui = getPageObject(user); | |||
renderPermissionsProjectApp({ qualifier, visibility: Visibility.Private }); | |||
await ui.appLoaded(); | |||
expect(screen.getByText(description)).toBeInTheDocument(); | |||
permissions.forEach((permission) => { | |||
expect(ui.permissionCheckbox('johndoe', permission).get()).toBeInTheDocument(); | |||
}); | |||
}); | |||
}); | |||
describe('filtering', () => { | |||
it('should allow to filter permission holders', async () => { | |||
const user = userEvent.setup(); | |||
const ui = getPageObject(user); | |||
renderPermissionsProjectApp(); | |||
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); | |||
renderPermissionsProjectApp(); | |||
await ui.appLoaded(); | |||
expect(screen.getAllByRole('row').length).toBe(7); | |||
await ui.toggleFilterByPermission(Permissions.Admin); | |||
expect(screen.getAllByRole('row').length).toBe(2); | |||
}); | |||
}); | |||
describe('assigning/revoking permissions', () => { | |||
it('should allow to apply a permission template', async () => { | |||
const user = userEvent.setup(); | |||
const ui = getPageObject(user); | |||
renderPermissionsProjectApp(); | |||
await ui.appLoaded(); | |||
await ui.openTemplateModal(); | |||
expect(ui.confirmApplyTemplateBtn.get()).toBeDisabled(); | |||
await ui.chooseTemplate('Permission Template 2'); | |||
expect(ui.templateSuccessfullyApplied.get()).toBeInTheDocument(); | |||
await ui.closeTemplateModal(); | |||
expect(ui.templateSuccessfullyApplied.query()).not.toBeInTheDocument(); | |||
}); | |||
it('should allow to turn a public project private (and vice-versa)', async () => { | |||
const user = userEvent.setup(); | |||
const ui = getPageObject(user); | |||
renderPermissionsProjectApp(); | |||
await ui.appLoaded(); | |||
expect(ui.visibilityRadio(Visibility.Public).get()).toBeChecked(); | |||
expect( | |||
ui.permissionCheckbox('sonar-users', Permissions.Browse).query() | |||
).not.toBeInTheDocument(); | |||
await act(async () => { | |||
await ui.turnProjectPrivate(); | |||
}); | |||
expect(ui.visibilityRadio(Visibility.Private).get()).toBeChecked(); | |||
expect(ui.permissionCheckbox('sonar-users', Permissions.Browse).get()).toBeInTheDocument(); | |||
await ui.turnProjectPublic(); | |||
expect(ui.makePublicDisclaimer.get()).toBeInTheDocument(); | |||
await act(async () => { | |||
await ui.confirmTurnProjectPublic(); | |||
}); | |||
expect(ui.visibilityRadio(Visibility.Public).get()).toBeChecked(); | |||
}); | |||
it('should add and remove permissions to/from a group', async () => { | |||
const user = userEvent.setup(); | |||
const ui = getPageObject(user); | |||
renderPermissionsProjectApp(); | |||
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); | |||
renderPermissionsProjectApp(); | |||
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(); | |||
}); | |||
}); | |||
function getPageObject(user: UserEvent) { | |||
const ui = { | |||
loading: byLabelText('loading'), | |||
permissionCheckbox: (target: string, permission: Permissions) => | |||
byRole('checkbox', { | |||
name: `permission.assign_x_to_y.projects_role.${permission}.${target}`, | |||
}), | |||
visibilityRadio: (visibility: Visibility) => | |||
byRole('radio', { name: `visibility.${visibility}` }), | |||
makePublicDisclaimer: byText( | |||
'projects_role.are_you_sure_to_turn_project_to_public.warning.TRK' | |||
), | |||
confirmPublicBtn: byRole('button', { name: 'projects_role.turn_project_to_public.TRK' }), | |||
openModalBtn: byRole('button', { name: 'projects_role.apply_template' }), | |||
closeModalBtn: byRole('button', { name: 'close' }), | |||
templateSelect: byRole('combobox', { name: /template/ }), | |||
templateSuccessfullyApplied: byText('projects_role.apply_template.success'), | |||
confirmApplyTemplateBtn: byRole('button', { name: 'apply' }), | |||
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' }), | |||
}; | |||
return { | |||
...ui, | |||
async appLoaded() { | |||
await waitFor(() => { | |||
expect(ui.loading.query()).not.toBeInTheDocument(); | |||
}); | |||
}, | |||
async togglePermission(target: string, permission: Permissions) { | |||
await user.click(ui.permissionCheckbox(target, permission).get()); | |||
}, | |||
async turnProjectPrivate() { | |||
await user.click(ui.visibilityRadio(Visibility.Private).get()); | |||
}, | |||
async turnProjectPublic() { | |||
await user.click(ui.visibilityRadio(Visibility.Public).get()); | |||
}, | |||
async confirmTurnProjectPublic() { | |||
await user.click(ui.confirmPublicBtn.get()); | |||
}, | |||
async openTemplateModal() { | |||
await user.click(ui.openModalBtn.get()); | |||
}, | |||
async closeTemplateModal() { | |||
await user.click(ui.closeModalBtn.get()); | |||
}, | |||
async chooseTemplate(name: string) { | |||
await selectEvent.select(ui.templateSelect.get(), [name]); | |||
await user.click(ui.confirmApplyTemplateBtn.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()); | |||
}, | |||
}; | |||
} | |||
function renderPermissionsProjectApp(override?: Partial<Component>) { | |||
function App({ component }: { component: Component }) { | |||
const [realComponent, setRealComponent] = React.useState(component); | |||
return ( | |||
<PermissionsProjectApp | |||
component={realComponent} | |||
onComponentChange={(changes: Partial<Component>) => { | |||
setRealComponent({ ...realComponent, ...changes }); | |||
}} | |||
/> | |||
); | |||
} | |||
return renderApp( | |||
'/', | |||
<App | |||
component={mockComponent({ | |||
visibility: Visibility.Public, | |||
configuration: { | |||
canUpdateProjectVisibilityToPrivate: true, | |||
canApplyPermissionTemplate: true, | |||
}, | |||
...override, | |||
})} | |||
/> | |||
); | |||
} |
@@ -1,246 +0,0 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`should render correctly 1`] = ` | |||
<div | |||
className="page page-limited" | |||
id="project-permissions-page" | |||
> | |||
<Helmet | |||
defer={false} | |||
encodeSpecialCharacters={true} | |||
prioritizeSeoTags={false} | |||
title="permissions.page" | |||
/> | |||
<PageHeader | |||
component={ | |||
{ | |||
"breadcrumbs": [], | |||
"key": "my-project", | |||
"name": "MyProject", | |||
"qualifier": "TRK", | |||
"qualityGate": { | |||
"isDefault": true, | |||
"key": "30", | |||
"name": "Sonar way", | |||
}, | |||
"qualityProfiles": [ | |||
{ | |||
"deleted": false, | |||
"key": "my-qp", | |||
"language": "ts", | |||
"name": "Sonar way", | |||
}, | |||
], | |||
"tags": [], | |||
} | |||
} | |||
loadHolders={[Function]} | |||
loading={true} | |||
/> | |||
<div> | |||
<VisibilitySelector | |||
className="big-spacer-top big-spacer-bottom" | |||
onChange={[Function]} | |||
/> | |||
</div> | |||
<AllHoldersList | |||
filter="all" | |||
grantPermissionToGroup={[Function]} | |||
grantPermissionToUser={[Function]} | |||
groups={[]} | |||
onFilter={[Function]} | |||
onLoadMore={[Function]} | |||
onQuery={[Function]} | |||
onSelectPermission={[Function]} | |||
permissions={ | |||
[ | |||
{ | |||
"description": "projects_role.user.desc", | |||
"key": "user", | |||
"name": "projects_role.user", | |||
}, | |||
{ | |||
"description": "projects_role.codeviewer.desc", | |||
"key": "codeviewer", | |||
"name": "projects_role.codeviewer", | |||
}, | |||
{ | |||
"description": "projects_role.issueadmin.desc", | |||
"key": "issueadmin", | |||
"name": "projects_role.issueadmin", | |||
}, | |||
{ | |||
"description": "projects_role.securityhotspotadmin.desc", | |||
"key": "securityhotspotadmin", | |||
"name": "projects_role.securityhotspotadmin", | |||
}, | |||
{ | |||
"description": "projects_role.admin.desc", | |||
"key": "admin", | |||
"name": "projects_role.admin", | |||
}, | |||
{ | |||
"description": "projects_role.scan.desc", | |||
"key": "scan", | |||
"name": "projects_role.scan", | |||
}, | |||
] | |||
} | |||
query="" | |||
revokePermissionFromGroup={[Function]} | |||
revokePermissionFromUser={[Function]} | |||
users={[]} | |||
/> | |||
</div> | |||
`; | |||
exports[`should render correctly 2`] = ` | |||
<div | |||
className="page page-limited" | |||
id="project-permissions-page" | |||
> | |||
<Helmet | |||
defer={false} | |||
encodeSpecialCharacters={true} | |||
prioritizeSeoTags={false} | |||
title="permissions.page" | |||
/> | |||
<PageHeader | |||
component={ | |||
{ | |||
"breadcrumbs": [], | |||
"key": "my-project", | |||
"name": "MyProject", | |||
"qualifier": "TRK", | |||
"qualityGate": { | |||
"isDefault": true, | |||
"key": "30", | |||
"name": "Sonar way", | |||
}, | |||
"qualityProfiles": [ | |||
{ | |||
"deleted": false, | |||
"key": "my-qp", | |||
"language": "ts", | |||
"name": "Sonar way", | |||
}, | |||
], | |||
"tags": [], | |||
} | |||
} | |||
loadHolders={[Function]} | |||
loading={false} | |||
/> | |||
<div> | |||
<VisibilitySelector | |||
className="big-spacer-top big-spacer-bottom" | |||
onChange={[Function]} | |||
/> | |||
</div> | |||
<AllHoldersList | |||
filter="all" | |||
grantPermissionToGroup={[Function]} | |||
grantPermissionToUser={[Function]} | |||
groups={ | |||
[ | |||
{ | |||
"description": "SonarSource team", | |||
"id": "1", | |||
"name": "SonarSource", | |||
"permissions": [ | |||
"admin", | |||
"codeviewer", | |||
"issueadmin", | |||
], | |||
}, | |||
{ | |||
"name": "Anyone", | |||
"permissions": [], | |||
}, | |||
] | |||
} | |||
groupsPaging={ | |||
{ | |||
"pageIndex": 1, | |||
"pageSize": 100, | |||
"total": 2, | |||
} | |||
} | |||
onFilter={[Function]} | |||
onLoadMore={[Function]} | |||
onQuery={[Function]} | |||
onSelectPermission={[Function]} | |||
permissions={ | |||
[ | |||
{ | |||
"description": "projects_role.user.desc", | |||
"key": "user", | |||
"name": "projects_role.user", | |||
}, | |||
{ | |||
"description": "projects_role.codeviewer.desc", | |||
"key": "codeviewer", | |||
"name": "projects_role.codeviewer", | |||
}, | |||
{ | |||
"description": "projects_role.issueadmin.desc", | |||
"key": "issueadmin", | |||
"name": "projects_role.issueadmin", | |||
}, | |||
{ | |||
"description": "projects_role.securityhotspotadmin.desc", | |||
"key": "securityhotspotadmin", | |||
"name": "projects_role.securityhotspotadmin", | |||
}, | |||
{ | |||
"description": "projects_role.admin.desc", | |||
"key": "admin", | |||
"name": "projects_role.admin", | |||
}, | |||
{ | |||
"description": "projects_role.scan.desc", | |||
"key": "scan", | |||
"name": "projects_role.scan", | |||
}, | |||
] | |||
} | |||
query="" | |||
revokePermissionFromGroup={[Function]} | |||
revokePermissionFromUser={[Function]} | |||
users={ | |||
[ | |||
{ | |||
"avatar": "admin-avatar", | |||
"email": "admin@gmail.com", | |||
"login": "admin", | |||
"name": "Admin Admin", | |||
"permissions": [ | |||
"admin", | |||
], | |||
}, | |||
{ | |||
"avatar": "user-avatar-1", | |||
"email": "user1@gmail.com", | |||
"login": "user1", | |||
"name": "User Number 1", | |||
"permissions": [], | |||
}, | |||
{ | |||
"avatar": "user-avatar-2", | |||
"email": "user2@gmail.com", | |||
"login": "user2", | |||
"name": "User Number 2", | |||
"permissions": [], | |||
}, | |||
] | |||
} | |||
usersPaging={ | |||
{ | |||
"pageIndex": 1, | |||
"pageSize": 100, | |||
"total": 3, | |||
} | |||
} | |||
/> | |||
</div> | |||
`; |
@@ -1,83 +0,0 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`render correctly 1`] = ` | |||
<SimpleModal | |||
header="projects_role.apply_template_to_xxx.Foo" | |||
onClose={[MockFunction]} | |||
onSubmit={[Function]} | |||
size="small" | |||
> | |||
<Component /> | |||
</SimpleModal> | |||
`; | |||
exports[`render correctly 2`] = ` | |||
<Modal | |||
contentLabel="projects_role.apply_template_to_xxx.Foo" | |||
onRequestClose={[MockFunction]} | |||
size="small" | |||
> | |||
<form | |||
id="project-permissions-apply-template-form" | |||
onSubmit={[Function]} | |||
> | |||
<header | |||
className="modal-head" | |||
> | |||
<h2> | |||
projects_role.apply_template_to_xxx.Foo | |||
</h2> | |||
</header> | |||
<div | |||
className="modal-body" | |||
> | |||
<MandatoryFieldsExplanation | |||
className="modal-field" | |||
/> | |||
<div | |||
className="modal-field" | |||
> | |||
<label | |||
htmlFor="project-permissions-template-input" | |||
> | |||
template | |||
<MandatoryFieldMarker /> | |||
</label> | |||
<Select | |||
className="Select" | |||
id="project-permissions-template" | |||
inputId="project-permissions-template-input" | |||
onChange={[Function]} | |||
options={ | |||
[ | |||
{ | |||
"label": "SonarSource projects", | |||
"value": "tmp1", | |||
}, | |||
] | |||
} | |||
value={[]} | |||
/> | |||
</div> | |||
</div> | |||
<footer | |||
className="modal-foot" | |||
> | |||
<DeferredSpinner | |||
className="spacer-right" | |||
loading={false} | |||
/> | |||
<SubmitButton | |||
disabled={true} | |||
> | |||
apply | |||
</SubmitButton> | |||
<ResetButtonLink | |||
onClick={[Function]} | |||
> | |||
cancel | |||
</ResetButtonLink> | |||
</footer> | |||
</form> | |||
</Modal> | |||
`; |
@@ -20,12 +20,12 @@ | |||
import React from 'react'; | |||
import { Route } from 'react-router-dom'; | |||
import GlobalPermissionsApp from './global/components/App'; | |||
import ProjectPermissionsApp from './project/components/App'; | |||
import PermissionsProjectApp from './project/components/PermissionsProjectApp'; | |||
export const globalPermissionsRoutes = () => ( | |||
<Route path="permissions" element={<GlobalPermissionsApp />} /> | |||
); | |||
export const projectPermissionsRoutes = () => ( | |||
<Route path="project_roles" element={<ProjectPermissionsApp />} /> | |||
<Route path="project_roles" element={<PermissionsProjectApp />} /> | |||
); |
@@ -99,7 +99,6 @@ export default class GroupHolder extends React.PureComponent<Props, State> { | |||
return ( | |||
<PermissionCell | |||
disabled={group.name === ANYONE && (isComponentPrivate || isAdminPermission)} | |||
isGroupItem={true} | |||
key={permissionKey} | |||
loading={this.state.loading} | |||
onCheck={this.handleCheck} |
@@ -42,7 +42,6 @@ interface Props { | |||
permissions: PermissionDefinitions; | |||
query?: string; | |||
selectedPermission?: string; | |||
showPublicProjectsWarning?: boolean; | |||
users: PermissionUser[]; | |||
} | |||
@@ -124,15 +123,7 @@ export default class HoldersList extends React.PureComponent<Props, State> { | |||
} | |||
render() { | |||
const { | |||
permissions, | |||
users, | |||
groups, | |||
loading, | |||
children, | |||
selectedPermission, | |||
showPublicProjectsWarning, | |||
} = this.props; | |||
const { permissions, users, groups, loading, children, selectedPermission } = this.props; | |||
const items = [...groups, ...users]; | |||
const [itemWithPermissions, itemWithoutPermissions] = partition(items, (item) => | |||
this.getItemInitialPermissionsCount(item) | |||
@@ -152,7 +143,6 @@ export default class HoldersList extends React.PureComponent<Props, State> { | |||
onSelectPermission={this.props.onSelectPermission} | |||
permission={permission} | |||
selectedPermission={selectedPermission} | |||
showPublicProjectsWarning={showPublicProjectsWarning} | |||
/> | |||
))} | |||
</tr> |
@@ -20,6 +20,7 @@ | |||
import classNames from 'classnames'; | |||
import * as React from 'react'; | |||
import Checkbox from '../../../../components/controls/Checkbox'; | |||
import { translateWithParameters } from '../../../../helpers/l10n'; | |||
import { | |||
PermissionDefinition, | |||
PermissionDefinitionGroup, | |||
@@ -28,9 +29,8 @@ import { | |||
} from '../../../../types/types'; | |||
import { isPermissionDefinitionGroup } from '../../utils'; | |||
interface Props { | |||
export interface PermissionCellProps { | |||
disabled?: boolean; | |||
isGroupItem?: boolean; | |||
loading: string[]; | |||
onCheck: (checked: boolean, permission?: string) => void; | |||
permission: PermissionDefinition | PermissionDefinitionGroup; | |||
@@ -38,72 +38,58 @@ interface Props { | |||
selectedPermission?: string; | |||
} | |||
export default class PermissionCell extends React.PureComponent<Props> { | |||
render() { | |||
const { | |||
disabled, | |||
isGroupItem, | |||
loading, | |||
onCheck, | |||
permission, | |||
permissionItem, | |||
selectedPermission, | |||
} = this.props; | |||
const tenant = `${isGroupItem ? 'group' : 'user'} '${permissionItem.name}'`; | |||
if (isPermissionDefinitionGroup(permission)) { | |||
return ( | |||
<td className="text-middle"> | |||
{permission.permissions.map((permissionDefinition) => { | |||
const isChecked = permissionItem.permissions.includes(permissionDefinition.key); | |||
const isDisabled = disabled || loading.includes(permissionDefinition.key); | |||
let state = isChecked ? 'checked' : 'unchecked'; | |||
if (isDisabled) { | |||
state = 'disabled'; | |||
} | |||
return ( | |||
<div key={permissionDefinition.key}> | |||
<Checkbox | |||
checked={isChecked} | |||
disabled={isDisabled} | |||
id={permissionDefinition.key} | |||
label={`${state} permission '${permissionDefinition.name}' for ${tenant}`} | |||
onCheck={onCheck} | |||
> | |||
<span className="little-spacer-left">{permissionDefinition.name}</span> | |||
</Checkbox> | |||
</div> | |||
); | |||
})} | |||
</td> | |||
); | |||
} | |||
const isChecked = permissionItem.permissions.includes(permission.key); | |||
const isDisabled = disabled || loading.includes(permission.key); | |||
let state = isChecked ? 'checked' : 'unchecked'; | |||
if (isDisabled) { | |||
state = 'disabled'; | |||
} | |||
export default function PermissionCell(props: PermissionCellProps) { | |||
const { disabled, loading, onCheck, permission, permissionItem, selectedPermission } = props; | |||
if (isPermissionDefinitionGroup(permission)) { | |||
return ( | |||
<td | |||
className={classNames('permission-column text-center text-middle', { | |||
selected: permission.key === selectedPermission, | |||
<td className="text-middle"> | |||
{permission.permissions.map((permissionDefinition) => { | |||
const isChecked = permissionItem.permissions.includes(permissionDefinition.key); | |||
const isDisabled = disabled || loading.includes(permissionDefinition.key); | |||
return ( | |||
<div key={permissionDefinition.key}> | |||
<Checkbox | |||
checked={isChecked} | |||
disabled={isDisabled} | |||
id={permissionDefinition.key} | |||
label={translateWithParameters( | |||
'permission.assign_x_to_y', | |||
permissionDefinition.name, | |||
permissionItem.name | |||
)} | |||
onCheck={onCheck} | |||
> | |||
<span className="little-spacer-left">{permissionDefinition.name}</span> | |||
</Checkbox> | |||
</div> | |||
); | |||
})} | |||
> | |||
<Checkbox | |||
checked={isChecked} | |||
disabled={isDisabled} | |||
id={permission.key} | |||
label={`${state} permission '${permission.name}' for ${tenant}`} | |||
onCheck={onCheck} | |||
/> | |||
</td> | |||
); | |||
} | |||
const isChecked = permissionItem.permissions.includes(permission.key); | |||
const isDisabled = disabled || loading.includes(permission.key); | |||
return ( | |||
<td | |||
className={classNames('permission-column text-center text-middle', { | |||
selected: permission.key === selectedPermission, | |||
})} | |||
> | |||
<Checkbox | |||
checked={isChecked} | |||
disabled={isDisabled} | |||
id={permission.key} | |||
label={translateWithParameters( | |||
'permission.assign_x_to_y', | |||
permission.name, | |||
permissionItem.name | |||
)} | |||
onCheck={onCheck} | |||
/> | |||
</td> | |||
); | |||
} |
@@ -22,7 +22,6 @@ import * as React from 'react'; | |||
import InstanceMessage from '../../../../components/common/InstanceMessage'; | |||
import HelpTooltip from '../../../../components/controls/HelpTooltip'; | |||
import Tooltip from '../../../../components/controls/Tooltip'; | |||
import { Alert } from '../../../../components/ui/Alert'; | |||
import { translate, translateWithParameters } from '../../../../helpers/l10n'; | |||
import { PermissionDefinition, PermissionDefinitionGroup } from '../../../../types/types'; | |||
import { isPermissionDefinitionGroup } from '../../utils'; | |||
@@ -31,7 +30,6 @@ interface Props { | |||
onSelectPermission?: (permission: string) => void; | |||
permission: PermissionDefinition | PermissionDefinitionGroup; | |||
selectedPermission?: string; | |||
showPublicProjectsWarning?: boolean; | |||
} | |||
export default class PermissionHeader extends React.PureComponent<Props> { | |||
@@ -55,19 +53,9 @@ export default class PermissionHeader extends React.PureComponent<Props> { | |||
<br /> | |||
</React.Fragment> | |||
)); | |||
} else { | |||
if (this.props.showPublicProjectsWarning && ['user', 'codeviewer'].includes(permission.key)) { | |||
return ( | |||
<div> | |||
<InstanceMessage message={permission.description} /> | |||
<Alert className="spacer-top" variant="warning"> | |||
{translate('projects_role.public_projects_warning')} | |||
</Alert> | |||
</div> | |||
); | |||
} | |||
return <InstanceMessage message={permission.description} />; | |||
} | |||
return <InstanceMessage message={permission.description} />; | |||
}; | |||
render() { |
@@ -18,34 +18,42 @@ | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { translate } from '../../helpers/l10n'; | |||
import { Permissions } from '../../types/permissions'; | |||
import { Dict, PermissionDefinition, PermissionDefinitionGroup } from '../../types/types'; | |||
export const PERMISSIONS_ORDER_FOR_PROJECT_TEMPLATE = [ | |||
'user', | |||
'codeviewer', | |||
'issueadmin', | |||
'securityhotspotadmin', | |||
'admin', | |||
'scan', | |||
Permissions.Browse, | |||
Permissions.CodeViewer, | |||
Permissions.IssueAdmin, | |||
Permissions.SecurityHotspotAdmin, | |||
Permissions.Admin, | |||
Permissions.Scan, | |||
]; | |||
export const PERMISSIONS_ORDER_GLOBAL = [ | |||
'admin', | |||
{ category: 'administer', permissions: ['gateadmin', 'profileadmin'] }, | |||
'scan', | |||
{ category: 'creator', permissions: ['provisioning', 'applicationcreator', 'portfoliocreator'] }, | |||
Permissions.Admin, | |||
{ | |||
category: 'administer', | |||
permissions: [Permissions.QualityGateAdmin, Permissions.QualityProfileAdmin], | |||
}, | |||
Permissions.Scan, | |||
{ | |||
category: 'creator', | |||
permissions: [ | |||
Permissions.ProjectCreation, | |||
Permissions.ApplicationCreation, | |||
Permissions.PortfolioCreation, | |||
], | |||
}, | |||
]; | |||
export const PERMISSIONS_ORDER_FOR_VIEW = ['user', 'admin']; | |||
export const PERMISSIONS_ORDER_FOR_DEV = ['user', 'admin']; | |||
export const PERMISSIONS_ORDER_FOR_VIEW = [Permissions.Browse, Permissions.Admin]; | |||
export const PERMISSIONS_ORDER_BY_QUALIFIER: Dict<string[]> = { | |||
TRK: PERMISSIONS_ORDER_FOR_PROJECT_TEMPLATE, | |||
VW: PERMISSIONS_ORDER_FOR_VIEW, | |||
SVW: PERMISSIONS_ORDER_FOR_VIEW, | |||
APP: PERMISSIONS_ORDER_FOR_VIEW, | |||
DEV: PERMISSIONS_ORDER_FOR_DEV, | |||
}; | |||
function convertToPermissionDefinition(permission: string, l10nPrefix: string) { | |||
@@ -60,7 +68,7 @@ function convertToPermissionDefinition(permission: string, l10nPrefix: string) { | |||
} | |||
export function filterPermissions( | |||
permissions: Array<string | { category: string; permissions: string[] }>, | |||
permissions: Array<Permissions | { category: string; permissions: Permissions[] }>, | |||
hasApplicationsEnabled: boolean, | |||
hasPortfoliosEnabled: boolean | |||
) { | |||
@@ -70,9 +78,9 @@ export function filterPermissions( | |||
...permission, | |||
permissions: permission.permissions.filter((p) => { | |||
return ( | |||
p === 'provisioning' || | |||
(p === 'portfoliocreator' && hasPortfoliosEnabled) || | |||
(p === 'applicationcreator' && hasApplicationsEnabled) | |||
p === Permissions.ProjectCreation || | |||
(p === Permissions.PortfolioCreation && hasPortfoliosEnabled) || | |||
(p === Permissions.ApplicationCreation && hasApplicationsEnabled) | |||
); | |||
}), | |||
}; |
@@ -21,7 +21,7 @@ import { shallow } from 'enzyme'; | |||
import * as React from 'react'; | |||
import { get, save } from '../../../../helpers/storage'; | |||
import { mockAppState, mockLocation } from '../../../../helpers/testMocks'; | |||
import { ComponentQualifier } from '../../../../types/component'; | |||
import { ComponentQualifier, Visibility } from '../../../../types/component'; | |||
import { AllProjects, LS_PROJECTS_SORT, LS_PROJECTS_VIEW } from '../AllProjects'; | |||
jest.mock( | |||
@@ -170,7 +170,7 @@ function shallowRender( | |||
name: 'Foo', | |||
qualifier: ComponentQualifier.Project, | |||
tags: [], | |||
visibility: 'public', | |||
visibility: Visibility.Public, | |||
}, | |||
], | |||
total: 0, |
@@ -22,7 +22,7 @@ import * as React from 'react'; | |||
import PrivacyBadgeContainer from '../../../../../components/common/PrivacyBadgeContainer'; | |||
import TagsList from '../../../../../components/tags/TagsList'; | |||
import { mockCurrentUser, mockLoggedInUser } from '../../../../../helpers/testMocks'; | |||
import { ComponentQualifier } from '../../../../../types/component'; | |||
import { ComponentQualifier, Visibility } from '../../../../../types/component'; | |||
import { CurrentUser } from '../../../../../types/users'; | |||
import { Project } from '../../../types'; | |||
import ProjectCard from '../ProjectCard'; | |||
@@ -42,7 +42,7 @@ const PROJECT: Project = { | |||
name: 'Foo', | |||
qualifier: ComponentQualifier.Project, | |||
tags: [], | |||
visibility: 'public', | |||
visibility: Visibility.Public, | |||
}; | |||
const USER_LOGGED_OUT = mockCurrentUser(); | |||
@@ -63,7 +63,7 @@ it('should display tags', () => { | |||
}); | |||
it('should display private badge', () => { | |||
const project: Project = { ...PROJECT, visibility: 'private' }; | |||
const project: Project = { ...PROJECT, visibility: Visibility.Private }; | |||
expect(shallowRender(project).find(PrivacyBadgeContainer).exists()).toBe(true); | |||
}); | |||
@@ -17,8 +17,8 @@ | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { ComponentQualifier } from '../../types/component'; | |||
import { Dict, Visibility } from '../../types/types'; | |||
import { ComponentQualifier, Visibility } from '../../types/component'; | |||
import { Dict } from '../../types/types'; | |||
export interface Project { | |||
analysisDate?: string; |
@@ -23,7 +23,7 @@ import Modal from '../../components/controls/Modal'; | |||
import Radio from '../../components/controls/Radio'; | |||
import { Alert } from '../../components/ui/Alert'; | |||
import { translate } from '../../helpers/l10n'; | |||
import { Visibility } from '../../types/types'; | |||
import { Visibility } from '../../types/component'; | |||
export interface Props { | |||
defaultVisibility: Visibility; | |||
@@ -58,7 +58,7 @@ export default class ChangeDefaultVisibilityForm extends React.PureComponent<Pro | |||
</header> | |||
<div className="modal-body"> | |||
{['public', 'private'].map((visibility) => ( | |||
{Object.values(Visibility).map((visibility) => ( | |||
<div className="big-spacer-bottom" key={visibility}> | |||
<Radio | |||
value={visibility} |
@@ -30,8 +30,8 @@ import MandatoryFieldMarker from '../../components/ui/MandatoryFieldMarker'; | |||
import MandatoryFieldsExplanation from '../../components/ui/MandatoryFieldsExplanation'; | |||
import { translate } from '../../helpers/l10n'; | |||
import { getProjectUrl } from '../../helpers/urls'; | |||
import { Visibility } from '../../types/component'; | |||
import { GlobalSettingKeys } from '../../types/settings'; | |||
import { Visibility } from '../../types/types'; | |||
interface Props { | |||
defaultProjectVisibility?: Visibility; |
@@ -20,7 +20,7 @@ | |||
import * as React from 'react'; | |||
import { Button, EditButton } from '../../components/controls/buttons'; | |||
import { translate } from '../../helpers/l10n'; | |||
import { Visibility } from '../../types/types'; | |||
import { Visibility } from '../../types/component'; | |||
import ChangeDefaultVisibilityForm from './ChangeDefaultVisibilityForm'; | |||
export interface Props { | |||
@@ -78,7 +78,7 @@ export default class Header extends React.PureComponent<Props, State> { | |||
{visibilityForm && ( | |||
<ChangeDefaultVisibilityForm | |||
defaultVisibility={defaultProjectVisibility || 'public'} | |||
defaultVisibility={defaultProjectVisibility || Visibility.Public} | |||
onClose={this.closeVisiblityForm} | |||
onConfirm={this.props.onChangeDefaultProjectVisibility} | |||
/> |
@@ -30,9 +30,9 @@ import { toShortNotSoISOString } from '../../helpers/dates'; | |||
import { throwGlobalError } from '../../helpers/error'; | |||
import { translate } from '../../helpers/l10n'; | |||
import { hasGlobalPermission } from '../../helpers/users'; | |||
import { Visibility } from '../../types/component'; | |||
import { Permissions } from '../../types/permissions'; | |||
import { SettingsKey } from '../../types/settings'; | |||
import { Visibility } from '../../types/types'; | |||
import { LoggedInUser } from '../../types/users'; | |||
import CreateProjectForm from './CreateProjectForm'; | |||
import Header from './Header'; |
@@ -31,7 +31,7 @@ import Select, { LabelValueSelectOption } from '../../components/controls/Select | |||
import QualifierIcon from '../../components/icons/QualifierIcon'; | |||
import { translate } from '../../helpers/l10n'; | |||
import { AppState } from '../../types/appstate'; | |||
import { Visibility } from '../../types/types'; | |||
import { Visibility } from '../../types/component'; | |||
import BulkApplyTemplateModal from './BulkApplyTemplateModal'; | |||
import DeleteModal from './DeleteModal'; | |||
@@ -172,8 +172,8 @@ export class Search extends React.PureComponent<Props, State> { | |||
renderVisibilityFilter = () => { | |||
const options = [ | |||
{ value: 'all', label: translate('visibility.both') }, | |||
{ value: 'public', label: translate('visibility.public') }, | |||
{ value: 'private', label: translate('visibility.private') }, | |||
{ value: Visibility.Public, label: translate('visibility.public') }, | |||
{ value: Visibility.Private, label: translate('visibility.private') }, | |||
]; | |||
return ( | |||
<td className="thin nowrap text-middle"> |
@@ -21,6 +21,7 @@ import { shallow } from 'enzyme'; | |||
import * as React from 'react'; | |||
import Radio from '../../../components/controls/Radio'; | |||
import { click } from '../../../helpers/testUtils'; | |||
import { Visibility } from '../../../types/component'; | |||
import ChangeDefaultVisibilityForm from '../ChangeDefaultVisibilityForm'; | |||
it('closes', () => { | |||
@@ -35,17 +36,17 @@ it('changes visibility', () => { | |||
const wrapper = shallowRender({ onConfirm }); | |||
expect(wrapper).toMatchSnapshot(); | |||
wrapper.find(Radio).first().props().onCheck('private'); | |||
wrapper.find(Radio).first().props().onCheck(Visibility.Private); | |||
expect(wrapper).toMatchSnapshot(); | |||
click(wrapper.find('.js-confirm')); | |||
expect(onConfirm).toHaveBeenCalledWith('private'); | |||
expect(onConfirm).toHaveBeenCalledWith(Visibility.Private); | |||
}); | |||
function shallowRender(props: Partial<ChangeDefaultVisibilityForm['props']> = {}) { | |||
return shallow( | |||
<ChangeDefaultVisibilityForm | |||
defaultVisibility="public" | |||
defaultVisibility={Visibility.Public} | |||
onClose={jest.fn()} | |||
onConfirm={jest.fn()} | |||
{...props} |
@@ -20,6 +20,7 @@ | |||
import { shallow } from 'enzyme'; | |||
import * as React from 'react'; | |||
import { click } from '../../../helpers/testUtils'; | |||
import { Visibility } from '../../../types/component'; | |||
import Header, { Props } from '../Header'; | |||
jest.mock('../../../helpers/system', () => ({ | |||
@@ -27,10 +28,8 @@ jest.mock('../../../helpers/system', () => ({ | |||
})); | |||
it('renders', () => { | |||
expect(shallowRender()).toMatchSnapshot('default'); | |||
expect(shallowRender({ defaultProjectVisibility: undefined })).toMatchSnapshot( | |||
'undefined visibility' | |||
); | |||
expect(shallowRender()).toMatchSnapshot('undefined visibility'); | |||
expect(shallowRender({ defaultProjectVisibility: Visibility.Public })).toMatchSnapshot('default'); | |||
}); | |||
it('creates project', () => { | |||
@@ -48,8 +47,8 @@ it('changes default visibility', () => { | |||
const modalWrapper = wrapper.find('ChangeDefaultVisibilityForm'); | |||
expect(modalWrapper).toMatchSnapshot(); | |||
modalWrapper.prop<Function>('onConfirm')('private'); | |||
expect(onChangeDefaultProjectVisibility).toHaveBeenCalledWith('private'); | |||
modalWrapper.prop<Function>('onConfirm')(Visibility.Private); | |||
expect(onChangeDefaultProjectVisibility).toHaveBeenCalledWith(Visibility.Private); | |||
modalWrapper.prop<Function>('onClose')(); | |||
wrapper.update(); | |||
@@ -59,7 +58,6 @@ it('changes default visibility', () => { | |||
function shallowRender(props?: { [P in keyof Props]?: Props[P] }) { | |||
return shallow( | |||
<Header | |||
defaultProjectVisibility="public" | |||
hasProvisionPermission={true} | |||
onChangeDefaultProjectVisibility={jest.fn()} | |||
onProjectCreate={jest.fn()} |
@@ -20,7 +20,7 @@ | |||
import { screen, within } from '@testing-library/react'; | |||
import userEvent from '@testing-library/user-event'; | |||
import { getComponents, SearchProjectsParameters } from '../../../api/components'; | |||
import PermissionTemplateServiceMock from '../../../api/mocks/PermissionTemplateServiceMock'; | |||
import PermissionsServiceMock from '../../../api/mocks/PermissionsServiceMock'; | |||
import { renderAppWithAdminContext } from '../../../helpers/testReactTestingUtils'; | |||
import { ComponentQualifier, Visibility } from '../../../types/component'; | |||
import routes from '../routes'; | |||
@@ -38,13 +38,13 @@ jest.mock('../../../api/settings', () => ({ | |||
const components = mockComponents(11); | |||
let permissionTemplateMock: PermissionTemplateServiceMock; | |||
let serviceMock: PermissionsServiceMock; | |||
beforeAll(() => { | |||
permissionTemplateMock = new PermissionTemplateServiceMock(); | |||
serviceMock = new PermissionsServiceMock(); | |||
}); | |||
afterEach(() => { | |||
permissionTemplateMock.reset(); | |||
serviceMock.reset(); | |||
}); | |||
describe('Bulk Apply', () => { |
@@ -24,6 +24,7 @@ import { changeProjectDefaultVisibility } from '../../../api/permissions'; | |||
import { getValue } from '../../../api/settings'; | |||
import { mockLoggedInUser } from '../../../helpers/testMocks'; | |||
import { waitAndUpdate } from '../../../helpers/testUtils'; | |||
import { ComponentQualifier, Visibility } from '../../../types/component'; | |||
import { ProjectManagementApp, Props } from '../ProjectManagementApp'; | |||
jest.mock('lodash', () => { | |||
@@ -60,9 +61,12 @@ beforeEach(() => { | |||
it('fetches all projects on mount', async () => { | |||
const wrapper = shallowRender(); | |||
await waitAndUpdate(wrapper); | |||
expect(getComponents).toHaveBeenLastCalledWith({ ...defaultSearchParameters, qualifiers: 'TRK' }); | |||
expect(getComponents).toHaveBeenLastCalledWith({ | |||
...defaultSearchParameters, | |||
qualifiers: ComponentQualifier.Project, | |||
}); | |||
expect(getValue).toHaveBeenCalled(); | |||
expect(wrapper.state().defaultProjectVisibility).toBe('public'); | |||
expect(wrapper.state().defaultProjectVisibility).toBe(Visibility.Public); | |||
}); | |||
it('selects provisioned', () => { | |||
@@ -109,12 +113,12 @@ it('should handle default project visibility change', async () => { | |||
await waitAndUpdate(wrapper); | |||
expect(wrapper.state().defaultProjectVisibility).toBe('public'); | |||
wrapper.instance().handleDefaultProjectVisibilityChange('private'); | |||
expect(wrapper.state().defaultProjectVisibility).toBe(Visibility.Public); | |||
wrapper.instance().handleDefaultProjectVisibilityChange(Visibility.Private); | |||
expect(changeProjectDefaultVisibility).toHaveBeenCalledWith('private'); | |||
expect(changeProjectDefaultVisibility).toHaveBeenCalledWith(Visibility.Private); | |||
await waitAndUpdate(wrapper); | |||
expect(wrapper.state().defaultProjectVisibility).toBe('private'); | |||
expect(wrapper.state().defaultProjectVisibility).toBe(Visibility.Private); | |||
}); | |||
it('loads more', () => { |
@@ -22,6 +22,7 @@ import * as React from 'react'; | |||
import { getComponentNavigation } from '../../../api/navigation'; | |||
import { mockLoggedInUser } from '../../../helpers/testMocks'; | |||
import { click, waitAndUpdate } from '../../../helpers/testUtils'; | |||
import { ComponentQualifier, Visibility } from '../../../types/component'; | |||
import ProjectRowActions, { Props } from '../ProjectRowActions'; | |||
jest.mock('../../../api/navigation', () => ({ | |||
@@ -123,8 +124,8 @@ function shallowRender(props: Partial<Props> = {}) { | |||
id: 'foo', | |||
key: 'foo', | |||
name: 'Foo', | |||
qualifier: 'TRK', | |||
visibility: 'private', | |||
qualifier: ComponentQualifier.Project, | |||
visibility: Visibility.Private, | |||
}} | |||
{...props} | |||
/> |
@@ -19,11 +19,12 @@ | |||
*/ | |||
import { shallow } from 'enzyme'; | |||
import * as React from 'react'; | |||
import { ComponentQualifier, Visibility } from '../../../types/component'; | |||
import Projects from '../Projects'; | |||
const projects = [ | |||
{ key: 'a', name: 'A', qualifier: 'TRK', visibility: 'public' }, | |||
{ key: 'b', name: 'B', qualifier: 'TRK', visibility: 'public' }, | |||
{ key: 'a', name: 'A', qualifier: ComponentQualifier.Project, visibility: Visibility.Public }, | |||
{ key: 'b', name: 'B', qualifier: ComponentQualifier.Project, visibility: Visibility.Public }, | |||
]; | |||
const selection = ['a']; | |||
@@ -21,7 +21,7 @@ import classNames from 'classnames'; | |||
import * as React from 'react'; | |||
import Tooltip from '../../components/controls/Tooltip'; | |||
import { translate } from '../../helpers/l10n'; | |||
import { Visibility } from '../../types/types'; | |||
import { Visibility } from '../../types/component'; | |||
interface PrivacyBadgeContainerProps { | |||
className?: string; | |||
@@ -34,7 +34,7 @@ export default function PrivacyBadgeContainer({ | |||
qualifier, | |||
visibility, | |||
}: PrivacyBadgeContainerProps) { | |||
if (visibility !== 'private') { | |||
if (visibility !== Visibility.Private) { | |||
return null; | |||
} | |||
@@ -21,38 +21,38 @@ import classNames from 'classnames'; | |||
import * as React from 'react'; | |||
import Radio from '../../components/controls/Radio'; | |||
import { translate } from '../../helpers/l10n'; | |||
import { Visibility } from '../../types/types'; | |||
import { Visibility } from '../../types/component'; | |||
interface Props { | |||
export interface VisibilitySelectorProps { | |||
canTurnToPrivate?: boolean; | |||
className?: string; | |||
onChange: (visibility: Visibility) => void; | |||
showDetails?: boolean; | |||
visibility?: Visibility; | |||
loading?: boolean; | |||
} | |||
export default class VisibilitySelector extends React.PureComponent<Props> { | |||
render() { | |||
return ( | |||
<div className={classNames(this.props.className)}> | |||
{['public', 'private'].map((visibility) => ( | |||
<Radio | |||
className={`huge-spacer-right visibility-${visibility}`} | |||
key={visibility} | |||
value={visibility} | |||
checked={this.props.visibility === visibility} | |||
onCheck={this.props.onChange} | |||
disabled={visibility === 'private' && !this.props.canTurnToPrivate} | |||
> | |||
<div> | |||
{translate('visibility', visibility)} | |||
{this.props.showDetails && ( | |||
<p className="note">{translate('visibility', visibility, 'description.long')}</p> | |||
)} | |||
</div> | |||
</Radio> | |||
))} | |||
</div> | |||
); | |||
} | |||
export default function VisibilitySelector(props: VisibilitySelectorProps) { | |||
const { className, canTurnToPrivate, visibility, showDetails, loading = false } = props; | |||
return ( | |||
<div className={classNames(className)}> | |||
{Object.values(Visibility).map((v) => ( | |||
<Radio | |||
className={`huge-spacer-right visibility-${v}`} | |||
key={v} | |||
value={v} | |||
checked={v === visibility} | |||
onCheck={props.onChange} | |||
disabled={(v === Visibility.Private && !canTurnToPrivate) || loading} | |||
> | |||
<div> | |||
{translate('visibility', v)} | |||
{showDetails && ( | |||
<p className="note">{translate('visibility', v, 'description.long')}</p> | |||
)} | |||
</div> | |||
</Radio> | |||
))} | |||
</div> | |||
); | |||
} |
@@ -19,7 +19,7 @@ | |||
*/ | |||
import { shallow } from 'enzyme'; | |||
import * as React from 'react'; | |||
import { ComponentQualifier } from '../../../types/component'; | |||
import { ComponentQualifier, Visibility } from '../../../types/component'; | |||
import PrivacyBadge from '../PrivacyBadgeContainer'; | |||
it('renders', () => { | |||
@@ -27,11 +27,15 @@ it('renders', () => { | |||
}); | |||
it('do not render', () => { | |||
expect(getWrapper({ visibility: 'public' })).toMatchSnapshot(); | |||
expect(getWrapper({ visibility: Visibility.Public })).toMatchSnapshot(); | |||
}); | |||
function getWrapper(props = {}) { | |||
return shallow( | |||
<PrivacyBadge qualifier={ComponentQualifier.Project} visibility="private" {...props} /> | |||
<PrivacyBadge | |||
qualifier={ComponentQualifier.Project} | |||
visibility={Visibility.Private} | |||
{...props} | |||
/> | |||
); | |||
} |
@@ -20,34 +20,35 @@ | |||
import { shallow } from 'enzyme'; | |||
import * as React from 'react'; | |||
import Radio from '../../../components/controls/Radio'; | |||
import VisibilitySelector from '../VisibilitySelector'; | |||
import { Visibility } from '../../../types/component'; | |||
import VisibilitySelector, { VisibilitySelectorProps } from '../VisibilitySelector'; | |||
it('changes visibility', () => { | |||
const onChange = jest.fn(); | |||
const wrapper = shallowRender({ onChange }); | |||
expect(wrapper).toMatchSnapshot(); | |||
wrapper.find(Radio).first().props().onCheck('private'); | |||
expect(onChange).toHaveBeenCalledWith('private'); | |||
wrapper.find(Radio).first().props().onCheck(Visibility.Private); | |||
expect(onChange).toHaveBeenCalledWith(Visibility.Private); | |||
wrapper.setProps({ visibility: 'private' }); | |||
wrapper.setProps({ visibility: Visibility.Private }); | |||
expect(wrapper).toMatchSnapshot(); | |||
wrapper.find(Radio).first().props().onCheck('public'); | |||
expect(onChange).toHaveBeenCalledWith('public'); | |||
wrapper.find(Radio).first().props().onCheck(Visibility.Public); | |||
expect(onChange).toHaveBeenCalledWith(Visibility.Public); | |||
}); | |||
it('renders disabled', () => { | |||
expect(shallowRender({ canTurnToPrivate: false })).toMatchSnapshot(); | |||
}); | |||
function shallowRender(props?: Partial<VisibilitySelector['props']>) { | |||
return shallow<VisibilitySelector>( | |||
function shallowRender(props?: Partial<VisibilitySelectorProps>) { | |||
return shallow<VisibilitySelectorProps>( | |||
<VisibilitySelector | |||
className="test-classname" | |||
canTurnToPrivate={true} | |||
onChange={jest.fn()} | |||
visibility="public" | |||
visibility={Visibility.Public} | |||
{...props} | |||
/> | |||
); |
@@ -17,7 +17,13 @@ | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { PermissionGroup, PermissionUser } from '../../types/types'; | |||
import { | |||
Permission, | |||
PermissionGroup, | |||
PermissionTemplate, | |||
PermissionTemplateGroup, | |||
PermissionUser, | |||
} from '../../types/types'; | |||
import { mockUser } from '../testMocks'; | |||
export function mockPermissionGroup(overrides: Partial<PermissionGroup> = {}): PermissionGroup { | |||
@@ -37,3 +43,52 @@ export function mockPermissionUser(overrides: Partial<PermissionUser> = {}): Per | |||
...overrides, | |||
}; | |||
} | |||
export function mockPermission(override: Partial<Permission> = {}) { | |||
return { | |||
key: 'admin', | |||
name: 'Admin', | |||
description: 'Can do anything he/she wants', | |||
...override, | |||
}; | |||
} | |||
export function mockPermissionTemplateGroup(override: Partial<PermissionTemplateGroup> = {}) { | |||
return { | |||
groupsCount: 1, | |||
usersCount: 1, | |||
key: 'admin', | |||
withProjectCreator: true, | |||
...override, | |||
}; | |||
} | |||
export function mockPermissionTemplate(override: Partial<PermissionTemplate> = {}) { | |||
return { | |||
id: 'template1', | |||
name: 'Permission Template 1', | |||
createdAt: '', | |||
defaultFor: [], | |||
permissions: [mockPermissionTemplateGroup()], | |||
...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, | |||
}; | |||
} |
@@ -18,7 +18,7 @@ | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { Project } from '../../apps/projects/types'; | |||
import { ComponentQualifier } from '../../types/component'; | |||
import { ComponentQualifier, Visibility } from '../../types/component'; | |||
export function mockProject(overrides: Partial<Project> = {}): Project { | |||
return { | |||
@@ -27,7 +27,7 @@ export function mockProject(overrides: Partial<Project> = {}): Project { | |||
measures: {}, | |||
qualifier: ComponentQualifier.Project, | |||
tags: [], | |||
visibility: 'public', | |||
visibility: Visibility.Public, | |||
...overrides, | |||
}; | |||
} |
@@ -42,11 +42,6 @@ import { | |||
Metric, | |||
Paging, | |||
Period, | |||
Permission, | |||
PermissionGroup, | |||
PermissionTemplate, | |||
PermissionTemplateGroup, | |||
PermissionUser, | |||
ProfileInheritanceDetails, | |||
Rule, | |||
RuleActivation, | |||
@@ -761,52 +756,3 @@ export function mockDumpStatus(props: Partial<DumpStatus> = {}): DumpStatus { | |||
export function mockRuleRepository(override: Partial<RuleRepository> = {}) { | |||
return { key: 'css', language: 'css', name: 'SonarQube', ...override }; | |||
} | |||
export function mockPermission(override: Partial<Permission> = {}) { | |||
return { | |||
key: 'admin', | |||
name: 'Admin', | |||
description: 'Can do anything he/she wants', | |||
...override, | |||
}; | |||
} | |||
export function mockPermissionTemplateGroup(override: Partial<PermissionTemplateGroup> = {}) { | |||
return { | |||
groupsCount: 1, | |||
usersCount: 1, | |||
key: 'admin', | |||
withProjectCreator: true, | |||
...override, | |||
}; | |||
} | |||
export function mockPermissionTemplate(override: Partial<PermissionTemplate> = {}) { | |||
return { | |||
id: 'template1', | |||
name: 'Permission Template 1', | |||
createdAt: '', | |||
defaultFor: [], | |||
permissions: [mockPermissionTemplateGroup()], | |||
...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, | |||
}; | |||
} |
@@ -19,8 +19,14 @@ | |||
*/ | |||
export enum Permissions { | |||
Admin = 'admin', | |||
Browse = 'user', | |||
ProjectCreation = 'provisioning', | |||
ApplicationCreation = 'applicationcreator', | |||
PortfolioCreation = 'portfoliocreator', | |||
QualityGateAdmin = 'gateadmin', | |||
QualityProfileAdmin = 'profileadmin', | |||
Scan = 'scan', | |||
CodeViewer = 'codeviewer', | |||
IssueAdmin = 'issueadmin', | |||
SecurityHotspotAdmin = 'securityhotspotadmin', | |||
} |
@@ -18,7 +18,7 @@ | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { RuleDescriptionSection } from '../apps/coding-rules/rule'; | |||
import { ComponentQualifier } from './component'; | |||
import { ComponentQualifier, Visibility } from './component'; | |||
import { MessageFormatting } from './issues'; | |||
import { UserActive, UserBase } from './users'; | |||
@@ -772,8 +772,6 @@ export interface UserSelected extends UserActive { | |||
selected: boolean; | |||
} | |||
export type Visibility = 'public' | 'private'; | |||
export namespace WebApi { | |||
export interface Action { | |||
key: string; |
@@ -2732,6 +2732,15 @@ metric.vulnerabilities.name=Vulnerabilities | |||
metric.wont_fix_issues.description=Won't fix issues | |||
metric.wont_fix_issues.name=Won't Fix Issues | |||
#------------------------------------------------------------------------------ | |||
# | |||
# PERMISSIONS | |||
# | |||
#------------------------------------------------------------------------------ | |||
permission.assign_x_to_y=Assign permission {0} to {1} | |||
#------------------------------------------------------------------------------ | |||
# | |||
# GLOBAL PERMISSIONS | |||
@@ -2783,7 +2792,7 @@ projects_role.scan=Execute Analysis | |||
projects_role.scan.desc=Ability to get all settings required to perform an analysis (including the secured settings like passwords) and to push analysis results to the {instance} server. | |||
projects_role.bulk_change=Bulk Change | |||
projects_role.apply_template=Apply Permission Template | |||
projects_role.apply_template_to_xxx=Apply Permission Template To "{0}" | |||
projects_role.apply_template_to_x=Apply Permission Template To "{0}" | |||
projects_role.apply_template.success=Permission template was successfully applied. | |||
projects_role.no_projects=There are currently no results to apply the permission template to. | |||
projects_role.turn_x_to_public=Turn "{0}" to Public | |||
@@ -2803,7 +2812,6 @@ projects_role.portfoliocreator=Create Portfolios | |||
projects_role.portfoliocreator.desc=Allow to create portfolios for non system administrator. | |||
#------------------------------------------------------------------------------ | |||
# | |||
# PERMISSION TEMPLATES | |||
@@ -2830,6 +2838,7 @@ permission_templates.bulk_apply_permission_template.apply_to_all=You're about to | |||
permission_templates.select_to_delete=You must select at least one item | |||
permission_templates.delete_selected=Delete all selected items | |||
#------------------------------------------------------------------------------ | |||
# | |||
# Promotion |