Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 79 additions & 0 deletions e2e/all-functions-read-shortcut.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,3 +127,82 @@ test.describe('ALL_FUNCTIONS_READ shortcut', () => {
await expect(page.getByRole('button', { name: CREATE_CLIENT_BUTTON })).toBeVisible();
});
});

async function mockAdminScreens(page: Page): Promise<string[]> {
const writes: string[] = [];
page.on('request', (request) => {
if (
request.method() !== 'GET' &&
/\/api\/v1\/(configurations|users|roles)/.test(request.url())
) {
writes.push(`${request.method()} ${request.url()}`);
}
});
await page.route('**/api/v1/configurations*', (route) =>
route.fulfill({
json: { globalConfiguration: [{ id: 1, name: 'maker-checker', enabled: false, value: 0 }] },
}),
);
await page.route('**/api/v1/users*', (route) =>
route.fulfill({
json: [
{
id: 4,
username: 'auditor',
firstname: 'Audit',
lastname: 'User',
officeId: 1,
officeName: 'Head Office',
selectedRoles: [],
},
],
}),
);
await page.route('**/api/v1/roles*', (route) =>
route.fulfill({
json: [{ id: 3, name: 'Auditor', description: 'Read only', disabled: false }],
}),
);
return writes;
}

test.describe('ALL_FUNCTIONS_READ on the admin screens', () => {
test('offers no write on Global Configurations, Users or Roles', async ({ page }) => {
const writes = await mockAdminScreens(page);
await login(page, ['ALL_FUNCTIONS_READ']);

await page.goto('/settings/configurations');
const toggle = page.getByTestId('config-toggle-1');
await expect(toggle).toBeVisible();
await expect(toggle).toHaveAttribute('aria-disabled', 'true');
await expect(page.getByTestId('config-edit-1')).toHaveAttribute('aria-disabled', 'true');
await toggle.click({ force: true });
await page.getByTestId('config-edit-1').click({ force: true });
await expect(page.getByRole('dialog')).toHaveCount(0);

await page.goto('/security/users');
await expect(page.getByText('auditor').first()).toBeVisible();
await expect(page.getByTestId('user-edit-4')).toHaveCount(0);

await page.goto('/security/roles');
await expect(page.getByText('Auditor').first()).toBeVisible();
await expect(page.getByTestId('role-edit-3')).toHaveCount(0);

expect(writes).toEqual([]);
});

test('ALL_FUNCTIONS still gets the toggles and the Edit actions', async ({ page }) => {
await mockAdminScreens(page);
await login(page, ['ALL_FUNCTIONS']);

await page.goto('/settings/configurations');
await expect(page.getByTestId('config-toggle-1')).not.toHaveAttribute('aria-disabled', 'true');
await expect(page.getByTestId('config-edit-1')).not.toHaveAttribute('aria-disabled', 'true');

await page.goto('/security/users');
await expect(page.getByTestId('user-edit-4')).toBeVisible();

await page.goto('/security/roles');
await expect(page.getByTestId('role-edit-3')).toBeVisible();
});
});
13 changes: 12 additions & 1 deletion src/app/features/security/roles/roles-list.component.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -205,10 +205,21 @@ describe('RolesListComponent', () => {
const html = fixture.nativeElement as HTMLElement;
expect(html.querySelector('[data-testid="role-disable-1"]')).toBeNull();
expect(html.querySelector('[data-testid="role-delete-1"]')).toBeNull();
// Edit is not gated here and stays, so this is not an empty screen.
expect(html.querySelector('[data-testid="role-edit-1"]')).toBeNull();
expect(html.querySelector('[data-testid="role-status-1"]')).not.toBeNull();
});

it('offers Edit to a session with UPDATE_ROLE', () => {
roleApiSpy.list.mockReturnValue(of([role()]));

fixture.detectChanges();

expect(
(fixture.nativeElement as HTMLElement).querySelector('[data-testid="role-edit-1"]'),
).not.toBeNull();
expect(authService.hasPermission).toHaveBeenCalledWith('UPDATE_ROLE', false);
});

/**
* The platform refuses while any user holds the role, and that refusal is the message worth
* showing — `errorInterceptor` reports a domain-rule 403 in the platform's own words. The
Expand Down
2 changes: 2 additions & 0 deletions src/app/features/security/roles/roles-list.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,10 @@ import { ButtonComponent } from '../../../ui/button/button.component';
emphasis="quiet"
[label]="'COMMON.EDIT' | appTranslate"
icon="create-outline"
[attr.data-testid]="'role-edit-' + role.id"
[appTooltip]="'COMMON.EDIT' | appTranslate"
(click)="onEditRole(role)"
*appHasPermission="'UPDATE_ROLE'"
/>
@if (role.disabled) {
<app-button
Expand Down
17 changes: 17 additions & 0 deletions src/app/features/security/users/users-list.component.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,23 @@ describe('UsersListComponent', () => {
expect(userApi['delete']).not.toHaveBeenCalled();
});

it('offers Edit to a session with UPDATE_USER', async () => {
await setup();

expect(
(fixture.nativeElement as HTMLElement).querySelector('[data-testid="user-edit-4"]'),
).not.toBeNull();
expect(authService.hasPermission).toHaveBeenCalledWith('UPDATE_USER', false);
});

it('hides Edit from a session without UPDATE_USER', async () => {
await setup([user()], 1, false);

expect(
(fixture.nativeElement as HTMLElement).querySelector('[data-testid="user-edit-4"]'),
).toBeNull();
});

it('hides Delete from a session without DELETE_USER', async () => {
await setup([user()], 1, false);

Expand Down
2 changes: 2 additions & 0 deletions src/app/features/security/users/users-list.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,9 @@ import { ButtonComponent } from '../../../ui/button/button.component';
intent="primary"
icon="create-outline"
[label]="'COMMON.EDIT' | appTranslate"
[attr.data-testid]="'user-edit-' + user.id"
(click)="onEditUser(user)"
*appHasPermission="'UPDATE_USER'"
/>
<!--
Not offered on your own account. The platform does allow it, and an administrator who
Expand Down
6 changes: 6 additions & 0 deletions src/app/features/settings/global-configurations.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import { IonButton, IonIcon, IonToggle } from '@ionic/angular/standalone';
import { NotificationService } from '../../core/services/notification.service';
import { DialogService } from '../../core/services/dialog.service';
import { TooltipDirective } from '../../shared/directives/tooltip.directive';
import { RequiresPermissionDirective } from '../../shared/directives/requires-permission.directive';

/**
* Component for managing global system configurations.
Expand All @@ -46,6 +47,7 @@ import { TooltipDirective } from '../../shared/directives/tooltip.directive';
IonButton,
IonToggle,
TooltipDirective,
RequiresPermissionDirective,
],
template: `
<app-data-table
Expand All @@ -62,6 +64,8 @@ import { TooltipDirective } from '../../shared/directives/tooltip.directive';
[checked]="!!config['enabled']"
(ionChange)="onToggleConfig(config)"
[attr.aria-label]="'Toggle ' + config['name']"
[attr.data-testid]="'config-toggle-' + config['id']"
appRequiresPermission="UPDATE_CONFIGURATION"
>
</ion-toggle>
</ng-template>
Expand All @@ -72,7 +76,9 @@ import { TooltipDirective } from '../../shared/directives/tooltip.directive';
color="primary"
[attr.aria-label]="'COMMON.EDIT' | appTranslate"
[appTooltip]="'COMMON.EDIT' | appTranslate"
[attr.data-testid]="'config-edit-' + config['id']"
(click)="onEditConfig(config)"
appRequiresPermission="UPDATE_CONFIGURATION"
>
<ion-icon name="create-outline"></ion-icon>
</ion-button>
Expand Down
Loading