vendredi 3 mai 2019

Angular 7 testing retryWhen with mock http requests fails to actually retry

I have the following interceptor that tries to use a OAuth refresh_token whenever any 401 (error) response is obtained.

The code itself works, although I'm sure that if I better understand rxjs, I would be able to refacture it somehow..

Basically a refresh token is obtained on the first 401 request and after it is obtained, the code waits 2,5 seconds. In most cases the second request will not trigger an error, but if it does (token couldn't be refreshed or whatever), the user is redirect to the login page.

export class RefreshAuthenticationInterceptor implements HttpInterceptor {
    private isRefreshingToken: boolean = false;

    constructor(
        private router: Router,
        private authenticationService: AuthenticationService,
        private tokenService: TokenService,
    ) {}

    public intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
        return next.handle(request)
            .pipe(
                // this catches 401 requests and tries to refresh the auth token and try again.
                retryWhen(errors => {
                    console.log(errors);
                    // this subject is returned to retryWhen
                    const subject = new Subject();

                    // didn't know a better way to keep track if this is the first
                    let first = true;

                    errors.subscribe((errorStatus) => {
                        // first time either pass the error through or get new token
                        if (first) {
                            if (!errorStatus.status || errorStatus.status !== 401 || errorStatus.error.error !== 'invalid_grant') {
                                subject.error(errorStatus);
                                return false;
                            }

                            if (!this.isRefreshingToken) {
                                this.isRefreshingToken = true;
                                this.authenticationService.authTokenGet('refresh_token', environment.clientId, environment.clientSecret, this.tokenService.getToken().refresh_token).subscribe((token: OauthAccessToken) => {
                                    const tok = this.tokenService.setupToken(token);
                                    this.tokenService.setToken(tok);
                                    this.tokenService.save();
                                    this.isRefreshingToken = false;
                                });
                            }

                        // second time still error means redirect to login
                        } else {
                            this.router.navigateByUrl('/auth/login')
                                .then(() => subject.complete());

                            return;
                        }

                        // and of course make sure the second time is indeed seen as second time
                        first = false;

                        // trigger retry after 2,5 second to give ample time for token request to succeed
                        setTimeout(() => subject.next(), 2500);
                    });

                    return subject;
                }),
            );
    }
}

The problem lies within the test. Everything works, except for the final check if the router was actually nagivated to /auth/login. It isn't, so the test fails.

With debugging, I know for sure the setTimeout callback is executed, but the subject.next() does not seem to start a new request.

I read somewhere that when normally using rxjs retry() on http mock requests, you should flush the request again. This is commented out in the code below, but gives a "Cannot flush a cancelled request."

    it('should catch 401 invalid_grant errors to try to refresh token the first time, redirect to login the second', fakeAsync(inject([HttpClient, HttpTestingController], (http: HttpClient, mock: HttpTestingController) => {
        const oauthAccessToken: OauthAccessToken = {
            access_token: '1234',
            expires_in: 3600,
            token_type: 'password',
            scope: '',
            refresh_token: '4321',
            time_requested: new Date().getTime()
        };

        authenticationService.authTokenGet.and.returnValue(of(oauthAccessToken).pipe(delay(0)));
        tokenService.getToken.and.returnValue(oauthAccessToken);

        // first request
        http.get('/api').subscribe(
            response => console.log(response),
            error => {
                expect(error.status).toEqual(401);
            }
        );

        const req = mock.expectOne('/api');
        req.flush({error: 'invalid_grant'}, {
            status: 401,
            statusText: 'Unauthorized'
        });

        // TODO test environment should NOT be mis-used for test release. If this is fixed these values can be more dummy
        expect(authenticationService.authTokenGet).toHaveBeenCalledWith('refresh_token', '59fafa20189f61ae2059c111_4ovlzteer30gwgggkwsk00g44k88soo8sgwkk40g4kg4kkscow', '60h46xgt9yo8ss8skc0owgccsccw04k0scwksc0cooo00g8c4w', '4321');

        // second request
        authenticationService.authTokenGet.calls.reset();

        // req.flush({error: 'invalid_grant'}, {
        //    status: 401,
        //    statusText: 'Unauthorized'
        // });

        tick(2500);
        expect(authenticationService.authTokenGet).toHaveBeenCalledTimes(0);
        expect(router.navigateByUrl).toHaveBeenCalledWith('/auth/login');

        mock.verify();
    })));

Does anyone know how to fix this test?

PS: Any pointers on the code itself are also welcome :)

Aucun commentaire:

Enregistrer un commentaire