4

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();
  });


});
bokzor
  • 413
  • 7
  • 19
  • (Inspired from [this question](https://stackoverflow.com/questions/46225164/unit-testing-httpinterceptor-from-angular-4), but not a duplicate) –  Sep 11 '18 at 12:04

2 Answers2

3
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.

Antoniossss
  • 31,590
  • 6
  • 57
  • 99
  • Yes. It makes sense. I suppose I have to find a way to prove that the interceptor is going to retry the request. – bokzor Sep 11 '18 at 12:17
  • You are on the right track. I did the same thing im my previous project - interceptor i mean, but I tested it right. Just add different expectations. What should happen if you got error code pointing to captcha? **another (different) request will be made** - that is what you are expect to see, so `expect` just that ;) Last expectation should be a original request beeing made. In my case it was JWT token autorefresh functionality so in last request I expected original request to be remade but with different token ;) Keep it up, you are on the right track. – Antoniossss Sep 11 '18 at 13:07
  • *I suppose I have to find a way to prove that the interceptor is going to retry the request.* httpMock.expectOne('captchaserviceurl') etc. – Antoniossss Sep 11 '18 at 13:08
1

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);
  }));


});
bokzor
  • 413
  • 7
  • 19