From 8b4cda9cb430543759e2d62da8c0a249a04dee6b Mon Sep 17 00:00:00 2001
From: Martin <git@mail.de>
Date: Fri, 17 Jan 2025 17:43:53 +0100
Subject: [PATCH] OZG-6986 OZG-7509 adjust initial navigation/routing

---
 .../apps/admin/src/app/app.component.spec.ts  | 212 +++++++++++++++---
 .../apps/admin/src/app/app.component.ts       |  82 +++++--
 alfa-client/apps/admin/src/app/app.routes.ts  |   6 +-
 3 files changed, 251 insertions(+), 49 deletions(-)

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 54c0b68df0..cb90801ee5 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 77e7d9b5bb..979c8024a5 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 e26615b6c1..a7ed852a61 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,
-- 
GitLab