diff --git a/alfa-client/libs/admin-settings/src/lib/admin-settings.linkrel.ts b/alfa-client/libs/admin-settings/src/lib/admin-settings.linkrel.ts new file mode 100644 index 0000000000000000000000000000000000000000..130035dfaa6cff20ff548bd8ffd42e0a683b8e92 --- /dev/null +++ b/alfa-client/libs/admin-settings/src/lib/admin-settings.linkrel.ts @@ -0,0 +1,4 @@ +export enum SettingListLinkRel { + CREATE = '', + LIST = 'settings', +} diff --git a/alfa-client/libs/admin-settings/src/lib/admin-settings.model.ts b/alfa-client/libs/admin-settings/src/lib/admin-settings.model.ts index 2bebbfe2ac0750eaa1e775bf7c9310a15fd93da6..62c9c53122c054f37214750f876b523092510e84 100644 --- a/alfa-client/libs/admin-settings/src/lib/admin-settings.model.ts +++ b/alfa-client/libs/admin-settings/src/lib/admin-settings.model.ts @@ -24,22 +24,17 @@ import { ListResource } from '@alfa-client/tech-shared'; import { Resource } from '@ngxp/rest'; -export interface SettingsListResource extends ListResource { - _embedded: { settings: SettingsItemResource[] }; +export interface SettingListResource extends ListResource { + _embedded: { settings: SettingItemResource[] }; } -export enum SettingsName { +export enum SettingName { POSTFACH = 'Postfach', } -export interface SettingsItem { - name: SettingsName; +export interface SettingItem { + name: SettingName; settingsBody: unknown; } -export declare type SettingsItemResource = Resource & SettingsItem; - -export enum SettingsListLinkRel { - CREATE = '', - LIST = 'settings', -} +export declare type SettingItemResource = Resource & SettingItem; diff --git a/alfa-client/libs/admin-settings/src/lib/admin-settings.service.spec.ts b/alfa-client/libs/admin-settings/src/lib/admin-settings.service.spec.ts index f12f0a59053b915d8ef399c732707a97eb395b3a..e9e03a8b1e658319e5d9a5c649bf53959b17c94b 100644 --- a/alfa-client/libs/admin-settings/src/lib/admin-settings.service.spec.ts +++ b/alfa-client/libs/admin-settings/src/lib/admin-settings.service.spec.ts @@ -1,7 +1,7 @@ import { Mock, mock, useFromMock } from '@alfa-client/test-utils'; import { ResourceRepository } from 'libs/tech-shared/src/lib/resource/resource.repository'; import { SettingsService } from './admin-settings.service'; -import { SettingsListLinkRel, SettingsListResource } from './admin-settings.model'; +import { SettingListResource } from './admin-settings.model'; import { createEmptyStateResource, createStateResource, @@ -10,12 +10,13 @@ import { import { Observable, of } from 'rxjs'; import { ListResourceServiceConfig } from 'libs/tech-shared/src/lib/resource/resource.model'; import { PostfachResource } from './postfach/postfach.model'; -import { createEmptyResource, createPostfachResource } from '../../test/postfach/postfach'; +import { createSettingItemResource, createPostfachResource } from '../../test/postfach/postfach'; import { createSettingsListResource } from '../../test/admin-settings'; import { ConfigurationService } from './configuration/configuration.service'; import { createConfigurationResource } from '../../test/configuration/configuration'; import { ConfigurationResource } from './configuration/configuration.model'; import { singleCold } from '../../../tech-shared/src/lib/resource/marbles'; +import { SettingListLinkRel } from './admin-settings.linkrel'; describe('SettingsService', () => { let service: SettingsService; @@ -53,13 +54,13 @@ describe('SettingsService', () => { it('should have createLinkRel', () => { const config: ListResourceServiceConfig<ConfigurationResource> = service.buildConfig(); - expect(config.createLinkRel).toBe(SettingsListLinkRel.CREATE); + expect(config.createLinkRel).toBe(SettingListLinkRel.CREATE); }); it('should have istLinKRel', () => { const config: ListResourceServiceConfig<ConfigurationResource> = service.buildConfig(); - expect(config.listLinkRel).toBe(SettingsListLinkRel.LIST); + expect(config.listLinkRel).toBe(SettingListLinkRel.LIST); }); }); @@ -67,7 +68,7 @@ describe('SettingsService', () => { const postfachResource = createPostfachResource(); const postfachStateResource: StateResource<PostfachResource> = createStateResource(postfachResource); - const settingsListResource: StateResource<SettingsListResource> = createStateResource( + const settingsListResource: StateResource<SettingListResource> = createStateResource( createSettingsListResource([postfachResource]), ); @@ -81,23 +82,23 @@ describe('SettingsService', () => { expect(service.resourceService.getList).toHaveBeenCalled(); }); - it('should return null for missing postfach resource', () => { - const emptySettingsListResource = createStateResource( - createSettingsListResource([createEmptyResource()]), + it('should return null for non postfach resource', () => { + const emptySettingsListResource: StateResource<SettingListResource> = createStateResource( + createSettingsListResource([createSettingItemResource()]), ); service.resourceService.getList = jest .fn() .mockReturnValue(singleCold(emptySettingsListResource)); - const postfach = service.getPostfach(); + const postfach: Observable<StateResource<PostfachResource>> = service.getPostfach(); expect(postfach).toBeObservable(singleCold(createEmptyStateResource())); }); it('should return item resource as postfach resource', () => { service.resourceService.getList = jest.fn().mockReturnValue(singleCold(settingsListResource)); - const postfach = service.getPostfach(); + const postfach: Observable<StateResource<PostfachResource>> = service.getPostfach(); expect(postfach).toBeObservable(singleCold(postfachStateResource)); }); diff --git a/alfa-client/libs/admin-settings/src/lib/admin-settings.service.ts b/alfa-client/libs/admin-settings/src/lib/admin-settings.service.ts index d8ef581bded3f1552e0fbb16f8491650c25af234..d87daa538d4abdba448b4ea696507255adab2b15 100644 --- a/alfa-client/libs/admin-settings/src/lib/admin-settings.service.ts +++ b/alfa-client/libs/admin-settings/src/lib/admin-settings.service.ts @@ -1,10 +1,6 @@ import { Injectable } from '@angular/core'; import { ResourceListService } from 'libs/tech-shared/src/lib/resource/list-resource.service'; -import { - SettingsItemResource, - SettingsListLinkRel, - SettingsListResource, -} from './admin-settings.model'; +import { SettingItemResource, SettingListResource } from './admin-settings.model'; import { ResourceRepository } from 'libs/tech-shared/src/lib/resource/resource.repository'; import { ListResourceServiceConfig } from 'libs/tech-shared/src/lib/resource/resource.model'; import { map, Observable } from 'rxjs'; @@ -13,13 +9,14 @@ import { PostfachResource } from './postfach/postfach.model'; import { ConfigurationService } from './configuration/configuration.service'; import { ConfigurationResource } from './configuration/configuration.model'; import { getPostfachResource } from './admin-settings.util'; +import { SettingListLinkRel } from './admin-settings.linkrel'; @Injectable() export class SettingsService { resourceService: ResourceListService< ConfigurationResource, - SettingsListResource, - SettingsItemResource + SettingListResource, + SettingItemResource >; constructor( @@ -32,8 +29,8 @@ export class SettingsService { buildConfig(): ListResourceServiceConfig<ConfigurationResource> { return { baseResource: this.configurationService.getConfigurationResource(), - createLinkRel: SettingsListLinkRel.CREATE, - listLinkRel: SettingsListLinkRel.LIST, + createLinkRel: SettingListLinkRel.CREATE, + listLinkRel: SettingListLinkRel.LIST, }; } diff --git a/alfa-client/libs/admin-settings/src/lib/admin-settings.util.spec.ts b/alfa-client/libs/admin-settings/src/lib/admin-settings.util.spec.ts index fdcbe1bb8afd91e1e653802eff2d48565cb42490..0139638d32fda4d6d34a72836e29574c4b3f1a21 100644 --- a/alfa-client/libs/admin-settings/src/lib/admin-settings.util.spec.ts +++ b/alfa-client/libs/admin-settings/src/lib/admin-settings.util.spec.ts @@ -5,14 +5,14 @@ import { createStateResource, StateResource, } from '@alfa-client/tech-shared'; -import { SettingsListResource } from './admin-settings.model'; +import { SettingListResource } from './admin-settings.model'; import { createFilledSettingsListResource } from '../../test/admin-settings'; import { getPostfachResource } from './admin-settings.util'; describe('get postfach resource', () => { it('should return state resource with postfach resource if exists', () => { const postfachResource: PostfachResource = createPostfachResource(); - const settingsListResource: StateResource<SettingsListResource> = createStateResource( + const settingsListResource: StateResource<SettingListResource> = createStateResource( createFilledSettingsListResource([postfachResource]), ); @@ -23,7 +23,7 @@ describe('get postfach resource', () => { }); it('should return empty state resource if postfach resource NOT exists', () => { - const settingsListResource: StateResource<SettingsListResource> = createStateResource( + const settingsListResource: StateResource<SettingListResource> = createStateResource( createFilledSettingsListResource([]), ); diff --git a/alfa-client/libs/admin-settings/src/lib/admin-settings.util.ts b/alfa-client/libs/admin-settings/src/lib/admin-settings.util.ts index 346207f05e8a07cdf8a3c49ccfe68119a16506c1..dad56bcb51a28d9750d181abe8e4d90909f07bce 100644 --- a/alfa-client/libs/admin-settings/src/lib/admin-settings.util.ts +++ b/alfa-client/libs/admin-settings/src/lib/admin-settings.util.ts @@ -6,21 +6,22 @@ import { StateResource, } from '@alfa-client/tech-shared'; import { PostfachResource } from './postfach/postfach.model'; -import { - SettingsItemResource, - SettingsListLinkRel, - SettingsListResource, SettingsName, -} from './admin-settings.model'; +import { SettingItemResource, SettingListResource, SettingName } from './admin-settings.model'; +import { SettingListLinkRel } from './admin-settings.linkrel'; export function getPostfachResource( - settingsListResource: StateResource<SettingsListResource>, + settingsListResource: StateResource<SettingListResource>, ): StateResource<PostfachResource> { - const entries: SettingsItemResource[] = getEmbeddedResources( + const entries: SettingItemResource[] = getEmbeddedResources( settingsListResource, - SettingsListLinkRel.LIST, + SettingListLinkRel.LIST, ); - const postfachEntry = entries.find((item) => item.name === SettingsName.POSTFACH); - return isNotNil(postfachEntry) ? - createStateResource(postfachEntry as PostfachResource) + const postfachSettingItemResource: SettingItemResource = entries.find(isPostfachSettingItem); + return isNotNil(postfachSettingItemResource) ? + createStateResource(postfachSettingItemResource as PostfachResource) : createEmptyStateResource(); } + +function isPostfachSettingItem(item: SettingItemResource): boolean { + return item.name === SettingName.POSTFACH; +} diff --git a/alfa-client/libs/admin-settings/src/lib/configuration/configuration.linkrel.ts b/alfa-client/libs/admin-settings/src/lib/configuration/configuration.linkrel.ts new file mode 100644 index 0000000000000000000000000000000000000000..51a83ba68f2324dc9018f9c1bb0c90afbdb7706d --- /dev/null +++ b/alfa-client/libs/admin-settings/src/lib/configuration/configuration.linkrel.ts @@ -0,0 +1,3 @@ +export enum ConfigurationLinkRel { + SETTING = 'settings', +} diff --git a/alfa-client/libs/admin-settings/src/lib/error/error.model.ts b/alfa-client/libs/admin-settings/src/lib/error/error.model.ts deleted file mode 100644 index ceb6bfe0f9fd29209e00056e1f4c63b81ed6122d..0000000000000000000000000000000000000000 --- a/alfa-client/libs/admin-settings/src/lib/error/error.model.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { HttpStatusCode } from '@angular/common/http'; -import { ValidationMessageCode } from '../../../../tech-shared/src/lib/validation/tech.validation.messages'; - -export interface ProblemDetail { - type?: string; - title: string; - status: HttpStatusCode; - detail: string; - instance: string; -} - -export interface InvalidParamsItem { - name: string; - reason: ValidationMessageCode; -} - -export interface ValidationProblemDetails extends ProblemDetail { - ['invalid-params']: InvalidParamsItem[]; -} diff --git a/alfa-client/libs/admin-settings/src/lib/postfach/postfach-container/postfach-form/postfach-form.component.spec.ts b/alfa-client/libs/admin-settings/src/lib/postfach/postfach-container/postfach-form/postfach-form.component.spec.ts index ffdf0df6598ef1766eabfceae635b9cc5b0e752a..230d2d32c60347cdc660c26797894526a31f0cfc 100644 --- a/alfa-client/libs/admin-settings/src/lib/postfach/postfach-container/postfach-form/postfach-form.component.spec.ts +++ b/alfa-client/libs/admin-settings/src/lib/postfach/postfach-container/postfach-form/postfach-form.component.spec.ts @@ -15,18 +15,20 @@ import { PostfachFormService } from './postfach.formservice'; import { MockComponent, ngMocks } from 'ng-mocks'; import { TextFieldComponent } from '../../../shared/text-field/text-field.component'; import { createPostfachResource } from '../../../../../test/postfach/postfach'; -import { createStateResource } from '@alfa-client/tech-shared'; +import { ProblemDetail, createStateResource } from '@alfa-client/tech-shared'; import { PostfachService } from '../../postfach.service'; -import { createValidationProblemDetails } from '../../../../../../tech-shared/test/error'; import { PostfachResource } from '../../postfach.model'; import { EMPTY } from 'rxjs'; import { singleCold } from '../../../../../../tech-shared/src/lib/resource/marbles'; +import { createInvalidParam, createProblemDetail } from '../../../../../../tech-shared/test/error'; describe('PostfachFormComponent', () => { let component: PostfachFormComponent; let fixture: ComponentFixture<PostfachFormComponent>; - const postfachService = mock(PostfachService); let formService: PostfachFormService; + + const postfachService = mock(PostfachService); + const saveButton: string = getDataTestIdOf('save-button'); const invalidMessageSpan: string = getDataTestIdOf('invalid-empty-message-span'); @@ -68,9 +70,10 @@ describe('PostfachFormComponent', () => { describe('updatePostfachResource', () => { it('should patch form with input postfach resource', () => { const patchFn = jest.spyOn(formService, 'patch'); + const postfachResource: PostfachResource = createPostfachResource(); - const postfachResource = createPostfachResource(); component.updatePostfachResource(postfachResource); + expect(patchFn).toHaveBeenCalledWith(postfachResource.settingsBody); }); }); @@ -151,13 +154,20 @@ describe('PostfachFormComponent', () => { describe('invalid message', () => { it('should show if form invalidEmpty', () => { - formService.setValidationDetails(createValidationProblemDetails()); + formService.setErrorByProblemDetail(createProblemDetailForAbsenderName()); fixture.detectChanges(); existsAsHtmlElement(fixture, invalidMessageSpan); }); + function createProblemDetailForAbsenderName(): ProblemDetail { + return { + ...createProblemDetail(), + 'invalid-params': [{ ...createInvalidParam(), name: 'settingsBody.absender.name' }], + }; + } + it('should not show if form valid', () => { expect(formService.form.invalid).toBeFalsy(); diff --git a/alfa-client/libs/admin-settings/src/lib/postfach/postfach-container/postfach-form/postfach.formservice.ts b/alfa-client/libs/admin-settings/src/lib/postfach/postfach-container/postfach-form/postfach.formservice.ts index 8689502606a1f1ce98cf0376ef47c9e1e917e213..fb53c0f7bb29b9069a78bf6e3df85f3b5b552b47 100644 --- a/alfa-client/libs/admin-settings/src/lib/postfach/postfach-container/postfach-form/postfach.formservice.ts +++ b/alfa-client/libs/admin-settings/src/lib/postfach/postfach-container/postfach-form/postfach.formservice.ts @@ -1,4 +1,4 @@ -import { AbstractFormService, StateResource } from '@alfa-client/tech-shared'; +import { AbstractFormService, HttpError, StateResource } from '@alfa-client/tech-shared'; import { Injectable } from '@angular/core'; import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; import { Observable } from 'rxjs'; @@ -8,12 +8,13 @@ import { isNil } from 'lodash-es'; @Injectable() export class PostfachFormService extends AbstractFormService { - public static readonly NAME_FIELD: string = 'name'; public static readonly ASBSENDER_GROUP: string = 'absender'; + public static readonly NAME_FIELD: string = 'name'; public static readonly ANSCHRIFT_FIELD: string = 'anschrift'; public static readonly DIENST_FIELD: string = 'dienst'; public static readonly MANDANT_FIELD: string = 'mandant'; public static readonly GEMEINDESCHLUESSEL_FIELD: string = 'gemeindeschluessel'; + public static readonly SIGNATUR_FIELD: string = 'signatur'; constructor( @@ -36,7 +37,7 @@ export class PostfachFormService extends AbstractFormService { }); } - protected doSubmit(): Observable<StateResource<PostfachResource>> { + protected doSubmit(): Observable<StateResource<PostfachResource | HttpError>> { const value: Postfach = this.getFormValue(); if (this.shouldSkipAbsender(value)) { delete value.absender; @@ -54,4 +55,8 @@ export class PostfachFormService extends AbstractFormService { protected getPathPrefix(): string { return 'settingsBody'; } + + public get invalidEmpty(): boolean { + return this.form.invalid; + } } diff --git a/alfa-client/libs/admin-settings/src/lib/postfach/postfach-navigation-item/postfach-navigation-item.component.spec.ts b/alfa-client/libs/admin-settings/src/lib/postfach/postfach-navigation-item/postfach-navigation-item.component.spec.ts index cf289bf454c6ea902e7bfb239d1b66103d3a9f6d..40288da836e1ed2a1fae575c4bcf3932955a7ce8 100644 --- a/alfa-client/libs/admin-settings/src/lib/postfach/postfach-navigation-item/postfach-navigation-item.component.spec.ts +++ b/alfa-client/libs/admin-settings/src/lib/postfach/postfach-navigation-item/postfach-navigation-item.component.spec.ts @@ -3,7 +3,7 @@ import { PostfachNavigationItemComponent } from './postfach-navigation-item.comp import { MockComponent } from 'ng-mocks'; import { NavigationItemComponent } from '@admin-client/admin-settings'; import { getMockComponent } from '@alfa-client/test-utils'; -import { SettingsName } from '../../admin-settings.model'; +import { SettingName } from '../../admin-settings.model'; describe('PostfachNavigationItemComponent', () => { let component: PostfachNavigationItemComponent; @@ -31,7 +31,7 @@ describe('PostfachNavigationItemComponent', () => { }); it('should be called with name', () => { - expect(navigationItemComponent.name).toBe(SettingsName.POSTFACH); + expect(navigationItemComponent.name).toBe(SettingName.POSTFACH); }); it('should be called with imageSrc', () => { diff --git a/alfa-client/libs/admin-settings/src/lib/postfach/postfach.linkrel.ts b/alfa-client/libs/admin-settings/src/lib/postfach/postfach.linkrel.ts new file mode 100644 index 0000000000000000000000000000000000000000..a6de0555d25af8ddaccf02840421988b5a8b05ff --- /dev/null +++ b/alfa-client/libs/admin-settings/src/lib/postfach/postfach.linkrel.ts @@ -0,0 +1,3 @@ +export enum PostfachLinkRel { + SELF = 'self', +} diff --git a/alfa-client/libs/admin-settings/src/lib/postfach/postfach.model.ts b/alfa-client/libs/admin-settings/src/lib/postfach/postfach.model.ts index 903e674761a22bc936f911060bcac6b77a748673..444e1b648851c1c452c86db59ec8591e539feb1a 100644 --- a/alfa-client/libs/admin-settings/src/lib/postfach/postfach.model.ts +++ b/alfa-client/libs/admin-settings/src/lib/postfach/postfach.model.ts @@ -23,7 +23,7 @@ */ import { Resource } from '@ngxp/rest'; -import { SettingsName } from '../admin-settings.model'; +import { SettingName } from '../admin-settings.model'; export interface Absender { name: string; @@ -39,11 +39,8 @@ export interface Postfach { } export declare type PostfachSettingsItem = { - name: SettingsName.POSTFACH; + name: SettingName.POSTFACH; settingsBody: Postfach; }; -export declare type PostfachResource = Resource & PostfachSettingsItem; -export enum PostfachLinkRel { - SELF = 'self', -} +export declare type PostfachResource = Resource & PostfachSettingsItem; diff --git a/alfa-client/libs/admin-settings/src/lib/postfach/postfach.service.spec.ts b/alfa-client/libs/admin-settings/src/lib/postfach/postfach.service.spec.ts index 3562e2e5f9291a739de28a31d0d56ffc24bf0121..ba062566e85c100a6762d2441ea86f28b6b7bcfb 100644 --- a/alfa-client/libs/admin-settings/src/lib/postfach/postfach.service.spec.ts +++ b/alfa-client/libs/admin-settings/src/lib/postfach/postfach.service.spec.ts @@ -1,16 +1,14 @@ import { Mock, mock, useFromMock } from '@alfa-client/test-utils'; -import { PostfachLinkRel, PostfachResource } from './postfach.model'; +import { PostfachResource } from './postfach.model'; import { ResourceRepository } from 'libs/tech-shared/src/lib/resource/resource.repository'; import { PostfachService } from './postfach.service'; import { SettingsService } from '../admin-settings.service'; -import { - createPostfachResource, - createPostfachSettingsItem, -} from '../../../test/postfach/postfach'; -import { createStateResource, StateResource } from '@alfa-client/tech-shared'; +import { createPostfachResource, createPostfachSettingItem } from '../../../test/postfach/postfach'; +import { createStateResource, HttpError, StateResource } from '@alfa-client/tech-shared'; import { Observable, of } from 'rxjs'; import { ResourceServiceConfig } from 'libs/tech-shared/src/lib/resource/resource.model'; import { singleCold, singleHot } from 'libs/tech-shared/src/lib/resource/marbles'; +import { PostfachLinkRel } from './postfach.linkrel'; describe('PostfachService', () => { let service: PostfachService; @@ -64,7 +62,7 @@ describe('PostfachService', () => { }); describe('save', () => { - const postfachSettingsItem = createPostfachSettingsItem(); + const postfachSettingsItem = createPostfachSettingItem(); it('should call resourceService', () => { service.resourceService.save = jest.fn(); @@ -80,7 +78,7 @@ describe('PostfachService', () => { createStateResource(postfachResource); service.resourceService.save = jest.fn().mockReturnValue(singleCold(postfachStateResource)); - const savedPostfach: Observable<StateResource<PostfachResource>> = service.save( + const savedPostfach: Observable<StateResource<PostfachResource | HttpError>> = service.save( postfachResource.settingsBody, ); diff --git a/alfa-client/libs/admin-settings/src/lib/postfach/postfach.service.ts b/alfa-client/libs/admin-settings/src/lib/postfach/postfach.service.ts index 99e30a4c5316f8be1163331cd7a01022b50be90f..0ec3360d14d706cb8507103f84325029fa9d2f85 100644 --- a/alfa-client/libs/admin-settings/src/lib/postfach/postfach.service.ts +++ b/alfa-client/libs/admin-settings/src/lib/postfach/postfach.service.ts @@ -1,17 +1,13 @@ import { Injectable } from '@angular/core'; import { ResourceRepository } from 'libs/tech-shared/src/lib/resource/resource.repository'; -import { - Postfach, - PostfachLinkRel, - PostfachResource, - PostfachSettingsItem, -} from './postfach.model'; +import { Postfach, PostfachResource, PostfachSettingsItem } from './postfach.model'; import { ResourceService } from 'libs/tech-shared/src/lib/resource/resource.service'; import { SettingsService } from '../admin-settings.service'; import { ResourceServiceConfig } from 'libs/tech-shared/src/lib/resource/resource.model'; import { Observable } from 'rxjs'; -import { StateResource } from '@alfa-client/tech-shared'; -import { SettingsName } from '../admin-settings.model'; +import { HttpError, StateResource } from '@alfa-client/tech-shared'; +import { SettingName } from '../admin-settings.model'; +import { PostfachLinkRel } from './postfach.linkrel'; @Injectable() export class PostfachService { @@ -33,10 +29,14 @@ export class PostfachService { }; } - public save(postfach: Postfach): Observable<StateResource<PostfachResource>> { - return this.resourceService.save({ - name: SettingsName.POSTFACH, + public save(postfach: Postfach): Observable<StateResource<PostfachResource | HttpError>> { + return this.resourceService.save(this.buildPostfachSettingItem(postfach)); + } + + private buildPostfachSettingItem(postfach: Postfach): PostfachSettingsItem { + return { + name: SettingName.POSTFACH, settingsBody: postfach, - } as PostfachSettingsItem); + }; } } diff --git a/alfa-client/libs/admin-settings/test/admin-settings.ts b/alfa-client/libs/admin-settings/test/admin-settings.ts index 7a65458eec9be2a797592bb3efc3362d473f0671..cddc0ef72f75df27b32b26de83416c5de4a03cd9 100644 --- a/alfa-client/libs/admin-settings/test/admin-settings.ts +++ b/alfa-client/libs/admin-settings/test/admin-settings.ts @@ -1,14 +1,11 @@ import { Resource } from '@ngxp/rest'; -import { - SettingsItemResource, - SettingsListLinkRel, - SettingsListResource, -} from '../src/lib/admin-settings.model'; +import { SettingItemResource, SettingListResource } from '../src/lib/admin-settings.model'; import { toResource } from '../../tech-shared/test/resource'; +import { SettingListLinkRel } from '../src/lib/admin-settings.linkrel'; export function createSettingsListResource( - settingsItems: SettingsItemResource[], -): SettingsListResource { + settingsItems: SettingItemResource[], +): SettingListResource { return toResource({}, [], { settings: settingsItems, }); @@ -17,8 +14,8 @@ export function createSettingsListResource( export function createFilledSettingsListResource( resources: Resource[], linkRelations: string[] = [], -): SettingsListResource { +): SettingListResource { return toResource({}, [...linkRelations], { - [SettingsListLinkRel.LIST]: resources, + [SettingListLinkRel.LIST]: resources, }); } diff --git a/alfa-client/libs/admin-settings/test/configuration/configuration.ts b/alfa-client/libs/admin-settings/test/configuration/configuration.ts index 402776edb31b2f2adade70c45fca9246d0ecd1c7..b634c257c5acd62e073cae78f7893361dd00e48e 100644 --- a/alfa-client/libs/admin-settings/test/configuration/configuration.ts +++ b/alfa-client/libs/admin-settings/test/configuration/configuration.ts @@ -1,6 +1,7 @@ import { ConfigurationResource } from '../../src/lib/configuration/configuration.model'; import { toResource } from '../../../tech-shared/test/resource'; +import { ConfigurationLinkRel } from '../../src/lib/configuration/configuration.linkrel'; export function createConfigurationResource(): ConfigurationResource { - return toResource({}, ['settings']); + return toResource({}, [ConfigurationLinkRel.SETTING]); } diff --git a/alfa-client/libs/admin-settings/test/postfach/postfach.ts b/alfa-client/libs/admin-settings/test/postfach/postfach.ts index f0606aa841aac40775cf7732f7dbca38adfab00c..a908cc8ed7622a56d471ba36b23c66ccf0a110bc 100644 --- a/alfa-client/libs/admin-settings/test/postfach/postfach.ts +++ b/alfa-client/libs/admin-settings/test/postfach/postfach.ts @@ -4,7 +4,7 @@ import { PostfachSettingsItem, } from '../../src/lib/postfach/postfach.model'; import { toResource } from '../../../tech-shared/test/resource'; -import { SettingsItemResource, SettingsName } from '../../src/lib/admin-settings.model'; +import { SettingItemResource, SettingName } from '../../src/lib/admin-settings.model'; import faker from '@faker-js/faker'; export function createPostfach(): Postfach { @@ -20,20 +20,20 @@ export function createPostfach(): Postfach { }; } -export function createPostfachSettingsItem(): PostfachSettingsItem { +export function createPostfachSettingItem(): PostfachSettingsItem { return { - name: SettingsName.POSTFACH, + name: SettingName.POSTFACH, settingsBody: createPostfach(), }; } export function createPostfachResource(): PostfachResource { - return toResource(createPostfachSettingsItem()); + return toResource(createPostfachSettingItem()); } -export function createEmptyResource(): SettingsItemResource { +export function createSettingItemResource(): SettingItemResource { return toResource({ - name: 'Empty', + name: faker.random.word(), settingsBody: {}, }); } diff --git a/alfa-client/libs/tech-shared/src/lib/resource/resource.service.spec.ts b/alfa-client/libs/tech-shared/src/lib/resource/resource.service.spec.ts index 19cb744763ee59e4a28558ccbd0c37880c74f74f..d47f41bfc31130a4816df58a63e2161f06cf7cd1 100644 --- a/alfa-client/libs/tech-shared/src/lib/resource/resource.service.spec.ts +++ b/alfa-client/libs/tech-shared/src/lib/resource/resource.service.spec.ts @@ -1,14 +1,22 @@ import { Mock, mock, useFromMock } from '@alfa-client/test-utils'; -import { Observable, of } from 'rxjs'; +import { Observable, lastValueFrom, of, throwError } from 'rxjs'; import { ResourceService } from './resource.service'; import { ResourceRepository } from './resource.repository'; import { LinkRelationName, ResourceServiceConfig, SaveResourceData } from './resource.model'; import { Resource, ResourceUri, getUrl } from '@ngxp/rest'; import { createDummyResource } from 'libs/tech-shared/test/resource'; -import { StateResource, createEmptyStateResource, createStateResource } from './resource.util'; +import { + StateResource, + createEmptyStateResource, + createErrorStateResource, + createStateResource, +} from './resource.util'; import { fakeAsync, tick } from '@angular/core/testing'; import { singleCold, singleHot } from './marbles'; import faker from '@faker-js/faker'; +import { HttpErrorResponse } from '@angular/common/http'; +import { createProblemDetail } from 'libs/tech-shared/test/error'; +import { HttpError, ProblemDetail } from '../tech.model'; describe('ResourceService', () => { let service: ResourceService<Resource, Resource>; @@ -126,10 +134,46 @@ describe('ResourceService', () => { service.resource.next(createStateResource(createDummyResource([config.editLinkRel]))); repository.save.mockReturnValue(singleHot(loadedResource)); - const saved: Observable<StateResource<Resource>> = service.save(dummyToSave); + const saved: Observable<StateResource<Resource | HttpError>> = service.save(dummyToSave); expect(saved).toBeObservable(singleCold(createStateResource(loadedResource))); }); + + it('should call handleError', () => { + service.resource.next(createStateResource(createDummyResource([config.editLinkRel]))); + const errorResponse: ProblemDetail = createProblemDetail(); + repository.save.mockReturnValue(throwError(() => errorResponse)); + service.handleError = jest.fn(); + + service.save(<any>{}).subscribe(); + + expect(service.handleError).toHaveBeenCalledWith(errorResponse); + }); + }); + + describe('handleError', () => { + it('should return error stateresource on problem unprocessable entity', (done) => { + const error: ProblemDetail = createProblemDetail(); + + service + .handleError(<HttpErrorResponse>(<any>error)) + .subscribe((responseError: StateResource<HttpError>) => { + expect(responseError).toEqual(createErrorStateResource(error)); + done(); + }); + }); + + it('should rethrow error', () => { + const error: HttpErrorResponse = <HttpErrorResponse>{ + status: 500, + statusText: 'Internal Server Error', + }; + + const thrownError$: Observable<StateResource<HttpError>> = service.handleError(error); + + expect.assertions(1); + expect(lastValueFrom(thrownError$)).rejects.toThrowError('Internal Server Error'); + }); }); describe('refresh', () => { diff --git a/alfa-client/libs/tech-shared/src/lib/resource/resource.service.ts b/alfa-client/libs/tech-shared/src/lib/resource/resource.service.ts index 61710fca3a077edab052532970b3cd4c0927a6b1..d2af94a8d3cdc21b6d7f45c212f517b1c1d829c0 100644 --- a/alfa-client/libs/tech-shared/src/lib/resource/resource.service.ts +++ b/alfa-client/libs/tech-shared/src/lib/resource/resource.service.ts @@ -1,10 +1,27 @@ -import { BehaviorSubject, Observable, first, map, mergeMap, of } from 'rxjs'; +import { + BehaviorSubject, + Observable, + catchError, + first, + map, + mergeMap, + of, + throwError, +} from 'rxjs'; import { ResourceServiceConfig } from './resource.model'; -import { StateResource, createEmptyStateResource, createStateResource } from './resource.util'; +import { + StateResource, + createEmptyStateResource, + createErrorStateResource, + createStateResource, +} from './resource.util'; import { Resource, ResourceUri, getUrl, hasLink } from '@ngxp/rest'; import { ResourceRepository } from './resource.repository'; import { isNull } from 'lodash-es'; import { isNotNull } from '../tech.util'; +import { HttpErrorResponse } from '@angular/common/http'; +import { isUnprocessableEntity } from '../http.util'; +import { HttpError, ProblemDetail } from '../tech.model'; /** * B = Type of baseresource @@ -42,13 +59,14 @@ export class ResourceService<B extends Resource, T extends Resource> { this.throwErrorOn(!hasLink(baseResource, this.config.getLinkRel), 'No get link exists.'); } - public save(toSave: unknown): Observable<StateResource<T>> { + public save(toSave: unknown): Observable<StateResource<T | HttpError>> { this.verifyEditLinkRel(); return this.resource.asObservable().pipe( mergeMap((selectedResource: StateResource<T>) => this.doSave(selectedResource.resource, toSave), ), map((loadedResource: T) => createStateResource(loadedResource)), + catchError((errorResponse: HttpErrorResponse) => this.handleError(errorResponse)), ); } @@ -68,6 +86,13 @@ export class ResourceService<B extends Resource, T extends Resource> { }); } + handleError(errorResponse: HttpErrorResponse): Observable<StateResource<HttpError>> { + if (isUnprocessableEntity(errorResponse.status)) { + return of(createErrorStateResource((<any>errorResponse) as ProblemDetail)); + } + return throwError(() => errorResponse); + } + public canEdit(): boolean { return this.hasLinkRel(this.config.editLinkRel); } diff --git a/alfa-client/libs/tech-shared/src/lib/resource/resource.util.ts b/alfa-client/libs/tech-shared/src/lib/resource/resource.util.ts index c895e8aa0d565474280e6edc770a6508890affbc..91674262941954dd3c0a0b09976ecd7039fbc3f2 100644 --- a/alfa-client/libs/tech-shared/src/lib/resource/resource.util.ts +++ b/alfa-client/libs/tech-shared/src/lib/resource/resource.util.ts @@ -24,7 +24,7 @@ import { encodeUrlForEmbedding, isNotNull } from '@alfa-client/tech-shared'; import { getEmbeddedResource, getUrl, Resource, ResourceUri } from '@ngxp/rest'; import { isEqual, isNil } from 'lodash-es'; -import { ApiError } from '../tech.model'; +import { HttpError } from '../tech.model'; export interface ListResource extends Resource { _embedded; @@ -35,7 +35,7 @@ export interface StateResource<T> { reload: boolean; loading: boolean; loaded: boolean; - error?: ApiError; + error?: HttpError; } export function createEmptyStateResource<T>(loading: boolean = false): StateResource<T> { @@ -46,7 +46,7 @@ export function createStateResource<T>(resource: T, loading: boolean = false): S return { loading, loaded: !isNil(resource), reload: false, resource }; } -export function createErrorStateResource<T>(error: ApiError): StateResource<any> { +export function createErrorStateResource<T>(error: HttpError): StateResource<any> { return { ...createEmptyStateResource<T>(), error, loaded: true }; } diff --git a/alfa-client/libs/tech-shared/src/lib/service/formservice.abstract.spec.ts b/alfa-client/libs/tech-shared/src/lib/service/formservice.abstract.spec.ts index a2dfe77ec2e19cc4be0cf11923c9d075d7932beb..384a9bfc4845cdce367acc86791b4139bbb33f17 100644 --- a/alfa-client/libs/tech-shared/src/lib/service/formservice.abstract.spec.ts +++ b/alfa-client/libs/tech-shared/src/lib/service/formservice.abstract.spec.ts @@ -21,26 +21,36 @@ * Die sprachspezifischen Genehmigungen und Beschränkungen * unter der Lizenz sind dem Lizenztext zu entnehmen. */ -import { FormControl, UntypedFormBuilder, UntypedFormControl, UntypedFormGroup } from '@angular/forms'; +import { UntypedFormBuilder, UntypedFormControl, UntypedFormGroup } from '@angular/forms'; import { CommandResource } from '@alfa-client/command-shared'; import { ApiError, createEmptyStateResource, createErrorStateResource, - getMessageForInvalidParamsItem, + HttpError, + InvalidParam, + Issue, + ProblemDetail, StateResource, } from '@alfa-client/tech-shared'; import { Resource } from '@ngxp/rest'; -import { createApiError, createValidationProblemDetails } from 'libs/tech-shared/test/error'; -import { Observable, of, throwError } from 'rxjs'; +import { + createApiError, + createInvalidParam, + createIssue, + createProblemDetail, +} from 'libs/tech-shared/test/error'; +import { Observable, of } from 'rxjs'; import { AbstractFormService } from './formservice.abstract'; import { cold } from 'jest-marbles'; +import * as ValidationUtil from '../validation/tech.validation.util'; + describe('AbstractFormService', () => { let formService: AbstractFormService; beforeEach(() => { - formService = new DummyFormService(new UntypedFormBuilder()); + formService = new TestFormService(new UntypedFormBuilder()); }); it('should create', () => { @@ -49,9 +59,11 @@ describe('AbstractFormService', () => { describe('submit', () => { describe('with api error', () => { - const stateResourceWithError = createErrorStateResource(createApiError()); + const stateResourceWithError: StateResource<ApiError> = + createErrorStateResource(createApiError()); + beforeEach(() => { - DummyFormService.SUBMIT_OBSERVABLE = () => of(stateResourceWithError); + TestFormService.SUBMIT_OBSERVABLE = () => of(stateResourceWithError); formService.handleResponse = jest.fn((stateResource) => stateResource); }); it('should call handle response for api error', (done) => { @@ -62,35 +74,12 @@ describe('AbstractFormService', () => { }); it('should return state resource observable', () => { - const submitObservable = formService.submit(); + const submitObservable: Observable<StateResource<Resource | HttpError>> = + formService.submit(); expect(submitObservable).toBeObservable(cold('(a|)', { a: stateResourceWithError })); }); }); - - describe('with validation problem', () => { - const validationProblemDetail = createValidationProblemDetails(); - - beforeEach(() => { - DummyFormService.SUBMIT_OBSERVABLE = () => throwError(() => validationProblemDetail); - formService.setValidationDetails = jest.fn(); - }); - - it('should call set validation details', (done) => { - formService.submit().subscribe({ - complete(): void { - expect(formService.setValidationDetails).toHaveBeenCalledWith(validationProblemDetail); - done(); - }, - }); - }); - - it('should return empty observable', () => { - const observable = formService.submit(); - - expect(observable).toBeObservable(cold('|')); - }); - }); }); describe('handleResponse', () => { @@ -98,76 +87,86 @@ describe('AbstractFormService', () => { const stateResource: StateResource<CommandResource> = createErrorStateResource(apiError); beforeEach(() => { - formService.setError = jest.fn(); + formService.handleError = jest.fn(); }); - it('should setError on validation error', () => { + it('should handleError on validation error', () => { formService.handleResponse({ ...stateResource, loading: false }); - expect(formService.setError).toHaveBeenCalledWith(apiError); + expect(formService.handleError).toHaveBeenCalledWith(apiError); }); it('should return stateresource while loading', () => { const commandStateResource: StateResource<CommandResource> = createEmptyStateResource(true); - const result: StateResource<Resource> = formService.handleResponse(commandStateResource); + const result: StateResource<Resource | HttpError> = + formService.handleResponse(commandStateResource); expect(result).toBe(commandStateResource); }); }); - describe('setError', () => { - let form: UntypedFormGroup; - const fieldName = 'test-field'; + describe('handle error', () => { + it('should set problem detail error', () => { + formService.setErrorByProblemDetail = jest.fn(); + const problemDetail: ProblemDetail = createProblemDetail(); - beforeEach(() => { - form = formService.form; - const control = new FormControl(''); - form.addControl(fieldName, control); - }); + formService.handleError(problemDetail); - it('should set issues of api-error on control', () => { - const apiError = createApiError(); + expect(formService.setErrorByProblemDetail).toHaveBeenCalledWith(problemDetail); + }); - const issue = apiError.issues[0]; - issue.field = 'testprefix.' + fieldName; + it('should set api error', () => { + formService.setErrorByApiError = jest.fn(); + const apiError: ApiError = createApiError(); - formService.setError(apiError); + formService.handleError(apiError); - expect(form.getError(issue.messageCode, fieldName)).toBe(issue); + expect(formService.setErrorByApiError).toHaveBeenCalledWith(apiError); }); }); - describe('setValidationDetails', () => { - let form: UntypedFormGroup; - const fieldName = 'test-field'; + describe('set error by api error', () => { + const issue: Issue = createIssue(); + const apiError: ApiError = createApiError([issue]); - beforeEach(() => { - form = formService.form; - const control = new FormControl(''); - form.addControl(fieldName, control); + it('should call setIssueValidationError', () => { + const setInvalidParamValidationErrorSpy: jest.SpyInstance<void> = jest + .spyOn(ValidationUtil, 'setIssueValidationError') + .mockImplementation(); + + formService.setErrorByApiError(apiError); + + expect(setInvalidParamValidationErrorSpy).toHaveBeenCalledWith( + formService.form, + issue, + TestFormService.PATH_PREFIX, + ); }); + }); + + describe('set error by problem detail', () => { + const invalidParam: InvalidParam = createInvalidParam(); + const problemDetail: ProblemDetail = createProblemDetail([invalidParam]); - it('should set invalid-params of validation-problem-details on control', () => { - const validationDetails = createValidationProblemDetails(); - const item = validationDetails['invalid-params'][0]; - item.name = 'testprefix.' + fieldName; + it('should call setInvalidParamValidationError', () => { + const setInvalidParamValidationErrorSpy: jest.SpyInstance<void> = jest + .spyOn(ValidationUtil, 'setInvalidParamValidationError') + .mockImplementation(); - formService.setValidationDetails(validationDetails); + formService.setErrorByProblemDetail(problemDetail); - const itemError = form.getError(item.reason, fieldName); - expect(itemError).toBe( - getMessageForInvalidParamsItem({ - ...item, - name: fieldName, - }), + expect(setInvalidParamValidationErrorSpy).toHaveBeenCalledWith( + formService.form, + invalidParam, + TestFormService.PATH_PREFIX, ); }); }); describe('patch', () => { it('should set form value', () => { - const formValue = { [DummyFormService.FIELD]: 'huhu' }; + const formValue: { [key: string]: string } = { [TestFormService.FIELD]: 'huhu' }; formService.patch(formValue); @@ -176,25 +175,27 @@ describe('AbstractFormService', () => { }); }); -export class DummyFormService extends AbstractFormService { - static FIELD = 'field'; - static SUBMIT_OBSERVABLE = () => of(createEmptyStateResource()); +class TestFormService extends AbstractFormService { + public static readonly FIELD: string = 'attribute'; + public static readonly PATH_PREFIX: string = 'path-prefix'; + + public static SUBMIT_OBSERVABLE = () => of(createEmptyStateResource()); constructor(formBuilder: UntypedFormBuilder) { super(formBuilder); } - initForm(): UntypedFormGroup { + protected initForm(): UntypedFormGroup { return this.formBuilder.group({ - [DummyFormService.FIELD]: new UntypedFormControl(null), + [TestFormService.FIELD]: new UntypedFormControl(null), }); } - doSubmit(): Observable<StateResource<any>> { - return DummyFormService.SUBMIT_OBSERVABLE(); + protected doSubmit(): Observable<StateResource<any>> { + return TestFormService.SUBMIT_OBSERVABLE(); } - getPathPrefix(): string { - return 'testprefix'; + protected getPathPrefix(): string { + return TestFormService.PATH_PREFIX; } } diff --git a/alfa-client/libs/tech-shared/src/lib/service/formservice.abstract.ts b/alfa-client/libs/tech-shared/src/lib/service/formservice.abstract.ts index 74a571685d699f00656eed76d01506aad2055f8a..ffb6ecd6eda97ae8f8ddc5e9e22fedf87b69efa3 100644 --- a/alfa-client/libs/tech-shared/src/lib/service/formservice.abstract.ts +++ b/alfa-client/libs/tech-shared/src/lib/service/formservice.abstract.ts @@ -21,20 +21,19 @@ * Die sprachspezifischen Genehmigungen und Beschränkungen * unter der Lizenz sind dem Lizenztext zu entnehmen. */ -import { AbstractControl, UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; -import { - ApiError, - getMessageForInvalidParamsItem, - hasError, - Issue, - setValidationError, - StateResource, -} from '@alfa-client/tech-shared'; +import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; + import { Resource } from '@ngxp/rest'; import { isNil } from 'lodash-es'; -import { EMPTY, Observable } from 'rxjs'; -import { catchError, map } from 'rxjs/operators'; -import { InvalidParamsItem, ValidationProblemDetails } from '../../../../admin-settings/src/lib/error/error.model'; +import { Observable } from 'rxjs'; +import { map } from 'rxjs/operators'; +import { StateResource, hasError } from '../resource/resource.util'; +import { HttpError, ProblemDetail, ApiError, InvalidParam, Issue } from '../tech.model'; +import { isNotUndefined } from '../tech.util'; +import { + setInvalidParamValidationError, + setIssueValidationError, +} from '../validation/tech.validation.util'; export abstract class AbstractFormService { form: UntypedFormGroup; @@ -47,64 +46,63 @@ export abstract class AbstractFormService { protected abstract initForm(): UntypedFormGroup; - public submit(): Observable<StateResource<Resource>> { - return this.doSubmit().pipe( - map((result) => this.handleResponse(result)), - catchError((error) => { - this.setValidationDetails(error as ValidationProblemDetails); - return EMPTY; - }), - ); + public submit(): Observable<StateResource<Resource | HttpError>> { + return this.doSubmit().pipe(map((result) => this.handleResponse(result))); } - protected abstract doSubmit(): Observable<StateResource<Resource>>; + protected abstract doSubmit(): Observable<StateResource<Resource | HttpError>>; - handleResponse(result: StateResource<Resource>): StateResource<Resource> { + handleResponse(result: StateResource<Resource | HttpError>): StateResource<Resource | HttpError> { if (result.loading) return result; if (hasError(result)) { - this.setError(result.error); + this.handleError(result.error); } return result; } - patch(valueToPatch: any): void { - this.form.reset(); - this.form.patchValue(valueToPatch); + handleError(error: HttpError): void { + if (this.isApiError(error)) { + this.setErrorByApiError(<ApiError>error); + } + if (this.isProblemDetail(error)) { + this.setErrorByProblemDetail(<ProblemDetail>error); + } + } - this.source = valueToPatch; + private isApiError(error: HttpError): boolean { + return isNotUndefined((<ApiError>error).issues); + } - this.doAfterPatch(valueToPatch); + private isProblemDetail(error: HttpError): boolean { + return isNotUndefined((<ProblemDetail>error)['invalid-params']); } - doAfterPatch(source: any): void { - //No Implementation here for abstract class + setErrorByApiError(apiError: ApiError): void { + apiError.issues.forEach((issue: Issue) => + setIssueValidationError(this.form, issue, this.getPathPrefix()), + ); } - setValidationDetails(error: ValidationProblemDetails): void { - error['invalid-params'].forEach((prefixedItem: InvalidParamsItem): void => { - const item: InvalidParamsItem = this.itemWithoutNamePrefix(prefixedItem); - const formControl: AbstractControl = this.form.get(item.name); - formControl.setErrors({ [item.reason]: getMessageForInvalidParamsItem(item) }); - formControl.markAsTouched(); + setErrorByProblemDetail(error: ProblemDetail): void { + error['invalid-params'].forEach((invalidParam: InvalidParam) => { + setInvalidParamValidationError(this.form, invalidParam, this.getPathPrefix()); }); } - private itemWithoutNamePrefix(item: InvalidParamsItem): InvalidParamsItem { - const namePath: string = item.name; - const prefix: string = this.getPathPrefix(); - const pathPrefix: string = prefix + (prefix.endsWith('.') ? '' : '.'); - if (!namePath.startsWith(pathPrefix)) - throw Error(`Expected prefix ${prefix} not found: ${namePath}`); - return { ...item, name: namePath.substring(pathPrefix.length) }; - } + protected abstract getPathPrefix(): string; - setError(apiError: ApiError) { - apiError.issues.forEach((issue: Issue) => - setValidationError(this.form, issue, this.getPathPrefix()), - ); + patch(valueToPatch: any): void { + this.form.reset(); + this.form.patchValue(valueToPatch); + + this.source = valueToPatch; + + this.doAfterPatch(valueToPatch); } - protected abstract getPathPrefix(): string; + doAfterPatch(source: any): void { + //No Implementation here for abstract class + } getFormValue(): any { return this.form.value; @@ -117,8 +115,4 @@ export abstract class AbstractFormService { public isPatch(): boolean { return !isNil(this.source); } - - public get invalidEmpty(): boolean { - return this.form.invalid; - } } diff --git a/alfa-client/libs/tech-shared/src/lib/tech.model.ts b/alfa-client/libs/tech-shared/src/lib/tech.model.ts index 01fd103a38e586a236163d0778b236f441628706..802ac9ba69948b9e494711186487029d8a1a7422 100644 --- a/alfa-client/libs/tech-shared/src/lib/tech.model.ts +++ b/alfa-client/libs/tech-shared/src/lib/tech.model.ts @@ -1,4 +1,5 @@ -import { HttpHeaders } from '@angular/common/http'; +import { HttpHeaders, HttpStatusCode } from '@angular/common/http'; +import { ValidationMessageCode } from './validation/tech.validation.messages'; /* * Copyright (C) 2022 Das Land Schleswig-Holstein vertreten durch den @@ -40,6 +41,22 @@ export interface IssueParam { value: string; } +export interface ProblemDetail { + type: string; + title: string; + status: HttpStatusCode; + detail: string; + instance: string; + 'invalid-params': InvalidParam[]; +} + +export interface InvalidParam { + name: string; + reason: ValidationMessageCode; +} + +export declare type HttpError = ProblemDetail | ApiError; + export enum HttpMethod { POST = 'POST', PUT = 'PUT', diff --git a/alfa-client/libs/tech-shared/src/lib/tech.util.ts b/alfa-client/libs/tech-shared/src/lib/tech.util.ts index 1b295bc50d9e83fda642d6a9b13f4e48ada0cf0a..7b6c48fdfff3133aa14a4bae41cdd2f1464df78a 100644 --- a/alfa-client/libs/tech-shared/src/lib/tech.util.ts +++ b/alfa-client/libs/tech-shared/src/lib/tech.util.ts @@ -40,15 +40,15 @@ export function isEmptyObject(obj: any): boolean { } export function replacePlaceholders(text: string, placeholders: { [key: string]: string }): string { - let replaced = text; + let replaced: string = text; Object.keys(placeholders).forEach( - (key) => (replaced = replacePlaceholder(replaced, key, placeholders[key])), + (key: string) => (replaced = replacePlaceholder(replaced, key, placeholders[key])), ); return replaced; } export function replacePlaceholder(text: string, placeholder: string, value: string): string { - const regex = new RegExp('{' + placeholder + '}', 'g'); + const regex: RegExp = new RegExp('{' + placeholder + '}', 'g'); return text.replace(regex, value); } @@ -58,8 +58,8 @@ export function hasExceptionId(apiError: ApiError): boolean { ); } -export function sleep(delayInMs: number) { - const start = new Date().getTime(); +export function sleep(delayInMs: number): void { + const start: number = new Date().getTime(); while (new Date().getTime() < start + delayInMs); } @@ -96,11 +96,11 @@ export function convertForDataTest(value: string): string { return simpleTransliteration(value); } -export function replaceAllWhitespaces(value: string, replaceWith: string) { +export function replaceAllWhitespaces(value: string, replaceWith: string): string { return value.replace(/ /g, replaceWith); } -export function simpleTransliteration(value: string) { +export function simpleTransliteration(value: string): string { return value.normalize('NFKD').replace(/[^-A-Za-z0-9_]/g, ''); } diff --git a/alfa-client/libs/tech-shared/src/lib/validation/tech.validation.util.spec.ts b/alfa-client/libs/tech-shared/src/lib/validation/tech.validation.util.spec.ts index a6366dfbc2db53b0fac466ffdd52b0f022093c0b..10218c11dcf905b85f05989b4a813eed123d97b4 100644 --- a/alfa-client/libs/tech-shared/src/lib/validation/tech.validation.util.spec.ts +++ b/alfa-client/libs/tech-shared/src/lib/validation/tech.validation.util.spec.ts @@ -21,25 +21,30 @@ * Die sprachspezifischen Genehmigungen und Beschränkungen * unter der Lizenz sind dem Lizenztext zu entnehmen. */ -import { UntypedFormControl, UntypedFormGroup } from '@angular/forms'; +import { + AbstractControl, + FormControl, + FormGroup, + UntypedFormControl, + UntypedFormGroup, +} from '@angular/forms'; import { createIssue } from '../../../test/error'; -import { Issue } from '../tech.model'; +import { InvalidParam, Issue } from '../tech.model'; import { getControlForIssue, - getMessageForInvalidParamsItem, + getMessageForInvalidParam, getMessageForIssue, - setValidationError, + setIssueValidationError, } from './tech.validation.util'; import { ValidationMessageCode } from './tech.validation.messages'; -import { InvalidParamsItem } from '../../../../admin-settings/src/lib/error/error.model'; describe('ValidationUtils', () => { - describe('setValidationError', () => { - const baseField1Control = new UntypedFormControl(); - const baseField2Control = new UntypedFormControl(); - const subGroupFieldControl = new UntypedFormControl(); + describe('set issue validation error', () => { + const baseField1Control: FormControl = new UntypedFormControl(); + const baseField2Control: FormControl = new UntypedFormControl(); + const subGroupFieldControl: FormControl = new UntypedFormControl(); - const form = new UntypedFormGroup({ + const form: FormGroup = new UntypedFormGroup({ baseField1: baseField1Control, baseField2: baseField2Control, subGroup: new UntypedFormGroup({ @@ -51,23 +56,23 @@ describe('ValidationUtils', () => { it('should return base field control', () => { const issue: Issue = { ...createIssue(), field: 'baseField1' }; - const control = getControlForIssue(form, issue); + const control: AbstractControl = getControlForIssue(form, issue); expect(control).toBe(baseField1Control); }); - it('should reeturn sub group field', () => { + it('should return sub group field', () => { const issue: Issue = { ...createIssue(), field: 'subGroup.subGroupField1' }; - const control = getControlForIssue(form, issue); + const control: AbstractControl = getControlForIssue(form, issue); expect(control).toBe(subGroupFieldControl); }); it('should ignore path prefix', () => { - const issue: Issue = { ...createIssue(), field: 'command.wiedervorlage.baseField1' }; + const issue: Issue = { ...createIssue(), field: 'pathprefix.resource.baseField1' }; - const control = getControlForIssue(form, issue, 'command.wiedervorlage'); + const control: AbstractControl = getControlForIssue(form, issue, 'pathprefix.resource'); expect(control).toBe(baseField1Control); }); @@ -77,25 +82,25 @@ describe('ValidationUtils', () => { const issue: Issue = { ...createIssue(), field: 'baseField1' }; it('should set error in control', () => { - setValidationError(form, issue); + setIssueValidationError(form, issue); expect(baseField1Control.errors).not.toBeNull(); }); it('should set message code in control', () => { - setValidationError(form, issue); + setIssueValidationError(form, issue); expect(baseField1Control.hasError(issue.messageCode)).toBe(true); }); it('should set control touched', () => { - setValidationError(form, issue); + setIssueValidationError(form, issue); expect(baseField1Control.touched).toBe(true); }); it('should not set error in other control', () => { - setValidationError(form, issue); + setIssueValidationError(form, issue); expect(baseField2Control.errors).toBeNull(); }); @@ -105,7 +110,7 @@ describe('ValidationUtils', () => { const issue: Issue = { ...createIssue(), field: 'subGroup.subGroupField1' }; it('should set error in control', () => { - setValidationError(form, issue); + setIssueValidationError(form, issue); expect(subGroupFieldControl.errors).not.toBeNull(); }); @@ -113,10 +118,10 @@ describe('ValidationUtils', () => { }); describe('get message for issue', () => { - const fieldLabel = 'Field Label'; + const fieldLabel: string = 'Field Label'; it('should return message', () => { - const msg = getMessageForIssue(fieldLabel, { + const msg: string = getMessageForIssue(fieldLabel, { ...createIssue(), messageCode: 'validation_field_size', }); @@ -125,7 +130,7 @@ describe('ValidationUtils', () => { }); it('should set field label', () => { - const msg = getMessageForIssue(fieldLabel, { + const msg: string = getMessageForIssue(fieldLabel, { ...createIssue(), messageCode: 'validation_field_size', }); @@ -134,7 +139,7 @@ describe('ValidationUtils', () => { }); it('should replace min param', () => { - const msg = getMessageForIssue(fieldLabel, { + const msg: string = getMessageForIssue(fieldLabel, { ...createIssue(), messageCode: 'validation_field_size', parameters: [{ name: 'min', value: '3' }], @@ -145,15 +150,80 @@ describe('ValidationUtils', () => { }); describe('get message for invalid-params-item', () => { - const item: InvalidParamsItem = { + const item: InvalidParam = { name: 'name-of-field', reason: ValidationMessageCode.VALIDATION_FIELD_EMPTY, }; it('should return message', () => { - const msg = getMessageForInvalidParamsItem(item); + const msg: string = getMessageForInvalidParam(item); expect(msg).toEqual(`Bitte ${item.name} ausfüllen`); }); }); }); + +// describe('setValidationDetails', () => { +// let form: UntypedFormGroup; +// const fieldName = 'test-field'; + +// beforeEach(() => { +// form = formService.form; +// const control = new FormControl(''); +// form.addControl(fieldName, control); +// }); + +// it('should set invalid-params of validation-problem-details on control', () => { +// const validationDetails: ProblemDetail = createAbsenderNameProblemDetail(); +// const item = validationDetails['invalid-params'][0]; +// item.name = '.' + fieldName; + +// formService.setErrorByProblemDetail(validationDetails); + +// const itemError = form.getError(item.reason, fieldName); +// expect(itemError).toBe( +// getMessageForInvalidParamsItem({ +// ...item, +// name: fieldName, +// }), +// ); +// }); +// }); + +// describe('set error by api error', () => { +// // let form: UntypedFormGroup; +// const fieldName: string = 'test-field'; + +// beforeEach(() => { +// // form = formService.form; +// // const control = ; +// formService.form.addControl(fieldName, new FormControl('')); +// }); + +// it('should set issues of api-error on control', () => { +// const issue: Issue = { ...createIssue(), field: TestFormService + '.' + fieldName }; +// const apiError: ApiError = createApiError([issue]); + +// formService.setErrorByApiError(apiError); + +// expect(formService.form.getError(issue.messageCode, fieldName)).toBe(issue); +// }); +// }); + +// export function createAbsenderNameProblemDetail(invalidParams: InvalidParam[]): ProblemDetail { +// return { +// type: 'about:blank', +// title: 'Unprocessable Entity', +// status: HttpStatusCode.UnprocessableEntity, +// detail: 'settingsBody.absender.name: validation_field_empty', +// instance: '/api/configuration/settings/65df039a69eeafef365a42dd', +// 'invalid-params': invalidParams, +// }; +// } + +// export function createAbsenderNameInvalidParam(): InvalidParam { +// return { +// name: 'settingsBody.absender.name', +// reason: ValidationMessageCode.VALIDATION_FIELD_EMPTY, +// }; +// } diff --git a/alfa-client/libs/tech-shared/src/lib/validation/tech.validation.util.ts b/alfa-client/libs/tech-shared/src/lib/validation/tech.validation.util.ts index 8528c45d4eaeae78cd5557781096f950545bbdf6..ff661a65b78a6e4d668c5b97afe1d83ab98ebf1d 100644 --- a/alfa-client/libs/tech-shared/src/lib/validation/tech.validation.util.ts +++ b/alfa-client/libs/tech-shared/src/lib/validation/tech.validation.util.ts @@ -21,19 +21,22 @@ * Die sprachspezifischen Genehmigungen und Beschränkungen * unter der Lizenz sind dem Lizenztext zu entnehmen. */ -import { AbstractControl, UntypedFormGroup } from '@angular/forms'; +import { AbstractControl, FormGroup, UntypedFormGroup } from '@angular/forms'; import { isNil } from 'lodash-es'; -import { ApiError, Issue } from '../tech.model'; +import { ApiError, InvalidParam, Issue, IssueParam } from '../tech.model'; import { replacePlaceholder } from '../tech.util'; import { VALIDATION_MESSAGES, ValidationMessageCode } from './tech.validation.messages'; -import { InvalidParamsItem } from '../../../../admin-settings/src/lib/error/error.model'; export function isValidationError(issue: Issue): boolean { return issue.messageCode.includes('javax.validation.constraints'); } -export function setValidationError(form: UntypedFormGroup, issue: Issue, pathPrefix?: string) { - const control = getControlForIssue(form, issue, pathPrefix); +export function setIssueValidationError( + form: UntypedFormGroup, + issue: Issue, + pathPrefix?: string, +): void { + const control: AbstractControl = getControlForIssue(form, issue, pathPrefix); control.setErrors({ [issue.messageCode]: issue }); control.markAsTouched(); @@ -44,7 +47,7 @@ export function getControlForIssue( issue: Issue, pathPrefix?: string, ): AbstractControl { - const fieldPath = pathPrefix ? issue.field.substring(pathPrefix.length + 1) : issue.field; + const fieldPath: string = pathPrefix ? issue.field.substring(pathPrefix.length + 1) : issue.field; let curControl: AbstractControl = form; fieldPath @@ -54,8 +57,8 @@ export function getControlForIssue( return curControl; } -export function getMessageForIssue(label: string, issue: Issue) { - let msg = VALIDATION_MESSAGES[issue.messageCode]; +export function getMessageForIssue(label: string, issue: Issue): string { + let msg: string = VALIDATION_MESSAGES[issue.messageCode]; if (isNil(msg)) { console.warn('No message for code ' + issue.messageCode + ' found.'); @@ -63,30 +66,61 @@ export function getMessageForIssue(label: string, issue: Issue) { } msg = replacePlaceholder(msg, 'field', label); - issue.parameters.forEach((param) => (msg = replacePlaceholder(msg, param.name, param.value))); + issue.parameters.forEach( + (param: IssueParam) => (msg = replacePlaceholder(msg, param.name, param.value)), + ); return msg; } -export function getMessageForInvalidParamsItem(item: InvalidParamsItem): string { - let formatString = VALIDATION_MESSAGES[item.reason] ?? item.reason; +export function isValidationFieldFileSizeExceedError(error: any) { + return getMessageCode(error) === ValidationMessageCode.VALIDATION_FIELD_FILE_SIZE_EXCEEDED; +} + +export function getMessageCode(apiError: ApiError): string { + return apiError.issues[0].messageCode; +} + +//TODO TDD entwickeln - kleine Tests +export function setInvalidParamValidationError( + form: UntypedFormGroup, + invalidParam: InvalidParam, + pathPrefix?: string, +): void { + const item: InvalidParam = itemWithoutNamePrefix(invalidParam, pathPrefix); + const formControl: AbstractControl = form.get(item.name); + + formControl.setErrors({ [item.reason]: getMessageForInvalidParam(item) }); + formControl.markAsTouched(); +} + +//Das Verb fehlt bei der Function +function itemWithoutNamePrefix(item: InvalidParam, pathPrefix?: string): InvalidParam { + const namePath: string = item.name; + //Was wird hier genau gemacht? warum? + const pathPrefixIrgendwas: string = pathPrefix + (pathPrefix.endsWith('.') ? '' : '.'); + //Der pathPrefix wird doch einmal konfiguriert und dann sollte das doch passen, wann und warum genau soll hier der Error geworfen werden? + if (!namePath.startsWith(pathPrefix)) + throw Error(`Expected prefix ${pathPrefix} not found: ${namePath}`); + //Ich finde das sehr merkwürdig, dass hier was im Item manipuliert und dann zurückgegeben wird, wieso nicht einfach den name zurückgeben? + return { ...item, name: namePath.substring(pathPrefixIrgendwas.length) }; +} +export function getMessageForInvalidParam(item: InvalidParam): string { + let formatString: string = VALIDATION_MESSAGES[item.reason] ?? item.reason; + + //Wieso wird hier durchiteriert? Das ist doch ein Item mit name und reason oder nicht? for (const [itemField, value] of Object.entries(item)) { + //Warum genau wird "das" hier aufgebaut? const itemFieldToPlaceholder = { name: 'field', }; formatString = replacePlaceholder( formatString, + //Verstehe ich auch nicht, wieso nicht einfach 'field'? itemFieldToPlaceholder[itemField] ?? itemField, value, ); } return formatString; } - -export function getMessageCode(apiError: ApiError): string { - return apiError.issues[0].messageCode; -} - -export function isValidationFieldFileSizeExceedError(error: any) { - return getMessageCode(error) === ValidationMessageCode.VALIDATION_FIELD_FILE_SIZE_EXCEEDED; -} +// diff --git a/alfa-client/libs/tech-shared/test/error.ts b/alfa-client/libs/tech-shared/test/error.ts index 1aa5a8441a2facbb5fe40034c935f0573e05a10d..9803a5a5ad90888ceb7125bc535f5c379548d126 100644 --- a/alfa-client/libs/tech-shared/test/error.ts +++ b/alfa-client/libs/tech-shared/test/error.ts @@ -23,9 +23,8 @@ */ import { HttpErrorResponse, HttpStatusCode } from '@angular/common/http'; import { faker } from '@faker-js/faker'; -import { ApiError, Issue, IssueParam } from '../src/lib/tech.model'; +import { ApiError, InvalidParam, Issue, IssueParam, ProblemDetail } from '../src/lib/tech.model'; import { ValidationMessageCode } from '../src/lib/validation/tech.validation.messages'; -import { ValidationProblemDetails } from '../../admin-settings/src/lib/error/error.model'; export function createIssueParam(): IssueParam { return { @@ -43,26 +42,8 @@ export function createIssue(): Issue { }; } -export function createApiError(): ApiError { - return { - issues: [createIssue()], - }; -} - -export function createValidationProblemDetails(): ValidationProblemDetails { - return { - type: 'about:blank', - title: 'Unprocessable Entity', - status: HttpStatusCode.UnprocessableEntity, - detail: 'settingsBody.absender.name: validation_field_empty', - instance: '/api/configuration/settings/65df039a69eeafef365a42dd', - 'invalid-params': [ - { - name: 'settingsBody.absender.name', - reason: ValidationMessageCode.VALIDATION_FIELD_EMPTY, - }, - ], - }; +export function createApiError(issues: Issue[] = [createIssue()]): ApiError { + return { issues }; } export function createHttpErrorResponse(apiError: ApiError = null): HttpErrorResponse { @@ -72,3 +53,20 @@ export function createHttpErrorResponse(apiError: ApiError = null): HttpErrorRes }, }; } + +export function createProblemDetail( + invalidParams: InvalidParam[] = [createInvalidParam()], +): ProblemDetail { + return { + status: HttpStatusCode.UnprocessableEntity, + title: faker.random.word(), + type: faker.random.word(), + instance: faker.internet.url(), + detail: faker.random.word(), + 'invalid-params': invalidParams, + }; +} + +export function createInvalidParam(): InvalidParam { + return { name: faker.random.word(), reason: ValidationMessageCode.VALIDATION_FIELD_EMPTY }; +} diff --git a/alfa-client/libs/ui/src/lib/ui/validation-error/validation-error.component.ts b/alfa-client/libs/ui/src/lib/ui/validation-error/validation-error.component.ts index 066afd9e872c3354d475f0421d8cb2f363d1acd8..0432ac003dae9960503559194148fb5d79f52c45 100644 --- a/alfa-client/libs/ui/src/lib/ui/validation-error/validation-error.component.ts +++ b/alfa-client/libs/ui/src/lib/ui/validation-error/validation-error.component.ts @@ -33,7 +33,7 @@ export class ValidationErrorComponent { @Input() label: string; @Input() issues: Issue[]; - message(issue: Issue): string { + public message(issue: Issue): string { return getMessageForIssue(this.label, issue); } } diff --git a/alfa-client/libs/user-profile/src/lib/user-profile-search-container/user-profile-search/user-profile.search.formservice.ts b/alfa-client/libs/user-profile/src/lib/user-profile-search-container/user-profile-search/user-profile.search.formservice.ts index 12d7f3fa7dfa380d681d263104350b32d7b382e0..4ec86ff50492e343fb1fe25fe2936d8cab1e8e9a 100644 --- a/alfa-client/libs/user-profile/src/lib/user-profile-search-container/user-profile-search/user-profile.search.formservice.ts +++ b/alfa-client/libs/user-profile/src/lib/user-profile-search-container/user-profile-search/user-profile.search.formservice.ts @@ -62,11 +62,11 @@ export class UserProfileSearchFormService extends AbstractFormService implements } public setEmptyUserProfileError(): void { - this.setError(emptyUserProfileError); + this.setErrorByApiError(emptyUserProfileError); } public setNoUserProfileFoundError(): void { - this.setError(noUserProfileFoundError); + this.setErrorByApiError(noUserProfileFoundError); } ngOnDestroy(): void {