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
Expand Up @@ -76,11 +76,25 @@ <h2 id="search" class="border-bottom pb-2">
title="{{labelPrefix + 'table.edit.buttons.edit' | translate: { name: dsoNameService.getName(epersonDto.eperson) } }}">
<i class="fas fa-edit fa-fw"></i>
</button>
<button *ngIf="epersonDto.ableToDelete" (click)="deleteEPerson(epersonDto.eperson)"
class="delete-button btn btn-outline-danger btn-sm access-control-deleteEPersonButton"
title="{{labelPrefix + 'table.edit.buttons.remove' | translate: { name: dsoNameService.getName(epersonDto.eperson) } }}">
<i class="fas fa-trash-alt fa-fw"></i>
</button>
<ng-container *ngIf="epersonDto.ableToDelete && currentAuthenticatedUserId">
<ng-container *ngIf="isCurrentUser(epersonDto.eperson); else enabledDeleteButton">
<span tabindex="0" [ngbTooltip]="selfDeleteWarningLabel | translate">
<button [dsBtnDisabled]="true"
tabindex="-1"
class="delete-button btn btn-outline-danger btn-sm access-control-deleteEPersonButton"
type="button">
<i class="fas fa-trash-alt fa-fw"></i>
</button>
</span>
</ng-container>
</ng-container>
<ng-template #enabledDeleteButton>
<button (click)="deleteEPerson(epersonDto.eperson)"
class="delete-button btn btn-outline-danger btn-sm access-control-deleteEPersonButton"
title="{{labelPrefix + 'table.edit.buttons.remove' | translate: { name: dsoNameService.getName(epersonDto.eperson) } }}">
<i class="fas fa-trash-alt fa-fw"></i>
</button>
</ng-template>
</div>
</td>
</tr>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,22 +1,26 @@
import { Router } from '@angular/router';
import { Observable, of as observableOf } from 'rxjs';
import { CommonModule } from '@angular/common';
import { DebugElement, NO_ERRORS_SCHEMA } from '@angular/core';
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { BrowserModule, By } from '@angular/platform-browser';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { TranslateLoader, TranslateModule, TranslateService } from '@ngx-translate/core';
import { buildPaginatedList, PaginatedList } from '../../core/data/paginated-list.model';
import { RemoteData } from '../../core/data/remote-data';
import { AuthService } from '../../core/auth/auth.service';
import { EPersonDataService } from '../../core/eperson/eperson-data.service';
import { GroupDataService } from '../../core/eperson/group-data.service';
import { EPerson } from '../../core/eperson/models/eperson.model';
import { Group } from '../../core/eperson/models/group.model';
import { PageInfo } from '../../core/shared/page-info.model';
import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
import { FormBuilderService } from '../../shared/form/builder/form-builder.service';
import { NotificationsService } from '../../shared/notifications/notifications.service';
import { EPeopleRegistryComponent } from './epeople-registry.component';
import { EPersonMock, EPersonMock2 } from '../../shared/testing/eperson.mock';
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
import { createFailedRemoteDataObject$, createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
import { getMockFormBuilderService } from '../../shared/mocks/form-builder-service.mock';
import { getMockTranslateService } from '../../shared/mocks/translate.service.mock';
import { TranslateLoaderMock } from '../../shared/mocks/translate-loader.mock';
Expand All @@ -26,8 +30,13 @@ import { AuthorizationDataService } from '../../core/data/feature-authorization/
import { RequestService } from '../../core/data/request.service';
import { PaginationService } from '../../core/pagination/pagination.service';
import { PaginationServiceStub } from '../../shared/testing/pagination-service.stub';
import { WorkspaceitemDataService } from '../../core/submission/workspaceitem-data.service';
import { WorkflowItemDataService } from '../../core/submission/workflowitem-data.service';
import { FindListOptions } from '../../core/data/find-list-options.model';
import {BtnDisabledDirective} from '../../shared/btn-disabled.directive';
import { SearchService } from '../../core/shared/search/search.service';
import { DSpaceObject } from '../../core/shared/dspace-object.model';
import { SearchObjects } from '../../shared/search/models/search-objects.model';
import { BtnDisabledDirective } from '../../shared/btn-disabled.directive';

describe('EPeopleRegistryComponent', () => {
let component: EPeopleRegistryComponent;
Expand All @@ -38,10 +47,36 @@ describe('EPeopleRegistryComponent', () => {
let mockEPeople;
let ePersonDataServiceStub: any;
let authorizationService: AuthorizationDataService;
let authService: jasmine.SpyObj<AuthService>;
let groupDataService: jasmine.SpyObj<GroupDataService>;
let workspaceItemDataService: jasmine.SpyObj<WorkspaceitemDataService>;
let workflowItemDataService: jasmine.SpyObj<WorkflowItemDataService>;
let searchService: jasmine.SpyObj<SearchService>;
let notificationsService: NotificationsServiceStub;
let modalService;
let modalRef;

let paginationService;

const buildRemoteList = <T>(items: T[], totalElements = items.length) => createSuccessfulRemoteDataObject$(
buildPaginatedList(new PageInfo({
elementsPerPage: items.length || 1,
totalElements,
totalPages: 1,
currentPage: 1
}), items)
);

const buildSearchObjects = (totalElements: number) => Object.assign(
new SearchObjects<DSpaceObject>(),
buildPaginatedList(new PageInfo({
elementsPerPage: 1,
totalElements,
totalPages: 1,
currentPage: 1
}), [])
);

beforeEach(waitForAsync(() => {
jasmine.getEnv().allowRespy(true);
mockEPeople = [EPersonMock, EPersonMock2];
Expand Down Expand Up @@ -119,8 +154,19 @@ describe('EPeopleRegistryComponent', () => {
authorizationService = jasmine.createSpyObj('authorizationService', {
isAuthorized: observableOf(true)
});
authService = jasmine.createSpyObj('authService', ['getAuthenticatedUserFromStore']);
authService.getAuthenticatedUserFromStore.and.returnValue(observableOf(EPersonMock2));
groupDataService = jasmine.createSpyObj('groupDataService', ['findListByHref']);
groupDataService.findListByHref.and.returnValue(buildRemoteList([], 0));
workspaceItemDataService = jasmine.createSpyObj('workspaceItemDataService', ['searchBy']);
workspaceItemDataService.searchBy.and.returnValue(buildRemoteList([], 0));
workflowItemDataService = jasmine.createSpyObj('workflowItemDataService', ['searchBy']);
workflowItemDataService.searchBy.and.returnValue(buildRemoteList([], 0));
searchService = jasmine.createSpyObj('searchService', ['search']);
searchService.search.and.returnValue(createSuccessfulRemoteDataObject$(buildSearchObjects(0)));
builderService = getMockFormBuilderService();
translateService = getMockTranslateService();
notificationsService = new NotificationsServiceStub();

paginationService = new PaginationServiceStub();
TestBed.configureTestingModule({
Expand All @@ -135,12 +181,20 @@ describe('EPeopleRegistryComponent', () => {
declarations: [EPeopleRegistryComponent, BtnDisabledDirective],
providers: [
{ provide: EPersonDataService, useValue: ePersonDataServiceStub },
{ provide: NotificationsService, useValue: new NotificationsServiceStub() },
{ provide: NotificationsService, useValue: notificationsService },
{ provide: AuthorizationDataService, useValue: authorizationService },
{ provide: AuthService, useValue: authService },
{ provide: GroupDataService, useValue: groupDataService },
{ provide: FormBuilderService, useValue: builderService },
{ provide: WorkspaceitemDataService, useValue: workspaceItemDataService },
{ provide: WorkflowItemDataService, useValue: workflowItemDataService },
{ provide: SearchService, useValue: searchService },
{ provide: Router, useValue: new RouterStub() },
{ provide: RequestService, useValue: jasmine.createSpyObj('requestService', ['removeByHrefSubstring']) },
{ provide: PaginationService, useValue: paginationService }
{ provide: PaginationService, useValue: paginationService },
{ provide: DSONameService, useValue: jasmine.createSpyObj('dsoNameService', {
getName: (dso: any) => dso?.name ?? dso?.email ?? dso?.id,
}) },
],
schemas: [NO_ERRORS_SCHEMA]
}).compileComponents();
Expand All @@ -150,7 +204,8 @@ describe('EPeopleRegistryComponent', () => {
fixture = TestBed.createComponent(EPeopleRegistryComponent);
component = fixture.componentInstance;
modalService = (component as any).modalService;
spyOn(modalService, 'open').and.returnValue(Object.assign({ componentInstance: Object.assign({ response: observableOf(true) }) }));
modalRef = Object.assign({ componentInstance: Object.assign({ response: observableOf(true) }) });
spyOn(modalService, 'open').and.returnValue(modalRef);
fixture.detectChanges();
});

Expand Down Expand Up @@ -227,6 +282,111 @@ describe('EPeopleRegistryComponent', () => {
});
});
});

it('should render the self delete button as disabled', () => {
const deleteButtons = fixture.debugElement.queryAll(By.css('.access-control-deleteEPersonButton'));

expect(deleteButtons.length).toBe(2);
expect(deleteButtons[0].nativeElement.getAttribute('aria-disabled')).toBeNull();
expect(deleteButtons[1].nativeElement.getAttribute('aria-disabled')).toBe('true');
expect(deleteButtons[1].nativeElement.classList.contains('disabled')).toBeTrue();
});

it('should call submitter checks and compose the combined warning label', fakeAsync(() => {
const adminGroup = Object.assign(new Group(), { _name: 'Administrator' });
workspaceItemDataService.searchBy.and.returnValue(buildRemoteList([{} as any], 1));
workflowItemDataService.searchBy.and.returnValue(buildRemoteList([], 0));
searchService.search.and.returnValue(createSuccessfulRemoteDataObject$(buildSearchObjects(0)));
groupDataService.findListByHref.and.returnValue(buildRemoteList([adminGroup], 1));
modalRef.componentInstance.response = observableOf(false);

const deleteButtons = fixture.debugElement.queryAll(By.css('.access-control-deleteEPersonButton'));
deleteButtons[0].triggerEventHandler('click', null);
tick();

expect(workspaceItemDataService.searchBy).toHaveBeenCalledWith('findBySubmitter', jasmine.any(FindListOptions));
expect(workflowItemDataService.searchBy).toHaveBeenCalledWith('findBySubmitter', jasmine.any(FindListOptions));
expect(searchService.search).toHaveBeenCalled();
expect(groupDataService.findListByHref).toHaveBeenCalledWith(EPersonMock._links.groups.href, jasmine.any(FindListOptions));
expect(modalRef.componentInstance.warningLabel).toBe('admin.access-control.epeople.delete.warning.submitterAndAdmin');
}));

it('should detect administrator membership on later pages', fakeAsync(() => {
const adminGroup = Object.assign(new Group(), { _name: 'Administrator' });
const firstPage = createSuccessfulRemoteDataObject$(
buildPaginatedList(new PageInfo({
elementsPerPage: 100,
totalElements: 101,
totalPages: 2,
currentPage: 1,
}), [Object.assign(new Group(), { _name: 'Regular Group' })])
);
const secondPage = createSuccessfulRemoteDataObject$(
buildPaginatedList(new PageInfo({
elementsPerPage: 100,
totalElements: 101,
totalPages: 2,
currentPage: 2,
}), [adminGroup])
);

workspaceItemDataService.searchBy.and.returnValue(buildRemoteList([], 0));
workflowItemDataService.searchBy.and.returnValue(buildRemoteList([], 0));
searchService.search.and.returnValue(createSuccessfulRemoteDataObject$(buildSearchObjects(0)));
groupDataService.findListByHref.and.returnValues(firstPage, secondPage);
modalRef.componentInstance.response = observableOf(false);

const deleteButtons = fixture.debugElement.queryAll(By.css('.access-control-deleteEPersonButton'));
deleteButtons[0].triggerEventHandler('click', null);
tick();

expect(groupDataService.findListByHref).toHaveBeenCalledTimes(2);
expect(modalRef.componentInstance.warningLabel).toBe('admin.access-control.epeople.delete.warning.admin');
}));

it('should show a friendly self-delete notification on backend 400 self-delete errors', fakeAsync(() => {
modalRef.componentInstance.response = observableOf(true);
ePersonDataServiceStub.deleteEPerson = jasmine.createSpy('deleteEPerson').and.returnValue(
createFailedRemoteDataObject$('You, as admin user, cannot delete yourself', 400)
);

const deleteButtons = fixture.debugElement.queryAll(By.css('.access-control-deleteEPersonButton'));
deleteButtons[0].triggerEventHandler('click', null);
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');
}));

it('should use the deleted.failure key for generic delete failures', fakeAsync(() => {
modalRef.componentInstance.response = observableOf(true);
ePersonDataServiceStub.deleteEPerson = jasmine.createSpy('deleteEPerson').and.returnValue(
createFailedRemoteDataObject$('server error', 500)
);

const deleteButtons = fixture.debugElement.queryAll(By.css('.access-control-deleteEPersonButton'));
deleteButtons[0].triggerEventHandler('click', null);
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.failure');
}));

it('should not open delete modal before authenticated user id is resolved', fakeAsync(() => {
component.currentAuthenticatedUserId = undefined;
const deleteSpy = spyOn(ePersonDataServiceStub, 'deleteEPerson').and.callThrough();

const deleteButtons = fixture.debugElement.queryAll(By.css('.access-control-deleteEPersonButton'));
deleteButtons[0].triggerEventHandler('click', null);
tick();

expect(modalService.open).not.toHaveBeenCalled();
expect(deleteSpy).not.toHaveBeenCalled();
}));
});

describe('delete EPerson button when the isAuthorized returns false', () => {
Expand All @@ -237,12 +397,9 @@ describe('EPeopleRegistryComponent', () => {
fixture.detectChanges();
});

it('should be disabled', () => {
it('should be hidden', () => {
ePeopleDeleteButton = fixture.debugElement.queryAll(By.css('#epeople tr td div button.delete-button'));
ePeopleDeleteButton.forEach((deleteButton: DebugElement) => {
expect(deleteButton.nativeElement.getAttribute('aria-disabled')).toBe('true');
expect(deleteButton.nativeElement.classList.contains('disabled')).toBeTrue();
});
expect(ePeopleDeleteButton.length).toBe(0);
});
});
});
Loading
Loading