bokzor
bokzor

Reputation: 445

Test angular HttpInterceptor

The goal of this interceptor is to re-send the request when a captcha-key is required by the server.

But it could be use when a jwt token should be refreshed.

The interceptor works fine but I cannot explain why the test is failing.

The flow will never pass into the httpClient.get('/error').subscribe(), if the response code != 200.

Here is a link of a reproductible demo : https://stackblitz.com/edit/angular-testing-template-mfqwpj?embed=1&file=app/interceptor.spec.ts

import {HttpEvent, HttpHandler, HttpInterceptor, HttpRequest} from '@angular/common/http';
import {Observable} from 'rxjs';
import {Injectable} from '@angular/core';
import {catchError, switchMap} from 'rxjs/operators';
import {CaptchaHeader, CaptchaV2Service} from 'century-lib';


@Injectable({
  providedIn: 'root'
})
export class CaptchaInterceptor implements HttpInterceptor {

  constructor(private captchaService: CaptchaV2Service) {
  }


  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    return next.handle(req).pipe(
      catchError(err => {
        if (!this.captchaIsRequired(err)) {
          return;
        }
        return this.captchaService.getCaptchaKey().pipe(
          switchMap((key) => {
            const newReq = this.applyCaptchaKey(req, key);
            return next.handle(newReq);
          })
        );
      })
    );
  }

  applyCaptchaKey(req, key) {
    return req.clone({
      headers: req.headers.set('Captcha-Token', key)
    });
  }

  private captchaIsRequired(error) {
    return (error.status === 400 && error.headers.get('Captcha-Status') === 'required');
  }

}

Test:

import {async, TestBed} from '@angular/core/testing';
import {HttpClientTestingModule, HttpTestingController} from '@angular/common/http/testing';
import {CaptchaV2Service} from 'century-lib';
import {HTTP_INTERCEPTORS, HttpClient, HttpHeaders} from '@angular/common/http';
import {CaptchaInterceptor} from './captcha.interceptor';
import {EventEmitter} from '@angular/core';

class MockCaptchaService {
  valid = new EventEmitter<string>();
  reset = new EventEmitter<boolean>();

  getCaptchaKey() {
    setTimeout(() => {
      this.valid.emit('captcha-key');
    }, 500);
    return this.valid;
  }
}

describe('Captcha interceptor', () => {
  let httpClient: HttpClient;
  let httpMock: HttpTestingController;
  let interceptor: CaptchaInterceptor;

  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [HttpClientTestingModule],
      providers: [
        CaptchaInterceptor,
        {provide: CaptchaV2Service, useValue: new MockCaptchaService()},
        {provide: HTTP_INTERCEPTORS, useClass: CaptchaInterceptor, multi: true},
      ]
    });

    httpClient = TestBed.get(HttpClient);
    httpMock = TestBed.get(HttpTestingController);
    interceptor = TestBed.get(CaptchaInterceptor);
  });


  it('should construct', async(() => {
    expect(interceptor).toBeDefined();
  }));

  it('Should interrogate the captchaService when service returns Captcha-Required', async(() => {
    httpClient.get('/error').subscribe(() => {
    }, () => {
    });
    const req = httpMock.expectOne('/error');
    req.error(new ErrorEvent('Captcha Error'), {
      status: 400,
      statusText: 'Captcha-Error',
      headers: new HttpHeaders().set('Captcha-Status', 'required')
    });
    expect(req.request.headers.get('Captcha-Token')).toBe('captcha-key');
    httpMock.verify();
  }));

  afterEach(() => {
    TestBed.resetTestingModule();
  });


});

Upvotes: 4

Views: 6776

Answers (2)

bokzor
bokzor

Reputation: 445

Here is my final test :

import {async, fakeAsync, TestBed, tick} from '@angular/core/testing';
import {HttpClientTestingModule, HttpTestingController} from '@angular/common/http/testing';
import {CaptchaV2Service} from 'century-lib';
import {HTTP_INTERCEPTORS, HttpClient, HttpHeaders} from '@angular/common/http';
import {CaptchaInterceptor} from './captcha.interceptor';
import {Observable} from 'rxjs';


function ObservableDelay<T>(val: T, delay: number, cb = () => {
}): Observable<any> {
  return new Observable(observer => {
    setTimeout(() => {
      observer.next(val);
      observer.complete();
      cb();
    }, delay);
  });
}

const CAPTCHA_TOKEN = 'captcha-token';

describe('Captcha interceptor', () => {
  let httpClient: HttpClient;
  let httpMock: HttpTestingController;
  let interceptor: CaptchaInterceptor;
  let captchaService: CaptchaV2Service;

  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [HttpClientTestingModule],
      providers: [
        CaptchaInterceptor,
        {provide: CaptchaV2Service, useClass: CaptchaV2Service},
        {provide: HTTP_INTERCEPTORS, useClass: CaptchaInterceptor, multi: true},
      ]
    });

    httpClient = TestBed.get(HttpClient);
    httpMock = TestBed.get(HttpTestingController);
    interceptor = TestBed.get(CaptchaInterceptor);
    captchaService = TestBed.get(CaptchaV2Service);
  });


  it('should construct', async(() => {
    expect(interceptor).toBeDefined();
  }));

  it('Should interrogate the captchaService when service returns Captcha-Required', fakeAsync(() => {

    spyOn(captchaService, 'getCaptchaKey').and.returnValue(ObservableDelay(CAPTCHA_TOKEN, 200, () => {
      httpMock
        .expectOne(r => r.headers.has('Captcha-Token') && r.headers.get('Captcha-Token') === CAPTCHA_TOKEN);
    }));

    httpClient.get('/error').subscribe();
    const req = httpMock.expectOne('/error');
    req.error(new ErrorEvent('Captcha Error'), {
      status: 400,
      statusText: 'Captcha-Error',
      headers: new HttpHeaders().set('Captcha-Status', 'required')
    });

    tick(200);
  }));


});

Upvotes: 1

Antoniossss
Antoniossss

Reputation: 32535

const req = httpMock.expectOne('/error');
req.error(new ErrorEvent('Captcha Error'), {
  status: 400,
  statusText: 'Captcha-Error',
  headers: new HttpHeaders().set('Captcha-Status', 'required')
});
expect(req.request.headers.get('Captcha-Token')).toBe('captcha-key');

This does not make any sense. You have single request req and you flush it with error. That is fine, but at this point request is complete and nothing will (you had request and you got the response).

Now last line expect exact opposite - that completed request will somehow change.

This is not what your interceptor is doing. Interceptor is making another request to get new token (or validate captcha) and then it retries the original request. Remove expect and mock.verify() will show you all requests that have been made.

Upvotes: 3

Related Questions