From 00d2eacb85f31351283640c99a03a37cd3f16314 Mon Sep 17 00:00:00 2001 From: Albert <Albert.Bruns@mgm-tp.com> Date: Mon, 17 Feb 2025 13:41:08 +0100 Subject: [PATCH 01/24] OZG-7590-7741 Weiterleitung --- .../apps/admin/src/app/app.component.spec.ts | 30 +++++++++++++++++-- .../apps/admin/src/app/app.component.ts | 29 +++++++++++++----- .../apps/admin/src/app/app.guard.spec.ts | 29 +++++++++++++----- alfa-client/apps/admin/src/app/app.guard.ts | 13 ++++---- .../src/lib/authentication.service.ts | 2 +- 5 files changed, 79 insertions(+), 24 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 34bd937efe..1a147f17f2 100644 --- a/alfa-client/apps/admin/src/app/app.component.spec.ts +++ b/alfa-client/apps/admin/src/app/app.component.spec.ts @@ -35,7 +35,7 @@ import { notExistsAsHtmlElement, } from '@alfa-client/test-utils'; import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { ActivatedRoute, Router, RouterOutlet } from '@angular/router'; +import { ActivatedRoute, NavigationEnd, NavigationStart, Router, RouterOutlet } from '@angular/router'; import { AuthenticationService } from '@authentication'; import { AdminLogoIconComponent, @@ -73,7 +73,11 @@ describe('AppComponent', () => { login: jest.fn().mockResolvedValue(Promise.resolve()), }; - const router: Mock<Router> = mock(Router); + const router: Mock<Router> = { + ...mock(Router), + events: of(new NavigationEnd(0, '', '')) as any, + }; + const route: Mock<ActivatedRoute> = { ...mock(ActivatedRoute), snapshot: { @@ -158,7 +162,19 @@ describe('AppComponent', () => { expect(authenticationService.login).toHaveBeenCalled(); }); + it('should not call doAfterLoggedIn if event is not NavigationEnd', async () => { + (router as any).events = of(new NavigationStart(0, '')); + component.doAfterLoggedIn = jest.fn(); + + component.ngOnInit(); + await fixture.whenStable(); + + expect(component.doAfterLoggedIn).not.toHaveBeenCalled(); + }); + it('should call doAfterLoggedIn', async () => { + (router as any).events = of(new NavigationEnd(0, '', '')); + component.doAfterLoggedIn = jest.fn(); component.ngOnInit(); @@ -166,6 +182,16 @@ describe('AppComponent', () => { expect(component.doAfterLoggedIn).toHaveBeenCalled(); }); + + it('should call doAfterLoggedIn only once', async () => { + (router as any).events = of(new NavigationEnd(0, 'url1', 'url1'), new NavigationEnd(1, 'url2', 'url2')); + component.doAfterLoggedIn = jest.fn(); + + component.ngOnInit(); + await fixture.whenStable(); + + expect(component.doAfterLoggedIn).toHaveBeenCalledTimes(1); + }); }); describe('do after logged in', () => { diff --git a/alfa-client/apps/admin/src/app/app.component.ts b/alfa-client/apps/admin/src/app/app.component.ts index fe6c276c6d..6a621a356e 100644 --- a/alfa-client/apps/admin/src/app/app.component.ts +++ b/alfa-client/apps/admin/src/app/app.component.ts @@ -30,11 +30,17 @@ import { BuildInfoComponent } from '@alfa-client/common'; import { isLoaded, isNotUndefined, mapToResource, StateResource, TechSharedModule } from '@alfa-client/tech-shared'; import { CommonModule } from '@angular/common'; import { Component, inject, OnInit } from '@angular/core'; -import { Router, RouterLink, RouterOutlet } from '@angular/router'; +import { Event, NavigationEnd, Router, RouterLink, RouterOutlet } from '@angular/router'; import { AuthenticationService } from '@authentication'; import { hasLink } from '@ngxp/rest'; -import { AdminLogoIconComponent, NavbarComponent, NavItemComponent, OrgaUnitIconComponent, UsersIconComponent, } from '@ods/system'; -import { filter, Observable, Subscription } from 'rxjs'; +import { + AdminLogoIconComponent, + NavbarComponent, + NavItemComponent, + OrgaUnitIconComponent, + UsersIconComponent, +} from '@ods/system'; +import { combineLatest, filter, first, from, 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'; @@ -77,7 +83,14 @@ export class AppComponent implements OnInit { public readonly routes = ROUTES; ngOnInit(): void { - this.authenticationService.login().then(() => this.doAfterLoggedIn()); + combineLatest([this.router.events, from(this.authenticationService.login())]) + .pipe( + filter(([event]: [Event, void]): boolean => event instanceof NavigationEnd), + first(), + ) + .subscribe(() => { + this.doAfterLoggedIn(); + }); } doAfterLoggedIn(): void { @@ -87,9 +100,11 @@ export class AppComponent implements OnInit { } evaluateInitialNavigation(): void { - this.apiRootSubscription = this.apiRootStateResource$ - .pipe(filter(isLoaded), mapToResource<ApiRootResource>()) - .subscribe((apiRootResource: ApiRootResource) => this.evaluateNavigationByApiRoot(apiRootResource)); + if (this.router.url === '/') { + this.apiRootSubscription = this.apiRootStateResource$ + .pipe(filter(isLoaded), mapToResource<ApiRootResource>()) + .subscribe((apiRootResource: ApiRootResource) => this.evaluateNavigationByApiRoot(apiRootResource)); + } } evaluateNavigationByApiRoot(apiRootResource: ApiRootResource): void { diff --git a/alfa-client/apps/admin/src/app/app.guard.spec.ts b/alfa-client/apps/admin/src/app/app.guard.spec.ts index 24e0a21eca..cd56cb02dd 100644 --- a/alfa-client/apps/admin/src/app/app.guard.spec.ts +++ b/alfa-client/apps/admin/src/app/app.guard.spec.ts @@ -139,6 +139,21 @@ describe('Guard', () => { const resource: Resource = createDummyResource([DummyLinkRel.DUMMY]); const stateResource$: Observable<StateResource<Resource>> = of(createStateResource(resource)); + let router: Mock<Router>; + + beforeEach(() => { + router = mock(Router); + + TestBed.configureTestingModule({ + providers: [ + { + provide: Router, + useValue: router, + }, + ], + }); + }); + afterEach(() => { jest.restoreAllMocks(); }); @@ -151,7 +166,7 @@ describe('Guard', () => { evaluate().subscribe(); - expect(evaluateResourceSpy).toHaveBeenCalledWith(resource, DummyLinkRel.DUMMY); + expect(evaluateResourceSpy).toHaveBeenCalledWith(resource, DummyLinkRel.DUMMY, router); }); it('should return evaluated boolean', (done) => { @@ -201,14 +216,16 @@ describe('Guard', () => { }); it('should return true if link exists', () => { - const result: boolean = <boolean>evaluateResource(createDummyResource([DummyLinkRel.DUMMY])); + const result: boolean = <boolean>( + Guard.evaluateResource(createDummyResource([DummyLinkRel.DUMMY]), DummyLinkRel.DUMMY, useFromMock(router)) + ); expect(result).toBeTruthy(); }); describe('if link not exists', () => { it('should call router', () => { - evaluateResource(createDummyResource()); + Guard.evaluateResource(createDummyResource(), DummyLinkRel.DUMMY, useFromMock(router)); expect(router.createUrlTree).toHaveBeenCalledWith(['/unavailable']); }); @@ -217,14 +234,10 @@ describe('Guard', () => { const urlTree: Mock<UrlTree> = mock(UrlTree); router.createUrlTree.mockReturnValue(urlTree); - const result: UrlTree = <UrlTree>evaluateResource(createDummyResource()); + const result: UrlTree = <UrlTree>Guard.evaluateResource(createDummyResource(), DummyLinkRel.DUMMY, useFromMock(router)); expect(result).toEqual(urlTree); }); }); - - function evaluateResource(dummyResource: Resource): boolean | UrlTree { - return <boolean | UrlTree>TestBed.runInInjectionContext(() => Guard.evaluateResource(dummyResource, DummyLinkRel.DUMMY)); - } }); }); diff --git a/alfa-client/apps/admin/src/app/app.guard.ts b/alfa-client/apps/admin/src/app/app.guard.ts index 85625be095..6c68eb7d33 100644 --- a/alfa-client/apps/admin/src/app/app.guard.ts +++ b/alfa-client/apps/admin/src/app/app.guard.ts @@ -21,6 +21,7 @@ * Die sprachspezifischen Genehmigungen und Beschränkungen * unter der Lizenz sind dem Lizenztext zu entnehmen. */ +import { ConfigurationService } from '@admin-client/configuration-shared'; import { ROUTES } from '@admin-client/shared'; import { ApiRootService } from '@alfa-client/api-root-shared'; import { LinkRelationName, mapToResource, StateResource } from '@alfa-client/tech-shared'; @@ -29,7 +30,6 @@ import { ActivatedRouteSnapshot, CanActivateFn, Router, UrlTree } from '@angular import { hasLink, Resource } from '@ngxp/rest'; import { filter, map, Observable } from 'rxjs'; import { GuardData } from './app.routes'; -import { ConfigurationService } from '@admin-client/configuration-shared'; import * as Guard from './app.guard'; @@ -47,10 +47,11 @@ export function evaluate( stateResource$: Observable<StateResource<Resource>>, linkRelationName: LinkRelationName, ): Observable<boolean | UrlTree> { + const router = inject(Router); return stateResource$.pipe( filter((stateResource: StateResource<Resource>) => stateResource.loaded), mapToResource<Resource>(), - map((resource: Resource) => Guard.evaluateResource(resource, linkRelationName)), + map((resource: Resource): boolean | UrlTree => Guard.evaluateResource(resource, linkRelationName, router)), ); } @@ -58,10 +59,10 @@ function getLinkRelationName(route: ActivatedRouteSnapshot): string { return (<GuardData>route.data).linkRelName; } -export function evaluateResource(resource: Resource, linkRelationName: LinkRelationName): boolean | UrlTree { - return hasLink(resource, linkRelationName) ? true : redirectToUnavailable(); +export function evaluateResource(resource: Resource, linkRelationName: LinkRelationName, router: Router): boolean | UrlTree { + return hasLink(resource, linkRelationName) ? true : redirectToUnavailable(router); } -function redirectToUnavailable(): UrlTree { - return inject(Router).createUrlTree(['/' + ROUTES.UNAVAILABLE]); +function redirectToUnavailable(router: Router): UrlTree { + return router.createUrlTree(['/' + ROUTES.UNAVAILABLE]); } diff --git a/alfa-client/libs/authentication/src/lib/authentication.service.ts b/alfa-client/libs/authentication/src/lib/authentication.service.ts index e350dcafe7..d1268cf606 100644 --- a/alfa-client/libs/authentication/src/lib/authentication.service.ts +++ b/alfa-client/libs/authentication/src/lib/authentication.service.ts @@ -90,7 +90,7 @@ export class AuthenticationService { return { issuer: this.envConfig.authServer + '/realms/' + this.envConfig.realm, tokenEndpoint: this.envConfig.authServer + '/realms/' + this.envConfig.realm + '/protocol/openid-connect/token', - redirectUri: window.location.origin + '/', + redirectUri: window.location.origin + window.location.pathname, clientId: this.envConfig.clientId, scope: 'openid profile', requireHttps: false, -- GitLab From 48137d14fbb43024a705da2bb740ab1d8d570cae Mon Sep 17 00:00:00 2001 From: Albert <Albert.Bruns@mgm-tp.com> Date: Mon, 17 Feb 2025 17:03:51 +0100 Subject: [PATCH 02/24] OZG-7590-7741 Weiterleitung --- .../apps/admin/src/app/app.guard.spec.ts | 108 +++++++++++++----- alfa-client/apps/admin/src/app/app.guard.ts | 18 ++- .../src/lib/authentication.service.spec.ts | 8 ++ .../src/lib/authentication.service.ts | 4 + 4 files changed, 107 insertions(+), 31 deletions(-) diff --git a/alfa-client/apps/admin/src/app/app.guard.spec.ts b/alfa-client/apps/admin/src/app/app.guard.spec.ts index cd56cb02dd..8f3a9f8e89 100644 --- a/alfa-client/apps/admin/src/app/app.guard.spec.ts +++ b/alfa-client/apps/admin/src/app/app.guard.spec.ts @@ -35,6 +35,7 @@ import { createDummyResource } from 'libs/tech-shared/test/resource'; import { Observable, of } from 'rxjs'; import { GuardData } from './app.routes'; +import { AuthenticationService } from '@authentication'; import * as Guard from './app.guard'; describe('Guard', () => { @@ -43,14 +44,20 @@ describe('Guard', () => { describe('api root guard', () => { let apiRootService: Mock<ApiRootService>; + let router: Mock<Router>; const apiRootStateResource$: Observable<StateResource<ApiRootResource>> = of(createStateResource(createApiRootResource())); beforeEach(() => { apiRootService = mock(ApiRootService); + router = mock(Router); TestBed.configureTestingModule({ providers: [ + { + provide: Router, + useValue: router, + }, { provide: ApiRootService, useValue: apiRootService, @@ -62,12 +69,21 @@ describe('Guard', () => { beforeEach(() => { apiRootService.getApiRoot.mockReturnValue(apiRootStateResource$); jest.spyOn(Guard, 'evaluate').mockReturnValue(of(true)); + jest.spyOn(Guard, 'login').mockReturnValue(of(undefined)); }); afterEach(() => { jest.restoreAllMocks(); }); + it('should call login', () => { + const loginSpy: jest.SpyInstance = jest.spyOn(Guard, 'login').mockReturnValue(of(undefined)); + + runGuard().subscribe(); + + expect(loginSpy).toHaveBeenCalled(); + }); + it('should call apiRootService', () => { runGuard().subscribe(); @@ -79,7 +95,7 @@ describe('Guard', () => { runGuard().subscribe(); - expect(evaluateSpy).toHaveBeenCalledWith(apiRootStateResource$, DummyLinkRel.DUMMY); + expect(evaluateSpy).toHaveBeenCalledWith(apiRootStateResource$, DummyLinkRel.DUMMY, useFromMock(router)); }); function runGuard(): Observable<boolean | UrlTree> { @@ -89,6 +105,7 @@ describe('Guard', () => { describe('configuration guard', () => { let configurationService: Mock<ConfigurationService>; + let router: Mock<Router>; const configurationStateResource$: Observable<StateResource<ConfigurationResource>> = of( createStateResource(createConfigurationResource()), @@ -96,9 +113,14 @@ describe('Guard', () => { beforeEach(() => { configurationService = mock(ConfigurationService); + router = mock(Router); TestBed.configureTestingModule({ providers: [ + { + provide: Router, + useValue: router, + }, { provide: ConfigurationService, useValue: configurationService, @@ -110,12 +132,21 @@ describe('Guard', () => { beforeEach(() => { configurationService.get.mockReturnValue(configurationStateResource$); jest.spyOn(Guard, 'evaluate').mockReturnValue(of(true)); + jest.spyOn(Guard, 'login').mockReturnValue(of(undefined)); }); afterEach(() => { jest.restoreAllMocks(); }); + it('should call login', () => { + const loginSpy: jest.SpyInstance = jest.spyOn(Guard, 'login').mockReturnValue(of(undefined)); + + runGuard().subscribe(); + + expect(loginSpy).toHaveBeenCalled(); + }); + it('should call configurationService', () => { runGuard().subscribe(); @@ -127,7 +158,7 @@ describe('Guard', () => { runGuard().subscribe(); - expect(evaluateSpy).toHaveBeenCalledWith(configurationStateResource$, DummyLinkRel.DUMMY); + expect(evaluateSpy).toHaveBeenCalledWith(configurationStateResource$, DummyLinkRel.DUMMY, router); }); function runGuard(): Observable<boolean | UrlTree> { @@ -135,25 +166,63 @@ describe('Guard', () => { } }); - describe('evaluate', () => { - const resource: Resource = createDummyResource([DummyLinkRel.DUMMY]); - const stateResource$: Observable<StateResource<Resource>> = of(createStateResource(resource)); - - let router: Mock<Router>; + describe('login', () => { + let authService: Mock<AuthenticationService>; beforeEach(() => { - router = mock(Router); + authService = { + ...mock(AuthenticationService), + login: jest.fn().mockReturnValue(Promise.resolve(undefined)), + }; TestBed.configureTestingModule({ providers: [ { - provide: Router, - useValue: router, + provide: AuthenticationService, + useValue: authService, }, ], }); }); + it('should check if user is logged in', () => { + login().subscribe(); + + expect(authService.isLoggedIn).toHaveBeenCalled(); + }); + + it('should return observable of null when user is logged in', (done) => { + login().subscribe((result) => { + expect(result).toBeUndefined(); + done(); + }); + }); + + it('should login user if not logged in', () => { + authService.isLoggedIn.mockReturnValue(false); + + login().subscribe(); + + expect(authService.login).toHaveBeenCalled(); + }); + + it('should return observable of null when user is not logged in', (done) => { + login().subscribe((result) => { + expect(result).toBeUndefined(); + done(); + }); + }); + + function login(): Observable<null> { + return <Observable<null>>TestBed.runInInjectionContext(() => Guard.login()); + } + }); + + describe('evaluate', () => { + const router: Mock<Router> = mock(Router); + const resource: Resource = createDummyResource([DummyLinkRel.DUMMY]); + const stateResource$: Observable<StateResource<Resource>> = of(createStateResource(resource)); + afterEach(() => { jest.restoreAllMocks(); }); @@ -190,26 +259,13 @@ describe('Guard', () => { function evaluate(): Observable<boolean | UrlTree> { return <Observable<boolean | UrlTree>>( - TestBed.runInInjectionContext(() => Guard.evaluate(stateResource$, DummyLinkRel.DUMMY)) + TestBed.runInInjectionContext(() => Guard.evaluate(stateResource$, DummyLinkRel.DUMMY, useFromMock(router))) ); } }); describe('evaluate resource', () => { - let router: Mock<Router>; - - beforeEach(() => { - router = mock(Router); - - TestBed.configureTestingModule({ - providers: [ - { - provide: Router, - useValue: router, - }, - ], - }); - }); + const router: Mock<Router> = mock(Router); afterEach(() => { jest.restoreAllMocks(); @@ -217,7 +273,7 @@ describe('Guard', () => { it('should return true if link exists', () => { const result: boolean = <boolean>( - Guard.evaluateResource(createDummyResource([DummyLinkRel.DUMMY]), DummyLinkRel.DUMMY, useFromMock(router)) + Guard.evaluateResource(createDummyResource([DummyLinkRel.DUMMY]), DummyLinkRel.DUMMY, null) ); expect(result).toBeTruthy(); diff --git a/alfa-client/apps/admin/src/app/app.guard.ts b/alfa-client/apps/admin/src/app/app.guard.ts index 6c68eb7d33..1bcfd075c4 100644 --- a/alfa-client/apps/admin/src/app/app.guard.ts +++ b/alfa-client/apps/admin/src/app/app.guard.ts @@ -28,26 +28,34 @@ import { LinkRelationName, mapToResource, StateResource } from '@alfa-client/tec import { inject } from '@angular/core'; import { ActivatedRouteSnapshot, CanActivateFn, Router, UrlTree } from '@angular/router'; import { hasLink, Resource } from '@ngxp/rest'; -import { filter, map, Observable } from 'rxjs'; +import { filter, from, map, Observable, of, switchMap } from 'rxjs'; import { GuardData } from './app.routes'; +import { AuthenticationService } from '@authentication'; import * as Guard from './app.guard'; export const apiRootGuard: CanActivateFn = (route: ActivatedRouteSnapshot) => { const apiRootService: ApiRootService = inject(ApiRootService); - return Guard.evaluate(apiRootService.getApiRoot(), getLinkRelationName(route)); + const router = inject(Router); + return Guard.login().pipe(switchMap(() => Guard.evaluate(apiRootService.getApiRoot(), getLinkRelationName(route), router))); }; -export const configurationGuard: CanActivateFn = (route: ActivatedRouteSnapshot) => { +export const configurationGuard: CanActivateFn = (route: ActivatedRouteSnapshot): Observable<boolean | UrlTree> => { + const router = inject(Router); const configurationService: ConfigurationService = inject(ConfigurationService); - return Guard.evaluate(configurationService.get(), getLinkRelationName(route)); + return Guard.login().pipe(switchMap(() => Guard.evaluate(configurationService.get(), getLinkRelationName(route), router))); }; +export function login(): Observable<void> { + const authService: AuthenticationService = inject(AuthenticationService); + return authService.isLoggedIn() ? of(undefined) : from(authService.login()); +} + export function evaluate( stateResource$: Observable<StateResource<Resource>>, linkRelationName: LinkRelationName, + router: Router, ): Observable<boolean | UrlTree> { - const router = inject(Router); return stateResource$.pipe( filter((stateResource: StateResource<Resource>) => stateResource.loaded), mapToResource<Resource>(), diff --git a/alfa-client/libs/authentication/src/lib/authentication.service.spec.ts b/alfa-client/libs/authentication/src/lib/authentication.service.spec.ts index 486415f425..1367a34118 100644 --- a/alfa-client/libs/authentication/src/lib/authentication.service.spec.ts +++ b/alfa-client/libs/authentication/src/lib/authentication.service.spec.ts @@ -350,4 +350,12 @@ describe('AuthenticationService', () => { expect(oAuthService.revokeTokenAndLogout).toHaveBeenCalled(); }); }); + + describe('isLoggedIn', () => { + it('should call oAuthService hasValidAccessToken', () => { + service.isLoggedIn(); + + expect(oAuthService.hasValidAccessToken).toHaveBeenCalled(); + }); + }); }); diff --git a/alfa-client/libs/authentication/src/lib/authentication.service.ts b/alfa-client/libs/authentication/src/lib/authentication.service.ts index d1268cf606..03f92b8fa3 100644 --- a/alfa-client/libs/authentication/src/lib/authentication.service.ts +++ b/alfa-client/libs/authentication/src/lib/authentication.service.ts @@ -119,4 +119,8 @@ export class AuthenticationService { public logout(): void { this.oAuthService.revokeTokenAndLogout(); } + + public isLoggedIn(): boolean { + return this.oAuthService.hasValidAccessToken(); + } } -- GitLab From bdbb3459ff4348b05c671d564f26ea126a570adb Mon Sep 17 00:00:00 2001 From: Albert <Albert.Bruns@mgm-tp.com> Date: Tue, 18 Feb 2025 10:51:48 +0100 Subject: [PATCH 03/24] OZG-7590-7741 checkbox hinzufuegen --- alfa-client/apps/admin/src/app/app.routes.ts | 5 +++++ .../user-form/user-form-roles/user-form-roles.component.html | 1 + .../libs/admin/user/src/lib/user-form/user.formservice.ts | 2 ++ 3 files changed, 8 insertions(+) diff --git a/alfa-client/apps/admin/src/app/app.routes.ts b/alfa-client/apps/admin/src/app/app.routes.ts index 279141277b..a4cbd29d95 100644 --- a/alfa-client/apps/admin/src/app/app.routes.ts +++ b/alfa-client/apps/admin/src/app/app.routes.ts @@ -92,4 +92,9 @@ export const appRoutes: Route[] = [ canActivate: [configurationGuard], data: <GuardData>{ linkRelName: ConfigurationLinkRel.AGGREGATION_MAPPINGS }, }, + { + path: '**', + component: UnavailablePageComponent, + title: 'Unavailable', + }, ]; diff --git a/alfa-client/libs/admin/user/src/lib/user-form/user-form-roles/user-form-roles.component.html b/alfa-client/libs/admin/user/src/lib/user-form/user-form-roles/user-form-roles.component.html index 026e846163..d5e1789d0f 100644 --- a/alfa-client/libs/admin/user/src/lib/user-form/user-form-roles/user-form-roles.component.html +++ b/alfa-client/libs/admin/user/src/lib/user-form/user-form-roles/user-form-roles.component.html @@ -4,6 +4,7 @@ <div [formGroupName]="UserFormService.ADMINISTRATION_GROUP" class="flex flex-col gap-2"> <h3 class="text-md block font-medium text-text">Administration</h3> <ods-checkbox-editor [formControlName]="UserFormService.ADMIN" label="Admin" inputId="admin" /> + <ods-checkbox-editor [formControlName]="UserFormService.DATENBEAUFTRAGUNG" label="Datenbeauftragung" inputId="datenbeauftragung" /> </div> <div [formGroupName]="UserFormService.ALFA_GROUP" class="flex flex-col gap-2"> <h3 class="text-md block font-medium text-text">Alfa</h3> diff --git a/alfa-client/libs/admin/user/src/lib/user-form/user.formservice.ts b/alfa-client/libs/admin/user/src/lib/user-form/user.formservice.ts index e7e537b362..fbfde6f564 100644 --- a/alfa-client/libs/admin/user/src/lib/user-form/user.formservice.ts +++ b/alfa-client/libs/admin/user/src/lib/user-form/user.formservice.ts @@ -52,6 +52,7 @@ export class UserFormService extends KeycloakFormService<User> implements OnDest public static readonly GROUPS: string = 'groups'; public static readonly ADMINISTRATION_GROUP: string = 'admin'; public static readonly ADMIN: string = 'ADMIN_ADMIN'; + public static readonly DATENBEAUFTRAGUNG: string = 'DATENBEAUFTRAGUNG'; public static readonly ALFA_GROUP: string = 'alfa'; public static readonly LOESCHEN: string = 'VERWALTUNG_LOESCHEN'; public static readonly USER: string = 'VERWALTUNG_USER'; @@ -100,6 +101,7 @@ export class UserFormService extends KeycloakFormService<User> implements OnDest { [UserFormService.ADMINISTRATION_GROUP]: this.formBuilder.group({ [UserFormService.ADMIN]: new FormControl(false), + [UserFormService.DATENBEAUFTRAGUNG]: new FormControl(false), }), [UserFormService.ALFA_GROUP]: this.formBuilder.group({ [UserFormService.LOESCHEN]: new FormControl(false), -- GitLab From 6d9852884b1dcd67f4dd6280102e32d66fd36c23 Mon Sep 17 00:00:00 2001 From: Albert <Albert.Bruns@mgm-tp.com> Date: Tue, 18 Feb 2025 12:25:42 +0100 Subject: [PATCH 04/24] OZG-7590 cleaner code --- .../apps/admin/src/app/app.component.spec.ts | 56 +++++++++++-------- .../apps/admin/src/app/app.guard.spec.ts | 29 ++++------ 2 files changed, 44 insertions(+), 41 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 1a147f17f2..d0d59b7b10 100644 --- a/alfa-client/apps/admin/src/app/app.component.spec.ts +++ b/alfa-client/apps/admin/src/app/app.component.spec.ts @@ -68,33 +68,31 @@ describe('AppComponent', () => { const routerOutletSelector: string = getDataTestIdOf('router-outlet'); const menuContainer: string = getDataTestIdOf('menu-container'); - const authenticationService: Mock<AuthenticationService> = { - ...mock(AuthenticationService), - login: jest.fn().mockResolvedValue(Promise.resolve()), - }; - - const router: Mock<Router> = { - ...mock(Router), - events: of(new NavigationEnd(0, '', '')) as any, - }; - - const route: Mock<ActivatedRoute> = { - ...mock(ActivatedRoute), - snapshot: { - queryParams: { - iss: 'some-iss', - state: 'some-state', - session_state: 'some-session-state', - code: 'some-code', - }, - } as any, - }; - - const apiRootService: Mock<ApiRootService> = mock(ApiRootService); + let authenticationService: Mock<AuthenticationService>; + let router: Mock<Router>; + let route: Mock<ActivatedRoute>; + let apiRootService: Mock<ApiRootService>; let configurationService: Mock<ConfigurationService>; let keycloakTokenService: Mock<KeycloakTokenService>; beforeEach(async () => { + authenticationService = { + ...mock(AuthenticationService), + login: jest.fn().mockResolvedValue(Promise.resolve()), + }; + router = mock(Router); + route = { + ...mock(ActivatedRoute), + snapshot: { + queryParams: { + iss: 'some-iss', + state: 'some-state', + session_state: 'some-session-state', + code: 'some-code', + }, + } as any, + }; + apiRootService = mock(ApiRootService); configurationService = mock(ConfigurationService); keycloakTokenService = mock(KeycloakTokenService); @@ -224,6 +222,7 @@ describe('AppComponent', () => { }); it('should call evaluate navigation by apiRoot', () => { + (router as any).url = '/'; const apiRootResource: ApiRootResource = createApiRootResource(); component.apiRootStateResource$ = of(createStateResource(apiRootResource)); @@ -232,7 +231,18 @@ describe('AppComponent', () => { expect(component.evaluateNavigationByApiRoot).toHaveBeenCalledWith(apiRootResource); }); + it('should not call evaluate navigation by apiRoot if url is not default', () => { + (router as any).url = '/path'; + const apiRootResource: ApiRootResource = createApiRootResource(); + component.apiRootStateResource$ = of(createStateResource(apiRootResource)); + + component.evaluateInitialNavigation(); + + expect(component.evaluateNavigationByApiRoot).not.toHaveBeenCalledWith(apiRootResource); + }); + it('should NOT call evaluate navigation by apiRoot if resource is loading', () => { + (router as any).url = '/'; component.apiRootStateResource$ = of(createEmptyStateResource<ApiRootResource>(true)); component.evaluateNavigationByApiRoot = jest.fn(); diff --git a/alfa-client/apps/admin/src/app/app.guard.spec.ts b/alfa-client/apps/admin/src/app/app.guard.spec.ts index 8f3a9f8e89..110d3766da 100644 --- a/alfa-client/apps/admin/src/app/app.guard.spec.ts +++ b/alfa-client/apps/admin/src/app/app.guard.spec.ts @@ -50,8 +50,8 @@ describe('Guard', () => { beforeEach(() => { apiRootService = mock(ApiRootService); - router = mock(Router); + TestBed.configureTestingModule({ providers: [ { @@ -191,14 +191,13 @@ describe('Guard', () => { expect(authService.isLoggedIn).toHaveBeenCalled(); }); - it('should return observable of null when user is logged in', (done) => { - login().subscribe((result) => { + it('should return observable of undefined when user is logged in', (done) => { + login().subscribe((result: undefined): void => { expect(result).toBeUndefined(); done(); }); }); - - it('should login user if not logged in', () => { + +it('should login user if not logged in', () => { authService.isLoggedIn.mockReturnValue(false); login().subscribe(); @@ -206,15 +205,15 @@ describe('Guard', () => { expect(authService.login).toHaveBeenCalled(); }); - it('should return observable of null when user is not logged in', (done) => { - login().subscribe((result) => { + it('should return observable of undefined when user is not logged in', (done) => { + login().subscribe((result: undefined): void => { expect(result).toBeUndefined(); done(); }); }); - function login(): Observable<null> { - return <Observable<null>>TestBed.runInInjectionContext(() => Guard.login()); + function login(): Observable<void> { + return <Observable<void>>TestBed.runInInjectionContext(() => Guard.login()); } }); @@ -233,7 +232,7 @@ describe('Guard', () => { .spyOn(Guard, 'evaluateResource') .mockReturnValue(useFromMock(urlTreeMock)); - evaluate().subscribe(); + Guard.evaluate(stateResource$, DummyLinkRel.DUMMY, useFromMock(router)).subscribe(); expect(evaluateResourceSpy).toHaveBeenCalledWith(resource, DummyLinkRel.DUMMY, router); }); @@ -241,7 +240,7 @@ describe('Guard', () => { it('should return evaluated boolean', (done) => { jest.spyOn(Guard, 'evaluateResource').mockReturnValue(true); - evaluate().subscribe((resolvedValue) => { + Guard.evaluate(stateResource$, DummyLinkRel.DUMMY, useFromMock(router)).subscribe((resolvedValue) => { expect(resolvedValue).toEqual(true); done(); }); @@ -251,17 +250,11 @@ describe('Guard', () => { const urlTreeMock: Mock<UrlTree> = mock(UrlTree); jest.spyOn(Guard, 'evaluateResource').mockReturnValue(useFromMock(urlTreeMock)); - evaluate().subscribe((resolvedValue) => { + Guard.evaluate(stateResource$, DummyLinkRel.DUMMY, useFromMock(router)).subscribe((resolvedValue) => { expect(resolvedValue).toEqual(urlTreeMock); done(); }); }); - - function evaluate(): Observable<boolean | UrlTree> { - return <Observable<boolean | UrlTree>>( - TestBed.runInInjectionContext(() => Guard.evaluate(stateResource$, DummyLinkRel.DUMMY, useFromMock(router))) - ); - } }); describe('evaluate resource', () => { -- GitLab From 25fe284594bdb1f7edb7a9203b81fd3959bff609 Mon Sep 17 00:00:00 2001 From: Albert <Albert.Bruns@mgm-tp.com> Date: Tue, 18 Feb 2025 13:00:08 +0100 Subject: [PATCH 05/24] OZG-7590 fix test --- alfa-client/libs/admin/user/test/form.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/alfa-client/libs/admin/user/test/form.ts b/alfa-client/libs/admin/user/test/form.ts index 7b3f06850d..d8ac4bb73c 100644 --- a/alfa-client/libs/admin/user/test/form.ts +++ b/alfa-client/libs/admin/user/test/form.ts @@ -12,6 +12,7 @@ export function createUserFormGroup(): UntypedFormGroup { [UserFormService.CLIENT_ROLES]: new UntypedFormGroup({ [UserFormService.ADMINISTRATION_GROUP]: new UntypedFormGroup({ [UserFormService.ADMIN]: new FormControl(false), + [UserFormService.DATENBEAUFTRAGUNG]: new FormControl(false), }), [UserFormService.ALFA_GROUP]: new UntypedFormGroup({ [UserFormService.LOESCHEN]: new FormControl(false), -- GitLab From 82c807e0d71f8928a411ad83e9833e7f4005651f Mon Sep 17 00:00:00 2001 From: Albert <Albert.Bruns@mgm-tp.com> Date: Wed, 19 Feb 2025 12:39:34 +0100 Subject: [PATCH 06/24] OZG-7590 CR Anmerkungen --- .../apps/admin/src/app/app.component.spec.ts | 31 +----- .../apps/admin/src/app/app.component.ts | 21 ++-- .../apps/admin/src/app/app.guard.spec.ts | 97 ++++++------------- alfa-client/apps/admin/src/app/app.guard.ts | 26 ++--- .../src/lib/authentication.service.spec.ts | 27 +++++- .../src/lib/authentication.service.ts | 12 ++- 6 files changed, 93 insertions(+), 121 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 d0d59b7b10..e5104f8395 100644 --- a/alfa-client/apps/admin/src/app/app.component.spec.ts +++ b/alfa-client/apps/admin/src/app/app.component.spec.ts @@ -35,7 +35,7 @@ import { notExistsAsHtmlElement, } from '@alfa-client/test-utils'; import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { ActivatedRoute, NavigationEnd, NavigationStart, Router, RouterOutlet } from '@angular/router'; +import { ActivatedRoute, Router, RouterOutlet } from '@angular/router'; import { AuthenticationService } from '@authentication'; import { AdminLogoIconComponent, @@ -160,29 +160,7 @@ describe('AppComponent', () => { expect(authenticationService.login).toHaveBeenCalled(); }); - it('should not call doAfterLoggedIn if event is not NavigationEnd', async () => { - (router as any).events = of(new NavigationStart(0, '')); - component.doAfterLoggedIn = jest.fn(); - - component.ngOnInit(); - await fixture.whenStable(); - - expect(component.doAfterLoggedIn).not.toHaveBeenCalled(); - }); - - it('should call doAfterLoggedIn', async () => { - (router as any).events = of(new NavigationEnd(0, '', '')); - - component.doAfterLoggedIn = jest.fn(); - - component.ngOnInit(); - await fixture.whenStable(); - - expect(component.doAfterLoggedIn).toHaveBeenCalled(); - }); - it('should call doAfterLoggedIn only once', async () => { - (router as any).events = of(new NavigationEnd(0, 'url1', 'url1'), new NavigationEnd(1, 'url2', 'url2')); component.doAfterLoggedIn = jest.fn(); component.ngOnInit(); @@ -231,14 +209,13 @@ describe('AppComponent', () => { expect(component.evaluateNavigationByApiRoot).toHaveBeenCalledWith(apiRootResource); }); - it('should not call evaluate navigation by apiRoot if url is not default', () => { + it('should call router navigate', () => { + window.location.pathname = '/path'; (router as any).url = '/path'; - const apiRootResource: ApiRootResource = createApiRootResource(); - component.apiRootStateResource$ = of(createStateResource(apiRootResource)); component.evaluateInitialNavigation(); - expect(component.evaluateNavigationByApiRoot).not.toHaveBeenCalledWith(apiRootResource); + expect(router.navigate).toHaveBeenCalledWith([window.location.pathname]); }); it('should NOT call evaluate navigation by apiRoot if resource is loading', () => { diff --git a/alfa-client/apps/admin/src/app/app.component.ts b/alfa-client/apps/admin/src/app/app.component.ts index fe5b90c3ad..1dbfc188e3 100644 --- a/alfa-client/apps/admin/src/app/app.component.ts +++ b/alfa-client/apps/admin/src/app/app.component.ts @@ -30,7 +30,7 @@ import { BuildInfoComponent } from '@alfa-client/common'; import { HasLinkPipe, isLoaded, isNotUndefined, mapToResource, StateResource } from '@alfa-client/tech-shared'; import { CommonModule } from '@angular/common'; import { Component, inject, OnInit } from '@angular/core'; -import { Event, NavigationEnd, Router, RouterLink, RouterOutlet } from '@angular/router'; +import { Router, RouterLink, RouterOutlet } from '@angular/router'; import { AuthenticationService } from '@authentication'; import { hasLink } from '@ngxp/rest'; import { @@ -40,7 +40,7 @@ import { OrgaUnitIconComponent, UsersIconComponent, } from '@ods/system'; -import { combineLatest, filter, first, from, Observable, Subscription } from 'rxjs'; +import { filter, from, 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'; @@ -83,14 +83,9 @@ export class AppComponent implements OnInit { public readonly routes = ROUTES; ngOnInit(): void { - combineLatest([this.router.events, from(this.authenticationService.login())]) - .pipe( - filter(([event]: [Event, void]): boolean => event instanceof NavigationEnd), - first(), - ) - .subscribe(() => { - this.doAfterLoggedIn(); - }); + from(this.authenticationService.login()).subscribe(() => { + this.doAfterLoggedIn(); + }); } doAfterLoggedIn(): void { @@ -104,9 +99,15 @@ export class AppComponent implements OnInit { this.apiRootSubscription = this.apiRootStateResource$ .pipe(filter(isLoaded), mapToResource<ApiRootResource>()) .subscribe((apiRootResource: ApiRootResource) => this.evaluateNavigationByApiRoot(apiRootResource)); + } else { + this.refreshForGuardEvaluation(); } } + private refreshForGuardEvaluation(): void { + this.router.navigate([window.location.pathname]); + } + evaluateNavigationByApiRoot(apiRootResource: ApiRootResource): void { if (hasLink(apiRootResource, ApiRootLinkRel.CONFIGURATION)) { this.evaluateNavigationByConfiguration(); diff --git a/alfa-client/apps/admin/src/app/app.guard.spec.ts b/alfa-client/apps/admin/src/app/app.guard.spec.ts index 110d3766da..77828836d1 100644 --- a/alfa-client/apps/admin/src/app/app.guard.spec.ts +++ b/alfa-client/apps/admin/src/app/app.guard.spec.ts @@ -44,20 +44,14 @@ describe('Guard', () => { describe('api root guard', () => { let apiRootService: Mock<ApiRootService>; - let router: Mock<Router>; const apiRootStateResource$: Observable<StateResource<ApiRootResource>> = of(createStateResource(createApiRootResource())); beforeEach(() => { apiRootService = mock(ApiRootService); - router = mock(Router); TestBed.configureTestingModule({ providers: [ - { - provide: Router, - useValue: router, - }, { provide: ApiRootService, useValue: apiRootService, @@ -76,14 +70,6 @@ describe('Guard', () => { jest.restoreAllMocks(); }); - it('should call login', () => { - const loginSpy: jest.SpyInstance = jest.spyOn(Guard, 'login').mockReturnValue(of(undefined)); - - runGuard().subscribe(); - - expect(loginSpy).toHaveBeenCalled(); - }); - it('should call apiRootService', () => { runGuard().subscribe(); @@ -95,7 +81,7 @@ describe('Guard', () => { runGuard().subscribe(); - expect(evaluateSpy).toHaveBeenCalledWith(apiRootStateResource$, DummyLinkRel.DUMMY, useFromMock(router)); + expect(evaluateSpy).toHaveBeenCalledWith(apiRootStateResource$, DummyLinkRel.DUMMY); }); function runGuard(): Observable<boolean | UrlTree> { @@ -105,7 +91,6 @@ describe('Guard', () => { describe('configuration guard', () => { let configurationService: Mock<ConfigurationService>; - let router: Mock<Router>; const configurationStateResource$: Observable<StateResource<ConfigurationResource>> = of( createStateResource(createConfigurationResource()), @@ -113,14 +98,9 @@ describe('Guard', () => { beforeEach(() => { configurationService = mock(ConfigurationService); - router = mock(Router); TestBed.configureTestingModule({ providers: [ - { - provide: Router, - useValue: router, - }, { provide: ConfigurationService, useValue: configurationService, @@ -139,14 +119,6 @@ describe('Guard', () => { jest.restoreAllMocks(); }); - it('should call login', () => { - const loginSpy: jest.SpyInstance = jest.spyOn(Guard, 'login').mockReturnValue(of(undefined)); - - runGuard().subscribe(); - - expect(loginSpy).toHaveBeenCalled(); - }); - it('should call configurationService', () => { runGuard().subscribe(); @@ -158,7 +130,7 @@ describe('Guard', () => { runGuard().subscribe(); - expect(evaluateSpy).toHaveBeenCalledWith(configurationStateResource$, DummyLinkRel.DUMMY, router); + expect(evaluateSpy).toHaveBeenCalledWith(configurationStateResource$, DummyLinkRel.DUMMY); }); function runGuard(): Observable<boolean | UrlTree> { @@ -166,17 +138,26 @@ describe('Guard', () => { } }); - describe('login', () => { + describe('evaluate', () => { + const resource: Resource = createDummyResource([DummyLinkRel.DUMMY]); + const stateResource$: Observable<StateResource<Resource>> = of(createStateResource(resource)); + let authService: Mock<AuthenticationService>; + let router: Mock<Router>; beforeEach(() => { authService = { ...mock(AuthenticationService), - login: jest.fn().mockReturnValue(Promise.resolve(undefined)), + isLoggedIn: jest.fn().mockReturnValue(true), }; + router = mock(Router); TestBed.configureTestingModule({ providers: [ + { + provide: Router, + useValue: router, + }, { provide: AuthenticationService, useValue: authService, @@ -185,54 +166,32 @@ describe('Guard', () => { }); }); - it('should check if user is logged in', () => { - login().subscribe(); + afterEach(() => { + jest.restoreAllMocks(); + }); + + it('should call authenticationService isLoggedIn', () => { + evaluate().subscribe(); expect(authService.isLoggedIn).toHaveBeenCalled(); }); - it('should return observable of undefined when user is logged in', (done) => { - login().subscribe((result: undefined): void => { - expect(result).toBeUndefined(); - done(); - }); - }); - +it('should login user if not logged in', () => { + it('should return observable true if not logged in', (done) => { authService.isLoggedIn.mockReturnValue(false); - login().subscribe(); - - expect(authService.login).toHaveBeenCalled(); - }); - - it('should return observable of undefined when user is not logged in', (done) => { - login().subscribe((result: undefined): void => { - expect(result).toBeUndefined(); + evaluate().subscribe((resolvedValue) => { + expect(resolvedValue).toEqual(true); done(); }); }); - function login(): Observable<void> { - return <Observable<void>>TestBed.runInInjectionContext(() => Guard.login()); - } - }); - - describe('evaluate', () => { - const router: Mock<Router> = mock(Router); - const resource: Resource = createDummyResource([DummyLinkRel.DUMMY]); - const stateResource$: Observable<StateResource<Resource>> = of(createStateResource(resource)); - - afterEach(() => { - jest.restoreAllMocks(); - }); - it('should evaluate resource', () => { const urlTreeMock: Mock<UrlTree> = mock(UrlTree); const evaluateResourceSpy: jest.SpyInstance = jest .spyOn(Guard, 'evaluateResource') .mockReturnValue(useFromMock(urlTreeMock)); - Guard.evaluate(stateResource$, DummyLinkRel.DUMMY, useFromMock(router)).subscribe(); + evaluate().subscribe(); expect(evaluateResourceSpy).toHaveBeenCalledWith(resource, DummyLinkRel.DUMMY, router); }); @@ -240,7 +199,7 @@ describe('Guard', () => { it('should return evaluated boolean', (done) => { jest.spyOn(Guard, 'evaluateResource').mockReturnValue(true); - Guard.evaluate(stateResource$, DummyLinkRel.DUMMY, useFromMock(router)).subscribe((resolvedValue) => { + evaluate().subscribe((resolvedValue) => { expect(resolvedValue).toEqual(true); done(); }); @@ -250,11 +209,17 @@ describe('Guard', () => { const urlTreeMock: Mock<UrlTree> = mock(UrlTree); jest.spyOn(Guard, 'evaluateResource').mockReturnValue(useFromMock(urlTreeMock)); - Guard.evaluate(stateResource$, DummyLinkRel.DUMMY, useFromMock(router)).subscribe((resolvedValue) => { + evaluate().subscribe((resolvedValue) => { expect(resolvedValue).toEqual(urlTreeMock); done(); }); }); + + function evaluate(): Observable<boolean | UrlTree> { + return <Observable<boolean | UrlTree>>( + TestBed.runInInjectionContext(() => Guard.evaluate(stateResource$, DummyLinkRel.DUMMY)) + ); + } }); describe('evaluate resource', () => { diff --git a/alfa-client/apps/admin/src/app/app.guard.ts b/alfa-client/apps/admin/src/app/app.guard.ts index 1bcfd075c4..d131726812 100644 --- a/alfa-client/apps/admin/src/app/app.guard.ts +++ b/alfa-client/apps/admin/src/app/app.guard.ts @@ -28,22 +28,20 @@ import { LinkRelationName, mapToResource, StateResource } from '@alfa-client/tec import { inject } from '@angular/core'; import { ActivatedRouteSnapshot, CanActivateFn, Router, UrlTree } from '@angular/router'; import { hasLink, Resource } from '@ngxp/rest'; -import { filter, from, map, Observable, of, switchMap } from 'rxjs'; +import { filter, from, map, Observable, of } from 'rxjs'; import { GuardData } from './app.routes'; import { AuthenticationService } from '@authentication'; import * as Guard from './app.guard'; -export const apiRootGuard: CanActivateFn = (route: ActivatedRouteSnapshot) => { +export const apiRootGuard: CanActivateFn = (route: ActivatedRouteSnapshot): Observable<true | UrlTree> => { const apiRootService: ApiRootService = inject(ApiRootService); - const router = inject(Router); - return Guard.login().pipe(switchMap(() => Guard.evaluate(apiRootService.getApiRoot(), getLinkRelationName(route), router))); + return Guard.evaluate(apiRootService.getApiRoot(), getLinkRelationName(route)); }; -export const configurationGuard: CanActivateFn = (route: ActivatedRouteSnapshot): Observable<boolean | UrlTree> => { - const router = inject(Router); +export const configurationGuard: CanActivateFn = (route: ActivatedRouteSnapshot): Observable<true | UrlTree> => { const configurationService: ConfigurationService = inject(ConfigurationService); - return Guard.login().pipe(switchMap(() => Guard.evaluate(configurationService.get(), getLinkRelationName(route), router))); + return Guard.evaluate(configurationService.get(), getLinkRelationName(route)); }; export function login(): Observable<void> { @@ -54,12 +52,16 @@ export function login(): Observable<void> { export function evaluate( stateResource$: Observable<StateResource<Resource>>, linkRelationName: LinkRelationName, - router: Router, -): Observable<boolean | UrlTree> { +): Observable<true | UrlTree> { + const authService = inject(AuthenticationService); + const router = inject(Router); + + if (!authService.isLoggedIn()) return of(true); + return stateResource$.pipe( - filter((stateResource: StateResource<Resource>) => stateResource.loaded), + filter((stateResource: StateResource<Resource>): boolean => stateResource.loaded), mapToResource<Resource>(), - map((resource: Resource): boolean | UrlTree => Guard.evaluateResource(resource, linkRelationName, router)), + map((resource: Resource): true | UrlTree => Guard.evaluateResource(resource, linkRelationName, router)), ); } @@ -67,7 +69,7 @@ function getLinkRelationName(route: ActivatedRouteSnapshot): string { return (<GuardData>route.data).linkRelName; } -export function evaluateResource(resource: Resource, linkRelationName: LinkRelationName, router: Router): boolean | UrlTree { +export function evaluateResource(resource: Resource, linkRelationName: LinkRelationName, router: Router): true | UrlTree { return hasLink(resource, linkRelationName) ? true : redirectToUnavailable(router); } diff --git a/alfa-client/libs/authentication/src/lib/authentication.service.spec.ts b/alfa-client/libs/authentication/src/lib/authentication.service.spec.ts index 1367a34118..79930137f9 100644 --- a/alfa-client/libs/authentication/src/lib/authentication.service.spec.ts +++ b/alfa-client/libs/authentication/src/lib/authentication.service.spec.ts @@ -23,10 +23,11 @@ */ import { Mock, mock, useFromMock } from '@alfa-client/test-utils'; import { UserProfileResource } from '@alfa-client/user-profile-shared'; +import { NavigationEnd, Router } from '@angular/router'; 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 { Subject } from 'rxjs'; +import { of, Subject } from 'rxjs'; import { createEnvironment } from '../../../environment-shared/test/environment'; import { createAuthConfig, createOAuthEvent } from '../../test/authentication'; import { AuthenticationService } from './authentication.service'; @@ -34,6 +35,7 @@ import { AuthenticationService } from './authentication.service'; describe('AuthenticationService', () => { let service: AuthenticationService; let oAuthService: Mock<OAuthService>; + let router: Mock<Router>; let environmentConfig: Environment; let eventsSubject: Subject<OAuthEvent>; @@ -41,6 +43,7 @@ describe('AuthenticationService', () => { beforeEach(() => { eventsSubject = new Subject<OAuthEvent>(); + router = mock(Router); oAuthService = <any>{ ...mock(OAuthService), loadDiscoveryDocumentAndLogin: jest.fn().mockResolvedValue(() => Promise.resolve()), @@ -50,12 +53,19 @@ describe('AuthenticationService', () => { Object.defineProperty(oAuthService, 'events', { get: () => eventsSubject }); environmentConfig = createEnvironment(); - service = new AuthenticationService(useFromMock(oAuthService), environmentConfig); + service = new AuthenticationService(useFromMock(oAuthService), useFromMock(router), environmentConfig); }); describe('login', () => { beforeEach(() => { service.buildAuthEventPromise = jest.fn(); + service._initialNavigationDone = jest.fn().mockResolvedValue(() => Promise.resolve()); + }); + + it('should wait for initial navigation', async () => { + await service.login(); + + expect(service._initialNavigationDone).toHaveBeenCalled(); }); it('should configure service with authConfig', async () => { @@ -89,8 +99,8 @@ describe('AuthenticationService', () => { expect(service.buildAuthEventPromise).toHaveBeenCalled(); }); - it('should load discovery document and login', () => { - service.login(); + it('should load discovery document and login', async () => { + await service.login(); expect(oAuthService.loadDiscoveryDocumentAndLogin).toHaveBeenCalled(); }); @@ -105,6 +115,15 @@ describe('AuthenticationService', () => { }); }); + describe('_initialNavigationDone', () => { + it('should wait for navigation end event', async () => { + (router.events as any) = of(new NavigationEnd(0, 'url1', 'url1')); + const promise: Promise<void> = service._initialNavigationDone(); + + await expect(promise).resolves.toBeUndefined(); + }); + }); + describe('build auth event promise', () => { const event: OAuthEvent = createOAuthEvent(); diff --git a/alfa-client/libs/authentication/src/lib/authentication.service.ts b/alfa-client/libs/authentication/src/lib/authentication.service.ts index 03f92b8fa3..539237767b 100644 --- a/alfa-client/libs/authentication/src/lib/authentication.service.ts +++ b/alfa-client/libs/authentication/src/lib/authentication.service.ts @@ -23,11 +23,12 @@ */ import { Environment, ENVIRONMENT_CONFIG } from '@alfa-client/environment-shared'; import { Inject, Injectable } from '@angular/core'; +import { NavigationEnd, Router } from '@angular/router'; import { AuthConfig, OAuthEvent, OAuthService } from 'angular-oauth2-oidc'; import { JwksValidationHandler } from 'angular-oauth2-oidc-jwks'; 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 { filter, Subscription } from 'rxjs'; +import { filter, firstValueFrom, Subscription } from 'rxjs'; @Injectable({ providedIn: 'root' }) export class AuthenticationService { @@ -37,10 +38,13 @@ export class AuthenticationService { constructor( private oAuthService: OAuthService, + private router: Router, @Inject(ENVIRONMENT_CONFIG) private envConfig: Environment, ) {} public async login(): Promise<void> { + await this._initialNavigationDone(); + this.oAuthService.configure(this.buildConfiguration()); this.oAuthService.setupAutomaticSilentRefresh(); this.oAuthService.tokenValidationHandler = new JwksValidationHandler(); @@ -50,6 +54,10 @@ export class AuthenticationService { return eventPromise; } + async _initialNavigationDone(): Promise<void> { + await firstValueFrom(this.router.events.pipe(filter((event) => event instanceof NavigationEnd))); + } + buildAuthEventPromise(): Promise<void> { return new Promise<void>((resolve, reject) => this.handleAuthEventsForPromise(resolve, reject)); } @@ -117,7 +125,7 @@ export class AuthenticationService { } public logout(): void { - this.oAuthService.revokeTokenAndLogout(); + this.oAuthService.logOut(); } public isLoggedIn(): boolean { -- GitLab From 073629623819efdc1b67f800d023179613fb7fe1 Mon Sep 17 00:00:00 2001 From: Albert <Albert.Bruns@mgm-tp.com> Date: Wed, 19 Feb 2025 13:01:26 +0100 Subject: [PATCH 07/24] OZG-7590 CR Anmerkungen --- .../libs/authentication/src/lib/authentication.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/alfa-client/libs/authentication/src/lib/authentication.service.ts b/alfa-client/libs/authentication/src/lib/authentication.service.ts index 539237767b..86617cc6bf 100644 --- a/alfa-client/libs/authentication/src/lib/authentication.service.ts +++ b/alfa-client/libs/authentication/src/lib/authentication.service.ts @@ -125,7 +125,7 @@ export class AuthenticationService { } public logout(): void { - this.oAuthService.logOut(); + this.oAuthService.revokeTokenAndLogout(); } public isLoggedIn(): boolean { -- GitLab From 015f8eb35483aca5535da25a1599cbd3c9e02827 Mon Sep 17 00:00:00 2001 From: Albert <Albert.Bruns@mgm-tp.com> Date: Thu, 20 Feb 2025 18:18:22 +0100 Subject: [PATCH 08/24] OZG-7590 cr anmerkungen --- alfa-client/apps/admin/src/app/app.component.ts | 6 ++---- .../apps/admin/src/app/app.guard.spec.ts | 6 ++---- alfa-client/apps/admin/src/app/app.guard.ts | 9 ++------- .../src/lib/authentication.service.spec.ts | 17 +++++++++++++---- .../src/lib/authentication.service.ts | 4 ++-- 5 files changed, 21 insertions(+), 21 deletions(-) diff --git a/alfa-client/apps/admin/src/app/app.component.ts b/alfa-client/apps/admin/src/app/app.component.ts index 1dbfc188e3..56a4563089 100644 --- a/alfa-client/apps/admin/src/app/app.component.ts +++ b/alfa-client/apps/admin/src/app/app.component.ts @@ -40,7 +40,7 @@ import { OrgaUnitIconComponent, UsersIconComponent, } from '@ods/system'; -import { filter, from, Observable, Subscription } 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'; @@ -83,9 +83,7 @@ export class AppComponent implements OnInit { public readonly routes = ROUTES; ngOnInit(): void { - from(this.authenticationService.login()).subscribe(() => { - this.doAfterLoggedIn(); - }); + this.authenticationService.login().then(() => this.doAfterLoggedIn()); } doAfterLoggedIn(): void { diff --git a/alfa-client/apps/admin/src/app/app.guard.spec.ts b/alfa-client/apps/admin/src/app/app.guard.spec.ts index 77828836d1..41cfe75063 100644 --- a/alfa-client/apps/admin/src/app/app.guard.spec.ts +++ b/alfa-client/apps/admin/src/app/app.guard.spec.ts @@ -27,6 +27,7 @@ import { createStateResource, LinkRelationName, StateResource } from '@alfa-clie import { Mock, mock, useFromMock } from '@alfa-client/test-utils'; import { TestBed } from '@angular/core/testing'; import { ActivatedRouteSnapshot, Router, UrlTree } from '@angular/router'; +import { AuthenticationService } from '@authentication'; import { Resource } from '@ngxp/rest'; import { createConfigurationResource } from 'libs/admin/configuration-shared/test/configuration'; import { createApiRootResource } from 'libs/api-root-shared/test/api-root'; @@ -35,7 +36,6 @@ import { createDummyResource } from 'libs/tech-shared/test/resource'; import { Observable, of } from 'rxjs'; import { GuardData } from './app.routes'; -import { AuthenticationService } from '@authentication'; import * as Guard from './app.guard'; describe('Guard', () => { @@ -63,7 +63,6 @@ describe('Guard', () => { beforeEach(() => { apiRootService.getApiRoot.mockReturnValue(apiRootStateResource$); jest.spyOn(Guard, 'evaluate').mockReturnValue(of(true)); - jest.spyOn(Guard, 'login').mockReturnValue(of(undefined)); }); afterEach(() => { @@ -112,7 +111,6 @@ describe('Guard', () => { beforeEach(() => { configurationService.get.mockReturnValue(configurationStateResource$); jest.spyOn(Guard, 'evaluate').mockReturnValue(of(true)); - jest.spyOn(Guard, 'login').mockReturnValue(of(undefined)); }); afterEach(() => { @@ -231,7 +229,7 @@ describe('Guard', () => { it('should return true if link exists', () => { const result: boolean = <boolean>( - Guard.evaluateResource(createDummyResource([DummyLinkRel.DUMMY]), DummyLinkRel.DUMMY, null) + Guard.evaluateResource(createDummyResource([DummyLinkRel.DUMMY]), DummyLinkRel.DUMMY, useFromMock(router)) ); expect(result).toBeTruthy(); diff --git a/alfa-client/apps/admin/src/app/app.guard.ts b/alfa-client/apps/admin/src/app/app.guard.ts index d131726812..c61df188c0 100644 --- a/alfa-client/apps/admin/src/app/app.guard.ts +++ b/alfa-client/apps/admin/src/app/app.guard.ts @@ -27,11 +27,11 @@ import { ApiRootService } from '@alfa-client/api-root-shared'; import { LinkRelationName, mapToResource, StateResource } from '@alfa-client/tech-shared'; import { inject } from '@angular/core'; import { ActivatedRouteSnapshot, CanActivateFn, Router, UrlTree } from '@angular/router'; +import { AuthenticationService } from '@authentication'; import { hasLink, Resource } from '@ngxp/rest'; -import { filter, from, map, Observable, of } from 'rxjs'; +import { filter, map, Observable, of } from 'rxjs'; import { GuardData } from './app.routes'; -import { AuthenticationService } from '@authentication'; import * as Guard from './app.guard'; export const apiRootGuard: CanActivateFn = (route: ActivatedRouteSnapshot): Observable<true | UrlTree> => { @@ -44,11 +44,6 @@ export const configurationGuard: CanActivateFn = (route: ActivatedRouteSnapshot) return Guard.evaluate(configurationService.get(), getLinkRelationName(route)); }; -export function login(): Observable<void> { - const authService: AuthenticationService = inject(AuthenticationService); - return authService.isLoggedIn() ? of(undefined) : from(authService.login()); -} - export function evaluate( stateResource$: Observable<StateResource<Resource>>, linkRelationName: LinkRelationName, diff --git a/alfa-client/libs/authentication/src/lib/authentication.service.spec.ts b/alfa-client/libs/authentication/src/lib/authentication.service.spec.ts index 79930137f9..0e2ba5d44d 100644 --- a/alfa-client/libs/authentication/src/lib/authentication.service.spec.ts +++ b/alfa-client/libs/authentication/src/lib/authentication.service.spec.ts @@ -59,13 +59,13 @@ describe('AuthenticationService', () => { describe('login', () => { beforeEach(() => { service.buildAuthEventPromise = jest.fn(); - service._initialNavigationDone = jest.fn().mockResolvedValue(() => Promise.resolve()); + service._initialNavigationIsDone = jest.fn().mockResolvedValue(() => Promise.resolve()); }); it('should wait for initial navigation', async () => { await service.login(); - expect(service._initialNavigationDone).toHaveBeenCalled(); + expect(service._initialNavigationIsDone).toHaveBeenCalled(); }); it('should configure service with authConfig', async () => { @@ -115,10 +115,11 @@ describe('AuthenticationService', () => { }); }); - describe('_initialNavigationDone', () => { + describe('_initialNavigationIsDone', () => { it('should wait for navigation end event', async () => { (router.events as any) = of(new NavigationEnd(0, 'url1', 'url1')); - const promise: Promise<void> = service._initialNavigationDone(); + + const promise: Promise<void> = service._initialNavigationIsDone(); await expect(promise).resolves.toBeUndefined(); }); @@ -376,5 +377,13 @@ describe('AuthenticationService', () => { expect(oAuthService.hasValidAccessToken).toHaveBeenCalled(); }); + + it('should return result', () => { + oAuthService.hasValidAccessToken.mockReturnValue(true); + + const isLoggedIn: boolean = service.isLoggedIn(); + + expect(isLoggedIn).toBe(true); + }); }); }); diff --git a/alfa-client/libs/authentication/src/lib/authentication.service.ts b/alfa-client/libs/authentication/src/lib/authentication.service.ts index 86617cc6bf..4918b8f8d7 100644 --- a/alfa-client/libs/authentication/src/lib/authentication.service.ts +++ b/alfa-client/libs/authentication/src/lib/authentication.service.ts @@ -43,7 +43,7 @@ export class AuthenticationService { ) {} public async login(): Promise<void> { - await this._initialNavigationDone(); + await this._initialNavigationIsDone(); this.oAuthService.configure(this.buildConfiguration()); this.oAuthService.setupAutomaticSilentRefresh(); @@ -54,7 +54,7 @@ export class AuthenticationService { return eventPromise; } - async _initialNavigationDone(): Promise<void> { + async _initialNavigationIsDone(): Promise<void> { await firstValueFrom(this.router.events.pipe(filter((event) => event instanceof NavigationEnd))); } -- GitLab From 2c3da852fad82db7b1034103740b12328d3989ed Mon Sep 17 00:00:00 2001 From: Martin <git@mail.de> Date: Mon, 24 Feb 2025 09:34:31 +0100 Subject: [PATCH 09/24] OZG-7619 add tooltip --- .../user-form-roles.component.html | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/alfa-client/libs/admin/user/src/lib/user-form/user-form-roles/user-form-roles.component.html b/alfa-client/libs/admin/user/src/lib/user-form/user-form-roles/user-form-roles.component.html index ed983c5f14..6615de8962 100644 --- a/alfa-client/libs/admin/user/src/lib/user-form/user-form-roles/user-form-roles.component.html +++ b/alfa-client/libs/admin/user/src/lib/user-form/user-form-roles/user-form-roles.component.html @@ -3,17 +3,22 @@ <div [formGroupName]="UserFormService.CLIENT_ROLES" class="mb-8 flex gap-56"> <div [formGroupName]="UserFormService.ADMINISTRATION_GROUP" class="flex flex-col gap-2"> <h3 class="text-md block font-medium text-text">Administration</h3> - <ods-checkbox-editor - [formControlName]="UserFormService.DATENBEAUFTRAGUNG" - label="Datenbeauftragung" - inputId="datenbeauftragung" - /> <div class="flex items-center gap-2"> <ods-checkbox-editor [formControlName]="UserFormService.ADMIN" label="Admin" inputId="admin" /> <ods-info-icon tooltip='Wird nur in Kombination mit "User" verwendet. Diese Rolle kann Funktionen in Keycloak und der Administration konfigurieren, z.B. Benutzer anlegen, Gruppen erstellen bzw. Organisationseinheiten hinzufügen und Rollen zuweisen.' /> </div> + <div class="flex items-center gap-2"> + <ods-checkbox-editor + [formControlName]="UserFormService.DATENBEAUFTRAGUNG" + label="Datenbeauftragung" + inputId="datenbeauftragung" + /> + <ods-info-icon + tooltip='Diese Rolle kann in der Administration unter dem Menüpunkt "Statistik" Felder zur Auswertung konfigurieren. Sie ist mit allen anderen Rollen kompatibel.' + /> + </div> </div> <div [formGroupName]="UserFormService.ALFA_GROUP" class="flex flex-col gap-2"> <h3 class="text-md block font-medium text-text">Alfa</h3> -- GitLab From 61b2ae24af1afd61696b01a2223f3e8b2437710c Mon Sep 17 00:00:00 2001 From: Martin <git@mail.de> Date: Mon, 24 Feb 2025 10:11:51 +0100 Subject: [PATCH 10/24] OZG-7590 fix patch error --- .../keycloak-shared/src/lib/form.util.spec.ts | 15 ++++++++++++--- .../admin/keycloak-shared/src/lib/form.util.ts | 4 +++- alfa-client/libs/test-utils/src/lib/mocking.ts | 6 ++++++ 3 files changed, 21 insertions(+), 4 deletions(-) diff --git a/alfa-client/libs/admin/keycloak-shared/src/lib/form.util.spec.ts b/alfa-client/libs/admin/keycloak-shared/src/lib/form.util.spec.ts index be3f115f38..348a6c4b49 100644 --- a/alfa-client/libs/admin/keycloak-shared/src/lib/form.util.spec.ts +++ b/alfa-client/libs/admin/keycloak-shared/src/lib/form.util.spec.ts @@ -1,18 +1,18 @@ +import { mockWindowError } from '@alfa-client/test-utils'; import { FormControl, FormGroup } from '@angular/forms'; import { patchForm } from './form.util'; describe('FormUtil', () => { describe('patch form', () => { it('should not throw any errors', () => { - const errorHandler = jest.fn(); - window.onerror = errorHandler; + const errorSpy: jest.SpyInstance = mockWindowError(); const formGroup: FormGroup = new FormGroup({ existingKey: new FormControl(null), }); patchForm({ missingKey: 'dummyValue' }, formGroup); - expect(errorHandler).not.toHaveBeenCalled(); + expect(errorSpy).not.toHaveBeenCalled(); }); describe('on strings', () => { @@ -57,6 +57,15 @@ describe('FormUtil', () => { expect(arrayFormGroup.controls['arrayValue1'].value).toBeTruthy(); expect(arrayFormGroup.controls['arrayValue2'].value).toBeFalsy(); }); + + it('should not throw any error if control is missing', () => { + const errorSpy: jest.SpyInstance = mockWindowError(); + const formGroup: FormGroup = new FormGroup({ array: new FormGroup({}) }); + + patchForm({ array: ['arrayValue1'] }, formGroup); + + expect(errorSpy).not.toHaveBeenCalled(); + }); }); describe('on object value', () => { diff --git a/alfa-client/libs/admin/keycloak-shared/src/lib/form.util.ts b/alfa-client/libs/admin/keycloak-shared/src/lib/form.util.ts index 877cf9e458..d57cf61935 100644 --- a/alfa-client/libs/admin/keycloak-shared/src/lib/form.util.ts +++ b/alfa-client/libs/admin/keycloak-shared/src/lib/form.util.ts @@ -15,7 +15,9 @@ function patchNonStringValues(valueToPatch: any, formGroup: FormGroup): void { function patchNonStringValue(value: any, formGroup: FormGroup): void { if (Array.isArray(value)) { - value.forEach((oneValue: any) => formGroup.controls[oneValue].patchValue(true)); + value.forEach((oneValue: any) => { + if (formGroup.contains(oneValue)) formGroup.controls[oneValue].patchValue(true); + }); } else if (!isString(value) && !isBoolean(value)) { patchNonStringValues(value, formGroup); } diff --git a/alfa-client/libs/test-utils/src/lib/mocking.ts b/alfa-client/libs/test-utils/src/lib/mocking.ts index 0fb616db60..20ac6152a4 100644 --- a/alfa-client/libs/test-utils/src/lib/mocking.ts +++ b/alfa-client/libs/test-utils/src/lib/mocking.ts @@ -53,3 +53,9 @@ export function mockGetValue(object: any, name: string, returnValue: any): void get: jest.fn(() => returnValue), }); } + +export function mockWindowError(): any { + const errorHandler = jest.fn(); + window.onerror = errorHandler; + return errorHandler; +} -- GitLab From 21c719b216aba55fc655c38d46175063b5e24efd Mon Sep 17 00:00:00 2001 From: cord <cord.westhoff@mgm-tp.com> Date: Mon, 24 Feb 2025 13:05:20 +0100 Subject: [PATCH 11/24] OZG-7590 OZG-7803 add test for Daria, remove Zelda from tests --- .../src/components/benutzer/benutzer.e2e.component.ts | 9 +++++++++ .../main-tests/benutzer_rollen/benutzer_rollen.cy.ts | 11 +++++++---- alfa-client/apps/admin-e2e/src/support/user-util.ts | 4 +++- 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/alfa-client/apps/admin-e2e/src/components/benutzer/benutzer.e2e.component.ts b/alfa-client/apps/admin-e2e/src/components/benutzer/benutzer.e2e.component.ts index 0af0f39b14..030fead80f 100644 --- a/alfa-client/apps/admin-e2e/src/components/benutzer/benutzer.e2e.component.ts +++ b/alfa-client/apps/admin-e2e/src/components/benutzer/benutzer.e2e.component.ts @@ -33,6 +33,7 @@ export class BenutzerE2EComponent { private readonly userBenutzername: string = 'Benutzername-text-input'; private readonly userMail: string = 'E-Mail-text-input'; private readonly adminCheckbox: string = 'Admin-checkbox-editor'; + private readonly datenCheckbox: string = 'Datenbeauftragung-checkbox-editor' private readonly loeschenCheckbox: string = 'Loschen-checkbox-editor'; private readonly userCheckbox: string = 'User-checkbox-editor'; private readonly postCheckbox: string = 'Poststelle-checkbox-editor'; @@ -133,6 +134,14 @@ export class BenutzerE2EComponent { this.getAdminCheckbox().click(); } + public getDatenCheckbox(): Cypress.Chainable<Element> { + return cy.getTestElement(this.datenCheckbox); + } + + public clickDatenCheckbox(): void { + this.getDatenCheckbox().click(); + } + public getLoeschenCheckbox() { return cy.getTestElement(this.loeschenCheckbox); } diff --git a/alfa-client/apps/admin-e2e/src/e2e/main-tests/benutzer_rollen/benutzer_rollen.cy.ts b/alfa-client/apps/admin-e2e/src/e2e/main-tests/benutzer_rollen/benutzer_rollen.cy.ts index 9514df893c..27b84ed666 100644 --- a/alfa-client/apps/admin-e2e/src/e2e/main-tests/benutzer_rollen/benutzer_rollen.cy.ts +++ b/alfa-client/apps/admin-e2e/src/e2e/main-tests/benutzer_rollen/benutzer_rollen.cy.ts @@ -48,11 +48,9 @@ describe('Benutzer und Rollen', () => { it('should show users and attributes in table', () => { exist(benutzerPage.getUserEntry('ariane')); benutzerPage.stringExistsInUserEntry(AlfaRollen.USER, AlfaUsers.DOROTHEA); + benutzerPage.stringExistsInUserEntry(AlfaRollen.DATEN, AlfaUsers.DARIA); benutzerPage.stringExistsInUserEntry(organisationsEinheitOrdnungsamt, AlfaUsers.LUDWIG); - benutzerPage.stringExistsInUserEntry(AlfaRollen.USER, AlfaUsers.ZELDA); benutzerPage.stringExistsInUserEntry(AlfaRollen.LOESCHEN, AlfaUsers.LUDWIG); - benutzerPage.stringExistsInUserEntry(organisationsEinheitDenkmalpflege, AlfaUsers.ZELDA); - benutzerPage.stringExistsInUserEntry(organisationsEinheitWirtschaftsfoerderung, AlfaUsers.ZELDA); benutzerPage.stringExistsInUserEntry(organisationsEinheitNone, AlfaUsers.RICHARD); benutzerPage.stringExistsInUserEntry(emailAddress, AlfaUsers.PETER); benutzerPage.stringExistsInUserEntry(AlfaRollen.POSTSTELLE, AlfaUsers.PETER); @@ -67,6 +65,7 @@ describe('Benutzer und Rollen', () => { exist(benutzerPage.getMailInput()); notBeChecked(benutzerPage.getAdminCheckbox()); + notBeChecked(benutzerPage.getDatenCheckbox()); notBeChecked(benutzerPage.getLoeschenCheckbox()); notBeChecked(benutzerPage.getUserCheckbox()); notBeChecked(benutzerPage.getPostCheckbox()); @@ -84,14 +83,18 @@ describe('Benutzer und Rollen', () => { beEnabled(benutzerPage.getPostCheckbox()); }); - it('should additionally activate and deactivate admin checkbox', () => { + it('should additionally activate and deactivate admin and data checkbox', () => { benutzerPage.clickLoeschenCheckbox(); benutzerPage.clickAdminCheckbox(); + benutzerPage.clickDatenCheckbox(); beChecked(benutzerPage.getLoeschenCheckbox()); beChecked(benutzerPage.getAdminCheckbox()); + beChecked(benutzerPage.getDatenCheckbox()); benutzerPage.clickAdminCheckbox(); + benutzerPage.clickDatenCheckbox(); notBeChecked(benutzerPage.getAdminCheckbox()); + notBeChecked(benutzerPage.getDatenCheckbox()); }); it('should activate user checkbox and deactivate the other two checkboxes', () => { diff --git a/alfa-client/apps/admin-e2e/src/support/user-util.ts b/alfa-client/apps/admin-e2e/src/support/user-util.ts index 46f07823c2..5fdf112e6f 100644 --- a/alfa-client/apps/admin-e2e/src/support/user-util.ts +++ b/alfa-client/apps/admin-e2e/src/support/user-util.ts @@ -85,10 +85,12 @@ export enum AlfaRollen { USER = 'VERWALTUNG_USER', LOESCHEN = 'VERWALTUNG_LOESCHEN', POSTSTELLE = 'VERWALTUNG_POSTSTELLE', - ADMIN = 'ADMIN_ADMIN' + ADMIN = 'ADMIN_ADMIN', + DATEN = 'DATENBEAUFTRAGUNG', } export enum AlfaUsers { + DARIA = 'daria', DOROTHEA = 'dorothea', LUDWIG = 'ludwig', PETER = 'peter', -- GitLab From dc5225bcf1930b57ca53b2e5d35126d9f66f55a2 Mon Sep 17 00:00:00 2001 From: Albert <Albert.Bruns@mgm-tp.com> Date: Mon, 24 Feb 2025 15:30:26 +0100 Subject: [PATCH 12/24] OZG-7797 rollen loeschen --- .../src/lib/user.repository.spec.ts | 179 ++++++++++++++---- .../src/lib/user.repository.ts | 89 ++++++--- 2 files changed, 207 insertions(+), 61 deletions(-) diff --git a/alfa-client/libs/admin/keycloak-shared/src/lib/user.repository.spec.ts b/alfa-client/libs/admin/keycloak-shared/src/lib/user.repository.spec.ts index ae9c56bedd..b4d1dc6f35 100644 --- a/alfa-client/libs/admin/keycloak-shared/src/lib/user.repository.spec.ts +++ b/alfa-client/libs/admin/keycloak-shared/src/lib/user.repository.spec.ts @@ -30,10 +30,10 @@ import { faker } from '@faker-js/faker'; import KcAdminClient from '@keycloak/keycloak-admin-client'; import GroupRepresentation from '@keycloak/keycloak-admin-client/lib/defs/groupRepresentation'; import MappingsRepresentation from '@keycloak/keycloak-admin-client/lib/defs/mappingsRepresentation'; -import RoleRepresentation, { RoleMappingPayload } from '@keycloak/keycloak-admin-client/lib/defs/roleRepresentation'; +import { RoleMappingPayload } from '@keycloak/keycloak-admin-client/lib/defs/roleRepresentation'; import { Users } from '@keycloak/keycloak-admin-client/lib/resources/users'; import { cold } from 'jest-marbles'; -import { omit } from 'lodash-es'; +import { omit, times } from 'lodash-es'; import { throwError } from 'rxjs'; import { createUser } from '../../../user-shared/test/user'; import { UserFormService } from '../../../user/src/lib/user-form/user.formservice'; @@ -71,18 +71,18 @@ describe('UserRepository', () => { expect(kcAdminClient.users['create']).toHaveBeenCalledWith(omit(user, 'groupIds')); }); - it('should call addUserRoles', fakeAsync(() => { - repository._addUserRoles = jest.fn(); + it('should call updateUserRoles', fakeAsync(() => { + repository._updateUserRoles = jest.fn(); repository.createInKeycloak(user).subscribe(); tick(); - expect(repository._addUserRoles).toHaveBeenCalledWith(user.id, user.clientRoles); + expect(repository._updateUserRoles).toHaveBeenCalledWith(user.id, user.clientRoles); })); it('should call sendActivationMail', (done) => { repository._sendActivationMail = jest.fn(); - repository._addUserRoles = jest.fn().mockReturnValue(Promise.resolve()); + repository._updateUserRoles = jest.fn().mockReturnValue(Promise.resolve()); repository.createInKeycloak(user).subscribe(() => { expect(repository._sendActivationMail).toHaveBeenCalledWith(user.id); @@ -107,7 +107,7 @@ describe('UserRepository', () => { update: jest.fn().mockReturnValue(Promise.resolve()), }; - repository._addUserRoles = jest.fn().mockReturnValue(Promise.resolve()); + repository._updateUserRoles = jest.fn().mockReturnValue(Promise.resolve()); repository._updateUserGroups = jest.fn().mockReturnValue(Promise.resolve()); }); @@ -117,11 +117,11 @@ describe('UserRepository', () => { expect(kcAdminClient.users['update']).toHaveBeenCalledWith({ id: user.id }, omit(user, 'groupIds')); }); - it('should call addUserRoles', fakeAsync(() => { + it('should call updateUserRoles', fakeAsync(() => { repository.saveInKeycloak(user).subscribe(); tick(); - expect(repository._addUserRoles).toHaveBeenCalledWith(user.id, user.clientRoles); + expect(repository._updateUserRoles).toHaveBeenCalledWith(user.id, user.clientRoles); })); it('should call updateUserGroups', (done) => { @@ -241,70 +241,155 @@ describe('UserRepository', () => { }); }); - describe('addUserRoles', () => { - it('should call addUserRolesForClient for admin', async () => { - repository._addUserRolesForClient = jest.fn(); + describe('UpdateUserRoles', () => { + it('should call UpdateUserRolesForClient for admin', async () => { + repository._updateUserRolesForClient = jest.fn(); - await repository._addUserRoles(user.id, { admin: [UserFormService.ADMIN], alfa: [] }); + await repository._updateUserRoles(user.id, { admin: [UserFormService.ADMIN], alfa: [] }); - expect(repository._addUserRolesForClient).toHaveBeenCalledWith( + expect(repository._updateUserRolesForClient).toHaveBeenCalledWith( user.id, [UserFormService.ADMIN], UserRepository.ADMIN_CLIENT_NAME, ); }); - it('should call addUserRolesForClient for admin', async () => { - repository._addUserRolesForClient = jest.fn(); + it('should call UpdateUserRolesForClient for admin', async () => { + repository._updateUserRolesForClient = jest.fn(); - await repository._addUserRoles(user.id, { alfa: [UserFormService.POSTSTELLE], admin: [] }); + await repository._updateUserRoles(user.id, { alfa: [UserFormService.POSTSTELLE], admin: [] }); - expect(repository._addUserRolesForClient).toHaveBeenCalledWith( + expect(repository._updateUserRolesForClient).toHaveBeenCalledWith( user.id, [UserFormService.POSTSTELLE], UserRepository.ALFA_CLIENT_NAME, ); }); + }); + + describe('updateUserRolesForClient', () => { + const client: string = faker.word.sample(); + const clientId: string = faker.string.uuid(); + const clientRoles: string[] = times(3, () => faker.word.sample()); + const clientRolesMapping: RoleMappingPayload[] = clientRoles.map((role: string) => ({ id: faker.string.uuid(), name: role })); + const oldClientRoles: RoleMappingPayload[] = clientRolesMapping.slice(1); + + beforeEach(() => { + repository._getOldUserRoles = jest.fn().mockReturnValue(Promise.resolve(oldClientRoles)); + repository._getClientId = jest.fn().mockReturnValue(Promise.resolve(clientId)); + repository._mapUserRoles = jest.fn().mockReturnValue(Promise.resolve(clientRolesMapping)); + repository._deleteUserRoles = jest.fn(); + repository._addUserRoles = jest.fn(); + }); + + it('should call getClientId', async () => { + await repository._updateUserRolesForClient(user.id, clientRoles, client); + + expect(repository._getClientId).toHaveBeenCalledWith(client); + }); + + it('should call mapUserRoles', async () => { + await repository._updateUserRolesForClient(user.id, clientRoles, client); + + expect(repository._mapUserRoles).toHaveBeenCalledWith(clientId, clientRoles); + }); + + it('should call getOldUserRoles', async () => { + await repository._updateUserRolesForClient(user.id, clientRoles, client); + + expect(repository._getOldUserRoles).toHaveBeenCalledWith(user.id, clientId); + }); - it('should not call addUserRolesForClient if clientRoles alfa and admin are empty', async () => { - repository._addUserRolesForClient = jest.fn(); + it('should call deleteUserRoles', async () => { + await repository._updateUserRolesForClient(user.id, clientRoles, client); - await repository._addUserRoles(user.id, { admin: [], alfa: [] }); + expect(repository._deleteUserRoles).toHaveBeenCalledWith(user.id, clientRolesMapping, oldClientRoles, clientId); + }); - expect(repository._addUserRolesForClient).not.toHaveBeenCalled(); + it('should call addUserRoles', async () => { + await repository._updateUserRolesForClient(user.id, clientRoles, client); + + expect(repository._addUserRoles).toHaveBeenCalledWith(user.id, clientRolesMapping, oldClientRoles, clientId); }); }); - describe('addUserRolesForClient', () => { + describe('getOldUserRoles', () => { const clientId: string = faker.string.uuid(); const roleMapping: RoleMappingPayload[] = [{ id: faker.string.uuid(), name: faker.word.sample() }]; beforeEach(() => { - repository._getClientId = jest.fn().mockReturnValue(Promise.resolve(clientId)); - repository._mapUserRoles = jest.fn().mockReturnValue(Promise.resolve(roleMapping)); - repository._addUserRolesInKeycloak = jest.fn(); + kcAdminClient.users = <any>{ + listClientRoleMappings: jest.fn().mockReturnValue(Promise.resolve(roleMapping)), + }; + }); + + it('should call users listClientRoleMappings', () => { + repository._getOldUserRoles(user.id, clientId); + + expect(kcAdminClient.users['listClientRoleMappings']).toHaveBeenCalledWith({ id: user.id, clientUniqueId: clientId }); + }); + + it('should return roleMapping', async () => { + const result: RoleMappingPayload[] = await repository._getOldUserRoles(user.id, clientId); + + expect(result).toEqual(roleMapping); + }); + }); + + describe('deleteUserRoles', () => { + const clientId: string = faker.string.uuid(); + const clientRoles: string[] = times(3, () => faker.word.sample()); + const oldClientRolesMapping: RoleMappingPayload[] = clientRoles.map((role: string) => ({ + id: faker.string.uuid(), + name: role, + })); + const clientRolesMapping: RoleMappingPayload[] = oldClientRolesMapping.slice(1); + + beforeEach(() => { + repository._deleteUserRolesInKeycloak = jest.fn(); kcAdminClient.users = <any>{ addClientRoleMappings: jest.fn().mockReturnValue(Promise.resolve()), }; }); - it('should call getClientId', async () => { - await repository._addUserRolesForClient(user.id, [UserFormService.ADMIN], UserRepository.ADMIN_CLIENT_NAME); + it('should call deleteUserRolesInKeycloak', async () => { + await repository._deleteUserRoles(user.id, clientRolesMapping, oldClientRolesMapping, clientId); + + expect(repository._deleteUserRolesInKeycloak).toHaveBeenCalledWith(user.id, clientId, [oldClientRolesMapping[0]]); + }); + + it('should call not call deleteUserRolesInKeycloak if no roles to delete', async () => { + await repository._deleteUserRoles(user.id, clientRolesMapping, clientRolesMapping, clientId); - expect(repository._getClientId).toHaveBeenCalled(); + expect(repository._deleteUserRolesInKeycloak).not.toHaveBeenCalled(); }); + }); - it('should call getAlfaClientId', async () => { - await repository._addUserRolesForClient(user.id, [UserFormService.ADMIN], UserRepository.ADMIN_CLIENT_NAME); + describe('addUserRoles', () => { + const clientId: string = faker.string.uuid(); + const clientRoles: string[] = times(3, () => faker.word.sample()); + const clientRolesMapping: RoleMappingPayload[] = clientRoles.map((role: string) => ({ id: faker.string.uuid(), name: role })); + const oldClientRolesMapping: RoleMappingPayload[] = clientRolesMapping.slice(1); - expect(repository._mapUserRoles).toHaveBeenCalledWith(clientId, [UserFormService.ADMIN]); + beforeEach(() => { + repository._addUserRolesInKeycloak = jest.fn(); + + kcAdminClient.users = <any>{ + addClientRoleMappings: jest.fn().mockReturnValue(Promise.resolve()), + }; }); it('should call addUserRolesInKeycloak', async () => { - await repository._addUserRolesForClient(user.id, [UserFormService.ADMIN], UserRepository.ADMIN_CLIENT_NAME); + await repository._addUserRoles(user.id, clientRolesMapping, oldClientRolesMapping, clientId); - expect(repository._addUserRolesInKeycloak).toHaveBeenCalledWith(user.id, clientId, roleMapping); + expect(repository._addUserRolesInKeycloak).toHaveBeenCalledWith(user.id, clientId, [clientRolesMapping[0]]); + }); + + it('should not call addUserRolesInKeycloak if no roles to add', async () => { + await repository._addUserRoles(user.id, clientRolesMapping, clientRolesMapping, clientId); + + expect(repository._addUserRolesInKeycloak).not.toHaveBeenCalled(); }); }); @@ -332,11 +417,11 @@ describe('UserRepository', () => { describe('mapUserRoles', () => { const clientId: string = faker.string.uuid(); - const clientRoles: RoleRepresentation[] = Array.from({ length: 3 }, () => ({ + const clientRoles: RoleMappingPayload[] = Array.from({ length: 3 }, () => ({ id: faker.string.uuid(), name: faker.word.sample(), })); - const userRoles: string[] = clientRoles.map((role) => role.name).slice(1); + const userRoles: string[] = clientRoles.map((role: RoleMappingPayload): string => role.name).slice(1); beforeEach(() => { kcAdminClient.clients = <any>{ @@ -353,7 +438,7 @@ describe('UserRepository', () => { it('should return roleMapping', async () => { const result: RoleMappingPayload[] = await repository._mapUserRoles(clientId, userRoles); - expect(result).toEqual(clientRoles.slice(1).map((role) => ({ id: role.id, name: role.name }))); + expect(result).toEqual(clientRoles.slice(1)); }); it('should filter roles if they are not in clientRoles', async () => { @@ -383,6 +468,26 @@ describe('UserRepository', () => { }); }); + describe('deleteUserRolesInKeycloak', () => { + beforeEach(() => { + kcAdminClient.users = <any>{ + delClientRoleMappings: jest.fn().mockReturnValue(Promise.resolve()), + }; + }); + + it('should call kcAdminClient users delClientRoleMappings', async () => { + const clientId: string = faker.string.uuid(); + const roles: RoleMappingPayload[] = Array.from({ length: 3 }, () => ({ + id: faker.string.uuid(), + name: faker.word.sample(), + })); + + await repository._deleteUserRolesInKeycloak(user.id, clientId, roles); + + expect(kcAdminClient.users['delClientRoleMappings']).toHaveBeenCalledWith({ id: user.id, clientUniqueId: clientId, roles }); + }); + }); + describe('sendActivationMail', () => { it('should call kcAdminClient users executeActionsEmail', () => { const userId: string = faker.string.uuid(); diff --git a/alfa-client/libs/admin/keycloak-shared/src/lib/user.repository.ts b/alfa-client/libs/admin/keycloak-shared/src/lib/user.repository.ts index 130176623c..7285510ddd 100644 --- a/alfa-client/libs/admin/keycloak-shared/src/lib/user.repository.ts +++ b/alfa-client/libs/admin/keycloak-shared/src/lib/user.repository.ts @@ -28,9 +28,10 @@ import KcAdminClient from '@keycloak/keycloak-admin-client'; import ClientRepresentation from '@keycloak/keycloak-admin-client/lib/defs/clientRepresentation'; import GroupRepresentation from '@keycloak/keycloak-admin-client/lib/defs/groupRepresentation'; import MappingsRepresentation from '@keycloak/keycloak-admin-client/lib/defs/mappingsRepresentation'; -import RoleRepresentation, { RoleMappingPayload } from '@keycloak/keycloak-admin-client/lib/defs/roleRepresentation'; +import { RoleMappingPayload } from '@keycloak/keycloak-admin-client/lib/defs/roleRepresentation'; import UserRepresentation from '@keycloak/keycloak-admin-client/lib/defs/userRepresentation'; -import { isNil, omit } from 'lodash-es'; +import * as _ from 'lodash-es'; +import { isNil } from 'lodash-es'; import { catchError, concatMap, forkJoin, from, map, mergeMap, Observable, tap, throwError } from 'rxjs'; @Injectable({ @@ -43,9 +44,9 @@ export class UserRepository { public static readonly ADMIN_CLIENT_NAME: string = 'admin'; public createInKeycloak(user: User): Observable<User> { - return from(this.kcAdminClient.users.create(omit(user, 'groupIds'))).pipe( + return from(this.kcAdminClient.users.create(_.omit(user, 'groupIds'))).pipe( concatMap(async (response: { id: string }): Promise<{ id: string }> => { - await this._addUserRoles(response.id, user.clientRoles); + await this._updateUserRoles(response.id, user.clientRoles); return response; }), tap((response: { id: string }): void => this._sendActivationMail(response.id)), @@ -58,7 +59,7 @@ export class UserRepository { const { groupIds, ...userToSave } = user; return from(this.kcAdminClient.users.update({ id: user.id }, userToSave)).pipe( concatMap(async (): Promise<void> => { - await this._addUserRoles(user.id, user.clientRoles); + await this._updateUserRoles(user.id, user.clientRoles); await this._updateUserGroups(user.id, user.groupIds); }), map((): User => user), @@ -73,8 +74,10 @@ export class UserRepository { } async _getOldUserGroupIds(userId: string): Promise<string[]> { - const oldUserGroupsReps: GroupRepresentation[] = await this.kcAdminClient.users.listGroups({ id: userId }); - return oldUserGroupsReps.map((group: GroupRepresentation): string => group.id); + const oldUserGroupsReps: RoleMappingPayload[] = <RoleMappingPayload[]>( + await this.kcAdminClient.users.listGroups({ id: userId }) + ); + return _.map(oldUserGroupsReps, 'id'); } async _deleteUserGroups(userId: string, newGroupIds: string[], oldGroupIds: string[]): Promise<void> { @@ -87,7 +90,7 @@ export class UserRepository { ); } - async _addUserGroups(userId: string, newGroupIds: string[], oldGroupIds): Promise<void> { + async _addUserGroups(userId: string, newGroupIds: string[], oldGroupIds: string[]): Promise<void> { await Promise.all( newGroupIds .filter((group) => !oldGroupIds.includes(group)) @@ -97,20 +100,52 @@ export class UserRepository { ); } - async _addUserRoles(userId: string, clientRoles: ClientRoles): Promise<void> { - if (clientRoles.admin.length > 0) { - await this._addUserRolesForClient(userId, clientRoles.admin, UserRepository.ADMIN_CLIENT_NAME); - } + async _updateUserRoles(userId: string, clientRoles: ClientRoles): Promise<void> { + await this._updateUserRolesForClient(userId, clientRoles.admin, UserRepository.ADMIN_CLIENT_NAME); + await this._updateUserRolesForClient(userId, clientRoles.alfa, UserRepository.ALFA_CLIENT_NAME); + } + + async _updateUserRolesForClient(userId: string, clientRoles: string[], client: string): Promise<void> { + const clientId: string = await this._getClientId(client); + const clientRolesMapping: RoleMappingPayload[] = await this._mapUserRoles(clientId, clientRoles); + const oldClientRolesMapping: RoleMappingPayload[] = await this._getOldUserRoles(userId, clientId); + await this._deleteUserRoles(userId, clientRolesMapping, oldClientRolesMapping, clientId); + await this._addUserRoles(userId, clientRolesMapping, oldClientRolesMapping, clientId); + } + + async _getOldUserRoles(userId: string, clientId: string): Promise<RoleMappingPayload[]> { + return <RoleMappingPayload[]>await this.kcAdminClient.users.listClientRoleMappings({ + id: userId, + clientUniqueId: clientId, + }); + } - if (clientRoles.alfa.length > 0) { - await this._addUserRolesForClient(userId, clientRoles.alfa, UserRepository.ALFA_CLIENT_NAME); + async _deleteUserRoles( + userId: string, + clientRolesMapping: RoleMappingPayload[], + oldClientRolesMapping: RoleMappingPayload[], + clientId: string, + ): Promise<void> { + const rolesToDelete: RoleMappingPayload[] = oldClientRolesMapping.filter( + (role: RoleMappingPayload) => !_.map(clientRolesMapping, 'name').includes(role.name), + ); + if (rolesToDelete.length > 0) { + await this._deleteUserRolesInKeycloak(userId, clientId, rolesToDelete); } } - async _addUserRolesForClient(userId: string, userRoles: string[], client: string): Promise<void> { - const clientId: string = await this._getClientId(client); - const roles: RoleMappingPayload[] = await this._mapUserRoles(clientId, userRoles); - await this._addUserRolesInKeycloak(userId, clientId, roles); + async _addUserRoles( + userId: string, + clientRolesMapping: RoleMappingPayload[], + oldClientRolesMapping: RoleMappingPayload[], + clientId: string, + ): Promise<void> { + const rolesToAdd: RoleMappingPayload[] = clientRolesMapping.filter( + (role: RoleMappingPayload) => !_.map(oldClientRolesMapping, 'name').includes(role.name), + ); + if (rolesToAdd.length > 0) { + await this._addUserRolesInKeycloak(userId, clientId, rolesToAdd); + } } async _getClientId(client: string): Promise<string | undefined> { @@ -119,10 +154,8 @@ export class UserRepository { } async _mapUserRoles(clientId: string, userRoles: string[]): Promise<RoleMappingPayload[]> { - const roles: RoleRepresentation[] = await this.kcAdminClient.clients.listRoles({ id: clientId }); - return roles - .filter((role: RoleRepresentation): boolean => userRoles.includes(role.name)) - .map((role: RoleRepresentation): RoleMappingPayload => ({ id: role.id, name: role.name })); + const roles: RoleMappingPayload[] = <RoleMappingPayload[]>await this.kcAdminClient.clients.listRoles({ id: clientId }); + return roles.filter((role: RoleMappingPayload): boolean => userRoles.includes(role.name)); } async _addUserRolesInKeycloak(userId: string, clientId: string, roles: RoleMappingPayload[]): Promise<void> { @@ -133,6 +166,14 @@ export class UserRepository { }); } + async _deleteUserRolesInKeycloak(userId: string, clientId: string, roles: RoleMappingPayload[]): Promise<void> { + await this.kcAdminClient.users.delClientRoleMappings({ + id: userId, + clientUniqueId: clientId, + roles, + }); + } + _sendActivationMail(userId: string): void { this.kcAdminClient.users.executeActionsEmail({ id: userId, @@ -204,7 +245,7 @@ export class UserRepository { private getUserGroups(user: User): Observable<string[]> { return from(this.kcAdminClient.users.listGroups({ id: user.id })).pipe( - map((groups: GroupRepresentation[]): string[] => groups.map((group: GroupRepresentation): string => group.name)), + map((groups: GroupRepresentation[]): string[] => _.map(groups, 'name')), ); } @@ -226,6 +267,6 @@ export class UserRepository { return []; } - return clientMappingsAlfa.mappings.map((role: RoleRepresentation): string => role.name); + return _.map(clientMappingsAlfa.mappings, 'name'); } } -- GitLab From febff64fc5269fe328b8336f889953d1f80edacf Mon Sep 17 00:00:00 2001 From: cord <cord.westhoff@mgm-tp.com> Date: Mon, 24 Feb 2025 16:32:00 +0100 Subject: [PATCH 13/24] OZG-7590 OZG-7803 finish test for data role --- .../src/e2e/main-tests/benutzer_rollen/benutzer_rollen.cy.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/alfa-client/apps/admin-e2e/src/e2e/main-tests/benutzer_rollen/benutzer_rollen.cy.ts b/alfa-client/apps/admin-e2e/src/e2e/main-tests/benutzer_rollen/benutzer_rollen.cy.ts index 27b84ed666..a35cddcb08 100644 --- a/alfa-client/apps/admin-e2e/src/e2e/main-tests/benutzer_rollen/benutzer_rollen.cy.ts +++ b/alfa-client/apps/admin-e2e/src/e2e/main-tests/benutzer_rollen/benutzer_rollen.cy.ts @@ -24,13 +24,11 @@ import { MainPage } from 'apps/admin-e2e/src/page-objects/main.po'; import { BenutzerE2EComponent } from '../../../components/benutzer/benutzer.e2e.component'; import { beChecked, beEnabled, exist, notBeChecked, notBeEnabled } from '../../../support/cypress.util'; -import { AlfaRollen, AlfaUsers, loginAsAriane } from '../../../support/user-util'; +import { AlfaRollen, AlfaUsers, loginAsAriane, loginAsDaria, logout } from '../../../support/user-util'; const mainPage: MainPage = new MainPage(); const benutzerPage: BenutzerE2EComponent = new BenutzerE2EComponent(); const organisationsEinheitOrdnungsamt: string = 'Ordnungsamt'; -const organisationsEinheitDenkmalpflege: string = 'Denkmalpflege'; -const organisationsEinheitWirtschaftsfoerderung: string = 'Wirtschaftsförderung'; const organisationsEinheitNone: string = 'keine zuständige Stelle zugewiesen'; const emailAddress: string = 'peter.von.der.post@ozg-sh.de'; -- GitLab From 24cf918cfdefd48264ad366087a5498e5cdfb9d6 Mon Sep 17 00:00:00 2001 From: cord <cord.westhoff@mgm-tp.com> Date: Mon, 24 Feb 2025 16:42:23 +0100 Subject: [PATCH 14/24] OZG-7590 OZG-7803 a little refactoring --- .../benutzer/benutzer.e2e.component.ts | 3 ++ .../benutzer_rollen/benutzer-anlegen.cy.ts | 39 ++++++++----------- alfa-client/apps/admin-e2e/src/model/util.ts | 1 + 3 files changed, 21 insertions(+), 22 deletions(-) diff --git a/alfa-client/apps/admin-e2e/src/components/benutzer/benutzer.e2e.component.ts b/alfa-client/apps/admin-e2e/src/components/benutzer/benutzer.e2e.component.ts index 030fead80f..80b969a0e1 100644 --- a/alfa-client/apps/admin-e2e/src/components/benutzer/benutzer.e2e.component.ts +++ b/alfa-client/apps/admin-e2e/src/components/benutzer/benutzer.e2e.component.ts @@ -61,6 +61,9 @@ export class BenutzerE2EComponent { if (user.isAdmin) { this.clickAdminCheckbox(); } + if (user.isData) { + this.clickDatenCheckbox(); + } if (user.isUser) { this.clickUserCheckbox(); } diff --git a/alfa-client/apps/admin-e2e/src/e2e/main-tests/benutzer_rollen/benutzer-anlegen.cy.ts b/alfa-client/apps/admin-e2e/src/e2e/main-tests/benutzer_rollen/benutzer-anlegen.cy.ts index 0b9aad6ab8..971716c8c1 100644 --- a/alfa-client/apps/admin-e2e/src/e2e/main-tests/benutzer_rollen/benutzer-anlegen.cy.ts +++ b/alfa-client/apps/admin-e2e/src/e2e/main-tests/benutzer_rollen/benutzer-anlegen.cy.ts @@ -2,31 +2,26 @@ import { MainPage } from 'apps/admin-e2e/src/page-objects/main.po'; import { BenutzerE2EComponent } from '../../../components/benutzer/benutzer.e2e.component'; import { beChecked, beEnabled, contains, exist, notBeChecked, notBeEnabled } from '../../../support/cypress.util'; -import { AlfaRollen, AlfaUsers, loginAsAriane } from '../../../support/user-util'; +import { AlfaRollen, AlfaUsers, loginAsAriane, logout } from '../../../support/user-util'; import { SnackBarE2EComponent } from '../../../components/ui/snackbar.e2e.component'; import { SnackbarMessagesE2E } from '../../../model/util'; import { getCypressEnv, interceptWithResponse, wait, waitOfInterceptor } from 'apps/admin-e2e/src/support/cypress-helper'; import { AdminUserE2E, HttpMethodE2E } from '../../../model/util'; -const mainPage: MainPage = new MainPage(); -const benutzerPage: BenutzerE2EComponent = new BenutzerE2EComponent(); -const snackBar: SnackBarE2EComponent = new SnackBarE2EComponent(); - -const vorname: string = 'Theo'; -const nachname: string = 'Testuser'; -const benutzername: string = 'testtheo'; -const emailAddress: string = 'theo.test@ozg-sh.de'; +describe('Benutzer anlegen', () => { + const mainPage: MainPage = new MainPage(); + const benutzerPage: BenutzerE2EComponent = new BenutzerE2EComponent(); + const snackBar: SnackBarE2EComponent = new SnackBarE2EComponent(); -const newUser: AdminUserE2E = { - vorname: 'Theo', - nachname: 'Testuser', - benutzername: 'testtheo', - email: 'theo.test@ozg-sh.de', - isAdmin: true, - isUser: true, -} + const newUser: AdminUserE2E = { + vorname: 'Theo', + nachname: 'Testuser', + benutzername: 'testtheo', + email: 'theo.test@ozg-sh.de', + isAdmin: true, + isUser: true, + } -describe('Benutzer anlegen', () => { before(() => { loginAsAriane(); }); @@ -61,9 +56,9 @@ describe('Benutzer anlegen', () => { }); it('should display new user in users table', () => { - benutzerPage.stringExistsInUserEntry(AlfaRollen.USER, benutzername); - benutzerPage.stringExistsInUserEntry(vorname, benutzername); - benutzerPage.stringExistsInUserEntry(nachname, benutzername); - // FEHLT NOCH: benutzerPage.stringExistsInUserEntry(AlfaRollen.ADMIN, benutzername); + benutzerPage.stringExistsInUserEntry(AlfaRollen.USER, newUser.benutzername); + benutzerPage.stringExistsInUserEntry(newUser.vorname, newUser.benutzername); + benutzerPage.stringExistsInUserEntry(newUser.nachname, newUser.benutzername); + benutzerPage.stringExistsInUserEntry(AlfaRollen.ADMIN, newUser.benutzername); }); }); diff --git a/alfa-client/apps/admin-e2e/src/model/util.ts b/alfa-client/apps/admin-e2e/src/model/util.ts index a6e185610f..f0977765a2 100644 --- a/alfa-client/apps/admin-e2e/src/model/util.ts +++ b/alfa-client/apps/admin-e2e/src/model/util.ts @@ -44,6 +44,7 @@ export interface AdminUserE2E { benutzername: string; email: string; isAdmin?: boolean; + isData?: boolean; isUser?: boolean; isLoeschen?: boolean; isPoststelle?: boolean; -- GitLab From e59d51d4bf467b930536b09ba924f8ee71e1afe7 Mon Sep 17 00:00:00 2001 From: cord <cord.westhoff@mgm-tp.com> Date: Mon, 24 Feb 2025 16:43:46 +0100 Subject: [PATCH 15/24] OZG-7803 prepare permission check for Ariane --- .../apps/admin-e2e/src/e2e/main-tests/navigation/ariane.cy.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/alfa-client/apps/admin-e2e/src/e2e/main-tests/navigation/ariane.cy.ts b/alfa-client/apps/admin-e2e/src/e2e/main-tests/navigation/ariane.cy.ts index 8098776599..822518401f 100644 --- a/alfa-client/apps/admin-e2e/src/e2e/main-tests/navigation/ariane.cy.ts +++ b/alfa-client/apps/admin-e2e/src/e2e/main-tests/navigation/ariane.cy.ts @@ -1,4 +1,5 @@ import { MainPage, waitForSpinnerToDisappear } from 'apps/admin-e2e/src/page-objects/main.po'; +import { visitUrl } from 'apps/admin-e2e/src/support/cypress-helper'; import { exist, notExist } from 'apps/admin-e2e/src/support/cypress.util'; import { loginAsAriane } from 'apps/admin-e2e/src/support/user-util'; @@ -26,6 +27,9 @@ describe('Navigation', () => { it('should hide statistik navigation item', () => { notExist(mainPage.getStatistikNavigationItem()); + + visitUrl('/statistik'); + }); }); }); -- GitLab From c5d9fd53e5c33b582b9a4bdc620942afafe3c071 Mon Sep 17 00:00:00 2001 From: cord <cord.westhoff@mgm-tp.com> Date: Mon, 24 Feb 2025 17:50:47 +0100 Subject: [PATCH 16/24] OZG-7590 OZG-7803 add test step for Ariane --- .../apps/admin-e2e/src/e2e/main-tests/navigation/ariane.cy.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/alfa-client/apps/admin-e2e/src/e2e/main-tests/navigation/ariane.cy.ts b/alfa-client/apps/admin-e2e/src/e2e/main-tests/navigation/ariane.cy.ts index 822518401f..fda055fd0d 100644 --- a/alfa-client/apps/admin-e2e/src/e2e/main-tests/navigation/ariane.cy.ts +++ b/alfa-client/apps/admin-e2e/src/e2e/main-tests/navigation/ariane.cy.ts @@ -26,10 +26,8 @@ describe('Navigation', () => { }); it('should hide statistik navigation item', () => { - notExist(mainPage.getStatistikNavigationItem()); - visitUrl('/statistik'); - + notExist(mainPage.getStatistikNavigationItem()); }); }); }); -- GitLab From d34bfd6bec42777a2ae1136351443540338a6810 Mon Sep 17 00:00:00 2001 From: Albert <Albert.Bruns@mgm-tp.com> Date: Wed, 26 Feb 2025 13:07:12 +0100 Subject: [PATCH 17/24] OZG-7590 cr anmerkung userRepository --- .../src/lib/user.repository.spec.ts | 136 ++++++++++++------ .../src/lib/user.repository.ts | 59 ++++---- .../admin/user-shared/src/lib/user.model.ts | 7 +- 3 files changed, 126 insertions(+), 76 deletions(-) diff --git a/alfa-client/libs/admin/keycloak-shared/src/lib/user.repository.spec.ts b/alfa-client/libs/admin/keycloak-shared/src/lib/user.repository.spec.ts index b4d1dc6f35..5df018711c 100644 --- a/alfa-client/libs/admin/keycloak-shared/src/lib/user.repository.spec.ts +++ b/alfa-client/libs/admin/keycloak-shared/src/lib/user.repository.spec.ts @@ -21,7 +21,7 @@ * Die sprachspezifischen Genehmigungen und Beschränkungen * unter der Lizenz sind dem Lizenztext zu entnehmen. */ -import { User } from '@admin-client/user-shared'; +import { RoleMappings, User } from '@admin-client/user-shared'; import { UserRepository } from '@admin/keycloak-shared'; import { StateResource } from '@alfa-client/tech-shared'; import { Mock, mock } from '@alfa-client/test-utils'; @@ -242,78 +242,111 @@ describe('UserRepository', () => { }); describe('UpdateUserRoles', () => { - it('should call UpdateUserRolesForClient for admin', async () => { + const clientId: string = faker.string.uuid(); + + beforeEach(() => { repository._updateUserRolesForClient = jest.fn(); + repository._getClientId = jest.fn().mockReturnValue(clientId); + }); + it('should call getClientId for admin', async () => { await repository._updateUserRoles(user.id, { admin: [UserFormService.ADMIN], alfa: [] }); - expect(repository._updateUserRolesForClient).toHaveBeenCalledWith( - user.id, - [UserFormService.ADMIN], - UserRepository.ADMIN_CLIENT_NAME, - ); + expect(repository._getClientId).toHaveBeenCalledWith(UserRepository.ADMIN_CLIENT_NAME); }); it('should call UpdateUserRolesForClient for admin', async () => { - repository._updateUserRolesForClient = jest.fn(); + await repository._updateUserRoles(user.id, { admin: [UserFormService.ADMIN], alfa: [] }); + expect(repository._updateUserRolesForClient).toHaveBeenCalledWith(user.id, [UserFormService.ADMIN], clientId); + }); + + it('should call getClientId for admin', async () => { await repository._updateUserRoles(user.id, { alfa: [UserFormService.POSTSTELLE], admin: [] }); - expect(repository._updateUserRolesForClient).toHaveBeenCalledWith( - user.id, - [UserFormService.POSTSTELLE], - UserRepository.ALFA_CLIENT_NAME, - ); + expect(repository._getClientId).toHaveBeenCalledWith(UserRepository.ALFA_CLIENT_NAME); + }); + + it('should call UpdateUserRolesForClient for admin', async () => { + await repository._updateUserRoles(user.id, { alfa: [UserFormService.POSTSTELLE], admin: [] }); + + expect(repository._updateUserRolesForClient).toHaveBeenCalledWith(user.id, [UserFormService.POSTSTELLE], clientId); }); }); describe('updateUserRolesForClient', () => { - const client: string = faker.word.sample(); const clientId: string = faker.string.uuid(); const clientRoles: string[] = times(3, () => faker.word.sample()); - const clientRolesMapping: RoleMappingPayload[] = clientRoles.map((role: string) => ({ id: faker.string.uuid(), name: role })); - const oldClientRoles: RoleMappingPayload[] = clientRolesMapping.slice(1); + const newClientRoleMappings: RoleMappingPayload[] = clientRoles.map((role: string) => ({ + id: faker.string.uuid(), + name: role, + })); + const oldClientRoleMappings: RoleMappingPayload[] = newClientRoleMappings.slice(1); + const clientRoleMappings: RoleMappings = { newClientRoleMappings, oldClientRoleMappings }; beforeEach(() => { - repository._getOldUserRoles = jest.fn().mockReturnValue(Promise.resolve(oldClientRoles)); - repository._getClientId = jest.fn().mockReturnValue(Promise.resolve(clientId)); - repository._mapUserRoles = jest.fn().mockReturnValue(Promise.resolve(clientRolesMapping)); + repository._getClientRoleMappings = jest.fn().mockReturnValue(Promise.resolve(clientRoleMappings)); repository._deleteUserRoles = jest.fn(); repository._addUserRoles = jest.fn(); }); - it('should call getClientId', async () => { - await repository._updateUserRolesForClient(user.id, clientRoles, client); + it('should call getClientRoleMappings', async () => { + await repository._updateUserRolesForClient(user.id, clientRoles, clientId); + + expect(repository._getClientRoleMappings).toHaveBeenCalledWith(user.id, clientId, clientRoles); + }); + + it('should call deleteUserRoles', async () => { + await repository._updateUserRolesForClient(user.id, clientRoles, clientId); - expect(repository._getClientId).toHaveBeenCalledWith(client); + expect(repository._deleteUserRoles).toHaveBeenCalledWith(user.id, clientRoleMappings, clientId); }); - it('should call mapUserRoles', async () => { - await repository._updateUserRolesForClient(user.id, clientRoles, client); + it('should call addUserRoles', async () => { + await repository._updateUserRolesForClient(user.id, clientRoles, clientId); - expect(repository._mapUserRoles).toHaveBeenCalledWith(clientId, clientRoles); + expect(repository._addUserRoles).toHaveBeenCalledWith(user.id, clientRoleMappings, clientId); }); + }); - it('should call getOldUserRoles', async () => { - await repository._updateUserRolesForClient(user.id, clientRoles, client); + describe('getClientRoleMappings', () => { + const clientId: string = faker.string.uuid(); + const newClientRoles: string[] = times(3, () => faker.word.sample()); + const newClientRoleMappings: RoleMappingPayload[] = newClientRoles.map((role: string) => ({ + id: faker.string.uuid(), + name: role, + })); + const oldClientRoleMappings: RoleMappingPayload[] = newClientRoleMappings.slice(1); + const clientRoleMappings: RoleMappings = { newClientRoleMappings, oldClientRoleMappings }; - expect(repository._getOldUserRoles).toHaveBeenCalledWith(user.id, clientId); + beforeEach(() => { + repository._getOldUserRoleMappings = jest.fn().mockReturnValue(Promise.resolve(oldClientRoleMappings)); + repository._getClientId = jest.fn().mockReturnValue(Promise.resolve(clientId)); + repository._mapUserRoles = jest.fn().mockReturnValue(Promise.resolve(newClientRoleMappings)); + repository._deleteUserRoles = jest.fn(); + repository._addUserRoles = jest.fn(); }); - it('should call deleteUserRoles', async () => { - await repository._updateUserRolesForClient(user.id, clientRoles, client); + it('should call mapUserRoles', () => { + repository._getClientRoleMappings(user.id, clientId, newClientRoles); - expect(repository._deleteUserRoles).toHaveBeenCalledWith(user.id, clientRolesMapping, oldClientRoles, clientId); + expect(repository._mapUserRoles).toHaveBeenCalledWith(clientId, newClientRoles); }); - it('should call addUserRoles', async () => { - await repository._updateUserRolesForClient(user.id, clientRoles, client); + it('should call _getOldUserRoleMappings', async () => { + await repository._getClientRoleMappings(user.id, clientId, newClientRoles); + + expect(repository._getOldUserRoleMappings).toHaveBeenCalledWith(user.id, clientId); + }); + + it('should return clientRoleMappings', async () => { + const result: RoleMappings = await repository._getClientRoleMappings(user.id, clientId, newClientRoles); - expect(repository._addUserRoles).toHaveBeenCalledWith(user.id, clientRolesMapping, oldClientRoles, clientId); + expect(result).toEqual(clientRoleMappings); }); }); - describe('getOldUserRoles', () => { + describe('getOldUserRoleMappings', () => { const clientId: string = faker.string.uuid(); const roleMapping: RoleMappingPayload[] = [{ id: faker.string.uuid(), name: faker.word.sample() }]; @@ -324,13 +357,13 @@ describe('UserRepository', () => { }); it('should call users listClientRoleMappings', () => { - repository._getOldUserRoles(user.id, clientId); + repository._getOldUserRoleMappings(user.id, clientId); expect(kcAdminClient.users['listClientRoleMappings']).toHaveBeenCalledWith({ id: user.id, clientUniqueId: clientId }); }); it('should return roleMapping', async () => { - const result: RoleMappingPayload[] = await repository._getOldUserRoles(user.id, clientId); + const result: RoleMappingPayload[] = await repository._getOldUserRoleMappings(user.id, clientId); expect(result).toEqual(roleMapping); }); @@ -339,11 +372,12 @@ describe('UserRepository', () => { describe('deleteUserRoles', () => { const clientId: string = faker.string.uuid(); const clientRoles: string[] = times(3, () => faker.word.sample()); - const oldClientRolesMapping: RoleMappingPayload[] = clientRoles.map((role: string) => ({ + const oldClientRoleMappings: RoleMappingPayload[] = clientRoles.map((role: string) => ({ id: faker.string.uuid(), name: role, })); - const clientRolesMapping: RoleMappingPayload[] = oldClientRolesMapping.slice(1); + const newClientRoleMappings: RoleMappingPayload[] = oldClientRoleMappings.slice(1); + const clientRoleMappings: RoleMappings = { newClientRoleMappings, oldClientRoleMappings }; beforeEach(() => { repository._deleteUserRolesInKeycloak = jest.fn(); @@ -354,13 +388,17 @@ describe('UserRepository', () => { }); it('should call deleteUserRolesInKeycloak', async () => { - await repository._deleteUserRoles(user.id, clientRolesMapping, oldClientRolesMapping, clientId); + await repository._deleteUserRoles(user.id, clientRoleMappings, clientId); - expect(repository._deleteUserRolesInKeycloak).toHaveBeenCalledWith(user.id, clientId, [oldClientRolesMapping[0]]); + expect(repository._deleteUserRolesInKeycloak).toHaveBeenCalledWith(user.id, clientId, [oldClientRoleMappings[0]]); }); it('should call not call deleteUserRolesInKeycloak if no roles to delete', async () => { - await repository._deleteUserRoles(user.id, clientRolesMapping, clientRolesMapping, clientId); + await repository._deleteUserRoles( + user.id, + { oldClientRoleMappings, newClientRoleMappings: oldClientRoleMappings }, + clientId, + ); expect(repository._deleteUserRolesInKeycloak).not.toHaveBeenCalled(); }); @@ -369,8 +407,12 @@ describe('UserRepository', () => { describe('addUserRoles', () => { const clientId: string = faker.string.uuid(); const clientRoles: string[] = times(3, () => faker.word.sample()); - const clientRolesMapping: RoleMappingPayload[] = clientRoles.map((role: string) => ({ id: faker.string.uuid(), name: role })); - const oldClientRolesMapping: RoleMappingPayload[] = clientRolesMapping.slice(1); + const newClientRoleMappings: RoleMappingPayload[] = clientRoles.map((role: string) => ({ + id: faker.string.uuid(), + name: role, + })); + const oldClientRoleMappings: RoleMappingPayload[] = newClientRoleMappings.slice(1); + const clientRoleMappings: RoleMappings = { newClientRoleMappings, oldClientRoleMappings }; beforeEach(() => { repository._addUserRolesInKeycloak = jest.fn(); @@ -381,13 +423,13 @@ describe('UserRepository', () => { }); it('should call addUserRolesInKeycloak', async () => { - await repository._addUserRoles(user.id, clientRolesMapping, oldClientRolesMapping, clientId); + await repository._addUserRoles(user.id, clientRoleMappings, clientId); - expect(repository._addUserRolesInKeycloak).toHaveBeenCalledWith(user.id, clientId, [clientRolesMapping[0]]); + expect(repository._addUserRolesInKeycloak).toHaveBeenCalledWith(user.id, clientId, [newClientRoleMappings[0]]); }); it('should not call addUserRolesInKeycloak if no roles to add', async () => { - await repository._addUserRoles(user.id, clientRolesMapping, clientRolesMapping, clientId); + await repository._addUserRoles(user.id, { newClientRoleMappings, oldClientRoleMappings: newClientRoleMappings }, clientId); expect(repository._addUserRolesInKeycloak).not.toHaveBeenCalled(); }); diff --git a/alfa-client/libs/admin/keycloak-shared/src/lib/user.repository.ts b/alfa-client/libs/admin/keycloak-shared/src/lib/user.repository.ts index 7285510ddd..ff50aa173d 100644 --- a/alfa-client/libs/admin/keycloak-shared/src/lib/user.repository.ts +++ b/alfa-client/libs/admin/keycloak-shared/src/lib/user.repository.ts @@ -21,7 +21,7 @@ * Die sprachspezifischen Genehmigungen und Beschränkungen * unter der Lizenz sind dem Lizenztext zu entnehmen. */ -import { ClientMapping, ClientRoles, User } from '@admin-client/user-shared'; +import { ClientMapping, ClientRoles, RoleMappings, User } from '@admin-client/user-shared'; import { createStateResource, StateResource } from '@alfa-client/tech-shared'; import { inject, Injectable } from '@angular/core'; import KcAdminClient from '@keycloak/keycloak-admin-client'; @@ -30,10 +30,11 @@ import GroupRepresentation from '@keycloak/keycloak-admin-client/lib/defs/groupR import MappingsRepresentation from '@keycloak/keycloak-admin-client/lib/defs/mappingsRepresentation'; import { RoleMappingPayload } from '@keycloak/keycloak-admin-client/lib/defs/roleRepresentation'; import UserRepresentation from '@keycloak/keycloak-admin-client/lib/defs/userRepresentation'; -import * as _ from 'lodash-es'; import { isNil } from 'lodash-es'; import { catchError, concatMap, forkJoin, from, map, mergeMap, Observable, tap, throwError } from 'rxjs'; +import * as _ from 'lodash-es'; + @Injectable({ providedIn: 'root', }) @@ -101,53 +102,55 @@ export class UserRepository { } async _updateUserRoles(userId: string, clientRoles: ClientRoles): Promise<void> { - await this._updateUserRolesForClient(userId, clientRoles.admin, UserRepository.ADMIN_CLIENT_NAME); - await this._updateUserRolesForClient(userId, clientRoles.alfa, UserRepository.ALFA_CLIENT_NAME); + await this._updateUserRolesForClient(userId, clientRoles.admin, await this._getClientId(UserRepository.ADMIN_CLIENT_NAME)); + await this._updateUserRolesForClient(userId, clientRoles.alfa, await this._getClientId(UserRepository.ALFA_CLIENT_NAME)); } - async _updateUserRolesForClient(userId: string, clientRoles: string[], client: string): Promise<void> { - const clientId: string = await this._getClientId(client); - const clientRolesMapping: RoleMappingPayload[] = await this._mapUserRoles(clientId, clientRoles); - const oldClientRolesMapping: RoleMappingPayload[] = await this._getOldUserRoles(userId, clientId); - await this._deleteUserRoles(userId, clientRolesMapping, oldClientRolesMapping, clientId); - await this._addUserRoles(userId, clientRolesMapping, oldClientRolesMapping, clientId); + async _updateUserRolesForClient(userId: string, clientRoles: string[], clientId: string): Promise<void> { + const roleMappings: RoleMappings = await this._getClientRoleMappings(userId, clientId, clientRoles); + await this._deleteUserRoles(userId, roleMappings, clientId); + await this._addUserRoles(userId, roleMappings, clientId); } - async _getOldUserRoles(userId: string, clientId: string): Promise<RoleMappingPayload[]> { + async _getClientRoleMappings(userId: string, clientId: string, clientRoles: string[]): Promise<RoleMappings> { + const newClientRoleMappings: RoleMappingPayload[] = await this._mapUserRoles(clientId, clientRoles); + const oldClientRoleMappings: RoleMappingPayload[] = await this._getOldUserRoleMappings(userId, clientId); + return { newClientRoleMappings, oldClientRoleMappings }; + } + + async _getOldUserRoleMappings(userId: string, clientId: string): Promise<RoleMappingPayload[]> { return <RoleMappingPayload[]>await this.kcAdminClient.users.listClientRoleMappings({ id: userId, clientUniqueId: clientId, }); } - async _deleteUserRoles( - userId: string, - clientRolesMapping: RoleMappingPayload[], - oldClientRolesMapping: RoleMappingPayload[], - clientId: string, - ): Promise<void> { - const rolesToDelete: RoleMappingPayload[] = oldClientRolesMapping.filter( - (role: RoleMappingPayload) => !_.map(clientRolesMapping, 'name').includes(role.name), - ); + async _deleteUserRoles(userId: string, roleMappings: RoleMappings, clientId: string): Promise<void> { + const rolesToDelete: RoleMappingPayload[] = this.getRolesToDelete(roleMappings); if (rolesToDelete.length > 0) { await this._deleteUserRolesInKeycloak(userId, clientId, rolesToDelete); } } - async _addUserRoles( - userId: string, - clientRolesMapping: RoleMappingPayload[], - oldClientRolesMapping: RoleMappingPayload[], - clientId: string, - ): Promise<void> { - const rolesToAdd: RoleMappingPayload[] = clientRolesMapping.filter( - (role: RoleMappingPayload) => !_.map(oldClientRolesMapping, 'name').includes(role.name), + private getRolesToDelete(roleMappings: RoleMappings): RoleMappingPayload[] { + return roleMappings.oldClientRoleMappings.filter( + (role: RoleMappingPayload) => !_.map(roleMappings.newClientRoleMappings, 'name').includes(role.name), ); + } + + async _addUserRoles(userId: string, roleMappings: RoleMappings, clientId: string): Promise<void> { + const rolesToAdd: RoleMappingPayload[] = this.rolesToAdd(roleMappings); if (rolesToAdd.length > 0) { await this._addUserRolesInKeycloak(userId, clientId, rolesToAdd); } } + private rolesToAdd(roleMappings: RoleMappings): RoleMappingPayload[] { + return roleMappings.newClientRoleMappings.filter( + (role: RoleMappingPayload) => !_.map(roleMappings.oldClientRoleMappings, 'name').includes(role.name), + ); + } + async _getClientId(client: string): Promise<string | undefined> { const clients: ClientRepresentation[] = await this.kcAdminClient.clients.find({ clientId: client }); return clients?.[0].id; diff --git a/alfa-client/libs/admin/user-shared/src/lib/user.model.ts b/alfa-client/libs/admin/user-shared/src/lib/user.model.ts index db185a0209..f496e42b87 100644 --- a/alfa-client/libs/admin/user-shared/src/lib/user.model.ts +++ b/alfa-client/libs/admin/user-shared/src/lib/user.model.ts @@ -21,7 +21,7 @@ * Die sprachspezifischen Genehmigungen und Beschränkungen * unter der Lizenz sind dem Lizenztext zu entnehmen. */ -import RoleRepresentation from '@keycloak/keycloak-admin-client/lib/defs/roleRepresentation'; +import RoleRepresentation, { RoleMappingPayload } from '@keycloak/keycloak-admin-client/lib/defs/roleRepresentation'; export interface User { id?: string; @@ -43,3 +43,8 @@ export interface ClientRoles { export interface ClientMapping { mappings: RoleRepresentation[]; } + +export interface RoleMappings { + newClientRoleMappings: RoleMappingPayload[]; + oldClientRoleMappings: RoleMappingPayload[]; +} -- GitLab From c1452952f662802319f163cb4b3e01d1ab628183 Mon Sep 17 00:00:00 2001 From: Albert <Albert.Bruns@mgm-tp.com> Date: Wed, 26 Feb 2025 14:30:51 +0100 Subject: [PATCH 18/24] OZG-7590 cr anmerkung --- .../benutzer/benutzer.e2e.component.ts | 14 +-- .../benutzer_rollen/benutzer_rollen.cy.ts | 12 +- alfa-client/apps/admin-e2e/src/model/util.ts | 2 +- .../apps/admin/src/app/app.component.spec.ts | 111 ++++++++++-------- .../apps/admin/src/app/app.component.ts | 38 +++--- 5 files changed, 95 insertions(+), 82 deletions(-) diff --git a/alfa-client/apps/admin-e2e/src/components/benutzer/benutzer.e2e.component.ts b/alfa-client/apps/admin-e2e/src/components/benutzer/benutzer.e2e.component.ts index 80b969a0e1..d2b0af5704 100644 --- a/alfa-client/apps/admin-e2e/src/components/benutzer/benutzer.e2e.component.ts +++ b/alfa-client/apps/admin-e2e/src/components/benutzer/benutzer.e2e.component.ts @@ -33,7 +33,7 @@ export class BenutzerE2EComponent { private readonly userBenutzername: string = 'Benutzername-text-input'; private readonly userMail: string = 'E-Mail-text-input'; private readonly adminCheckbox: string = 'Admin-checkbox-editor'; - private readonly datenCheckbox: string = 'Datenbeauftragung-checkbox-editor' + private readonly datenbeauftragungCheckbox: string = 'Datenbeauftragung-checkbox-editor'; private readonly loeschenCheckbox: string = 'Loschen-checkbox-editor'; private readonly userCheckbox: string = 'User-checkbox-editor'; private readonly postCheckbox: string = 'Poststelle-checkbox-editor'; @@ -61,8 +61,8 @@ export class BenutzerE2EComponent { if (user.isAdmin) { this.clickAdminCheckbox(); } - if (user.isData) { - this.clickDatenCheckbox(); + if (user.isDatenbeauftragung) { + this.clickDatenbeauftragungCheckbox(); } if (user.isUser) { this.clickUserCheckbox(); @@ -137,12 +137,12 @@ export class BenutzerE2EComponent { this.getAdminCheckbox().click(); } - public getDatenCheckbox(): Cypress.Chainable<Element> { - return cy.getTestElement(this.datenCheckbox); + public getDatenbeauftragungCheckbox(): Cypress.Chainable<Element> { + return cy.getTestElement(this.datenbeauftragungCheckbox); } - public clickDatenCheckbox(): void { - this.getDatenCheckbox().click(); + public clickDatenbeauftragungCheckbox(): void { + this.getDatenbeauftragungCheckbox().click(); } public getLoeschenCheckbox() { diff --git a/alfa-client/apps/admin-e2e/src/e2e/main-tests/benutzer_rollen/benutzer_rollen.cy.ts b/alfa-client/apps/admin-e2e/src/e2e/main-tests/benutzer_rollen/benutzer_rollen.cy.ts index a35cddcb08..1dd5fd7fd9 100644 --- a/alfa-client/apps/admin-e2e/src/e2e/main-tests/benutzer_rollen/benutzer_rollen.cy.ts +++ b/alfa-client/apps/admin-e2e/src/e2e/main-tests/benutzer_rollen/benutzer_rollen.cy.ts @@ -24,7 +24,7 @@ import { MainPage } from 'apps/admin-e2e/src/page-objects/main.po'; import { BenutzerE2EComponent } from '../../../components/benutzer/benutzer.e2e.component'; import { beChecked, beEnabled, exist, notBeChecked, notBeEnabled } from '../../../support/cypress.util'; -import { AlfaRollen, AlfaUsers, loginAsAriane, loginAsDaria, logout } from '../../../support/user-util'; +import { AlfaRollen, AlfaUsers, loginAsAriane } from '../../../support/user-util'; const mainPage: MainPage = new MainPage(); const benutzerPage: BenutzerE2EComponent = new BenutzerE2EComponent(); @@ -63,7 +63,7 @@ describe('Benutzer und Rollen', () => { exist(benutzerPage.getMailInput()); notBeChecked(benutzerPage.getAdminCheckbox()); - notBeChecked(benutzerPage.getDatenCheckbox()); + notBeChecked(benutzerPage.getDatenbeauftragungCheckbox()); notBeChecked(benutzerPage.getLoeschenCheckbox()); notBeChecked(benutzerPage.getUserCheckbox()); notBeChecked(benutzerPage.getPostCheckbox()); @@ -84,15 +84,15 @@ describe('Benutzer und Rollen', () => { it('should additionally activate and deactivate admin and data checkbox', () => { benutzerPage.clickLoeschenCheckbox(); benutzerPage.clickAdminCheckbox(); - benutzerPage.clickDatenCheckbox(); + benutzerPage.clickDatenbeauftragungCheckbox(); beChecked(benutzerPage.getLoeschenCheckbox()); beChecked(benutzerPage.getAdminCheckbox()); - beChecked(benutzerPage.getDatenCheckbox()); + beChecked(benutzerPage.getDatenbeauftragungCheckbox()); benutzerPage.clickAdminCheckbox(); - benutzerPage.clickDatenCheckbox(); + benutzerPage.clickDatenbeauftragungCheckbox(); notBeChecked(benutzerPage.getAdminCheckbox()); - notBeChecked(benutzerPage.getDatenCheckbox()); + notBeChecked(benutzerPage.getDatenbeauftragungCheckbox()); }); it('should activate user checkbox and deactivate the other two checkboxes', () => { diff --git a/alfa-client/apps/admin-e2e/src/model/util.ts b/alfa-client/apps/admin-e2e/src/model/util.ts index f0977765a2..0326ff0a1a 100644 --- a/alfa-client/apps/admin-e2e/src/model/util.ts +++ b/alfa-client/apps/admin-e2e/src/model/util.ts @@ -44,7 +44,7 @@ export interface AdminUserE2E { benutzername: string; email: string; isAdmin?: boolean; - isData?: boolean; + isDatenbeauftragung?: boolean; isUser?: boolean; isLoeschen?: boolean; isPoststelle?: boolean; 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 e5104f8395..aefc4923d4 100644 --- a/alfa-client/apps/admin/src/app/app.component.spec.ts +++ b/alfa-client/apps/admin/src/app/app.component.spec.ts @@ -161,91 +161,100 @@ describe('AppComponent', () => { }); it('should call doAfterLoggedIn only once', async () => { - component.doAfterLoggedIn = jest.fn(); + component._doAfterLoggedIn = jest.fn(); component.ngOnInit(); await fixture.whenStable(); - expect(component.doAfterLoggedIn).toHaveBeenCalledTimes(1); + expect(component._doAfterLoggedIn).toHaveBeenCalledTimes(1); }); }); describe('do after logged in', () => { beforeEach(() => { - component.evaluateInitialNavigation = jest.fn(); + component._evaluateInitialNavigation = jest.fn(); }); it('should call apiRootService to getApiRoot', () => { - component.doAfterLoggedIn(); + component._doAfterLoggedIn(); expect(apiRootService.getApiRoot).toHaveBeenCalled(); }); it('should call keycloak token service to register token provider', () => { - component.doAfterLoggedIn(); + component._doAfterLoggedIn(); expect(keycloakTokenService.registerAccessTokenProvider).toHaveBeenCalled(); }); it('should call evaluateInitialNavigation', () => { - component.doAfterLoggedIn(); + component._doAfterLoggedIn(); - expect(component.evaluateInitialNavigation).toHaveBeenCalled(); + expect(component._evaluateInitialNavigation).toHaveBeenCalled(); }); }); describe('evaluate initial navigation', () => { beforeEach(() => { - component.evaluateNavigationByApiRoot = jest.fn(); - }); - - it('should call evaluate navigation by apiRoot', () => { - (router as any).url = '/'; - const apiRootResource: ApiRootResource = createApiRootResource(); - component.apiRootStateResource$ = of(createStateResource(apiRootResource)); - - component.evaluateInitialNavigation(); - - expect(component.evaluateNavigationByApiRoot).toHaveBeenCalledWith(apiRootResource); + component._evaluateNavigationByApiRoot = jest.fn(); }); it('should call router navigate', () => { window.location.pathname = '/path'; (router as any).url = '/path'; - component.evaluateInitialNavigation(); + component._evaluateInitialNavigation(); expect(router.navigate).toHaveBeenCalledWith([window.location.pathname]); }); + }); + + describe('subscribeApiRootForEvaluation', () => { + const apiRootResource: ApiRootResource = createApiRootResource(); + + beforeEach(() => { + component.apiRootStateResource$ = of(createStateResource(apiRootResource)); + component._evaluateNavigationByApiRoot = jest.fn(); + }); + + it('should set apiRootSubscription', () => { + component._subscribeApiRootForEvaluation(); + + expect(component.apiRootSubscription).toBeDefined(); + }); + + it('should call evaluate navigation by apiRoot', () => { + component._subscribeApiRootForEvaluation(); + + expect(component._evaluateNavigationByApiRoot).toHaveBeenCalledWith(apiRootResource); + }); it('should NOT call evaluate navigation by apiRoot if resource is loading', () => { - (router as any).url = '/'; - component.apiRootStateResource$ = of(createEmptyStateResource<ApiRootResource>(true)); - component.evaluateNavigationByApiRoot = jest.fn(); + component.apiRootStateResource$ = of(createStateResource(apiRootResource, true)); - component.evaluateInitialNavigation(); + component._subscribeApiRootForEvaluation(); - expect(component.evaluateNavigationByApiRoot).not.toHaveBeenCalled(); + expect(component._evaluateNavigationByApiRoot).not.toHaveBeenCalled(); }); }); describe('evaluate navigation api root', () => { it('should evaluate navigation by configuration if link exists', () => { - component.evaluateNavigationByConfiguration = jest.fn(); + component._evaluateNavigationByConfiguration = jest.fn(); const apiRootResource: ApiRootResource = createApiRootResource([ApiRootLinkRel.CONFIGURATION]); - component.evaluateNavigationByApiRoot(apiRootResource); + component._evaluateNavigationByApiRoot(apiRootResource); - expect(component.evaluateNavigationByConfiguration).toHaveBeenCalled(); + expect(component._evaluateNavigationByConfiguration).toHaveBeenCalled(); }); it('should navigate by api root if link is missing', () => { - component.navigateByApiRoot = jest.fn(); + component._navigateByApiRoot = jest.fn(); const apiRootResource: ApiRootResource = createApiRootResource(); - component.evaluateNavigationByApiRoot(apiRootResource); + component._evaluateNavigationByApiRoot(apiRootResource); - expect(component.navigateByApiRoot).toHaveBeenCalledWith(apiRootResource); + expect(component._navigateByApiRoot).toHaveBeenCalledWith(apiRootResource); }); }); @@ -254,83 +263,83 @@ describe('AppComponent', () => { beforeEach(() => { configurationService.get.mockReturnValue(of(createStateResource(configurationResource))); - component.navigateByConfiguration = jest.fn(); + component._navigateByConfiguration = jest.fn(); }); it('should call configuration service to get resource', () => { - component.evaluateNavigationByConfiguration(); + component._evaluateNavigationByConfiguration(); expect(configurationService.get).toHaveBeenCalled(); }); it('should call navigate by configuration', () => { - component.evaluateNavigationByConfiguration(); + component._evaluateNavigationByConfiguration(); - expect(component.navigateByConfiguration).toHaveBeenCalledWith(configurationResource); + 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(); + component._evaluateNavigationByConfiguration(); - expect(component.navigateByConfiguration).not.toHaveBeenCalled(); + expect(component._navigateByConfiguration).not.toHaveBeenCalled(); }); }); describe('navigate by configuration', () => { beforeEach(() => { - component.unsubscribe = jest.fn(); + component._unsubscribe = jest.fn(); }); it('should navigate to postfach if settings link exists', () => { - component.navigateByConfiguration(createConfigurationResource([ConfigurationLinkRel.SETTING])); + 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])); + component._navigateByConfiguration(createConfigurationResource([ConfigurationLinkRel.AGGREGATION_MAPPINGS])); expect(router.navigate).toHaveBeenCalledWith(['/statistik']); }); it('should navigate to unavailable page if no link exists', () => { - component.navigateByConfiguration(createConfigurationResource()); + component._navigateByConfiguration(createConfigurationResource()); expect(router.navigate).toHaveBeenCalledWith(['/unavailable']); }); it('should call unsubscribe', () => { - component.navigateByConfiguration(createConfigurationResource()); + component._navigateByConfiguration(createConfigurationResource()); - expect(component.unsubscribe).toHaveBeenCalled(); + expect(component._unsubscribe).toHaveBeenCalled(); }); }); describe('navigate by api root', () => { beforeEach(() => { - component.unsubscribe = jest.fn(); + component._unsubscribe = jest.fn(); }); it('should navigate to benutzer und rollen if users link exists', () => { const apiRootResource: ApiRootResource = createApiRootResource([ApiRootLinkRel.USERS]); - component.navigateByApiRoot(apiRootResource); + component._navigateByApiRoot(apiRootResource); expect(router.navigate).toHaveBeenCalledWith([`/${ROUTES.BENUTZER}`]); }); it('should navigate to unavailable page if no link exists', () => { - component.navigateByApiRoot(createApiRootResource()); + component._navigateByApiRoot(createApiRootResource()); expect(router.navigate).toHaveBeenCalledWith([`/${ROUTES.UNAVAILABLE}`]); }); it('should call unsubscribe', () => { - component.navigateByApiRoot(createApiRootResource()); + component._navigateByApiRoot(createApiRootResource()); - expect(component.unsubscribe).toHaveBeenCalled(); + expect(component._unsubscribe).toHaveBeenCalled(); }); }); @@ -340,7 +349,7 @@ describe('AppComponent', () => { component.apiRootSubscription = <any>mock(Subscription); component.apiRootSubscription.unsubscribe = jest.fn(); - component.unsubscribe(); + component._unsubscribe(); expect(component.apiRootSubscription.unsubscribe).toHaveBeenCalled(); }); @@ -349,7 +358,7 @@ describe('AppComponent', () => { component.apiRootSubscription = undefined; const unsubscribeSpy: jest.SpyInstance = jest.spyOn(Subscription.prototype, 'unsubscribe'); - component.unsubscribe(); + component._unsubscribe(); expect(unsubscribeSpy).not.toHaveBeenCalled(); unsubscribeSpy.mockRestore(); @@ -361,7 +370,7 @@ describe('AppComponent', () => { component.configurationSubscription = <any>mock(Subscription); component.configurationSubscription.unsubscribe = jest.fn(); - component.unsubscribe(); + component._unsubscribe(); expect(component.configurationSubscription.unsubscribe).toHaveBeenCalled(); }); @@ -370,7 +379,7 @@ describe('AppComponent', () => { component.apiRootSubscription = undefined; const unsubscribeSpy: jest.SpyInstance = jest.spyOn(Subscription.prototype, 'unsubscribe'); - component.unsubscribe(); + component._unsubscribe(); expect(unsubscribeSpy).not.toHaveBeenCalled(); unsubscribeSpy.mockRestore(); diff --git a/alfa-client/apps/admin/src/app/app.component.ts b/alfa-client/apps/admin/src/app/app.component.ts index 56a4563089..581cb51ffe 100644 --- a/alfa-client/apps/admin/src/app/app.component.ts +++ b/alfa-client/apps/admin/src/app/app.component.ts @@ -83,45 +83,49 @@ export class AppComponent implements OnInit { public readonly routes = ROUTES; ngOnInit(): void { - this.authenticationService.login().then(() => this.doAfterLoggedIn()); + this.authenticationService.login().then(() => this._doAfterLoggedIn()); } - doAfterLoggedIn(): void { + _doAfterLoggedIn(): void { this.apiRootStateResource$ = this.apiRootService.getApiRoot(); this.keycloakTokenService.registerAccessTokenProvider(); - this.evaluateInitialNavigation(); + this._evaluateInitialNavigation(); } - evaluateInitialNavigation(): void { + _evaluateInitialNavigation(): void { if (this.router.url === '/') { - this.apiRootSubscription = this.apiRootStateResource$ - .pipe(filter(isLoaded), mapToResource<ApiRootResource>()) - .subscribe((apiRootResource: ApiRootResource) => this.evaluateNavigationByApiRoot(apiRootResource)); + this._subscribeApiRootForEvaluation(); } else { this.refreshForGuardEvaluation(); } } + _subscribeApiRootForEvaluation(): void { + this.apiRootSubscription = this.apiRootStateResource$ + .pipe(filter(isLoaded), mapToResource<ApiRootResource>()) + .subscribe((apiRootResource: ApiRootResource) => this._evaluateNavigationByApiRoot(apiRootResource)); + } + private refreshForGuardEvaluation(): void { this.router.navigate([window.location.pathname]); } - evaluateNavigationByApiRoot(apiRootResource: ApiRootResource): void { + _evaluateNavigationByApiRoot(apiRootResource: ApiRootResource): void { if (hasLink(apiRootResource, ApiRootLinkRel.CONFIGURATION)) { - this.evaluateNavigationByConfiguration(); + this._evaluateNavigationByConfiguration(); } else { - this.navigateByApiRoot(apiRootResource); + this._navigateByApiRoot(apiRootResource); } } - evaluateNavigationByConfiguration(): void { + _evaluateNavigationByConfiguration(): void { this.configurationSubscription = this.configurationService .get() .pipe(filter(isLoaded), mapToResource<ApiRootResource>()) - .subscribe((configurationResource: ConfigurationResource) => this.navigateByConfiguration(configurationResource)); + .subscribe((configurationResource: ConfigurationResource) => this._navigateByConfiguration(configurationResource)); } - navigateByConfiguration(configurationResource: ConfigurationResource): void { + _navigateByConfiguration(configurationResource: ConfigurationResource): void { if (hasLink(configurationResource, ConfigurationLinkRel.SETTING)) { this.navigate(ROUTES.POSTFACH); } else if (hasLink(configurationResource, ConfigurationLinkRel.AGGREGATION_MAPPINGS)) { @@ -129,23 +133,23 @@ export class AppComponent implements OnInit { } else { this.navigate(ROUTES.UNAVAILABLE); } - this.unsubscribe(); + this._unsubscribe(); } - navigateByApiRoot(apiRootResource: ApiRootResource): void { + _navigateByApiRoot(apiRootResource: ApiRootResource): void { if (hasLink(apiRootResource, ApiRootLinkRel.USERS)) { this.navigate(ROUTES.BENUTZER); } else { this.navigate(ROUTES.UNAVAILABLE); } - this.unsubscribe(); + this._unsubscribe(); } private navigate(routePath: string): void { this.router.navigate(['/' + routePath]); } - unsubscribe(): void { + _unsubscribe(): void { if (isNotUndefined(this.apiRootSubscription)) this.apiRootSubscription.unsubscribe(); if (isNotUndefined(this.configurationSubscription)) this.configurationSubscription.unsubscribe(); } -- GitLab From 4f45e9bb6dede2baaf1c52ccf64959dd4ec9b435 Mon Sep 17 00:00:00 2001 From: Albert <Albert.Bruns@mgm-tp.com> Date: Wed, 26 Feb 2025 15:44:51 +0100 Subject: [PATCH 19/24] OZG-7590 cr anmerkung --- .../admin/keycloak-shared/src/lib/user.repository.spec.ts | 2 +- .../libs/admin/keycloak-shared/src/lib/user.repository.ts | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/alfa-client/libs/admin/keycloak-shared/src/lib/user.repository.spec.ts b/alfa-client/libs/admin/keycloak-shared/src/lib/user.repository.spec.ts index 5df018711c..c78dbfbe4e 100644 --- a/alfa-client/libs/admin/keycloak-shared/src/lib/user.repository.spec.ts +++ b/alfa-client/libs/admin/keycloak-shared/src/lib/user.repository.spec.ts @@ -333,7 +333,7 @@ describe('UserRepository', () => { expect(repository._mapUserRoles).toHaveBeenCalledWith(clientId, newClientRoles); }); - it('should call _getOldUserRoleMappings', async () => { + it('should call get old user role mappings', async () => { await repository._getClientRoleMappings(user.id, clientId, newClientRoles); expect(repository._getOldUserRoleMappings).toHaveBeenCalledWith(user.id, clientId); diff --git a/alfa-client/libs/admin/keycloak-shared/src/lib/user.repository.ts b/alfa-client/libs/admin/keycloak-shared/src/lib/user.repository.ts index ff50aa173d..9fb1960424 100644 --- a/alfa-client/libs/admin/keycloak-shared/src/lib/user.repository.ts +++ b/alfa-client/libs/admin/keycloak-shared/src/lib/user.repository.ts @@ -30,7 +30,7 @@ import GroupRepresentation from '@keycloak/keycloak-admin-client/lib/defs/groupR import MappingsRepresentation from '@keycloak/keycloak-admin-client/lib/defs/mappingsRepresentation'; import { RoleMappingPayload } from '@keycloak/keycloak-admin-client/lib/defs/roleRepresentation'; import UserRepresentation from '@keycloak/keycloak-admin-client/lib/defs/userRepresentation'; -import { isNil } from 'lodash-es'; +import { isNil, omit } from 'lodash-es'; import { catchError, concatMap, forkJoin, from, map, mergeMap, Observable, tap, throwError } from 'rxjs'; import * as _ from 'lodash-es'; @@ -45,7 +45,7 @@ export class UserRepository { public static readonly ADMIN_CLIENT_NAME: string = 'admin'; public createInKeycloak(user: User): Observable<User> { - return from(this.kcAdminClient.users.create(_.omit(user, 'groupIds'))).pipe( + return from(this.kcAdminClient.users.create(omit(user, 'groupIds'))).pipe( concatMap(async (response: { id: string }): Promise<{ id: string }> => { await this._updateUserRoles(response.id, user.clientRoles); return response; @@ -139,13 +139,13 @@ export class UserRepository { } async _addUserRoles(userId: string, roleMappings: RoleMappings, clientId: string): Promise<void> { - const rolesToAdd: RoleMappingPayload[] = this.rolesToAdd(roleMappings); + const rolesToAdd: RoleMappingPayload[] = this.getRolesToAdd(roleMappings); if (rolesToAdd.length > 0) { await this._addUserRolesInKeycloak(userId, clientId, rolesToAdd); } } - private rolesToAdd(roleMappings: RoleMappings): RoleMappingPayload[] { + private getRolesToAdd(roleMappings: RoleMappings): RoleMappingPayload[] { return roleMappings.newClientRoleMappings.filter( (role: RoleMappingPayload) => !_.map(roleMappings.oldClientRoleMappings, 'name').includes(role.name), ); -- GitLab From 19bfd25d044d08baab9cb0e0a8b020858101150e Mon Sep 17 00:00:00 2001 From: Albert <Albert.Bruns@mgm-tp.com> Date: Wed, 26 Feb 2025 17:22:10 +0100 Subject: [PATCH 20/24] OZG-7590 lodash --- .../admin/keycloak-shared/src/lib/user.repository.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/alfa-client/libs/admin/keycloak-shared/src/lib/user.repository.ts b/alfa-client/libs/admin/keycloak-shared/src/lib/user.repository.ts index 9fb1960424..49b1d23ca2 100644 --- a/alfa-client/libs/admin/keycloak-shared/src/lib/user.repository.ts +++ b/alfa-client/libs/admin/keycloak-shared/src/lib/user.repository.ts @@ -33,7 +33,7 @@ import UserRepresentation from '@keycloak/keycloak-admin-client/lib/defs/userRep import { isNil, omit } from 'lodash-es'; import { catchError, concatMap, forkJoin, from, map, mergeMap, Observable, tap, throwError } from 'rxjs'; -import * as _ from 'lodash-es'; +import * as lodash from 'lodash-es'; @Injectable({ providedIn: 'root', @@ -78,7 +78,7 @@ export class UserRepository { const oldUserGroupsReps: RoleMappingPayload[] = <RoleMappingPayload[]>( await this.kcAdminClient.users.listGroups({ id: userId }) ); - return _.map(oldUserGroupsReps, 'id'); + return lodash.map(oldUserGroupsReps, 'id'); } async _deleteUserGroups(userId: string, newGroupIds: string[], oldGroupIds: string[]): Promise<void> { @@ -134,7 +134,7 @@ export class UserRepository { private getRolesToDelete(roleMappings: RoleMappings): RoleMappingPayload[] { return roleMappings.oldClientRoleMappings.filter( - (role: RoleMappingPayload) => !_.map(roleMappings.newClientRoleMappings, 'name').includes(role.name), + (role: RoleMappingPayload) => !lodash.map(roleMappings.newClientRoleMappings, 'name').includes(role.name), ); } @@ -147,7 +147,7 @@ export class UserRepository { private getRolesToAdd(roleMappings: RoleMappings): RoleMappingPayload[] { return roleMappings.newClientRoleMappings.filter( - (role: RoleMappingPayload) => !_.map(roleMappings.oldClientRoleMappings, 'name').includes(role.name), + (role: RoleMappingPayload) => !lodash.map(roleMappings.oldClientRoleMappings, 'name').includes(role.name), ); } @@ -248,7 +248,7 @@ export class UserRepository { private getUserGroups(user: User): Observable<string[]> { return from(this.kcAdminClient.users.listGroups({ id: user.id })).pipe( - map((groups: GroupRepresentation[]): string[] => _.map(groups, 'name')), + map((groups: GroupRepresentation[]): string[] => lodash.map(groups, 'name')), ); } @@ -270,6 +270,6 @@ export class UserRepository { return []; } - return _.map(clientMappingsAlfa.mappings, 'name'); + return lodash.map(clientMappingsAlfa.mappings, 'name'); } } -- GitLab From 05816a60ef5bcb03b46ebf8773a394ac0b1d3c00 Mon Sep 17 00:00:00 2001 From: Albert <Albert.Bruns@mgm-tp.com> Date: Wed, 26 Feb 2025 18:05:36 +0100 Subject: [PATCH 21/24] Revert "OZG-7590 lodash" This reverts commit 19bfd25d044d08baab9cb0e0a8b020858101150e. --- .../admin/keycloak-shared/src/lib/user.repository.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/alfa-client/libs/admin/keycloak-shared/src/lib/user.repository.ts b/alfa-client/libs/admin/keycloak-shared/src/lib/user.repository.ts index 49b1d23ca2..9fb1960424 100644 --- a/alfa-client/libs/admin/keycloak-shared/src/lib/user.repository.ts +++ b/alfa-client/libs/admin/keycloak-shared/src/lib/user.repository.ts @@ -33,7 +33,7 @@ import UserRepresentation from '@keycloak/keycloak-admin-client/lib/defs/userRep import { isNil, omit } from 'lodash-es'; import { catchError, concatMap, forkJoin, from, map, mergeMap, Observable, tap, throwError } from 'rxjs'; -import * as lodash from 'lodash-es'; +import * as _ from 'lodash-es'; @Injectable({ providedIn: 'root', @@ -78,7 +78,7 @@ export class UserRepository { const oldUserGroupsReps: RoleMappingPayload[] = <RoleMappingPayload[]>( await this.kcAdminClient.users.listGroups({ id: userId }) ); - return lodash.map(oldUserGroupsReps, 'id'); + return _.map(oldUserGroupsReps, 'id'); } async _deleteUserGroups(userId: string, newGroupIds: string[], oldGroupIds: string[]): Promise<void> { @@ -134,7 +134,7 @@ export class UserRepository { private getRolesToDelete(roleMappings: RoleMappings): RoleMappingPayload[] { return roleMappings.oldClientRoleMappings.filter( - (role: RoleMappingPayload) => !lodash.map(roleMappings.newClientRoleMappings, 'name').includes(role.name), + (role: RoleMappingPayload) => !_.map(roleMappings.newClientRoleMappings, 'name').includes(role.name), ); } @@ -147,7 +147,7 @@ export class UserRepository { private getRolesToAdd(roleMappings: RoleMappings): RoleMappingPayload[] { return roleMappings.newClientRoleMappings.filter( - (role: RoleMappingPayload) => !lodash.map(roleMappings.oldClientRoleMappings, 'name').includes(role.name), + (role: RoleMappingPayload) => !_.map(roleMappings.oldClientRoleMappings, 'name').includes(role.name), ); } @@ -248,7 +248,7 @@ export class UserRepository { private getUserGroups(user: User): Observable<string[]> { return from(this.kcAdminClient.users.listGroups({ id: user.id })).pipe( - map((groups: GroupRepresentation[]): string[] => lodash.map(groups, 'name')), + map((groups: GroupRepresentation[]): string[] => _.map(groups, 'name')), ); } @@ -270,6 +270,6 @@ export class UserRepository { return []; } - return lodash.map(clientMappingsAlfa.mappings, 'name'); + return _.map(clientMappingsAlfa.mappings, 'name'); } } -- GitLab From 3f4121af7268ec49abc35275bc686f0b72d17f84 Mon Sep 17 00:00:00 2001 From: Martin <git@mail.de> Date: Thu, 27 Feb 2025 18:07:13 +0100 Subject: [PATCH 22/24] OZG-7590 adjust e2e tests --- .../postfach/postfach.e2e.component.ts | 9 +++- ...zustaendige-stelle-dialog.e2e.component.ts | 4 +- .../benutzer_rollen/benutzer_rollen.cy.ts | 42 ++++++++----------- .../e2e/main-tests/navigation/ariane.cy.ts | 24 ++++++----- .../src/e2e/main-tests/navigation/daria.cy.ts | 29 +++++-------- .../e2e/main-tests/navigation/safira.cy.ts | 37 +++++----------- .../organisationseinheiten-hinzufuegen.cy.ts | 2 +- .../organisationseinheiten-laden.cy.ts | 2 +- .../postfach/postfach-signatur.cy.ts | 13 +++--- .../src/helper/postfach/postfach.helper.ts | 9 ++++ .../src/helper/postfach/postfach.navigator.ts | 18 ++++++++ .../admin-e2e/src/page-objects/main.po.ts | 33 ++------------- .../admin-e2e/src/support/cypress.util.ts | 4 -- .../postfach-container.component.html | 2 +- 14 files changed, 98 insertions(+), 130 deletions(-) create mode 100644 alfa-client/apps/admin-e2e/src/helper/postfach/postfach.helper.ts create mode 100644 alfa-client/apps/admin-e2e/src/helper/postfach/postfach.navigator.ts diff --git a/alfa-client/apps/admin-e2e/src/components/postfach/postfach.e2e.component.ts b/alfa-client/apps/admin-e2e/src/components/postfach/postfach.e2e.component.ts index 6252771178..f177cf791e 100644 --- a/alfa-client/apps/admin-e2e/src/components/postfach/postfach.e2e.component.ts +++ b/alfa-client/apps/admin-e2e/src/components/postfach/postfach.e2e.component.ts @@ -21,19 +21,24 @@ * Die sprachspezifischen Genehmigungen und Beschränkungen * unter der Lizenz sind dem Lizenztext zu entnehmen. */ -import { haveValue, typeText } from '../../support/cypress.util'; +import { haveValue } from '../../support/cypress.util'; export class PostfachE2EComponent { + private readonly headline: string = 'headline'; private readonly signaturText: string = 'signature-textarea'; private readonly saveSignaturButton: string = 'save-button'; + public getHeadline(): any { + return cy.getTestElement(this.headline); + } + public getSignaturText(): any { return cy.getTestElement(this.signaturText); } public setSignatur(signatur: string): void { this.clearSignatur(); - typeText(this.getSignaturText(), signatur); + this.getSignaturText().type(signatur); } public clearSignatur(): void { diff --git a/alfa-client/apps/admin-e2e/src/components/zustaendige-stelle/zustaendige-stelle-dialog.e2e.component.ts b/alfa-client/apps/admin-e2e/src/components/zustaendige-stelle/zustaendige-stelle-dialog.e2e.component.ts index 337cad0fe5..6bd293d6fc 100644 --- a/alfa-client/apps/admin-e2e/src/components/zustaendige-stelle/zustaendige-stelle-dialog.e2e.component.ts +++ b/alfa-client/apps/admin-e2e/src/components/zustaendige-stelle/zustaendige-stelle-dialog.e2e.component.ts @@ -1,5 +1,3 @@ -import { typeText } from '../../support/cypress.util'; - export class ZustaendigeStelleDialogE2EComponent { private readonly locatorZustaendigeStelleForm: string = 'search-organisations-einheit'; private readonly locatorSearchInput: string = 'instant_search-text-input'; @@ -15,7 +13,7 @@ export class ZustaendigeStelleDialogE2EComponent { } public enterSearchTerm(searchTerm: string): void { - typeText(this.getSearchInput(), searchTerm); + this.getSearchInput().type(searchTerm); } public countSearchEntries(): Cypress.Chainable<number> { diff --git a/alfa-client/apps/admin-e2e/src/e2e/main-tests/benutzer_rollen/benutzer_rollen.cy.ts b/alfa-client/apps/admin-e2e/src/e2e/main-tests/benutzer_rollen/benutzer_rollen.cy.ts index 15f015c750..f39ed75f13 100644 --- a/alfa-client/apps/admin-e2e/src/e2e/main-tests/benutzer_rollen/benutzer_rollen.cy.ts +++ b/alfa-client/apps/admin-e2e/src/e2e/main-tests/benutzer_rollen/benutzer_rollen.cy.ts @@ -47,7 +47,7 @@ describe('Benutzer und Rollen', () => { it('should show users and attributes in list', () => { helper.openBenutzerListPage(); - const ariane: BenutzerListItemE2EComponent = benutzerListPage.getItem(AlfaUsers.ARAINE); + const ariane: BenutzerListItemE2EComponent = benutzerListPage.getItem(AlfaUsers.ARIANE); exist(ariane.getRoot()); contains(ariane.getRoles(), AlfaRollen.USER); @@ -76,14 +76,9 @@ describe('Benutzer und Rollen', () => { exist(richard.getNoOrganisationsEinheitText()); }); - it('should show single user screen on click', () => { + it('should show checkbox for each role', () => { helper.openNewBenutzerPage(); - exist(benutzerPage.getVornameInput()); - exist(benutzerPage.getNachnameInput()); - exist(benutzerPage.getBenutzernameInput()); - exist(benutzerPage.getMailInput()); - notBeChecked(benutzerPage.getAdminCheckbox()); notBeChecked(benutzerPage.getDatenbeauftragungCheckbox()); notBeChecked(benutzerPage.getLoeschenCheckbox()); @@ -91,7 +86,7 @@ describe('Benutzer und Rollen', () => { notBeChecked(benutzerPage.getPostCheckbox()); }); - it('should activate loeschen checkbox and deactivate the other two checkboxes', () => { + it('should deactivate other alfa roles if "loeschen" role is selected', () => { benutzerPage.getLoeschenCheckbox().click(); beChecked(benutzerPage.getLoeschenCheckbox()); notBeEnabled(benutzerPage.getUserCheckbox()); @@ -103,22 +98,7 @@ describe('Benutzer und Rollen', () => { beEnabled(benutzerPage.getPostCheckbox()); }); - it('should additionally activate and deactivate admin checkbox', () => { - benutzerPage.getLoeschenCheckbox().click(); - benutzerPage.getAdminCheckbox().click(); - benutzerPage.getDatenbeauftragungCheckbox().click(); - beChecked(benutzerPage.getLoeschenCheckbox()); - beChecked(benutzerPage.getAdminCheckbox()); - beChecked(benutzerPage.getDatenbeauftragungCheckbox()); - - benutzerPage.getAdminCheckbox().click(); - notBeChecked(benutzerPage.getLoeschenCheckbox()); - notBeChecked(benutzerPage.getAdminCheckbox()); - notBeChecked(benutzerPage.getDatenbeauftragungCheckbox()); - }); - - it('should activate user checkbox and deactivate the other two checkboxes', () => { - benutzerPage.getLoeschenCheckbox().click(); + it('should deactivate other alfa roles if "user" role is selected', () => { benutzerPage.getUserCheckbox().click(); beChecked(benutzerPage.getUserCheckbox()); notBeEnabled(benutzerPage.getLoeschenCheckbox()); @@ -130,7 +110,7 @@ describe('Benutzer und Rollen', () => { beEnabled(benutzerPage.getPostCheckbox()); }); - it('should activate post checkbox and deactivate the other two checkboxes', () => { + it('should deactivate other alfa roles if "poststelle" role is selected', () => { benutzerPage.getPostCheckbox().click(); beChecked(benutzerPage.getPostCheckbox()); notBeEnabled(benutzerPage.getLoeschenCheckbox()); @@ -141,4 +121,16 @@ describe('Benutzer und Rollen', () => { beEnabled(benutzerPage.getLoeschenCheckbox()); beEnabled(benutzerPage.getUserCheckbox()); }); + + it('should activate and deactivate admin roles', () => { + benutzerPage.getAdminCheckbox().click(); + benutzerPage.getDatenbeauftragungCheckbox().click(); + beChecked(benutzerPage.getAdminCheckbox()); + beChecked(benutzerPage.getDatenbeauftragungCheckbox()); + + benutzerPage.getAdminCheckbox().click(); + benutzerPage.getDatenbeauftragungCheckbox().click(); + notBeChecked(benutzerPage.getAdminCheckbox()); + notBeChecked(benutzerPage.getDatenbeauftragungCheckbox()); + }); }); diff --git a/alfa-client/apps/admin-e2e/src/e2e/main-tests/navigation/ariane.cy.ts b/alfa-client/apps/admin-e2e/src/e2e/main-tests/navigation/ariane.cy.ts index fda055fd0d..64218bf8c5 100644 --- a/alfa-client/apps/admin-e2e/src/e2e/main-tests/navigation/ariane.cy.ts +++ b/alfa-client/apps/admin-e2e/src/e2e/main-tests/navigation/ariane.cy.ts @@ -1,32 +1,34 @@ -import { MainPage, waitForSpinnerToDisappear } from 'apps/admin-e2e/src/page-objects/main.po'; -import { visitUrl } from 'apps/admin-e2e/src/support/cypress-helper'; -import { exist, notExist } from 'apps/admin-e2e/src/support/cypress.util'; +import { MainPage } from 'apps/admin-e2e/src/page-objects/main.po'; +import { containClass, exist, notExist } from 'apps/admin-e2e/src/support/cypress.util'; import { loginAsAriane } from 'apps/admin-e2e/src/support/user-util'; -describe('Navigation', () => { +describe('Ariane Navigation', () => { const mainPage: MainPage = new MainPage(); describe('with user ariane', () => { before(() => { loginAsAriane(); - - waitForSpinnerToDisappear(); }); it('should show benutzer navigation item', () => { exist(mainPage.getBenutzerNavigationItem()); }); - it('should show postfach navigation item', () => { - exist(mainPage.getPostfachNavigationItem()); - }); - it('should show organisationseinheiten navigation item', () => { exist(mainPage.getOrganisationEinheitNavigationItem()); }); + describe('postfach navigation item', () => { + it('should be visible', () => { + exist(mainPage.getPostfachNavigationItem()); + }); + + it('should be selected initial', () => { + containClass(mainPage.getPostfachNavigationItem(), 'border-selected'); + }); + }); + it('should hide statistik navigation item', () => { - visitUrl('/statistik'); notExist(mainPage.getStatistikNavigationItem()); }); }); diff --git a/alfa-client/apps/admin-e2e/src/e2e/main-tests/navigation/daria.cy.ts b/alfa-client/apps/admin-e2e/src/e2e/main-tests/navigation/daria.cy.ts index 33a06a73b8..4fda0a2dee 100644 --- a/alfa-client/apps/admin-e2e/src/e2e/main-tests/navigation/daria.cy.ts +++ b/alfa-client/apps/admin-e2e/src/e2e/main-tests/navigation/daria.cy.ts @@ -1,43 +1,34 @@ -import { MainPage, waitForSpinnerToDisappear } from 'apps/admin-e2e/src/page-objects/main.po'; -import { exist, notExist, visible } from 'apps/admin-e2e/src/support/cypress.util'; +import { MainPage } from 'apps/admin-e2e/src/page-objects/main.po'; +import { containClass, exist, notExist } from 'apps/admin-e2e/src/support/cypress.util'; import { loginAsDaria } from 'apps/admin-e2e/src/support/user-util'; -import { StatistikE2EComponent } from '../../../components/statistik/statistik.e2e.component'; -describe('Navigation', () => { +describe('Daria Navigation', () => { const mainPage: MainPage = new MainPage(); - const statistikPage: StatistikE2EComponent = new StatistikE2EComponent(); - describe('with user daria', () => { before(() => { loginAsDaria(); - - waitForSpinnerToDisappear(); }); - it('should hide other navigation item', () => { + it('should hide benutzer navigation item', () => { notExist(mainPage.getBenutzerNavigationItem()); }); - it('should hide postfach navigation item', () => { - notExist(mainPage.getPostfachNavigationItem()); - }); - it('should hide organisationseinheiten navigation item', () => { notExist(mainPage.getOrganisationEinheitNavigationItem()); }); - describe('statistik', () => { + it('should hide postfach navigation item', () => { + notExist(mainPage.getPostfachNavigationItem()); + }); + + describe('statistik navigation item', () => { it('should be visible', () => { exist(mainPage.getStatistikNavigationItem()); }); it('should be initial selected', () => { - mainPage.isStatistikNavigationItemSelected(); - }); - - it('should show header text', () => { - visible(statistikPage.getHeaderText()); + containClass(mainPage.getStatistikNavigationItem(), 'border-selected'); }); }); }); diff --git a/alfa-client/apps/admin-e2e/src/e2e/main-tests/navigation/safira.cy.ts b/alfa-client/apps/admin-e2e/src/e2e/main-tests/navigation/safira.cy.ts index 005da2e5a5..ae328f579e 100644 --- a/alfa-client/apps/admin-e2e/src/e2e/main-tests/navigation/safira.cy.ts +++ b/alfa-client/apps/admin-e2e/src/e2e/main-tests/navigation/safira.cy.ts @@ -1,50 +1,35 @@ -import { MainPage, waitForSpinnerToDisappear } from 'apps/admin-e2e/src/page-objects/main.po'; -import { exist, visible } from 'apps/admin-e2e/src/support/cypress.util'; +import { MainPage } from 'apps/admin-e2e/src/page-objects/main.po'; +import { containClass, exist } from 'apps/admin-e2e/src/support/cypress.util'; import { loginAsSafira } from 'apps/admin-e2e/src/support/user-util'; -import { StatistikE2EComponent } from '../../../components/statistik/statistik.e2e.component'; -describe('Navigation', () => { +describe('Safira Navigation', () => { const mainPage: MainPage = new MainPage(); - const statistikPage: StatistikE2EComponent = new StatistikE2EComponent(); - describe('with user safira', () => { before(() => { loginAsSafira(); - - waitForSpinnerToDisappear(); }); it('should show benutzer navigation item', () => { exist(mainPage.getBenutzerNavigationItem()); }); - it('should show postfach navigation item', () => { - exist(mainPage.getPostfachNavigationItem()); - }); - it('should show organisationseinheiten navigation item', () => { exist(mainPage.getOrganisationEinheitNavigationItem()); }); - describe('statistik', () => { + describe('postfach navigation item', () => { it('should be visible', () => { - exist(mainPage.getStatistikNavigationItem()); + exist(mainPage.getPostfachNavigationItem()); }); - describe('on selection', () => { - before(() => { - mainPage.clickStatistikNavigationItem(); - }); - - it('should show page on selection', () => { - visible(statistikPage.getHeaderText()); - }); - - it('should mark navigation item as selected', () => { - mainPage.isStatistikNavigationItemSelected(); - }); + it('should be selected initial', () => { + containClass(mainPage.getPostfachNavigationItem(), 'border-selected'); }); }); + + it('should show statistik navigation item', () => { + exist(mainPage.getStatistikNavigationItem()); + }); }); }); diff --git a/alfa-client/apps/admin-e2e/src/e2e/main-tests/organisationseinheiten/organisationseinheiten-hinzufuegen.cy.ts b/alfa-client/apps/admin-e2e/src/e2e/main-tests/organisationseinheiten/organisationseinheiten-hinzufuegen.cy.ts index c88d54bf3d..ce01dec8af 100644 --- a/alfa-client/apps/admin-e2e/src/e2e/main-tests/organisationseinheiten/organisationseinheiten-hinzufuegen.cy.ts +++ b/alfa-client/apps/admin-e2e/src/e2e/main-tests/organisationseinheiten/organisationseinheiten-hinzufuegen.cy.ts @@ -18,7 +18,7 @@ describe('Organisationseinheiten', () => { it('should show table with Organisationseinheiten', () => { waitForSpinnerToDisappear(); - mainPage.clickOrganisationsEinheitenNavigationItem(); + mainPage.getOrganisationEinheitNavigationItem().click(); exist(organisationsEinheitenComponent.getOrganisationsEinheitHinzufuegenButton()); }); diff --git a/alfa-client/apps/admin-e2e/src/e2e/main-tests/organisationseinheiten/organisationseinheiten-laden.cy.ts b/alfa-client/apps/admin-e2e/src/e2e/main-tests/organisationseinheiten/organisationseinheiten-laden.cy.ts index 2b8eaeb70a..e65e64480c 100644 --- a/alfa-client/apps/admin-e2e/src/e2e/main-tests/organisationseinheiten/organisationseinheiten-laden.cy.ts +++ b/alfa-client/apps/admin-e2e/src/e2e/main-tests/organisationseinheiten/organisationseinheiten-laden.cy.ts @@ -36,7 +36,7 @@ describe('Organisationsheiten list', () => { it('should show Organisationseinheiten list', () => { waitForSpinnerToDisappear(); - mainPage.clickOrganisationsEinheitenNavigationItem(); + mainPage.getOrganisationEinheitNavigationItem().click(); waitForSpinnerToDisappear(); exist(organisationsEinheitenTab.getOrganisationsEinheitList()); diff --git a/alfa-client/apps/admin-e2e/src/e2e/main-tests/postfach/postfach-signatur.cy.ts b/alfa-client/apps/admin-e2e/src/e2e/main-tests/postfach/postfach-signatur.cy.ts index 1a8b4a69f8..779a88f3c4 100644 --- a/alfa-client/apps/admin-e2e/src/e2e/main-tests/postfach/postfach-signatur.cy.ts +++ b/alfa-client/apps/admin-e2e/src/e2e/main-tests/postfach/postfach-signatur.cy.ts @@ -21,12 +21,12 @@ * Die sprachspezifischen Genehmigungen und Beschränkungen * unter der Lizenz sind dem Lizenztext zu entnehmen. */ +import { E2EPostfachHelper } from 'apps/admin-e2e/src/helper/postfach/postfach.helper'; import { PostfachE2EComponent } from '../../../components/postfach/postfach.e2e.component'; -import { waitForSpinnerToDisappear } from '../../../page-objects/main.po'; -import { exist } from '../../../support/cypress.util'; import { loginAsAriane } from '../../../support/user-util'; -describe('Signatur', () => { +describe('(TODO: Ist noch wackelig in Bezug auf die Eingabe in das Feld) Postfach Signatur', () => { + const postfachHelper: E2EPostfachHelper = new E2EPostfachHelper(); const postfach: PostfachE2EComponent = new PostfachE2EComponent(); const signaturText: string = 'Signatur\nmit\n\n\n\nZeilenumbruch\n\n'; @@ -35,12 +35,9 @@ describe('Signatur', () => { loginAsAriane(); }); - it('should show Postfach page', () => { - waitForSpinnerToDisappear(); - exist(postfach.getSignaturText()); - }); - it('should show signature input with scrollbar', () => { + postfachHelper.openPostfachPage(); + postfach.setSignatur(signaturText); postfach.saveSignatur(); diff --git a/alfa-client/apps/admin-e2e/src/helper/postfach/postfach.helper.ts b/alfa-client/apps/admin-e2e/src/helper/postfach/postfach.helper.ts new file mode 100644 index 0000000000..90624b0151 --- /dev/null +++ b/alfa-client/apps/admin-e2e/src/helper/postfach/postfach.helper.ts @@ -0,0 +1,9 @@ +import { E2EPostfachNavigator } from './postfach.navigator'; + +export class E2EPostfachHelper { + private readonly navigator: E2EPostfachNavigator = new E2EPostfachNavigator(); + + public openPostfachPage(): void { + this.navigator.openPostfachPage(); + } +} diff --git a/alfa-client/apps/admin-e2e/src/helper/postfach/postfach.navigator.ts b/alfa-client/apps/admin-e2e/src/helper/postfach/postfach.navigator.ts new file mode 100644 index 0000000000..074438baaa --- /dev/null +++ b/alfa-client/apps/admin-e2e/src/helper/postfach/postfach.navigator.ts @@ -0,0 +1,18 @@ +import { PostfachE2EComponent } from '../../components/postfach/postfach.e2e.component'; +import { MainPage } from '../../page-objects/main.po'; +import { exist } from '../../support/cypress.util'; + +export class E2EPostfachNavigator { + private mainPage: MainPage = new MainPage(); + private postfach: PostfachE2EComponent = new PostfachE2EComponent(); + + public openPostfachPage(): void { + this.navigateToDomain(); + this.mainPage.getPostfachNavigationItem().click(); + exist(this.postfach.getHeadline()); + } + + private navigateToDomain(): void { + this.mainPage.getHeader().getLogo().click(); + } +} diff --git a/alfa-client/apps/admin-e2e/src/page-objects/main.po.ts b/alfa-client/apps/admin-e2e/src/page-objects/main.po.ts index 338c98286a..75ecbdaefa 100644 --- a/alfa-client/apps/admin-e2e/src/page-objects/main.po.ts +++ b/alfa-client/apps/admin-e2e/src/page-objects/main.po.ts @@ -22,17 +22,16 @@ * unter der Lizenz sind dem Lizenztext zu entnehmen. */ import { BuildInfoE2EComponent } from '../components/buildinfo/buildinfo.e2e.component'; -import { containClass, exist } from '../support/cypress.util'; import { HeaderE2EComponent } from './header.po'; export class MainPage { private readonly buildInfo: BuildInfoE2EComponent = new BuildInfoE2EComponent(); private readonly header: HeaderE2EComponent = new HeaderE2EComponent(); - private readonly benutzerNavigationItem: string = 'caption-Benutzer__Rollen'; - private readonly postfachNavigationItem: string = 'postfach-navigation'; - private readonly organisationEinheitNavigationItem: string = 'organisations-einheiten-navigation'; - private readonly statistikNavigationItem: string = 'statistik-navigation'; + private readonly benutzerNavigationItem: string = 'link-path-benutzer'; + private readonly organisationEinheitNavigationItem: string = 'link-path-organisationseinheiten'; + private readonly postfachNavigationItem: string = 'link-path-postfach'; + private readonly statistikNavigationItem: string = 'link-path-statistik'; public getBuildInfo(): BuildInfoE2EComponent { return this.buildInfo; @@ -46,41 +45,17 @@ export class MainPage { return cy.getTestElement(this.benutzerNavigationItem); } - public clickBenutzerNavigationItem(): void { - this.getBenutzerNavigationItem().click(); - } - - public benutzerNavigationItemIsVisible(): void { - exist(this.getBenutzerNavigationItem()); - } - public getPostfachNavigationItem(): Cypress.Chainable<Element> { return cy.getTestElement(this.postfachNavigationItem); } - public clickPostfachNavigationItem(): void { - this.getPostfachNavigationItem().click(); - } - public getOrganisationEinheitNavigationItem(): Cypress.Chainable<Element> { return cy.getTestElement(this.organisationEinheitNavigationItem); } - public clickOrganisationsEinheitenNavigationItem(): void { - this.getOrganisationEinheitNavigationItem().click(); - } - public getStatistikNavigationItem(): Cypress.Chainable<Element> { return cy.getTestElement(this.statistikNavigationItem); } - - public clickStatistikNavigationItem(): void { - this.getStatistikNavigationItem().click(); - } - - public isStatistikNavigationItemSelected(): void { - containClass(this.getStatistikNavigationItem().get('a'), 'border-selected'); - } } export function waitForSpinnerToDisappear(): boolean { diff --git a/alfa-client/apps/admin-e2e/src/support/cypress.util.ts b/alfa-client/apps/admin-e2e/src/support/cypress.util.ts index 6379636090..498a30cf73 100644 --- a/alfa-client/apps/admin-e2e/src/support/cypress.util.ts +++ b/alfa-client/apps/admin-e2e/src/support/cypress.util.ts @@ -128,10 +128,6 @@ export function enterWith(element: Cypress.Chainable<Element>, value: string, de element.type(CypressKeyboardActions.ENTER); } -export function typeText(element: Cypress.Chainable<Element>, value: string): void { - element.type(value); -} - export function backspaceOn(element: Cypress.Chainable<Element>): void { element.type(CypressKeyboardActions.BACKSPACE); } diff --git a/alfa-client/libs/admin/postfach/src/lib/postfach-container/postfach-container.component.html b/alfa-client/libs/admin/postfach/src/lib/postfach-container/postfach-container.component.html index 3e8e4b49ae..4cc1c39f9c 100644 --- a/alfa-client/libs/admin/postfach/src/lib/postfach-container/postfach-container.component.html +++ b/alfa-client/libs/admin/postfach/src/lib/postfach-container/postfach-container.component.html @@ -1,2 +1,2 @@ -<h1 class="heading-1">Postfach</h1> +<h1 class="heading-1" data-test-id="headline">Postfach</h1> <admin-postfach-form [postfachStateResource]="postfachStateResource$ | async" /> -- GitLab From e797e7da38a8eb66215bf949ef0d1fca20667314 Mon Sep 17 00:00:00 2001 From: Albert <Albert.Bruns@mgm-tp.com> Date: Mon, 3 Mar 2025 13:27:57 +0100 Subject: [PATCH 23/24] OZG-7590-7851 fix navigation --- alfa-client/apps/admin/src/app/app.component.spec.ts | 9 +++++++++ alfa-client/apps/admin/src/app/app.component.ts | 4 ++-- alfa-client/apps/admin/src/app/app.routes.ts | 6 ++++++ 3 files changed, 17 insertions(+), 2 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 aefc4923d4..be4c62dc1e 100644 --- a/alfa-client/apps/admin/src/app/app.component.spec.ts +++ b/alfa-client/apps/admin/src/app/app.component.spec.ts @@ -197,6 +197,7 @@ describe('AppComponent', () => { describe('evaluate initial navigation', () => { beforeEach(() => { component._evaluateNavigationByApiRoot = jest.fn(); + component._subscribeApiRootForEvaluation = jest.fn(); }); it('should call router navigate', () => { @@ -207,6 +208,14 @@ describe('AppComponent', () => { expect(router.navigate).toHaveBeenCalledWith([window.location.pathname]); }); + + it('should call subscribe api root evaluation if url starts with /?state', () => { + (router as any).url = '/?state=some-state'; + + component._evaluateInitialNavigation(); + + expect(component._subscribeApiRootForEvaluation).toHaveBeenCalled(); + }); }); describe('subscribeApiRootForEvaluation', () => { diff --git a/alfa-client/apps/admin/src/app/app.component.ts b/alfa-client/apps/admin/src/app/app.component.ts index 581cb51ffe..c76ad70cfc 100644 --- a/alfa-client/apps/admin/src/app/app.component.ts +++ b/alfa-client/apps/admin/src/app/app.component.ts @@ -93,7 +93,7 @@ export class AppComponent implements OnInit { } _evaluateInitialNavigation(): void { - if (this.router.url === '/') { + if (this.router.url.startsWith('/?state')) { this._subscribeApiRootForEvaluation(); } else { this.refreshForGuardEvaluation(); @@ -106,7 +106,7 @@ export class AppComponent implements OnInit { .subscribe((apiRootResource: ApiRootResource) => this._evaluateNavigationByApiRoot(apiRootResource)); } - private refreshForGuardEvaluation(): void { + private async refreshForGuardEvaluation(): Promise<void> { this.router.navigate([window.location.pathname]); } diff --git a/alfa-client/apps/admin/src/app/app.routes.ts b/alfa-client/apps/admin/src/app/app.routes.ts index a4cbd29d95..c5974993c2 100644 --- a/alfa-client/apps/admin/src/app/app.routes.ts +++ b/alfa-client/apps/admin/src/app/app.routes.ts @@ -44,6 +44,7 @@ export const appRoutes: Route[] = [ path: ROUTES.POSTFACH, component: PostfachPageComponent, title: 'Admin | Postfach', + runGuardsAndResolvers: 'always', canActivate: [configurationGuard], data: <GuardData>{ linkRelName: ConfigurationLinkRel.SETTING }, }, @@ -51,6 +52,7 @@ export const appRoutes: Route[] = [ path: ROUTES.BENUTZER, component: UserListPageComponent, title: 'Admin | Benutzer', + runGuardsAndResolvers: 'always', canActivate: [apiRootGuard], data: <GuardData>{ linkRelName: ApiRootLinkRel.USERS }, }, @@ -58,6 +60,7 @@ export const appRoutes: Route[] = [ path: ROUTES.BENUTZER_NEU, component: UserFormPageComponent, title: 'Admin | Benutzer anlegen', + runGuardsAndResolvers: 'always', canActivate: [apiRootGuard], data: <GuardData>{ linkRelName: ApiRootLinkRel.USERS }, }, @@ -65,6 +68,7 @@ export const appRoutes: Route[] = [ path: ROUTES.BENUTZER_ID, component: UserFormComponent, title: 'Admin | Benutzer bearbeiten', + runGuardsAndResolvers: 'always', canActivate: [apiRootGuard], data: <GuardData>{ linkRelName: ApiRootLinkRel.USERS }, }, @@ -82,6 +86,7 @@ export const appRoutes: Route[] = [ path: ROUTES.STATISTIK, component: StatistikPageComponent, title: 'Admin | Statistik', + runGuardsAndResolvers: 'always', canActivate: [configurationGuard], data: <GuardData>{ linkRelName: ConfigurationLinkRel.AGGREGATION_MAPPINGS }, }, @@ -89,6 +94,7 @@ export const appRoutes: Route[] = [ path: ROUTES.STATISTIK_NEU, component: StatistikFieldsFormPageComponent, title: 'Admin | Statistik weitere Felder auswerten', + runGuardsAndResolvers: 'always', canActivate: [configurationGuard], data: <GuardData>{ linkRelName: ConfigurationLinkRel.AGGREGATION_MAPPINGS }, }, -- GitLab From f0863ea4cbaa7feb181263ecb0db8d6d2f58e543 Mon Sep 17 00:00:00 2001 From: sebo <sebastian.bergandy@external.mgm-cp.com> Date: Mon, 3 Mar 2025 14:35:11 +0100 Subject: [PATCH 24/24] OZG-7590 remove unnecessary promise --- alfa-client/apps/admin/src/app/app.component.ts | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/alfa-client/apps/admin/src/app/app.component.ts b/alfa-client/apps/admin/src/app/app.component.ts index c76ad70cfc..e348ba876d 100644 --- a/alfa-client/apps/admin/src/app/app.component.ts +++ b/alfa-client/apps/admin/src/app/app.component.ts @@ -33,13 +33,7 @@ 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, - NavItemComponent, - OrgaUnitIconComponent, - UsersIconComponent, -} from '@ods/system'; +import { AdminLogoIconComponent, NavbarComponent, NavItemComponent, OrgaUnitIconComponent, UsersIconComponent, } from '@ods/system'; 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'; @@ -106,7 +100,7 @@ export class AppComponent implements OnInit { .subscribe((apiRootResource: ApiRootResource) => this._evaluateNavigationByApiRoot(apiRootResource)); } - private async refreshForGuardEvaluation(): Promise<void> { + private refreshForGuardEvaluation(): void { this.router.navigate([window.location.pathname]); } -- GitLab