Skip to content
Snippets Groups Projects
Commit bd1470a9 authored by Martin's avatar Martin
Browse files

Merge remote-tracking branch 'origin/main' into OZG-6988-admin-new-statistic-fields

parents c324a9bd 3817ffa8
No related branches found
No related tags found
1 merge request!27OZG-6988 implement button
Showing
with 536 additions and 69 deletions
...@@ -21,10 +21,11 @@ ...@@ -21,10 +21,11 @@
* Die sprachspezifischen Genehmigungen und Beschränkungen * Die sprachspezifischen Genehmigungen und Beschränkungen
* unter der Lizenz sind dem Lizenztext zu entnehmen. * 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 { ApiRootLinkRel, ApiRootResource, ApiRootService } from '@alfa-client/api-root-shared';
import { BuildInfoComponent } from '@alfa-client/common'; import { BuildInfoComponent } from '@alfa-client/common';
import { HasLinkPipe, createEmptyStateResource, createStateResource } from '@alfa-client/tech-shared'; 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 { ComponentFixture, TestBed } from '@angular/core/testing';
import { ActivatedRoute, Router, RouterOutlet } from '@angular/router'; import { ActivatedRoute, Router, RouterOutlet } from '@angular/router';
import { AuthenticationService } from '@authentication'; import { AuthenticationService } from '@authentication';
...@@ -36,11 +37,12 @@ import { ...@@ -36,11 +37,12 @@ import {
OrgaUnitIconComponent, OrgaUnitIconComponent,
UsersIconComponent, UsersIconComponent,
} from '@ods/system'; } 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 { MenuContainerComponent } from 'libs/admin/configuration/src/lib/menu-container/menu-container.component';
import { createApiRootResource } from 'libs/api-root-shared/test/api-root'; import { createApiRootResource } from 'libs/api-root-shared/test/api-root';
import { getDataTestIdOf } from 'libs/tech-shared/test/data-test'; import { getDataTestIdOf } from 'libs/tech-shared/test/data-test';
import { MockComponent, MockDirective } from 'ng-mocks'; 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 { UserProfileButtonContainerComponent } from '../common/user-profile-button-container/user-profile.button-container.component';
import { UnavailablePageComponent } from '../pages/unavailable/unavailable-page/unavailable-page.component'; import { UnavailablePageComponent } from '../pages/unavailable/unavailable-page/unavailable-page.component';
import { AppComponent } from './app.component'; import { AppComponent } from './app.component';
...@@ -55,7 +57,6 @@ describe('AppComponent', () => { ...@@ -55,7 +57,6 @@ describe('AppComponent', () => {
const usersRolesNavigationSelector: string = getDataTestIdOf('users-roles-navigation'); const usersRolesNavigationSelector: string = getDataTestIdOf('users-roles-navigation');
const organisationsEinheitenNavigationSelector: string = getDataTestIdOf('organisations-einheiten-navigation'); const organisationsEinheitenNavigationSelector: string = getDataTestIdOf('organisations-einheiten-navigation');
const logoLink: string = getDataTestIdOf('logo-link');
const routerOutletSelector: string = getDataTestIdOf('router-outlet'); const routerOutletSelector: string = getDataTestIdOf('router-outlet');
const menuContainer: string = getDataTestIdOf('menu-container'); const menuContainer: string = getDataTestIdOf('menu-container');
...@@ -78,8 +79,11 @@ describe('AppComponent', () => { ...@@ -78,8 +79,11 @@ describe('AppComponent', () => {
}; };
const apiRootService: Mock<ApiRootService> = mock(ApiRootService); const apiRootService: Mock<ApiRootService> = mock(ApiRootService);
let configurationService: Mock<ConfigurationService>;
beforeEach(async () => { beforeEach(async () => {
configurationService = mock(ConfigurationService);
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [HasLinkPipe], imports: [HasLinkPipe],
declarations: [ declarations: [
...@@ -105,6 +109,10 @@ describe('AppComponent', () => { ...@@ -105,6 +109,10 @@ describe('AppComponent', () => {
provide: ApiRootService, provide: ApiRootService,
useValue: apiRootService, useValue: apiRootService,
}, },
{
provide: ConfigurationService,
useValue: configurationService,
},
{ {
provide: Router, provide: Router,
useValue: router, useValue: router,
...@@ -147,26 +155,195 @@ describe('AppComponent', () => { ...@@ -147,26 +155,195 @@ describe('AppComponent', () => {
}); });
describe('do after logged in', () => { describe('do after logged in', () => {
beforeEach(() => {
component.evaluateInitialNavigation = jest.fn();
});
it('should call apiRootService to getApiRoot', () => { it('should call apiRootService to getApiRoot', () => {
component.doAfterLoggedIn(); component.doAfterLoggedIn();
expect(apiRootService.getApiRoot).toHaveBeenCalled(); expect(apiRootService.getApiRoot).toHaveBeenCalled();
}); });
it('should call forwardWithoutAuthenticationParams', () => { it('should call evaluateInitialNavigation', () => {
component.forwardWithoutAuthenticationParams = jest.fn(); component.evaluateInitialNavigation = jest.fn();
component.doAfterLoggedIn(); 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 navigate by api root if link is missing', () => {
component.navigateByApiRoot = jest.fn();
const apiRootResource: ApiRootResource = createApiRootResource();
component.evaluateNavigationByApiRoot(apiRootResource);
expect(component.navigateByApiRoot).toHaveBeenCalledWith(apiRootResource);
});
});
describe('evaluate navigation by configuration', () => {
const configurationResource: ConfigurationResource = createConfigurationResource();
beforeEach(() => {
configurationService.get.mockReturnValue(of(createStateResource(configurationResource)));
component.navigateByConfiguration = jest.fn();
});
it('should call configuration service to get resource', () => {
component.evaluateNavigationByConfiguration();
expect(configurationService.get).toHaveBeenCalled();
});
it('should call navigate by configuration', () => {
component.evaluateNavigationByConfiguration();
expect(component.navigateByConfiguration).toHaveBeenCalledWith(configurationResource);
});
it('should NOT call navigate by configuration if resource is loading', () => {
configurationService.get.mockReturnValue(of(createEmptyStateResource<ConfigurationResource>(true)));
component.evaluateNavigationByConfiguration();
expect(component.navigateByConfiguration).not.toHaveBeenCalled();
});
});
describe('navigate by configuration', () => {
beforeEach(() => {
component.unsubscribe = jest.fn();
});
it('should navigate to postfach if settings link exists', () => {
component.navigateByConfiguration(createConfigurationResource([ConfigurationLinkRel.SETTING]));
expect(router.navigate).toHaveBeenCalledWith(['/postfach']);
});
it('should navigate to statistik if aggregation mapping link exists', () => {
component.navigateByConfiguration(createConfigurationResource([ConfigurationLinkRel.AGGREGATION_MAPPINGS]));
expect(router.navigate).toHaveBeenCalledWith(['/statistik']);
});
it('should navigate to unavailable page if no link exists', () => {
component.navigateByConfiguration(createConfigurationResource());
expect(router.navigate).toHaveBeenCalledWith(['/unavailable']);
});
it('should call unsubscribe', () => {
component.navigateByConfiguration(createConfigurationResource());
expect(component.unsubscribe).toHaveBeenCalled();
});
});
describe('navigate 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.navigateByApiRoot(apiRootResource);
expect(router.navigate).toHaveBeenCalledWith(['/benutzer_und_rollen']);
});
it('should navigate to unavailable page if no link exists', () => {
component.navigateByApiRoot(createApiRootResource());
expect(router.navigate).toHaveBeenCalledWith(['/unavailable']);
});
it('should call unsubscribe', () => {
component.navigateByApiRoot(createApiRootResource());
expect(component.unsubscribe).toHaveBeenCalled();
}); });
}); });
describe('forward without authentication params', () => { describe('unsubscribe', () => {
it('should navigate to same route without authentication params', () => { describe('apiRoot subscription', () => {
component.forwardWithoutAuthenticationParams(); 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', () => { ...@@ -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('navigation', () => {
describe('user and roles', () => { describe('user and roles', () => {
it('should show if users link is present', () => { it('should show if users link is present', () => {
......
...@@ -22,13 +22,16 @@ ...@@ -22,13 +22,16 @@
* unter der Lizenz sind dem Lizenztext zu entnehmen. * unter der Lizenz sind dem Lizenztext zu entnehmen.
*/ */
import { MenuContainerComponent } from '@admin-client/configuration'; 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 { ApiRootLinkRel, ApiRootResource, ApiRootService } from '@alfa-client/api-root-shared';
import { BuildInfoComponent } from '@alfa-client/common'; 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 { CommonModule } from '@angular/common';
import { Component, OnInit } from '@angular/core'; import { Component, inject, OnInit } from '@angular/core';
import { ActivatedRoute, Params, Router, RouterLink, RouterOutlet } from '@angular/router'; import { Router, RouterLink, RouterOutlet } from '@angular/router';
import { AuthenticationService } from '@authentication'; import { AuthenticationService } from '@authentication';
import { hasLink } from '@ngxp/rest';
import { import {
AdminLogoIconComponent, AdminLogoIconComponent,
NavbarComponent, NavbarComponent,
...@@ -36,7 +39,7 @@ import { ...@@ -36,7 +39,7 @@ import {
OrgaUnitIconComponent, OrgaUnitIconComponent,
UsersIconComponent, UsersIconComponent,
} from '@ods/system'; } 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 { UserProfileButtonContainerComponent } from '../common/user-profile-button-container/user-profile.button-container.component';
import { UnavailablePageComponent } from '../pages/unavailable/unavailable-page/unavailable-page.component'; import { UnavailablePageComponent } from '../pages/unavailable/unavailable-page/unavailable-page.component';
...@@ -64,16 +67,17 @@ import { UnavailablePageComponent } from '../pages/unavailable/unavailable-page/ ...@@ -64,16 +67,17 @@ import { UnavailablePageComponent } from '../pages/unavailable/unavailable-page/
export class AppComponent implements OnInit { export class AppComponent implements OnInit {
readonly title: string = 'admin'; readonly title: string = 'admin';
private readonly authenticationService = inject(AuthenticationService);
private readonly apiRootService = inject(ApiRootService);
private readonly router = inject(Router);
private readonly configurationService = inject(ConfigurationService);
public apiRootStateResource$: Observable<StateResource<ApiRootResource>>; public apiRootStateResource$: Observable<StateResource<ApiRootResource>>;
public readonly apiRootLinkRel = ApiRootLinkRel; apiRootSubscription: Subscription;
configurationSubscription: Subscription;
constructor( public readonly apiRootLinkRel = ApiRootLinkRel;
public authenticationService: AuthenticationService,
private apiRootService: ApiRootService,
private router: Router,
private route: ActivatedRoute,
) {}
ngOnInit(): void { ngOnInit(): void {
this.authenticationService.login().then(() => this.doAfterLoggedIn()); this.authenticationService.login().then(() => this.doAfterLoggedIn());
...@@ -81,16 +85,56 @@ export class AppComponent implements OnInit { ...@@ -81,16 +85,56 @@ export class AppComponent implements OnInit {
doAfterLoggedIn(): void { doAfterLoggedIn(): void {
this.apiRootStateResource$ = this.apiRootService.getApiRoot(); 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.navigateByApiRoot(apiRootResource);
}
}
evaluateNavigationByConfiguration(): void {
this.configurationSubscription = this.configurationService
.get()
.pipe(filter(isLoaded), mapToResource<ApiRootResource>())
.subscribe((configurationResource: ConfigurationResource) => this.navigateByConfiguration(configurationResource));
}
navigateByConfiguration(configurationResource: ConfigurationResource): void {
if (hasLink(configurationResource, ConfigurationLinkRel.SETTING)) {
this.navigate(ROUTES.POSTFACH);
} else if (hasLink(configurationResource, ConfigurationLinkRel.AGGREGATION_MAPPINGS)) {
this.navigate(ROUTES.STATISTIK);
} else {
this.navigate(ROUTES.UNAVAILABLE);
}
this.unsubscribe();
}
navigateByApiRoot(apiRootResource: ApiRootResource): void {
if (hasLink(apiRootResource, ApiRootLinkRel.USERS)) {
this.navigate(ROUTES.BENUTZER_UND_ROLLEN);
} else {
this.navigate(ROUTES.UNAVAILABLE);
}
this.unsubscribe();
} }
forwardWithoutAuthenticationParams(): void { private navigate(routePath: string): void {
const queryParams: Params = this.getQueryParamsWithoutAuthentication(); this.router.navigate(['/' + routePath]);
this.router.navigate([], { queryParams });
} }
private getQueryParamsWithoutAuthentication(): Params { unsubscribe(): void {
const { iss, state, session_state, code, ...queryParams } = this.route.snapshot.queryParams; if (isNotUndefined(this.apiRootSubscription)) this.apiRootSubscription.unsubscribe();
return queryParams; if (isNotUndefined(this.configurationSubscription)) this.configurationSubscription.unsubscribe();
} }
} }
...@@ -40,11 +40,6 @@ export interface GuardData { ...@@ -40,11 +40,6 @@ export interface GuardData {
} }
export const appRoutes: Route[] = [ export const appRoutes: Route[] = [
{
path: '',
redirectTo: ROUTES.POSTFACH,
pathMatch: 'full',
},
{ {
path: ROUTES.POSTFACH, path: ROUTES.POSTFACH,
component: PostfachPageComponent, component: PostfachPageComponent,
......
...@@ -23,30 +23,39 @@ ...@@ -23,30 +23,39 @@
*/ */
import { Mock, mock, useFromMock } from '@alfa-client/test-utils'; import { Mock, mock, useFromMock } from '@alfa-client/test-utils';
import { UserProfileResource } from '@alfa-client/user-profile-shared'; import { UserProfileResource } from '@alfa-client/user-profile-shared';
import { AuthConfig, OAuthEvent, OAuthService } from 'angular-oauth2-oidc';
import { Environment } from 'libs/environment-shared/src/lib/environment.model';
import { createUserProfileResource } from 'libs/user-profile-shared/test/user-profile'; import { createUserProfileResource } from 'libs/user-profile-shared/test/user-profile';
import { AuthConfig, OAuthService } from 'angular-oauth2-oidc'; import { Subject } from 'rxjs';
import { AuthenticationService } from './authentication.service';
import { createAuthConfig } from '../../test/authentication';
import { createEnvironment } from '../../../environment-shared/test/environment'; import { createEnvironment } from '../../../environment-shared/test/environment';
import { Environment } from 'libs/environment-shared/src/lib/environment.model'; import { createAuthConfig, createOAuthEvent } from '../../test/authentication';
import { AuthenticationService } from './authentication.service';
describe('AuthenticationService', () => { describe('AuthenticationService', () => {
let service: AuthenticationService; let service: AuthenticationService;
let oAuthService: Mock<OAuthService>; let oAuthService: Mock<OAuthService>;
let environmentConfig: Environment; let environmentConfig: Environment;
let eventsSubject: Subject<OAuthEvent>;
beforeEach(() => { beforeEach(() => {
eventsSubject = new Subject<OAuthEvent>();
oAuthService = <any>{ oAuthService = <any>{
...mock(OAuthService), ...mock(OAuthService),
loadDiscoveryDocumentAndLogin: jest.fn().mockResolvedValue(() => Promise.resolve()), loadDiscoveryDocumentAndLogin: jest.fn().mockResolvedValue(() => Promise.resolve()),
hasValidAccessToken: jest.fn(),
hasValidIdToken: jest.fn(),
}; };
Object.defineProperty(oAuthService, 'events', { get: () => eventsSubject });
environmentConfig = createEnvironment(); environmentConfig = createEnvironment();
service = new AuthenticationService(useFromMock(oAuthService), environmentConfig); service = new AuthenticationService(useFromMock(oAuthService), environmentConfig);
}); });
describe('login', () => { describe('login', () => {
beforeEach(() => { beforeEach(() => {
service.setCurrentUser = jest.fn(); service.buildAuthEventPromise = jest.fn();
}); });
it('should configure service with authConfig', async () => { it('should configure service with authConfig', async () => {
...@@ -72,17 +81,228 @@ describe('AuthenticationService', () => { ...@@ -72,17 +81,228 @@ describe('AuthenticationService', () => {
expect(oAuthService.tokenValidationHandler).not.toBeNull(); expect(oAuthService.tokenValidationHandler).not.toBeNull();
}); });
it('should build auth event promise', async () => {
service.buildAuthEventPromise = jest.fn().mockResolvedValue(() => Promise.resolve());
await service.login();
expect(service.buildAuthEventPromise).toHaveBeenCalled();
});
it('should load discovery document and login', () => { it('should load discovery document and login', () => {
service.login(); service.login();
expect(oAuthService.loadDiscoveryDocumentAndLogin).toHaveBeenCalled(); expect(oAuthService.loadDiscoveryDocumentAndLogin).toHaveBeenCalled();
}); });
it('should set current user', async () => { it('should return eventPromise', async () => {
await service.login(); const promise: Promise<void> = Promise.resolve();
service.buildAuthEventPromise = jest.fn().mockResolvedValue(promise);
const returnPromise: Promise<void> = service.login();
await expect(returnPromise).resolves.toBeUndefined();
});
});
describe('build auth event promise', () => {
const event: OAuthEvent = createOAuthEvent();
beforeEach(() => {
service.shouldProceedByAuthEvent = jest.fn().mockReturnValue(true);
service.setCurrentUser = jest.fn();
service.unsubscribeEvents = jest.fn();
});
it('should call shouldProceedByAuthEvent on event trigger', () => {
service.buildAuthEventPromise();
eventsSubject.next(event);
expect(service.shouldProceedByAuthEvent).toHaveBeenCalledWith(event);
});
describe('on next', () => {
it('should set current user', () => {
service.buildAuthEventPromise();
eventsSubject.next(event);
expect(service.setCurrentUser).toHaveBeenCalled(); expect(service.setCurrentUser).toHaveBeenCalled();
}); });
it('should unsubscribe event', () => {
service.buildAuthEventPromise();
eventsSubject.next(event);
expect(service.unsubscribeEvents).toHaveBeenCalled();
});
it('should resolved promise with a valid event', async () => {
const promise: Promise<void> = service.buildAuthEventPromise();
eventsSubject.next(event);
await expect(promise).resolves.toBeUndefined();
});
});
describe('on error', () => {
const errorMessage: string = 'Test Error';
const error: Error = new Error(errorMessage);
it('should unsubscribe event', () => {
service.buildAuthEventPromise();
eventsSubject.error(error);
expect(service.unsubscribeEvents).toHaveBeenCalled();
});
it('should reject the promise with an error', async () => {
const promise: Promise<void> = service.buildAuthEventPromise();
eventsSubject.error(error);
await expect(promise).rejects.toThrow(errorMessage);
});
});
});
describe('should proceed by auth event', () => {
const event: OAuthEvent = createOAuthEvent();
it('should call considered as login event', () => {
service.consideredAsLoginEvent = jest.fn();
service.shouldProceedByAuthEvent(event);
expect(service.consideredAsLoginEvent).toHaveBeenCalledWith(event.type);
});
it('should return true on login event', () => {
service.consideredAsLoginEvent = jest.fn().mockReturnValue(true);
const proceed: boolean = service.shouldProceedByAuthEvent(event);
expect(proceed).toBeTruthy();
});
it('should call considered as page reload event', () => {
service.consideredAsLoginEvent = jest.fn().mockReturnValue(false);
service.consideredAsPageReloadEvent = jest.fn();
service.shouldProceedByAuthEvent(event);
expect(service.consideredAsPageReloadEvent).toHaveBeenCalledWith(event.type);
});
it('should return true on page reload event', () => {
service.consideredAsLoginEvent = jest.fn().mockReturnValue(false);
service.consideredAsPageReloadEvent = jest.fn().mockReturnValue(true);
const proceed: boolean = service.shouldProceedByAuthEvent(event);
expect(proceed).toBeTruthy();
});
it('should return false on non login or page reload event', () => {
service.consideredAsLoginEvent = jest.fn().mockReturnValue(false);
service.consideredAsPageReloadEvent = jest.fn().mockReturnValue(false);
const proceed: boolean = service.shouldProceedByAuthEvent(event);
expect(proceed).toBeFalsy();
});
});
describe('consideredAsLoginEvent', () => {
it('should return true if event is "token_received"', () => {
const event: string = 'token_received';
const result: boolean = service.consideredAsLoginEvent(event);
expect(result).toBeTruthy();
});
it('should return false if event is not "token_received"', () => {
const event: string = 'something_else';
const result: boolean = service.consideredAsLoginEvent(event);
expect(result).toBeFalsy();
});
});
describe('consideredAsPageReloadEvent', () => {
it('should return true if event is "discovery_document_loaded" and tokens are valid', () => {
service.hasValidToken = jest.fn().mockReturnValue(true);
const event: string = 'discovery_document_loaded';
const result: boolean = service.consideredAsPageReloadEvent(event);
expect(result).toBeTruthy();
});
it('should return false if event is "discovery_document_loaded" and tokens are invalid', () => {
service.hasValidToken = jest.fn().mockReturnValue(false);
const event: string = 'discovery_document_loaded';
const result: boolean = service.consideredAsPageReloadEvent(event);
expect(result).toBeFalsy();
});
it('should return false if event is not "discovery_document_loaded" and tokens are valid', () => {
service.hasValidToken = jest.fn().mockReturnValue(true);
const event: string = 'something_else';
const result: boolean = service.consideredAsPageReloadEvent(event);
expect(result).toBeFalsy();
});
it('should return false if event is not "discovery_document_loaded" and tokens are invalid', () => {
service.hasValidToken = jest.fn().mockReturnValue(false);
const event: string = 'something_else';
const result: boolean = service.consideredAsPageReloadEvent(event);
expect(result).toBeFalsy();
});
});
describe('hasValidToken', () => {
it('should return true if both tokens are valid', () => {
oAuthService.hasValidAccessToken.mockReturnValue(true);
oAuthService.hasValidIdToken.mockReturnValue(true);
const result: boolean = service.hasValidToken();
expect(result).toBeTruthy();
});
it('should return false if access token is invalid', () => {
oAuthService.hasValidAccessToken.mockReturnValue(false);
const result: boolean = service.hasValidToken();
expect(result).toBeFalsy();
});
it('should return false if id token is invalid', () => {
oAuthService.hasValidAccessToken.mockReturnValue(true);
oAuthService.hasValidIdToken.mockReturnValue(false);
const result: boolean = service.hasValidToken();
expect(result).toBeFalsy();
});
it('should return false if both tokens are invalid', () => {
oAuthService.hasValidAccessToken.mockReturnValue(false);
oAuthService.hasValidIdToken.mockReturnValue(false);
const result: boolean = service.hasValidToken();
expect(result).toBeFalsy();
});
}); });
describe('set current user', () => { describe('set current user', () => {
......
...@@ -23,15 +23,18 @@ ...@@ -23,15 +23,18 @@
*/ */
import { Environment, ENVIRONMENT_CONFIG } from '@alfa-client/environment-shared'; import { Environment, ENVIRONMENT_CONFIG } from '@alfa-client/environment-shared';
import { Inject, Injectable } from '@angular/core'; import { Inject, Injectable } from '@angular/core';
import { AuthConfig, OAuthService } from 'angular-oauth2-oidc'; import { AuthConfig, OAuthEvent, OAuthService } from 'angular-oauth2-oidc';
import { JwksValidationHandler } from 'angular-oauth2-oidc-jwks'; import { JwksValidationHandler } from 'angular-oauth2-oidc-jwks';
import { UserProfileResource } from 'libs/user-profile-shared/src/lib/user-profile.model'; import { UserProfileResource } from 'libs/user-profile-shared/src/lib/user-profile.model';
import { getUserNameInitials } from 'libs/user-profile-shared/src/lib/user-profile.util'; import { getUserNameInitials } from 'libs/user-profile-shared/src/lib/user-profile.util';
import { filter, Subscription } from 'rxjs';
@Injectable({ providedIn: 'root' }) @Injectable({ providedIn: 'root' })
export class AuthenticationService { export class AuthenticationService {
currentUserResource: UserProfileResource; currentUserResource: UserProfileResource;
private eventSubscription: Subscription;
constructor( constructor(
private oAuthService: OAuthService, private oAuthService: OAuthService,
@Inject(ENVIRONMENT_CONFIG) private envConfig: Environment, @Inject(ENVIRONMENT_CONFIG) private envConfig: Environment,
...@@ -41,18 +44,52 @@ export class AuthenticationService { ...@@ -41,18 +44,52 @@ export class AuthenticationService {
this.oAuthService.configure(this.buildConfiguration()); this.oAuthService.configure(this.buildConfiguration());
this.oAuthService.setupAutomaticSilentRefresh(); this.oAuthService.setupAutomaticSilentRefresh();
this.oAuthService.tokenValidationHandler = new JwksValidationHandler(); this.oAuthService.tokenValidationHandler = new JwksValidationHandler();
const eventPromise: Promise<void> = this.buildAuthEventPromise();
await this.oAuthService.loadDiscoveryDocumentAndLogin(); await this.oAuthService.loadDiscoveryDocumentAndLogin();
return eventPromise;
}
buildAuthEventPromise(): Promise<void> {
return new Promise<void>((resolve, reject) => this.handleAuthEventsForPromise(resolve, reject));
}
private handleAuthEventsForPromise(resolve: (value: void | PromiseLike<void>) => void, reject: (reason?: any) => void): void {
this.eventSubscription = this.oAuthService.events
.pipe(filter((event: OAuthEvent) => this.shouldProceedByAuthEvent(event)))
.subscribe({
next: () => {
this.setCurrentUser(); this.setCurrentUser();
this.unsubscribeEvents();
resolve();
},
error: (error: any) => {
this.unsubscribeEvents();
reject(error);
},
});
}
shouldProceedByAuthEvent(event: OAuthEvent): boolean {
return this.consideredAsLoginEvent(event.type) || this.consideredAsPageReloadEvent(event.type);
}
consideredAsLoginEvent(eventType: string): boolean {
return eventType === 'token_received';
}
consideredAsPageReloadEvent(eventType: string): boolean {
return eventType === 'discovery_document_loaded' && this.hasValidToken();
}
hasValidToken(): boolean {
return this.oAuthService.hasValidAccessToken() && this.oAuthService.hasValidIdToken();
} }
buildConfiguration(): AuthConfig { buildConfiguration(): AuthConfig {
return { return {
issuer: this.envConfig.authServer + '/realms/' + this.envConfig.realm, issuer: this.envConfig.authServer + '/realms/' + this.envConfig.realm,
tokenEndpoint: tokenEndpoint: this.envConfig.authServer + '/realms/' + this.envConfig.realm + '/protocol/openid-connect/token',
this.envConfig.authServer +
'/realms/' +
this.envConfig.realm +
'/protocol/openid-connect/token',
redirectUri: window.location.origin + '/', redirectUri: window.location.origin + '/',
clientId: this.envConfig.clientId, clientId: this.envConfig.clientId,
scope: 'openid profile', scope: 'openid profile',
...@@ -71,6 +108,10 @@ export class AuthenticationService { ...@@ -71,6 +108,10 @@ export class AuthenticationService {
this.currentUserResource = userResource; this.currentUserResource = userResource;
} }
unsubscribeEvents(): void {
this.eventSubscription.unsubscribe();
}
public getCurrentUserInitials(): string { public getCurrentUserInitials(): string {
return getUserNameInitials(this.currentUserResource); return getUserNameInitials(this.currentUserResource);
} }
......
...@@ -21,8 +21,13 @@ ...@@ -21,8 +21,13 @@
* Die sprachspezifischen Genehmigungen und Beschränkungen * Die sprachspezifischen Genehmigungen und Beschränkungen
* unter der Lizenz sind dem Lizenztext zu entnehmen. * unter der Lizenz sind dem Lizenztext zu entnehmen.
*/ */
import { AuthConfig } from 'angular-oauth2-oidc'; import { faker } from '@faker-js/faker';
import { AuthConfig, OAuthEvent } from 'angular-oauth2-oidc';
export function createAuthConfig(): AuthConfig { export function createAuthConfig(): AuthConfig {
return {}; return {};
} }
export function createOAuthEvent(): OAuthEvent {
return { type: <any>faker.lorem.word() };
}
...@@ -32,7 +32,7 @@ import { Component, EventEmitter, Input, Output } from '@angular/core'; ...@@ -32,7 +32,7 @@ import { Component, EventEmitter, Input, Output } from '@angular/core';
class="flex min-h-12 w-full items-center gap-4 border-2 border-transparent px-4 py-3 text-start outline-none hover:border-primary focus-visible:border-focus" class="flex min-h-12 w-full items-center gap-4 border-2 border-transparent px-4 py-3 text-start outline-none hover:border-primary focus-visible:border-focus"
role="menuitem" role="menuitem"
(click)="itemClicked.emit()" (click)="itemClicked.emit()"
[attr.data-test-id]="buttonTestId" [attr.data-test-id]="dataTestId"
> >
<ng-content select="[icon]" /> <ng-content select="[icon]" />
<p class="text-text">{{ caption }}</p> <p class="text-text">{{ caption }}</p>
...@@ -40,7 +40,7 @@ import { Component, EventEmitter, Input, Output } from '@angular/core'; ...@@ -40,7 +40,7 @@ import { Component, EventEmitter, Input, Output } from '@angular/core';
}) })
export class DropdownMenuButtonItemComponent { export class DropdownMenuButtonItemComponent {
@Input({ required: true }) caption!: string; @Input({ required: true }) caption!: string;
@Input() buttonTestId: string; @Input() dataTestId: string;
@Output() itemClicked: EventEmitter<MouseEvent> = new EventEmitter(); @Output() itemClicked: EventEmitter<MouseEvent> = new EventEmitter();
} }
...@@ -36,7 +36,7 @@ ...@@ -36,7 +36,7 @@
class="user-profile-icon" class="user-profile-icon"
> >
</alfa-user-icon> </alfa-user-icon>
<ods-dropdown-menu-button-item caption="Abmelden" (itemClicked)="logoutEmitter.emit()" buttonTestId="logout-button"> <ods-dropdown-menu-button-item caption="Abmelden" (itemClicked)="logoutEmitter.emit()" dataTestId="logout-button">
<ods-logout-icon icon /> <ods-logout-icon icon />
</ods-dropdown-menu-button-item> </ods-dropdown-menu-button-item>
</ods-dropdown-menu> </ods-dropdown-menu>
...@@ -30,7 +30,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; ...@@ -30,7 +30,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MatIcon } from '@angular/material/icon'; import { MatIcon } from '@angular/material/icon';
import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { DropdownMenuButtonItemComponent, DropdownMenuComponent, LogoutIconComponent } from '@ods/system'; import { DropdownMenuButtonItemComponent, DropdownMenuComponent, LogoutIconComponent } from '@ods/system';
import { getDataTestIdOf } from 'libs/tech-shared/test/data-test'; import { getDataTestIdAttributeOf, getDataTestIdOf } from 'libs/tech-shared/test/data-test';
import { createUserProfileResource } from 'libs/user-profile-shared/test/user-profile'; import { createUserProfileResource } from 'libs/user-profile-shared/test/user-profile';
import { MockComponent } from 'ng-mocks'; import { MockComponent } from 'ng-mocks';
import { UserProfileInHeaderComponent } from './user-profile-in-header.component'; import { UserProfileInHeaderComponent } from './user-profile-in-header.component';
...@@ -39,7 +39,7 @@ describe('UserProfileInHeaderComponent', () => { ...@@ -39,7 +39,7 @@ describe('UserProfileInHeaderComponent', () => {
let component: UserProfileInHeaderComponent; let component: UserProfileInHeaderComponent;
let fixture: ComponentFixture<UserProfileInHeaderComponent>; let fixture: ComponentFixture<UserProfileInHeaderComponent>;
const logoutButton: string = getDataTestIdOf('logout-button'); const logoutButton: string = getDataTestIdAttributeOf('logout-button');
const userIconDropDownMenu: string = getDataTestIdOf('user-icon-dropdown-menu'); const userIconDropDownMenu: string = getDataTestIdOf('user-icon-dropdown-menu');
const userProfile: UserProfileResource = createUserProfileResource(); const userProfile: UserProfileResource = createUserProfileResource();
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment