diff --git a/goofy-client/libs/ui/src/lib/interceptor/http-connection-timeout.interceptor.spec.ts b/goofy-client/libs/ui/src/lib/interceptor/http-connection-timeout.interceptor.spec.ts index 2157fa90e087e7f842279313c5cf47162d8d6075..98b93d174246338423ade82677240219253ff70d 100644 --- a/goofy-client/libs/ui/src/lib/interceptor/http-connection-timeout.interceptor.spec.ts +++ b/goofy-client/libs/ui/src/lib/interceptor/http-connection-timeout.interceptor.spec.ts @@ -1,17 +1,23 @@ -import { HttpErrorResponse } from '@angular/common/http'; +import { HttpErrorResponse, HttpHandler, HttpRequest, HttpStatusCode } from '@angular/common/http'; +import { HttpErrorHandler } from '@goofy-client/tech-shared'; import { mock, useFromMock } from '@goofy-client/test-utils'; -import { DialogService, RetryInTimeDialog } from '@goofy-client/ui'; -import { Observable } from 'rxjs'; +import { DialogService } from '@goofy-client/ui'; +import { RetryInTimeDialog } from '../ui/dialog/retry-in-time.dialog'; import { HttpConnectionTimeoutInterceptor } from './http-connection-timeout.interceptor'; describe('HttpConnectionTimeoutInterceptor', () => { - const dialogService = mock(DialogService); let interceptor: HttpConnectionTimeoutInterceptor + const dialogService = mock(DialogService); + const errorHandler = mock(HttpErrorHandler); + const tokenExtractor = <any>{ getToken: () => jest.fn }; + + const request: HttpRequest<unknown> = <any>{}; + const next: HttpHandler = <any>{}; const retryDialog = mock(RetryInTimeDialog); beforeAll(() => { - interceptor = new HttpConnectionTimeoutInterceptor(useFromMock(dialogService)); + interceptor = new HttpConnectionTimeoutInterceptor(useFromMock(dialogService), useFromMock(errorHandler), tokenExtractor); dialogService.getRetryDialog.mockReturnValue(retryDialog); }) @@ -21,86 +27,127 @@ describe('HttpConnectionTimeoutInterceptor', () => { it('should initRetryDialog', () => { interceptor.retryDialog = null; - interceptor.handleError(<HttpErrorResponse>{ status: 503 }); + executeHandleError(HttpStatusCode.ServiceUnavailable); expect(interceptor.retryDialog).not.toBeNull(); }) it('shouldCallService', () => { - interceptor.handleError(<HttpErrorResponse>{ status: 503 }); + executeHandleError(); expect(dialogService.getRetryDialog).toHaveBeenCalled(); }) it('should not call service if already initiated', () => { dialogService.getRetryDialog.mockClear(); - interceptor.retryDialog = <any>{}; + interceptor.doRetry = jest.fn(); + interceptor.retryDialog = <any>{ shouldRetry: () => true }; - interceptor.handleError(<HttpErrorResponse>{ status: 500 }); + executeHandleError(); expect(dialogService.getRetryDialog).not.toHaveBeenCalled(); }) - it('should do nothing on wrong status', () => { + it('should do nothing on no connection timeout status', () => { dialogService.getRetryDialog.mockClear(); - interceptor.handleError(<HttpErrorResponse>{ status: 500 }); + executeHandleError(HttpStatusCode.InternalServerError); expect(dialogService.getRetryDialog).not.toHaveBeenCalled(); }) - }) - describe('initRetryDialog', () => { + it('should do retry', () => { + interceptor.retryDialog = <any>{ shouldRetry: () => true }; + interceptor.doRetry = jest.fn(); - it('should call service', () => { - interceptor.showRetryDialog(); + executeHandleError(); - expect(dialogService.getRetryDialog).toHaveBeenCalled(); + expect(interceptor.doRetry).toHaveBeenCalled(); }) + + function executeHandleError(status: number = HttpStatusCode.ServiceUnavailable) { + interceptor.handleError(request, next, <HttpErrorResponse>{ status }, true); + } }) - describe('handleRetry', () => { + describe('handleErrorRetry', () => { - const event = { response: { error: {} } }; + describe('on connection timeout error', () => { - it('should call service', () => { - interceptor.retryDialog = <any>{ shouldRetry: () => true }; + const retryDialog = <any>{ shouldRetry: () => true, finish: () => jest.fn() }; + + it('should do retry if condition fits', () => { + interceptor.retryDialog = retryDialog; + interceptor.doRetry = jest.fn(); + + executeHandleErrorRetry(); + + expect(interceptor.doRetry).toHaveBeenCalledWith(request, next); + }) + + it('should unset retryDialog if condition does not fit', () => { + interceptor.retryDialog = { ...retryDialog, shouldRetry: () => false }; - const result = interceptor.handleRetry(event); + executeHandleErrorRetry(); - expect(result).toBeInstanceOf(Observable); + expect(interceptor.retryDialog).toBeNull(); + }) + + it('should call stop retry if condition does not fit', () => { + interceptor.retryDialog = { ...retryDialog, shouldRetry: () => false }; + interceptor.stopRetry = jest.fn(); + + executeHandleErrorRetry(); + + expect(interceptor.stopRetry).toHaveBeenCalled(); + }) + + function executeHandleErrorRetry(status: number = HttpStatusCode.ServiceUnavailable) { + interceptor.handleErrorRetry(request, next, <HttpErrorResponse>{ status }); + } }) }) - describe('handleResponse', () => { const retryDialog = <any>{}; - beforeEach(() => { - dialogService.closeAll.mockClear(); - }) + describe('on valid response', () => { - it('should close all dialogs if reponse body is present', () => { - interceptor.handleResponse({ body: {} }); + const response = { type: 4, ok: true }; - expect(dialogService.closeAll).toHaveBeenCalled(); - }) + it('should close all retry dialogs if response is valid', () => { + interceptor.handleResponse(response); - it('should unset retryDialog', () => { - interceptor.retryDialog = retryDialog; - interceptor.handleResponse({ body: {} }); + expect(dialogService.closeById).toHaveBeenCalledWith(RetryInTimeDialog.ID); + }) - expect(interceptor.retryDialog).toBeNull(); + it('should unset retryDialog', () => { + interceptor.retryDialog = retryDialog; + + interceptor.handleResponse(response); + + expect(interceptor.retryDialog).toBeNull(); + }) }) - it('should do nothing on non existing response body', () => { - interceptor.retryDialog = retryDialog; + describe('on invalid response', () => { + + const response = { type: 0 }; - interceptor.handleResponse({ body: undefined }); + beforeEach(() => { + dialogService.closeById.mockClear(); + }) - expect(dialogService.closeAll).not.toHaveBeenCalled(); - expect(interceptor.retryDialog).toStrictEqual(retryDialog); + it('should do nothing on invalid response', () => { + interceptor.retryDialog = retryDialog; + + interceptor.handleResponse(response); + + expect(dialogService.closeById).not.toHaveBeenCalled(); + expect(interceptor.retryDialog).toStrictEqual(retryDialog); + }) }) + }) }) \ No newline at end of file diff --git a/goofy-client/libs/ui/src/lib/interceptor/http-connection-timeout.interceptor.ts b/goofy-client/libs/ui/src/lib/interceptor/http-connection-timeout.interceptor.ts index 009852be887aaea50b169cce99cd54c77983c2c6..98981d51be255e994ae73be68afbbc22503008f9 100644 --- a/goofy-client/libs/ui/src/lib/interceptor/http-connection-timeout.interceptor.ts +++ b/goofy-client/libs/ui/src/lib/interceptor/http-connection-timeout.interceptor.ts @@ -1,58 +1,89 @@ -import { HttpErrorResponse, HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http'; +import { HttpErrorResponse, HttpEvent, HttpHandler, HttpInterceptor, HttpRequest, HttpXsrfTokenExtractor } from '@angular/common/http'; import { Injectable } from '@angular/core'; -import { isConnectionTimeout } from '@goofy-client/tech-shared'; -import { isNull, isObject } from 'lodash-es'; -import { Observable, of, throwError } from 'rxjs'; -import { catchError, delay, retryWhen, switchMap, tap } from 'rxjs/operators'; -import { DialogService, RetryInTimeDialog } from '../ui/dialog/dialog.service'; +import { addRequestHeader, HttpErrorHandler, isConnectionTimeout, sleep } from '@goofy-client/tech-shared'; +import { HttpXsrfInterceptor } from 'libs/tech-shared/src/lib/interceptor/http-xsrf.interceptor'; +import { isNull } from 'lodash-es'; +import { Observable, throwError } from 'rxjs'; +import { catchError, finalize, tap } from 'rxjs/operators'; +import { DialogService } from '../ui/dialog/dialog.service'; +import { RetryInTimeDialog } from '../ui/dialog/retry-in-time.dialog'; @Injectable() export class HttpConnectionTimeoutInterceptor implements HttpInterceptor { - private readonly DELAY_BEFORE_RETRY_MS = 1000; - + readonly RETRY_IN_MS = 1000; retryDialog: RetryInTimeDialog = null; - constructor(private dialogService: DialogService) { } + constructor(private dialogService: DialogService, private errorHandler: HttpErrorHandler, private tokenExtractor: HttpXsrfTokenExtractor) { } intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> { + const defaultHandling: boolean = this.errorHandler.shouldDoRetry(); return next.handle(request).pipe( - catchError((error: HttpErrorResponse) => this.handleError(error)), - retryWhen((response: Observable<any>) => response.pipe( - switchMap((event: any) => this.handleRetry(event)), - delay(this.DELAY_BEFORE_RETRY_MS) - )), - tap(response => this.handleResponse(response)) + catchError((error: HttpErrorResponse) => this.handleError(request, next, error, defaultHandling)), + finalize(() => this.errorHandler.enableRetry()) ) } - handleError(response: HttpErrorResponse): Observable<any> { - if (isConnectionTimeout(response.status)) { - if (isNull(this.retryDialog)) { - this.showRetryDialog(); + handleError(request: HttpRequest<unknown>, next: HttpHandler, response: HttpErrorResponse, defaultHandling: boolean): Observable<any> { + if (defaultHandling) { + if (isConnectionTimeout(response.status)) { + this.initRetry(); + + if (this.retryDialog.shouldRetry()) { + return this.doRetry(request, next); + } } } return throwError({ response }); } - showRetryDialog(): void { - this.retryDialog = this.dialogService.getRetryDialog(); - this.retryDialog.show(); + initRetry(): void { + if (isNull(this.retryDialog)) { + this.retryDialog = this.dialogService.getRetryDialog(); + this.retryDialog.show(); + } + } + + doRetry(request: HttpRequest<unknown>, next: HttpHandler): Observable<any> { + this.sleepUntilRetry(); + return next.handle(addRequestHeader(request, HttpXsrfInterceptor.X_XSRF_TOKEN_HEADER, this.getToken())).pipe( + catchError((error: HttpErrorResponse) => this.handleErrorRetry(request, next, error)), + tap(response => this.handleResponse(response)), + finalize(() => this.errorHandler.enableRetry())) + } + + sleepUntilRetry(): void { + sleep(this.RETRY_IN_MS); } - handleRetry(event: any): Observable<any> { - if (!isNull(this.retryDialog)) { + handleErrorRetry(request: HttpRequest<unknown>, next: HttpHandler, response: HttpErrorResponse): Observable<any> { + if (isConnectionTimeout(response.status)) { if (this.retryDialog.shouldRetry()) { - return of(event); + return this.doRetry(request, next); + } else { + this.stopRetry(); } } - return throwError(event); + return throwError({ response }); + } + + stopRetry(): void { + this.retryDialog.finish(); + this.retryDialog = null; + } + + private getToken(): string { + return this.tokenExtractor.getToken(); } handleResponse(response: any): void { - if (isObject(response.body)) { - this.dialogService.closeAll(); + if (this.isValidResponse(response)) { + this.dialogService.closeById(RetryInTimeDialog.ID); this.retryDialog = null; } } + + isValidResponse(response: any): boolean { + return response.type !== 0 && response.ok; + } } \ No newline at end of file