Skip to content
Snippets Groups Projects
Commit 50762621 authored by OZGCloud's avatar OZGCloud
Browse files

OZG-359 OZG-3352 handle "concurrent modification" API exception

parent acaabee5
No related branches found
No related tags found
No related merge requests found
Showing
with 337 additions and 79 deletions
...@@ -21,7 +21,9 @@ ...@@ -21,7 +21,9 @@
* Die sprachspezifischen Genehmigungen und Beschränkungen * Die sprachspezifischen Genehmigungen und Beschränkungen
* unter der Lizenz sind dem Lizenztext zu entnehmen. * unter der Lizenz sind dem Lizenztext zu entnehmen.
*/ */
export * from './lib/+state/command.actions';
export * from './lib/command-shared.module'; export * from './lib/command-shared.module';
export * from './lib/command.model'; export * from './lib/command.model';
export * from './lib/command.service'; export * from './lib/command.service';
export * from './lib/command.util'; export * from './lib/command.util';
/*
* Copyright (C) 2022 Das Land Schleswig-Holstein vertreten durch den
* Ministerpräsidenten des Landes Schleswig-Holstein
* Staatskanzlei
* Abteilung Digitalisierung und zentrales IT-Management der Landesregierung
*
* Lizenziert unter der EUPL, Version 1.2 oder - sobald
* diese von der Europäischen Kommission genehmigt wurden -
* Folgeversionen der EUPL ("Lizenz");
* Sie dürfen dieses Werk ausschließlich gemäß
* dieser Lizenz nutzen.
* Eine Kopie der Lizenz finden Sie hier:
*
* https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
*
* Sofern nicht durch anwendbare Rechtsvorschriften
* gefordert oder in schriftlicher Form vereinbart, wird
* die unter der Lizenz verbreitete Software "so wie sie
* ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN -
* ausdrücklich oder stillschweigend - verbreitet.
* Die sprachspezifischen Genehmigungen und Beschränkungen
* unter der Lizenz sind dem Lizenztext zu entnehmen.
*/
import { TypedActionCreator } from '@goofy-client/tech-shared';
import { createAction } from '@ngrx/store';
export const publishConcurrentModificationAction: TypedActionCreator = createAction('[Command/API] Concurrent Modification')
/*
* Copyright (C) 2022 Das Land Schleswig-Holstein vertreten durch den
* Ministerpräsidenten des Landes Schleswig-Holstein
* Staatskanzlei
* Abteilung Digitalisierung und zentrales IT-Management der Landesregierung
*
* Lizenziert unter der EUPL, Version 1.2 oder - sobald
* diese von der Europäischen Kommission genehmigt wurden -
* Folgeversionen der EUPL ("Lizenz");
* Sie dürfen dieses Werk ausschließlich gemäß
* dieser Lizenz nutzen.
* Eine Kopie der Lizenz finden Sie hier:
*
* https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
*
* Sofern nicht durch anwendbare Rechtsvorschriften
* gefordert oder in schriftlicher Form vereinbart, wird
* die unter der Lizenz verbreitete Software "so wie sie
* ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN -
* ausdrücklich oder stillschweigend - verbreitet.
* Die sprachspezifischen Genehmigungen und Beschränkungen
* unter der Lizenz sind dem Lizenztext zu entnehmen.
*/
import { Mock, mock, useFromMock } from '@goofy-client/test-utils';
import { Store } from '@ngrx/store';
import { Subject } from 'rxjs';
import { CommandFacade } from './command.facade';
import * as CommandActions from './command.actions';
describe('CommandFacade', () => {
let facade: CommandFacade;
let store: Mock<Store>;
let selectionSubject: Subject<any>;
beforeEach(() => {
store = mock(Store);
selectionSubject = new Subject();
store.select.mockReturnValue(selectionSubject);
store.dispatch = jest.fn();
facade = new CommandFacade(useFromMock(<any>store));
})
it('is initialized', () => {
expect(facade).toBeTruthy();
})
describe('dispatchConcurrentModificationCommand', () => {
it('should dispatch "publishConcurrentModificationAction" action', () => {
facade.dispatchConcurrentModificationCommand();
expect(store.dispatch).toHaveBeenCalledWith(CommandActions.publishConcurrentModificationAction());
});
})
})
\ No newline at end of file
/*
* Copyright (C) 2022 Das Land Schleswig-Holstein vertreten durch den
* Ministerpräsidenten des Landes Schleswig-Holstein
* Staatskanzlei
* Abteilung Digitalisierung und zentrales IT-Management der Landesregierung
*
* Lizenziert unter der EUPL, Version 1.2 oder - sobald
* diese von der Europäischen Kommission genehmigt wurden -
* Folgeversionen der EUPL ("Lizenz");
* Sie dürfen dieses Werk ausschließlich gemäß
* dieser Lizenz nutzen.
* Eine Kopie der Lizenz finden Sie hier:
*
* https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
*
* Sofern nicht durch anwendbare Rechtsvorschriften
* gefordert oder in schriftlicher Form vereinbart, wird
* die unter der Lizenz verbreitete Software "so wie sie
* ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN -
* ausdrücklich oder stillschweigend - verbreitet.
* Die sprachspezifischen Genehmigungen und Beschränkungen
* unter der Lizenz sind dem Lizenztext zu entnehmen.
*/
import { Injectable } from '@angular/core';
import { Store } from '@ngrx/store';
import * as CommandActions from './command.actions';
@Injectable()
export class CommandFacade {
constructor(private readonly store: Store) { }
public dispatchConcurrentModificationCommand(): void {
this.store.dispatch(CommandActions.publishConcurrentModificationAction());
}
}
\ No newline at end of file
...@@ -23,8 +23,11 @@ ...@@ -23,8 +23,11 @@
*/ */
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { CommandFacade } from './+state/command.facade';
@NgModule({ @NgModule({
imports: [CommonModule] imports: [CommonModule],
providers:[CommandFacade]
}) })
export class CommandSharedModule { } export class CommandSharedModule { }
export enum CommandErrorMessage {
CONCURRENT_MODIFICATION = 'concurrent modification'
}
export const COMMAND_ERROR_MESSAGES: { [code: string]: string } = {
[CommandErrorMessage.CONCURRENT_MODIFICATION]: 'Der Vorgang wurde zwischenzeitlich verändert und wurde neu geladen.',
}
...@@ -25,21 +25,27 @@ import { HttpErrorResponse, HttpStatusCode } from '@angular/common/http'; ...@@ -25,21 +25,27 @@ import { HttpErrorResponse, HttpStatusCode } from '@angular/common/http';
import { faker } from '@faker-js/faker'; import { faker } from '@faker-js/faker';
import { createEmptyStateResource, createErrorStateResource, createStateResource, StateResource } from '@goofy-client/tech-shared'; import { createEmptyStateResource, createErrorStateResource, createStateResource, StateResource } from '@goofy-client/tech-shared';
import { mock, Mock, useFromMock } from '@goofy-client/test-utils'; import { mock, Mock, useFromMock } from '@goofy-client/test-utils';
import { SnackBarService } from '@goofy-client/ui';
import { Resource } from '@ngxp/rest'; import { Resource } from '@ngxp/rest';
import { cold, hot } from 'jest-marbles'; import { cold, hot } from 'jest-marbles';
import { createCommand, createCommandResource } from 'libs/command-shared/test/command'; import { createCommand, createCommandErrorResource, createCommandResource } from 'libs/command-shared/test/command';
import { createHttpErrorResponse } from 'libs/tech-shared/test/http'; import { createHttpErrorResponse } from 'libs/tech-shared/test/http';
import { toResource } from 'libs/tech-shared/test/resource'; import { toResource } from 'libs/tech-shared/test/resource';
import { Observable, throwError } from 'rxjs'; import { Observable, of, throwError } from 'rxjs';
import { CommandFacade } from './+state/command.facade';
import { CommandLinkRel } from './command.linkrel'; import { CommandLinkRel } from './command.linkrel';
import { CommandErrorMessage } from './command.message';
import { Command, CommandResource } from './command.model'; import { Command, CommandResource } from './command.model';
import { CommandRepository } from './command.repository'; import { CommandRepository } from './command.repository';
import { CommandService, startInterval } from './command.service'; import { CommandService, IntervallHandleWithTickObservable, startInterval } from './command.service';
describe('CommandService', () => { describe('CommandService', () => {
let service; let service: CommandService;
let repository: Mock<CommandRepository>; let repository: Mock<CommandRepository>;
const snackbarService: Mock<SnackBarService> = mock(SnackBarService);
const commandFacade: Mock<CommandFacade> = mock(CommandFacade);
const command: Command = createCommand(); const command: Command = createCommand();
const commandResource: CommandResource = createCommandResource(); const commandResource: CommandResource = createCommandResource();
...@@ -49,18 +55,16 @@ describe('CommandService', () => { ...@@ -49,18 +55,16 @@ describe('CommandService', () => {
beforeEach(() => { beforeEach(() => {
repository = mock(CommandRepository); repository = mock(CommandRepository);
service = new CommandService(useFromMock(repository)); service = new CommandService(useFromMock(repository), useFromMock(snackbarService), useFromMock(commandFacade));
}) })
describe('create command', () => { describe('create command', () => {
const commandResource: CommandResource = commandResourceWithUpdateLink;
const resource: Resource = toResource({}); const resource: Resource = toResource({});
const linkRel: string = faker.random.word(); const linkRel: string = faker.random.word();
beforeEach(() => { beforeEach(() => {
repository.createCommand.mockReturnValue(cold('a', { a: commandResource })); repository.createCommand.mockReturnValue(cold('a', { a: commandResourceWithUpdateLink }));
}) })
it('should call repository with resource, linkrel and command', () => { it('should call repository with resource, linkrel and command', () => {
...@@ -69,18 +73,64 @@ describe('CommandService', () => { ...@@ -69,18 +73,64 @@ describe('CommandService', () => {
expect(repository.createCommand).toHaveBeenCalledWith(resource, linkRel, command); expect(repository.createCommand).toHaveBeenCalledWith(resource, linkRel, command);
}) })
describe('should return value', () => { it('should call handleHttpError', () => {
const errorResponse: HttpErrorResponse = createHttpErrorResponse(); const errorResponse: HttpErrorResponse = createHttpErrorResponse();
it('should return apiError as stateResource', () => {
repository.createCommand.mockReturnValue(throwError(errorResponse)); repository.createCommand.mockReturnValue(throwError(errorResponse));
service.handleError = jest.fn(); service.handleHttpError = jest.fn();
service.createCommand(resource, linkRel, command).subscribe(); service.createCommand(resource, linkRel, command).subscribe();
expect(service.handleError).toHaveBeenCalledWith(errorResponse); expect(service.handleHttpError).toHaveBeenCalledWith(errorResponse);
})
it('should call handleCreatedCommand', () => {
repository.createCommand.mockReturnValue(of(commandResource));
service.handleCreatedCommand = jest.fn();
service.createCommand(resource, linkRel, command).subscribe();
expect(service.handleCreatedCommand).toHaveBeenCalledWith(commandResource);
})
describe('handleCreatedCommand', () => {
it('should call handleCommandError', () => {
service.handleCommandError = jest.fn();
const commandWithError: CommandResource = createCommandErrorResource(CommandErrorMessage.CONCURRENT_MODIFICATION);
service.handleCreatedCommand(commandWithError);
expect(service.handleCommandError).toHaveBeenCalledWith(commandWithError);
})
it('should call startPolling', () => {
const commandWithoutError: CommandResource = {...createCommandResource(), errorMessage: null};
service.startPolling = jest.fn();
service.handleCreatedCommand(commandWithoutError);
expect(service.startPolling).toHaveBeenCalledWith(commandWithoutError);
})
describe('handleCommandError', () => {
describe('on concurrent modifiation error', () => {
it('should call snackBarService on concurrentModification error', () => {
service.handleCommandError({...commandResource, errorMessage: 'concurrent modification'});
expect(snackbarService.showError).toHaveBeenCalledWith('Der Vorgang wurde zwischenzeitlich verändert und wurde neu geladen.');
})
it('should call command facade "dispatchConcurrentModificationCommand"', () => {
service.handleCommandError({...commandResource, errorMessage: 'concurrent modification'});
expect(commandFacade.dispatchConcurrentModificationCommand).toHaveBeenCalled();
})
})
it('should return command as stateResource', () => {
service.handleCommandError(commandResource).subscribe(() => {
expect(commandStateResource).toEqual(createStateResource(commandResource));
});
})
}) })
}) })
}) })
...@@ -102,7 +152,7 @@ describe('CommandService', () => { ...@@ -102,7 +152,7 @@ describe('CommandService', () => {
beforeEach(() => { beforeEach(() => {
repository.revokeCommand.mockReturnValue(cold('a', { a: commandResourceWithUpdateLink })); repository.revokeCommand.mockReturnValue(cold('a', { a: commandResourceWithUpdateLink }));
service.pollCommand = jest.fn(); service.pollCommand = jest.fn();
service.pollCommand.mockReturnValue(cold('a', { a: createStateResource(commandResourceWithUpdateLink, true) })); (<any>service.pollCommand).mockReturnValue(cold('a', { a: createStateResource(commandResourceWithUpdateLink, true) }));
}) })
it('should return value with loading true', () => { it('should return value with loading true', () => {
...@@ -124,7 +174,7 @@ describe('CommandService', () => { ...@@ -124,7 +174,7 @@ describe('CommandService', () => {
describe('handle interval', () => { describe('handle interval', () => {
const interval = { handle: () => null }; const interval: IntervallHandleWithTickObservable = { handle: 0, tickObservable: of(null) };
beforeEach(() => { beforeEach(() => {
service.clearInterval = jest.fn(); service.clearInterval = jest.fn();
...@@ -225,9 +275,9 @@ describe('CommandService', () => { ...@@ -225,9 +275,9 @@ describe('CommandService', () => {
}) })
it('should call repository', () => { it('should call repository', () => {
service.getPendingCommands(command, CommandLinkRel.SELF); service.getPendingCommands(commandResource, CommandLinkRel.SELF);
expect(repository.getPendingCommands).toHaveBeenLastCalledWith(command, CommandLinkRel.SELF); expect(repository.getPendingCommands).toHaveBeenLastCalledWith(commandResource, CommandLinkRel.SELF);
}) })
}) })
......
...@@ -24,25 +24,54 @@ ...@@ -24,25 +24,54 @@
import { HttpErrorResponse } from '@angular/common/http'; import { HttpErrorResponse } from '@angular/common/http';
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { createErrorStateResource, createStateResource, isUnprocessableEntity, StateResource } from '@goofy-client/tech-shared'; import { createErrorStateResource, createStateResource, isUnprocessableEntity, StateResource } from '@goofy-client/tech-shared';
import { SnackBarService } from '@goofy-client/ui';
import { Resource } from '@ngxp/rest'; import { Resource } from '@ngxp/rest';
import { Observable, of, Subject, throwError } from 'rxjs'; import { Observable, of, Subject, throwError } from 'rxjs';
import { catchError, map, mergeMap, tap } from 'rxjs/operators'; import { catchError, map, mergeMap, tap } from 'rxjs/operators';
import { CommandFacade } from './+state/command.facade';
import { COMMAND_ERROR_MESSAGES } from './command.message';
import { CommandListResource, CommandResource, CreateCommand } from './command.model'; import { CommandListResource, CommandResource, CreateCommand } from './command.model';
import { CommandRepository } from './command.repository'; import { CommandRepository } from './command.repository';
import { isPending } from './command.util'; import { hasError, isConcurrentModification, isPending } from './command.util';
@Injectable({ providedIn: 'root' }) @Injectable({ providedIn: 'root' })
export class CommandService { export class CommandService {
intervalTimer: number = 500; intervalTimer: number = 500;
constructor(private repository: CommandRepository) { } constructor(private repository: CommandRepository, private snackBarService: SnackBarService, private facade: CommandFacade) { }
public createCommand(resource: Resource, linkRel: string, command: CreateCommand): Observable<StateResource<CommandResource>> { public createCommand(resource: Resource, linkRel: string, command: CreateCommand): Observable<StateResource<CommandResource>> {
return this.repository.createCommand(resource, linkRel, command).pipe( return this.repository.createCommand(resource, linkRel, command).pipe(
mergeMap(createdCommand => this.startPolling(createdCommand)), mergeMap(createdCommand => this.handleCreatedCommand(createdCommand)),
catchError(errorResponse => this.handleError(errorResponse)) catchError(errorResponse => this.handleHttpError(errorResponse)));
); }
handleCreatedCommand(command: CommandResource): Observable<StateResource<CommandResource>> {
if(hasError(command)){
return this.handleCommandError(command);
} else {
return this.startPolling(command);
}
}
handleCommandError(command: CommandResource): Observable<StateResource<CommandResource>> {
if(isConcurrentModification(command.errorMessage)){
this.snackBarService.showError(COMMAND_ERROR_MESSAGES[command.errorMessage]);
this.facade.dispatchConcurrentModificationCommand();
}
return of(createStateResource(command));
}
handleHttpError(errorResponse: HttpErrorResponse): Observable<StateResource<CommandResource>> {
return of(this.handleErrorByStatus(errorResponse));
}
handleErrorByStatus(error: HttpErrorResponse): StateResource<CommandResource> {
if (isUnprocessableEntity(error.status)) {
return createErrorStateResource(error.error);
}
throwError({ error });
} }
public revokeCommand(resource: CommandResource): Observable<StateResource<CommandResource>> { public revokeCommand(resource: CommandResource): Observable<StateResource<CommandResource>> {
...@@ -78,18 +107,6 @@ export class CommandService { ...@@ -78,18 +107,6 @@ export class CommandService {
window.clearInterval(handler); window.clearInterval(handler);
} }
private handleError(errorResponse: HttpErrorResponse): Observable<StateResource<CommandResource>> {
return of(this.handleErrorByStatus(errorResponse));
}
handleErrorByStatus(error: HttpErrorResponse): StateResource<CommandResource> {
if (isUnprocessableEntity(error.status)) {
return createErrorStateResource(error.error);
}
throwError({ error });
}
public getPendingCommands(resource: Resource, linkRel: string): Observable<StateResource<CommandListResource>> { public getPendingCommands(resource: Resource, linkRel: string): Observable<StateResource<CommandListResource>> {
return this.repository.getPendingCommands(resource, linkRel).pipe(map(res => createStateResource(res))); return this.repository.getPendingCommands(resource, linkRel).pipe(map(res => createStateResource(res)));
} }
......
...@@ -21,10 +21,11 @@ ...@@ -21,10 +21,11 @@
* Die sprachspezifischen Genehmigungen und Beschränkungen * Die sprachspezifischen Genehmigungen und Beschränkungen
* unter der Lizenz sind dem Lizenztext zu entnehmen. * unter der Lizenz sind dem Lizenztext zu entnehmen.
*/ */
import { createCommandListResource, createCommandResource } from 'libs/command-shared/test/command' import { createCommandErrorResource, createCommandListResource, createCommandResource } from 'libs/command-shared/test/command'
import { CommandLinkRel } from './command.linkrel' import { CommandLinkRel } from './command.linkrel'
import { CommandErrorMessage } from './command.message'
import { CommandListResource, CommandResource } from './command.model' import { CommandListResource, CommandResource } from './command.model'
import { getPendingCommandByOrder, hasError, isDone, isPending, isRevokeable } from './command.util' import { getPendingCommandByOrder, hasError, isConcurrentModification, isDone, isPending, isRevokeable } from './command.util'
describe('CommandUtil', () => { describe('CommandUtil', () => {
...@@ -70,7 +71,7 @@ describe('CommandUtil', () => { ...@@ -70,7 +71,7 @@ describe('CommandUtil', () => {
describe('hasError', () => { describe('hasError', () => {
it('should be true if no update link is present and command has error message', () => { it('should be true if no update link is present and command has error message', () => {
const result = hasError(createCommandResource()); const result = hasError(createCommandErrorResource(CommandErrorMessage.CONCURRENT_MODIFICATION));
expect(result).toBeTruthy(); expect(result).toBeTruthy();
}) })
...@@ -82,7 +83,7 @@ describe('CommandUtil', () => { ...@@ -82,7 +83,7 @@ describe('CommandUtil', () => {
}) })
it('should be false if error message is not present', () => { it('should be false if error message is not present', () => {
const result = hasError({ ...createCommandResource(), errorMessage: null }); const result = hasError(createCommandResource());
expect(result).toBeFalsy(); expect(result).toBeFalsy();
}) })
...@@ -119,4 +120,13 @@ describe('CommandUtil', () => { ...@@ -119,4 +120,13 @@ describe('CommandUtil', () => {
expect(pendingCommand).toBe(command); expect(pendingCommand).toBe(command);
}) })
}) })
describe('isConcurrentModification', () => {
it('should return true on matching error message', () => {
const doesMatch: boolean = isConcurrentModification('concurrent modification');
expect(doesMatch).toBeTruthy();
})
})
}) })
\ No newline at end of file
...@@ -24,6 +24,7 @@ ...@@ -24,6 +24,7 @@
import { getEmbeddedResource, hasLink } from '@ngxp/rest'; import { getEmbeddedResource, hasLink } from '@ngxp/rest';
import { isEmpty, isNil, isObject } from 'lodash-es'; import { isEmpty, isNil, isObject } from 'lodash-es';
import { CommandLinkRel, CommandListLinkRel } from './command.linkrel'; import { CommandLinkRel, CommandListLinkRel } from './command.linkrel';
import { CommandErrorMessage } from './command.message';
import { CommandListResource, CommandResource } from './command.model'; import { CommandListResource, CommandResource } from './command.model';
export function isRevokeable(commandResource: CommandResource): boolean { export function isRevokeable(commandResource: CommandResource): boolean {
...@@ -46,15 +47,19 @@ export function hasError(commandResource: CommandResource): boolean { ...@@ -46,15 +47,19 @@ export function hasError(commandResource: CommandResource): boolean {
return hasErrorMessage(commandResource) && !isPending(commandResource); return hasErrorMessage(commandResource) && !isPending(commandResource);
} }
export function getEmbeddedCommandResources(commandListResource: CommandListResource): CommandResource[] {
return getEmbeddedResource<CommandResource[]>(commandListResource, CommandListLinkRel.COMMAND_LIST);
}
export function getPendingCommandByOrder(pendingCommands: CommandListResource, commandOrder: any[]): CommandResource { export function getPendingCommandByOrder(pendingCommands: CommandListResource, commandOrder: any[]): CommandResource {
var commands: CommandResource[] = getEmbeddedCommandResources(pendingCommands).filter(command => commandOrder.includes(command.order)); var commands: CommandResource[] = getEmbeddedCommandResources(pendingCommands).filter(command => commandOrder.includes(command.order));
return commands.length > 0 ? commands[0] : null; return commands.length > 0 ? commands[0] : null;
} }
function getEmbeddedCommandResources(commandListResource: CommandListResource): CommandResource[] {
return getEmbeddedResource<CommandResource[]>(commandListResource, CommandListLinkRel.COMMAND_LIST);
}
export function doIfCommandIsDone(commandResource: CommandResource, action: () => void) { export function doIfCommandIsDone(commandResource: CommandResource, action: () => void) {
if (isObject(commandResource) && isDone(commandResource)) action(); if (isObject(commandResource) && isDone(commandResource)) action();
} }
export function isConcurrentModification(errorMessage: string): boolean{
return errorMessage === CommandErrorMessage.CONCURRENT_MODIFICATION;
}
\ No newline at end of file
...@@ -26,12 +26,16 @@ import 'jest-preset-angular/setup-jest'; ...@@ -26,12 +26,16 @@ import 'jest-preset-angular/setup-jest';
import { getTestBed } from '@angular/core/testing'; import { getTestBed } from '@angular/core/testing';
import { import {
BrowserDynamicTestingModule, BrowserDynamicTestingModule,
platformBrowserDynamicTesting, platformBrowserDynamicTesting
} from '@angular/platform-browser-dynamic/testing'; } from '@angular/platform-browser-dynamic/testing';
getTestBed().resetTestEnvironment(); getTestBed().resetTestEnvironment();
getTestBed().initTestEnvironment( getTestBed().initTestEnvironment(
BrowserDynamicTestingModule, BrowserDynamicTestingModule,
platformBrowserDynamicTesting(), platformBrowserDynamicTesting(),
{ teardown: { destroyAfterEach: false } } {
teardown: { destroyAfterEach: false },
errorOnUnknownProperties: true,
errorOnUnknownElements: true
}
); );
...@@ -25,6 +25,7 @@ import { faker } from '@faker-js/faker'; ...@@ -25,6 +25,7 @@ import { faker } from '@faker-js/faker';
import { toResource } from 'libs/tech-shared/test/resource'; import { toResource } from 'libs/tech-shared/test/resource';
import { times } from 'lodash-es'; import { times } from 'lodash-es';
import { CommandListLinkRel } from '../src/lib/command.linkrel'; import { CommandListLinkRel } from '../src/lib/command.linkrel';
import { CommandErrorMessage } from '../src/lib/command.message';
import { Command, CommandListResource, CommandResource, CommandStatus } from '../src/lib/command.model'; import { Command, CommandListResource, CommandResource, CommandStatus } from '../src/lib/command.model';
export function createCommand(): Command { export function createCommand(): Command {
...@@ -34,7 +35,7 @@ export function createCommand(): Command { ...@@ -34,7 +35,7 @@ export function createCommand(): Command {
finishedAt: faker.date.past(), finishedAt: faker.date.past(),
order: faker.random.word(), order: faker.random.word(),
status: CommandStatus.FINISHED, status: CommandStatus.FINISHED,
errorMessage: faker.random.words(10) errorMessage: null
} }
} }
...@@ -51,3 +52,7 @@ export function createCommandListResource(commandResources: CommandResource[] = ...@@ -51,3 +52,7 @@ export function createCommandListResource(commandResources: CommandResource[] =
[CommandListLinkRel.COMMAND_LIST]: commandResources [CommandListLinkRel.COMMAND_LIST]: commandResources
}); });
} }
export function createCommandErrorResource(errorMessage: CommandErrorMessage, linkRelations: string[] = []): CommandResource {
return {...createCommandResource(linkRelations), errorMessage };
}
\ No newline at end of file
...@@ -26,12 +26,16 @@ import 'jest-preset-angular/setup-jest'; ...@@ -26,12 +26,16 @@ import 'jest-preset-angular/setup-jest';
import { getTestBed } from '@angular/core/testing'; import { getTestBed } from '@angular/core/testing';
import { import {
BrowserDynamicTestingModule, BrowserDynamicTestingModule,
platformBrowserDynamicTesting, platformBrowserDynamicTesting
} from '@angular/platform-browser-dynamic/testing'; } from '@angular/platform-browser-dynamic/testing';
getTestBed().resetTestEnvironment(); getTestBed().resetTestEnvironment();
getTestBed().initTestEnvironment( getTestBed().initTestEnvironment(
BrowserDynamicTestingModule, BrowserDynamicTestingModule,
platformBrowserDynamicTesting(), platformBrowserDynamicTesting(),
{ teardown: { destroyAfterEach: false } } {
teardown: { destroyAfterEach: false },
errorOnUnknownProperties: true,
errorOnUnknownElements: true
}
); );
import { Mock, useFromMock } from '@goofy-client/test-utils';
export function mockWindowLocation(): void {
delete window.location;
window.location = useFromMock(<Mock<Location>>{ reload: jest.fn() });
}
\ No newline at end of file
...@@ -26,7 +26,7 @@ import 'jest-preset-angular/setup-jest'; ...@@ -26,7 +26,7 @@ import 'jest-preset-angular/setup-jest';
import { getTestBed } from '@angular/core/testing'; import { getTestBed } from '@angular/core/testing';
import { import {
BrowserDynamicTestingModule, BrowserDynamicTestingModule,
platformBrowserDynamicTesting, platformBrowserDynamicTesting
} from '@angular/platform-browser-dynamic/testing'; } from '@angular/platform-browser-dynamic/testing';
getTestBed().resetTestEnvironment(); getTestBed().resetTestEnvironment();
......
...@@ -28,6 +28,7 @@ import { HttpErrorHandler } from '@goofy-client/tech-shared'; ...@@ -28,6 +28,7 @@ import { HttpErrorHandler } from '@goofy-client/tech-shared';
import { mock } from '@goofy-client/test-utils'; import { mock } from '@goofy-client/test-utils';
import { createApiError } from 'libs/tech-shared/test/error'; import { createApiError } from 'libs/tech-shared/test/error';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
import { mockWindowLocation } from '../../../../tech-shared/test/window';
import { SnackBarService } from '../snackbar/snackbar.service'; import { SnackBarService } from '../snackbar/snackbar.service';
import { DialogService } from '../ui/dialog/dialog.service'; import { DialogService } from '../ui/dialog/dialog.service';
import { Messages } from '../ui/messages'; import { Messages } from '../ui/messages';
...@@ -130,13 +131,13 @@ describe('HttpErrorInterceptor', () => { ...@@ -130,13 +131,13 @@ describe('HttpErrorInterceptor', () => {
}) })
it('should refresh window on error 401', () => { it('should refresh window on error 401', () => {
interceptor.reloadWindow = jest.fn(); mockWindowLocation();
const response: HttpErrorResponse = new HttpErrorResponse({ error: createApiError(), status: HttpStatusCode.Unauthorized }); const response: HttpErrorResponse = new HttpErrorResponse({ error: createApiError(), status: HttpStatusCode.Unauthorized });
const error = {response}; const error = {response};
interceptor.handleError(error); interceptor.handleError(error);
expect(interceptor.reloadWindow).toHaveBeenCalled(); expect(window.location.reload).toHaveBeenCalled();
}) })
it('should open snackbar on error 403', () => { it('should open snackbar on error 403', () => {
......
...@@ -58,9 +58,8 @@ export class HttpErrorInterceptor implements HttpInterceptor { ...@@ -58,9 +58,8 @@ export class HttpErrorInterceptor implements HttpInterceptor {
this.handleServerError(errorResponse.error); this.handleServerError(errorResponse.error);
return EMPTY; return EMPTY;
} }
if (isUnauthorized(errorResponse.status)) { if (isUnauthorized(errorResponse.status)) {
this.reloadWindow(); window.location.reload();
} }
if (isForbidden(errorResponse.status)) { if (isForbidden(errorResponse.status)) {
this.handleForbiddenError(); this.handleForbiddenError();
...@@ -68,10 +67,6 @@ export class HttpErrorInterceptor implements HttpInterceptor { ...@@ -68,10 +67,6 @@ export class HttpErrorInterceptor implements HttpInterceptor {
return throwError({ error: errorResponse }); return throwError({ error: errorResponse });
} }
reloadWindow(): void {
window.location.reload();
}
private handleServerError(error: ApiError): void { private handleServerError(error: ApiError): void {
this.dialogService.openApiErrorInfo(error); this.dialogService.openApiErrorInfo(error);
} }
......
...@@ -51,6 +51,7 @@ export class VorgangDetailMetaDataComponent implements OnInit { ...@@ -51,6 +51,7 @@ export class VorgangDetailMetaDataComponent implements OnInit {
} }
this.metadata[name] = { ...formData[name] }; this.metadata[name] = { ...formData[name] };
delete this.metadata[name]._kopControlData; delete this.metadata[name]._kopControlData;
//FIXME: Hier tauchen consolen Fehler auf bspw. bei (fehlender) zustaendigeStelle
delete formData[name]; delete formData[name];
this.hasNoMetaData.emit(false); this.hasNoMetaData.emit(false);
} }
......
...@@ -23,13 +23,9 @@ ...@@ -23,13 +23,9 @@
*/ */
import { HttpErrorResponse } from '@angular/common/http'; import { HttpErrorResponse } from '@angular/common/http';
import { ApiRootResource } from '@goofy-client/api-root-shared'; import { ApiRootResource } from '@goofy-client/api-root-shared';
import { ApiError } from '@goofy-client/tech-shared'; import { ApiError, TypedActionCreator, TypedActionCreatorWithProps } from '@goofy-client/tech-shared';
import { ActionCreator, createAction, props } from '@ngrx/store'; import { createAction, props } from '@ngrx/store';
import { TypedAction } from '@ngrx/store/src/models'; import { VorgangListResource, VorgangWithEingangResource } from '../vorgang.model';
import { VorgangListResource } from '../vorgang.model';
export interface VorgangActionCreator<T> extends ActionCreator<string, (props: T) => T & TypedAction<string>> { }
export interface TypedActionCreator extends ActionCreator<string, () => TypedAction<string>> { }
export interface SearchVorgaengeByProps { export interface SearchVorgaengeByProps {
apiRoot: ApiRootResource, apiRoot: ApiRootResource,
...@@ -56,27 +52,38 @@ export interface VorgangListAction { ...@@ -56,27 +52,38 @@ export interface VorgangListAction {
vorgangList: VorgangListResource vorgangList: VorgangListResource
} }
export interface VorgangWithEingangAction {
vorgangWithEingang: VorgangWithEingangResource
}
//VorgangList
export const noOperation: TypedActionCreator = createAction('[Vorgang-Routing] No Operation'); export const noOperation: TypedActionCreator = createAction('[Vorgang-Routing] No Operation');
export const loadVorgangList: VorgangActionCreator<ApiRootAction> = createAction('[Vorgang] Load VorgangList', props<ApiRootAction>()); export const loadVorgangList: TypedActionCreatorWithProps<ApiRootAction> = createAction('[Vorgang] Load VorgangList', props<ApiRootAction>());
export const searchVorgaengeBy: VorgangActionCreator<SearchVorgaengeByProps> = createAction('[Vorgang] Search VorgangList', props<SearchVorgaengeByProps>()); export const searchVorgaengeBy: TypedActionCreatorWithProps<SearchVorgaengeByProps> = createAction('[Vorgang] Search VorgangList', props<SearchVorgaengeByProps>());
export const searchVorgaengeBySuccess: VorgangActionCreator<VorgangListAction> = createAction('[Vorgang] Search VorgangList Success', props<VorgangListAction>()); export const searchVorgaengeBySuccess: TypedActionCreatorWithProps<VorgangListAction> = createAction('[Vorgang] Search VorgangList Success', props<VorgangListAction>());
export const searchVorgaengeByFailure: VorgangActionCreator<HttpErrorAction> = createAction('[Vorgang] Search VorgangList Failure', props<HttpErrorAction>()); export const searchVorgaengeByFailure: TypedActionCreatorWithProps<HttpErrorAction> = createAction('[Vorgang] Search VorgangList Failure', props<HttpErrorAction>());
export const loadMyVorgaengeList: VorgangActionCreator<ApiRootAction> = createAction('[Vorgang] Load MyVorgaengList', props<ApiRootAction>()); export const loadMyVorgaengeList: TypedActionCreatorWithProps<ApiRootAction> = createAction('[Vorgang] Load MyVorgaengList', props<ApiRootAction>());
export const loadVorgangListSuccess: VorgangActionCreator<VorgangListAction> = createAction('[Vorgang] Load VorgangList Success', props<VorgangListAction>()); export const loadVorgangListSuccess: TypedActionCreatorWithProps<VorgangListAction> = createAction('[Vorgang] Load VorgangList Success', props<VorgangListAction>());
export const loadVorgangListFailure: VorgangActionCreator<ApiErrorAction> = createAction('[Vorgang] Load VorgangList Failure', props<ApiErrorAction>()); export const loadVorgangListFailure: TypedActionCreatorWithProps<ApiErrorAction> = createAction('[Vorgang] Load VorgangList Failure', props<ApiErrorAction>());
export const loadNextPage: TypedActionCreator = createAction('[Vorgang] Load next VorgangList page'); export const loadNextPage: TypedActionCreator = createAction('[Vorgang] Load next VorgangList page');
export const loadNextPageSuccess: VorgangActionCreator<VorgangListAction> = createAction('[Vorgang] Load next VorgangList page Success', props<VorgangListAction>()); export const loadNextPageSuccess: TypedActionCreatorWithProps<VorgangListAction> = createAction('[Vorgang] Load next VorgangList page Success', props<VorgangListAction>());
export const searchForPreview: VorgangActionCreator<StringBasedProps> = createAction('[Vorgang] Search for preview', props<StringBasedProps>()); export const searchForPreview: TypedActionCreatorWithProps<StringBasedProps> = createAction('[Vorgang] Search for preview', props<StringBasedProps>());
export const searchForPreviewSuccess: VorgangActionCreator<VorgangListAction> = createAction('[Vorgang] Search for preview Success', props<VorgangListAction>()); export const searchForPreviewSuccess: TypedActionCreatorWithProps<VorgangListAction> = createAction('[Vorgang] Search for preview Success', props<VorgangListAction>());
export const searchForPreviewFailure: VorgangActionCreator<HttpErrorAction> = createAction('[Vorgang] Search for preview Failure', props<HttpErrorAction>()); export const searchForPreviewFailure: TypedActionCreatorWithProps<HttpErrorAction> = createAction('[Vorgang] Search for preview Failure', props<HttpErrorAction>());
export const clearSearchPreviewList: TypedActionCreator = createAction('[Vorgang] Clear search preview list'); export const clearSearchPreviewList: TypedActionCreator = createAction('[Vorgang] Clear search preview list');
export const clearSearchString: TypedActionCreator = createAction('[Vorgang] Clear search string'); export const clearSearchString: TypedActionCreator = createAction('[Vorgang] Clear search string');
export const setSearchString: VorgangActionCreator<StringBasedProps> = createAction('[Vorgang] Set search string', props<StringBasedProps>()); export const setSearchString: TypedActionCreatorWithProps<StringBasedProps> = createAction('[Vorgang] Set search string', props<StringBasedProps>());
export const setReloadVorgangList: TypedActionCreator = createAction('[Vorgang] Set reload at VorgangList') export const setReloadVorgangList: TypedActionCreator = createAction('[Vorgang] Set reload at VorgangList')
//VorgangWithEingang
export const loadVorgangWithEingang: TypedActionCreator = createAction('[Vorgang] Load VorgangWithEingang');
export const loadVorgangWithEingangSuccess: TypedActionCreatorWithProps<VorgangWithEingangAction> = createAction('[Vorgang] Load VorgangWithEingang Success', props<VorgangWithEingangAction>());
export const setReloadVorgangWithEingang: TypedActionCreator = createAction('[Vorgang] Set reload at VorgangWithEingang');
...@@ -26,7 +26,7 @@ import { ApiRootResource } from '@goofy-client/api-root-shared'; ...@@ -26,7 +26,7 @@ import { ApiRootResource } from '@goofy-client/api-root-shared';
import { StateResource } from '@goofy-client/tech-shared'; import { StateResource } from '@goofy-client/tech-shared';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { VorgangListResource, VorgangResource } from '../vorgang.model'; import { VorgangListResource, VorgangResource, VorgangWithEingangResource } from '../vorgang.model';
import * as VorgangActions from './vorgang.actions'; import * as VorgangActions from './vorgang.actions';
import * as VorgangSelectors from './vorgang.selectors'; import * as VorgangSelectors from './vorgang.selectors';
...@@ -35,6 +35,7 @@ export class VorgangFacade { ...@@ -35,6 +35,7 @@ export class VorgangFacade {
constructor(private readonly store: Store) { } constructor(private readonly store: Store) { }
//VorgangList
public getVorgangList(): Observable<StateResource<VorgangListResource>> { public getVorgangList(): Observable<StateResource<VorgangListResource>> {
return this.store.select(VorgangSelectors.vorgangList); return this.store.select(VorgangSelectors.vorgangList);
} }
...@@ -86,4 +87,21 @@ export class VorgangFacade { ...@@ -86,4 +87,21 @@ export class VorgangFacade {
public setReloadVorgangList(): void { public setReloadVorgangList(): void {
this.store.dispatch(VorgangActions.setReloadVorgangList()); this.store.dispatch(VorgangActions.setReloadVorgangList());
} }
//VorgangWithEingang
public setVorgangWithEingang(vorgangWithEingang: VorgangWithEingangResource): void{
this.store.dispatch(VorgangActions.loadVorgangWithEingangSuccess({ vorgangWithEingang }));
}
public getVorgangWithEingang(): Observable<StateResource<VorgangWithEingangResource>> {
return this.store.select(VorgangSelectors.vorgangWithEingang);
}
public reloadVorgangWithEingang(): void {
this.store.dispatch(VorgangActions.setReloadVorgangWithEingang());
}
public loadVorgangWithEingang(): void {
this.store.dispatch(VorgangActions.loadVorgangWithEingang());
}
} }
\ No newline at end of file
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment