diff --git a/src/app/+item-page/edit-item-page/item-operation/item-operation.component.spec.ts b/src/app/+item-page/edit-item-page/item-operation/item-operation.component.spec.ts index 00e7f8452a..7570119b3a 100644 --- a/src/app/+item-page/edit-item-page/item-operation/item-operation.component.spec.ts +++ b/src/app/+item-page/edit-item-page/item-operation/item-operation.component.spec.ts @@ -28,19 +28,19 @@ describe('ItemOperationComponent', () => { }); it('should render operation row', () => { - const span = fixture.debugElement.query(By.css('span')).nativeElement; + const span = fixture.debugElement.query(By.css('.action-label span')).nativeElement; expect(span.textContent).toContain('item.edit.tabs.status.buttons.key1.label'); - const link = fixture.debugElement.query(By.css('a')).nativeElement; - expect(link.href).toContain('url1'); - expect(link.textContent).toContain('item.edit.tabs.status.buttons.key1.button'); + const button = fixture.debugElement.query(By.css('button')).nativeElement; + expect(button.textContent).toContain('item.edit.tabs.status.buttons.key1.button'); }); it('should render disabled operation row', () => { itemOperation.setDisabled(true); fixture.detectChanges(); - const span = fixture.debugElement.query(By.css('span')).nativeElement; + const span = fixture.debugElement.query(By.css('.action-label span')).nativeElement; expect(span.textContent).toContain('item.edit.tabs.status.buttons.key1.label'); - const span2 = fixture.debugElement.query(By.css('span.btn-danger')).nativeElement; - expect(span2.textContent).toContain('item.edit.tabs.status.buttons.key1.button'); + const button = fixture.debugElement.query(By.css('button')).nativeElement; + expect(button.disabled).toBeTrue(); + expect(button.textContent).toContain('item.edit.tabs.status.buttons.key1.button'); }); }); diff --git a/src/app/core/data/feature-authorization/feature-authorization-guard/dso-page-single-feature.guard.spec.ts b/src/app/core/data/feature-authorization/feature-authorization-guard/dso-page-single-feature.guard.spec.ts index 06677d1f22..6c1f330c69 100644 --- a/src/app/core/data/feature-authorization/feature-authorization-guard/dso-page-single-feature.guard.spec.ts +++ b/src/app/core/data/feature-authorization/feature-authorization-guard/dso-page-single-feature.guard.spec.ts @@ -9,9 +9,9 @@ import { FeatureID } from '../feature-id'; import { AuthService } from '../../../auth/auth.service'; /** - * Test implementation of abstract class DsoPageAdministratorGuard + * Test implementation of abstract class DsoPageSingleFeatureGuard */ -class DsoPageFeatureGuardImpl extends DsoPageSingleFeatureGuard { +class DsoPageSingleFeatureGuardImpl extends DsoPageSingleFeatureGuard { constructor(protected resolver: Resolve>, protected authorizationService: AuthorizationDataService, protected router: Router, @@ -25,7 +25,7 @@ class DsoPageFeatureGuardImpl extends DsoPageSingleFeatureGuard { } } -describe('DsoPageAdministratorGuard', () => { +describe('DsoPageSingleFeatureGuard', () => { let guard: DsoPageSingleFeatureGuard; let authorizationService: AuthorizationDataService; let router: Router; @@ -62,7 +62,7 @@ describe('DsoPageAdministratorGuard', () => { }, parent: parentRoute }; - guard = new DsoPageFeatureGuardImpl(resolver, authorizationService, router, authService, undefined); + guard = new DsoPageSingleFeatureGuardImpl(resolver, authorizationService, router, authService, undefined); } beforeEach(() => { diff --git a/src/app/core/data/feature-authorization/feature-authorization-guard/dso-page-some-feature.guard.spec.ts b/src/app/core/data/feature-authorization/feature-authorization-guard/dso-page-some-feature.guard.spec.ts new file mode 100644 index 0000000000..071b1b0731 --- /dev/null +++ b/src/app/core/data/feature-authorization/feature-authorization-guard/dso-page-some-feature.guard.spec.ts @@ -0,0 +1,87 @@ +import { AuthorizationDataService } from '../authorization-data.service'; +import { ActivatedRouteSnapshot, Resolve, Router, RouterStateSnapshot } from '@angular/router'; +import { RemoteData } from '../../remote-data'; +import { Observable, of as observableOf } from 'rxjs'; +import { createSuccessfulRemoteDataObject$ } from '../../../../shared/remote-data.utils'; +import { DSpaceObject } from '../../../shared/dspace-object.model'; +import { FeatureID } from '../feature-id'; +import { AuthService } from '../../../auth/auth.service'; +import { DsoPageSomeFeatureGuard } from './dso-page-some-feature.guard'; + +/** + * Test implementation of abstract class DsoPageSomeFeatureGuard + */ +class DsoPageSomeFeatureGuardImpl extends DsoPageSomeFeatureGuard { + constructor(protected resolver: Resolve>, + protected authorizationService: AuthorizationDataService, + protected router: Router, + protected authService: AuthService, + protected featureIDs: FeatureID[]) { + super(resolver, authorizationService, router, authService); + } + + getFeatureIDs(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { + return observableOf(this.featureIDs); + } +} + +describe('DsoPageSomeFeatureGuard', () => { + let guard: DsoPageSomeFeatureGuard; + let authorizationService: AuthorizationDataService; + let router: Router; + let authService: AuthService; + let resolver: Resolve>; + let object: DSpaceObject; + let route; + let parentRoute; + + function init() { + object = { + self: 'test-selflink' + } as DSpaceObject; + + authorizationService = jasmine.createSpyObj('authorizationService', { + isAuthorized: observableOf(true) + }); + router = jasmine.createSpyObj('router', { + parseUrl: {} + }); + resolver = jasmine.createSpyObj('resolver', { + resolve: createSuccessfulRemoteDataObject$(object) + }); + authService = jasmine.createSpyObj('authService', { + isAuthenticated: observableOf(true) + }); + parentRoute = { + params: { + id: '3e1a5327-dabb-41ff-af93-e6cab9d032f0' + } + }; + route = { + params: { + }, + parent: parentRoute + }; + guard = new DsoPageSomeFeatureGuardImpl(resolver, authorizationService, router, authService, []); + } + + beforeEach(() => { + init(); + }); + + describe('getObjectUrl', () => { + it('should return the resolved object\'s selflink', (done) => { + guard.getObjectUrl(route, undefined).subscribe((selflink) => { + expect(selflink).toEqual(object.self); + done(); + }); + }); + }); + + describe('getRouteWithDSOId', () => { + it('should return the route that has the UUID of the DSO', () => { + const foundRoute = (guard as any).getRouteWithDSOId(route); + expect(foundRoute).toBe(parentRoute); + }); + }); +}); diff --git a/src/app/core/data/feature-authorization/feature-authorization-guard/dso-page-some-feature.guard.ts b/src/app/core/data/feature-authorization/feature-authorization-guard/dso-page-some-feature.guard.ts index 7b7cb4c196..8683709345 100644 --- a/src/app/core/data/feature-authorization/feature-authorization-guard/dso-page-some-feature.guard.ts +++ b/src/app/core/data/feature-authorization/feature-authorization-guard/dso-page-some-feature.guard.ts @@ -10,7 +10,7 @@ import { hasNoValue, hasValue } from '../../../../shared/empty.util'; import { SomeFeatureAuthorizationGuard } from './some-feature-authorization.guard'; /** - * Abstract Guard for preventing unauthorized access to {@link DSpaceObject} pages that require rights for a specific feature + * Abstract Guard for preventing unauthorized access to {@link DSpaceObject} pages that require rights for any specific feature in a list * This guard utilizes a resolver to retrieve the relevant object to check authorizations for */ export abstract class DsoPageSomeFeatureGuard extends SomeFeatureAuthorizationGuard { diff --git a/src/app/core/data/feature-authorization/feature-authorization-guard/single-feature-authorization.guard.spec.ts b/src/app/core/data/feature-authorization/feature-authorization-guard/single-feature-authorization.guard.spec.ts index 1fa5498f12..635aa3530b 100644 --- a/src/app/core/data/feature-authorization/feature-authorization-guard/single-feature-authorization.guard.spec.ts +++ b/src/app/core/data/feature-authorization/feature-authorization-guard/single-feature-authorization.guard.spec.ts @@ -6,10 +6,10 @@ import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/ro import { AuthService } from '../../../auth/auth.service'; /** - * Test implementation of abstract class FeatureAuthorizationGuard + * Test implementation of abstract class SingleFeatureAuthorizationGuard * Provide the return values of the overwritten getters as constructor arguments */ -class FeatureAuthorizationGuardImpl extends SingleFeatureAuthorizationGuard { +class SingleFeatureAuthorizationGuardImpl extends SingleFeatureAuthorizationGuard { constructor(protected authorizationService: AuthorizationDataService, protected router: Router, protected authService: AuthService, @@ -32,7 +32,7 @@ class FeatureAuthorizationGuardImpl extends SingleFeatureAuthorizationGuard { } } -describe('FeatureAuthorizationGuard', () => { +describe('SingleFeatureAuthorizationGuard', () => { let guard: SingleFeatureAuthorizationGuard; let authorizationService: AuthorizationDataService; let router: Router; @@ -56,7 +56,7 @@ describe('FeatureAuthorizationGuard', () => { authService = jasmine.createSpyObj('authService', { isAuthenticated: observableOf(true) }); - guard = new FeatureAuthorizationGuardImpl(authorizationService, router, authService, featureId, objectUrl, ePersonUuid); + guard = new SingleFeatureAuthorizationGuardImpl(authorizationService, router, authService, featureId, objectUrl, ePersonUuid); } beforeEach(() => { diff --git a/src/app/core/data/feature-authorization/feature-authorization-guard/some-feature-authorization.guard.spec.ts b/src/app/core/data/feature-authorization/feature-authorization-guard/some-feature-authorization.guard.spec.ts new file mode 100644 index 0000000000..90153d2d14 --- /dev/null +++ b/src/app/core/data/feature-authorization/feature-authorization-guard/some-feature-authorization.guard.spec.ts @@ -0,0 +1,110 @@ +import { AuthorizationDataService } from '../authorization-data.service'; +import { FeatureID } from '../feature-id'; +import { Observable, of as observableOf } from 'rxjs'; +import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router'; +import { AuthService } from '../../../auth/auth.service'; +import { SomeFeatureAuthorizationGuard } from './some-feature-authorization.guard'; + +/** + * Test implementation of abstract class SomeFeatureAuthorizationGuard + * Provide the return values of the overwritten getters as constructor arguments + */ +class SomeFeatureAuthorizationGuardImpl extends SomeFeatureAuthorizationGuard { + constructor(protected authorizationService: AuthorizationDataService, + protected router: Router, + protected authService: AuthService, + protected featureIds: FeatureID[], + protected objectUrl: string, + protected ePersonUuid: string) { + super(authorizationService, router, authService); + } + + getFeatureIDs(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { + return observableOf(this.featureIds); + } + + getObjectUrl(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { + return observableOf(this.objectUrl); + } + + getEPersonUuid(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { + return observableOf(this.ePersonUuid); + } +} + +describe('SomeFeatureAuthorizationGuard', () => { + let guard: SomeFeatureAuthorizationGuard; + let authorizationService: AuthorizationDataService; + let router: Router; + let authService: AuthService; + + let featureIds: FeatureID[]; + let authorizedFeatureIds: FeatureID[]; + let objectUrl: string; + let ePersonUuid: string; + + function init() { + featureIds = [FeatureID.LoginOnBehalfOf, FeatureID.CanDelete]; + authorizedFeatureIds = []; + objectUrl = 'fake-object-url'; + ePersonUuid = 'fake-eperson-uuid'; + + authorizationService = Object.assign({ + isAuthorized(featureId?: FeatureID): Observable { + return observableOf(authorizedFeatureIds.indexOf(featureId) > -1); + } + }); + router = jasmine.createSpyObj('router', { + parseUrl: {} + }); + authService = jasmine.createSpyObj('authService', { + isAuthenticated: observableOf(true) + }); + guard = new SomeFeatureAuthorizationGuardImpl(authorizationService, router, authService, featureIds, objectUrl, ePersonUuid); + } + + beforeEach(() => { + init(); + }); + + describe('canActivate', () => { + describe('when the user isn\'t authorized', () => { + beforeEach(() => { + authorizedFeatureIds = []; + }); + + it('should not return true', (done) => { + guard.canActivate(undefined, { url: 'current-url' } as any).subscribe((result) => { + expect(result).not.toEqual(true); + done(); + }); + }); + }); + + describe('when the user is authorized for at least one of the guard\'s features', () => { + beforeEach(() => { + authorizedFeatureIds = [featureIds[0]]; + }); + + it('should return true', (done) => { + guard.canActivate(undefined, { url: 'current-url' } as any).subscribe((result) => { + expect(result).toEqual(true); + done(); + }); + }); + }); + + describe('when the user is authorized for all of the guard\'s features', () => { + beforeEach(() => { + authorizedFeatureIds = featureIds; + }); + + it('should return true', (done) => { + guard.canActivate(undefined, { url: 'current-url' } as any).subscribe((result) => { + expect(result).toEqual(true); + done(); + }); + }); + }); + }); +}); diff --git a/src/app/shared/mocks/dspace-rest/mocks/mock-feature-item-can-delete-none-response.json b/src/app/shared/mocks/dspace-rest/mocks/mock-feature-item-can-delete-none-response.json new file mode 100644 index 0000000000..51968bd5da --- /dev/null +++ b/src/app/shared/mocks/dspace-rest/mocks/mock-feature-item-can-delete-none-response.json @@ -0,0 +1,13 @@ +{ + "_links": { + "self": { + "href": "https://api7.dspace.org/server/api/authz/authorizations/search/object?uri=https://api7.dspace.org/server/api/core/items/96715576-3748-4761-ad45-001646632963&feature=canDelete" + } + }, + "page": { + "size": 20, + "totalElements": 0, + "totalPages": 1, + "number": 0 + } +} diff --git a/src/app/shared/mocks/dspace-rest/mocks/mock-feature-item-can-move-response.json b/src/app/shared/mocks/dspace-rest/mocks/mock-feature-item-can-move-response.json new file mode 100644 index 0000000000..692751d671 --- /dev/null +++ b/src/app/shared/mocks/dspace-rest/mocks/mock-feature-item-can-move-response.json @@ -0,0 +1,50 @@ +{ + "_embedded": { + "authorizations": [ + { + "id": "cd824a61-95be-4e16-bccd-51fea26707d0_canMove_core.item_96715576-3748-4761-ad45-001646632963", + "type": "authorization", + "_links": { + "eperson": { + "href": "https://api7.dspace.org/server/api/authz/authorizations/cd824a61-95be-4e16-bccd-51fea26707d0_canMove_core.item_96715576-3748-4761-ad45-001646632963/eperson" + }, + "feature": { + "href": "https://api7.dspace.org/server/api/authz/authorizations/cd824a61-95be-4e16-bccd-51fea26707d0_canMove_core.item_96715576-3748-4761-ad45-001646632963/feature" + }, + "object": { + "href": "https://api7.dspace.org/server/api/authz/authorizations/cd824a61-95be-4e16-bccd-51fea26707d0_canMove_core.item_96715576-3748-4761-ad45-001646632963/object" + }, + "self": { + "href": "https://api7.dspace.org/server/api/authz/authorizations/cd824a61-95be-4e16-bccd-51fea26707d0_canMove_core.item_96715576-3748-4761-ad45-001646632963" + } + }, + "_embedded": { + "feature": { + "id": "canMove", + "description": "It can be used to verify if the user is allowed to move items", + "type": "feature", + "resourcetypes": [ + "core.item" + ], + "_links": { + "self": { + "href": "https://api7.dspace.org/server/api/authz/features/canMove" + } + } + } + } + } + ] + }, + "_links": { + "self": { + "href": "https://api7.dspace.org/server/api/authz/authorizations/search/object?uri=https://api7.dspace.org/server/api/core/items/96715576-3748-4761-ad45-001646632963&feature=canMove" + } + }, + "page": { + "size": 20, + "totalElements": 1, + "totalPages": 1, + "number": 0 + } +} diff --git a/src/app/shared/mocks/dspace-rest/mocks/response-map.mock.ts b/src/app/shared/mocks/dspace-rest/mocks/response-map.mock.ts index 4cc5c39b7d..f276c24bbf 100644 --- a/src/app/shared/mocks/dspace-rest/mocks/response-map.mock.ts +++ b/src/app/shared/mocks/dspace-rest/mocks/response-map.mock.ts @@ -3,6 +3,8 @@ import { InjectionToken } from '@angular/core'; // import mockPublicationResponse from './mock-publication-response.json'; // import mockUntypedItemResponse from './mock-untyped-item-response.json'; import mockFeatureItemCanManageBitstreamsResponse from './mock-feature-item-can-manage-bitstreams-response.json'; +import mockFeatureItemCanMoveResponse from './mock-feature-item-can-move-response.json'; +import mockFeatureItemCanDeleteNoneResponse from './mock-feature-item-can-delete-none-response.json'; export class ResponseMapMock extends Map {} @@ -18,4 +20,6 @@ export const mockResponseMap: ResponseMapMock = new Map([ // [ '/api/pid/find', mockPublicationResponse ], // [ '/api/pid/find', mockUntypedItemResponse ], [ 'https://api7.dspace.org/server/api/authz/authorizations/search/object?uri=https://api7.dspace.org/server/api/core/items/96715576-3748-4761-ad45-001646632963&feature=canManageBitstreams&embed=feature', mockFeatureItemCanManageBitstreamsResponse ], + [ 'https://api7.dspace.org/server/api/authz/authorizations/search/object?uri=https://api7.dspace.org/server/api/core/items/96715576-3748-4761-ad45-001646632963&feature=canMove&embed=feature', mockFeatureItemCanMoveResponse ], + [ 'https://api7.dspace.org/server/api/authz/authorizations/search/object?uri=https://api7.dspace.org/server/api/core/items/96715576-3748-4761-ad45-001646632963&feature=canDelete&embed=feature', mockFeatureItemCanDeleteNoneResponse ], ]);