11import { Router } from '@angular/router' ;
2- import { Observable , of as observableOf } from 'rxjs' ;
2+ import { defer , Observable , of as observableOf , throwError as observableThrowError } from 'rxjs' ;
33import { CommonModule } from '@angular/common' ;
4- import { DebugElement , NO_ERRORS_SCHEMA } from '@angular/core' ;
4+ import { NO_ERRORS_SCHEMA } from '@angular/core' ;
55import { ComponentFixture , fakeAsync , TestBed , tick , waitForAsync } from '@angular/core/testing' ;
66import { FormsModule , ReactiveFormsModule } from '@angular/forms' ;
77import { BrowserModule , By } from '@angular/platform-browser' ;
88import { NgbModule } from '@ng-bootstrap/ng-bootstrap' ;
99import { TranslateLoader , TranslateModule , TranslateService } from '@ngx-translate/core' ;
1010import { buildPaginatedList , PaginatedList } from '../../core/data/paginated-list.model' ;
1111import { RemoteData } from '../../core/data/remote-data' ;
12+ import { AuthService } from '../../core/auth/auth.service' ;
1213import { EPersonDataService } from '../../core/eperson/eperson-data.service' ;
1314import { EPerson } from '../../core/eperson/models/eperson.model' ;
1415import { PageInfo } from '../../core/shared/page-info.model' ;
16+ import { DSONameService } from '../../core/breadcrumbs/dso-name.service' ;
1517import { FormBuilderService } from '../../shared/form/builder/form-builder.service' ;
1618import { NotificationsService } from '../../shared/notifications/notifications.service' ;
1719import { EPeopleRegistryComponent } from './epeople-registry.component' ;
1820import { EPersonMock , EPersonMock2 } from '../../shared/testing/eperson.mock' ;
19- import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils' ;
21+ import { createFailedRemoteDataObject$ , createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils' ;
2022import { getMockFormBuilderService } from '../../shared/mocks/form-builder-service.mock' ;
2123import { getMockTranslateService } from '../../shared/mocks/translate.service.mock' ;
2224import { TranslateLoaderMock } from '../../shared/mocks/translate-loader.mock' ;
2325import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub' ;
2426import { RouterStub } from '../../shared/testing/router.stub' ;
2527import { 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' ;
2630import { RequestService } from '../../core/data/request.service' ;
2731import { PaginationService } from '../../core/pagination/pagination.service' ;
2832import { 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' ;
2935import { FindListOptions } from '../../core/data/find-list-options.model' ;
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' ;
3039
3140describe ( 'EPeopleRegistryComponent' , ( ) => {
3241 let component : EPeopleRegistryComponent ;
@@ -37,10 +46,35 @@ describe('EPeopleRegistryComponent', () => {
3746 let mockEPeople ;
3847 let ePersonDataServiceStub : any ;
3948 let authorizationService : AuthorizationDataService ;
49+ let authService : jasmine . SpyObj < AuthService > ;
50+ let workspaceItemDataService : jasmine . SpyObj < WorkspaceitemDataService > ;
51+ let workflowItemDataService : jasmine . SpyObj < WorkflowItemDataService > ;
52+ let searchService : jasmine . SpyObj < SearchService > ;
53+ let notificationsService : NotificationsServiceStub ;
4054 let modalService ;
55+ let modalRef ;
4156
4257 let paginationService ;
4358
59+ const buildRemoteList = < T > ( items : T [ ] , totalElements = items . length ) => createSuccessfulRemoteDataObject$ (
60+ buildPaginatedList ( new PageInfo ( {
61+ elementsPerPage : items . length || 1 ,
62+ totalElements,
63+ totalPages : 1 ,
64+ currentPage : 1
65+ } ) , items )
66+ ) ;
67+
68+ const buildSearchObjects = ( totalElements : number ) => Object . assign (
69+ new SearchObjects < DSpaceObject > ( ) ,
70+ buildPaginatedList ( new PageInfo ( {
71+ elementsPerPage : 1 ,
72+ totalElements,
73+ totalPages : 1 ,
74+ currentPage : 1
75+ } ) , [ ] )
76+ ) ;
77+
4478 beforeEach ( waitForAsync ( ( ) => {
4579 jasmine . getEnv ( ) . allowRespy ( true ) ;
4680 mockEPeople = [ EPersonMock , EPersonMock2 ] ;
@@ -118,8 +152,17 @@ describe('EPeopleRegistryComponent', () => {
118152 authorizationService = jasmine . createSpyObj ( 'authorizationService' , {
119153 isAuthorized : observableOf ( true )
120154 } ) ;
155+ authService = jasmine . createSpyObj ( 'authService' , [ 'getAuthenticatedUserFromStore' ] ) ;
156+ authService . getAuthenticatedUserFromStore . and . returnValue ( observableOf ( EPersonMock2 ) ) ;
157+ workspaceItemDataService = jasmine . createSpyObj ( 'workspaceItemDataService' , [ 'searchBy' ] ) ;
158+ workspaceItemDataService . searchBy . and . returnValue ( buildRemoteList ( [ ] , 0 ) ) ;
159+ workflowItemDataService = jasmine . createSpyObj ( 'workflowItemDataService' , [ 'searchBy' ] ) ;
160+ workflowItemDataService . searchBy . and . returnValue ( buildRemoteList ( [ ] , 0 ) ) ;
161+ searchService = jasmine . createSpyObj ( 'searchService' , [ 'search' ] ) ;
162+ searchService . search . and . returnValue ( createSuccessfulRemoteDataObject$ ( buildSearchObjects ( 0 ) ) ) ;
121163 builderService = getMockFormBuilderService ( ) ;
122164 translateService = getMockTranslateService ( ) ;
165+ notificationsService = new NotificationsServiceStub ( ) ;
123166
124167 paginationService = new PaginationServiceStub ( ) ;
125168 TestBed . configureTestingModule ( {
@@ -134,12 +177,20 @@ describe('EPeopleRegistryComponent', () => {
134177 declarations : [ EPeopleRegistryComponent ] ,
135178 providers : [
136179 { provide : EPersonDataService , useValue : ePersonDataServiceStub } ,
137- { provide : NotificationsService , useValue : new NotificationsServiceStub ( ) } ,
180+ { provide : NotificationsService , useValue : notificationsService } ,
138181 { provide : AuthorizationDataService , useValue : authorizationService } ,
182+ { provide : AuthService , useValue : authService } ,
139183 { provide : FormBuilderService , useValue : builderService } ,
184+ { provide : WorkspaceitemDataService , useValue : workspaceItemDataService } ,
185+ { provide : WorkflowItemDataService , useValue : workflowItemDataService } ,
186+ { provide : SearchService , useValue : searchService } ,
187+ EPersonDeleteGuardService ,
140188 { provide : Router , useValue : new RouterStub ( ) } ,
141189 { provide : RequestService , useValue : jasmine . createSpyObj ( 'requestService' , [ 'removeByHrefSubstring' ] ) } ,
142- { provide : PaginationService , useValue : paginationService }
190+ { provide : PaginationService , useValue : paginationService } ,
191+ { provide : DSONameService , useValue : jasmine . createSpyObj ( 'dsoNameService' , {
192+ getName : ( dso : any ) => dso ?. name ?? dso ?. email ?? dso ?. id ,
193+ } ) } ,
143194 ] ,
144195 schemas : [ NO_ERRORS_SCHEMA ]
145196 } ) . compileComponents ( ) ;
@@ -149,7 +200,8 @@ describe('EPeopleRegistryComponent', () => {
149200 fixture = TestBed . createComponent ( EPeopleRegistryComponent ) ;
150201 component = fixture . componentInstance ;
151202 modalService = ( component as any ) . modalService ;
152- spyOn ( modalService , 'open' ) . and . returnValue ( Object . assign ( { componentInstance : Object . assign ( { response : observableOf ( true ) } ) } ) ) ;
203+ modalRef = Object . assign ( { componentInstance : Object . assign ( { response : observableOf ( true ) } ) } ) ;
204+ spyOn ( modalService , 'open' ) . and . returnValue ( modalRef ) ;
153205 fixture . detectChanges ( ) ;
154206 } ) ;
155207
@@ -226,6 +278,125 @@ describe('EPeopleRegistryComponent', () => {
226278 } ) ;
227279 } ) ;
228280 } ) ;
281+
282+ it ( 'should render the self delete button as disabled' , ( ) => {
283+ const deleteButtons = fixture . debugElement . queryAll ( By . css ( '.access-control-deleteEPersonButton' ) ) ;
284+
285+ expect ( deleteButtons . length ) . toBe ( 2 ) ;
286+ expect ( deleteButtons [ 0 ] . nativeElement . disabled ) . toBeFalse ( ) ;
287+ expect ( deleteButtons [ 1 ] . nativeElement . disabled ) . toBeTrue ( ) ;
288+ } ) ;
289+
290+ it ( 'should call submitter checks and compose the combined warning label' , fakeAsync ( ( ) => {
291+ workspaceItemDataService . searchBy . and . returnValue ( buildRemoteList ( [ { } as any ] , 1 ) ) ;
292+ workflowItemDataService . searchBy . and . returnValue ( buildRemoteList ( [ ] , 0 ) ) ;
293+ searchService . search . and . returnValue ( createSuccessfulRemoteDataObject$ ( buildSearchObjects ( 0 ) ) ) ;
294+ // isAuthorized returns true by default -> the target is treated as an administrator
295+ modalRef . componentInstance . response = observableOf ( false ) ;
296+
297+ const deleteButtons = fixture . debugElement . queryAll ( By . css ( '.access-control-deleteEPersonButton' ) ) ;
298+ deleteButtons [ 0 ] . triggerEventHandler ( 'click' , null ) ;
299+ tick ( ) ;
300+
301+ expect ( workspaceItemDataService . searchBy ) . toHaveBeenCalledWith ( 'findBySubmitter' , jasmine . any ( FindListOptions ) ) ;
302+ expect ( workflowItemDataService . searchBy ) . toHaveBeenCalledWith ( 'findBySubmitter' , jasmine . any ( FindListOptions ) ) ;
303+ expect ( searchService . search ) . toHaveBeenCalled ( ) ;
304+ expect ( authorizationService . isAuthorized ) . toHaveBeenCalledWith ( FeatureID . AdministratorOf , undefined , EPersonMock . id ) ;
305+ expect ( modalRef . componentInstance . warningLabel ) . toBe ( 'admin.access-control.epeople.delete.warning.submitterAndAdmin' ) ;
306+ } ) ) ;
307+
308+ it ( 'should detect administrator via the authorization feature' , fakeAsync ( ( ) => {
309+ workspaceItemDataService . searchBy . and . returnValue ( buildRemoteList ( [ ] , 0 ) ) ;
310+ workflowItemDataService . searchBy . and . returnValue ( buildRemoteList ( [ ] , 0 ) ) ;
311+ searchService . search . and . returnValue ( createSuccessfulRemoteDataObject$ ( buildSearchObjects ( 0 ) ) ) ;
312+ // admin -> true, all submitter probes empty -> admin-only warning
313+ ( authorizationService . isAuthorized as jasmine . Spy ) . and . callFake ( ( featureId : FeatureID ) => observableOf ( featureId === FeatureID . AdministratorOf ) ) ;
314+ modalRef . componentInstance . response = observableOf ( false ) ;
315+
316+ const deleteButtons = fixture . debugElement . queryAll ( By . css ( '.access-control-deleteEPersonButton' ) ) ;
317+ deleteButtons [ 0 ] . triggerEventHandler ( 'click' , null ) ;
318+ tick ( ) ;
319+
320+ expect ( authorizationService . isAuthorized ) . toHaveBeenCalledWith ( FeatureID . AdministratorOf , undefined , EPersonMock . id ) ;
321+ expect ( modalRef . componentInstance . warningLabel ) . toBe ( 'admin.access-control.epeople.delete.warning.admin' ) ;
322+ } ) ) ;
323+
324+ it ( 'should still open the delete modal when a submitter probe errors (centralised catchError)' , fakeAsync ( ( ) => {
325+ workspaceItemDataService . searchBy . and . returnValue ( observableThrowError ( ( ) => new Error ( 'boom' ) ) ) ;
326+ workflowItemDataService . searchBy . and . returnValue ( buildRemoteList ( [ ] , 0 ) ) ;
327+ searchService . search . and . returnValue ( createSuccessfulRemoteDataObject$ ( buildSearchObjects ( 0 ) ) ) ;
328+ // CanDelete stays true so the button renders; AdministratorOf false so the only warning could come from submitter probes
329+ ( authorizationService . isAuthorized as jasmine . Spy ) . and . callFake ( ( featureId : FeatureID ) => observableOf ( featureId !== FeatureID . AdministratorOf ) ) ;
330+ modalRef . componentInstance . response = observableOf ( false ) ;
331+
332+ const deleteButtons = fixture . debugElement . queryAll ( By . css ( '.access-control-deleteEPersonButton' ) ) ;
333+ deleteButtons [ 0 ] . triggerEventHandler ( 'click' , null ) ;
334+ tick ( ) ;
335+
336+ expect ( modalService . open ) . toHaveBeenCalled ( ) ;
337+ expect ( modalRef . componentInstance . warningLabel ) . toBeUndefined ( ) ;
338+ } ) ) ;
339+
340+ it ( 'should show a friendly self-delete notification on backend 400 self-delete errors' , fakeAsync ( ( ) => {
341+ modalRef . componentInstance . response = observableOf ( true ) ;
342+ ePersonDataServiceStub . deleteEPerson = jasmine . createSpy ( 'deleteEPerson' ) . and . returnValue (
343+ createFailedRemoteDataObject$ ( 'You, as admin user, cannot delete yourself' , 400 )
344+ ) ;
345+
346+ const deleteButtons = fixture . debugElement . queryAll ( By . css ( '.access-control-deleteEPersonButton' ) ) ;
347+ deleteButtons [ 0 ] . triggerEventHandler ( 'click' , null ) ;
348+ tick ( ) ;
349+
350+ expect ( notificationsService . error ) . toHaveBeenCalled ( ) ;
351+ let translatedKey : string ;
352+ notificationsService . error . calls . mostRecent ( ) . args [ 0 ] . subscribe ( ( value ) => translatedKey = value ) ;
353+ expect ( translatedKey ) . toBe ( 'admin.access-control.epeople.notification.deleted.forbidden.self' ) ;
354+ } ) ) ;
355+
356+ it ( 'should use the deleted.failure key for generic delete failures' , fakeAsync ( ( ) => {
357+ modalRef . componentInstance . response = observableOf ( true ) ;
358+ ePersonDataServiceStub . deleteEPerson = jasmine . createSpy ( 'deleteEPerson' ) . and . returnValue (
359+ createFailedRemoteDataObject$ ( 'server error' , 500 )
360+ ) ;
361+
362+ const deleteButtons = fixture . debugElement . queryAll ( By . css ( '.access-control-deleteEPersonButton' ) ) ;
363+ deleteButtons [ 0 ] . triggerEventHandler ( 'click' , null ) ;
364+ tick ( ) ;
365+
366+ expect ( notificationsService . error ) . toHaveBeenCalled ( ) ;
367+ let translatedKey : string ;
368+ notificationsService . error . calls . mostRecent ( ) . args [ 0 ] . subscribe ( ( value ) => translatedKey = value ) ;
369+ expect ( translatedKey ) . toBe ( 'admin.access-control.epeople.notification.deleted.failure' ) ;
370+ } ) ) ;
371+
372+ it ( 'should not open delete modal before authenticated user id is resolved' , fakeAsync ( ( ) => {
373+ component . currentAuthenticatedUserId = undefined ;
374+ const deleteSpy = spyOn ( ePersonDataServiceStub , 'deleteEPerson' ) . and . callThrough ( ) ;
375+
376+ const deleteButtons = fixture . debugElement . queryAll ( By . css ( '.access-control-deleteEPersonButton' ) ) ;
377+ deleteButtons [ 0 ] . triggerEventHandler ( 'click' , null ) ;
378+ tick ( ) ;
379+
380+ expect ( modalService . open ) . not . toHaveBeenCalled ( ) ;
381+ expect ( deleteSpy ) . not . toHaveBeenCalled ( ) ;
382+ } ) ) ;
383+
384+ 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 ( ( ) => {
385+ modalRef . componentInstance . response = observableOf ( true ) ;
386+ component . currentAuthenticatedUserId = EPersonMock . id ;
387+ ePersonDataServiceStub . deleteEPerson = jasmine . createSpy ( 'deleteEPerson' ) . and . returnValue ( defer ( ( ) => {
388+ component . currentAuthenticatedUserId = EPersonMock2 . id ;
389+ return createFailedRemoteDataObject$ ( undefined , 400 ) ;
390+ } ) ) ;
391+
392+ component . deleteEPerson ( EPersonMock2 ) ;
393+ tick ( ) ;
394+
395+ expect ( notificationsService . error ) . toHaveBeenCalled ( ) ;
396+ let translatedKey : string ;
397+ notificationsService . error . calls . mostRecent ( ) . args [ 0 ] . subscribe ( ( value ) => translatedKey = value ) ;
398+ expect ( translatedKey ) . toBe ( 'admin.access-control.epeople.notification.deleted.forbidden.self' ) ;
399+ } ) ) ;
229400 } ) ;
230401
231402 describe ( 'delete EPerson button when the isAuthorized returns false' , ( ) => {
@@ -236,11 +407,9 @@ describe('EPeopleRegistryComponent', () => {
236407 fixture . detectChanges ( ) ;
237408 } ) ;
238409
239- it ( 'should be disabled ' , ( ) => {
410+ it ( 'should be hidden ' , ( ) => {
240411 ePeopleDeleteButton = fixture . debugElement . queryAll ( By . css ( '#epeople tr td div button.delete-button' ) ) ;
241- ePeopleDeleteButton . forEach ( ( deleteButton : DebugElement ) => {
242- expect ( deleteButton . nativeElement . disabled ) . toBe ( true ) ;
243- } ) ;
412+ expect ( ePeopleDeleteButton . length ) . toBe ( 0 ) ;
244413 } ) ;
245414 } ) ;
246415} ) ;
0 commit comments