diff --git a/alfa-client/libs/admin/keycloak-shared/src/lib/keycloak.resource.service.spec.ts b/alfa-client/libs/admin/keycloak-shared/src/lib/keycloak.resource.service.spec.ts index c39b6007d81d0834d4a1b06c0840408e7f1accf6..3928e72cc9b20e768c905eedb69dc48c4ecfba3b 100644 --- a/alfa-client/libs/admin/keycloak-shared/src/lib/keycloak.resource.service.spec.ts +++ b/alfa-client/libs/admin/keycloak-shared/src/lib/keycloak.resource.service.spec.ts @@ -40,6 +40,9 @@ describe('KeycloakResourceService', () => { const dummyObject: Dummy = createDummy(); const dummyAction: Observable<Dummy> = of(dummyObject); + const dummyError: Error = new Error('Test error'); + const dummyErrorAction: Observable<Error> = new Observable((observer) => observer.error(dummyError)); + beforeEach(() => { TestBed.configureTestingModule({ providers: [TestResourceService], @@ -52,7 +55,7 @@ describe('KeycloakResourceService', () => { const stateResource: StateResource<unknown> = createStateResource([]); beforeEach(() => { - service.handleChanges = jest.fn().mockReturnValue(singleCold(stateResource)); + service._handleChanges = jest.fn().mockReturnValue(singleCold(stateResource)); }); it('should return stateResource', () => { @@ -64,7 +67,7 @@ describe('KeycloakResourceService', () => { it('should call handleChanges ', fakeAsync(() => { service.getAll().subscribe(); - expect(service.handleChanges).toHaveBeenCalled(); + expect(service._handleChanges).toHaveBeenCalled(); })); }); @@ -76,7 +79,7 @@ describe('KeycloakResourceService', () => { }); it('should call doIfLoadingRequired', () => { - service.handleChanges(emptyStateResource); + service._handleChanges(emptyStateResource); expect(doIfLoadingRequiredSpy).toHaveBeenCalled(); }); @@ -84,7 +87,7 @@ describe('KeycloakResourceService', () => { it('should return stateResource', (done) => { service.stateResource.next(createStateResource([])); - service.handleChanges(emptyStateResource).subscribe((stateResource: StateResource<[]>) => { + service._handleChanges(emptyStateResource).subscribe((stateResource: StateResource<[]>) => { expect(stateResource).toEqual(createStateResource([])); done(); }); @@ -93,18 +96,18 @@ describe('KeycloakResourceService', () => { describe('loadResource', () => { it('should call set loading', () => { - service.setLoading = jest.fn(); + service._setLoading = jest.fn(); - service.loadResource(); + service._loadResource(); - expect(service.setLoading).toHaveBeenCalled(); + expect(service._setLoading).toHaveBeenCalled(); }); it('should update Resource', fakeAsync(() => { const dummyItems = [createDummy(), createDummy()]; jest.spyOn(service, '_getItemsFromKeycloak' as any).mockReturnValue(of(dummyItems)); - service.loadResource(); + service._loadResource(); tick(); expect(service.stateResource.value.resource).toEqual(dummyItems); @@ -115,11 +118,11 @@ describe('KeycloakResourceService', () => { const saveObject: Dummy = createDummy(); it('should call handleLoading', () => { - service.handleLoading = jest.fn(); + service._handleLoading = jest.fn(); service.create(saveObject); - expect(service.handleLoading).toHaveBeenCalled(); + expect(service._handleLoading).toHaveBeenCalled(); }); it('should call createInKeycloak', () => { @@ -133,11 +136,11 @@ describe('KeycloakResourceService', () => { describe('save', () => { it('should call handleLoading', () => { - service.handleLoading = jest.fn(); + service._handleLoading = jest.fn(); service.save(dummyObject); - expect(service.handleLoading).toHaveBeenCalled(); + expect(service._handleLoading).toHaveBeenCalled(); }); it('should call createInKeycloak', () => { @@ -151,11 +154,11 @@ describe('KeycloakResourceService', () => { describe('delete', () => { it('should call handleLoading', () => { - service.handleLoading = jest.fn(); + service._handleLoading = jest.fn(); service.delete(id); - expect(service.handleLoading).toHaveBeenCalled(); + expect(service._handleLoading).toHaveBeenCalled(); }); it('should call createInKeycloak', () => { @@ -168,37 +171,58 @@ describe('KeycloakResourceService', () => { }); describe('handleLoading', () => { + it('should set loading', () => { + service._handleLoading(dummyAction); + + expect(service.stateResource.value.loading).toBe(true); + }); + it('should call refreshAfterFirstEmit', () => { - service.refreshAfterFirstEmit = jest.fn().mockReturnValue(dummyAction); + service._refreshAfterEmit = jest.fn().mockReturnValue(dummyAction); - service.handleLoading(dummyAction); + service._handleLoading(dummyAction); - expect(service.refreshAfterFirstEmit).toHaveBeenCalled(); + expect(service._refreshAfterEmit).toHaveBeenCalled(); }); it('should call progress', () => { - service.setLoadingInStateResource = jest.fn().mockReturnValue(dummyAction); + service._setLoadingInStateResource = jest.fn().mockReturnValue(dummyAction); - service.handleLoading(dummyAction); + service._handleLoading(dummyAction); - expect(service.setLoadingInStateResource).toHaveBeenCalled(); + expect(service._setLoadingInStateResource).toHaveBeenCalled(); }); }); - describe('refreshAfterFirstEmit', () => { - it('should call refresh after first emit', fakeAsync(() => { + describe('refreshAfterEmit', () => { + it('should call refresh after first emit', () => { service.refresh = jest.fn(); - service.refreshAfterFirstEmit(dummyAction).subscribe(); - tick(); + service._refreshAfterEmit(dummyAction).subscribe(); expect(service.refresh).toHaveBeenCalled(); - })); + }); + + it('should call refresh on error', () => { + service.refresh = jest.fn(); + + service._refreshAfterEmit(dummyErrorAction).subscribe(); + + expect(service.refresh).toHaveBeenCalled(); + }); + + it('should throw error refresh on error', () => { + service.refresh = jest.fn(); + + const result: Observable<unknown> = service._refreshAfterEmit(dummyErrorAction); + + expect(result).toBeObservable(cold('#', null, dummyError)); + }); }); describe('setLoadingInStateResource', () => { it('should emit emptyState first with loading and then state without loading', () => { - const result: Observable<StateResource<Dummy>> = service.setLoadingInStateResource<Dummy>(cold('--x', { x: dummyObject })); + const result: Observable<StateResource<Dummy>> = service._setLoadingInStateResource<Dummy>(cold('--x', { x: dummyObject })); expect(result).toBeObservable(cold('a-b', { a: createEmptyStateResource(true), b: createStateResource(dummyObject) })); }); @@ -208,7 +232,7 @@ describe('KeycloakResourceService', () => { it('should set loading in state to true without parameter', () => { service.stateResource.value.loading = false; - service.setLoading(); + service._setLoading(); expect(service.stateResource.value.loading).toEqual(true); }); diff --git a/alfa-client/libs/admin/keycloak-shared/src/lib/keycloak.resource.service.ts b/alfa-client/libs/admin/keycloak-shared/src/lib/keycloak.resource.service.ts index 8e5d247cd2cdcde0cdded7d540204cd8fff142d8..31ce426e6aa5a8797107108e5e7489a85a1cfb73 100644 --- a/alfa-client/libs/admin/keycloak-shared/src/lib/keycloak.resource.service.ts +++ b/alfa-client/libs/admin/keycloak-shared/src/lib/keycloak.resource.service.ts @@ -22,7 +22,7 @@ * unter der Lizenz sind dem Lizenztext zu entnehmen. */ import { createEmptyStateResource, createStateResource, doIfLoadingRequired, StateResource } from '@alfa-client/tech-shared'; -import { BehaviorSubject, first, map, Observable, startWith, switchMap, tap } from 'rxjs'; +import { BehaviorSubject, catchError, first, map, Observable, startWith, switchMap, tap } from 'rxjs'; export abstract class KeycloakResourceService<T> { readonly stateResource: BehaviorSubject<StateResource<T[]>> = new BehaviorSubject(createEmptyStateResource()); @@ -30,16 +30,16 @@ export abstract class KeycloakResourceService<T> { public getAll(): Observable<StateResource<T[]>> { return this.stateResource .asObservable() - .pipe(switchMap((stateResource: StateResource<T[]>) => this.handleChanges(stateResource))); + .pipe(switchMap((stateResource: StateResource<T[]>) => this._handleChanges(stateResource))); } - handleChanges(stateResource: StateResource<T[]>): Observable<StateResource<T[]>> { - doIfLoadingRequired(stateResource, (): void => this.loadResource()); + _handleChanges(stateResource: StateResource<T[]>): Observable<StateResource<T[]>> { + doIfLoadingRequired(stateResource, (): void => this._loadResource()); return this.stateResource.asObservable(); } - loadResource(): void { - this.setLoading(); + _loadResource(): void { + this._setLoading(); this._getItemsFromKeycloak() .pipe(first()) .subscribe((items: T[]): void => this.updateResource(items)); @@ -52,42 +52,49 @@ export abstract class KeycloakResourceService<T> { } public create(item: Partial<T>): Observable<StateResource<T>> { - return this.handleLoading<T>(this._createInKeycloak(item)); + return this._handleLoading<T>(this._createInKeycloak(item)); } protected abstract _createInKeycloak(item: Partial<T>): Observable<T>; public save(item: T): Observable<StateResource<T>> { - return this.handleLoading<T>(this._saveInKeycloak(item)); + return this._handleLoading<T>(this._saveInKeycloak(item)); } protected abstract _saveInKeycloak(item: T): Observable<T>; public delete(id: string): Observable<StateResource<void>> { - return this.handleLoading(this._deleteInKeycloak(id)); + return this._handleLoading(this._deleteInKeycloak(id)); } protected abstract _deleteInKeycloak(id: string): Observable<void>; - handleLoading<D>(action: Observable<D>): Observable<StateResource<D>> { - return this.setLoadingInStateResource<D>(this.refreshAfterFirstEmit<D>(action)); + _handleLoading<D>(action: Observable<D>): Observable<StateResource<D>> { + this._setLoading(); + return this._setLoadingInStateResource<D>(this._refreshAfterEmit<D>(action)); } - refreshAfterFirstEmit<D>(action: Observable<D>): Observable<D> { + _refreshAfterEmit<D>(action: Observable<D>): Observable<D> { return action.pipe( first(), tap((): void => this.refresh()), + catchError((err: Error) => this.handleError(err)), ); } - setLoadingInStateResource<D>(action: Observable<D>): Observable<StateResource<D>> { + handleError(err: Error): never { + this.refresh(); + throw err; + } + + _setLoadingInStateResource<D>(action: Observable<D>): Observable<StateResource<D>> { return action.pipe( map((value: D): StateResource<D> => createStateResource<D>(value)), startWith(createEmptyStateResource<D>(true)), ); } - setLoading(): void { + _setLoading(): void { this.stateResource.next({ ...this.stateResource.value, loading: true }); }