mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 01:54:15 +00:00
[CST4981] finished task, working on unit testing
This commit is contained in:
@@ -21,6 +21,7 @@ import { MenuService } from '../../shared/menu/menu.service';
|
||||
import { CSSVariableService } from '../../shared/sass-helper/sass-helper.service';
|
||||
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
|
||||
import { FeatureID } from '../../core/data/feature-authorization/feature-id';
|
||||
import { Router, ActivatedRoute } from '@angular/router';
|
||||
|
||||
/**
|
||||
* Component representing the admin sidebar
|
||||
@@ -67,10 +68,11 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
|
||||
private variableService: CSSVariableService,
|
||||
private authService: AuthService,
|
||||
private modalService: NgbModal,
|
||||
private authorizationService: AuthorizationDataService,
|
||||
public authorizationService: AuthorizationDataService,
|
||||
private scriptDataService: ScriptDataService,
|
||||
public route: ActivatedRoute
|
||||
) {
|
||||
super(menuService, injector);
|
||||
super(menuService, injector, authorizationService, route);
|
||||
this.inFocus$ = new BehaviorSubject(false);
|
||||
}
|
||||
|
||||
|
@@ -72,6 +72,7 @@ import { ThemedCollectionPageComponent } from './themed-collection-page.componen
|
||||
id: 'statistics_collection_:id',
|
||||
active: true,
|
||||
visible: true,
|
||||
type: 'statistics',
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.statistics',
|
||||
|
@@ -55,6 +55,7 @@ import { ThemedCommunityPageComponent } from './themed-community-page.component'
|
||||
id: 'statistics_community_:id',
|
||||
active: true,
|
||||
visible: true,
|
||||
type: 'statistics',
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.statistics',
|
||||
|
@@ -0,0 +1,28 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { SingleFeatureAuthorizationGuard } from './single-feature-authorization.guard';
|
||||
import { AuthorizationDataService } from '../authorization-data.service';
|
||||
import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router';
|
||||
import { AuthService } from '../../../auth/auth.service';
|
||||
import { Observable, of as observableOf } from 'rxjs';
|
||||
import { FeatureID } from '../feature-id';
|
||||
import { tap } from 'rxjs/operators';
|
||||
|
||||
/**
|
||||
* Prevent unauthorized activating and loading of routes when the current authenticated user doesn't have group
|
||||
* management rights
|
||||
*/
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class StatisticsAdministratorGuard extends SingleFeatureAuthorizationGuard {
|
||||
constructor(protected authorizationService: AuthorizationDataService, protected router: Router, protected authService: AuthService) {
|
||||
super(authorizationService, router, authService);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check group management rights
|
||||
*/
|
||||
getFeatureID(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<FeatureID> {
|
||||
return observableOf(FeatureID.CanViewUsageStatistics);
|
||||
}
|
||||
}
|
@@ -25,4 +25,5 @@ export enum FeatureID {
|
||||
CanEditVersion = 'canEditVersion',
|
||||
CanDeleteVersion = 'canDeleteVersion',
|
||||
CanCreateVersion = 'canCreateVersion',
|
||||
CanViewUsageStatistics = 'canViewUsageStatistics',
|
||||
}
|
||||
|
@@ -21,6 +21,7 @@ import { ThemedHomePageComponent } from './themed-home-page.component';
|
||||
active: true,
|
||||
visible: true,
|
||||
index: 2,
|
||||
type: 'statistics',
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.statistics',
|
||||
|
@@ -58,6 +58,7 @@ import { REQUEST_COPY_MODULE_PATH } from '../app-routing-paths';
|
||||
id: 'statistics_item_:id',
|
||||
active: true,
|
||||
visible: true,
|
||||
type: 'statistics',
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.statistics',
|
||||
|
@@ -1,4 +1,3 @@
|
||||
<div class="nav-item navbar-section">
|
||||
<ng-container
|
||||
*ngComponentOutlet="(sectionMap$ | async).get(section.id).component; injector: (sectionMap$ | async).get(section.id).injector;"></ng-container>
|
||||
<ng-container *ngComponentOutlet="(sectionMap$ | async).get(section.id).component; injector: (sectionMap$ | async).get(section.id).injector;"></ng-container>
|
||||
</div>
|
@@ -1,18 +1,15 @@
|
||||
<nav [ngClass]="{'open': !(menuCollapsed | async)}"
|
||||
[@slideMobileNav]="!(windowService.isXsOrSm() | async) ? 'default' : ((menuCollapsed | async) ? 'collapsed' : 'expanded')"
|
||||
class="navbar navbar-light navbar-expand-md p-md-0 navbar-container"
|
||||
role="navigation" [attr.aria-label]="'nav.main.description' | translate"> <!-- TODO remove navbar-container class when https://github.com/twbs/bootstrap/issues/24726 is fixed -->
|
||||
<nav [ngClass]="{'open': !(menuCollapsed | async)}" [@slideMobileNav]="!(windowService.isXsOrSm() | async) ? 'default' : ((menuCollapsed | async) ? 'collapsed' : 'expanded')"
|
||||
class="navbar navbar-light navbar-expand-md p-md-0 navbar-container" role="navigation" [attr.aria-label]="'nav.main.description' | translate">
|
||||
<!-- TODO remove navbar-container class when https://github.com/twbs/bootstrap/issues/24726 is fixed -->
|
||||
<div class="container">
|
||||
<div class="reset-padding-md w-100">
|
||||
<div id="collapsingNav">
|
||||
<ul class="navbar-nav mr-auto shadow-none">
|
||||
<ng-container *ngFor="let section of (sections | async)">
|
||||
<ng-container
|
||||
*ngComponentOutlet="(sectionMap$ | async).get(section.id).component; injector: (sectionMap$ | async).get(section.id).injector;"></ng-container>
|
||||
<ng-container *ngComponentOutlet="(sectionMap$ | async).get(section.id)?.component; injector: (sectionMap$ | async).get(section.id).injector;"></ng-container>
|
||||
</ng-container>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
|
@@ -7,6 +7,11 @@ import { TextMenuItemModel } from '../shared/menu/menu-item/models/text.model';
|
||||
import { LinkMenuItemModel } from '../shared/menu/menu-item/models/link.model';
|
||||
import { HostWindowService } from '../shared/host-window.service';
|
||||
import { environment } from '../../environments/environment';
|
||||
import { AuthorizationDataService } from 'src/app/core/data/feature-authorization/authorization-data.service';
|
||||
import { Router, ActivatedRoute } from '@angular/router';
|
||||
import { map, take } from 'rxjs/operators';
|
||||
import { RemoteData } from '../core/data/remote-data';
|
||||
import { Collection } from 'src/app/core/shared/collection.model';
|
||||
|
||||
/**
|
||||
* Component representing the public navbar
|
||||
@@ -26,9 +31,11 @@ export class NavbarComponent extends MenuComponent {
|
||||
|
||||
constructor(protected menuService: MenuService,
|
||||
protected injector: Injector,
|
||||
public windowService: HostWindowService
|
||||
public windowService: HostWindowService,
|
||||
public authorizationService: AuthorizationDataService,
|
||||
public route: ActivatedRoute
|
||||
) {
|
||||
super(menuService, injector);
|
||||
super(menuService, injector, authorizationService, route);
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
|
@@ -7,9 +7,12 @@ import { MenuComponent } from './menu.component';
|
||||
import { MenuServiceStub } from '../testing/menu-service.stub';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { MenuSection } from './menu.reducer';
|
||||
import { Router } from '@angular/router';
|
||||
import { Router, ActivatedRoute } from '@angular/router';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { MenuID } from './initial-menus-state';
|
||||
import { AuthorizationDataService } from 'src/app/core/data/feature-authorization/authorization-data.service';
|
||||
import { createSuccessfulRemoteDataObject } from 'src/app/shared/remote-data.utils';
|
||||
import { Item } from '../../core/shared/item.model';
|
||||
|
||||
describe('MenuComponent', () => {
|
||||
let comp: MenuComponent;
|
||||
@@ -19,13 +22,38 @@ describe('MenuComponent', () => {
|
||||
|
||||
const mockMenuID = 'mock-menuID' as MenuID;
|
||||
|
||||
const authorizationService = jasmine.createSpyObj('authorizationService', {
|
||||
isAuthorized: observableOf(true)
|
||||
});
|
||||
|
||||
const mockItem = Object.assign(new Item(), {
|
||||
id: 'fake-id',
|
||||
uuid: 'fake-id',
|
||||
handle: 'fake/handle',
|
||||
lastModified: '2018',
|
||||
_links: {
|
||||
self: {
|
||||
href: 'https://localhost:8000/items/fake-id'
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const routeStub = {
|
||||
data: observableOf({
|
||||
dso: createSuccessfulRemoteDataObject(mockItem)
|
||||
}),
|
||||
children: []
|
||||
};
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [TranslateModule.forRoot(), NoopAnimationsModule, RouterTestingModule],
|
||||
declarations: [MenuComponent],
|
||||
providers: [
|
||||
Injector,
|
||||
{ provide: MenuService, useClass: MenuServiceStub }
|
||||
{ provide: MenuService, useClass: MenuServiceStub },
|
||||
{ provide: AuthorizationDataService, useValue: authorizationService },
|
||||
{ provide: ActivatedRoute, useValue: routeStub },
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
}).overrideComponent(MenuComponent, {
|
||||
@@ -95,4 +123,38 @@ describe('MenuComponent', () => {
|
||||
expect(menuService.collapseMenuPreview).toHaveBeenCalledWith(comp.menuID);
|
||||
}));
|
||||
});
|
||||
|
||||
describe('when unauthorized statistics', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
comp.sections = observableOf([{ 'id': 'browse_global_communities_and_collections', 'active': false, 'visible': true, 'index': 0, 'model': { 'type': 1, 'text': 'menu.section.browse_global_communities_and_collections', 'link': '/community-list' }, 'shouldPersistOnRouteChange': true }, { 'id': 'browse_global', 'active': false, 'visible': true, 'index': 1, 'model': { 'type': 0, 'text': 'menu.section.browse_global' }, 'shouldPersistOnRouteChange': true }, { 'id': 'statistics_site', 'active': true, 'visible': true, 'index': 2, 'type': 'statistics', 'model': { 'type': 1, 'text': 'menu.section.statistics', 'link': 'statistics' } }]);
|
||||
authorizationService.isAuthorized().and.returnValue(observableOf(false));
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('when authorized statistics', (done => {
|
||||
comp.sections.subscribe((sections) => {
|
||||
expect(sections.length).toEqual(2);
|
||||
done();
|
||||
});
|
||||
}));
|
||||
});
|
||||
|
||||
describe('get authorized statistics', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
comp.sections = observableOf([{ 'id': 'browse_global_communities_and_collections', 'active': false, 'visible': true, 'index': 0, 'model': { 'type': 1, 'text': 'menu.section.browse_global_communities_and_collections', 'link': '/community-list' }, 'shouldPersistOnRouteChange': true }, { 'id': 'browse_global', 'active': false, 'visible': true, 'index': 1, 'model': { 'type': 0, 'text': 'menu.section.browse_global' }, 'shouldPersistOnRouteChange': true }, { 'id': 'statistics_site', 'active': true, 'visible': true, 'index': 2, 'type': 'statistics', 'model': { 'type': 1, 'text': 'menu.section.statistics', 'link': 'statistics' } }]);
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('get authorized statistics', (done => {
|
||||
comp.sections.subscribe((sections) => {
|
||||
expect(sections.length).toEqual(3);
|
||||
done();
|
||||
});
|
||||
}));
|
||||
});
|
||||
|
||||
|
||||
|
||||
});
|
||||
|
@@ -1,14 +1,17 @@
|
||||
import { ChangeDetectionStrategy, Component, Injector, OnDestroy, OnInit } from '@angular/core';
|
||||
import { BehaviorSubject, Observable, Subscription } from 'rxjs';
|
||||
import { BehaviorSubject, Observable, Subscription, of as observableOf } from 'rxjs';
|
||||
import { MenuService } from './menu.service';
|
||||
import { MenuID } from './initial-menus-state';
|
||||
import { MenuSection } from './menu.reducer';
|
||||
import { distinctUntilChanged, map, switchMap } from 'rxjs/operators';
|
||||
import { distinctUntilChanged, map, switchMap, mergeMap, tap, isEmpty } from 'rxjs/operators';
|
||||
import { GenericConstructor } from '../../core/shared/generic-constructor';
|
||||
import { hasValue } from '../empty.util';
|
||||
import { hasValue, isNotEmpty, hasValueOperator, isNotEmptyOperator } from '../empty.util';
|
||||
import { MenuSectionComponent } from './menu-section/menu-section.component';
|
||||
import { getComponentForMenu } from './menu-section.decorator';
|
||||
import { compareArraysUsingIds } from '../../item-page/simple/item-types/shared/item-relationships-utils';
|
||||
import { AuthorizationDataService } from 'src/app/core/data/feature-authorization/authorization-data.service';
|
||||
import { FeatureID } from 'src/app/core/data/feature-authorization/feature-id';
|
||||
import { Router, ActivatedRoute } from '@angular/router';
|
||||
|
||||
/**
|
||||
* A basic implementation of a MenuComponent
|
||||
@@ -67,22 +70,34 @@ export class MenuComponent implements OnInit, OnDestroy {
|
||||
*/
|
||||
subs: Subscription[] = [];
|
||||
|
||||
constructor(protected menuService: MenuService, protected injector: Injector) {
|
||||
private activatedRouteLastChild: ActivatedRoute;
|
||||
|
||||
constructor(protected menuService: MenuService, protected injector: Injector, public authorizationService: AuthorizationDataService, public route: ActivatedRoute) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets all instance variables to their initial values
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
this.activatedRouteLastChild = this.getActivatedRoute(this.route);
|
||||
this.menuCollapsed = this.menuService.isMenuCollapsed(this.menuID);
|
||||
this.menuPreviewCollapsed = this.menuService.isMenuPreviewCollapsed(this.menuID);
|
||||
this.menuVisible = this.menuService.isMenuVisible(this.menuID);
|
||||
this.sections = this.menuService.getMenuTopSections(this.menuID).pipe(distinctUntilChanged(compareArraysUsingIds()));
|
||||
|
||||
this.subs.push(
|
||||
this.sections.pipe(
|
||||
tap(t => console.log(t)),
|
||||
// if you return an array from a switchMap it will emit each element as a separate event.
|
||||
// So this switchMap is equivalent to a subscribe with a forEach inside
|
||||
switchMap((sections: MenuSection[]) => sections),
|
||||
switchMap((section: MenuSection) => {
|
||||
if (section.id.includes('statistics')) {
|
||||
return this.getAuthorizedStatistics(section);
|
||||
}
|
||||
return observableOf(section);
|
||||
}),
|
||||
isNotEmptyOperator(),
|
||||
switchMap((section: MenuSection) => this.getSectionComponent(section).pipe(
|
||||
map((component: GenericConstructor<MenuSectionComponent>) => ({ section, component }))
|
||||
)),
|
||||
@@ -98,6 +113,43 @@ export class MenuComponent implements OnInit, OnDestroy {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get activated route of the deepest activated route
|
||||
*/
|
||||
getActivatedRoute(route) {
|
||||
if (route.children.length > 0) {
|
||||
return this.getActivatedRoute(route.firstChild);
|
||||
} else {
|
||||
return route;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get section of statistics after checking authorization
|
||||
*/
|
||||
getAuthorizedStatistics(section) {
|
||||
return this.activatedRouteLastChild.data.pipe(
|
||||
switchMap((data) => {
|
||||
return this.authorizationService.isAuthorized(FeatureID.CanViewUsageStatistics, this.getObjectUrl(data)).pipe(
|
||||
map((canViewUsageStatistics: boolean) => {
|
||||
if (!canViewUsageStatistics) {
|
||||
return {};
|
||||
} else {
|
||||
return section;
|
||||
}
|
||||
}));
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get statistics route dso data
|
||||
*/
|
||||
getObjectUrl(data) {
|
||||
const object = data.site ? data.site : data.dso.payload;
|
||||
return object._links.self.href;
|
||||
}
|
||||
|
||||
/**
|
||||
* Collapse this menu when it's currently expanded, expand it when its currently collapsed
|
||||
* @param {Event} event The user event that triggered this method
|
||||
|
@@ -72,7 +72,6 @@ export class MenuEffects {
|
||||
const last: boolean = hasNoValue(route.firstChild);
|
||||
|
||||
if (hasValue(data) && hasValue(data.menu) && hasValue(data.menu[menuID])) {
|
||||
|
||||
let menuSections: MenuSection[] | MenuSection = data.menu[menuID];
|
||||
menuSections = this.resolveSubstitutions(menuSections, params);
|
||||
|
||||
@@ -120,4 +119,5 @@ export class MenuEffects {
|
||||
}
|
||||
return resolved;
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -10,6 +10,7 @@ import { ThemedCommunityStatisticsPageComponent } from './community-statistics-p
|
||||
import { ThemedItemStatisticsPageComponent } from './item-statistics-page/themed-item-statistics-page.component';
|
||||
import { ThemedSiteStatisticsPageComponent } from './site-statistics-page/themed-site-statistics-page.component';
|
||||
import { ItemResolver } from '../item-page/item.resolver';
|
||||
import { StatisticsAdministratorGuard } from 'src/app/core/data/feature-authorization/feature-authorization-guard/statistics-administrator.guard';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
@@ -29,7 +30,8 @@ import { ItemResolver } from '../item-page/item.resolver';
|
||||
path: '',
|
||||
component: ThemedSiteStatisticsPageComponent,
|
||||
},
|
||||
]
|
||||
],
|
||||
canActivate: [StatisticsAdministratorGuard]
|
||||
},
|
||||
{
|
||||
path: `items/:id`,
|
||||
@@ -42,6 +44,7 @@ import { ItemResolver } from '../item-page/item.resolver';
|
||||
breadcrumbKey: 'statistics'
|
||||
},
|
||||
component: ThemedItemStatisticsPageComponent,
|
||||
canActivate: [StatisticsAdministratorGuard]
|
||||
},
|
||||
{
|
||||
path: `collections/:id`,
|
||||
@@ -54,6 +57,7 @@ import { ItemResolver } from '../item-page/item.resolver';
|
||||
breadcrumbKey: 'statistics'
|
||||
},
|
||||
component: ThemedCollectionStatisticsPageComponent,
|
||||
canActivate: [StatisticsAdministratorGuard]
|
||||
},
|
||||
{
|
||||
path: `communities/:id`,
|
||||
@@ -66,6 +70,7 @@ import { ItemResolver } from '../item-page/item.resolver';
|
||||
breadcrumbKey: 'statistics'
|
||||
},
|
||||
component: ThemedCommunityStatisticsPageComponent,
|
||||
canActivate: [StatisticsAdministratorGuard]
|
||||
},
|
||||
]
|
||||
)
|
||||
|
@@ -15,6 +15,6 @@ getTestBed().initTestEnvironment(
|
||||
platformBrowserDynamicTesting()
|
||||
);
|
||||
// Then we find all the tests.
|
||||
const context = require.context('./', true, /\.spec\.ts$/);
|
||||
const context = require.context('./app/shared/menu', true, /\.spec\.ts$/);
|
||||
// And load the modules.
|
||||
context.keys().map(context);
|
||||
|
@@ -1,7 +1,5 @@
|
||||
<nav [ngClass]="{'open': !(menuCollapsed | async)}"
|
||||
[@slideMobileNav]="!(windowService.isXsOrSm() | async) ? 'default' : ((menuCollapsed | async) ? 'collapsed' : 'expanded')"
|
||||
class="navbar navbar-expand-md navbar-light p-0 navbar-container"
|
||||
role="navigation" [attr.aria-label]="'nav.main.description' | translate">
|
||||
<nav [ngClass]="{'open': !(menuCollapsed | async)}" [@slideMobileNav]="!(windowService.isXsOrSm() | async) ? 'default' : ((menuCollapsed | async) ? 'collapsed' : 'expanded')"
|
||||
class="navbar navbar-expand-md navbar-light p-0 navbar-container" role="navigation" [attr.aria-label]="'nav.main.description' | translate">
|
||||
<div class="container h-100">
|
||||
<a class="navbar-brand my-2" routerLink="/home">
|
||||
<img src="assets/images/dspace-logo.svg" [attr.alt]="'menu.header.image.logo' | translate" />
|
||||
@@ -10,8 +8,7 @@
|
||||
<div id="collapsingNav" class="w-100 h-100">
|
||||
<ul class="navbar-nav me-auto mb-2 mb-lg-0 h-100">
|
||||
<ng-container *ngFor="let section of (sections | async)">
|
||||
<ng-container
|
||||
*ngComponentOutlet="(sectionMap$ | async).get(section.id).component; injector: (sectionMap$ | async).get(section.id).injector;"></ng-container>
|
||||
<ng-container *ngComponentOutlet="(sectionMap$ | async).get(section.id)?.component; injector: (sectionMap$ | async).get(section.id)?.injector;"></ng-container>
|
||||
</ng-container>
|
||||
</ul>
|
||||
</div>
|
||||
@@ -21,4 +18,3 @@
|
||||
<ds-impersonate-navbar class="navbar-collapsed"></ds-impersonate-navbar>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
|
Reference in New Issue
Block a user