diff --git a/cypress/e2e/hub/namespaces.cy.ts b/cypress/e2e/hub/namespaces.cy.ts index 7ed02d779d..13b947ab87 100644 --- a/cypress/e2e/hub/namespaces.cy.ts +++ b/cypress/e2e/hub/namespaces.cy.ts @@ -73,16 +73,20 @@ describe('Namespaces', () => { cy.clickButton(/^Clear all filters$/); }); - it('verify user and team access for namespace', () => { + it('verify user and team access tabs are available for a namespace', () => { const nameSpaceName = `test_namespace_access_${randomString(5, undefined, { isLowercase: true, })}`; cy.createNamespace(nameSpaceName); cy.navigateTo('hub', Namespaces.url); - // cy.setTablePageSize('50'); + cy.get('[data-cy="table-view"]').click(); cy.clickTableRow(nameSpaceName); cy.verifyPageTitle(nameSpaceName); - cy.clickTab(/^Access$/, true); - cy.pause(); + cy.clickTab(/^User access$/, true); + cy.contains(/^There are currently no users added.$/); + cy.clickTab(/^Team access$/, true); + cy.contains(/^There are currently no teams added.$/); + // TODO: tests for adding/removing users/roles when those features are implemented + cy.deleteNamespace(nameSpaceName); }); }); diff --git a/cypress/fixtures/hub_namespace.json b/cypress/fixtures/hub_namespace.json new file mode 100644 index 0000000000..015c4891ab --- /dev/null +++ b/cypress/fixtures/hub_namespace.json @@ -0,0 +1,52 @@ +{ + "meta": { + "count": 1 + }, + "links": { + "first": "/api/automation-hub/_ui/v1/namespaces/?include_related=my_permissions&limit=1&name=test2&offset=0", + "previous": null, + "next": null, + "last": "/api/automation-hub/_ui/v1/namespaces/?include_related=my_permissions&limit=1&name=test2&offset=0" + }, + "data": [ + { + "pulp_href": "/api/automation-hub/pulp/api/v3/pulp_ansible/namespaces/2/", + "id": 2, + "name": "test2", + "company": "", + "email": "", + "avatar_url": "", + "description": "", + "groups": [ + { + "id": 1, + "name": "Test Group", + "object_roles": ["galaxy.collection_publisher"] + } + ], + "users": [ + { + "id": 2, + "name": "new-user", + "object_roles": ["galaxy.collection_namespace_owner", "galaxy.collection_publisher"] + }, + { + "id": 3, + "name": "obj-role-test-user", + "object_roles": ["galaxy.collection_namespace_owner"] + } + ], + "related_fields": { + "my_permissions": [ + "galaxy.add_namespace", + "galaxy.change_namespace", + "galaxy.delete_namespace", + "galaxy.upload_to_namespace", + "galaxy.view_namespace" + ] + }, + "metadata_sha256": "7e4efa6ba218f9d9a6394bba10d0bb6b452bbe24078bc4b8f62dfe727390dd42", + "avatar_sha256": null + } + ] +} diff --git a/cypress/fixtures/hub_namespace_permissions.json b/cypress/fixtures/hub_namespace_permissions.json new file mode 100644 index 0000000000..d28870335b --- /dev/null +++ b/cypress/fixtures/hub_namespace_permissions.json @@ -0,0 +1,15 @@ +{ + "count": 1, + "next": null, + "previous": null, + "results": [ + { + "pulp_href": "/api/automation-hub/pulp/api/v3/roles/018d1319-5a39-77cb-b368-00f504c0889e/", + "pulp_created": "2024-01-16T16:27:25.114238Z", + "name": "galaxy.collection_namespace_owner", + "description": null, + "permissions": ["galaxy.change_namespace", "galaxy.upload_to_namespace"], + "locked": true + } + ] +} diff --git a/frontend/hub/access/roles/components/RolePermissions.tsx b/frontend/hub/access/roles/components/RolePermissions.tsx index 5de8577a1e..15331a7aa4 100644 --- a/frontend/hub/access/roles/components/RolePermissions.tsx +++ b/frontend/hub/access/roles/components/RolePermissions.tsx @@ -125,8 +125,8 @@ export function usePermissionCategories( () => allGroups.map((group) => ({ ...group, - selectedPermissions: group.allPermissions.filter((permission) => - permissions?.includes(permission) + selectedPermissions: group.allPermissions.filter( + (permission) => permissions?.includes(permission) ), availablePermissions: group.allPermissions.filter( (permission) => !permissions?.includes(permission) diff --git a/frontend/hub/namespaces/HubNamespacePage/HubNamespaceAccessRoles.cy.tsx b/frontend/hub/namespaces/HubNamespacePage/HubNamespaceAccessRoles.cy.tsx new file mode 100644 index 0000000000..ca82fc9e41 --- /dev/null +++ b/frontend/hub/namespaces/HubNamespacePage/HubNamespaceAccessRoles.cy.tsx @@ -0,0 +1,117 @@ +import * as useHubContext from '../../common/useHubContext'; +import mockUser from '../../../../cypress/fixtures/hub_admin.json'; +import mockNamespaceResponse from '../../../../cypress/fixtures/hub_namespace.json'; +import { hubAPI, pulpAPI } from '../../common/api/formatPath'; +import { HubNamespaceAccessRoles } from './HubNamespaceAccessRoles'; + +describe('HubNamespaceAccessRoles', () => { + beforeEach(() => { + cy.intercept( + { + method: 'GET', + url: hubAPI`/_ui/v1/namespaces/*`, + }, + mockNamespaceResponse + ).as('namespaceDetails'); + }); + it('Roles list renders', () => { + cy.stub(useHubContext, 'useHubContext').callsFake(() => ({ + user: mockUser, + hasPermission: () => true, + })); + cy.mount(, { + path: '/namespaces/:id/users/:username', + initialEntries: ['/namespaces/2/users/new-user'], + }); + cy.get('tbody').find('tr').should('have.length', 2); + }); + it('Role can be expanded to view permissions', () => { + cy.intercept( + { + method: 'GET', + url: pulpAPI`/roles/*`, + }, + { + fixture: 'hub_namespace_permissions.json', + } + ).as('roleDetails'); + cy.stub(useHubContext, 'useHubContext').callsFake(() => ({ + user: mockUser, + hasPermission: () => true, + featureFlags: {}, + })); + cy.mount(, { + path: '/namespaces/:id/users/:username', + initialEntries: ['/namespaces/2/users/new-user'], + }); + cy.get('#expand-toggle0 > .pf-v5-c-table__toggle-icon').click(); + cy.contains(/^Change and upload collections to namespaces.$/); + cy.contains(/^Change namespace$/); + cy.contains(/^Upload to namespace$/); + }); + it('Filter roles by name', () => { + cy.stub(useHubContext, 'useHubContext').callsFake(() => ({ + user: mockUser, + hasPermission: () => true, + })); + cy.mount(, { + path: '/namespaces/:id/users/:username', + initialEntries: ['/namespaces/2/users/new-user'], + }); + cy.filterTableByText('namespace'); + cy.contains('galaxy.collection_namespace_owner'); + cy.get('tbody').find('tr').should('have.length', 1); + cy.clickButton(/^Clear all filters$/); + }); + it('Add/delete role actions are enabled for a user with permission to edit access for the namespace', () => { + cy.stub(useHubContext, 'useHubContext').callsFake(() => ({ + user: mockUser, + hasPermission: () => true, + })); + cy.mount(, { + path: '/namespaces/:id/users/:username', + initialEntries: ['/namespaces/2/users/new-user'], + }); + cy.contains('a[data-cy="add-role"]', /^Add role$/).should( + 'have.attr', + 'aria-disabled', + 'false' + ); + cy.contains('tr', 'galaxy.collection_namespace_owner').within(() => { + cy.get('button.toggle-kebab').click(); + cy.contains('.pf-v5-c-dropdown__menu-item', /^Delete role$/).should( + 'have.attr', + 'aria-disabled', + 'false' + ); + }); + }); + it('Add/delete role actions are disabled for a user without permission to edit access for the namespace', () => { + const mockEmptyUsersResponse = { ...mockNamespaceResponse }; + mockEmptyUsersResponse.data[0].related_fields.my_permissions = []; + cy.intercept( + { + method: 'GET', + url: hubAPI`/_ui/v1/namespaces/*`, + }, + mockEmptyUsersResponse + ).as('nonEmptyListWithoutPermissions'); + cy.stub(useHubContext, 'useHubContext').callsFake(() => ({ + user: { ...mockUser, is_superuser: false }, + hasPermission: () => false, + })); + cy.mount(, { + path: '/namespaces/:id/users/:username', + initialEntries: ['/namespaces/2/users/new-user'], + }); + cy.contains('a[data-cy="add-role"]', /^Add role$/).should('have.attr', 'aria-disabled', 'true'); + cy.contains('tr', 'galaxy.collection_namespace_owner').within(() => { + cy.get('button.toggle-kebab').click(); + cy.contains('.pf-v5-c-dropdown__menu-item', /^Delete role$/).should( + 'have.attr', + 'aria-disabled', + 'true' + ); + }); + }); +}); diff --git a/frontend/hub/namespaces/HubNamespacePage/HubNamespacePage.tsx b/frontend/hub/namespaces/HubNamespacePage/HubNamespacePage.tsx index 4c51a3c840..d267e4513c 100644 --- a/frontend/hub/namespaces/HubNamespacePage/HubNamespacePage.tsx +++ b/frontend/hub/namespaces/HubNamespacePage/HubNamespacePage.tsx @@ -86,11 +86,11 @@ export function HubNamespacePage() { page: HubRoute.NamespaceCLI, }, { - label: t('User Access'), + label: t('User access'), page: HubRoute.NamespaceUserAccess, }, { - label: t('Team Access'), + label: t('Team access'), page: HubRoute.NamespaceTeamAccess, }, ]} diff --git a/frontend/hub/namespaces/HubNamespacePage/HubNamespaceTeamAccess.cy.tsx b/frontend/hub/namespaces/HubNamespacePage/HubNamespaceTeamAccess.cy.tsx new file mode 100644 index 0000000000..50637e74e6 --- /dev/null +++ b/frontend/hub/namespaces/HubNamespacePage/HubNamespaceTeamAccess.cy.tsx @@ -0,0 +1,131 @@ +import * as useHubContext from '../../common/useHubContext'; +import mockUser from '../../../../cypress/fixtures/hub_admin.json'; +import mockNamespaceResponse from '../../../../cypress/fixtures/hub_namespace.json'; +import { hubAPI } from '../../common/api/formatPath'; +import { HubNamespaceTeamAccess } from './HubNamespaceTeamAccess'; + +describe('HubNamespaceTeamAccess', () => { + describe('Non-empty list', () => { + beforeEach(() => { + cy.intercept( + { + method: 'GET', + url: hubAPI`/_ui/v1/namespaces/*`, + }, + mockNamespaceResponse + ).as('nonEmptyListWithPermissions'); + }); + it('Teams list renders', () => { + cy.stub(useHubContext, 'useHubContext').callsFake(() => ({ + user: mockUser, + hasPermission: () => true, + })); + cy.mount(); + cy.get('tbody').find('tr').should('have.length', 1); + }); + it('Filter teams by name', () => { + cy.stub(useHubContext, 'useHubContext').callsFake(() => ({ + user: mockUser, + hasPermission: () => true, + })); + cy.mount(); + cy.filterTableByText('Test Group'); + cy.contains('Test Group'); + cy.get('tbody').find('tr').should('have.length', 1); + cy.clickButton(/^Clear all filters$/); + }); + it('Add/delete team actions are enabled for a user with permission to edit access for the namespace', () => { + cy.stub(useHubContext, 'useHubContext').callsFake(() => ({ + user: mockUser, + hasPermission: () => true, + })); + cy.mount(); + cy.contains('a[data-cy="add-team"]', /^Add team$/).should( + 'have.attr', + 'aria-disabled', + 'false' + ); + cy.contains('tr', 'Test Group').within(() => { + cy.get('button.toggle-kebab').click(); + cy.contains('.pf-v5-c-dropdown__menu-item', /^Delete team$/).should( + 'have.attr', + 'aria-disabled', + 'false' + ); + }); + }); + it('Add/delete team actions are disabled for a user without permission to edit access for the namespace', () => { + const mockEmptyUsersResponse = { ...mockNamespaceResponse }; + mockEmptyUsersResponse.data[0].related_fields.my_permissions = []; + cy.intercept( + { + method: 'GET', + url: hubAPI`/_ui/v1/namespaces/*`, + }, + mockEmptyUsersResponse + ).as('nonEmptyListWithoutPermissions'); + cy.stub(useHubContext, 'useHubContext').callsFake(() => ({ + user: { ...mockUser, is_superuser: false }, + hasPermission: () => false, + })); + cy.mount(); + cy.contains('a[data-cy="add-team"]', /^Add team$/).should( + 'have.attr', + 'aria-disabled', + 'true' + ); + cy.contains('tr', 'Test Group').within(() => { + cy.get('button.toggle-kebab').click(); + cy.contains('.pf-v5-c-dropdown__menu-item', /^Delete team$/).should( + 'have.attr', + 'aria-disabled', + 'true' + ); + }); + }); + }); + describe('Empty list', () => { + it('Empty state is displayed correctly for a user with permissions to add teams', () => { + const mockEmptyUsersResponse = { ...mockNamespaceResponse }; + mockEmptyUsersResponse.data[0].groups = []; + cy.intercept( + { + method: 'GET', + url: hubAPI`/_ui/v1/namespaces/*`, + }, + mockEmptyUsersResponse + ).as('emptyList'); + cy.stub(useHubContext, 'useHubContext').callsFake(() => ({ + user: mockUser, + hasPermission: () => true, + })); + cy.mount(); + cy.contains(/^There are currently no teams added.$/); + cy.contains(/^Please add a team by using the button below.$/); + cy.contains('a', /^Add team$/).should('be.visible'); + cy.contains('a', /^Add team$/).should('not.be.disabled'); + }); + it('Empty state is displayed correctly for user without permission to add teams', () => { + const mockEmptyUsersResponse = { ...mockNamespaceResponse }; + mockEmptyUsersResponse.data[0].groups = []; + mockEmptyUsersResponse.data[0].related_fields.my_permissions = []; + cy.intercept( + { + method: 'GET', + url: hubAPI`/_ui/v1/namespaces/*`, + }, + mockEmptyUsersResponse + ).as('emptyList'); + cy.stub(useHubContext, 'useHubContext').callsFake(() => ({ + user: { ...mockUser, is_superuser: false }, + hasPermission: () => false, + })); + cy.mount(); + cy.contains(/^You do not have permission to add a team.$/); + cy.contains( + /^Please contact your organization administrator if there is an issue with your access.$/ + ); + cy.contains('a', /^Add team$/).should('not.exist'); + }); + }); +}); diff --git a/frontend/hub/namespaces/HubNamespacePage/HubNamespaceUserAccess.cy.tsx b/frontend/hub/namespaces/HubNamespacePage/HubNamespaceUserAccess.cy.tsx new file mode 100644 index 0000000000..4918f5d829 --- /dev/null +++ b/frontend/hub/namespaces/HubNamespacePage/HubNamespaceUserAccess.cy.tsx @@ -0,0 +1,131 @@ +import * as useHubContext from '../../common/useHubContext'; +import mockUser from '../../../../cypress/fixtures/hub_admin.json'; +import mockNamespaceResponse from '../../../../cypress/fixtures/hub_namespace.json'; +import { HubNamespaceUserAccess } from './HubNamespaceUserAccess'; +import { hubAPI } from '../../common/api/formatPath'; + +describe('HubNamespaceUserAccess', () => { + describe('Non-empty list', () => { + beforeEach(() => { + cy.intercept( + { + method: 'GET', + url: hubAPI`/_ui/v1/namespaces/*`, + }, + mockNamespaceResponse + ).as('nonEmptyListWithPermissions'); + }); + it('Users list renders', () => { + cy.stub(useHubContext, 'useHubContext').callsFake(() => ({ + user: mockUser, + hasPermission: () => true, + })); + cy.mount(); + cy.get('tbody').find('tr').should('have.length', 2); + }); + it('Filter users by name', () => { + cy.stub(useHubContext, 'useHubContext').callsFake(() => ({ + user: mockUser, + hasPermission: () => true, + })); + cy.mount(); + cy.filterTableByText('new-user'); + cy.contains('new-user'); + cy.get('tbody').find('tr').should('have.length', 1); + cy.clickButton(/^Clear all filters$/); + }); + it('Add/delete user actions are enabled for a user with permission to edit access for the namespace', () => { + cy.stub(useHubContext, 'useHubContext').callsFake(() => ({ + user: mockUser, + hasPermission: () => true, + })); + cy.mount(); + cy.contains('a[data-cy="add-user"]', /^Add user$/).should( + 'have.attr', + 'aria-disabled', + 'false' + ); + cy.contains('tr', 'new-user').within(() => { + cy.get('button.toggle-kebab').click(); + cy.contains('.pf-v5-c-dropdown__menu-item', /^Delete user$/).should( + 'have.attr', + 'aria-disabled', + 'false' + ); + }); + }); + it('Add/delete user actions are disabled for a user without permission to edit access for the namespace', () => { + const mockEmptyUsersResponse = { ...mockNamespaceResponse }; + mockEmptyUsersResponse.data[0].related_fields.my_permissions = []; + cy.intercept( + { + method: 'GET', + url: hubAPI`/_ui/v1/namespaces/*`, + }, + mockEmptyUsersResponse + ).as('nonEmptyListWithoutPermissions'); + cy.stub(useHubContext, 'useHubContext').callsFake(() => ({ + user: { ...mockUser, is_superuser: false }, + hasPermission: () => false, + })); + cy.mount(); + cy.contains('a[data-cy="add-user"]', /^Add user$/).should( + 'have.attr', + 'aria-disabled', + 'true' + ); + cy.contains('tr', 'new-user').within(() => { + cy.get('button.toggle-kebab').click(); + cy.contains('.pf-v5-c-dropdown__menu-item', /^Delete user$/).should( + 'have.attr', + 'aria-disabled', + 'true' + ); + }); + }); + }); + describe('Empty list', () => { + it('Empty state is displayed correctly for a user with permissions to add users', () => { + const mockEmptyUsersResponse = { ...mockNamespaceResponse }; + mockEmptyUsersResponse.data[0].users = []; + cy.intercept( + { + method: 'GET', + url: hubAPI`/_ui/v1/namespaces/*`, + }, + mockEmptyUsersResponse + ).as('emptyList'); + cy.stub(useHubContext, 'useHubContext').callsFake(() => ({ + user: mockUser, + hasPermission: () => true, + })); + cy.mount(); + cy.contains(/^There are currently no users added.$/); + cy.contains(/^Please add a user by using the button below.$/); + cy.contains('a', /^Add user$/).should('be.visible'); + cy.contains('a', /^Add user$/).should('not.be.disabled'); + }); + it('Empty state is displayed correctly for user without permission to add users', () => { + const mockEmptyUsersResponse = { ...mockNamespaceResponse }; + mockEmptyUsersResponse.data[0].users = []; + mockEmptyUsersResponse.data[0].related_fields.my_permissions = []; + cy.intercept( + { + method: 'GET', + url: hubAPI`/_ui/v1/namespaces/*`, + }, + mockEmptyUsersResponse + ).as('emptyListWithoutPermissions'); + cy.stub(useHubContext, 'useHubContext').callsFake(() => ({ + user: { ...mockUser, is_superuser: false }, + hasPermission: () => false, + })); + cy.mount(); + cy.contains(/^You do not have permission to add a user.$/); + cy.contains( + /^Please contact your organization administrator if there is an issue with your access.$/ + ); + cy.contains('a', /^Add user$/).should('not.exist'); + }); + }); +});