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
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Router } from '@angular/router';
import { Observable, of as observableOf, throwError as observableThrowError } from 'rxjs';
import { defer, Observable, of as observableOf, throwError as observableThrowError } from 'rxjs';
import { CommonModule } from '@angular/common';
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing';
Expand Down Expand Up @@ -382,6 +382,27 @@ describe('EPeopleRegistryComponent', () => {
expect(modalService.open).not.toHaveBeenCalled();
expect(deleteSpy).not.toHaveBeenCalled();
}));

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(() => {
// Simulates the authenticated-user subscription resolving after the click (so the
// pre-flight self-delete check is bypassed) combined with a backend response whose error
// message can't be pattern-matched (e.g. Spring Boot's default message suppression).
// The self-delete notification must still win over the generic failure one.
modalRef.componentInstance.response = observableOf(true);
component.currentAuthenticatedUserId = EPersonMock.id;
ePersonDataServiceStub.deleteEPerson = jasmine.createSpy('deleteEPerson').and.returnValue(defer(() => {
component.currentAuthenticatedUserId = EPersonMock2.id;
return createFailedRemoteDataObject$(undefined, 400);
}));

component.deleteEPerson(EPersonMock2);
tick();

expect(notificationsService.error).toHaveBeenCalled();
let translatedKey: string;
notificationsService.error.calls.mostRecent().args[0].subscribe((value) => translatedKey = value);
expect(translatedKey).toBe('admin.access-control.epeople.notification.deleted.forbidden.self');
}));
});

describe('delete EPerson button when the isAuthorized returns false', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy {
this.epersonService.deleteEPerson(ePerson).pipe(getFirstCompletedRemoteData()).subscribe((restResponse: RemoteData<NoContent>) => {
if (restResponse.hasSucceeded) {
this.notificationsService.success(this.translateService.get(this.labelPrefix + 'notification.deleted.success', {name: this.dsoNameService.getName(ePerson)}));
} else if (this.deleteGuard.isSelfDeletionError(restResponse)) {
} else if (this.isCurrentUser(ePerson) || this.deleteGuard.isSelfDeletionError(restResponse)) {
this.deleteGuard.showSelfDeleteNotification();
} else {
this.notificationsService.error(this.translateService.get(this.labelPrefix + 'notification.deleted.failure', {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,9 @@ export class EPersonDeleteGuardService {

/**
* Whether the backend rejected the delete because an admin tried to delete themselves.
* Best-effort only: Spring Boot omits the exception message from the response body by
* default, so callers should treat this as a fallback alongside a client-side identity
* check rather than the sole signal.
*/
isSelfDeletionError(restResponse: RemoteData<NoContent> | null): boolean {
return restResponse?.statusCode === 400 && restResponse?.errorMessage?.toLowerCase().includes('cannot delete yourself');
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Observable, of as observableOf, throwError as observableThrowError } from 'rxjs';
import { defer, Observable, of as observableOf, throwError as observableThrowError } from 'rxjs';
import { FeatureID } from '../../../core/data/feature-authorization/feature-id';
import { EPersonDeleteGuardService } from '../eperson-delete-guard.service';
import { CommonModule } from '@angular/common';
Expand Down Expand Up @@ -610,6 +610,25 @@ describe('EPersonFormComponent', () => {
expect(modalService.open).not.toHaveBeenCalled();
expect(deleteSpy).not.toHaveBeenCalled();
});

it('should still show the friendly self-delete notification if the authenticated user id resolves late and the backend rejection carries no usable message', () => {
// Simulates the authenticated-user subscription resolving after the modal was confirmed
// (so the pre-flight self-delete check was bypassed) combined with a backend response
// whose error message can't be pattern-matched (e.g. Spring Boot's default message
// suppression). The self-delete notification must still win over the generic failure one.
spyOn(component.epersonService, 'deleteEPerson').and.returnValue(defer(() => {
component.currentAuthenticatedUserId = eperson.id;
return createFailedRemoteDataObject$(undefined, 400);
}));

const deleteButton = fixture.debugElement.query(By.css('.delete-button'));
deleteButton.triggerEventHandler('click', null);

expect(notificationsService.error).toHaveBeenCalled();
let translatedKey: string;
notificationsService.error.calls.mostRecent().args[0].subscribe((value) => translatedKey = value);
expect(translatedKey).toBe('admin.access-control.epeople.notification.deleted.forbidden.self');
});
});

describe('self delete button', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -535,7 +535,7 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
if (restResponse?.hasSucceeded) {
this.notificationsService.success(this.translateService.get(this.labelPrefix + 'notification.deleted.success', { name: this.dsoNameService.getName(eperson) }));
void this.router.navigate([getEPersonsRoute()]);
} else if (this.deleteGuard.isSelfDeletionError(restResponse)) {
} else if (this.isCurrentUser(eperson) || this.deleteGuard.isSelfDeletionError(restResponse)) {
this.deleteGuard.showSelfDeleteNotification();
} else {
this.notificationsService.error(this.translateService.get(this.labelPrefix + 'notification.deleted.failure', {
Expand Down
Loading