diff --git a/alfa-client/apps/admin/src/app/app.component.spec.ts b/alfa-client/apps/admin/src/app/app.component.spec.ts index 54c0b68df01382aeda9158d6b8b348db958f1360..cb90801ee5a3e396f47f17bf9daa5dcdf8fb7f38 100644 --- a/alfa-client/apps/admin/src/app/app.component.spec.ts +++ b/alfa-client/apps/admin/src/app/app.component.spec.ts @@ -21,10 +21,11 @@ * Die sprachspezifischen Genehmigungen und Beschränkungen * unter der Lizenz sind dem Lizenztext zu entnehmen. */ +import { ConfigurationLinkRel, ConfigurationResource, ConfigurationService } from '@admin-client/configuration-shared'; import { ApiRootLinkRel, ApiRootResource, ApiRootService } from '@alfa-client/api-root-shared'; import { BuildInfoComponent } from '@alfa-client/common'; import { HasLinkPipe, createEmptyStateResource, createStateResource } from '@alfa-client/tech-shared'; -import { Mock, dispatchEventFromFixture, existsAsHtmlElement, mock, notExistsAsHtmlElement } from '@alfa-client/test-utils'; +import { Mock, existsAsHtmlElement, mock, notExistsAsHtmlElement } from '@alfa-client/test-utils'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ActivatedRoute, Router, RouterOutlet } from '@angular/router'; import { AuthenticationService } from '@authentication'; @@ -36,11 +37,12 @@ import { OrgaUnitIconComponent, UsersIconComponent, } from '@ods/system'; +import { createConfigurationResource } from 'libs/admin/configuration-shared/test/configuration'; import { MenuContainerComponent } from 'libs/admin/configuration/src/lib/menu-container/menu-container.component'; import { createApiRootResource } from 'libs/api-root-shared/test/api-root'; import { getDataTestIdOf } from 'libs/tech-shared/test/data-test'; import { MockComponent, MockDirective } from 'ng-mocks'; -import { of } from 'rxjs'; +import { Subscription, of } from 'rxjs'; import { UserProfileButtonContainerComponent } from '../common/user-profile-button-container/user-profile.button-container.component'; import { UnavailablePageComponent } from '../pages/unavailable/unavailable-page/unavailable-page.component'; import { AppComponent } from './app.component'; @@ -55,7 +57,6 @@ describe('AppComponent', () => { const usersRolesNavigationSelector: string = getDataTestIdOf('users-roles-navigation'); const organisationsEinheitenNavigationSelector: string = getDataTestIdOf('organisations-einheiten-navigation'); - const logoLink: string = getDataTestIdOf('logo-link'); const routerOutletSelector: string = getDataTestIdOf('router-outlet'); const menuContainer: string = getDataTestIdOf('menu-container'); @@ -78,8 +79,11 @@ describe('AppComponent', () => { }; const apiRootService: Mock<ApiRootService> = mock(ApiRootService); + let configurationService: Mock<ConfigurationService>; beforeEach(async () => { + configurationService = mock(ConfigurationService); + await TestBed.configureTestingModule({ imports: [HasLinkPipe], declarations: [ @@ -105,6 +109,10 @@ describe('AppComponent', () => { provide: ApiRootService, useValue: apiRootService, }, + { + provide: ConfigurationService, + useValue: configurationService, + }, { provide: Router, useValue: router, @@ -147,26 +155,195 @@ describe('AppComponent', () => { }); describe('do after logged in', () => { + beforeEach(() => { + component.evaluateInitialNavigation = jest.fn(); + }); + it('should call apiRootService to getApiRoot', () => { component.doAfterLoggedIn(); expect(apiRootService.getApiRoot).toHaveBeenCalled(); }); - it('should call forwardWithoutAuthenticationParams', () => { - component.forwardWithoutAuthenticationParams = jest.fn(); + it('should call evaluateInitialNavigation', () => { + component.evaluateInitialNavigation = jest.fn(); component.doAfterLoggedIn(); - expect(component.forwardWithoutAuthenticationParams).toHaveBeenCalled(); + expect(component.evaluateInitialNavigation).toHaveBeenCalled(); + }); + }); + + describe('evaluate initial navigation', () => { + beforeEach(() => { + component.evaluateNavigationByApiRoot = jest.fn(); + }); + + it('should call evaluate navigation by apiRoot', () => { + const apiRootResource: ApiRootResource = createApiRootResource(); + component.apiRootStateResource$ = of(createStateResource(apiRootResource)); + + component.evaluateInitialNavigation(); + + expect(component.evaluateNavigationByApiRoot).toHaveBeenCalledWith(apiRootResource); + }); + + it('should NOT call evaluate navigation by apiRoot if resource is loading', () => { + component.apiRootStateResource$ = of(createEmptyStateResource<ApiRootResource>(true)); + component.evaluateNavigationByApiRoot = jest.fn(); + + component.evaluateInitialNavigation(); + + expect(component.evaluateNavigationByApiRoot).not.toHaveBeenCalled(); + }); + }); + + describe('evaluate navigation api root', () => { + it('should evaluate navigation by configuration if link exists', () => { + component.evaluateNavigationByConfiguration = jest.fn(); + const apiRootResource: ApiRootResource = createApiRootResource([ApiRootLinkRel.CONFIGURATION]); + + component.evaluateNavigationByApiRoot(apiRootResource); + + expect(component.evaluateNavigationByConfiguration).toHaveBeenCalled(); + }); + + it('should do navigation by api root if link is missing', () => { + component.doNavigationByApiRoot = jest.fn(); + const apiRootResource: ApiRootResource = createApiRootResource(); + + component.evaluateNavigationByApiRoot(apiRootResource); + + expect(component.doNavigationByApiRoot).toHaveBeenCalledWith(apiRootResource); + }); + }); + + describe('evaluate navigation by configuration', () => { + const configurationResource: ConfigurationResource = createConfigurationResource(); + + beforeEach(() => { + configurationService.get.mockReturnValue(of(createStateResource(configurationResource))); + component.doNavigationByConfiguration = jest.fn(); + }); + + it('should call configuration service to get resource', () => { + component.evaluateNavigationByConfiguration(); + + expect(configurationService.get).toHaveBeenCalled(); + }); + + it('should call do navigation by configuration', () => { + component.evaluateNavigationByConfiguration(); + + expect(component.doNavigationByConfiguration).toHaveBeenCalledWith(configurationResource); + }); + + it('should NOT call do navigation by configuration if resource is loading', () => { + configurationService.get.mockReturnValue(of(createEmptyStateResource<ConfigurationResource>(true))); + + component.evaluateNavigationByConfiguration(); + + expect(component.doNavigationByConfiguration).not.toHaveBeenCalled(); + }); + }); + + describe('do navigation by configuration', () => { + beforeEach(() => { + component.unsubscribe = jest.fn(); + }); + + it('should navigate to postfach if settings link exists', () => { + component.doNavigationByConfiguration(createConfigurationResource([ConfigurationLinkRel.SETTING])); + + expect(router.navigate).toHaveBeenCalledWith(['/postfach']); + }); + + it('should navigate to statistik if aggregation mapping link exists', () => { + component.doNavigationByConfiguration(createConfigurationResource([ConfigurationLinkRel.AGGREGATION_MAPPINGS])); + + expect(router.navigate).toHaveBeenCalledWith(['/statistik']); + }); + + it('should navigate to unavailable page if no link exists', () => { + component.doNavigationByConfiguration(createConfigurationResource()); + + expect(router.navigate).toHaveBeenCalledWith(['/unavailable']); + }); + + it('should call unsubscribe', () => { + component.doNavigationByConfiguration(createConfigurationResource()); + + expect(component.unsubscribe).toHaveBeenCalled(); + }); + }); + + describe('do navigation by api root', () => { + beforeEach(() => { + component.unsubscribe = jest.fn(); + }); + + it('should navigate to benutzer und rollen if users link exists', () => { + const apiRootResource: ApiRootResource = createApiRootResource([ApiRootLinkRel.USERS]); + + component.doNavigationByApiRoot(apiRootResource); + + expect(router.navigate).toHaveBeenCalledWith(['/benutzer_und_rollen']); + }); + + it('should navigate to unavailable page if no link exists', () => { + component.doNavigationByApiRoot(createApiRootResource()); + + expect(router.navigate).toHaveBeenCalledWith(['/unavailable']); + }); + + it('should call unsubscribe', () => { + component.doNavigationByApiRoot(createApiRootResource()); + + expect(component.unsubscribe).toHaveBeenCalled(); }); }); - describe('forward without authentication params', () => { - it('should navigate to same route without authentication params', () => { - component.forwardWithoutAuthenticationParams(); + describe('unsubscribe', () => { + describe('apiRoot subscription', () => { + it('should unsubscribe if exists', () => { + component.apiRootSubscription = <any>mock(Subscription); + component.apiRootSubscription.unsubscribe = jest.fn(); - expect(router.navigate).toHaveBeenCalledWith([], { queryParams: {} }); + component.unsubscribe(); + + expect(component.apiRootSubscription.unsubscribe).toHaveBeenCalled(); + }); + + it('should do nothing if not exists', () => { + component.apiRootSubscription = undefined; + const unsubscribeSpy: jest.SpyInstance = jest.spyOn(Subscription.prototype, 'unsubscribe'); + + component.unsubscribe(); + + expect(unsubscribeSpy).not.toHaveBeenCalled(); + unsubscribeSpy.mockRestore(); + }); + }); + + describe('configuration subscription', () => { + it('should unsubscribe if exists', () => { + component.configurationSubscription = <any>mock(Subscription); + component.configurationSubscription.unsubscribe = jest.fn(); + + component.unsubscribe(); + + expect(component.configurationSubscription.unsubscribe).toHaveBeenCalled(); + }); + + it('should do nothing if not exists', () => { + component.apiRootSubscription = undefined; + const unsubscribeSpy: jest.SpyInstance = jest.spyOn(Subscription.prototype, 'unsubscribe'); + + component.unsubscribe(); + + expect(unsubscribeSpy).not.toHaveBeenCalled(); + unsubscribeSpy.mockRestore(); + }); }); }); }); @@ -190,21 +367,6 @@ describe('AppComponent', () => { }); }); - describe('administration logo', () => { - const apiResource: ApiRootResource = createApiRootResource(); - - beforeEach(() => { - component.apiRootStateResource$ = of(createStateResource(apiResource)); - fixture.detectChanges(); - }); - - it('should navigate to start page on click', () => { - dispatchEventFromFixture(fixture, logoLink, 'click'); - - expect(router.navigate).toHaveBeenCalledWith([], { queryParams: {} }); - }); - }); - describe('navigation', () => { describe('user and roles', () => { it('should show if users link is present', () => { diff --git a/alfa-client/apps/admin/src/app/app.component.ts b/alfa-client/apps/admin/src/app/app.component.ts index 77e7d9b5bbf53858ac3024ac65c3a40d82c8c46f..979c8024a57108a66e9e037d8c20eb9b1f2179ab 100644 --- a/alfa-client/apps/admin/src/app/app.component.ts +++ b/alfa-client/apps/admin/src/app/app.component.ts @@ -22,13 +22,16 @@ * unter der Lizenz sind dem Lizenztext zu entnehmen. */ import { MenuContainerComponent } from '@admin-client/configuration'; +import { ConfigurationLinkRel, ConfigurationResource, ConfigurationService } from '@admin-client/configuration-shared'; +import { ROUTES } from '@admin-client/shared'; import { ApiRootLinkRel, ApiRootResource, ApiRootService } from '@alfa-client/api-root-shared'; import { BuildInfoComponent } from '@alfa-client/common'; -import { StateResource, TechSharedModule } from '@alfa-client/tech-shared'; +import { isLoaded, isNotUndefined, mapToResource, StateResource, TechSharedModule } from '@alfa-client/tech-shared'; import { CommonModule } from '@angular/common'; -import { Component, OnInit } from '@angular/core'; -import { ActivatedRoute, Params, Router, RouterLink, RouterOutlet } from '@angular/router'; +import { Component, inject, OnInit } from '@angular/core'; +import { Router, RouterLink, RouterOutlet } from '@angular/router'; import { AuthenticationService } from '@authentication'; +import { hasLink } from '@ngxp/rest'; import { AdminLogoIconComponent, NavbarComponent, @@ -36,7 +39,7 @@ import { OrgaUnitIconComponent, UsersIconComponent, } from '@ods/system'; -import { Observable } from 'rxjs'; +import { filter, Observable, Subscription } from 'rxjs'; import { UserProfileButtonContainerComponent } from '../common/user-profile-button-container/user-profile.button-container.component'; import { UnavailablePageComponent } from '../pages/unavailable/unavailable-page/unavailable-page.component'; @@ -62,18 +65,19 @@ import { UnavailablePageComponent } from '../pages/unavailable/unavailable-page/ ], }) export class AppComponent implements OnInit { - readonly title: string = 'admin'; + readonly title = 'admin'; + + private readonly authenticationService: AuthenticationService = inject(AuthenticationService); + private readonly apiRootService: ApiRootService = inject(ApiRootService); + private readonly router: Router = inject(Router); + private readonly configurationService: ConfigurationService = inject(ConfigurationService); public apiRootStateResource$: Observable<StateResource<ApiRootResource>>; - public readonly apiRootLinkRel = ApiRootLinkRel; + apiRootSubscription: Subscription; + configurationSubscription: Subscription; - constructor( - public authenticationService: AuthenticationService, - private apiRootService: ApiRootService, - private router: Router, - private route: ActivatedRoute, - ) {} + public readonly apiRootLinkRel = ApiRootLinkRel; ngOnInit(): void { this.authenticationService.login().then(() => this.doAfterLoggedIn()); @@ -81,16 +85,56 @@ export class AppComponent implements OnInit { doAfterLoggedIn(): void { this.apiRootStateResource$ = this.apiRootService.getApiRoot(); - this.forwardWithoutAuthenticationParams(); + this.evaluateInitialNavigation(); + } + + evaluateInitialNavigation(): void { + this.apiRootSubscription = this.apiRootStateResource$ + .pipe(filter(isLoaded), mapToResource<ApiRootResource>()) + .subscribe((apiRootResource: ApiRootResource) => this.evaluateNavigationByApiRoot(apiRootResource)); + } + + evaluateNavigationByApiRoot(apiRootResource: ApiRootResource): void { + if (hasLink(apiRootResource, ApiRootLinkRel.CONFIGURATION)) { + this.evaluateNavigationByConfiguration(); + } else { + this.doNavigationByApiRoot(apiRootResource); + } + } + + evaluateNavigationByConfiguration(): void { + this.configurationSubscription = this.configurationService + .get() + .pipe(filter(isLoaded), mapToResource<ApiRootResource>()) + .subscribe((configurationResource: ConfigurationResource) => this.doNavigationByConfiguration(configurationResource)); + } + + doNavigationByConfiguration(configurationResource: ConfigurationResource): void { + if (hasLink(configurationResource, ConfigurationLinkRel.SETTING)) { + this.doInitialNavigation(ROUTES.POSTFACH); + } else if (hasLink(configurationResource, ConfigurationLinkRel.AGGREGATION_MAPPINGS)) { + this.doInitialNavigation(ROUTES.STATISTIK); + } else { + this.doInitialNavigation(ROUTES.UNAVAILABLE); + } + this.unsubscribe(); + } + + doNavigationByApiRoot(apiRootResource: ApiRootResource): void { + if (hasLink(apiRootResource, ApiRootLinkRel.USERS)) { + this.doInitialNavigation(ROUTES.BENUTZER_UND_ROLLEN); + } else { + this.doInitialNavigation(ROUTES.UNAVAILABLE); + } + this.unsubscribe(); } - forwardWithoutAuthenticationParams(): void { - const queryParams: Params = this.getQueryParamsWithoutAuthentication(); - this.router.navigate([], { queryParams }); + private doInitialNavigation(routePath: string): void { + this.router.navigate(['/' + routePath]); } - private getQueryParamsWithoutAuthentication(): Params { - const { iss, state, session_state, code, ...queryParams } = this.route.snapshot.queryParams; - return queryParams; + unsubscribe(): void { + if (isNotUndefined(this.apiRootSubscription)) this.apiRootSubscription.unsubscribe(); + if (isNotUndefined(this.configurationSubscription)) this.configurationSubscription.unsubscribe(); } } diff --git a/alfa-client/apps/admin/src/app/app.routes.ts b/alfa-client/apps/admin/src/app/app.routes.ts index e26615b6c170bfc1cf6924e3bcf9e5f19a4defbe..a7ed852a6132c902566603a81021da5d46f2357f 100644 --- a/alfa-client/apps/admin/src/app/app.routes.ts +++ b/alfa-client/apps/admin/src/app/app.routes.ts @@ -37,12 +37,8 @@ import { apiRootGuard, configurationGuard } from './app.guard'; export interface GuardData { linkRelName: string; } + export const appRoutes: Route[] = [ - { - path: '', - redirectTo: ROUTES.POSTFACH, - pathMatch: 'full', - }, { path: ROUTES.POSTFACH, component: PostfachPageComponent,