Skip to content

Commit 204c11b

Browse files
KasinhoukosarkoamadulhaxxaniMatusBekeclaude
authored
VSB-TUO/Prevent an admin from deleting their own account (#1445)
* UFAL/[Port to dtq-dev] 117 better control of user deletion in user administration page (#1335) * 117 better control of user deletion in user administration page (ufal#140) * Disable self-delete and add delete warnings Prevent administrators from deleting their own account by disabling the delete button for the current authenticated user and showing a tooltip. Add a warningLabel input to the confirmation modal and render it when present. Implement logic in EPeopleRegistryComponent to determine the current user, compute a contextual delete warning (submitter, admin, or both) by querying workspace/workflow submissions, search, and group membership, and open the confirmation modal with that warning. Handle delete responses to show a friendly notification for backend 400 self-delete errors and use a generic failure notification otherwise. Update templates, component imports and helper methods (isCurrentUser, getDeleteWarningLabel, hasSubmittedItems, isAdministrator, isSelfDeletionError, showSelfDeleteNotification), and extend unit tests to cover disabled self-delete UI, composed warning labels, and notification behavior. * Prevent self-delete and show delete warnings Disable direct self-deletion and surface contextual warnings when deleting an EPerson. Template: render the delete button as disabled for the currently authenticated user and show a tooltip explaining self-delete restrictions; otherwise show the normal delete button. Component: track the current authenticated user, compute a combined warning label based on whether the EPerson is an administrator and/or has submitted items (checks workspace, workflow and archived submissions), present that warning in the confirmation modal, and handle deletion results including a friendly notification for backend self-delete errors. Added helper methods (isCurrentUser, getDeleteWarningLabel, hasSubmittedItems, isAdministrator, isSelfDeletionError, showSelfDeleteNotification) and adjusted delete flow to update canDelete$ appropriately. Tests: updated and added specs and mocks to cover the disabled self-delete UI, combined warning label usage, and the friendly self-delete error notification; added required service mocks and test setup changes. * Add EPerson delete warnings and self-delete message Add four i18n keys to en.json5 and cs.json5 for EPerson deletion flows: a forbidden self-delete message and warnings for deleting users who are submitters, administrators, or both. Provides localized English and Czech strings to surface these messages in the admin access-control UI. * Guard EPerson delete until auth ID resolved Prevent delete actions from running before the current authenticated user id is available. Add a template condition to hide the delete button until currentAuthenticatedUserId is set, add an early-return guard in deleteEPerson when the id is missing, and add a unit test ensuring the modal is not opened and delete is not called before the id is resolved. * Hide delete button until auth user ID resolved Prevent delete actions before the current authenticated user ID is available. Add *ngIf to the delete button in the template, add an early return guard in the component's delete flow when currentAuthenticatedUserId is not set, and include unit tests verifying the button is hidden and the delete/modal are not invoked until the ID is resolved. * Restrict delete button visibility and update tests Template: Wrap self-delete tooltip/button in a guard that checks epersonDto.ableToDelete and currentAuthenticatedUserId so delete UI is only rendered when deletion is allowed; simplify the enabledDeleteButton markup by removing a duplicated *ngIf on the inner button. Spec: remove unused DebugElement import, rename the test from 'should be disabled' to 'should be hidden', and update assertions to expect no delete buttons to be present (reflecting the new visibility behavior). * Detect Administrator group across paginated pages Add support for detecting Administrator group membership across paginated group lists and a unit test for it. - Introduce hasAdministratorGroupOnPage(groupsHref, currentPage) which fetches group pages (elementsPerPage=100) and recursively checks subsequent pages when the Administrator group is not found on the current page. - isAdministrator now delegates to hasAdministratorGroupOnPage starting at page 1. - Preserve error handling to return false on failures. - Add a unit test that simulates a two-page group response (administrator present on the second page) and verifies the component detects administrator membership and displays the correct warning label. * Detect Administrator group across pages Refactor group membership check to traverse paginated group lists until an "Administrator" group is found or pages are exhausted. Introduces a recursive hasAdministratorGroupOnPage that requests pages, validates payload/pageInfo, checks for the admin group, and requests the next page when needed. Adds a unit test covering detection on later pages and a minor spec setup tweak (setting currentAuthenticatedUserId) to correctly exercise deletion behavior. * Fix Czech typo in delete warning Corrects a spelling mistake in src/assets/i18n/cs.json5 for key admin.access-control.epeople.delete.warning.submitter: changed 'repositáři' to 'repozitáři' to improve Czech translation accuracy. * Make disabled delete button unfocusable Add tabindex="-1" to the disabled delete button in the epeople registry template so it cannot receive keyboard focus. This ensures the parent span (which provides the tooltip) remains the sole focus target for the current-user case, improving accessibility and preventing duplicate focusable elements. * Make disabled self-delete button unfocusable Add tabindex="-1" to the disabled delete button in eperson-form.component.html so the button cannot receive keyboard focus when showing the current user. The tooltip remains on the parent span, improving keyboard navigation and accessibility for the self-delete warning. * Remove stray 'after' attribute from eperson template Remove an unintended "after" attribute from a <span> in eperson-form.component.html. The change cleans up the template by leaving only the intended *ngIf binding for conditional rendering of the delete/impersonation UI, preventing potential template parsing or linting issues. * code cleanup - remove duplicities * cleanup * copilot comments * copilot's comment --------- Co-authored-by: Ondrej Kosarko <kosarko@ufal.mff.cuni.cz> (cherry picked from commit d0b5ecf) * fix(access-control): quote submitter UUID in archived item search query --------- Co-authored-by: Amad Ul Hassan <hassan@ufal.mff.cuni.cz> (cherry picked from commit 07957d8) * UFAL/Fix misleading notification when admin self-delete is rejected (#1357) * Fix misleading message when self-delete rejection lacks a matched error text isSelfDeletionError() matches the backend's rejection message as plain text, but Spring Boot omits exception messages from error response bodies by default and DSpaceBadRequestException/IllegalStateException have no dedicated JSON-body exception handler, so the match can silently fail and fall through to the generic, unfriendly failure notification instead of the "you cannot delete your own account" one. Add a deterministic client-side identity check as a fallback alongside the text match so the friendly message shows reliably regardless of what the backend's error body contains. Fixes dataquest-dev/dspace-customers#782 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * Removed redundant comments --------- Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com> Co-authored-by: Matus Kasak <matus.kasak@dataquest.sk> (cherry picked from commit 43cf493) * UFAL/Fix self-delete tooltip being clipped by the EPeople table (#1373) The disabled delete button's ngbTooltip renders inside the table's .table-responsive wrapper, which has overflow-x: auto — clipping the tooltip popover since the button sits near the table's right edge. Add container="body" so the tooltip is appended to <body> instead, escaping the overflow-clipped ancestor (same convention already used elsewhere in this codebase, e.g. health-status and orcid-queue). Fixes dataquest-dev/dspace-customers#800 Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com> (cherry picked from commit 96209c4) * Reword the Czech self-delete / delete-warning messages Rewrites the four Czech strings for the self-delete guard so they read naturally rather than as literal translations, keeping the repository's established Czech terminology (uživatel / správce / záznamy / smazat) and active phrasing ("Jeho smazáním odeberete…" instead of the nominal "Smazání tohoto uživatele odebere…"). Wording is identical across all customer branches. Raised in review on PR #1447. Refs dataquest-dev/dspace-customers#855 --------- Co-authored-by: Ondřej Košarko <ko_ok@centrum.cz> Co-authored-by: Amad Ul Hassan <hassan@ufal.mff.cuni.cz> Co-authored-by: MatusBeke <matus.beke7@gmail.com> Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com> Co-authored-by: Matus Kasak <matus.kasak@dataquest.sk>
1 parent a686cd0 commit 204c11b

14 files changed

Lines changed: 820 additions & 66 deletions

src/app/access-control/access-control.module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { SharedModule } from '../shared/shared.module';
55
import { AccessControlRoutingModule } from './access-control-routing.module';
66
import { EPeopleRegistryComponent } from './epeople-registry/epeople-registry.component';
77
import { EPersonFormComponent } from './epeople-registry/eperson-form/eperson-form.component';
8+
import { EPersonDeleteGuardService } from './epeople-registry/eperson-delete-guard.service';
89
import { GroupFormComponent } from './group-registry/group-form/group-form.component';
910
import { MembersListComponent } from './group-registry/group-form/members-list/members-list.component';
1011
import { SubgroupsListComponent } from './group-registry/group-form/subgroup-list/subgroups-list.component';
@@ -57,6 +58,7 @@ export const ValidateEmailErrorStateMatcher: DynamicErrorMessagesMatcher =
5758
provide: DYNAMIC_ERROR_MESSAGES_MATCHER,
5859
useValue: ValidateEmailErrorStateMatcher
5960
},
61+
EPersonDeleteGuardService,
6062
]
6163
})
6264
/**

src/app/access-control/epeople-registry/epeople-registry.component.html

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -72,11 +72,26 @@ <h2 id="search" class="border-bottom pb-2">
7272
title="{{labelPrefix + 'table.edit.buttons.edit' | translate: { name: dsoNameService.getName(epersonDto.eperson) } }}">
7373
<i class="fas fa-edit fa-fw"></i>
7474
</button>
75-
<button *ngIf="epersonDto.ableToDelete" (click)="deleteEPerson(epersonDto.eperson)"
76-
class="delete-button btn btn-outline-danger btn-sm access-control-deleteEPersonButton"
77-
title="{{labelPrefix + 'table.edit.buttons.remove' | translate: { name: dsoNameService.getName(epersonDto.eperson) } }}">
78-
<i class="fas fa-trash-alt fa-fw"></i>
79-
</button>
75+
<ng-container *ngIf="epersonDto.ableToDelete && currentAuthenticatedUserId">
76+
<ng-container *ngIf="isCurrentUser(epersonDto.eperson); else enabledDeleteButton">
77+
<span tabindex="0" [ngbTooltip]="selfDeleteWarningLabel | translate" container="body">
78+
<button [dsBtnDisabled]="true"
79+
tabindex="-1"
80+
[attr.aria-label]="selfDeleteWarningLabel | translate"
81+
class="delete-button btn btn-outline-danger btn-sm access-control-deleteEPersonButton"
82+
type="button">
83+
<i class="fas fa-trash-alt fa-fw"></i>
84+
</button>
85+
</span>
86+
</ng-container>
87+
</ng-container>
88+
<ng-template #enabledDeleteButton>
89+
<button (click)="deleteEPerson(epersonDto.eperson)"
90+
class="delete-button btn btn-outline-danger btn-sm access-control-deleteEPersonButton"
91+
title="{{labelPrefix + 'table.edit.buttons.remove' | translate: { name: dsoNameService.getName(epersonDto.eperson) } }}">
92+
<i class="fas fa-trash-alt fa-fw"></i>
93+
</button>
94+
</ng-template>
8095
</div>
8196
</td>
8297
</tr>

src/app/access-control/epeople-registry/epeople-registry.component.spec.ts

Lines changed: 181 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,42 @@
11
import { Router } from '@angular/router';
2-
import { Observable, of as observableOf } from 'rxjs';
2+
import { defer, Observable, of as observableOf, throwError as observableThrowError } from 'rxjs';
33
import { CommonModule } from '@angular/common';
4-
import { DebugElement, NO_ERRORS_SCHEMA } from '@angular/core';
4+
import { NO_ERRORS_SCHEMA } from '@angular/core';
55
import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing';
66
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
77
import { BrowserModule, By } from '@angular/platform-browser';
88
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
99
import { TranslateLoader, TranslateModule, TranslateService } from '@ngx-translate/core';
1010
import { buildPaginatedList, PaginatedList } from '../../core/data/paginated-list.model';
1111
import { RemoteData } from '../../core/data/remote-data';
12+
import { AuthService } from '../../core/auth/auth.service';
1213
import { EPersonDataService } from '../../core/eperson/eperson-data.service';
1314
import { EPerson } from '../../core/eperson/models/eperson.model';
1415
import { PageInfo } from '../../core/shared/page-info.model';
16+
import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
1517
import { FormBuilderService } from '../../shared/form/builder/form-builder.service';
1618
import { NotificationsService } from '../../shared/notifications/notifications.service';
1719
import { EPeopleRegistryComponent } from './epeople-registry.component';
1820
import { EPersonMock, EPersonMock2 } from '../../shared/testing/eperson.mock';
19-
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
21+
import { createFailedRemoteDataObject$, createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
2022
import { getMockFormBuilderService } from '../../shared/mocks/form-builder-service.mock';
2123
import { getMockTranslateService } from '../../shared/mocks/translate.service.mock';
2224
import { TranslateLoaderMock } from '../../shared/mocks/translate-loader.mock';
2325
import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub';
2426
import { RouterStub } from '../../shared/testing/router.stub';
2527
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
28+
import { FeatureID } from '../../core/data/feature-authorization/feature-id';
29+
import { EPersonDeleteGuardService } from './eperson-delete-guard.service';
2630
import { RequestService } from '../../core/data/request.service';
2731
import { PaginationService } from '../../core/pagination/pagination.service';
2832
import { PaginationServiceStub } from '../../shared/testing/pagination-service.stub';
33+
import { WorkspaceitemDataService } from '../../core/submission/workspaceitem-data.service';
34+
import { WorkflowItemDataService } from '../../core/submission/workflowitem-data.service';
2935
import { FindListOptions } from '../../core/data/find-list-options.model';
30-
import {BtnDisabledDirective} from '../../shared/btn-disabled.directive';
36+
import { SearchService } from '../../core/shared/search/search.service';
37+
import { DSpaceObject } from '../../core/shared/dspace-object.model';
38+
import { SearchObjects } from '../../shared/search/models/search-objects.model';
39+
import { BtnDisabledDirective } from '../../shared/btn-disabled.directive';
3140

3241
describe('EPeopleRegistryComponent', () => {
3342
let component: EPeopleRegistryComponent;
@@ -38,10 +47,35 @@ describe('EPeopleRegistryComponent', () => {
3847
let mockEPeople;
3948
let ePersonDataServiceStub: any;
4049
let authorizationService: AuthorizationDataService;
50+
let authService: jasmine.SpyObj<AuthService>;
51+
let workspaceItemDataService: jasmine.SpyObj<WorkspaceitemDataService>;
52+
let workflowItemDataService: jasmine.SpyObj<WorkflowItemDataService>;
53+
let searchService: jasmine.SpyObj<SearchService>;
54+
let notificationsService: NotificationsServiceStub;
4155
let modalService;
56+
let modalRef;
4257

4358
let paginationService;
4459

60+
const buildRemoteList = <T>(items: T[], totalElements = items.length) => createSuccessfulRemoteDataObject$(
61+
buildPaginatedList(new PageInfo({
62+
elementsPerPage: items.length || 1,
63+
totalElements,
64+
totalPages: 1,
65+
currentPage: 1
66+
}), items)
67+
);
68+
69+
const buildSearchObjects = (totalElements: number) => Object.assign(
70+
new SearchObjects<DSpaceObject>(),
71+
buildPaginatedList(new PageInfo({
72+
elementsPerPage: 1,
73+
totalElements,
74+
totalPages: 1,
75+
currentPage: 1
76+
}), [])
77+
);
78+
4579
beforeEach(waitForAsync(() => {
4680
jasmine.getEnv().allowRespy(true);
4781
mockEPeople = [EPersonMock, EPersonMock2];
@@ -119,8 +153,17 @@ describe('EPeopleRegistryComponent', () => {
119153
authorizationService = jasmine.createSpyObj('authorizationService', {
120154
isAuthorized: observableOf(true)
121155
});
156+
authService = jasmine.createSpyObj('authService', ['getAuthenticatedUserFromStore']);
157+
authService.getAuthenticatedUserFromStore.and.returnValue(observableOf(EPersonMock2));
158+
workspaceItemDataService = jasmine.createSpyObj('workspaceItemDataService', ['searchBy']);
159+
workspaceItemDataService.searchBy.and.returnValue(buildRemoteList([], 0));
160+
workflowItemDataService = jasmine.createSpyObj('workflowItemDataService', ['searchBy']);
161+
workflowItemDataService.searchBy.and.returnValue(buildRemoteList([], 0));
162+
searchService = jasmine.createSpyObj('searchService', ['search']);
163+
searchService.search.and.returnValue(createSuccessfulRemoteDataObject$(buildSearchObjects(0)));
122164
builderService = getMockFormBuilderService();
123165
translateService = getMockTranslateService();
166+
notificationsService = new NotificationsServiceStub();
124167

125168
paginationService = new PaginationServiceStub();
126169
TestBed.configureTestingModule({
@@ -135,12 +178,20 @@ describe('EPeopleRegistryComponent', () => {
135178
declarations: [EPeopleRegistryComponent, BtnDisabledDirective],
136179
providers: [
137180
{ provide: EPersonDataService, useValue: ePersonDataServiceStub },
138-
{ provide: NotificationsService, useValue: new NotificationsServiceStub() },
181+
{ provide: NotificationsService, useValue: notificationsService },
139182
{ provide: AuthorizationDataService, useValue: authorizationService },
183+
{ provide: AuthService, useValue: authService },
140184
{ provide: FormBuilderService, useValue: builderService },
185+
{ provide: WorkspaceitemDataService, useValue: workspaceItemDataService },
186+
{ provide: WorkflowItemDataService, useValue: workflowItemDataService },
187+
{ provide: SearchService, useValue: searchService },
188+
EPersonDeleteGuardService,
141189
{ provide: Router, useValue: new RouterStub() },
142190
{ provide: RequestService, useValue: jasmine.createSpyObj('requestService', ['removeByHrefSubstring']) },
143-
{ provide: PaginationService, useValue: paginationService }
191+
{ provide: PaginationService, useValue: paginationService },
192+
{ provide: DSONameService, useValue: jasmine.createSpyObj('dsoNameService', {
193+
getName: (dso: any) => dso?.name ?? dso?.email ?? dso?.id,
194+
}) },
144195
],
145196
schemas: [NO_ERRORS_SCHEMA]
146197
}).compileComponents();
@@ -150,7 +201,8 @@ describe('EPeopleRegistryComponent', () => {
150201
fixture = TestBed.createComponent(EPeopleRegistryComponent);
151202
component = fixture.componentInstance;
152203
modalService = (component as any).modalService;
153-
spyOn(modalService, 'open').and.returnValue(Object.assign({ componentInstance: Object.assign({ response: observableOf(true) }) }));
204+
modalRef = Object.assign({ componentInstance: Object.assign({ response: observableOf(true) }) });
205+
spyOn(modalService, 'open').and.returnValue(modalRef);
154206
fixture.detectChanges();
155207
});
156208

@@ -227,6 +279,126 @@ describe('EPeopleRegistryComponent', () => {
227279
});
228280
});
229281
});
282+
283+
it('should render the self delete button as disabled', () => {
284+
const deleteButtons = fixture.debugElement.queryAll(By.css('.access-control-deleteEPersonButton'));
285+
286+
expect(deleteButtons.length).toBe(2);
287+
expect(deleteButtons[0].nativeElement.getAttribute('aria-disabled')).toBeNull();
288+
expect(deleteButtons[1].nativeElement.getAttribute('aria-disabled')).toBe('true');
289+
expect(deleteButtons[1].nativeElement.classList.contains('disabled')).toBeTrue();
290+
});
291+
292+
it('should call submitter checks and compose the combined warning label', fakeAsync(() => {
293+
workspaceItemDataService.searchBy.and.returnValue(buildRemoteList([{} as any], 1));
294+
workflowItemDataService.searchBy.and.returnValue(buildRemoteList([], 0));
295+
searchService.search.and.returnValue(createSuccessfulRemoteDataObject$(buildSearchObjects(0)));
296+
// isAuthorized returns true by default -> the target is treated as an administrator
297+
modalRef.componentInstance.response = observableOf(false);
298+
299+
const deleteButtons = fixture.debugElement.queryAll(By.css('.access-control-deleteEPersonButton'));
300+
deleteButtons[0].triggerEventHandler('click', null);
301+
tick();
302+
303+
expect(workspaceItemDataService.searchBy).toHaveBeenCalledWith('findBySubmitter', jasmine.any(FindListOptions));
304+
expect(workflowItemDataService.searchBy).toHaveBeenCalledWith('findBySubmitter', jasmine.any(FindListOptions));
305+
expect(searchService.search).toHaveBeenCalled();
306+
expect(authorizationService.isAuthorized).toHaveBeenCalledWith(FeatureID.AdministratorOf, undefined, EPersonMock.id);
307+
expect(modalRef.componentInstance.warningLabel).toBe('admin.access-control.epeople.delete.warning.submitterAndAdmin');
308+
}));
309+
310+
it('should detect administrator via the authorization feature', fakeAsync(() => {
311+
workspaceItemDataService.searchBy.and.returnValue(buildRemoteList([], 0));
312+
workflowItemDataService.searchBy.and.returnValue(buildRemoteList([], 0));
313+
searchService.search.and.returnValue(createSuccessfulRemoteDataObject$(buildSearchObjects(0)));
314+
// admin -> true, all submitter probes empty -> admin-only warning
315+
(authorizationService.isAuthorized as jasmine.Spy).and.callFake((featureId: FeatureID) => observableOf(featureId === FeatureID.AdministratorOf));
316+
modalRef.componentInstance.response = observableOf(false);
317+
318+
const deleteButtons = fixture.debugElement.queryAll(By.css('.access-control-deleteEPersonButton'));
319+
deleteButtons[0].triggerEventHandler('click', null);
320+
tick();
321+
322+
expect(authorizationService.isAuthorized).toHaveBeenCalledWith(FeatureID.AdministratorOf, undefined, EPersonMock.id);
323+
expect(modalRef.componentInstance.warningLabel).toBe('admin.access-control.epeople.delete.warning.admin');
324+
}));
325+
326+
it('should still open the delete modal when a submitter probe errors (centralised catchError)', fakeAsync(() => {
327+
workspaceItemDataService.searchBy.and.returnValue(observableThrowError(() => new Error('boom')));
328+
workflowItemDataService.searchBy.and.returnValue(buildRemoteList([], 0));
329+
searchService.search.and.returnValue(createSuccessfulRemoteDataObject$(buildSearchObjects(0)));
330+
// CanDelete stays true so the button renders; AdministratorOf false so the only warning could come from submitter probes
331+
(authorizationService.isAuthorized as jasmine.Spy).and.callFake((featureId: FeatureID) => observableOf(featureId !== FeatureID.AdministratorOf));
332+
modalRef.componentInstance.response = observableOf(false);
333+
334+
const deleteButtons = fixture.debugElement.queryAll(By.css('.access-control-deleteEPersonButton'));
335+
deleteButtons[0].triggerEventHandler('click', null);
336+
tick();
337+
338+
expect(modalService.open).toHaveBeenCalled();
339+
expect(modalRef.componentInstance.warningLabel).toBeUndefined();
340+
}));
341+
342+
it('should show a friendly self-delete notification on backend 400 self-delete errors', fakeAsync(() => {
343+
modalRef.componentInstance.response = observableOf(true);
344+
ePersonDataServiceStub.deleteEPerson = jasmine.createSpy('deleteEPerson').and.returnValue(
345+
createFailedRemoteDataObject$('You, as admin user, cannot delete yourself', 400)
346+
);
347+
348+
const deleteButtons = fixture.debugElement.queryAll(By.css('.access-control-deleteEPersonButton'));
349+
deleteButtons[0].triggerEventHandler('click', null);
350+
tick();
351+
352+
expect(notificationsService.error).toHaveBeenCalled();
353+
let translatedKey: string;
354+
notificationsService.error.calls.mostRecent().args[0].subscribe((value) => translatedKey = value);
355+
expect(translatedKey).toBe('admin.access-control.epeople.notification.deleted.forbidden.self');
356+
}));
357+
358+
it('should use the deleted.failure key for generic delete failures', fakeAsync(() => {
359+
modalRef.componentInstance.response = observableOf(true);
360+
ePersonDataServiceStub.deleteEPerson = jasmine.createSpy('deleteEPerson').and.returnValue(
361+
createFailedRemoteDataObject$('server error', 500)
362+
);
363+
364+
const deleteButtons = fixture.debugElement.queryAll(By.css('.access-control-deleteEPersonButton'));
365+
deleteButtons[0].triggerEventHandler('click', null);
366+
tick();
367+
368+
expect(notificationsService.error).toHaveBeenCalled();
369+
let translatedKey: string;
370+
notificationsService.error.calls.mostRecent().args[0].subscribe((value) => translatedKey = value);
371+
expect(translatedKey).toBe('admin.access-control.epeople.notification.deleted.failure');
372+
}));
373+
374+
it('should not open delete modal before authenticated user id is resolved', fakeAsync(() => {
375+
component.currentAuthenticatedUserId = undefined;
376+
const deleteSpy = spyOn(ePersonDataServiceStub, 'deleteEPerson').and.callThrough();
377+
378+
const deleteButtons = fixture.debugElement.queryAll(By.css('.access-control-deleteEPersonButton'));
379+
deleteButtons[0].triggerEventHandler('click', null);
380+
tick();
381+
382+
expect(modalService.open).not.toHaveBeenCalled();
383+
expect(deleteSpy).not.toHaveBeenCalled();
384+
}));
385+
386+
it('should still show the friendly self-delete notification if the authenticated user id resolves late and the backend rejection carries no usable message', fakeAsync(() => {
387+
modalRef.componentInstance.response = observableOf(true);
388+
component.currentAuthenticatedUserId = EPersonMock.id;
389+
ePersonDataServiceStub.deleteEPerson = jasmine.createSpy('deleteEPerson').and.returnValue(defer(() => {
390+
component.currentAuthenticatedUserId = EPersonMock2.id;
391+
return createFailedRemoteDataObject$(undefined, 400);
392+
}));
393+
394+
component.deleteEPerson(EPersonMock2);
395+
tick();
396+
397+
expect(notificationsService.error).toHaveBeenCalled();
398+
let translatedKey: string;
399+
notificationsService.error.calls.mostRecent().args[0].subscribe((value) => translatedKey = value);
400+
expect(translatedKey).toBe('admin.access-control.epeople.notification.deleted.forbidden.self');
401+
}));
230402
});
231403

232404
describe('delete EPerson button when the isAuthorized returns false', () => {
@@ -237,12 +409,9 @@ describe('EPeopleRegistryComponent', () => {
237409
fixture.detectChanges();
238410
});
239411

240-
it('should be disabled', () => {
412+
it('should be hidden', () => {
241413
ePeopleDeleteButton = fixture.debugElement.queryAll(By.css('#epeople tr td div button.delete-button'));
242-
ePeopleDeleteButton.forEach((deleteButton: DebugElement) => {
243-
expect(deleteButton.nativeElement.getAttribute('aria-disabled')).toBe('true');
244-
expect(deleteButton.nativeElement.classList.contains('disabled')).toBeTrue();
245-
});
414+
expect(ePeopleDeleteButton.length).toBe(0);
246415
});
247416
});
248417
});

0 commit comments

Comments
 (0)