Skip to content

Commit

Permalink
[AN-149] Add ability to edit namespace permissions (#5203)
Browse files Browse the repository at this point in the history
  • Loading branch information
salonishah11 authored Jan 6, 2025
1 parent e84b809 commit 315c146
Show file tree
Hide file tree
Showing 9 changed files with 606 additions and 241 deletions.
13 changes: 13 additions & 0 deletions src/libs/ajax/methods/Methods.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,19 @@ export const Methods = (signal?: AbortSignal) => ({
return res.json();
},

getNamespacePermissions: async (namespace: string): Promise<MethodConfigACL> => {
const res = await fetchOrchestration(`api/methods/${namespace}/permissions`, _.merge(authOpts(), { signal }));
return res.json();
},

setNamespacePermissions: async (namespace: string, payload: MethodConfigACL): Promise<MethodConfigACL> => {
const res = await fetchOrchestration(
`api/methods/${namespace}/permissions`,
_.mergeAll([authOpts(), jsonBody(payload), { signal, method: 'POST' }])
);
return res.json();
},

method: (namespace, name, snapshotId) => {
const root = `methods/${namespace}/${name}/${snapshotId}`;

Expand Down
145 changes: 145 additions & 0 deletions src/libs/ajax/methods/providers/PermissionsProvider.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import { asMockedFn, partial } from '@terra-ui-packages/test-utils';
import { Methods, MethodsAjaxContract } from 'src/libs/ajax/methods/Methods';
import {
namespacePermissionsProvider,
snapshotPermissionsProvider,
} from 'src/libs/ajax/methods/providers/PermissionsProvider';
import { WorkflowsPermissions } from 'src/workflows/workflows-acl-utils';

jest.mock('src/libs/ajax/methods/Methods');

const mockPermissions: WorkflowsPermissions = [
{
role: 'OWNER',
user: '[email protected]',
},
{
role: 'READER',
user: '[email protected]',
},
];

const updatedMockPermissions: WorkflowsPermissions = [
{
role: 'OWNER',
user: '[email protected]',
},
{
role: 'OWNER',
user: '[email protected]',
},
];

type MethodsNamespaceAjaxNeeds = Pick<MethodsAjaxContract, 'getNamespacePermissions' | 'setNamespacePermissions'>;
type MethodObjContract = MethodsAjaxContract['method'];

const mockSnapshotAjax = () => {
const mockGetSnapshotPermissions = jest.fn().mockReturnValue(mockPermissions);
const mockSetSnapshotPermissions = jest.fn().mockReturnValue(updatedMockPermissions);
asMockedFn(Methods).mockReturnValue({
method: jest.fn(() => {
return partial<ReturnType<MethodObjContract>>({
permissions: mockGetSnapshotPermissions,
setPermissions: mockSetSnapshotPermissions,
});
}) as MethodObjContract,
} as MethodsAjaxContract);
};

const mockNamespaceAjax = (): MethodsNamespaceAjaxNeeds => {
const partialMethods: MethodsNamespaceAjaxNeeds = {
getNamespacePermissions: jest.fn().mockReturnValue(mockPermissions),
setNamespacePermissions: jest.fn().mockReturnValue(updatedMockPermissions),
};
asMockedFn(Methods).mockReturnValue(partial<MethodsAjaxContract>(partialMethods));

return partialMethods;
};

describe('PermissionsProvider', () => {
it('handles get snapshot permissions call', async () => {
// Arrange
mockSnapshotAjax();
const signal = new window.AbortController().signal;

// Act
const result = await snapshotPermissionsProvider('groot-method', 3).getPermissions('groot-namespace', { signal });

// Assert
expect(Methods).toBeCalledTimes(1);
expect(Methods().method).toHaveBeenCalledWith('groot-namespace', 'groot-method', 3);
expect(Methods().method('groot-namespace', 'groot-method', 3).permissions).toHaveBeenCalled();
expect(result).toEqual(mockPermissions);
});

it('handles set snapshot permissions call', async () => {
// Arrange
mockSnapshotAjax();
const signal = new window.AbortController().signal;

// Act
const result = await snapshotPermissionsProvider('groot-method', 3).updatePermissions(
'groot-namespace',
[
{
role: 'OWNER',
user: '[email protected]',
},
],
{ signal }
);

// Assert
expect(Methods).toBeCalledTimes(1);
expect(Methods().method).toHaveBeenCalledWith('groot-namespace', 'groot-method', 3);
expect(Methods().method('groot-namespace', 'groot-method', 3).setPermissions).toHaveBeenCalledWith([
{
role: 'OWNER',
user: '[email protected]',
},
]);
expect(result).toEqual(updatedMockPermissions);
});

it('handles get namespace permissions call', async () => {
// Arrange
mockNamespaceAjax();
const signal = new window.AbortController().signal;

// Act
const result = await namespacePermissionsProvider.getPermissions('groot-namespace', { signal });

// Assert
expect(Methods).toBeCalledTimes(1);
expect(Methods().getNamespacePermissions).toHaveBeenCalledWith('groot-namespace');
expect(result).toEqual(mockPermissions);
});

it('handles set namespace permissions call', async () => {
// Arrange
mockNamespaceAjax();
const signal = new window.AbortController().signal;

// Act
const result = await namespacePermissionsProvider.updatePermissions(
'groot-namespace',
[
{
role: 'OWNER',
user: '[email protected]',
},
],
{ signal }
);

// Assert
expect(Methods).toBeCalledTimes(1);
expect(Methods().setNamespacePermissions).toHaveBeenCalledWith('groot-namespace', [
{
role: 'OWNER',
user: '[email protected]',
},
]);
expect(result).toEqual(updatedMockPermissions);
});
});
51 changes: 51 additions & 0 deletions src/libs/ajax/methods/providers/PermissionsProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { AbortOption } from '@terra-ui-packages/data-client-core';
import { Methods } from 'src/libs/ajax/methods/Methods';
import { MethodConfigACL } from 'src/libs/ajax/methods/methods-models';

export interface PermissionsProvider {
getPermissions: (methodNamespace: string, options?: AbortOption) => Promise<MethodConfigACL>;
updatePermissions: (
methodNamespace: string,
updatedPermissions: MethodConfigACL,
options?: AbortOption
) => Promise<MethodConfigACL>;
}

/**
* Permissions provider to fetch and update the namespace permissions in Broad Methods Repository
*/
export const namespacePermissionsProvider: PermissionsProvider = {
getPermissions: async (methodNamespace: string, options: AbortOption = {}): Promise<MethodConfigACL> => {
const { signal } = options;
return await Methods(signal).getNamespacePermissions(methodNamespace);
},

updatePermissions: async (
methodNamespace: string,
updatedPermissions: MethodConfigACL,
options: AbortOption = {}
): Promise<MethodConfigACL> => {
const { signal } = options;
return await Methods(signal).setNamespacePermissions(methodNamespace, updatedPermissions);
},
};

/**
* Permissions provider to fetch and update the method snapshot permissions in Broad Methods Repository
*/
export const snapshotPermissionsProvider = (methodName: string, snapshotId: number): PermissionsProvider => {
return {
getPermissions: async (methodNamespace: string, options: AbortOption = {}): Promise<MethodConfigACL> => {
const { signal } = options;
return await Methods(signal).method(methodNamespace, methodName, snapshotId).permissions();
},
updatePermissions: async (
methodNamespace,
updatedPermissions: MethodConfigACL,
options: AbortOption = {}
): Promise<MethodConfigACL> => {
const { signal } = options;
return await Methods(signal).method(methodNamespace, methodName, snapshotId).setPermissions(updatedPermissions);
},
};
};
78 changes: 66 additions & 12 deletions src/pages/workflows/WorkflowList.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { notify } from 'src/libs/notifications';
import { TerraUser, TerraUserState, userStore } from 'src/libs/state';
import { WorkflowList } from 'src/pages/workflows/WorkflowList';
import { asMockedFn, partial, renderWithAppContexts as render } from 'src/testing/test-utils';
import { WorkflowsPermissions } from 'src/workflows/workflows-acl-utils';

jest.mock('src/libs/ajax/methods/Methods');

Expand Down Expand Up @@ -52,9 +53,21 @@ jest.mock('react-virtualized', () => {
};
});

const mockPublicPermissions: WorkflowsPermissions = [
{
role: 'READER',
user: 'public',
},
{
role: 'OWNER',
user: '[email protected]',
},
];

const mockMethods = (methods: MethodDefinition[]): MethodsAjaxContract => {
return partial<MethodsAjaxContract>({
definitions: jest.fn(async () => methods),
getNamespacePermissions: jest.fn(async () => mockPublicPermissions),
});
};

Expand Down Expand Up @@ -226,26 +239,67 @@ describe('workflows table', () => {
const table: HTMLElement = await screen.findByRole('table');

// Assert
expect(table).toHaveAttribute('aria-colcount', '4');
expect(table).toHaveAttribute('aria-colcount', '5');
expect(table).toHaveAttribute('aria-rowcount', '2');

const headers: HTMLElement[] = within(table).getAllByRole('columnheader');
expect(headers).toHaveLength(4);
expect(headers[0]).toHaveTextContent('Workflow');
expect(headers[1]).toHaveTextContent('Synopsis');
expect(headers[2]).toHaveTextContent('Owners');
expect(headers[3]).toHaveTextContent('Versions');
expect(headers).toHaveLength(5);
expect(headers[0]).toHaveTextContent('Actions');
expect(headers[1]).toHaveTextContent('Workflow');
expect(headers[2]).toHaveTextContent('Synopsis');
expect(headers[3]).toHaveTextContent('Owners');
expect(headers[4]).toHaveTextContent('Versions');

const rows: HTMLElement[] = within(table).getAllByRole('row');
expect(rows).toHaveLength(2);

const methodCells: HTMLElement[] = within(rows[1]).getAllByRole('cell');
expect(methodCells).toHaveLength(4);
within(methodCells[0]).getByText('revali bird namespace');
within(methodCells[0]).getByText('revali method 2');
expect(methodCells[1]).toHaveTextContent('another revali description');
expect(methodCells[2]).toHaveTextContent('[email protected], [email protected]');
expect(methodCells[3]).toHaveTextContent('1');
expect(methodCells).toHaveLength(5);
within(methodCells[1]).getByText('revali bird namespace');
within(methodCells[1]).getByText('revali method 2');
expect(methodCells[2]).toHaveTextContent('another revali description');
expect(methodCells[3]).toHaveTextContent('[email protected], [email protected]');
expect(methodCells[4]).toHaveTextContent('1');
});

it('allows editing permissions for namespace owned by user', async () => {
// Arrange
asMockedFn(Methods).mockReturnValue(mockMethods([revaliMethod]));
const user: UserEvent = userEvent.setup();

// set the user's email
jest.spyOn(userStore, 'get').mockImplementation(jest.fn().mockReturnValue(mockUserState('[email protected]')));

// Act
await act(async () => {
render(<WorkflowList />);
});

const table: HTMLElement = await screen.findByRole('table');

const rows: HTMLElement[] = within(table).getAllByRole('row');
expect(rows).toHaveLength(2);

const methodCells: HTMLElement[] = within(rows[1]).getAllByRole('cell');
expect(methodCells).toHaveLength(5);
within(methodCells[1]).getByText('revali bird namespace');
within(methodCells[1]).getByText('revali method');

const actionsMenu = within(methodCells[0]).getByRole('button');

// Act
await user.click(actionsMenu);

// Assert
expect(screen.getByText('Edit namespace permissions'));

// Act
await user.click(screen.getByRole('button', { name: 'Edit namespace permissions' }));

// Assert
expect(
screen.queryByRole('dialog', { name: /Edit permissions for namespace revali bird namespace/i })
).toBeInTheDocument();
});

it('displays a message with no my workflows', async () => {
Expand Down
Loading

0 comments on commit 315c146

Please sign in to comment.