diff --git a/alfa-client/apps/alfa/src/styles/material/_formfield.scss b/alfa-client/apps/alfa/src/styles/material/_formfield.scss index 2e22e8b8ecf4a04dc2810a38022639466d2f3962..5cd264736c1e1a67ae882b08427d5c3239d0e895 100644 --- a/alfa-client/apps/alfa/src/styles/material/_formfield.scss +++ b/alfa-client/apps/alfa/src/styles/material/_formfield.scss @@ -26,11 +26,12 @@ ozgcloud-fixed-dialog { body.dark { mat-form-field { - --mdc-theme-error: red; - --mdc-filled-text-field-error-focus-label-text-color: red; - --mdc-outlined-text-field-error-focus-label-text-color: red; - --mdc-filled-text-field-error-label-text-color: red; - --mdc-outlined-text-field-error-label-text-color: red; - --mdc-filled-text-field-disabled-active-indicator-color: red; + --mdc-theme-error: theme('colors.error'); + --mat-form-field-error-text-color: theme('colors.error'); + --mdc-filled-text-field-error-focus-label-text-color: theme('colors.error'); + --mdc-outlined-text-field-error-focus-label-text-color: theme('colors.error'); + --mdc-filled-text-field-error-label-text-color: theme('colors.error'); + --mdc-outlined-text-field-error-label-text-color: theme('colors.error'); + --mdc-filled-text-field-disabled-active-indicator-color: theme('colors.error'); } } 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 f786c3e04a73e2bc719cacf654aaf0c01d77039d..f3e6c68f3f15f51d766224ea4add00605a4e05be 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 @@ -175,7 +175,7 @@ describe('PostfachFormComponent', () => { function createProblemDetailForAbsenderName(): ProblemDetail { return { ...createProblemDetail(), - 'invalid-params': [{ ...createInvalidParam(), name: 'settingBody.absender.name' }], + invalidParams: [{ ...createInvalidParam(), name: 'settingBody.absender.name' }], }; } diff --git a/alfa-client/libs/binary-file-shared/src/lib/binary-file.service.spec.ts b/alfa-client/libs/binary-file-shared/src/lib/binary-file.service.spec.ts index 9d69ae78a151079401a9d94caceff99e8c980675..328960de1f9b9a87f8442595ad8b609f339f9da8 100644 --- a/alfa-client/libs/binary-file-shared/src/lib/binary-file.service.spec.ts +++ b/alfa-client/libs/binary-file-shared/src/lib/binary-file.service.spec.ts @@ -262,7 +262,7 @@ describe('BinaryFileService', () => { service.handleSnackBar(buildUnprocessableEntityErrorResponse(), true); expect(snackBarService.showError).toHaveBeenCalledWith( - VALIDATION_MESSAGES[ValidationMessageCode.VALIDATION_FIELD_FILE_SIZE_EXCEEDED], + VALIDATION_MESSAGES[ValidationMessageCode.FIELD_FILE_SIZE_EXCEEDED], ); }); @@ -274,7 +274,7 @@ describe('BinaryFileService', () => { it('should not call snackbarService if not file size exceeded error', () => { service.handleSnackBar( - buildUnprocessableEntityErrorResponse(ValidationMessageCode.VALIDATION_FIELD_EMPTY), + buildUnprocessableEntityErrorResponse(ValidationMessageCode.FIELD_EMPTY), true, ); @@ -283,15 +283,15 @@ describe('BinaryFileService', () => { }); function buildUnprocessableEntityErrorResponse( - validationMessageCode: ValidationMessageCode = ValidationMessageCode.VALIDATION_FIELD_FILE_SIZE_EXCEEDED, + validationMessageCode: ValidationMessageCode = ValidationMessageCode.FIELD_FILE_SIZE_EXCEEDED, ): HttpErrorResponse { return <HttpErrorResponse>{ status: 422, error: { - issues: [ + invalidParams: [ { - messageCode: validationMessageCode, - parameters: [], + reason: validationMessageCode, + constraintParameters: [], }, ], }, diff --git a/alfa-client/libs/binary-file-shared/src/lib/binary-file.service.ts b/alfa-client/libs/binary-file-shared/src/lib/binary-file.service.ts index bb6592cb76931ff30edca949cc28a864686ecfce..57773cccbfa61d4dab2af4984a88d6d3f9cf7b14 100644 --- a/alfa-client/libs/binary-file-shared/src/lib/binary-file.service.ts +++ b/alfa-client/libs/binary-file-shared/src/lib/binary-file.service.ts @@ -29,7 +29,7 @@ import { createEmptyStateResource, createErrorStateResource, createStateResource, - getMessageForIssue, + getMessageForInvalidParam, isNotNil, isUnprocessableEntity, isValidationFieldFileSizeExceedError, @@ -90,7 +90,9 @@ export class BinaryFileService { handleSnackBar(error: HttpErrorResponse, showValidationErrorSnackBar: boolean) { if (showValidationErrorSnackBar && isValidationFieldFileSizeExceedError(error.error)) { - this.snackbarService.showError(getMessageForIssue(EMPTY_STRING, error.error.issues[0])); + this.snackbarService.showError( + getMessageForInvalidParam(EMPTY_STRING, error.error.invalidParams[0]), + ); } } diff --git a/alfa-client/libs/design-component/src/lib/form/formcontrol-editor.abstract.component.ts b/alfa-client/libs/design-component/src/lib/form/formcontrol-editor.abstract.component.ts index a023a009d1780be33395159d25234da39285f745..cb0f3f5842b32a3ce76321d194097c9a2f64229a 100644 --- a/alfa-client/libs/design-component/src/lib/form/formcontrol-editor.abstract.component.ts +++ b/alfa-client/libs/design-component/src/lib/form/formcontrol-editor.abstract.component.ts @@ -21,7 +21,7 @@ * Die sprachspezifischen Genehmigungen und Beschränkungen * unter der Lizenz sind dem Lizenztext zu entnehmen. */ -import { Issue } from '@alfa-client/tech-shared'; +import { InvalidParam } from '@alfa-client/tech-shared'; import { Component, OnDestroy, OnInit, Optional, Self } from '@angular/core'; import { ControlValueAccessor, NgControl, UntypedFormControl } from '@angular/forms'; import { isEmpty } from 'lodash-es'; @@ -89,9 +89,11 @@ export abstract class FormControlEditorAbstractComponent } } - get issues(): Issue[] { + get invalidParams(): InvalidParam[] { return this.fieldControl.errors ? - Object.keys(this.fieldControl.errors).map((key) => <Issue>this.fieldControl.errors[key]) + Object.keys(this.fieldControl.errors).map( + (key) => <InvalidParam>this.fieldControl.errors[key], + ) : []; } } diff --git a/alfa-client/libs/design-component/src/lib/form/text-editor/text-editor.component.html b/alfa-client/libs/design-component/src/lib/form/text-editor/text-editor.component.html index 544b9312b03adba3e0e097e8eba8a20751e5b013..9252326a5bdcc9d9c25ec164981bd0e24915bafa 100644 --- a/alfa-client/libs/design-component/src/lib/form/text-editor/text-editor.component.html +++ b/alfa-client/libs/design-component/src/lib/form/text-editor/text-editor.component.html @@ -10,7 +10,7 @@ > <ods-validation-error error - [issues]="issues" + [invalidParams]="invalidParams" [label]="label" [attr.data-test-id]="(label | convertForDataTest) + '-text-editor-error'" ></ods-validation-error> diff --git a/alfa-client/libs/design-component/src/lib/form/text-editor/text-editor.component.ts b/alfa-client/libs/design-component/src/lib/form/text-editor/text-editor.component.ts index c78a7554800ed6f61eb9d189617811e7f9d0490a..c40849cf31e2279a3727527898213567bab07c4c 100644 --- a/alfa-client/libs/design-component/src/lib/form/text-editor/text-editor.component.ts +++ b/alfa-client/libs/design-component/src/lib/form/text-editor/text-editor.component.ts @@ -26,6 +26,6 @@ export class TextEditorComponent extends FormControlEditorAbstractComponent { @Input() focus: boolean = false; get variant(): string { - return this.issues.length > 0 ? 'error' : 'default'; + return this.invalidParams.length > 0 ? 'error' : 'default'; } } diff --git a/alfa-client/libs/design-component/src/lib/form/textarea-editor/textarea-editor.component.html b/alfa-client/libs/design-component/src/lib/form/textarea-editor/textarea-editor.component.html index 5f3959bd34260ac44c11db6a108f24583986b9b0..c27f425a3f9ca703f85b9961a8c87175a50a0a9f 100644 --- a/alfa-client/libs/design-component/src/lib/form/textarea-editor/textarea-editor.component.html +++ b/alfa-client/libs/design-component/src/lib/form/textarea-editor/textarea-editor.component.html @@ -10,7 +10,7 @@ > <ods-validation-error error - [issues]="issues" + [invalidParams]="invalidParams" [label]="label" [attr.data-test-id]="(label | convertForDataTest) + '-textarea-editor-error'" ></ods-validation-error> diff --git a/alfa-client/libs/design-component/src/lib/form/textarea-editor/textarea-editor.component.ts b/alfa-client/libs/design-component/src/lib/form/textarea-editor/textarea-editor.component.ts index f78ee2c938fdb89b91a17060ba862ecadcff04aa..a47b955192b12c4a9ce1379062cd16543e455e2a 100644 --- a/alfa-client/libs/design-component/src/lib/form/textarea-editor/textarea-editor.component.ts +++ b/alfa-client/libs/design-component/src/lib/form/textarea-editor/textarea-editor.component.ts @@ -26,6 +26,6 @@ export class TextareaEditorComponent extends FormControlEditorAbstractComponent @Input() focus: boolean = false; get variant(): string { - return this.issues.length > 0 ? 'error' : 'default'; + return this.invalidParams.length > 0 ? 'error' : 'default'; } } diff --git a/alfa-client/libs/design-component/src/lib/form/validation-error/validation-error.component.html b/alfa-client/libs/design-component/src/lib/form/validation-error/validation-error.component.html index 703f757c2b17228e8cdfb7d158a7bb7e6a058a93..28ffaaac9b7880787146c5091eceedee3d2a662e 100644 --- a/alfa-client/libs/design-component/src/lib/form/validation-error/validation-error.component.html +++ b/alfa-client/libs/design-component/src/lib/form/validation-error/validation-error.component.html @@ -1,3 +1,3 @@ -<ng-container *ngFor="let issue of issues" - ><ods-error-message [text]="message(issue)"></ods-error-message +<ng-container *ngFor="let invalidParam of invalidParams" + ><ods-error-message [text]="message(invalidParam)"></ods-error-message ></ng-container> diff --git a/alfa-client/libs/design-component/src/lib/form/validation-error/validation-error.component.spec.ts b/alfa-client/libs/design-component/src/lib/form/validation-error/validation-error.component.spec.ts index 6abe6133bddfc36ca6ca42e2aa28d049fa96ef4d..845bf47eb456d09d581f97fae71493c8808729ac 100644 --- a/alfa-client/libs/design-component/src/lib/form/validation-error/validation-error.component.spec.ts +++ b/alfa-client/libs/design-component/src/lib/form/validation-error/validation-error.component.spec.ts @@ -1,6 +1,7 @@ -import { getMessageForIssue } from '@alfa-client/tech-shared'; +import { getMessageForInvalidParam, InvalidParam } from '@alfa-client/tech-shared'; import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { createIssue } from 'libs/tech-shared/test/error'; +import { ValidationMessageCode } from 'libs/tech-shared/src/lib/validation/tech.validation.messages'; +import { createInvalidParam } from 'libs/tech-shared/test/error'; import { ValidationErrorComponent } from './validation-error.component'; describe('ValidationErrorComponent', () => { @@ -21,32 +22,29 @@ describe('ValidationErrorComponent', () => { expect(component).toBeTruthy(); }); - describe('get message for issue', () => { + describe('get message from invalidParam', () => { const fieldLabel: string = 'Field Label'; + const invalidParam: InvalidParam = { + ...createInvalidParam(), + reason: ValidationMessageCode.FIELD_SIZE, + }; - it('should return message', () => { - const msg: string = getMessageForIssue(fieldLabel, { - ...createIssue(), - messageCode: 'validation_field_size', - }); + it('should contain', () => { + const msg: string = getMessageForInvalidParam(fieldLabel, invalidParam); expect(msg).toContain('muss mindestens'); }); it('should set field label', () => { - const msg: string = getMessageForIssue(fieldLabel, { - ...createIssue(), - messageCode: 'validation_field_size', - }); + const msg: string = getMessageForInvalidParam(fieldLabel, invalidParam); expect(msg).toContain(fieldLabel); }); it('should replace min param', () => { - const msg: string = getMessageForIssue(fieldLabel, { - ...createIssue(), - messageCode: 'validation_field_size', - parameters: [{ name: 'min', value: '3' }], + const msg: string = getMessageForInvalidParam(fieldLabel, { + ...invalidParam, + constraintParameters: [{ name: 'min', value: '3' }], }); expect(msg).toContain('3'); diff --git a/alfa-client/libs/design-component/src/lib/form/validation-error/validation-error.component.ts b/alfa-client/libs/design-component/src/lib/form/validation-error/validation-error.component.ts index d47b675b4984616c8ff76acaa04d3c73ebafad42..4d8a67a6e5f9ee9d39a99c9fe5fd97fe6136fe2c 100644 --- a/alfa-client/libs/design-component/src/lib/form/validation-error/validation-error.component.ts +++ b/alfa-client/libs/design-component/src/lib/form/validation-error/validation-error.component.ts @@ -1,4 +1,4 @@ -import { Issue, getMessageForIssue } from '@alfa-client/tech-shared'; +import { InvalidParam, getMessageForInvalidParam } from '@alfa-client/tech-shared'; import { CommonModule } from '@angular/common'; import { Component, Input } from '@angular/core'; import { ErrorMessageComponent } from '@ods/system'; @@ -11,9 +11,9 @@ import { ErrorMessageComponent } from '@ods/system'; }) export class ValidationErrorComponent { @Input() label: string; - @Input() issues: Issue[]; + @Input() invalidParams: InvalidParam[]; - public message(issue: Issue): string { - return getMessageForIssue(this.label, issue); + public message(invalidParam: InvalidParam): string { + return getMessageForInvalidParam(this.label, invalidParam); } } diff --git a/alfa-client/libs/navigation/src/lib/build-info/build-info.component.html b/alfa-client/libs/navigation/src/lib/build-info/build-info.component.html index 3befc6c17ff6c1c67843bb0969acfb0ed2ce4ef3..41cce668006927fdd81db4f1c49e8eaca49358cd 100644 --- a/alfa-client/libs/navigation/src/lib/build-info/build-info.component.html +++ b/alfa-client/libs/navigation/src/lib/build-info/build-info.component.html @@ -30,6 +30,6 @@ <span data-test-id="build-time">{{ buildTime }}</span> </ng-container> </p> -<p *ngIf="isNotProduction" data-test-id="not-production-text" class="test-environment"> +<p *ngIf="isNotProduction" data-test-id="not-production-text" class="test-environment text-error"> Achtung Testumgebung </p> diff --git a/alfa-client/libs/navigation/src/lib/build-info/build-info.component.scss b/alfa-client/libs/navigation/src/lib/build-info/build-info.component.scss index ec3f82e9c13c393371836db8fecfca6ecc6467d5..fbbb84ab523edbd1ed3fd2d158e9465a5df9342b 100644 --- a/alfa-client/libs/navigation/src/lib/build-info/build-info.component.scss +++ b/alfa-client/libs/navigation/src/lib/build-info/build-info.component.scss @@ -61,5 +61,4 @@ p { margin-right: $navigation-height + $header-height; letter-spacing: 0.42em; text-transform: uppercase; - color: #ff0000; } diff --git a/alfa-client/libs/navigation/src/lib/header-container/header/header.component.html b/alfa-client/libs/navigation/src/lib/header-container/header/header.component.html index 3ef7590196924bd350d246c4b084c71a1cc7cdd3..100cd3141dbcb8562608433313d8cdf30c0af56b 100644 --- a/alfa-client/libs/navigation/src/lib/header-container/header/header.component.html +++ b/alfa-client/libs/navigation/src/lib/header-container/header/header.component.html @@ -30,7 +30,7 @@ <div class="middle"> <alfa-vorgang-search-container></alfa-vorgang-search-container> </div> - <div class="right"> + <div class="flex items-center text-ozggray-800 dark:text-ozggray-300"> <alfa-help-menu [apiRootStateResource]="apiRootStateResource" data-test-id="help-menu" diff --git a/alfa-client/libs/navigation/src/lib/header-container/header/header.component.scss b/alfa-client/libs/navigation/src/lib/header-container/header/header.component.scss index 167169bea4acdf42c9032a84041bf48006b45809..4e6bde87b4b462a01803569bc823015ce6b9f535 100644 --- a/alfa-client/libs/navigation/src/lib/header-container/header/header.component.scss +++ b/alfa-client/libs/navigation/src/lib/header-container/header/header.component.scss @@ -53,9 +53,3 @@ header { min-width: 240px; } } - -.right { - color: $grey; - display: flex; - align-items: center; -} diff --git a/alfa-client/libs/postfach/src/lib/postfach-mail-list-container/postfach-mail-list/postfach-mail/outgoing-mail/outgoing-mail-error-container/outgoing-mail-error/outgoing-mail-error.component.html b/alfa-client/libs/postfach/src/lib/postfach-mail-list-container/postfach-mail-list/postfach-mail/outgoing-mail/outgoing-mail-error-container/outgoing-mail-error/outgoing-mail-error.component.html index ba6a2c55856a17a9780c64cbb785fd4ea54f96b8..ba91c347f14db1b8a045f6f1c19ee795f3618cb7 100644 --- a/alfa-client/libs/postfach/src/lib/postfach-mail-list-container/postfach-mail-list/postfach-mail/outgoing-mail/outgoing-mail-error-container/outgoing-mail-error/outgoing-mail-error.component.html +++ b/alfa-client/libs/postfach/src/lib/postfach-mail-list-container/postfach-mail-list/postfach-mail/outgoing-mail/outgoing-mail-error-container/outgoing-mail-error/outgoing-mail-error.component.html @@ -29,7 +29,7 @@ class="mail-send-error" > <span data-test-id="mail-send-error-text">{{ message }}</span> - <mat-icon data-test-id="mail-send-error-icon" class="mail-send-error__icon mat-icon-error" + <mat-icon data-test-id="mail-send-error-icon" class="mail-send-error__icon text-error" >error_outline_white</mat-icon > </div> diff --git a/alfa-client/libs/postfach/src/lib/postfach-mail-list-container/postfach-mail-list/postfach-mail/outgoing-mail/outgoing-mail-error-container/outgoing-mail-error/outgoing-mail-error.component.scss b/alfa-client/libs/postfach/src/lib/postfach-mail-list-container/postfach-mail-list/postfach-mail/outgoing-mail/outgoing-mail-error-container/outgoing-mail-error/outgoing-mail-error.component.scss index d62fb722598b2db40eaa946e2e79722616ecaf0d..aadb9a11f319d4c53e79987a439f2647db86c7f8 100644 --- a/alfa-client/libs/postfach/src/lib/postfach-mail-list-container/postfach-mail-list/postfach-mail/outgoing-mail/outgoing-mail-error-container/outgoing-mail-error/outgoing-mail-error.component.scss +++ b/alfa-client/libs/postfach/src/lib/postfach-mail-list-container/postfach-mail-list/postfach-mail/outgoing-mail/outgoing-mail-error-container/outgoing-mail-error/outgoing-mail-error.component.scss @@ -25,10 +25,6 @@ @use '@angular/material' as mat; @import 'variables'; -.mat-icon-error { - color: mat.get-color-from-palette($warnPalette); -} - .mail-send-error { display: flex; align-items: center; diff --git a/alfa-client/libs/tech-shared/src/index.ts b/alfa-client/libs/tech-shared/src/index.ts index 64e34b9ffcb66244bb7687070f685ed6090b4950..6447da0179658e13795f1dbe8bfa8f0df2adeda3 100644 --- a/alfa-client/libs/tech-shared/src/index.ts +++ b/alfa-client/libs/tech-shared/src/index.ts @@ -33,6 +33,7 @@ export * from './lib/message-code'; export * from './lib/ngrx/actions'; export * from './lib/pipe/convert-api-error-to-error-messages.pipe'; export * from './lib/pipe/convert-for-data-test.pipe'; +export * from './lib/pipe/convert-problem-detail-to-error-messages.pipe'; export * from './lib/pipe/convert-to-boolean.pipe'; export * from './lib/pipe/enum-to-label.pipe'; export * from './lib/pipe/file-size.pipe'; diff --git a/alfa-client/libs/tech-shared/src/lib/pipe/convert-problem-detail-to-error-messages.pipe.spec.ts b/alfa-client/libs/tech-shared/src/lib/pipe/convert-problem-detail-to-error-messages.pipe.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..ffef78f2195217763693fbd09abf727915afac23 --- /dev/null +++ b/alfa-client/libs/tech-shared/src/lib/pipe/convert-problem-detail-to-error-messages.pipe.spec.ts @@ -0,0 +1,42 @@ +import { createInvalidParam, createProblemDetail } from '../../../test/error'; +import { InvalidParam, ProblemDetail } from '../tech.model'; +import { ValidationMessageCode } from '../validation/tech.validation.messages'; +import * as TechValidationUtil from '../validation/tech.validation.util'; +import { ConvertProblemDetailToErrorMessagesPipe } from './convert-problem-detail-to-error-messages.pipe'; + +describe('convertProblemDetailToErrorMessages', () => { + const pipe = new ConvertProblemDetailToErrorMessagesPipe(); + + it('create an instance', () => { + expect(pipe).toBeTruthy(); + }); + + describe('transform', () => { + const getMessageForInvalidParam = jest.spyOn(TechValidationUtil, 'getMessageForInvalidParam'); + + it('should not call getMessageForInvalidParam', () => { + pipe.transform(null); + + expect(getMessageForInvalidParam).not.toHaveBeenCalled(); + }); + + it('should call getMessageForInvalidParam', () => { + pipe.transform(createProblemDetail()); + + expect(getMessageForInvalidParam).toHaveBeenCalled(); + }); + + it('should return array of error messages', () => { + const expectedErrorMessage = 'Bitte ausfüllen'; + const invalidParam: InvalidParam = { + ...createInvalidParam(), + reason: ValidationMessageCode.FIELD_EMPTY, + }; + const problemDetail: ProblemDetail = createProblemDetail([invalidParam]); + + const errorMessages: string[] = pipe.transform(problemDetail); + + expect(errorMessages).toEqual([expectedErrorMessage]); + }); + }); +}); diff --git a/alfa-client/libs/tech-shared/src/lib/pipe/convert-problem-detail-to-error-messages.pipe.ts b/alfa-client/libs/tech-shared/src/lib/pipe/convert-problem-detail-to-error-messages.pipe.ts new file mode 100644 index 0000000000000000000000000000000000000000..437483a923c7f85353b96a745a2c24e1cc88e0fb --- /dev/null +++ b/alfa-client/libs/tech-shared/src/lib/pipe/convert-problem-detail-to-error-messages.pipe.ts @@ -0,0 +1,17 @@ +import { Pipe, PipeTransform } from '@angular/core'; +import { isNil } from 'lodash-es'; +import { InvalidParam, ProblemDetail } from '../tech.model'; +import { EMPTY_STRING } from '../tech.util'; +import { getMessageForInvalidParam } from '../validation/tech.validation.util'; + +@Pipe({ name: 'convertProblemDetailToErrorMessages' }) +export class ConvertProblemDetailToErrorMessagesPipe implements PipeTransform { + transform(value: ProblemDetail) { + if (isNil(value)) { + return []; + } + return value.invalidParams.map((invalidParam: InvalidParam) => + getMessageForInvalidParam(EMPTY_STRING, invalidParam), + ); + } +} 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 1472e6fe3c880b33faeaf14e875f6973dde18a73..ee4a624cb890cbf0da636f80ae01f1aeb7fdc616 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 @@ -25,12 +25,12 @@ import { CommandResource } from '@alfa-client/command-shared'; import { UntypedFormBuilder, UntypedFormControl, UntypedFormGroup } from '@angular/forms'; import { Resource } from '@ngxp/rest'; import { cold } from 'jest-marbles'; -import { createApiError, createInvalidParam, createIssue, createProblemDetail } from 'libs/tech-shared/test/error'; +import { createInvalidParam, createProblemDetail } from 'libs/tech-shared/test/error'; import { Observable, of } from 'rxjs'; import { AbstractFormService } from './formservice.abstract'; import { createEmptyStateResource, createErrorStateResource, createStateResource, StateResource } from '../resource/resource.util'; -import { ApiError, HttpError, InvalidParam, Issue, ProblemDetail } from '../tech.model'; +import { HttpError, InvalidParam, ProblemDetail } from '../tech.model'; import { createCommandResource } from '../../../../command-shared/test/command'; import * as ValidationUtil from '../validation/tech.validation.util'; @@ -47,16 +47,16 @@ describe('AbstractFormService', () => { }); describe('submit', () => { - describe('with api error', () => { - const stateResourceWithError: StateResource<ApiError> = - createErrorStateResource(createApiError()); + describe('with ProblemDetail', () => { + const stateResourceWithError: StateResource<ProblemDetail> = + createErrorStateResource(createProblemDetail()); beforeEach(() => { TestFormService.SUBMIT_OBSERVABLE = () => of(stateResourceWithError); formService.handleResponse = jest.fn((stateResource) => stateResource); }); - it('should call handle response for api error', (done) => { + it('should call handle response for ProblemDetail', (done) => { formService.submit().subscribe(() => { expect(formService.handleResponse).toHaveBeenCalledWith(stateResourceWithError); done(); @@ -97,8 +97,8 @@ describe('AbstractFormService', () => { }); describe('handleResponse', () => { - const apiError: ApiError = createApiError(); - const stateResource: StateResource<CommandResource> = createErrorStateResource(apiError); + const problemDetail: ProblemDetail = createProblemDetail(); + const stateResource: StateResource<CommandResource> = createErrorStateResource(problemDetail); beforeEach(() => { formService.handleError = jest.fn(); @@ -107,7 +107,7 @@ describe('AbstractFormService', () => { it('should handleError on validation error', () => { formService.handleResponse({ ...stateResource, loading: false }); - expect(formService.handleError).toHaveBeenCalledWith(apiError); + expect(formService.handleError).toHaveBeenCalledWith(problemDetail); }); it('should return stateresource while loading', () => { @@ -129,34 +129,6 @@ describe('AbstractFormService', () => { expect(formService.setErrorByProblemDetail).toHaveBeenCalledWith(problemDetail); }); - - it('should set api error', () => { - formService.setErrorByApiError = jest.fn(); - const apiError: ApiError = createApiError(); - - formService.handleError(apiError); - - expect(formService.setErrorByApiError).toHaveBeenCalledWith(apiError); - }); - }); - - describe('set error by api error', () => { - const issue: Issue = createIssue(); - const apiError: ApiError = createApiError([issue]); - - 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', () => { 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 ab647437a8483fb73bccc4fa0e4d083ef8c48cde..1af38f9f8065ae4685acb7326d1a65b3d88fcd69 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 @@ -28,16 +28,16 @@ import { isNil } from 'lodash-es'; import { identity, Observable, OperatorFunction } from 'rxjs'; import { map } from 'rxjs/operators'; import { hasStateResourceError, StateResource } from '../resource/resource.util'; -import { ApiError, HttpError, InvalidParam, Issue, ProblemDetail } from '../tech.model'; +import { HttpError, InvalidParam, ProblemDetail } from '../tech.model'; import { isNotUndefined } from '../tech.util'; -import { setInvalidParamValidationError, setIssueValidationError } from '../validation/tech.validation.util'; +import { setInvalidParamValidationError } from '../validation/tech.validation.util'; export abstract class AbstractFormService { form: UntypedFormGroup; pathPrefix: string; source: any; - private readonly PROBLEM_DETAIL_INVALID_PARAMS_KEY: string = 'invalid-params'; + private readonly PROBLEM_DETAIL_INVALID_PARAMS_KEY: string = 'invalidParams'; constructor(public formBuilder: UntypedFormBuilder) { this.form = this.initForm(); @@ -65,28 +65,15 @@ export abstract class AbstractFormService { } handleError(error: HttpError): void { - if (this.isApiError(error)) { - this.setErrorByApiError(<ApiError>error); - } - if (this.isProblemDetail(error)) { + if (this.hasError(error)) { this.setErrorByProblemDetail(<ProblemDetail>error); } } - private isApiError(error: HttpError): boolean { - return isNotUndefined((<ApiError>error).issues); - } - - private isProblemDetail(error: HttpError): boolean { + private hasError(error: HttpError): boolean { return isNotUndefined((<ProblemDetail>error)[this.PROBLEM_DETAIL_INVALID_PARAMS_KEY]); } - setErrorByApiError(apiError: ApiError): void { - apiError.issues.forEach((issue: Issue) => - setIssueValidationError(this.form, issue, this.getPathPrefix()), - ); - } - setErrorByProblemDetail(error: ProblemDetail): void { error[this.PROBLEM_DETAIL_INVALID_PARAMS_KEY].forEach((invalidParam: InvalidParam) => { setInvalidParamValidationError(this.form, invalidParam, this.getPathPrefix()); diff --git a/alfa-client/libs/tech-shared/src/lib/tech-shared.module.ts b/alfa-client/libs/tech-shared/src/lib/tech-shared.module.ts index 331031b298487fcfa77df7c10041a33cfedec7bb..a91930ffa732c82e0be57db7f18516b24eb393c9 100644 --- a/alfa-client/libs/tech-shared/src/lib/tech-shared.module.ts +++ b/alfa-client/libs/tech-shared/src/lib/tech-shared.module.ts @@ -29,6 +29,7 @@ import { HttpXsrfInterceptor } from './interceptor/http-xsrf.interceptor'; import { XhrInterceptor } from './interceptor/xhr.interceptor'; import { ConvertApiErrorToErrorMessagesPipe } from './pipe/convert-api-error-to-error-messages.pipe'; import { ConvertForDataTestPipe } from './pipe/convert-for-data-test.pipe'; +import { ConvertProblemDetailToErrorMessagesPipe } from './pipe/convert-problem-detail-to-error-messages.pipe'; import { ConvertToBooleanPipe } from './pipe/convert-to-boolean.pipe'; import { EnumToLabelPipe } from './pipe/enum-to-label.pipe'; import { FileSizePlainPipe } from './pipe/file-size-plain.pipe'; @@ -69,6 +70,7 @@ import { ToTrafficLightPipe } from './pipe/to-traffic-light.pipe'; GetUrlPipe, ConvertToBooleanPipe, ConvertApiErrorToErrorMessagesPipe, + ConvertProblemDetailToErrorMessagesPipe, ], exports: [ FormatToPrettyDatePipe, @@ -90,6 +92,7 @@ import { ToTrafficLightPipe } from './pipe/to-traffic-light.pipe'; GetUrlPipe, ConvertToBooleanPipe, ConvertApiErrorToErrorMessagesPipe, + ConvertProblemDetailToErrorMessagesPipe, ], providers: [ { 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 c85852676e5184152aa17856cca07bd33b3a3223..869313d09ddd0b5c7703f80ea79f23bf72403e07 100644 --- a/alfa-client/libs/tech-shared/src/lib/tech.model.ts +++ b/alfa-client/libs/tech-shared/src/lib/tech.model.ts @@ -47,12 +47,19 @@ export interface ProblemDetail { status: HttpStatusCode; detail: string; instance: string; - 'invalid-params': InvalidParam[]; + invalidParams: InvalidParam[]; } export interface InvalidParam { + constraintParameters: ConstraintParameter[]; name: string; reason: ValidationMessageCode; + value: string; +} + +export interface ConstraintParameter { + name: string; + value: string; } export declare type HttpError = ProblemDetail | ApiError; diff --git a/alfa-client/libs/tech-shared/src/lib/validation/tech.validation.messages.ts b/alfa-client/libs/tech-shared/src/lib/validation/tech.validation.messages.ts index 6c71c22768bb5ae231daf6b3522002a9110bda00..82da103d936c9c009cdfb20a9b1e652cf5b41648 100644 --- a/alfa-client/libs/tech-shared/src/lib/validation/tech.validation.messages.ts +++ b/alfa-client/libs/tech-shared/src/lib/validation/tech.validation.messages.ts @@ -22,23 +22,31 @@ * unter der Lizenz sind dem Lizenztext zu entnehmen. */ export enum ValidationMessageCode { - VALIDATION_FIELD_FILE_SIZE_EXCEEDED = 'validation_field_file_size_exceeded', - VALIDATION_FIELD_EMPTY = 'validation_field_empty', - VALIDATION_FIELD_FILE_CONTENT_TYPE_INVALID = 'validation_field_file_content_type_invalid', + FIELD_FILE_SIZE_EXCEEDED = 'validation_field_file_size_exceeded', + FIELD_EMPTY = 'validation_field_empty', + FIELD_SIZE = 'validation_field_size', + FIELD_FILE_CONTENT_TYPE_INVALID = 'validation_field_file_content_type_invalid', + FIELD_MIN_SIZE = 'validation_field_min_size', + FIELD_MAX_SIZE = 'validation_field_max_size', + FIELD_DATE_PAST = 'validation_field_date_past', + FIELD_INVALID = 'validation_field_invalid', + FIELD_DATE_FORMAT_INVALID = 'validation_field_date_format_invalid', + FIELD_ASSIGN_BEARBEITER_NOT_EXIST = 'fe_only_validation_bearbeiter_not_exist', } export const VALIDATION_MESSAGES: { [code: string]: string } = { - [ValidationMessageCode.VALIDATION_FIELD_EMPTY]: 'Bitte {field} ausfüllen', - validation_field_max_size: '{field} darf höchstens {max} Zeichen enthalten', - validation_field_min_size: '{field} muss aus mindestens {min} Zeichen bestehen', - validation_field_size: '{field} muss mindestens {min} und darf höchstens {max} Zeichen enthalten', - validation_field_date_past: 'Das Datum für {field} muss in der Zukunft liegen', - validation_field_invalid: 'Bitte {field} korrekt ausfüllen', - [ValidationMessageCode.VALIDATION_FIELD_FILE_SIZE_EXCEEDED]: + [ValidationMessageCode.FIELD_EMPTY]: 'Bitte {field} ausfüllen', + [ValidationMessageCode.FIELD_MAX_SIZE]: '{field} darf höchstens {max} Zeichen enthalten', + [ValidationMessageCode.FIELD_MIN_SIZE]: '{field} muss aus mindestens {min} Zeichen bestehen', + [ValidationMessageCode.FIELD_SIZE]: + '{field} muss mindestens {min} und darf höchstens {max} Zeichen enthalten', + [ValidationMessageCode.FIELD_DATE_PAST]: 'Das Datum für {field} muss in der Zukunft liegen', + [ValidationMessageCode.FIELD_INVALID]: 'Bitte {field} korrekt ausfüllen', + [ValidationMessageCode.FIELD_FILE_SIZE_EXCEEDED]: 'Anhänge größer {max}{unit} können nicht hinzugefügt werden.', fe_only_validation_bearbeiter_not_exist: 'Der Bearbeiter existiert nicht', - validation_field_date_format_invalid: 'Geben Sie ein gültiges Datum ein', - [ValidationMessageCode.VALIDATION_FIELD_FILE_CONTENT_TYPE_INVALID]: + [ValidationMessageCode.FIELD_DATE_FORMAT_INVALID]: 'Geben Sie ein gültiges Datum ein', + [ValidationMessageCode.FIELD_FILE_CONTENT_TYPE_INVALID]: 'Erlaubte Dateiendungen: pdf, jpg, png, jpeg', }; 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 9e0902270b2423a9510872b1a5f15828549eb8d6..bb2e5b51d1be004bced187df35871e16a00d738a 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,23 +21,12 @@ * Die sprachspezifischen Genehmigungen und Beschränkungen * unter der Lizenz sind dem Lizenztext zu entnehmen. */ -import { - AbstractControl, - FormControl, - FormGroup, - UntypedFormControl, - UntypedFormGroup, -} from '@angular/forms'; -import { createInvalidParam, createIssue } from '../../../test/error'; +import { AbstractControl, FormControl, FormGroup, UntypedFormControl, UntypedFormGroup } from '@angular/forms'; +import { faker } from '@faker-js/faker'; +import { createInvalidParam, createIssue, createProblemDetail } from '../../../test/error'; import { InvalidParam, Issue } from '../tech.model'; -import { - getControlForInvalidParam, - getControlForIssue, - getMessageForInvalidParam, - getMessageForIssue, - setInvalidParamValidationError, - setIssueValidationError, -} from './tech.validation.util'; +import { VALIDATION_MESSAGES, ValidationMessageCode } from './tech.validation.messages'; +import { getControlForInvalidParam, getControlForIssue, getFieldPath, getMessageForInvalidParam, getMessageForIssue, getMessageReason, setInvalidParamValidationError, setIssueValidationError } from './tech.validation.util'; describe('ValidationUtils', () => { const baseField1Control: FormControl = new UntypedFormControl(); @@ -55,7 +44,7 @@ describe('ValidationUtils', () => { describe('set issue validation error', () => { describe('get control for issue', () => { it('should return base field control', () => { - const issue: Issue = { ...createIssue(), field: 'baseField1' }; + const issue: Issue = { ...createIssue(), field: 'class.resource.baseField1' }; const control: AbstractControl = getControlForIssue(form, issue); @@ -63,24 +52,24 @@ describe('ValidationUtils', () => { }); it('should return sub group field', () => { - const issue: Issue = { ...createIssue(), field: 'subGroup.subGroupField1' }; + const issue: Issue = { ...createIssue(), field: 'class.resource.subGroup.subGroupField1' }; - const control: AbstractControl = getControlForIssue(form, issue); + const control: AbstractControl = getControlForIssue(form, issue, 'resource'); expect(control).toBe(subGroupFieldControl); }); it('should ignore path prefix', () => { - const issue: Issue = { ...createIssue(), field: 'pathprefix.resource.baseField1' }; + const issue: Issue = { ...createIssue(), field: 'class.resource.baseField1' }; - const control: AbstractControl = getControlForIssue(form, issue, 'pathprefix.resource'); + const control: AbstractControl = getControlForIssue(form, issue, 'resource'); expect(control).toBe(baseField1Control); }); }); describe('in base field', () => { - const issue: Issue = { ...createIssue(), field: 'baseField1' }; + const issue: Issue = { ...createIssue(), field: 'class.resource.baseField1' }; it('should set error in control', () => { setIssueValidationError(form, issue); @@ -108,10 +97,10 @@ describe('ValidationUtils', () => { }); describe('in subGroup Field', () => { - const issue: Issue = { ...createIssue(), field: 'subGroup.subGroupField1' }; + const issue: Issue = { ...createIssue(), field: 'class.resource.subGroup.subGroupField1' }; it('should set error in control', () => { - setIssueValidationError(form, issue); + setIssueValidationError(form, issue, 'resource'); expect(subGroupFieldControl.errors).not.toBeNull(); }); @@ -150,102 +139,200 @@ describe('ValidationUtils', () => { }); }); - describe('invalid param', () => { - const formPrefixes: string[] = ['', 'some-prefix']; - const fieldNames: string[] = ['baseField1', 'baseField2', 'subGroup.subGroupField1']; - const prefixNameCombinations: string[][] = formPrefixes.flatMap((prefix) => - fieldNames.map((name) => [prefix, name]), - ); - const unknownName = 'unknown-field'; - - describe.each(prefixNameCombinations)( - 'with prefix "%s" and fieldName "%s"', - (prefix, fieldName) => { - let invalidParam: InvalidParam; - - beforeEach(() => { - form.reset(); - invalidParam = { - ...createInvalidParam(), - name: prefix.length ? `${prefix}.${fieldName}` : fieldName, - }; - }); - - describe('get message for invalid param', () => { - it('should return', () => { - const msg: string = getMessageForInvalidParam(invalidParam, prefix); - - expect(msg).toEqual(`Bitte ${fieldName} ausfüllen`); - }); - }); - - describe('get control for invalid param', () => { - it('should find', () => { - const control: AbstractControl = getControlForInvalidParam(form, invalidParam, prefix); - - expect(control).toBeTruthy(); - }); - }); - - describe('set invalid param validation error', () => { - it('should assign invalidParam to form control error without prefix', () => { - const message: string = getMessageForInvalidParam(invalidParam, prefix); - - setInvalidParamValidationError(form, invalidParam, prefix); - - const errorMessage: string = form.getError(invalidParam.reason, fieldName); - expect(errorMessage).toBe(message); - }); - - it('should mark form as touched', () => { - setInvalidParamValidationError(form, invalidParam, prefix); - - expect(form.touched).toBeTruthy(); - }); - }); - }, - ); - - describe.each([ - ['', '', 'unknown-field'], - ['valid-prefix', 'valid-prefix', 'unknown-field'], - ['valid-prefix', 'valid-prefix', 'subGroup.unknown-field'], - ['unknown-prefix', 'valid-prefix', 'unknown-field'], - ['unknown-prefix', 'valid-prefix', 'baseField1'], - ])( - 'with pathPrefix "%s", paramPrefix "%s", and field-name "%s"', - (pathPrefix, paramPrefix, fieldName) => { - let invalidParam: InvalidParam; - - beforeEach(() => { - form.reset(); - invalidParam = createInvalidParam(); - invalidParam.name = paramPrefix.length > 0 ? `${paramPrefix}.${fieldName}` : fieldName; - }); - - it('should not find form control', () => { - const control: AbstractControl = getControlForInvalidParam( - form, - invalidParam, - pathPrefix, - ); - - expect(control).toBeFalsy(); - }); - - it('should not assign to field control error', () => { - setInvalidParamValidationError(form, invalidParam, pathPrefix); - - const errorMessage = form.getError(invalidParam.reason, unknownName); - expect(errorMessage).toBeFalsy(); - }); - - it('should not mark as touched', () => { - setInvalidParamValidationError(form, invalidParam, pathPrefix); - - expect(form.touched).toBeFalsy(); - }); - }, - ); + describe('set invalidParam validation error', () => { + describe('get control for invalidParam', () => { + it('should return base field control', () => { + const invalidParam: InvalidParam = { + ...createInvalidParam(), + name: 'class.resource.baseField1', + }; + + const control: AbstractControl = getControlForInvalidParam(form, invalidParam); + + expect(control).toBe(baseField1Control); + }); + + it('should return sub group field', () => { + const invalidParam: InvalidParam = { + ...createInvalidParam(), + name: 'class.resource.subGroup.subGroupField1', + }; + + const control: AbstractControl = getControlForInvalidParam(form, invalidParam, 'resource'); + + expect(control).toBe(subGroupFieldControl); + }); + + it('should ignore path prefix', () => { + const invalidParam: InvalidParam = { + ...createInvalidParam(), + name: 'class.resource.baseField1', + }; + + const control: AbstractControl = getControlForInvalidParam(form, invalidParam, 'resource'); + + expect(control).toBe(baseField1Control); + }); + }); + + describe('in base field', () => { + const invalidParam: InvalidParam = { + ...createInvalidParam(), + name: 'class.resource.baseField1', + }; + + it('should set error in control', () => { + setInvalidParamValidationError(form, invalidParam); + + expect(baseField1Control.errors).not.toBeNull(); + }); + + it('should set message code in control', () => { + setInvalidParamValidationError(form, invalidParam); + + expect(baseField1Control.hasError(invalidParam.reason)).toBe(true); + }); + + it('should set control touched', () => { + setInvalidParamValidationError(form, invalidParam); + + expect(baseField1Control.touched).toBe(true); + }); + + it('should not set error in other control', () => { + setInvalidParamValidationError(form, invalidParam); + + expect(baseField2Control.errors).toBeNull(); + }); + }); + + describe('in subGroup Field', () => { + const invalidParam: InvalidParam = { + ...createInvalidParam(), + name: 'class.resource.subGroup.subGroupField1', + }; + + it('should set error in control', () => { + setInvalidParamValidationError(form, invalidParam, 'resource'); + + expect(subGroupFieldControl.errors).not.toBeNull(); + }); + }); + }); + + describe('getFieldPath', () => { + const resource: string = 'resource'; + const backendClassName: string = 'class'; + + it('should return field path', () => { + const fieldPath: string = 'field1'; + const fullPath: string = `${backendClassName}.${resource}.${fieldPath}`; + + const result: string = getFieldPath(fullPath, resource); + + expect(result).toBe(fieldPath); + }); + + it('should get all parts after the prefix', () => { + const fieldPath: string = 'group.field1'; + const fullPath: string = `${backendClassName}.${resource}.${fieldPath}`; + + const result: string = getFieldPath(fullPath, resource); + + expect(result).toBe(fieldPath); + }); + + it('should return field from full path when resource is undefined', () => { + const fieldPath: string = 'field1'; + const fullPath: string = `${backendClassName}.${resource}.${fieldPath}`; + + const result: string = getFieldPath(fullPath, undefined); + + expect(result).toBe(fieldPath); + }); + + it('should return field from field when resource is undefined', () => { + const fieldPath: string = 'field1'; + + const result: string = getFieldPath(fieldPath, undefined); + + expect(result).toBe(fieldPath); + }); + }); + + describe('getMessageReason', () => { + it('should return reason', () => { + const problemDetail = createProblemDetail(); + + const reason: ValidationMessageCode = getMessageReason(problemDetail); + + expect(reason).toEqual(problemDetail.invalidParams[0].reason); + }); + + it('should return null', () => { + const problemDetail = createProblemDetail([{ ...createInvalidParam(), reason: null }]); + + const reason: ValidationMessageCode = getMessageReason(problemDetail); + + expect(reason).toBeNull(); + }); + }); + + describe('getMessageForInvalidParam', () => { + const label: string = faker.random.word(); + + it('should return undefined reason', () => { + const invalidParam: InvalidParam = createInvalidParam(); + + const message: string = getMessageForInvalidParam(label, { + ...invalidParam, + reason: undefined, + }); + + expect(message).toBeUndefined(); + }); + + it('should return message', () => { + const invalidParam: InvalidParam = createInvalidParam(); + + const message: string = getMessageForInvalidParam(label, { + ...invalidParam, + reason: ValidationMessageCode.FIELD_DATE_FORMAT_INVALID, + }); + expect(message).toEqual(VALIDATION_MESSAGES[ValidationMessageCode.FIELD_DATE_FORMAT_INVALID]); + }); + + it('should return message with field placeholder', () => { + const invalidParam: InvalidParam = createInvalidParam(); + + const message: string = getMessageForInvalidParam(label, { + ...invalidParam, + reason: ValidationMessageCode.FIELD_INVALID, + }); + expect(message).toEqual( + VALIDATION_MESSAGES[ValidationMessageCode.FIELD_INVALID].replace('{field}', label), + ); + }); + + it('should return message with placeholders', () => { + const invalidParam: InvalidParam = createInvalidParam(); + const min: string = '1'; + const max: string = '5'; + + const message: string = getMessageForInvalidParam(label, { + ...invalidParam, + reason: ValidationMessageCode.FIELD_SIZE, + constraintParameters: [ + { name: 'min', value: min }, + { name: 'max', value: max }, + ], + }); + expect(message).toEqual( + VALIDATION_MESSAGES[ValidationMessageCode.FIELD_SIZE] + .replace('{field}', label) + .replace('{min}', min) + .replace('{max}', max), + ); + }); }); }); 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 4a34005de2f231027678dc6ce606a8cc2e3e1bed..d562a4543baec28671348cd0ef81402c055b154d 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 @@ -22,9 +22,9 @@ * unter der Lizenz sind dem Lizenztext zu entnehmen. */ import { AbstractControl, UntypedFormGroup } from '@angular/forms'; -import { isNil } from 'lodash-es'; -import { ApiError, InvalidParam, Issue, IssueParam } from '../tech.model'; -import { isNotNil, replacePlaceholder } from '../tech.util'; +import { isEmpty, isNil } from 'lodash-es'; +import { ApiError, InvalidParam, Issue, IssueParam, ProblemDetail } from '../tech.model'; +import { replacePlaceholder } from '../tech.util'; import { VALIDATION_MESSAGES, ValidationMessageCode } from './tech.validation.messages'; export function isValidationError(issue: Issue): boolean { @@ -47,7 +47,7 @@ export function getControlForIssue( issue: Issue, pathPrefix?: string, ): AbstractControl { - const fieldPath: string = getFieldPathWithoutPrefix(issue.field, pathPrefix); + const fieldPath: string = getFieldPath(issue.field, pathPrefix); let curControl: AbstractControl = form; fieldPath @@ -72,8 +72,12 @@ export function getMessageForIssue(label: string, issue: Issue): string { return msg; } -export function isValidationFieldFileSizeExceedError(error: any) { - return getMessageCode(error) === ValidationMessageCode.VALIDATION_FIELD_FILE_SIZE_EXCEEDED; +export function isValidationFieldFileSizeExceedError(error: ProblemDetail): boolean { + return getMessageReason(error) === ValidationMessageCode.FIELD_FILE_SIZE_EXCEEDED; +} + +export function getMessageReason(problemDetail: ProblemDetail): ValidationMessageCode | null { + return problemDetail.invalidParams[0].reason ?? null; } export function getMessageCode(apiError: ApiError): string { @@ -86,12 +90,9 @@ export function setInvalidParamValidationError( pathPrefix?: string, ): void { const control: AbstractControl = getControlForInvalidParam(form, invalidParam, pathPrefix); - if (isNotNil(control)) { - control.setErrors({ - [invalidParam.reason]: getMessageForInvalidParam(invalidParam, pathPrefix), - }); - control.markAsTouched(); - } + + control.setErrors({ [invalidParam.reason]: invalidParam }); + control.markAsTouched(); } export function getControlForInvalidParam( @@ -99,17 +100,29 @@ export function getControlForInvalidParam( invalidParam: InvalidParam, pathPrefix?: string, ): AbstractControl { - return form.get(getFieldPathWithoutPrefix(invalidParam.name, pathPrefix)); + return form.get(getFieldPath(invalidParam.name, pathPrefix)); } -export function getMessageForInvalidParam(item: InvalidParam, pathPrefix: string): string { - return replacePlaceholder( - VALIDATION_MESSAGES[item.reason], - 'field', - getFieldPathWithoutPrefix(item.name, pathPrefix), +export function getMessageForInvalidParam(label: string, invalidParam: InvalidParam): string { + let msg: string = VALIDATION_MESSAGES[invalidParam.reason]; + + if (isNil(msg)) { + console.warn('No message for code ' + invalidParam.reason + ' found.'); + return invalidParam.reason; + } + + msg = replacePlaceholder(msg, 'field', label); + invalidParam.constraintParameters.forEach( + (param: IssueParam) => (msg = replacePlaceholder(msg, param.name, param.value)), ); + return msg; } -function getFieldPathWithoutPrefix(name: string, pathPrefix?: string): string { - return pathPrefix ? name.substring(pathPrefix.length + 1) : name; +export function getFieldPath(name: string, pathPrefix: string): string { + if (isEmpty(pathPrefix)) { + return name.split('.').pop(); + } + + const indexOfField = name.lastIndexOf(pathPrefix) + pathPrefix.length + 1; + return name.slice(indexOfField); } diff --git a/alfa-client/libs/tech-shared/test/error.ts b/alfa-client/libs/tech-shared/test/error.ts index 9803a5a5ad90888ceb7125bc535f5c379548d126..3f80af06d43d07792780eaf023c378bcfd697345 100644 --- a/alfa-client/libs/tech-shared/test/error.ts +++ b/alfa-client/libs/tech-shared/test/error.ts @@ -23,7 +23,14 @@ */ import { HttpErrorResponse, HttpStatusCode } from '@angular/common/http'; import { faker } from '@faker-js/faker'; -import { ApiError, InvalidParam, Issue, IssueParam, ProblemDetail } from '../src/lib/tech.model'; +import { + ApiError, + ConstraintParameter, + InvalidParam, + Issue, + IssueParam, + ProblemDetail, +} from '../src/lib/tech.model'; import { ValidationMessageCode } from '../src/lib/validation/tech.validation.messages'; export function createIssueParam(): IssueParam { @@ -63,10 +70,22 @@ export function createProblemDetail( type: faker.random.word(), instance: faker.internet.url(), detail: faker.random.word(), - 'invalid-params': invalidParams, + invalidParams: invalidParams, }; } export function createInvalidParam(): InvalidParam { - return { name: faker.random.word(), reason: ValidationMessageCode.VALIDATION_FIELD_EMPTY }; + return { + name: faker.random.word(), + reason: ValidationMessageCode.FIELD_EMPTY, + value: faker.random.words(10), + constraintParameters: [createInvalidParamConstraintParameter()], + }; +} + +export function createInvalidParamConstraintParameter(): ConstraintParameter { + return { + name: faker.random.word(), + value: faker.random.word(), + }; } diff --git a/alfa-client/libs/ui/src/lib/assets/update.svg b/alfa-client/libs/ui/src/lib/assets/update.svg index 1eb6a0751c7673b2de4a52c06aba58f191f32570..64c8cf90dc1a17b7183fd9dc03a66effd3273449 100644 --- a/alfa-client/libs/ui/src/lib/assets/update.svg +++ b/alfa-client/libs/ui/src/lib/assets/update.svg @@ -1 +1,4 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#5f6368"><path d="M480-120q-75 0-140.5-28.5t-114-77q-48.5-48.5-77-114T120-480q0-75 28.5-140.5t77-114q48.5-48.5 114-77T480-840q82 0 155.5 35T760-706v-94h80v240H600v-80h110q-41-56-101-88t-129-32q-117 0-198.5 81.5T200-480q0 117 81.5 198.5T480-200q105 0 183.5-68T756-440h82q-15 137-117.5 228.5T480-120Zm112-192L440-464v-216h80v184l128 128-56 56Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="inherit"> + <path + d="M480-120q-75 0-140.5-28.5t-114-77q-48.5-48.5-77-114T120-480q0-75 28.5-140.5t77-114q48.5-48.5 114-77T480-840q82 0 155.5 35T760-706v-94h80v240H600v-80h110q-41-56-101-88t-129-32q-117 0-198.5 81.5T200-480q0 117 81.5 198.5T480-200q105 0 183.5-68T756-440h82q-15 137-117.5 228.5T480-120Zm112-192L440-464v-216h80v184l128 128-56 56Z" /> +</svg> \ No newline at end of file diff --git a/alfa-client/libs/ui/src/lib/ui/editor/autocomplete-editor/autocomplete-editor.component.html b/alfa-client/libs/ui/src/lib/ui/editor/autocomplete-editor/autocomplete-editor.component.html index 6f484f807eca85ab9ccc7bc42c43c12c9bdc850f..8a0fb969d94a66f00007edfff7f6f84f55edd969 100644 --- a/alfa-client/libs/ui/src/lib/ui/editor/autocomplete-editor/autocomplete-editor.component.html +++ b/alfa-client/libs/ui/src/lib/ui/editor/autocomplete-editor/autocomplete-editor.component.html @@ -54,7 +54,7 @@ <mat-error> <ozgcloud-validation-error [attr.data-test-id]="(label | convertForDataTest) + '-autocomplete-error'" - [issues]="issues" + [invalidParams]="invalidParams" [label]="label" > </ozgcloud-validation-error> diff --git a/alfa-client/libs/ui/src/lib/ui/editor/date-editor/date-editor.component.html b/alfa-client/libs/ui/src/lib/ui/editor/date-editor/date-editor.component.html index 589e1679db0ad9e505aa9b98a1c981b366e5abfb..07ec3ad16554c885825d55b1f14139a7e05a246f 100644 --- a/alfa-client/libs/ui/src/lib/ui/editor/date-editor/date-editor.component.html +++ b/alfa-client/libs/ui/src/lib/ui/editor/date-editor/date-editor.component.html @@ -43,7 +43,7 @@ <mat-error> <ozgcloud-validation-error [attr.data-test-id]="(label | convertForDataTest) + '-date-error'" - [issues]="issues" + [invalidParams]="invalidParams" [label]="label" ></ozgcloud-validation-error> </mat-error> diff --git a/alfa-client/libs/ui/src/lib/ui/editor/file-upload-editor/file-upload-editor.component.html b/alfa-client/libs/ui/src/lib/ui/editor/file-upload-editor/file-upload-editor.component.html index a6f83cf4e2fa94e8a84d5ee0aebbfd9fa1efd8d1..94acabf0cf558593f633a252fb6bdb35f07f5fd0 100644 --- a/alfa-client/libs/ui/src/lib/ui/editor/file-upload-editor/file-upload-editor.component.html +++ b/alfa-client/libs/ui/src/lib/ui/editor/file-upload-editor/file-upload-editor.component.html @@ -48,7 +48,7 @@ <mat-error> <ozgcloud-validation-error [attr.data-test-id]="(label | convertForDataTest) + '-file-upload-error'" - [issues]="issues" + [invalidParams]="invalidParams" [label]="label" > </ozgcloud-validation-error> diff --git a/alfa-client/libs/ui/src/lib/ui/editor/text-editor/text-editor.component.html b/alfa-client/libs/ui/src/lib/ui/editor/text-editor/text-editor.component.html index bcf6e536c27a4a13aab0a99e43c2c155920bc241..8a8e01260fd7b5cb4af14320ea4af0d264b9a980 100644 --- a/alfa-client/libs/ui/src/lib/ui/editor/text-editor/text-editor.component.html +++ b/alfa-client/libs/ui/src/lib/ui/editor/text-editor/text-editor.component.html @@ -58,7 +58,7 @@ <mat-error> <ozgcloud-validation-error [attr.data-test-id]="(getPlaceholderLabel() | convertForDataTest) + '-text-error'" - [issues]="issues" + [invalidParams]="invalidParams" [label]="getPlaceholderLabel()" ></ozgcloud-validation-error> </mat-error> diff --git a/alfa-client/libs/ui/src/lib/ui/editor/textarea-editor/textarea-editor.component.html b/alfa-client/libs/ui/src/lib/ui/editor/textarea-editor/textarea-editor.component.html index cf3135707e7a986230e2dc9957326bb1766a6a88..14034ce169db28951cd062e19343976a81ca8d7b 100644 --- a/alfa-client/libs/ui/src/lib/ui/editor/textarea-editor/textarea-editor.component.html +++ b/alfa-client/libs/ui/src/lib/ui/editor/textarea-editor/textarea-editor.component.html @@ -39,8 +39,8 @@ <mat-error> <ozgcloud-validation-error - [issues]="issues" [label]="label" + [invalidParams]="invalidParams" [attr.data-test-id]="(label | convertForDataTest) + '-textarea-error'" > </ozgcloud-validation-error> diff --git a/alfa-client/libs/ui/src/lib/ui/validation-error/validation-error.component.html b/alfa-client/libs/ui/src/lib/ui/validation-error/validation-error.component.html index 13f1ef45c46566b76aad56247845108a014310e6..2c47c8ccdafa3929335cd41715394e7c2e11357d 100644 --- a/alfa-client/libs/ui/src/lib/ui/validation-error/validation-error.component.html +++ b/alfa-client/libs/ui/src/lib/ui/validation-error/validation-error.component.html @@ -23,4 +23,4 @@ unter der Lizenz sind dem Lizenztext zu entnehmen. --> -<span *ngFor="let issue of issues">{{ message(issue) }}</span> +<span *ngFor="let invalidParam of invalidParams">{{ message(invalidParam) }}</span> diff --git a/alfa-client/libs/ui/src/lib/ui/validation-error/validation-error.component.spec.ts b/alfa-client/libs/ui/src/lib/ui/validation-error/validation-error.component.spec.ts index 299af463232908e4f3edec96f97f7fd912379b0e..e486895e359ed8cbb702456e19b5f88530683e48 100644 --- a/alfa-client/libs/ui/src/lib/ui/validation-error/validation-error.component.spec.ts +++ b/alfa-client/libs/ui/src/lib/ui/validation-error/validation-error.component.spec.ts @@ -21,8 +21,10 @@ * Die sprachspezifischen Genehmigungen und Beschränkungen * unter der Lizenz sind dem Lizenztext zu entnehmen. */ +import { InvalidParam, getMessageForInvalidParam } from '@alfa-client/tech-shared'; import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { createIssue } from 'libs/tech-shared/test/error'; +import { ValidationMessageCode } from 'libs/tech-shared/src/lib/validation/tech.validation.messages'; +import { createInvalidParam } from 'libs/tech-shared/test/error'; import { ValidationErrorComponent } from './validation-error.component'; describe('ValidationErrorComponent', () => { @@ -45,9 +47,32 @@ describe('ValidationErrorComponent', () => { expect(component).toBeTruthy(); }); - it('should get message', () => { - var msg = component.message({ ...createIssue(), messageCode: 'validation_field_size' }); + describe('get message from invalidParam', () => { + const fieldLabel: string = 'Field Label'; + const invalidParam: InvalidParam = { + ...createInvalidParam(), + reason: ValidationMessageCode.FIELD_SIZE, + }; - expect(msg).not.toHaveLength(0); + it('should contain ', () => { + const msg: string = getMessageForInvalidParam(fieldLabel, invalidParam); + + expect(msg).toContain('muss mindestens'); + }); + + it('should set field label', () => { + const msg: string = getMessageForInvalidParam(fieldLabel, invalidParam); + + expect(msg).toContain(fieldLabel); + }); + + it('should replace min param', () => { + const msg: string = getMessageForInvalidParam(fieldLabel, { + ...invalidParam, + constraintParameters: [{ name: 'min', value: '3' }], + }); + + expect(msg).toContain('3'); + }); }); }); 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 0432ac003dae9960503559194148fb5d79f52c45..1c5e6df544c2a3d480dd40d6915b72ef166c7273 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 @@ -21,8 +21,8 @@ * Die sprachspezifischen Genehmigungen und Beschränkungen * unter der Lizenz sind dem Lizenztext zu entnehmen. */ +import { InvalidParam, getMessageForInvalidParam } from '@alfa-client/tech-shared'; import { Component, Input } from '@angular/core'; -import { getMessageForIssue, Issue } from '@alfa-client/tech-shared'; @Component({ selector: 'ozgcloud-validation-error', @@ -31,9 +31,9 @@ import { getMessageForIssue, Issue } from '@alfa-client/tech-shared'; }) export class ValidationErrorComponent { @Input() label: string; - @Input() issues: Issue[]; + @Input() invalidParams: InvalidParam[]; - public message(issue: Issue): string { - return getMessageForIssue(this.label, issue); + public message(invalidParam: InvalidParam): string { + return getMessageForInvalidParam(this.label, invalidParam); } } diff --git a/alfa-client/libs/user-assistance/src/lib/help-menu/help-menu.component.html b/alfa-client/libs/user-assistance/src/lib/help-menu/help-menu.component.html index 2b0db504b3f464f1624afcbceb95df15cfb6e3e0..8c00cbfc6abd899bf3da59fc774efc61a8c19755 100644 --- a/alfa-client/libs/user-assistance/src/lib/help-menu/help-menu.component.html +++ b/alfa-client/libs/user-assistance/src/lib/help-menu/help-menu.component.html @@ -5,9 +5,9 @@ [matMenuTriggerFor]="helpMenu.matMenu" data-test-id="help-menu-button" > - <div class="help-menu"> - <ozgcloud-icon icon="help_outline"></ozgcloud-icon> - <div class="text">Hilfe</div> + <div class="flex items-center text-ozggray-800 dark:text-ozggray-300"> + <ozgcloud-icon class="mr-1" icon="help_outline"></ozgcloud-icon> + <div>Hilfe</div> </div> </button> <ozgcloud-menu #helpMenu> diff --git a/alfa-client/libs/user-assistance/src/lib/help-menu/help-menu.component.scss b/alfa-client/libs/user-assistance/src/lib/help-menu/help-menu.component.scss deleted file mode 100644 index 99cbeef04c23dba9c76fa256dbdb4764f5a1e2b4..0000000000000000000000000000000000000000 --- a/alfa-client/libs/user-assistance/src/lib/help-menu/help-menu.component.scss +++ /dev/null @@ -1,11 +0,0 @@ -@import 'variables'; - -.help-menu { - display: flex; - align-items: center; - color: $grey; -} - -ozgcloud-icon { - margin-right: 4px; -} diff --git a/alfa-client/libs/user-assistance/src/lib/help-menu/help-menu.component.ts b/alfa-client/libs/user-assistance/src/lib/help-menu/help-menu.component.ts index 602c0ee4effc7366a69024c531427d7ff5410ae9..5c7c59840a98bac1696c1a1b3a5b5de3c9b14618 100644 --- a/alfa-client/libs/user-assistance/src/lib/help-menu/help-menu.component.ts +++ b/alfa-client/libs/user-assistance/src/lib/help-menu/help-menu.component.ts @@ -1,12 +1,11 @@ -import { Component, Input } from '@angular/core'; import { ApiRootLinkRel, ApiRootResource } from '@alfa-client/api-root-shared'; import { StateResource } from '@alfa-client/tech-shared'; +import { Component, Input } from '@angular/core'; import { hasLink } from '@ngxp/rest'; @Component({ selector: 'alfa-help-menu', templateUrl: './help-menu.component.html', - styleUrls: ['./help-menu.component.scss'], }) export class HelpMenuComponent { @Input() apiRootStateResource: StateResource<ApiRootResource>; 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 4ec86ff50492e343fb1fe25fe2936d8cab1e8e9a..7293348a84e515fc1ae97109279f3ea8e49375d4 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 @@ -21,12 +21,13 @@ * Die sprachspezifischen Genehmigungen und Beschränkungen * unter der Lizenz sind dem Lizenztext zu entnehmen. */ +import { AbstractFormService, ProblemDetail, StateResource } from '@alfa-client/tech-shared'; +import { UserProfileListResource, UserProfileService } from '@alfa-client/user-profile-shared'; import { Injectable, OnDestroy } from '@angular/core'; import { UntypedFormBuilder, UntypedFormControl, UntypedFormGroup } from '@angular/forms'; -import { AbstractFormService, StateResource } from '@alfa-client/tech-shared'; -import { UserProfileListResource, UserProfileService } from '@alfa-client/user-profile-shared'; import { isNil } from 'lodash-es'; import { Observable, Subscription } from 'rxjs'; +import { ValidationMessageCode } from '../../../../../tech-shared/src/lib/validation/tech.validation.messages'; @Injectable() export class UserProfileSearchFormService extends AbstractFormService implements OnDestroy { @@ -62,11 +63,11 @@ export class UserProfileSearchFormService extends AbstractFormService implements } public setEmptyUserProfileError(): void { - this.setErrorByApiError(emptyUserProfileError); + this.setErrorByProblemDetail(emptyUserProfileError); } public setNoUserProfileFoundError(): void { - this.setErrorByApiError(noUserProfileFoundError); + this.setErrorByProblemDetail(noUserProfileFoundError); } ngOnDestroy(): void { @@ -74,23 +75,34 @@ export class UserProfileSearchFormService extends AbstractFormService implements } } -const noUserProfileFoundError = { - issues: [ +const noUserProfileFoundError: ProblemDetail = { + type: null, + title: null, + status: null, + detail: null, + instance: null, + invalidParams: [ { - field: 'only.fe.searchBy', - message: 'fe_only_validation_bearbeiter_not_exist', - messageCode: 'fe_only_validation_bearbeiter_not_exist', - parameters: [], + name: 'only.fe.searchBy', + reason: ValidationMessageCode.FIELD_ASSIGN_BEARBEITER_NOT_EXIST, + constraintParameters: [], + value: null, }, ], }; -const emptyUserProfileError = { - issues: [ + +const emptyUserProfileError: ProblemDetail = { + type: null, + title: null, + status: null, + detail: null, + instance: null, + invalidParams: [ { - field: 'only.fe.searchBy', - message: 'validation_field_empty', - messageCode: 'validation_field_empty', - parameters: [], + name: 'only.fe.searchBy', + reason: ValidationMessageCode.FIELD_EMPTY, + constraintParameters: [], + value: null, }, ], }; diff --git a/alfa-client/libs/vorgang-detail/src/lib/vorgang-detail-page/vorgang-detail-bescheiden/vorgang-detail-bescheiden-result/vorgang-detail-bescheiden-result-attachments/vorgang-detail-bescheiden-result-attachments.component.html b/alfa-client/libs/vorgang-detail/src/lib/vorgang-detail-page/vorgang-detail-bescheiden/vorgang-detail-bescheiden-result/vorgang-detail-bescheiden-result-attachments/vorgang-detail-bescheiden-result-attachments.component.html index ff4e542e0d9181e9ee7bc98b468e62b2a1d23482..78e9fba0ee241ee0b39e7a9e4d771e48cb1b84fd 100644 --- a/alfa-client/libs/vorgang-detail/src/lib/vorgang-detail-page/vorgang-detail-bescheiden/vorgang-detail-bescheiden-result/vorgang-detail-bescheiden-result-attachments/vorgang-detail-bescheiden-result-attachments.component.html +++ b/alfa-client/libs/vorgang-detail/src/lib/vorgang-detail-page/vorgang-detail-bescheiden/vorgang-detail-bescheiden-result/vorgang-detail-bescheiden-result-attachments/vorgang-detail-bescheiden-result-attachments.component.html @@ -22,7 +22,7 @@ *ngIf="uploadFileInProgress.loading || uploadFileInProgress.error" [loadingCaption]="uploadFileInProgress.fileName" errorCaption="Fehler beim Hochladen" - [errorMessages]="uploadFileInProgress.error | convertApiErrorToErrorMessages" + [errorMessages]="uploadFileInProgress.error | convertProblemDetailToErrorMessages" description="Anhang wird hochgeladen" [isLoading]="uploadFileInProgress.loading" ></ods-attachment> diff --git a/alfa-client/libs/vorgang-detail/src/lib/vorgang-detail-page/vorgang-detail-bescheiden/vorgang-detail-bescheiden-result/vorgang-detail-bescheiden-result-attachments/vorgang-detail-bescheiden-result-attachments.component.spec.ts b/alfa-client/libs/vorgang-detail/src/lib/vorgang-detail-page/vorgang-detail-bescheiden/vorgang-detail-bescheiden-result/vorgang-detail-bescheiden-result-attachments/vorgang-detail-bescheiden-result-attachments.component.spec.ts index 63758c537759f45fbe750b6c286b6b2e5778864d..caed9958a4f9dc70ae170976ae44c01c77e8e235 100644 --- a/alfa-client/libs/vorgang-detail/src/lib/vorgang-detail-page/vorgang-detail-bescheiden/vorgang-detail-bescheiden-result/vorgang-detail-bescheiden-result-attachments/vorgang-detail-bescheiden-result-attachments.component.spec.ts +++ b/alfa-client/libs/vorgang-detail/src/lib/vorgang-detail-page/vorgang-detail-bescheiden/vorgang-detail-bescheiden-result/vorgang-detail-bescheiden-result-attachments/vorgang-detail-bescheiden-result-attachments.component.spec.ts @@ -2,7 +2,6 @@ import { BescheidService } from '@alfa-client/bescheid-shared'; import { BinaryFile2ContainerComponent } from '@alfa-client/binary-file'; import { BinaryFileResource } from '@alfa-client/binary-file-shared'; import { - ConvertApiErrorToErrorMessagesPipe, convertForDataTest, ConvertForDataTestPipe, createErrorStateResource, @@ -15,6 +14,7 @@ import { OzgcloudSvgIconComponent, SpinnerComponent } from '@alfa-client/ui'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { MatIcon } from '@angular/material/icon'; import { AttachmentComponent, AttachmentWrapperComponent, SpinnerIconComponent } from '@ods/system'; +import { ConvertProblemDetailToErrorMessagesPipe } from 'libs/tech-shared/src/lib/pipe/convert-problem-detail-to-error-messages.pipe'; import { MockComponent, MockPipe } from 'ng-mocks'; import { BehaviorSubject, EMPTY, Observable, of, Subscription } from 'rxjs'; import { createUploadFileInProgress } from '../../../../../../../bescheid-shared/src/test/bescheid'; @@ -51,7 +51,7 @@ describe('VorgangDetailBescheidenResultAttachmentsComponent', () => { ConvertForDataTestPipe, MatIcon, MockPipe(FileSizePipe), - MockPipe(ConvertApiErrorToErrorMessagesPipe), + MockPipe(ConvertProblemDetailToErrorMessagesPipe), MockComponent(OzgcloudSvgIconComponent), MockComponent(SpinnerComponent), MockComponent(AttachmentWrapperComponent), diff --git a/alfa-client/libs/vorgang-detail/src/lib/vorgang-detail-page/vorgang-detail-bescheiden/vorgang-detail-bescheiden-result/vorgang-detail-bescheiden-result-dokument/vorgang-detail-bescheiden-result-dokument.component.html b/alfa-client/libs/vorgang-detail/src/lib/vorgang-detail-page/vorgang-detail-bescheiden/vorgang-detail-bescheiden-result/vorgang-detail-bescheiden-result-dokument/vorgang-detail-bescheiden-result-dokument.component.html index bb762af88d7b449d222804be6eccd2be60690f5e..d94edab530d6fb8fcdce754bee1d5083f0e7e6b7 100644 --- a/alfa-client/libs/vorgang-detail/src/lib/vorgang-detail-page/vorgang-detail-bescheiden/vorgang-detail-bescheiden-result/vorgang-detail-bescheiden-result-dokument/vorgang-detail-bescheiden-result-dokument.component.html +++ b/alfa-client/libs/vorgang-detail/src/lib/vorgang-detail-page/vorgang-detail-bescheiden/vorgang-detail-bescheiden-result/vorgang-detail-bescheiden-result-dokument/vorgang-detail-bescheiden-result-dokument.component.html @@ -27,7 +27,7 @@ 'upload-bescheid-document-error-' + !!uploadBescheidDocumentInProgress.error " [isLoading]="uploadBescheidDocumentInProgress.loading" - [errorMessages]="uploadBescheidDocumentInProgress.error | convertApiErrorToErrorMessages" + [errorMessages]="uploadBescheidDocumentInProgress.error | convertProblemDetailToErrorMessages" description="Bescheiddokument wird hochgeladen" ></ods-attachment> <ods-attachment diff --git a/alfa-client/libs/vorgang-detail/src/lib/vorgang-detail-page/vorgang-detail-bescheiden/vorgang-detail-bescheiden-result/vorgang-detail-bescheiden-result-dokument/vorgang-detail-bescheiden-result-dokument.component.spec.ts b/alfa-client/libs/vorgang-detail/src/lib/vorgang-detail-page/vorgang-detail-bescheiden/vorgang-detail-bescheiden-result/vorgang-detail-bescheiden-result-dokument/vorgang-detail-bescheiden-result-dokument.component.spec.ts index c61b90e368bb525ef644dfd2e95d156c06338d22..7dceb5ce5ba5e75c88ddb80fdd529a89b3e5ed41 100644 --- a/alfa-client/libs/vorgang-detail/src/lib/vorgang-detail-page/vorgang-detail-bescheiden/vorgang-detail-bescheiden-result/vorgang-detail-bescheiden-result-dokument/vorgang-detail-bescheiden-result-dokument.component.spec.ts +++ b/alfa-client/libs/vorgang-detail/src/lib/vorgang-detail-page/vorgang-detail-bescheiden/vorgang-detail-bescheiden-result/vorgang-detail-bescheiden-result-dokument/vorgang-detail-bescheiden-result-dokument.component.spec.ts @@ -2,7 +2,6 @@ import { BescheidLinkRel, BescheidResource, BescheidService } from '@alfa-client import { BinaryFile2ContainerComponent } from '@alfa-client/binary-file'; import { CommandResource } from '@alfa-client/command-shared'; import { - ConvertApiErrorToErrorMessagesPipe, StateResource, createEmptyStateResource, createStateResource, @@ -13,6 +12,7 @@ import { getUrl } from '@ngxp/rest'; import { AttachmentComponent, AttachmentWrapperComponent } from '@ods/system'; import { createBescheidResource } from 'libs/bescheid-shared/src/test/bescheid'; import { createBinaryFileResource } from 'libs/binary-file-shared/test/binary-file'; +import { ConvertProblemDetailToErrorMessagesPipe } from 'libs/tech-shared/src/lib/pipe/convert-problem-detail-to-error-messages.pipe'; import { getDataTestIdOf } from 'libs/tech-shared/test/data-test'; import { createApiError } from 'libs/tech-shared/test/error'; import { MockComponent, MockPipe } from 'ng-mocks'; @@ -48,7 +48,7 @@ describe('VorgangDetailBescheidenResultDokumentComponent', () => { MockComponent(BinaryFile2ContainerComponent), MockComponent(AttachmentComponent), MockComponent(AttachmentWrapperComponent), - MockPipe(ConvertApiErrorToErrorMessagesPipe), + MockPipe(ConvertProblemDetailToErrorMessagesPipe), ], providers: [ { diff --git a/alfa-client/libs/vorgang-shared-ui/src/lib/wiedervorlage-icon/wiedervorlage-icon.component.html b/alfa-client/libs/vorgang-shared-ui/src/lib/wiedervorlage-icon/wiedervorlage-icon.component.html index 9bf2265a277651fe8d40eff1a80ad736136077ea..929a086df43bd18a6e9a4b0097d02c877d3d51e1 100644 --- a/alfa-client/libs/vorgang-shared-ui/src/lib/wiedervorlage-icon/wiedervorlage-icon.component.html +++ b/alfa-client/libs/vorgang-shared-ui/src/lib/wiedervorlage-icon/wiedervorlage-icon.component.html @@ -1,4 +1,7 @@ -<div [class.red]="isOverdue" data-test-class="wiedervorlage-icon"> +<div + [ngClass]="{ 'text-error': isOverdue, 'text-text': !isOverdue }" + data-test-class="wiedervorlage-icon" +> <ng-container *ngIf="isOverdue; else defaultFrist"> <ozgcloud-svgicon svgIcon="resubmission_expired" diff --git a/alfa-client/libs/vorgang-shared-ui/src/lib/wiedervorlage-icon/wiedervorlage-icon.component.scss b/alfa-client/libs/vorgang-shared-ui/src/lib/wiedervorlage-icon/wiedervorlage-icon.component.scss index 9315d4d7758a4c1fcfe23122a58d1d7059d8b257..5d71aeb617f12da1e3cf893fb102314114d1e7df 100644 --- a/alfa-client/libs/vorgang-shared-ui/src/lib/wiedervorlage-icon/wiedervorlage-icon.component.scss +++ b/alfa-client/libs/vorgang-shared-ui/src/lib/wiedervorlage-icon/wiedervorlage-icon.component.scss @@ -6,11 +6,3 @@ margin-right: 6px; height: $iconHeight; } - -.red { - color: mat.get-color-from-palette($warnPalette, darker); -} - -body.dark :host .red { - color: red; -} diff --git a/alfa-client/libs/vorgang/src/lib/vorgang-list-container/vorgang-list/vorgang-list-item/vorgang-next-frist-button/vorgang-next-frist-button.component.html b/alfa-client/libs/vorgang/src/lib/vorgang-list-container/vorgang-list/vorgang-list-item/vorgang-next-frist-button/vorgang-next-frist-button.component.html index 37b136c5f1d31d7d6f21f87df330bec6cce80a9c..206b3aa1c4c6986f0b4f18fdcb876810bf81d610 100644 --- a/alfa-client/libs/vorgang/src/lib/vorgang-list-container/vorgang-list/vorgang-list-item/vorgang-next-frist-button/vorgang-next-frist-button.component.html +++ b/alfa-client/libs/vorgang/src/lib/vorgang-list-container/vorgang-list/vorgang-list-item/vorgang-next-frist-button/vorgang-next-frist-button.component.html @@ -31,7 +31,11 @@ (mouseleave)="showWiedervorlagen = false" (focusout)="showWiedervorlagen = false" > - <div class="date" [class.red]="isOverdue" data-test-class="wiedervorlage-next-frist"> + <div + class="date" + [ngClass]="{ 'text-error': isOverdue, 'text-text': !isOverdue }" + data-test-class="wiedervorlage-next-frist" + > <alfa-wiedervorlage-icon [isOverdue]="isOverdue"></alfa-wiedervorlage-icon> <span>{{ vorgang.nextFrist | formatToPrettyDate }}</span> </div> diff --git a/alfa-client/libs/vorgang/src/lib/vorgang-list-container/vorgang-list/vorgang-list-item/vorgang-next-frist-button/vorgang-next-frist-button.component.scss b/alfa-client/libs/vorgang/src/lib/vorgang-list-container/vorgang-list/vorgang-list-item/vorgang-next-frist-button/vorgang-next-frist-button.component.scss index aa806fcb134fd31ffe3294c79892fccf159157fa..458f0a6eb3a1c8807675fab809d27e13ca79503b 100644 --- a/alfa-client/libs/vorgang/src/lib/vorgang-list-container/vorgang-list/vorgang-list-item/vorgang-next-frist-button/vorgang-next-frist-button.component.scss +++ b/alfa-client/libs/vorgang/src/lib/vorgang-list-container/vorgang-list/vorgang-list-item/vorgang-next-frist-button/vorgang-next-frist-button.component.scss @@ -29,7 +29,6 @@ background-color: inherit; border: 0; padding: 0; - color: inherit; position: relative; } @@ -38,11 +37,3 @@ align-items: center; white-space: nowrap; } - -.red { - color: mat.get-color-from-palette($warnPalette, darker); -} - -body.dark :host .red { - color: red; -} diff --git a/alfa-service/src/main/java/de/ozgcloud/alfa/common/errorhandling/ExceptionController.java b/alfa-service/src/main/java/de/ozgcloud/alfa/common/errorhandling/ExceptionController.java index 5e4f5be4e7d0a8077ab670e49769d3655c8abd47..facaacd37af7212818e9e220331f7c6a24c4c4a4 100644 --- a/alfa-service/src/main/java/de/ozgcloud/alfa/common/errorhandling/ExceptionController.java +++ b/alfa-service/src/main/java/de/ozgcloud/alfa/common/errorhandling/ExceptionController.java @@ -23,49 +23,38 @@ */ package de.ozgcloud.alfa.common.errorhandling; -import java.util.Arrays; -import java.util.HashSet; -import java.util.List; -import java.util.Map.Entry; -import java.util.Objects; -import java.util.Optional; -import java.util.Set; import java.util.UUID; -import java.util.stream.Stream; -import jakarta.validation.ConstraintViolation; import jakarta.validation.ConstraintViolationException; -import jakarta.validation.Path; -import jakarta.validation.metadata.ConstraintDescriptor; -import org.hibernate.validator.engine.HibernateConstraintViolation; import org.springframework.core.annotation.Order; import org.springframework.http.HttpStatus; +import org.springframework.http.ProblemDetail; import org.springframework.security.access.AccessDeniedException; -import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; -import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; -import de.ozgcloud.alfa.common.binaryfile.DynamicViolationParameter; import de.ozgcloud.common.errorhandling.ExceptionUtil; import de.ozgcloud.common.errorhandling.TechnicalException; +import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; -@ControllerAdvice +@RestControllerAdvice +@RequiredArgsConstructor @Log4j2 @Order(98) -public class ExceptionController { - - private static final Set<String> IGNORABLE_CONSTRAINT_VIOLATION_ATTRIBUTES = new HashSet<>(Arrays.asList("groups", "payload", "message")); +public class ExceptionController extends ResponseEntityExceptionHandler { static final String RUNTIME_MESSAGE_CODE = "generale.server_error"; static final String RESOURCE_NOT_FOUNT_MESSAGE_CODE = "resource.not_found"; static final String ACCESS_DENIED_MESSAGE_CODE = "generale.access_denied"; + private final ProblemDetailMapper problemDetailMapper; + @ExceptionHandler(FunctionalException.class) @ResponseStatus(HttpStatus.BAD_REQUEST) - @ResponseBody public ApiError handleFunctionalException(FunctionalException e) { LOG.error("BadRequest", e); return ApiError.builder().issue(buildIssueForFunctionalException(e)).build(); @@ -77,7 +66,6 @@ public class ExceptionController { @ExceptionHandler(AccessDeniedException.class) @ResponseStatus(HttpStatus.FORBIDDEN) - @ResponseBody public ApiError handleAccessDeniedException(AccessDeniedException e) { var exceptionId = createExceptionId(); var messageWithExceptionId = ExceptionUtil.formatMessageWithExceptionId(e.getMessage(), exceptionId); @@ -92,7 +80,6 @@ public class ExceptionController { @ExceptionHandler(ResourceNotFoundException.class) @ResponseStatus(HttpStatus.NOT_FOUND) - @ResponseBody public ApiError handleResourceNotFoundException(ResourceNotFoundException e) { LOG.warn("Resource not found: {}", e.getMessage()); return ApiError.builder().issue(buildIssueForResourceNotFoundException(e)).build(); @@ -103,66 +90,14 @@ public class ExceptionController { } @ExceptionHandler(ConstraintViolationException.class) - @ResponseStatus(HttpStatus.UNPROCESSABLE_ENTITY) - @ResponseBody - public ApiError handleConstraintViolationException(ConstraintViolationException e) { - var exceptionId = createExceptionId(); - var messageWithExceptionId = ExceptionUtil.formatMessageWithExceptionId(e.getMessage(), exceptionId); - LOG.warn("Validation Exception: {}", messageWithExceptionId); - return ApiError.builder().issues(buildIssues(e, exceptionId)).build(); - } - - private List<Issue> buildIssues(ConstraintViolationException e, String exceptionId) { - return e.getConstraintViolations().stream() - .map(violation -> buildIssue(violation, exceptionId)) - .toList(); - } - - private Issue buildIssue(ConstraintViolation<?> violation, String exceptionId) { - return Issue.builder()// - .field(buildFieldPath(violation.getPropertyPath()))// - .messageCode(violation.getMessageTemplate().replace("{", "").replace("}", ""))// - .message(violation.getMessage())// - .parameters(buildParameters(violation).toList()) - .exceptionId(exceptionId) - .build(); - } - - private String buildFieldPath(Path propertyPath) { - return propertyPath.toString().substring(propertyPath.toString().indexOf('.') + 1); - } - - Stream<IssueParam> buildParameters(ConstraintViolation<?> violation) { - var dynamicPayload = getDynamicPayload(violation); - return Optional.ofNullable(violation.getConstraintDescriptor()) - .map(ConstraintDescriptor::getAttributes) - .map(descr -> descr.entrySet().stream() - .filter(entry -> !IGNORABLE_CONSTRAINT_VIOLATION_ATTRIBUTES.contains(entry.getKey())) - .map(entryMap -> buildIssueParam(entryMap, dynamicPayload))) - .orElse(Stream.empty()); - } - - private IssueParam buildIssueParam(Entry<String, Object> entry, Optional<DynamicViolationParameter> dynamicValues) { - return IssueParam.builder().name(entry.getKey()).value(getValue(entry, dynamicValues)).build(); - } - - private String getValue(Entry<String, Object> entry, Optional<DynamicViolationParameter> dynamicValues) { - return dynamicValues - .map(DynamicViolationParameter::getMap) - .map(map -> map.get(entry.getKey())) - .filter(Objects::nonNull) - .map(String::valueOf) - .orElse(String.valueOf(entry.getValue())); - } + public ProblemDetail handleConstraintViolationException(ConstraintViolationException e) { + LOG.warn("Validation Exception: {}", problemDetailMapper.buildMessageWithExceptionId(e)); - private Optional<DynamicViolationParameter> getDynamicPayload(ConstraintViolation<?> violation) { - HibernateConstraintViolation<?> hibernateViolation = violation.unwrap(HibernateConstraintViolation.class); - return Optional.ofNullable(hibernateViolation.getDynamicPayload(DynamicViolationParameter.class)); + return problemDetailMapper.fromConstraintViolationException(e); } @ExceptionHandler(TechnicalException.class) @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) - @ResponseBody public ApiError handleTechnicalException(TechnicalException e) { LOG.error("TechnicalException on Request", e); return buildRuntimeApiError(e.getMessage(), e.getExceptionId()); @@ -170,7 +105,6 @@ public class ExceptionController { @ExceptionHandler(RuntimeException.class) @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) - @ResponseBody public ApiError handleRuntimeException(RuntimeException e) { var exceptionId = createExceptionId(); var messageWithExceptionId = ExceptionUtil.formatMessageWithExceptionId(e.getMessage(), exceptionId); diff --git a/alfa-service/src/main/java/de/ozgcloud/alfa/common/errorhandling/ProblemDetailMapper.java b/alfa-service/src/main/java/de/ozgcloud/alfa/common/errorhandling/ProblemDetailMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..c4a358f396674e8bcd4f8f4fb36eb677eec7b74b --- /dev/null +++ b/alfa-service/src/main/java/de/ozgcloud/alfa/common/errorhandling/ProblemDetailMapper.java @@ -0,0 +1,90 @@ +package de.ozgcloud.alfa.common.errorhandling; + +import java.util.Collections; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.UUID; +import java.util.stream.Stream; + +import jakarta.validation.ConstraintViolation; +import jakarta.validation.ConstraintViolationException; +import jakarta.validation.metadata.ConstraintDescriptor; + +import org.hibernate.validator.engine.HibernateConstraintViolation; +import org.springframework.http.HttpStatus; +import org.springframework.http.ProblemDetail; +import org.springframework.stereotype.Component; + +import de.ozgcloud.alfa.common.binaryfile.DynamicViolationParameter; +import de.ozgcloud.common.errorhandling.ExceptionUtil; + +@Component +public class ProblemDetailMapper { + + static final String INVALID_PARAMS_KEY_CONSTRAINT_PARAMETERS = "constraintParameters"; + static final String INVALID_PARAMS_KEY_REASON = "reason"; + static final String INVALID_PARAMS_KEY_VALUE = "value"; + static final String INVALID_PARAMS_KEY_NAME = "name"; + static final String INVALID_PARAMS = "invalidParams"; + static final String PROVIDED_VALUE_WAS_NULL = "Provided value was null!"; + private static final Set<String> IGNORABLE_CONSTRAINT_VIOLATION_ATTRIBUTES = Set.of("groups", "payload", "message"); + + public ProblemDetail fromConstraintViolationException(ConstraintViolationException e) { + var problemDetail = ProblemDetail.forStatusAndDetail(HttpStatus.UNPROCESSABLE_ENTITY, buildMessageWithExceptionId(e)); + problemDetail.setProperty(INVALID_PARAMS, buildInvalidParams(e.getConstraintViolations()).toList()); + return problemDetail; + } + + public String buildMessageWithExceptionId(ConstraintViolationException e) { + var exceptionId = createExceptionId(); + return ExceptionUtil.formatMessageWithExceptionId(e.getLocalizedMessage(), + exceptionId); + } + + String createExceptionId() { + return UUID.randomUUID().toString(); + } + + Stream<Map<String, Object>> buildInvalidParams(Set<ConstraintViolation<?>> violations) { + return Objects.requireNonNullElse(violations, Collections.<ConstraintViolation<?>>emptySet()).stream().map(this::buildDetailedViolation); + } + + Map<String, Object> buildDetailedViolation(ConstraintViolation<?> violation) { + return Map.of( + INVALID_PARAMS_KEY_NAME, violation.getPropertyPath().toString(), + INVALID_PARAMS_KEY_VALUE, Objects.requireNonNullElse(violation.getInvalidValue(), PROVIDED_VALUE_WAS_NULL).toString(), + INVALID_PARAMS_KEY_REASON, violation.getMessage(), + INVALID_PARAMS_KEY_CONSTRAINT_PARAMETERS, buildParameters(violation).toList()); + } + + Stream<IssueParam> buildParameters(ConstraintViolation<?> violation) { + var dynamicPayload = getDynamicPayload(violation); + return Optional.ofNullable(violation.getConstraintDescriptor()) + .map(ConstraintDescriptor::getAttributes) + .map(descr -> descr.entrySet().stream() + .filter(entry -> !IGNORABLE_CONSTRAINT_VIOLATION_ATTRIBUTES.contains(entry.getKey())) + .map(entryMap -> buildIssueParam(entryMap, dynamicPayload))) + .orElse(Stream.empty()); + } + + private IssueParam buildIssueParam(Entry<String, Object> entry, Optional<DynamicViolationParameter> dynamicValues) { + return IssueParam.builder().name(entry.getKey()).value(getValue(entry, dynamicValues)).build(); + } + + private String getValue(Entry<String, Object> entry, Optional<DynamicViolationParameter> dynamicValues) { + return dynamicValues + .map(DynamicViolationParameter::getMap) + .map(map -> map.get(entry.getKey())) + .filter(Objects::nonNull) + .map(String::valueOf) + .orElse(String.valueOf(entry.getValue())); + } + + private Optional<DynamicViolationParameter> getDynamicPayload(ConstraintViolation<?> violation) { + HibernateConstraintViolation<?> hibernateViolation = violation.unwrap(HibernateConstraintViolation.class); + return Optional.ofNullable(hibernateViolation.getDynamicPayload(DynamicViolationParameter.class)); + } +} diff --git a/alfa-service/src/test/java/de/ozgcloud/alfa/bescheid/BescheidCommandITCase.java b/alfa-service/src/test/java/de/ozgcloud/alfa/bescheid/BescheidCommandITCase.java index d4c71d099ea22b5a27736889e0fb0aebff784a44..b85d323ef153890af71025bd9b886ab230a6a67f 100644 --- a/alfa-service/src/test/java/de/ozgcloud/alfa/bescheid/BescheidCommandITCase.java +++ b/alfa-service/src/test/java/de/ozgcloud/alfa/bescheid/BescheidCommandITCase.java @@ -52,9 +52,9 @@ public class BescheidCommandITCase { String content = createInvalidRequestContent(BescheidTestFactory.createBuilder().beschiedenAm(null).build()); doRequest(content).andExpect(status().isUnprocessableEntity()) - .andExpect(jsonPath("$.issues.length()").value(1)) - .andExpect(jsonPath("$.issues.[0].field").value("command.body.beschiedenAm")) - .andExpect(jsonPath("$.issues.[0].messageCode").value(ValidationMessageCodes.FIELD_DATE_FORMAT_INVALID)); + .andExpect(jsonPath("$.invalidParams.length()").value(1)) + .andExpect(jsonPath("$.invalidParams[0].name").value("createCommand.command.body.beschiedenAm")) + .andExpect(jsonPath("$.invalidParams[0].reason").value(ValidationMessageCodes.FIELD_DATE_FORMAT_INVALID)); } @ParameterizedTest @@ -82,9 +82,9 @@ public class BescheidCommandITCase { return mockMvc.perform( post(CommandByRelationController.COMMAND_BY_RELATION_PATH, VorgangHeaderTestFactory.ID, VorgangHeaderTestFactory.ID, VorgangHeaderTestFactory.VERSION) - .with(csrf()) - .contentType(MediaType.APPLICATION_JSON) - .content(content)); + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(content)); } } } diff --git a/alfa-service/src/test/java/de/ozgcloud/alfa/common/binaryfile/BinaryFileControllerITCase.java b/alfa-service/src/test/java/de/ozgcloud/alfa/common/binaryfile/BinaryFileControllerITCase.java index 16cd0b895439464f3feba0aeda52642d63985cc4..18ddf8ce29170a549e2bd63d0c080a5c6ebd3451 100644 --- a/alfa-service/src/test/java/de/ozgcloud/alfa/common/binaryfile/BinaryFileControllerITCase.java +++ b/alfa-service/src/test/java/de/ozgcloud/alfa/common/binaryfile/BinaryFileControllerITCase.java @@ -122,7 +122,7 @@ class BinaryFileControllerITCase { void setTokenToSecuriyContext(String token) throws Exception { mockMvc.perform(get(DownloadTokenController.DOWNLOAD_TOKEN_PATH + "?" + DownloadTokenController.PARAM_TOKEN + "=" + token) - .with(csrf())) + .with(csrf())) .andExpect(status().isOk()); } } @@ -168,8 +168,8 @@ class BinaryFileControllerITCase { @Test void shouldReturnValidationMessage() throws Exception { performRequest(createFile(MediaType.TEXT_PLAIN)).andExpect(status().isUnprocessableEntity()) - .andExpect(jsonPath("$.issues.length()").value(1)) - .andExpect(jsonPath("$.issues.[0].messageCode").value(ValidationMessageCodes.FIELD_FILE_CONTENT_TYPE_INVALID)); + .andExpect(jsonPath("$.invalidParams.length()").value(1)) + .andExpect(jsonPath("$.invalidParams[0].reason").value(ValidationMessageCodes.FIELD_FILE_CONTENT_TYPE_INVALID)); } @Test @@ -183,7 +183,7 @@ class BinaryFileControllerITCase { return mockMvc.perform(multipart( String.format("%s/%s/%s/file", BinaryFileController.PATH, VorgangHeaderTestFactory.ID, UploadBinaryFileTestFactory.FIELD)).file( - file).with(csrf())); + file).with(csrf())); } private MockMultipartFile createFile(MediaType contentType) { diff --git a/alfa-service/src/test/java/de/ozgcloud/alfa/common/binaryfile/BinaryFileITCase.java b/alfa-service/src/test/java/de/ozgcloud/alfa/common/binaryfile/BinaryFileITCase.java index dcc9f9afcf7b622d9ac9e5d2f12a1a60b3bb34c2..6ab03aafe02d6aa07594f3622255b66b47e107d5 100644 --- a/alfa-service/src/test/java/de/ozgcloud/alfa/common/binaryfile/BinaryFileITCase.java +++ b/alfa-service/src/test/java/de/ozgcloud/alfa/common/binaryfile/BinaryFileITCase.java @@ -85,33 +85,33 @@ class BinaryFileITCase { void shouldReturnConstraintViolationExceptionData() throws Exception { var result = callEndpoint(TEST_FILE, BinaryFileTestFactory.FIELD); - result.andExpect(jsonPath("issues[0].field").value("uploadBinaryFileRequest")) - .andExpect(jsonPath("issues[0].messageCode").value("validation_field_file_size_exceeded")) - .andExpect(jsonPath("issues[0].message").value("validation_field_file_size_exceeded")) - .andExpect(jsonPath("issues[0].parameters[0].name").value("unit")) - .andExpect(jsonPath("issues[0].parameters[0].value").value(UploadBinaryFileSizeValidator.DEFAULT_UNIT_STRING)); + result.andExpect(jsonPath("invalidParams[0].name").value("uploadFile.uploadBinaryFileRequest")) + .andExpect(jsonPath("invalidParams[0].reason").value("validation_field_file_size_exceeded")) + .andExpect(jsonPath("invalidParams[0].constraintParameters[0].name").value("unit")) + .andExpect( + jsonPath("invalidParams[0].constraintParameters[0].value").value(UploadBinaryFileSizeValidator.DEFAULT_UNIT_STRING)); } @Test void shouldContainsExceptionDataOnDefaultMaxSize() throws Exception { var result = callEndpoint(TEST_FILE, BinaryFileTestFactory.FIELD); - result.andExpect(jsonPath("issues[0].parameters[1].name").value("max")) - .andExpect(jsonPath("issues[0].parameters[1].value").value("40")); + result.andExpect(jsonPath("invalidParams[0].constraintParameters[1].name").value("max")) + .andExpect(jsonPath("invalidParams[0].constraintParameters[1].value").value("40")); } } @Nested class TestForPostfachNachricht { - private final static String FIELD = "postfachNachrichtAttachment"; + private static final String FIELD = "postfachNachrichtAttachment"; @Test void shouldContainsExceptionDataOnPostfachNachrichtMaxSize() throws Exception { var result = callEndpoint(TEST_FILE, FIELD); - result.andExpect(jsonPath("issues[0].parameters[1].name").value("max")) - .andExpect(jsonPath("issues[0].parameters[1].value").value("3")); + result.andExpect(jsonPath("invalidParams[0].constraintParameters[1].name").value("max")) + .andExpect(jsonPath("invalidParams[0].constraintParameters[1].value").value("3")); } } diff --git a/alfa-service/src/test/java/de/ozgcloud/alfa/common/command/CommandControllerTest.java b/alfa-service/src/test/java/de/ozgcloud/alfa/common/command/CommandControllerTest.java index 53f6ec125efb2914bb7754ff51211c646f904bbc..47da8200c2c2a6ab2465778f1110f3213e8c139b 100644 --- a/alfa-service/src/test/java/de/ozgcloud/alfa/common/command/CommandControllerTest.java +++ b/alfa-service/src/test/java/de/ozgcloud/alfa/common/command/CommandControllerTest.java @@ -43,6 +43,7 @@ import org.springframework.test.web.servlet.ResultActions; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import de.ozgcloud.alfa.common.errorhandling.ExceptionController; +import de.ozgcloud.alfa.common.errorhandling.ProblemDetailMapper; import de.ozgcloud.alfa.vorgang.VorgangHeaderTestFactory; import de.ozgcloud.common.test.TestUtils; @@ -54,12 +55,14 @@ class CommandControllerTest { private CommandService service; @Mock private CommandModelAssembler modelAssembler; + @Mock + private ProblemDetailMapper problemDetailMapper; private MockMvc mockMvc; @BeforeEach void initTest() { - mockMvc = MockMvcBuilders.standaloneSetup(controller).setControllerAdvice(new ExceptionController()).build(); + mockMvc = MockMvcBuilders.standaloneSetup(controller).setControllerAdvice(new ExceptionController(problemDetailMapper)).build(); } @Nested @@ -171,8 +174,8 @@ class CommandControllerTest { private ResultActions doRequest() throws Exception { return mockMvc.perform(get(CommandController.COMMANDS_PATH) - .param(CommandController.PARAM_PENDING, Boolean.toString(true)) - .param(CommandController.PARAM_VORGANG_ID, VorgangHeaderTestFactory.ID)) + .param(CommandController.PARAM_PENDING, Boolean.toString(true)) + .param(CommandController.PARAM_VORGANG_ID, VorgangHeaderTestFactory.ID)) .andExpect(status().is2xxSuccessful()); } } diff --git a/alfa-service/src/test/java/de/ozgcloud/alfa/common/command/CommandITCase.java b/alfa-service/src/test/java/de/ozgcloud/alfa/common/command/CommandITCase.java index 7ee0005a679ae552bd6e78cdc24ae00e6ab99213..3f396641e5d9aca17161d0276e9a0fe8d8521a44 100644 --- a/alfa-service/src/test/java/de/ozgcloud/alfa/common/command/CommandITCase.java +++ b/alfa-service/src/test/java/de/ozgcloud/alfa/common/command/CommandITCase.java @@ -126,9 +126,9 @@ public class CommandITCase { .email(null).build()); doRequest(requestContent).andExpect(status().isUnprocessableEntity()) - .andExpect(jsonPath("$.issues.length()").value(1)) - .andExpect(jsonPath("$.issues.[0].field").value("command.redirectRequest.email")) - .andExpect(jsonPath("$.issues.[0].messageCode").value(ValidationMessageCodes.FIELD_IS_EMPTY)); + .andExpect(jsonPath("$.invalidParams.length()").value(1)) + .andExpect(jsonPath("$.invalidParams[0].name").value("createCommand.command.redirectRequest.email")) + .andExpect(jsonPath("$.invalidParams[0].reason").value(ValidationMessageCodes.FIELD_IS_EMPTY)); } } @@ -145,7 +145,7 @@ public class CommandITCase { .build()); doRequest(requestContent).andExpect(status().isUnprocessableEntity()) - .andExpect(jsonPath("$.issues.length()").value(2)); + .andExpect(jsonPath("$.invalidParams.length()").value(2)); } } @@ -153,16 +153,16 @@ public class CommandITCase { @Nested class TestEmail { - private final String FIELD = "email"; + private static final String FIELD = "email"; @Test void shouldReturnErrorOnNullEMail() throws Exception { var requestContent = buildRedirectRequestWithEmail(null); doRequest(requestContent).andExpect(status().isUnprocessableEntity()) - .andExpect(jsonPath("$.issues.length()").value(1)) - .andExpect(jsonPath("$.issues.[0].field").value("command.redirectRequest." + FIELD)) - .andExpect(jsonPath("$.issues.[0].messageCode").value(ValidationMessageCodes.FIELD_IS_EMPTY)); + .andExpect(jsonPath("$.invalidParams.length()").value(1)) + .andExpect(jsonPath("$.invalidParams[0].name").value("createCommand.command.redirectRequest." + FIELD)) + .andExpect(jsonPath("$.invalidParams[0].reason").value(ValidationMessageCodes.FIELD_IS_EMPTY)); } @Test @@ -170,9 +170,9 @@ public class CommandITCase { var requestContent = buildRedirectRequestWithEmail("local@@domain.com"); doRequest(requestContent).andExpect(status().isUnprocessableEntity()) - .andExpect(jsonPath("$.issues.length()").value(1)) - .andExpect(jsonPath("$.issues.[0].field").value("command.redirectRequest." + FIELD)) - .andExpect(jsonPath("$.issues.[0].messageCode").value(ValidationMessageCodes.FIELD_INVALID)); + .andExpect(jsonPath("$.invalidParams.length()").value(1)) + .andExpect(jsonPath("$.invalidParams[0].name").value("createCommand.command.redirectRequest." + FIELD)) + .andExpect(jsonPath("$.invalidParams[0].reason").value(ValidationMessageCodes.FIELD_INVALID)); } private String buildRedirectRequestWithEmail(String eMail) { @@ -191,13 +191,13 @@ public class CommandITCase { var requestContent = buildRedirectRequestWithPassword(RandomStringUtils.randomAlphabetic(7)); doRequest(requestContent).andExpect(status().isUnprocessableEntity()) - .andExpect(jsonPath("$.issues.length()").value(1)) - .andExpect(jsonPath("$.issues.[0].field").value("command.redirectRequest." + FIELD)) - .andExpect(jsonPath("$.issues.[0].messageCode").value(ValidationMessageCodes.FIELD_SIZE)) - .andExpect(jsonPath("$.issues.[0].parameters[0].name").value("min")) - .andExpect(jsonPath("$.issues.[0].parameters[0].value").value(8)) - .andExpect(jsonPath("$.issues.[0].parameters[1].name").value("max")) - .andExpect(jsonPath("$.issues.[0].parameters[1].value").value(40)); + .andExpect(jsonPath("$.invalidParams.length()").value(1)) + .andExpect(jsonPath("$.invalidParams[0].name").value("createCommand.command.redirectRequest." + FIELD)) + .andExpect(jsonPath("$.invalidParams[0].reason").value(ValidationMessageCodes.FIELD_SIZE)) + .andExpect(jsonPath("$.invalidParams[0].constraintParameters[0].name").value("min")) + .andExpect(jsonPath("$.invalidParams[0].constraintParameters[0].value").value(8)) + .andExpect(jsonPath("$.invalidParams[0].constraintParameters[1].name").value("max")) + .andExpect(jsonPath("$.invalidParams[0].constraintParameters[1].value").value(40)); } @Test @@ -205,9 +205,9 @@ public class CommandITCase { var requestContent = buildRedirectRequestWithPassword(RandomStringUtils.randomAlphabetic(41)); doRequest(requestContent).andExpect(status().isUnprocessableEntity()) - .andExpect(jsonPath("$.issues.length()").value(1)) - .andExpect(jsonPath("$.issues.[0].field").value("command.redirectRequest." + FIELD)) - .andExpect(jsonPath("$.issues.[0].messageCode").value(ValidationMessageCodes.FIELD_SIZE)); + .andExpect(jsonPath("$.invalidParams.length()").value(1)) + .andExpect(jsonPath("$.invalidParams[0].name").value("createCommand.command.redirectRequest." + FIELD)) + .andExpect(jsonPath("$.invalidParams[0].reason").value(ValidationMessageCodes.FIELD_SIZE)); } private String buildRedirectRequestWithPassword(String password) { @@ -245,9 +245,9 @@ public class CommandITCase { .buildSendPostfachMailContent(PostfachMailTestFactory.createBuilder().subject(null).build()); doRequest(requestContent).andExpect(status().isUnprocessableEntity()) - .andExpect(jsonPath("$.issues.length()").value(1)) - .andExpect(jsonPath("$.issues.[0].field").value("command.body.subject")) - .andExpect(jsonPath("$.issues.[0].messageCode").value(ValidationMessageCodes.FIELD_IS_EMPTY)); + .andExpect(jsonPath("$.invalidParams.length()").value(1)) + .andExpect(jsonPath("$.invalidParams[0].name").value("createCommand.command.body.subject")) + .andExpect(jsonPath("$.invalidParams[0].reason").value(ValidationMessageCodes.FIELD_IS_EMPTY)); } @Test @@ -257,9 +257,9 @@ public class CommandITCase { PostfachMailTestFactory.createBuilder().subject(RandomStringUtils.randomAlphanumeric(71)).build()); doRequest(requestContent).andExpect(status().isUnprocessableEntity()) - .andExpect(jsonPath("$.issues.length()").value(1)) - .andExpect(jsonPath("$.issues.[0].field").value("command.body.subject")) - .andExpect(jsonPath("$.issues.[0].messageCode").value(ValidationMessageCodes.FIELD_MAX_SIZE)); + .andExpect(jsonPath("$.invalidParams.length()").value(1)) + .andExpect(jsonPath("$.invalidParams[0].name").value("createCommand.command.body.subject")) + .andExpect(jsonPath("$.invalidParams[0].reason").value(ValidationMessageCodes.FIELD_MAX_SIZE)); } @Test @@ -280,9 +280,9 @@ public class CommandITCase { .buildSendPostfachMailContent(PostfachMailTestFactory.createBuilder().mailBody(null).build()); doRequest(request).andExpect(status().isUnprocessableEntity()) - .andExpect(jsonPath("$.issues.length()").value(1)) - .andExpect(jsonPath("$.issues.[0].field").value("command.body.mailBody")) - .andExpect(jsonPath("$.issues.[0].messageCode").value(ValidationMessageCodes.FIELD_IS_EMPTY)); + .andExpect(jsonPath("$.invalidParams.length()").value(1)) + .andExpect(jsonPath("$.invalidParams[0].name").value("createCommand.command.body.mailBody")) + .andExpect(jsonPath("$.invalidParams[0].reason").value(ValidationMessageCodes.FIELD_IS_EMPTY)); } @Test @@ -299,9 +299,9 @@ public class CommandITCase { ResultActions doRequest(String content) throws Exception { return mockMvc.perform(post("/api/vorgangs/" + CommandTestFactory.VORGANG_ID + "/relations/" + CommandTestFactory.RELATION_ID + "/" + CommandTestFactory.RELATION_VERSION + "/commands") - .with(csrf()) - .contentType(MediaType.APPLICATION_JSON) - .content(content)); + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(content)); } } } \ No newline at end of file diff --git a/alfa-service/src/test/java/de/ozgcloud/alfa/common/errorhandling/ExceptionControllerTest.java b/alfa-service/src/test/java/de/ozgcloud/alfa/common/errorhandling/ExceptionControllerTest.java index 489f0a8f4f2504e203d81504db92d12efc76f777..50a0b2d1af3cce26dfd38934071b5cf48264c048 100644 --- a/alfa-service/src/test/java/de/ozgcloud/alfa/common/errorhandling/ExceptionControllerTest.java +++ b/alfa-service/src/test/java/de/ozgcloud/alfa/common/errorhandling/ExceptionControllerTest.java @@ -27,7 +27,6 @@ import static org.assertj.core.api.Assertions.*; import static org.mockito.Mockito.*; import java.util.Collections; -import java.util.Map; import jakarta.validation.ConstraintViolationException; @@ -36,10 +35,14 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.mockito.InjectMocks; +import org.mockito.Mock; import org.mockito.Spy; +import org.springframework.http.HttpStatus; +import org.springframework.http.ProblemDetail; import org.springframework.security.access.AccessDeniedException; -import de.ozgcloud.alfa.common.binaryfile.DynamicViolationParameter; +import com.thedeanda.lorem.LoremIpsum; + import de.ozgcloud.alfa.common.command.LegacyOrder; import de.ozgcloud.common.errorhandling.TechnicalException; @@ -49,6 +52,9 @@ class ExceptionControllerTest { @InjectMocks private ExceptionController exceptionController; + @Mock + private ProblemDetailMapper problemDetailMapper; + @Nested class TestHandleFunctionalException { @@ -162,79 +168,42 @@ class ExceptionControllerTest { @Nested class TestContraintValidationException { - private final ConstraintViolationException exception = new ConstraintViolationException(ExceptionTestFactory.MESSAGE, - Collections.singleton(ExceptionTestFactory.buildMockedConstraintViolation())); - - @BeforeEach - void mockExceptionId() { - doReturn(ExceptionTestFactory.EXCEPTION_ID).when(exceptionController).createExceptionId(); - } - - @Test - void shouldHaveField() { - var error = handleException(); - - assertThat(error.getIssues()).hasSize(1); - assertThat(error.getIssues().get(0).getField()).isEqualTo(ExceptionTestFactory.PATH_FIELD); - } - - @Test - void shouldHaveMessageCode() { - var error = handleException(); - - assertThat(error.getIssues()).hasSize(1); - assertThat(error.getIssues().get(0).getMessageCode()).isEqualTo(ExceptionTestFactory.MESSAGE_CODE); - } - - @Test - void shouldHaveMessage() { - var error = handleException(); - - assertThat(error.getIssues()).hasSize(1); - assertThat(error.getIssues().get(0).getMessage()).isEqualTo(ExceptionTestFactory.MESSAGE); - } - - @Test - void shouldHaveExceptionId() { - var error = handleException(); + @Nested + class TestHandleConstraintViolationException { + private final String exceptionMessage = LoremIpsum.getInstance().getWords(5); - assertThat(error.getIssues().get(0).getExceptionId()).isEqualTo(ExceptionTestFactory.EXCEPTION_ID); - } + private final ConstraintViolationException exception = new ConstraintViolationException(exceptionMessage, + Collections.singleton(ExceptionTestFactory.buildMockedConstraintViolation())); - @Test - void shouldHaveIssueParameter() { - var error = handleException(); + @Test + void shouldGetMessageWithExceptionId() { + handleException(); - var issueParameter = error.getIssues().get(0).getParameters().get(0); - assertThat(issueParameter.getName()).isEqualTo(ExceptionTestFactory.PARAM_NAME); - assertThat(issueParameter.getValue()).isEqualTo(ExceptionTestFactory.PARAM_DYNAMIC_VALUE); - } + verify(problemDetailMapper).buildMessageWithExceptionId(exception); + } - @Nested - class TestWithDynamicPayload { + @Test + void shouldBuildConstraintViolationProblemDetail() { + handleException(); - private final DynamicViolationParameter dynamicViolationParameter = DynamicViolationParameter.builder() - .map(Map.of(ExceptionTestFactory.PARAM_NAME, ExceptionTestFactory.PARAM_DYNAMIC_VALUE)).build(); - private final ConstraintViolationException exceptionWithDynamicPayload = new ConstraintViolationException(ExceptionTestFactory.MESSAGE, - Collections.singleton(ExceptionTestFactory.buildMockedConstraintViolationWithDynamicPayload(dynamicViolationParameter))); + verify(problemDetailMapper).fromConstraintViolationException(exception); + } @Test - void shouldHaveReplacedParams() { - var error = handleExceptionWithDynamicPayload(); + void shouldReturnBuiltProblemDetail() { + var expectedProblemDetail = ProblemDetail.forStatusAndDetail(HttpStatus.UNPROCESSABLE_ENTITY, exceptionMessage); + when(problemDetailMapper.fromConstraintViolationException(exception)).thenReturn(expectedProblemDetail); + + var problemDetail = handleException(); - var issueParameter = error.getIssues().get(0).getParameters().get(0); - assertThat(issueParameter.getName()).isEqualTo(ExceptionTestFactory.PARAM_NAME); - assertThat(issueParameter.getValue()).isEqualTo(ExceptionTestFactory.PARAM_DYNAMIC_VALUE); + assertThat(problemDetail).isEqualTo(expectedProblemDetail); } - private ApiError handleExceptionWithDynamicPayload() { - return exceptionController.handleConstraintViolationException(exceptionWithDynamicPayload); + private ProblemDetail handleException() { + return exceptionController.handleConstraintViolationException(exception); } } - private ApiError handleException() { - return exceptionController.handleConstraintViolationException(exception); - } } @Nested diff --git a/alfa-service/src/test/java/de/ozgcloud/alfa/common/errorhandling/ExceptionTestFactory.java b/alfa-service/src/test/java/de/ozgcloud/alfa/common/errorhandling/ExceptionTestFactory.java index d96c42787390b1b99df6bd412efce278c3dd7335..19cdbe389dc959ade05e069b6b820513c980d3c2 100644 --- a/alfa-service/src/test/java/de/ozgcloud/alfa/common/errorhandling/ExceptionTestFactory.java +++ b/alfa-service/src/test/java/de/ozgcloud/alfa/common/errorhandling/ExceptionTestFactory.java @@ -48,26 +48,26 @@ public class ExceptionTestFactory { static final String PARAM_DYNAMIC_VALUE = "20"; static final String MESSAGE = LoremIpsum.getInstance().getWords(5); static final String MESSAGE_CODE = "message.code"; - private static final String PATH_SUFFIX = "createCommandByRelation"; + private static final String PATH_PREFIX = "createCommandByRelation"; static final String PATH_FIELD = "command.wiedervorlage.betreff"; - private static final String PATH = PATH_SUFFIX + "." + PATH_FIELD; + public static final String PATH = PATH_PREFIX + "." + PATH_FIELD; public static ConstraintViolation<?> buildMockedConstraintViolation() { return ExceptionTestFactory.buildMockedConstraintViolationWithDynamicPayload(null); } - @SuppressWarnings({ "rawtypes", "unchecked" }) + @SuppressWarnings({ "unchecked" }) public static <T> ConstraintViolation<T> buildMockedConstraintViolationWithDynamicPayload(DynamicViolationParameter dynamicViolationParameter) { - ConstraintViolation violation = mock(ConstraintViolation.class); - HibernateConstraintViolation hibernateViolation = mock(HibernateConstraintViolation.class); - ConstraintDescriptor constraintDescriptor = mock(ConstraintDescriptor.class); + var violation = mock(ConstraintViolation.class); + var hibernateViolation = mock(HibernateConstraintViolation.class); + var constraintDescriptor = mock(ConstraintDescriptor.class); var path = mock(Path.class); when(path.toString()).thenReturn(PATH); when(violation.getPropertyPath()).thenReturn(path); - when(violation.getMessageTemplate()).thenReturn("{" + MESSAGE_CODE + "}"); when(violation.getMessage()).thenReturn(MESSAGE); when(violation.getConstraintDescriptor()).thenReturn(constraintDescriptor); + when(violation.getInvalidValue()).thenReturn(PARAM_VALUE); when(constraintDescriptor.getAttributes()).thenReturn(Map.of(PARAM_NAME, PARAM_DYNAMIC_VALUE)); when(violation.unwrap(any())).thenReturn(hibernateViolation); when(hibernateViolation.getDynamicPayload(any())).thenReturn(dynamicViolationParameter); diff --git a/alfa-service/src/test/java/de/ozgcloud/alfa/common/errorhandling/IssueParamTestFactory.java b/alfa-service/src/test/java/de/ozgcloud/alfa/common/errorhandling/IssueParamTestFactory.java new file mode 100644 index 0000000000000000000000000000000000000000..2a9a035c179a40230996e0d97cc72ebbb0df7366 --- /dev/null +++ b/alfa-service/src/test/java/de/ozgcloud/alfa/common/errorhandling/IssueParamTestFactory.java @@ -0,0 +1,17 @@ +package de.ozgcloud.alfa.common.errorhandling; + +import de.ozgcloud.alfa.common.errorhandling.IssueParam.IssueParamBuilder; + +public class IssueParamTestFactory { + public static final String PARAM_NAME = ExceptionTestFactory.PARAM_NAME; + public static final String PARAM_DYNAMIC_VALUE = ExceptionTestFactory.PARAM_DYNAMIC_VALUE; + + public static IssueParam create() { + return createBuilder() + .build(); + } + + public static IssueParamBuilder createBuilder() { + return IssueParam.builder().name(PARAM_NAME).value(PARAM_DYNAMIC_VALUE); + } +} diff --git a/alfa-service/src/test/java/de/ozgcloud/alfa/common/errorhandling/ProblemDetailMapperTest.java b/alfa-service/src/test/java/de/ozgcloud/alfa/common/errorhandling/ProblemDetailMapperTest.java new file mode 100644 index 0000000000000000000000000000000000000000..b55e6f4ee9a7f15c902446f1846893ca4614f3b0 --- /dev/null +++ b/alfa-service/src/test/java/de/ozgcloud/alfa/common/errorhandling/ProblemDetailMapperTest.java @@ -0,0 +1,313 @@ +package de.ozgcloud.alfa.common.errorhandling; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import java.util.AbstractMap.SimpleEntry; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.stream.Stream; + +import jakarta.validation.ConstraintViolation; +import jakarta.validation.ConstraintViolationException; +import jakarta.validation.metadata.ConstraintDescriptor; + +import org.hibernate.validator.engine.HibernateConstraintViolation; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.Spy; +import org.springframework.http.HttpStatus; +import org.springframework.http.ProblemDetail; + +import com.thedeanda.lorem.LoremIpsum; + +import de.ozgcloud.alfa.common.binaryfile.DynamicViolationParameter; +import de.ozgcloud.common.errorhandling.ExceptionUtil; + +class ProblemDetailMapperTest { + + @Spy + private ProblemDetailMapper mapper; + + @Nested + class TestFromConstraintViolationException { + private final String exceptionMessage = LoremIpsum.getInstance().getWords(5); + private final Set<ConstraintViolation<?>> violations = Collections.singleton(ExceptionTestFactory.buildMockedConstraintViolation()); + private final ConstraintViolationException exception = new ConstraintViolationException(exceptionMessage, violations); + private final String message = LoremIpsum.getInstance().getWords(5); + + @BeforeEach + void mockMapper() { + doReturn(message).when(mapper).buildMessageWithExceptionId(exception); + } + + @Test + void shouldGetMessageWithExcpetionId() { + callMapper(); + + verify(mapper).buildMessageWithExceptionId(exception); + } + + @Test + void shouldHaveStatusUnprocessableEntity() { + var problemDetail = callMapper(); + + assertThat(problemDetail.getStatus()).isEqualTo(HttpStatus.UNPROCESSABLE_ENTITY.value()); + } + + @Test + void shouldHaveMessageInDetail() { + var problemDetail = callMapper(); + + assertThat(problemDetail.getDetail()).contains(message); + } + + @Test + void shouldGetDetailedViolationList() { + callMapper(); + + verify(mapper).buildInvalidParams(violations); + } + + @Test + void shouldSetPropertyInvalidParams() { + var expectedInvalidParamsValue = Map.of(); + doReturn(Stream.of(expectedInvalidParamsValue)).when(mapper).buildInvalidParams(violations); + + var problemDetail = callMapper(); + + assertThat(problemDetail.getProperties()) + .containsExactly(new SimpleEntry<String, Object>(ProblemDetailMapper.INVALID_PARAMS, List.of(expectedInvalidParamsValue))); + } + + private ProblemDetail callMapper() { + return mapper.fromConstraintViolationException(exception); + } + } + + @Nested + class TestBuildMessageWithExceptionId { + + private final String exceptionMessage = LoremIpsum.getInstance().getWords(5); + private final String exceptionId = UUID.randomUUID().toString(); + private final ConstraintViolationException exception = new ConstraintViolationException(exceptionMessage, null); + + @BeforeEach + void mockCreateExcpetionId() { + doReturn(exceptionId).when(mapper).createExceptionId(); + } + + @Test + void shouldCreateExceptionId() { + callMapper(); + + verify(mapper).createExceptionId(); + } + + @Test + void shouldFormatMessageWithExceptionId() { + var messageWithId = callMapper(); + + assertThat(messageWithId).isEqualTo(ExceptionUtil.formatMessageWithExceptionId(exceptionMessage, exceptionId)); + } + + private String callMapper() { + return mapper.buildMessageWithExceptionId(exception); + } + } + + @Nested + class TestBuildInvalidParams { + + @Nested + class OnViolations { + private final Set<ConstraintViolation<?>> violations = Set.of(ExceptionTestFactory.buildMockedConstraintViolation(), + ExceptionTestFactory.buildMockedConstraintViolation()); + + @Test + void shouldCallBuildDetailedViolation() { + callMapper().toList(); + + violations.forEach(violation -> verify(mapper).buildDetailedViolation(violation)); + } + + @Test + void shouldReturnListWithDetailedViolations() { + Map<String, Object> detailsMap = Map.of(LoremIpsum.getInstance().getWords(1), LoremIpsum.getInstance().getWords(1)); + violations.forEach(violation -> doReturn(detailsMap).when(mapper).buildDetailedViolation(violation)); + + var detailedViolations = callMapper(); + + assertThat(detailedViolations).containsExactly(detailsMap, detailsMap); + } + + private Stream<Map<String, Object>> callMapper() { + return mapper.buildInvalidParams(violations); + } + } + + @Nested + class OnEmptyViolations { + private final Set<ConstraintViolation<?>> violations = Collections.emptySet(); + + @Test + void shouldCallNotBuildDetailedViolation() { + callMapper(); + + verify(mapper, never()).buildDetailedViolation(any()); + } + + @Test + void shouldReturnListWithDetailedViolations() { + var detailedViolations = callMapper(); + + assertThat(detailedViolations).isEmpty(); + } + + private Stream<Map<String, Object>> callMapper() { + return mapper.buildInvalidParams(violations); + } + } + } + + @Nested + class TestBuildDetailedViolation { + private final ConstraintViolation<?> violation = ExceptionTestFactory.buildMockedConstraintViolation(); + + @Test + void shouldContainFieldName() { + var expectedEntry = new SimpleEntry<>(ProblemDetailMapper.INVALID_PARAMS_KEY_NAME, ExceptionTestFactory.PATH); + + var detailedViolation = callMapper(); + + assertThat(detailedViolation).contains(expectedEntry); + } + + @Test + void shouldContainValue() { + var expectedEntry = new SimpleEntry<>(ProblemDetailMapper.INVALID_PARAMS_KEY_VALUE, ExceptionTestFactory.PARAM_VALUE); + + var detailedViolation = callMapper(); + + assertThat(detailedViolation).contains(expectedEntry); + } + + @Test + void shouldHandleNullValue() { + when(violation.getInvalidValue()).thenReturn(null); + var expectedEntry = new SimpleEntry<>(ProblemDetailMapper.INVALID_PARAMS_KEY_VALUE, + ProblemDetailMapper.PROVIDED_VALUE_WAS_NULL); + + var detailedViolation = callMapper(); + + assertThat(detailedViolation).contains(expectedEntry); + } + + @Test + void shouldContainReason() { + var expectedEntry = new SimpleEntry<>(ProblemDetailMapper.INVALID_PARAMS_KEY_REASON, ExceptionTestFactory.MESSAGE); + + var detailedViolation = callMapper(); + + assertThat(detailedViolation).contains(expectedEntry); + } + + @Test + void shouldBuildParameters() { + callMapper(); + + verify(mapper).buildParameters(violation); + } + + @Test + void shouldContainConstraintParameters() { + var issueParameter = IssueParamTestFactory.create(); + doReturn(Stream.of(issueParameter)).when(mapper).buildParameters(violation); + var expectedEntry = new SimpleEntry<String, Object>(ProblemDetailMapper.INVALID_PARAMS_KEY_CONSTRAINT_PARAMETERS, + List.of(issueParameter)); + + var detailedViolation = callMapper(); + + assertThat(detailedViolation).contains(expectedEntry); + } + + private Map<String, Object> callMapper() { + return mapper.buildDetailedViolation(violation); + } + } + + @Nested + class TestBuildParameters { + + @Mock + private ConstraintViolation<?> violation; + + @Mock + @SuppressWarnings("rawtypes") + private ConstraintDescriptor constraintDescriptor; + + @Mock + @SuppressWarnings("rawtypes") + private HibernateConstraintViolation hibernateConstraintViolation; + + @SuppressWarnings("unchecked") + @BeforeEach + void setUpViolationMocks() { + when(violation.getConstraintDescriptor()).thenReturn(constraintDescriptor); + when(violation.unwrap(HibernateConstraintViolation.class)).thenReturn(hibernateConstraintViolation); + when(constraintDescriptor.getAttributes()) + .thenReturn(Map.of(ExceptionTestFactory.PARAM_NAME, ExceptionTestFactory.PARAM_DYNAMIC_VALUE)); + } + + @Nested + class OnNonDynamicPayload { + + @Test + void shouldBuildIssueParam() { + var issueParams = mapper.buildParameters(violation); + + assertThat(issueParams).usingRecursiveFieldByFieldElementComparator().contains(IssueParamTestFactory.create()); + } + + } + + @Nested + class TestWithDynamicPayload { + + @SuppressWarnings("unchecked") + @Test + void shouldHaveReplacedIssueParameterName() { + var dynamicValue = LoremIpsum.getInstance().getWords(1); + var dynamicViolationParameter = DynamicViolationParameter.builder() + .map(Map.of(ExceptionTestFactory.PARAM_NAME, dynamicValue)) + .build(); + when(hibernateConstraintViolation.getDynamicPayload(DynamicViolationParameter.class)).thenReturn(dynamicViolationParameter); + var expectedIssueParam = IssueParamTestFactory.createBuilder().value(dynamicValue).build(); + + var issueParams = mapper.buildParameters(violation); + + assertThat(issueParams).usingRecursiveFieldByFieldElementComparator().contains(expectedIssueParam); + } + + @SuppressWarnings("unchecked") + @Test + void shouldIgnoreKeyNotPresentInEntry() { + var dynamicViolationParameter = DynamicViolationParameter.builder() + .map(Map.of(LoremIpsum.getInstance().getWords(1), LoremIpsum.getInstance().getWords(1))) + .build(); + when(hibernateConstraintViolation.getDynamicPayload(DynamicViolationParameter.class)).thenReturn(dynamicViolationParameter); + var expectedIssueParam = IssueParamTestFactory.create(); + + var issueParams = mapper.buildParameters(violation); + + assertThat(issueParams).usingRecursiveFieldByFieldElementComparator().contains(expectedIssueParam); + } + } + } +} diff --git a/alfa-service/src/test/java/de/ozgcloud/alfa/kommentar/KommentarCommandITCase.java b/alfa-service/src/test/java/de/ozgcloud/alfa/kommentar/KommentarCommandITCase.java index 87d5be0da2eae911f865f47380307fe8a7fa61b5..0eed2bf6a4c58bdce7d81ccb29f1de9d40ad372d 100644 --- a/alfa-service/src/test/java/de/ozgcloud/alfa/kommentar/KommentarCommandITCase.java +++ b/alfa-service/src/test/java/de/ozgcloud/alfa/kommentar/KommentarCommandITCase.java @@ -107,9 +107,9 @@ class KommentarCommandITCase { String content = buildContentWithText(null); doRequestByKommentarId(content).andExpect(status().isUnprocessableEntity()) - .andExpect(jsonPath("$.issues.length()").value(1)) - .andExpect(jsonPath("$.issues.[0].field").value("kommentar.text")) - .andExpect(jsonPath("$.issues.[0].messageCode").value(ValidationMessageCodes.FIELD_IS_EMPTY)); + .andExpect(jsonPath("$.invalidParams.length()").value(1)) + .andExpect(jsonPath("$.invalidParams[0].name").value("editKommentar.kommentar.text")) + .andExpect(jsonPath("$.invalidParams[0].reason").value(ValidationMessageCodes.FIELD_IS_EMPTY)); } @SneakyThrows @@ -119,7 +119,7 @@ class KommentarCommandITCase { String content = buildContentWithText(StringUtils.EMPTY); doRequestByKommentarId(content).andExpect(status().isUnprocessableEntity()) - .andExpect(jsonPath("$.issues.[0].field").value("kommentar.text")); + .andExpect(jsonPath("$.invalidParams[0].name").value("editKommentar.kommentar.text")); } @@ -130,7 +130,7 @@ class KommentarCommandITCase { String content = buildContentWithText(StringUtils.EMPTY); doRequestByKommentarId(content).andExpect(status().isUnprocessableEntity()) - .andExpect(jsonPath("$.issues[0].parameters.length()").value(2)); + .andExpect(jsonPath("$.invalidParams[0].constraintParameters.length()").value(2)); } private String buildContentWithText(String text) { @@ -190,9 +190,9 @@ class KommentarCommandITCase { String content = buildContentWithText(null); doRequestByVorgangId(content).andExpect(status().isUnprocessableEntity()) - .andExpect(jsonPath("$.issues.length()").value(1)) - .andExpect(jsonPath("$.issues.[0].field").value("kommentar.text")) - .andExpect(jsonPath("$.issues.[0].messageCode").value(ValidationMessageCodes.FIELD_IS_EMPTY)); + .andExpect(jsonPath("$.invalidParams.length()").value(1)) + .andExpect(jsonPath("$.invalidParams.[0].name").value("createKommentar.kommentar.text")) + .andExpect(jsonPath("$.invalidParams.[0].reason").value(ValidationMessageCodes.FIELD_IS_EMPTY)); } @SneakyThrows @@ -202,7 +202,7 @@ class KommentarCommandITCase { String content = buildContentWithText(StringUtils.EMPTY); doRequestByVorgangId(content).andExpect(status().isUnprocessableEntity()) - .andExpect(jsonPath("$.issues.[0].field").value("kommentar.text")); + .andExpect(jsonPath("$.invalidParams.[0].name").value("createKommentar.kommentar.text")); } @@ -213,7 +213,7 @@ class KommentarCommandITCase { String content = buildContentWithText(StringUtils.EMPTY); doRequestByVorgangId(content).andExpect(status().isUnprocessableEntity()) - .andExpect(jsonPath("$.issues[0].parameters.length()").value(2)); + .andExpect(jsonPath("$.invalidParams[0].constraintParameters.length()").value(2)); } private String buildContentWithText(String text) { diff --git a/alfa-service/src/test/java/de/ozgcloud/alfa/loeschanforderung/LoeschAnforderungByVorgangControllerTest.java b/alfa-service/src/test/java/de/ozgcloud/alfa/loeschanforderung/LoeschAnforderungByVorgangControllerTest.java index 63101495e8422e6f26a9be4389fe19c995f30853..c27019e2998e2ef073b0caf0c15a8daf07f15e8f 100644 --- a/alfa-service/src/test/java/de/ozgcloud/alfa/loeschanforderung/LoeschAnforderungByVorgangControllerTest.java +++ b/alfa-service/src/test/java/de/ozgcloud/alfa/loeschanforderung/LoeschAnforderungByVorgangControllerTest.java @@ -26,6 +26,7 @@ import de.ozgcloud.alfa.common.command.CommandOrder; import de.ozgcloud.alfa.common.command.CommandTestFactory; import de.ozgcloud.alfa.common.command.CreateCommand; import de.ozgcloud.alfa.common.errorhandling.ExceptionController; +import de.ozgcloud.alfa.common.errorhandling.ProblemDetailMapper; import de.ozgcloud.alfa.loeschanforderung.LoeschAnforderungController.LoeschAnforderungByVorgangController; import de.ozgcloud.alfa.vorgang.VorgangController; import de.ozgcloud.alfa.vorgang.VorgangHeaderTestFactory; @@ -44,11 +45,14 @@ class LoeschAnforderungByVorgangControllerTest { @Mock private VorgangController vorgangController; + @Mock + private ProblemDetailMapper problemDetailMapper; + private MockMvc mockMvc; @BeforeEach void init() { - mockMvc = MockMvcBuilders.standaloneSetup(controller).setControllerAdvice(new ExceptionController()).build(); + mockMvc = MockMvcBuilders.standaloneSetup(controller).setControllerAdvice(new ExceptionController(problemDetailMapper)).build(); } @DisplayName("Create LoeschAnforderung") @@ -93,7 +97,7 @@ class LoeschAnforderungByVorgangControllerTest { private ResultActions doRequest() throws Exception { var requestBody = CommandTestFactory.buildCreateVorgangCommandContent(CommandOrder.VORGANG_ZUM_LOESCHEN_MARKIEREN.name()); return mockMvc.perform(post(LoeschAnforderungByVorgangController.BASE_PATH, - VorgangHeaderTestFactory.ID, VorgangHeaderTestFactory.VERSION) + VorgangHeaderTestFactory.ID, VorgangHeaderTestFactory.VERSION) .content(requestBody).contentType(MediaType.APPLICATION_JSON).characterEncoding(StandardCharsets.UTF_8.name())) .andExpect(status().isCreated()); } diff --git a/alfa-service/src/test/java/de/ozgcloud/alfa/loeschanforderung/LoeschAnforderungCommandControllerTest.java b/alfa-service/src/test/java/de/ozgcloud/alfa/loeschanforderung/LoeschAnforderungCommandControllerTest.java index 392b53911f57a0836a38022192c77c7d6b72411c..ad87a8ced732c2ec48ef91262ce188a9647aabf3 100644 --- a/alfa-service/src/test/java/de/ozgcloud/alfa/loeschanforderung/LoeschAnforderungCommandControllerTest.java +++ b/alfa-service/src/test/java/de/ozgcloud/alfa/loeschanforderung/LoeschAnforderungCommandControllerTest.java @@ -3,8 +3,8 @@ package de.ozgcloud.alfa.loeschanforderung; import static de.ozgcloud.alfa.common.command.CommandController.*; import static org.assertj.core.api.Assertions.*; import static org.hamcrest.CoreMatchers.*; -import static org.mockito.ArgumentMatchers.*; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; @@ -40,6 +40,7 @@ import de.ozgcloud.alfa.common.command.CreateCommand; import de.ozgcloud.alfa.common.command.LegacyOrder; import de.ozgcloud.alfa.common.command.StatusPatch; import de.ozgcloud.alfa.common.errorhandling.ExceptionController; +import de.ozgcloud.alfa.common.errorhandling.ProblemDetailMapper; import de.ozgcloud.alfa.vorgang.VorgangController; import de.ozgcloud.alfa.vorgang.VorgangHeaderTestFactory; import de.ozgcloud.alfa.vorgang.VorgangWithEingang; @@ -64,11 +65,14 @@ class LoeschAnforderungCommandControllerTest { @Mock private CommandController commandController; + @Mock + private ProblemDetailMapper problemDetailMapper; + private MockMvc mockMvc; @BeforeEach void init() { - mockMvc = MockMvcBuilders.standaloneSetup(controller).setControllerAdvice(new ExceptionController()).build(); + mockMvc = MockMvcBuilders.standaloneSetup(controller).setControllerAdvice(new ExceptionController(problemDetailMapper)).build(); } @DisplayName("Create command") diff --git a/alfa-service/src/test/java/de/ozgcloud/alfa/loeschanforderung/LoeschAnforderungControllerTest.java b/alfa-service/src/test/java/de/ozgcloud/alfa/loeschanforderung/LoeschAnforderungControllerTest.java index a0ea03964f79264344bb89b24f16bddcc4cf0ed1..0ccd8259172c85ae64c5b55356d9eadc30e2b9a1 100644 --- a/alfa-service/src/test/java/de/ozgcloud/alfa/loeschanforderung/LoeschAnforderungControllerTest.java +++ b/alfa-service/src/test/java/de/ozgcloud/alfa/loeschanforderung/LoeschAnforderungControllerTest.java @@ -17,6 +17,7 @@ import org.springframework.test.web.servlet.setup.MockMvcBuilders; import de.ozgcloud.alfa.common.UserProfileUrlProvider; import de.ozgcloud.alfa.common.errorhandling.ExceptionController; +import de.ozgcloud.alfa.common.errorhandling.ProblemDetailMapper; import lombok.SneakyThrows; class LoeschAnforderungControllerTest { @@ -30,11 +31,14 @@ class LoeschAnforderungControllerTest { @Mock private LoeschAnforderungModelAssembler modelAssembler; + @Mock + private ProblemDetailMapper problemDetailMapper; + private MockMvc mockMvc; @BeforeEach void init() { - mockMvc = MockMvcBuilders.standaloneSetup(controller).setControllerAdvice(new ExceptionController()).build(); + mockMvc = MockMvcBuilders.standaloneSetup(controller).setControllerAdvice(new ExceptionController(problemDetailMapper)).build(); } @Nested diff --git a/alfa-service/src/test/java/de/ozgcloud/alfa/wiedervorlage/WiedervorlageCommandITCase.java b/alfa-service/src/test/java/de/ozgcloud/alfa/wiedervorlage/WiedervorlageCommandITCase.java index 88a17b72ba185204edb3514e4a2bfcd90bcf53a6..d0328ea3258d380bf799e934d07e4f20227c4528 100644 --- a/alfa-service/src/test/java/de/ozgcloud/alfa/wiedervorlage/WiedervorlageCommandITCase.java +++ b/alfa-service/src/test/java/de/ozgcloud/alfa/wiedervorlage/WiedervorlageCommandITCase.java @@ -103,9 +103,9 @@ class WiedervorlageCommandITCase { String content = buildContentWithBetreff(null); doRequest(content).andExpect(status().isUnprocessableEntity()) - .andExpect(jsonPath("$.issues.length()").value(1)) - .andExpect(jsonPath("$.issues.[0].field").value("wiedervorlage.betreff")) - .andExpect(jsonPath("$.issues.[0].messageCode").value(ValidationMessageCodes.FIELD_IS_EMPTY)); + .andExpect(jsonPath("$.invalidParams.length()").value(1)) + .andExpect(jsonPath("$.invalidParams[0].name").value("editWiedervorlage.wiedervorlage.betreff")) + .andExpect(jsonPath("$.invalidParams[0].reason").value(ValidationMessageCodes.FIELD_IS_EMPTY)); } @SneakyThrows @@ -115,7 +115,7 @@ class WiedervorlageCommandITCase { String content = buildContentWithBetreff("a"); doRequest(content).andExpect(status().isUnprocessableEntity()) - .andExpect(jsonPath("$.issues.[0].field").value("wiedervorlage.betreff")); + .andExpect(jsonPath("$.invalidParams[0].name").value("editWiedervorlage.wiedervorlage.betreff")); } @@ -125,7 +125,8 @@ class WiedervorlageCommandITCase { void minMaxParameter() { String content = buildContentWithBetreff("a"); - doRequest(content).andExpect(status().isUnprocessableEntity()).andExpect(jsonPath("$.issues[0].parameters.length()").value(2)); + doRequest(content).andExpect(status().isUnprocessableEntity()) + .andExpect(jsonPath("$.invalidParams[0].constraintParameters.length()").value(2)); } @SneakyThrows @@ -148,9 +149,9 @@ class WiedervorlageCommandITCase { String content = createEditContent(WiedervorlageTestFactory.createBuilder().frist(LocalDate.parse("2020-01-01")).build()); doRequest(content).andExpect(status().isUnprocessableEntity()) - .andExpect(jsonPath("$.issues.length()").value(1)) - .andExpect(jsonPath("$.issues.[0].field").value("wiedervorlage.frist")) - .andExpect(jsonPath("$.issues.[0].messageCode").value(ValidationMessageCodes.FIELD_DATE_PAST)); + .andExpect(jsonPath("$.invalidParams.length()").value(1)) + .andExpect(jsonPath("$.invalidParams[0].name").value("editWiedervorlage.wiedervorlage.frist")) + .andExpect(jsonPath("$.invalidParams[0].reason").value(ValidationMessageCodes.FIELD_DATE_PAST)); } @SneakyThrows @@ -160,9 +161,9 @@ class WiedervorlageCommandITCase { String content = createEditContent(WiedervorlageTestFactory.createBuilder().frist(null).build()); doRequest(content).andExpect(status().isUnprocessableEntity()) - .andExpect(jsonPath("$.issues.length()").value(1)) - .andExpect(jsonPath("$.issues.[0].field").value("wiedervorlage.frist")) - .andExpect(jsonPath("$.issues.[0].messageCode").value(ValidationMessageCodes.FIELD_IS_EMPTY)); + .andExpect(jsonPath("$.invalidParams.length()").value(1)) + .andExpect(jsonPath("$.invalidParams[0].name").value("editWiedervorlage.wiedervorlage.frist")) + .andExpect(jsonPath("$.invalidParams[0].reason").value(ValidationMessageCodes.FIELD_IS_EMPTY)); } private String createEditContent(Wiedervorlage wiedervorlage) { @@ -225,9 +226,9 @@ class WiedervorlageCommandITCase { String content = buildContentWithBetreff(null); doRequest(content).andExpect(status().isUnprocessableEntity()) - .andExpect(jsonPath("$.issues.length()").value(1)) - .andExpect(jsonPath("$.issues.[0].field").value("wiedervorlage.betreff")) - .andExpect(jsonPath("$.issues.[0].messageCode").value(ValidationMessageCodes.FIELD_IS_EMPTY)); + .andExpect(jsonPath("$.invalidParams.length()").value(1)) + .andExpect(jsonPath("$.invalidParams[0].name").value("createWiedervorlage.wiedervorlage.betreff")) + .andExpect(jsonPath("$.invalidParams[0].reason").value(ValidationMessageCodes.FIELD_IS_EMPTY)); } @SneakyThrows @@ -237,7 +238,7 @@ class WiedervorlageCommandITCase { String content = buildContentWithBetreff("a"); doRequest(content).andExpect(status().isUnprocessableEntity()) - .andExpect(jsonPath("$.issues.[0].field").value("wiedervorlage.betreff")); + .andExpect(jsonPath("$.invalidParams[0].name").value("createWiedervorlage.wiedervorlage.betreff")); } @@ -247,7 +248,8 @@ class WiedervorlageCommandITCase { void minMaxParameter() { String content = buildContentWithBetreff("a"); - doRequest(content).andExpect(status().isUnprocessableEntity()).andExpect(jsonPath("$.issues[0].parameters.length()").value(2)); + doRequest(content).andExpect(status().isUnprocessableEntity()) + .andExpect(jsonPath("$.invalidParams[0].constraintParameters.length()").value(2)); } @SneakyThrows @@ -272,9 +274,9 @@ class WiedervorlageCommandITCase { createWithWiedervorlage(WiedervorlageTestFactory.createBuilder().frist(LocalDate.parse("2020-01-01")).build())); doRequest(content).andExpect(status().isUnprocessableEntity()) - .andExpect(jsonPath("$.issues.length()").value(1)) - .andExpect(jsonPath("$.issues.[0].field").value("wiedervorlage.frist")) - .andExpect(jsonPath("$.issues.[0].messageCode").value(ValidationMessageCodes.FIELD_DATE_PAST)); + .andExpect(jsonPath("$.invalidParams.length()").value(1)) + .andExpect(jsonPath("$.invalidParams[0].name").value("createWiedervorlage.wiedervorlage.frist")) + .andExpect(jsonPath("$.invalidParams[0].reason").value(ValidationMessageCodes.FIELD_DATE_PAST)); } @SneakyThrows @@ -285,9 +287,9 @@ class WiedervorlageCommandITCase { createWithWiedervorlage(WiedervorlageTestFactory.createBuilder().frist(null).build())); doRequest(content).andExpect(status().isUnprocessableEntity()) - .andExpect(jsonPath("$.issues.length()").value(1)) - .andExpect(jsonPath("$.issues.[0].field").value("wiedervorlage.frist")) - .andExpect(jsonPath("$.issues.[0].messageCode").value(ValidationMessageCodes.FIELD_IS_EMPTY)); + .andExpect(jsonPath("$.invalidParams.length()").value(1)) + .andExpect(jsonPath("$.invalidParams[0].name").value("createWiedervorlage.wiedervorlage.frist")) + .andExpect(jsonPath("$.invalidParams[0].reason").value(ValidationMessageCodes.FIELD_IS_EMPTY)); } }