From a21aeab12d11296867c414bec0faf3b4ebd1738c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Popovics=20Andr=C3=A1s?= Date: Wed, 29 Apr 2020 09:41:49 +0200 Subject: [PATCH] [ACA-3131] Add extension loader guard (#1426) * [ACA-3131] Add extension loader guard * Fix linting issue * Fix cspell issues --- cspell.json | 4 +- .../extensions-data-loader.guard.spec.ts | 135 ++++++++++++++++++ .../extensions-data-loader.guard.ts | 74 ++++++++++ projects/aca-shared/src/public-api.ts | 1 + src/app/app.routes.ts | 5 +- 5 files changed, 216 insertions(+), 3 deletions(-) create mode 100644 projects/aca-shared/src/lib/adf-extensions/extensions-data-loader.guard.spec.ts create mode 100644 projects/aca-shared/src/lib/adf-extensions/extensions-data-loader.guard.ts diff --git a/cspell.json b/cspell.json index bbfeb7ade..8e9a174f0 100644 --- a/cspell.json +++ b/cspell.json @@ -65,7 +65,9 @@ "submenu", "submenus", "dateitem", - "versionable" + "versionable", + "erroredSpy", + "errored" ], "dictionaries": ["html", "en-gb", "en_US"] } diff --git a/projects/aca-shared/src/lib/adf-extensions/extensions-data-loader.guard.spec.ts b/projects/aca-shared/src/lib/adf-extensions/extensions-data-loader.guard.spec.ts new file mode 100644 index 000000000..e3ba1ff4d --- /dev/null +++ b/projects/aca-shared/src/lib/adf-extensions/extensions-data-loader.guard.spec.ts @@ -0,0 +1,135 @@ +/*! + * @license + * Alfresco Example Content Application + * + * Copyright (C) 2005 - 2020 Alfresco Software Limited + * + * This file is part of the Alfresco Example Content Application. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * The Alfresco Example Content Application is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * The Alfresco Example Content Application is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ + +import { ExtensionsDataLoaderGuard } from './extensions-data-loader.guard'; +import { ActivatedRouteSnapshot } from '@angular/router'; +import { Subject, throwError } from 'rxjs'; + +describe('ExtensionsDataLoaderGuard', () => { + let route: ActivatedRouteSnapshot; + let emittedSpy; + let completedSpy; + let erroredSpy; + + describe('canActivate', () => { + beforeEach(() => { + route = {} as ActivatedRouteSnapshot; + emittedSpy = jasmine.createSpy('emitted'); + completedSpy = jasmine.createSpy('completed'); + erroredSpy = jasmine.createSpy('errored'); + }); + + it('should emit true and complete if no callback are present', () => { + const guard = new ExtensionsDataLoaderGuard([]); + + guard.canActivate(route).subscribe(emittedSpy, erroredSpy, completedSpy); + expect(emittedSpy).toHaveBeenCalledWith(true); + expect(erroredSpy).not.toHaveBeenCalled(); + expect(completedSpy).toHaveBeenCalled(); + }); + + it('should emit true and complete in case of only one callback is present, completed', () => { + const subject = new Subject(); + const guard = new ExtensionsDataLoaderGuard([ + () => subject.asObservable() + ]); + + guard.canActivate(route).subscribe(emittedSpy, erroredSpy, completedSpy); + + subject.next(true); + expect(emittedSpy).not.toHaveBeenCalled(); + expect(erroredSpy).not.toHaveBeenCalled(); + expect(completedSpy).not.toHaveBeenCalled(); + + subject.complete(); + expect(emittedSpy).toHaveBeenCalledWith(true); + expect(erroredSpy).not.toHaveBeenCalled(); + expect(completedSpy).toHaveBeenCalled(); + }); + + it('should emit true and complete in case of only one callback is present, errored', () => { + const guard = new ExtensionsDataLoaderGuard([ + () => throwError(new Error()) + ]); + + guard.canActivate(route).subscribe(emittedSpy, erroredSpy, completedSpy); + expect(emittedSpy).toHaveBeenCalledWith(true); + expect(erroredSpy).not.toHaveBeenCalled(); + expect(completedSpy).toHaveBeenCalled(); + }); + + it('should NOT complete in case of multiple callbacks are present and not all of them have been completed', () => { + const subject1 = new Subject(); + const subject2 = new Subject(); + const guard = new ExtensionsDataLoaderGuard([ + () => subject1.asObservable(), + () => subject2.asObservable() + ]); + + guard.canActivate(route).subscribe(emittedSpy, erroredSpy, completedSpy); + + subject1.next(); + subject2.next(); + subject1.complete(); + expect(emittedSpy).not.toHaveBeenCalled(); + expect(erroredSpy).not.toHaveBeenCalled(); + expect(completedSpy).not.toHaveBeenCalled(); + }); + + it('should emit true and complete in case of multiple callbacks are present and all of them have been completed', () => { + const subject1 = new Subject(); + const subject2 = new Subject(); + const guard = new ExtensionsDataLoaderGuard([ + () => subject1.asObservable(), + () => subject2.asObservable() + ]); + + guard.canActivate(route).subscribe(emittedSpy, erroredSpy, completedSpy); + + subject1.next(); + subject2.next(); + subject1.complete(); + subject2.complete(); + expect(emittedSpy).toHaveBeenCalledWith(true); + expect(erroredSpy).not.toHaveBeenCalled(); + expect(completedSpy).toHaveBeenCalled(); + }); + + it('should emit true and complete even if one of the observables are errored, to not block the application loading', () => { + const subject1 = new Subject(); + const guard = new ExtensionsDataLoaderGuard([ + () => subject1.asObservable(), + () => throwError(new Error()) + ]); + + guard.canActivate(route).subscribe(emittedSpy, erroredSpy, completedSpy); + + subject1.next(); + expect(emittedSpy).toHaveBeenCalledWith(true); + expect(erroredSpy).not.toHaveBeenCalled(); + expect(completedSpy).toHaveBeenCalled(); + }); + }); +}); diff --git a/projects/aca-shared/src/lib/adf-extensions/extensions-data-loader.guard.ts b/projects/aca-shared/src/lib/adf-extensions/extensions-data-loader.guard.ts new file mode 100644 index 000000000..03a6e2dca --- /dev/null +++ b/projects/aca-shared/src/lib/adf-extensions/extensions-data-loader.guard.ts @@ -0,0 +1,74 @@ +/*! + * @license + * Alfresco Example Content Application + * + * Copyright (C) 2005 - 2020 Alfresco Software Limited + * + * This file is part of the Alfresco Example Content Application. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * The Alfresco Example Content Application is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * The Alfresco Example Content Application is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ + +import { Injectable, InjectionToken, Inject } from '@angular/core'; +import { CanActivate, ActivatedRouteSnapshot } from '@angular/router'; +import { Observable, forkJoin, of } from 'rxjs'; +import { map, catchError } from 'rxjs/operators'; + +export type ExtensionLoaderCallback = ( + route: ActivatedRouteSnapshot +) => Observable; + +export const EXTENSION_DATA_LOADERS = new InjectionToken< + ExtensionLoaderCallback[] +>('EXTENSION_DATA_LOADERS', { + providedIn: 'root', + factory: () => [] +}); + +@Injectable({ providedIn: 'root' }) +export class ExtensionsDataLoaderGuard implements CanActivate { + constructor( + @Inject(EXTENSION_DATA_LOADERS) + private extensionDataLoaders: ExtensionLoaderCallback[] + ) {} + + canActivate(route: ActivatedRouteSnapshot): Observable { + if (!this.extensionDataLoaders.length) { + return of(true); + } + + const dataLoaderCallbacks = this.extensionDataLoaders.map(callback => + callback(route) + ); + + // Undocumented forkJoin behaviour/bug: + // https://github.com/ReactiveX/rxjs/issues/3246 + // So all callbacks need to emit before completion, otherwise forkJoin will short circuit + return forkJoin(...dataLoaderCallbacks).pipe( + map(() => true), + catchError(e => { + // tslint:disable-next-line + console.error( + 'Some of the extension data loader guards has been errored.' + ); + // tslint:disable-next-line + console.error(e); + return of(true); + }) + ); + } +} diff --git a/projects/aca-shared/src/public-api.ts b/projects/aca-shared/src/public-api.ts index a3a2a55b1..b5390e5c5 100644 --- a/projects/aca-shared/src/public-api.ts +++ b/projects/aca-shared/src/public-api.ts @@ -23,6 +23,7 @@ * along with Alfresco. If not, see . */ +export * from './lib/adf-extensions/extensions-data-loader.guard'; export * from './lib/components/page-layout/page-layout-content.component'; export * from './lib/components/page-layout/page-layout-error.component'; export * from './lib/components/page-layout/page-layout-header.component'; diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index a9a3ae65a..d884b8063 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -33,7 +33,8 @@ import { SearchLibrariesResultsComponent } from './components/search/search-libr import { LoginComponent } from './components/login/login.component'; import { AppSharedRuleGuard, - GenericErrorComponent + GenericErrorComponent, + ExtensionsDataLoaderGuard } from '@alfresco/aca-shared'; import { AuthGuardEcm } from '@alfresco/adf-core'; import { FavoritesComponent } from './components/favorites/favorites.component'; @@ -76,7 +77,7 @@ export const APP_ROUTES: Routes = [ { path: '', component: AppLayoutComponent, - canActivate: [AuthGuardEcm], + canActivate: [AuthGuardEcm, ExtensionsDataLoaderGuard], children: [ { path: '',