diff --git a/alfa-client/apps/alfa/src/app/app.component.spec.ts b/alfa-client/apps/alfa/src/app/app.component.spec.ts index 5bf3f422b0147d964b21fd1f8f585b20c69f2e06..0ca6382a2e0987cdbc277b9d388a891049849563 100644 --- a/alfa-client/apps/alfa/src/app/app.component.spec.ts +++ b/alfa-client/apps/alfa/src/app/app.component.spec.ts @@ -21,16 +21,16 @@ * Die sprachspezifischen Genehmigungen und Beschränkungen * unter der Lizenz sind dem Lizenztext zu entnehmen. */ -import { registerLocaleData } from '@angular/common'; -import localeDe from '@angular/common/locales/de'; -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { RouterTestingModule } from '@angular/router/testing'; import { ApiRootFacade } from '@alfa-client/api-root-shared'; import { ENVIRONMENT_CONFIG } from '@alfa-client/environment-shared'; import { NavigationService } from '@alfa-client/navigation-shared'; import { createEmptyStateResource, createStateResource } from '@alfa-client/tech-shared'; import { mock, notExistsAsHtmlElement } from '@alfa-client/test-utils'; import { IconService, SpinnerComponent } from '@alfa-client/ui'; +import { registerLocaleData } from '@angular/common'; +import localeDe from '@angular/common/locales/de'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; import { AuthConfig, OAuthEvent, OAuthService } from 'angular-oauth2-oidc'; import { createApiRootResource } from 'libs/api-root-shared/test/api-root'; import { BuildInfoComponent } from 'libs/navigation/src/lib/build-info/build-info.component'; @@ -41,6 +41,7 @@ import { MockComponent } from 'ng-mocks'; import { of } from 'rxjs'; import { AppComponent } from './app.component'; +import { Params } from '@angular/router'; import * as VorgangNavigationUtil from 'libs/vorgang-shared/src/lib/vorgang-navigation.util'; registerLocaleData(localeDe); @@ -364,6 +365,15 @@ describe('AppComponent', () => { done(); }); }); + + it('should call buildInitialQueryParams', (done) => { + component.buildInitialQueryParams = jest.fn(); + + component.loadApiRootAndNavigate().subscribe(() => { + expect(component.buildInitialQueryParams).toHaveBeenCalled(); + done(); + }); + }); }); describe('apiRoot.resource is null', () => { @@ -420,9 +430,7 @@ describe('AppComponent', () => { it('should return path segments from localStorage', () => { const pathSegments: string[] = ['alle', 'angenommen']; - jest - .spyOn(VorgangNavigationUtil, 'buildPathSegmentsFromLocalStorage') - .mockReturnValue(pathSegments); + jest.spyOn(VorgangNavigationUtil, 'buildPathSegmentsFromLocalStorage').mockReturnValue(pathSegments); const redirectUri: string = component.buildInitialPath(); @@ -430,4 +438,36 @@ describe('AppComponent', () => { }); }); }); + + describe('buildInitialQueryParams', () => { + describe('with existing window.location.search', () => { + const query: string = '?uri=some-uri'; + const url: string = 'http://localhost/resources' + query; + + beforeEach(() => { + setWindowLocationUrl(url); + }); + + it('should return query params', () => { + const expected: Params = { uri: 'some-uri' }; + const queryParams: Params = component.buildInitialQueryParams(); + + expect(queryParams).toStrictEqual(expected); + }); + }); + + describe('with empty window.location.search', () => { + const url: string = 'http://localhost/resources'; + + beforeEach(() => { + setWindowLocationUrl(url); + }); + + it('should return undefined', () => { + const queryParams: Params = component.buildInitialQueryParams(); + + expect(queryParams).toBeUndefined(); + }); + }); + }); }); diff --git a/alfa-client/apps/alfa/src/app/app.component.ts b/alfa-client/apps/alfa/src/app/app.component.ts index db8d9b623124ec7b0b5a1cdc259d3d919742220e..b8a23b6510cbd1aabcb5d28e3f77697b2d4d22c5 100644 --- a/alfa-client/apps/alfa/src/app/app.component.ts +++ b/alfa-client/apps/alfa/src/app/app.component.ts @@ -28,6 +28,7 @@ import { StateResource, isNotNull } from '@alfa-client/tech-shared'; import { IconService } from '@alfa-client/ui'; import { buildPathSegmentsFromLocalStorage } from '@alfa-client/vorgang-shared'; import { Component, Inject, OnInit } from '@angular/core'; +import { Params } from '@angular/router'; import { AuthConfig, OAuthEvent, OAuthService } from 'angular-oauth2-oidc'; import { JwksValidationHandler } from 'angular-oauth2-oidc-jwks'; import { Environment } from 'libs/environment-shared/src/lib/environment.model'; @@ -70,7 +71,7 @@ export class AppComponent implements OnInit { issuer: this.envConfig.authServer + '/realms/' + this.envConfig.realm, // URL of the SPA to redirect the user to after login - redirectUri: window.location.origin + this.buildInitialPath(), + redirectUri: window.location.origin + this.buildInitialPath() + window.location.search, postLogoutRedirectUri: window.location.origin + '/', // URL of the SPA to redirect the user after silent refresh @@ -87,18 +88,32 @@ export class AppComponent implements OnInit { } buildInitialPath(): string { - return window.location.pathname === '/' ? - '/' + buildPathSegmentsFromLocalStorage().join('/') - : window.location.pathname; + const currentPath: string = window.location.pathname; + if (currentPath === '/') { + return currentPath + buildPathSegmentsFromLocalStorage().join('/'); + } + + return currentPath; + } + + buildInitialQueryParams(): Params | undefined { + const queryParams: URLSearchParams = new URLSearchParams(window.location.search); + if (!queryParams.toString()) { + return undefined; + } + + const params: Params = {}; + queryParams.forEach((value: string, key: string) => { + params[key] = value; + }); + + return params; } listenToOAuthEvents(): void { this.oAuthEventSubscription = this.oAuthService.events .pipe( - filter( - (event: OAuthEvent) => - this.consideredAsLoginEvent(event.type) || this.consideredAsPageReloadEvent(event.type), - ), + filter((event: OAuthEvent) => this.consideredAsLoginEvent(event.type) || this.consideredAsPageReloadEvent(event.type)), ) .subscribe(() => (this.apiRoot$ = this.loadApiRootAndNavigate())); } @@ -119,7 +134,7 @@ export class AppComponent implements OnInit { return this.apiRootFacade.getApiRoot().pipe( filter((apiRoot) => isNotNull(apiRoot.resource)), tap(() => { - this.navigationService.navigate(this.buildInitialPath()); + this.navigationService.navigate(this.buildInitialPath(), this.buildInitialQueryParams()); this.oAuthEventSubscription.unsubscribe(); }), ); diff --git a/alfa-client/apps/alfa/src/app/app.module.ts b/alfa-client/apps/alfa/src/app/app.module.ts index d62c5b53a37b34edb07e591c0a9bbc659e0e0da0..1fa6b2275b9937d22bb5fd8f8b8ce4c6995a2e34 100644 --- a/alfa-client/apps/alfa/src/app/app.module.ts +++ b/alfa-client/apps/alfa/src/app/app.module.ts @@ -27,6 +27,7 @@ import { EnvironmentModule } from '@alfa-client/environment-shared'; import { HintSharedModule } from '@alfa-client/hint-shared'; import { NavigationModule } from '@alfa-client/navigation'; import { OzgCloudUrlSerializer } from '@alfa-client/navigation-shared'; +import { ResourceRedirectComponent } from '@alfa-client/resource-redirect'; import { UiModule } from '@alfa-client/ui'; import { registerLocaleData } from '@angular/common'; import { HttpClientModule } from '@angular/common/http'; @@ -50,6 +51,10 @@ const routes: Routes = [ path: '', loadChildren: () => import('@alfa-client/vorgang').then((m) => m.VorgangModule), }, + { + path: 'resources', + component: ResourceRedirectComponent, + }, ]; @NgModule({ diff --git a/alfa-client/libs/api-root-shared/src/lib/api-root.linkrel.ts b/alfa-client/libs/api-root-shared/src/lib/api-root.linkrel.ts index e4d1553f981c6a4947b1151afcbd7af95636ba47..fed6b6cc56cb6ded4a3d5b9f19a68578ec28edcb 100644 --- a/alfa-client/libs/api-root-shared/src/lib/api-root.linkrel.ts +++ b/alfa-client/libs/api-root-shared/src/lib/api-root.linkrel.ts @@ -60,4 +60,5 @@ export enum ApiRootLinkRel { UNASSIGNED_ZU_LOESCHEN = 'vorgaenge_zu_loeschen_unassigned', DOCUMENTATIONS = 'documentations', HINTS = 'hints', + RESOURCE = 'resource', } diff --git a/alfa-client/libs/navigation-shared/src/lib/navigation.service.spec.ts b/alfa-client/libs/navigation-shared/src/lib/navigation.service.spec.ts index 3e145df00f98d92bea93280ca06fedb67a4fae90..9352facb64aceeb52f8ce199c338f54ff5dd882d 100644 --- a/alfa-client/libs/navigation-shared/src/lib/navigation.service.spec.ts +++ b/alfa-client/libs/navigation-shared/src/lib/navigation.service.spec.ts @@ -21,9 +21,9 @@ * Die sprachspezifischen Genehmigungen und Beschränkungen * unter der Lizenz sind dem Lizenztext zu entnehmen. */ -import { ActivatedRoute, Params, Router, UrlSegment } from '@angular/router'; import { AppService } from '@alfa-client/app-shared'; import { Mock, mock, useFromMock } from '@alfa-client/test-utils'; +import { ActivatedRoute, Params, Router, UrlSegment } from '@angular/router'; import { ResourceUri } from '@ngxp/rest'; import { OAuthService } from 'angular-oauth2-oidc'; import { of } from 'rxjs'; @@ -272,10 +272,18 @@ describe('NavigationService', () => { describe('navigate', () => { const navigationRoute: string = 'dummyNavigationRoute'; - it('should call router with given navigationRoute', () => { + it('should call router with given navigationRoute and queryParams', () => { + const queryParams: Params = { uri: 'some-uri' }; + + service.navigate(navigationRoute, queryParams); + + expect(router.navigate).toHaveBeenCalledWith([navigationRoute], { queryParams }); + }); + + it('should call router with given navigationRoute and without queryParams', () => { service.navigate(navigationRoute); - expect(router.navigate).toHaveBeenCalledWith([navigationRoute]); + expect(router.navigate).toHaveBeenCalledWith([navigationRoute], { queryParams: undefined }); }); }); diff --git a/alfa-client/libs/navigation-shared/src/lib/navigation.service.ts b/alfa-client/libs/navigation-shared/src/lib/navigation.service.ts index f026dc54b49dd0be9457230bfdfdadbb9978112e..4d17176a8489f2a47596f9e8187670f287151058 100644 --- a/alfa-client/libs/navigation-shared/src/lib/navigation.service.ts +++ b/alfa-client/libs/navigation-shared/src/lib/navigation.service.ts @@ -21,22 +21,22 @@ * Die sprachspezifischen Genehmigungen und Beschränkungen * unter der Lizenz sind dem Lizenztext zu entnehmen. */ +import { + decodeUrlFromEmbedding, + isEmptyObject, + isNotNil, + isNotNull, + isNotUndefined, +} from '@alfa-client/tech-shared'; import { Injectable } from '@angular/core'; import { ActivatedRoute, NavigationEnd, - Params, PRIMARY_OUTLET, + Params, Router, UrlSegment, } from '@angular/router'; -import { - decodeUrlFromEmbedding, - isEmptyObject, - isNotNil, - isNotNull, - isNotUndefined, -} from '@alfa-client/tech-shared'; import { ResourceUri } from '@ngxp/rest'; import { OAuthService } from 'angular-oauth2-oidc'; import { has, isNil, isUndefined } from 'lodash-es'; @@ -220,8 +220,8 @@ export class NavigationService { ); } - navigate(navigationRoute: string): void { - this.router.navigate([navigationRoute]); + navigate(navigationRoute: string, queryParams?: Params): void { + this.router.navigate([navigationRoute], { queryParams }); } public navigateToVorgang(linkUri: ResourceUri): void { diff --git a/alfa-client/libs/resource-redirect-shared/.eslintrc.json b/alfa-client/libs/resource-redirect-shared/.eslintrc.json new file mode 100644 index 0000000000000000000000000000000000000000..243c51741f65cc7afb3a7d85531c24afdcab5e56 --- /dev/null +++ b/alfa-client/libs/resource-redirect-shared/.eslintrc.json @@ -0,0 +1,33 @@ +{ + "extends": ["../../.eslintrc.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts"], + "extends": ["plugin:@nx/angular", "plugin:@angular-eslint/template/process-inline-templates"], + "rules": { + "@angular-eslint/directive-selector": [ + "error", + { + "type": "attribute", + "prefix": "alfa", + "style": "camelCase" + } + ], + "@angular-eslint/component-selector": [ + "error", + { + "type": "element", + "prefix": "alfa", + "style": "kebab-case" + } + ] + } + }, + { + "files": ["*.html"], + "extends": ["plugin:@nx/angular-template"], + "rules": {} + } + ] +} diff --git a/alfa-client/libs/resource-redirect-shared/README.md b/alfa-client/libs/resource-redirect-shared/README.md new file mode 100644 index 0000000000000000000000000000000000000000..fb7bd17a7b0de755ee41a4baf2d7b381c646d38e --- /dev/null +++ b/alfa-client/libs/resource-redirect-shared/README.md @@ -0,0 +1,7 @@ +# resource-redirect-shared + +This library was generated with [Nx](https://nx.dev). + +## Running unit tests + +Run `nx test resource-redirect-shared` to execute the unit tests. diff --git a/alfa-client/libs/resource-redirect-shared/jest.config.ts b/alfa-client/libs/resource-redirect-shared/jest.config.ts new file mode 100644 index 0000000000000000000000000000000000000000..5f5416cd232f2295dc3b0651a88a73fe4bd2b125 --- /dev/null +++ b/alfa-client/libs/resource-redirect-shared/jest.config.ts @@ -0,0 +1,23 @@ +/* eslint-disable */ +export default { + displayName: 'resource-redirect-shared', + preset: '../../jest.preset.js', + setupFilesAfterEnv: ['<rootDir>/src/test-setup.ts'], + globals: {}, + coverageDirectory: '../../coverage/libs/resource-redirect-shared', + snapshotSerializers: [ + 'jest-preset-angular/build/serializers/no-ng-attributes', + 'jest-preset-angular/build/serializers/ng-snapshot', + 'jest-preset-angular/build/serializers/html-comment', + ], + transform: { + '^.+.(ts|mjs|js|html)$': [ + 'jest-preset-angular', + { + tsconfig: '<rootDir>/tsconfig.spec.json', + stringifyContentPathRegex: '\\.(html|svg)$', + }, + ], + }, + transformIgnorePatterns: ['node_modules/(?!.*.mjs$)'], +}; diff --git a/alfa-client/libs/resource-redirect-shared/project.json b/alfa-client/libs/resource-redirect-shared/project.json new file mode 100644 index 0000000000000000000000000000000000000000..bc5ab7022b5c2451a172119c4d63192c87e6d46d --- /dev/null +++ b/alfa-client/libs/resource-redirect-shared/project.json @@ -0,0 +1,22 @@ +{ + "name": "resource-redirect-shared", + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "projectType": "library", + "sourceRoot": "libs/resource-redirect-shared/src", + "prefix": "alfa", + "tags": [], + "targets": { + "test": { + "executor": "@nx/jest:jest", + "outputs": ["{workspaceRoot}/coverage/libs/resource-redirect-shared"], + "options": { + "tsConfig": "libs/resource-redirect-shared/tsconfig.spec.json", + "jestConfig": "libs/resource-redirect-shared/jest.config.ts" + } + }, + "lint": { + "executor": "@nx/eslint:lint", + "outputs": ["{options.outputFile}"] + } + } +} diff --git a/alfa-client/libs/resource-redirect-shared/src/index.ts b/alfa-client/libs/resource-redirect-shared/src/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..0fddcd8bb9c2da9870a37ff64a1d22e6fbf47937 --- /dev/null +++ b/alfa-client/libs/resource-redirect-shared/src/index.ts @@ -0,0 +1,2 @@ +export * from './lib/resource-redirect-shared.module'; +export * from './lib/resource-redirect.service'; diff --git a/alfa-client/libs/resource-redirect-shared/src/lib/resource-linkrel.ts b/alfa-client/libs/resource-redirect-shared/src/lib/resource-linkrel.ts new file mode 100644 index 0000000000000000000000000000000000000000..b29d9cb7e4fae12057f20b46928254748967ed88 --- /dev/null +++ b/alfa-client/libs/resource-redirect-shared/src/lib/resource-linkrel.ts @@ -0,0 +1,3 @@ +export enum ResourceLinkRel { + VORGANG = 'vorgang', +} diff --git a/alfa-client/libs/resource-redirect-shared/src/lib/resource-redirect-shared.module.spec.ts b/alfa-client/libs/resource-redirect-shared/src/lib/resource-redirect-shared.module.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..20513fcbeffe5a87e7b3cf38530e82fb9793c0cf --- /dev/null +++ b/alfa-client/libs/resource-redirect-shared/src/lib/resource-redirect-shared.module.spec.ts @@ -0,0 +1,14 @@ +import { ResourceRedirectSharedModule } from '@alfa-client/resource-redirect-shared'; +import { TestBed } from '@angular/core/testing'; + +describe('ResourceRedirectSharedModule', () => { + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ResourceRedirectSharedModule], + }).compileComponents(); + }); + + it('should create', () => { + expect(ResourceRedirectSharedModule).toBeDefined(); + }); +}); diff --git a/alfa-client/libs/resource-redirect-shared/src/lib/resource-redirect-shared.module.ts b/alfa-client/libs/resource-redirect-shared/src/lib/resource-redirect-shared.module.ts new file mode 100644 index 0000000000000000000000000000000000000000..256e6ccd43670ceb3d9e267a0844304c40b80b30 --- /dev/null +++ b/alfa-client/libs/resource-redirect-shared/src/lib/resource-redirect-shared.module.ts @@ -0,0 +1,7 @@ +import { TechSharedModule } from '@alfa-client/tech-shared'; +import { NgModule } from '@angular/core'; + +@NgModule({ + imports: [TechSharedModule], +}) +export class ResourceRedirectSharedModule {} diff --git a/alfa-client/libs/resource-redirect-shared/src/lib/resource-redirect.service.spec.ts b/alfa-client/libs/resource-redirect-shared/src/lib/resource-redirect.service.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..af57548e97be941f72b6e57cb287649c6b6baecd --- /dev/null +++ b/alfa-client/libs/resource-redirect-shared/src/lib/resource-redirect.service.spec.ts @@ -0,0 +1,167 @@ +import { ApiRootLinkRel, ApiRootResource, ApiRootService } from '@alfa-client/api-root-shared'; +import { NavigationService } from '@alfa-client/navigation-shared'; +import { ResourceRedirectService } from '@alfa-client/resource-redirect-shared'; +import { + ResourceRepository, + StateResource, + createEmptyStateResource, + createStateResource, + toResourceUri, +} from '@alfa-client/tech-shared'; +import { Mock, mock, useFromMock } from '@alfa-client/test-utils'; +import { Messages, SnackBarService } from '@alfa-client/ui'; +import { Resource, ResourceUri } from '@ngxp/rest'; +import { of, throwError } from 'rxjs'; +import { createApiRootResource } from '../../../api-root-shared/test/api-root'; +import { createDummyResource } from '../../../tech-shared/test/resource'; +import { ResourceLinkRel } from './resource-linkrel'; + +describe('ResourceRedirectService', () => { + let service: ResourceRedirectService; + let apiRootService: Mock<ApiRootService>; + let repository: Mock<ResourceRepository>; + let snackbarService: Mock<SnackBarService>; + let navigationService: Mock<NavigationService>; + + const resourceUri: ResourceUri = 'some-uri'; + + beforeEach(() => { + apiRootService = mock(ApiRootService); + repository = mock(ResourceRepository); + snackbarService = mock(SnackBarService); + navigationService = mock(NavigationService); + + service = new ResourceRedirectService( + useFromMock(apiRootService), + useFromMock(repository), + useFromMock(snackbarService), + useFromMock(navigationService), + ); + }); + + describe('redirectToResource', () => { + it('should call getResolvedResource', () => { + const resource: Resource = createDummyResource([ResourceLinkRel.VORGANG]); + service.getResolvedResource = jest.fn().mockReturnValue(of(resource)); + + service.redirectToResource(resourceUri); + + expect(service.getResolvedResource).toHaveBeenCalled(); + }); + + it('should call navigateToResource on success', () => { + const resource: Resource = createDummyResource([ResourceLinkRel.VORGANG]); + service.getResolvedResource = jest.fn().mockReturnValue(of(resource)); + service.navigateToResource = jest.fn(); + + service.redirectToResource(resourceUri); + + expect(service.navigateToResource).toHaveBeenCalled(); + }); + + it('should call handleResolveError on error', () => { + service.getResolvedResource = jest.fn().mockReturnValue(throwError(() => new Error('some error'))); + service.handleResolveError = jest.fn(); + + service.redirectToResource(resourceUri); + + expect(service.handleResolveError).toHaveBeenCalled(); + }); + }); + + describe('getResolvedResource', () => { + it('should call apiRootService getApiRoot', () => { + const apiRoot: StateResource<ApiRootResource> = createStateResource(createApiRootResource()); + apiRootService.getApiRoot.mockReturnValue(of(apiRoot)); + + service.getResolvedResource(resourceUri).subscribe(() => { + expect(apiRootService.getApiRoot).toHaveBeenCalled(); + }); + }); + + it('should return null observable on empty api root resource', () => { + apiRootService.getApiRoot.mockReturnValue(of(createEmptyStateResource())); + + service.getResolvedResource(resourceUri).subscribe((resolvedResource: Resource) => { + expect(resolvedResource).toBeNull(); + }); + }); + + it('should call repository getResource with result from buildResolveUri', () => { + const apiRoot: StateResource<ApiRootResource> = createStateResource(createApiRootResource([ApiRootLinkRel.RESOURCE])); + apiRootService.getApiRoot.mockReturnValue(of(apiRoot)); + service.buildResolveUri = jest.fn().mockReturnValue('some-uri'); + + service.getResolvedResource(resourceUri).subscribe(() => { + expect(repository.getResource).toHaveBeenCalledWith('some-uri'); + }); + }); + + it('should return null observable on repository error', () => { + const apiRoot: StateResource<ApiRootResource> = createStateResource(createApiRootResource([ApiRootLinkRel.RESOURCE])); + apiRootService.getApiRoot.mockReturnValue(of(apiRoot)); + repository.getResource.mockReturnValue(throwError(() => new Error('some error'))); + + service.getResolvedResource(resourceUri).subscribe((resolvedResource: Resource) => { + expect(resolvedResource).toBeNull(); + }); + }); + + it('should return resolved resource', () => { + const resource: Resource = createDummyResource([ResourceLinkRel.VORGANG]); + const apiRoot: StateResource<ApiRootResource> = createStateResource(createApiRootResource([ApiRootLinkRel.RESOURCE])); + apiRootService.getApiRoot.mockReturnValue(of(apiRoot)); + repository.getResource.mockReturnValue(of(resource)); + + service.getResolvedResource(resourceUri).subscribe((resolvedResource: Resource) => { + expect(resolvedResource).toEqual(resource); + }); + }); + }); + + describe('buildResolveUri', () => { + it('should build resolve uri', () => { + const apiRootResource: ApiRootResource = createApiRootResource([ApiRootLinkRel.RESOURCE]); + const resolveUri: ResourceUri = service.buildResolveUri(apiRootResource, resourceUri); + + expect(resolveUri).toBeDefined(); + }); + + it('should throw error on missing resource uri', () => { + const apiRootResource: ApiRootResource = createApiRootResource(); + + expect(() => service.buildResolveUri(apiRootResource, resourceUri)).toThrow(); + }); + }); + + describe('handleResolveError', () => { + it('should call snackbarService showInfo', () => { + service.handleResolveError(); + + expect(snackbarService.showInfo).toHaveBeenCalledWith(Messages.HTTP_STATUS_RESOURCE_NOT_FOUND); + }); + + it('should call navigationService navigateToVorgangList', () => { + service.handleResolveError(); + + expect(navigationService.navigateToVorgangList).toHaveBeenCalled(); + }); + }); + + describe('navigateToResource', () => { + it('should call navigationService navigateToVorgang', () => { + const resource: Resource = createDummyResource([ResourceLinkRel.VORGANG]); + const resourceUri: ResourceUri = toResourceUri(resource, ResourceLinkRel.VORGANG); + + service.navigateToResource(resource); + + expect(navigationService.navigateToVorgang).toHaveBeenCalledWith(resourceUri); + }); + + it('should not call navigationService on missing resource', () => { + service.navigateToResource(createDummyResource()); + + expect(navigationService.navigateToVorgang).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/alfa-client/libs/resource-redirect-shared/src/lib/resource-redirect.service.ts b/alfa-client/libs/resource-redirect-shared/src/lib/resource-redirect.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..a07508e0d0d73bd38329590059db15448e80b755 --- /dev/null +++ b/alfa-client/libs/resource-redirect-shared/src/lib/resource-redirect.service.ts @@ -0,0 +1,55 @@ +import { ApiRootLinkRel, ApiRootResource, ApiRootService } from '@alfa-client/api-root-shared'; +import { NavigationService } from '@alfa-client/navigation-shared'; +import { ResourceRepository, StateResource, isLoaded, toResourceUri } from '@alfa-client/tech-shared'; +import { Messages, SnackBarService } from '@alfa-client/ui'; +import { Injectable } from '@angular/core'; +import { Resource, ResourceUri, getUrl, hasLink } from '@ngxp/rest'; +import { Observable, first, switchMap } from 'rxjs'; +import { filter } from 'rxjs/operators'; +import { ResourceLinkRel } from './resource-linkrel'; + +@Injectable({ providedIn: 'root' }) +export class ResourceRedirectService { + private readonly PARAM_URI_PLACEHOLDER: string = '{uri}'; + + constructor( + private apiRootService: ApiRootService, + private repository: ResourceRepository, + private snackbarService: SnackBarService, + private navigationService: NavigationService, + ) {} + + public redirectToResource(resourceUri: ResourceUri): void { + this.getResolvedResource(resourceUri) + .pipe(first()) + .subscribe({ + next: (resource: Resource) => this.navigateToResource(resource), + error: () => this.handleResolveError(), + }); + } + + getResolvedResource(resourceUri: string): Observable<Resource> { + return this.apiRootService.getApiRoot().pipe( + filter(isLoaded), + switchMap((apiRoot: StateResource<ApiRootResource>) => + this.repository.getResource<Resource>(this.buildResolveUri(apiRoot.resource, resourceUri)), + ), + ); + } + + buildResolveUri(apiRootResource: ApiRootResource, resourceUri: ResourceUri): ResourceUri { + return getUrl(apiRootResource, ApiRootLinkRel.RESOURCE).replace(this.PARAM_URI_PLACEHOLDER, encodeURIComponent(resourceUri)); + } + + navigateToResource(resource: Resource): void { + if (hasLink(resource, ResourceLinkRel.VORGANG)) { + const vorgangUri: ResourceUri = toResourceUri(resource, ResourceLinkRel.VORGANG); + this.navigationService.navigateToVorgang(vorgangUri); + } + } + + handleResolveError(): void { + this.snackbarService.showInfo(Messages.HTTP_STATUS_RESOURCE_NOT_FOUND); + this.navigationService.navigateToVorgangList(); + } +} diff --git a/alfa-client/libs/resource-redirect-shared/src/test-setup.ts b/alfa-client/libs/resource-redirect-shared/src/test-setup.ts new file mode 100644 index 0000000000000000000000000000000000000000..f7fad5e59f9d4e62a097bbea63040551de396175 --- /dev/null +++ b/alfa-client/libs/resource-redirect-shared/src/test-setup.ts @@ -0,0 +1,11 @@ +import 'jest-preset-angular/setup-jest'; + +import { getTestBed } from '@angular/core/testing'; +import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; + +getTestBed().resetTestEnvironment(); +getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting(), { + teardown: { destroyAfterEach: false }, + errorOnUnknownProperties: true, + errorOnUnknownElements: true, +}); diff --git a/alfa-client/libs/resource-redirect-shared/tsconfig.json b/alfa-client/libs/resource-redirect-shared/tsconfig.json new file mode 100644 index 0000000000000000000000000000000000000000..7cc6baf2f58ed5ccfba098131996f579979e9f18 --- /dev/null +++ b/alfa-client/libs/resource-redirect-shared/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "../../tsconfig.base.json", + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ], + "compilerOptions": { + "target": "es2022" + } +} diff --git a/alfa-client/libs/resource-redirect-shared/tsconfig.lib.json b/alfa-client/libs/resource-redirect-shared/tsconfig.lib.json new file mode 100644 index 0000000000000000000000000000000000000000..1cc2d08ae0a5326f0b4f68921f77e265c7b9f2f4 --- /dev/null +++ b/alfa-client/libs/resource-redirect-shared/tsconfig.lib.json @@ -0,0 +1,14 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "target": "es2015", + "declaration": true, + "declarationMap": true, + "inlineSources": true, + "types": [], + "lib": ["dom", "es2018"] + }, + "exclude": ["src/test-setup.ts", "**/*.spec.ts", "jest.config.ts"], + "include": ["**/*.ts"] +} diff --git a/alfa-client/libs/resource-redirect-shared/tsconfig.spec.json b/alfa-client/libs/resource-redirect-shared/tsconfig.spec.json new file mode 100644 index 0000000000000000000000000000000000000000..3a690070a7f5e48080dd36522d6a0db384d940aa --- /dev/null +++ b/alfa-client/libs/resource-redirect-shared/tsconfig.spec.json @@ -0,0 +1,12 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "module": "commonjs", + "types": ["jest", "node"], + "target": "ES2022", + "useDefineForClassFields": false + }, + "files": ["src/test-setup.ts"], + "include": ["**/*.spec.ts", "**/*.d.ts", "jest.config.ts"] +} diff --git a/alfa-client/libs/resource-redirect/.eslintrc.json b/alfa-client/libs/resource-redirect/.eslintrc.json new file mode 100644 index 0000000000000000000000000000000000000000..243c51741f65cc7afb3a7d85531c24afdcab5e56 --- /dev/null +++ b/alfa-client/libs/resource-redirect/.eslintrc.json @@ -0,0 +1,33 @@ +{ + "extends": ["../../.eslintrc.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts"], + "extends": ["plugin:@nx/angular", "plugin:@angular-eslint/template/process-inline-templates"], + "rules": { + "@angular-eslint/directive-selector": [ + "error", + { + "type": "attribute", + "prefix": "alfa", + "style": "camelCase" + } + ], + "@angular-eslint/component-selector": [ + "error", + { + "type": "element", + "prefix": "alfa", + "style": "kebab-case" + } + ] + } + }, + { + "files": ["*.html"], + "extends": ["plugin:@nx/angular-template"], + "rules": {} + } + ] +} diff --git a/alfa-client/libs/resource-redirect/README.md b/alfa-client/libs/resource-redirect/README.md new file mode 100644 index 0000000000000000000000000000000000000000..0a136c5a9e89c41f65db0d361b5a22246fb4b310 --- /dev/null +++ b/alfa-client/libs/resource-redirect/README.md @@ -0,0 +1,7 @@ +# resource-redirect + +This library was generated with [Nx](https://nx.dev). + +## Running unit tests + +Run `nx test resource-redirect` to execute the unit tests. diff --git a/alfa-client/libs/resource-redirect/jest.config.ts b/alfa-client/libs/resource-redirect/jest.config.ts new file mode 100644 index 0000000000000000000000000000000000000000..b6b14e2a28ee8fa74596ebd24877c7539cf3fccd --- /dev/null +++ b/alfa-client/libs/resource-redirect/jest.config.ts @@ -0,0 +1,23 @@ +/* eslint-disable */ +export default { + displayName: 'resource-redirect', + preset: '../../jest.preset.js', + setupFilesAfterEnv: ['<rootDir>/src/test-setup.ts'], + globals: {}, + coverageDirectory: '../../coverage/libs/resource-redirect', + snapshotSerializers: [ + 'jest-preset-angular/build/serializers/no-ng-attributes', + 'jest-preset-angular/build/serializers/ng-snapshot', + 'jest-preset-angular/build/serializers/html-comment', + ], + transform: { + '^.+.(ts|mjs|js|html)$': [ + 'jest-preset-angular', + { + tsconfig: '<rootDir>/tsconfig.spec.json', + stringifyContentPathRegex: '\\.(html|svg)$', + }, + ], + }, + transformIgnorePatterns: ['node_modules/(?!.*.mjs$)'], +}; diff --git a/alfa-client/libs/resource-redirect/project.json b/alfa-client/libs/resource-redirect/project.json new file mode 100644 index 0000000000000000000000000000000000000000..499fa861a271efac4aa55fa3f4c11ebd053816ae --- /dev/null +++ b/alfa-client/libs/resource-redirect/project.json @@ -0,0 +1,22 @@ +{ + "name": "resource-redirect", + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "projectType": "library", + "sourceRoot": "libs/resource-redirect/src", + "prefix": "alfa", + "tags": [], + "targets": { + "test": { + "executor": "@nx/jest:jest", + "outputs": ["{workspaceRoot}/coverage/libs/resource-redirect"], + "options": { + "tsConfig": "libs/resource-redirect/tsconfig.spec.json", + "jestConfig": "libs/resource-redirect/jest.config.ts" + } + }, + "lint": { + "executor": "@nx/eslint:lint", + "outputs": ["{options.outputFile}"] + } + } +} diff --git a/alfa-client/libs/resource-redirect/src/index.ts b/alfa-client/libs/resource-redirect/src/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..e4a766253a2782eff7d0ecfda2de2d801841f432 --- /dev/null +++ b/alfa-client/libs/resource-redirect/src/index.ts @@ -0,0 +1,2 @@ +export * from './lib/resource-redirect.module'; +export * from './lib/resource-redirect/resource-redirect.component'; diff --git a/alfa-client/libs/resource-redirect/src/lib/resource-redirect.module.spec.ts b/alfa-client/libs/resource-redirect/src/lib/resource-redirect.module.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..e05228ea76680c16b381cbb8f2d0028704e11d16 --- /dev/null +++ b/alfa-client/libs/resource-redirect/src/lib/resource-redirect.module.spec.ts @@ -0,0 +1,14 @@ +import { ResourceRedirectModule } from '@alfa-client/resource-redirect'; +import { TestBed } from '@angular/core/testing'; + +describe('ResourceRedirectModule', () => { + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ResourceRedirectModule], + }).compileComponents(); + }); + + it('should create', () => { + expect(ResourceRedirectModule).toBeDefined(); + }); +}); diff --git a/alfa-client/libs/resource-redirect/src/lib/resource-redirect.module.ts b/alfa-client/libs/resource-redirect/src/lib/resource-redirect.module.ts new file mode 100644 index 0000000000000000000000000000000000000000..d90f3d0714637282aa4694cf8e941cdd9b33425a --- /dev/null +++ b/alfa-client/libs/resource-redirect/src/lib/resource-redirect.module.ts @@ -0,0 +1,9 @@ +import { ResourceRedirectSharedModule } from '@alfa-client/resource-redirect-shared'; +import { NgModule } from '@angular/core'; +import { ResourceRedirectComponent } from './resource-redirect/resource-redirect.component'; + +@NgModule({ + imports: [ResourceRedirectSharedModule, ResourceRedirectComponent], + exports: [ResourceRedirectComponent], +}) +export class ResourceRedirectModule {} diff --git a/alfa-client/libs/resource-redirect/src/lib/resource-redirect/resource-redirect.component.spec.ts b/alfa-client/libs/resource-redirect/src/lib/resource-redirect/resource-redirect.component.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..b56eb80fb1e8f017d8d4568ac348f1269c652528 --- /dev/null +++ b/alfa-client/libs/resource-redirect/src/lib/resource-redirect/resource-redirect.component.spec.ts @@ -0,0 +1,67 @@ +import { ResourceRedirectComponent } from '@alfa-client/resource-redirect'; +import { ResourceRedirectService } from '@alfa-client/resource-redirect-shared'; +import { Mock, mock } from '@alfa-client/test-utils'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ActivatedRoute } from '@angular/router'; +import { Observable, of } from 'rxjs'; + +describe('ResourceRedirectComponent', () => { + let component: ResourceRedirectComponent; + let fixture: ComponentFixture<ResourceRedirectComponent>; + let resourceRedirectService: Mock<ResourceRedirectService>; + + const initActivatedRoute = (queryParamKey?: string): { queryParamMap: Observable<Map<string, string>> } => ({ + queryParamMap: of(new Map(queryParamKey ? [[queryParamKey, 'some-uri']] : [])), + }); + + const createComponentWithQueryParam = (queryParamKey?: string): void => { + TestBed.configureTestingModule({ + imports: [ResourceRedirectComponent], + providers: [ + { provide: ActivatedRoute, useValue: initActivatedRoute(queryParamKey) }, + { provide: ResourceRedirectService, useValue: resourceRedirectService }, + ], + }); + + fixture = TestBed.createComponent(ResourceRedirectComponent); + component = fixture.componentInstance; + }; + + beforeEach(() => { + TestBed.resetTestingModule(); + + resourceRedirectService = mock(ResourceRedirectService); + }); + + it('should create', () => { + createComponentWithQueryParam(); + + expect(component).toBeTruthy(); + }); + + describe('ngOnInit', () => { + it('should call resource service redirectToResource', () => { + createComponentWithQueryParam('uri'); + + component.ngOnInit(); + + expect(resourceRedirectService.redirectToResource).toHaveBeenCalledWith('some-uri'); + }); + + it('should not call resource service redirectToResource with other query param', () => { + createComponentWithQueryParam('some-other-param'); + + component.ngOnInit(); + + expect(resourceRedirectService.redirectToResource).not.toHaveBeenCalled(); + }); + + it('should not call resource service redirectToResource without uri param', () => { + createComponentWithQueryParam(); + + component.ngOnInit(); + + expect(resourceRedirectService.redirectToResource).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/alfa-client/libs/resource-redirect/src/lib/resource-redirect/resource-redirect.component.ts b/alfa-client/libs/resource-redirect/src/lib/resource-redirect/resource-redirect.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..37274fc2dd8dd16574e7de19234ba5fdf55e06ed --- /dev/null +++ b/alfa-client/libs/resource-redirect/src/lib/resource-redirect/resource-redirect.component.ts @@ -0,0 +1,26 @@ +import { ResourceRedirectService } from '@alfa-client/resource-redirect-shared'; +import { Component, OnInit } from '@angular/core'; +import { ActivatedRoute, ParamMap } from '@angular/router'; +import { ResourceUri } from '@ngxp/rest'; + +@Component({ + standalone: true, + template: '', +}) +export class ResourceRedirectComponent implements OnInit { + private readonly PARAM_URI: string = 'uri'; + + constructor( + private route: ActivatedRoute, + private resourceRedirectService: ResourceRedirectService, + ) {} + + ngOnInit(): void { + this.route.queryParamMap.subscribe((queryParams: ParamMap) => { + const resourceUri: ResourceUri = queryParams.get(this.PARAM_URI); + if (resourceUri) { + this.resourceRedirectService.redirectToResource(resourceUri); + } + }); + } +} diff --git a/alfa-client/libs/resource-redirect/src/test-setup.ts b/alfa-client/libs/resource-redirect/src/test-setup.ts new file mode 100644 index 0000000000000000000000000000000000000000..f7fad5e59f9d4e62a097bbea63040551de396175 --- /dev/null +++ b/alfa-client/libs/resource-redirect/src/test-setup.ts @@ -0,0 +1,11 @@ +import 'jest-preset-angular/setup-jest'; + +import { getTestBed } from '@angular/core/testing'; +import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; + +getTestBed().resetTestEnvironment(); +getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting(), { + teardown: { destroyAfterEach: false }, + errorOnUnknownProperties: true, + errorOnUnknownElements: true, +}); diff --git a/alfa-client/libs/resource-redirect/tsconfig.json b/alfa-client/libs/resource-redirect/tsconfig.json new file mode 100644 index 0000000000000000000000000000000000000000..7cc6baf2f58ed5ccfba098131996f579979e9f18 --- /dev/null +++ b/alfa-client/libs/resource-redirect/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "../../tsconfig.base.json", + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ], + "compilerOptions": { + "target": "es2022" + } +} diff --git a/alfa-client/libs/resource-redirect/tsconfig.lib.json b/alfa-client/libs/resource-redirect/tsconfig.lib.json new file mode 100644 index 0000000000000000000000000000000000000000..1cc2d08ae0a5326f0b4f68921f77e265c7b9f2f4 --- /dev/null +++ b/alfa-client/libs/resource-redirect/tsconfig.lib.json @@ -0,0 +1,14 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "target": "es2015", + "declaration": true, + "declarationMap": true, + "inlineSources": true, + "types": [], + "lib": ["dom", "es2018"] + }, + "exclude": ["src/test-setup.ts", "**/*.spec.ts", "jest.config.ts"], + "include": ["**/*.ts"] +} diff --git a/alfa-client/libs/resource-redirect/tsconfig.spec.json b/alfa-client/libs/resource-redirect/tsconfig.spec.json new file mode 100644 index 0000000000000000000000000000000000000000..3a690070a7f5e48080dd36522d6a0db384d940aa --- /dev/null +++ b/alfa-client/libs/resource-redirect/tsconfig.spec.json @@ -0,0 +1,12 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "module": "commonjs", + "types": ["jest", "node"], + "target": "ES2022", + "useDefineForClassFields": false + }, + "files": ["src/test-setup.ts"], + "include": ["**/*.spec.ts", "**/*.d.ts", "jest.config.ts"] +} diff --git a/alfa-client/libs/ui/src/lib/ui/messages.ts b/alfa-client/libs/ui/src/lib/ui/messages.ts index 49891db3e6afb98e20be5f33e612a2054bb2f41b..9e9757cace91a29221e4b8cf5b4b6a90fb532d12 100644 --- a/alfa-client/libs/ui/src/lib/ui/messages.ts +++ b/alfa-client/libs/ui/src/lib/ui/messages.ts @@ -25,4 +25,5 @@ export enum Messages { HTTP_STATUS_FORBIDDEN = 'Die Aktion konnte wegen fehlender Berechtigungen nicht durchgeführt werden.', HTTP_USER_MANAGER_SERVICE_UNAVAILABLE = 'Der UserManager ist zurzeit leider nicht verfügbar.', HTTP_STATUS_VORGANG_NOT_FOUND = 'Der aufgerufene Vorgang wurde nicht gefunden.', + HTTP_STATUS_RESOURCE_NOT_FOUND = 'Die aufgerufene Ressource wurde nicht gefunden.', } diff --git a/alfa-client/tsconfig.base.json b/alfa-client/tsconfig.base.json index 718027c42393e9c9066bb1b65c2287c66654a873..040e181ce114beeca3c6842d2454527342586f25 100644 --- a/alfa-client/tsconfig.base.json +++ b/alfa-client/tsconfig.base.json @@ -55,6 +55,8 @@ "@alfa-client/vorgang-shared-ui": ["libs/vorgang-shared-ui/src/index.ts"], "@alfa-client/wiedervorlage": ["libs/wiedervorlage/src/index.ts"], "@alfa-client/wiedervorlage-shared": ["libs/wiedervorlage-shared/src/index.ts"], + "@alfa-client/resource-redirect": ["libs/resource-redirect/src/index.ts"], + "@alfa-client/resource-redirect-shared": ["libs/resource-redirect-shared/src/index.ts"], "@ods/component": ["libs/design-component/src/index.ts"], "@ods/system": ["libs/design-system/src/index.ts"], "authentication": ["libs/authentication/src/index.ts"]