Home > other >  Unit Testing Angular 12 HTTP Interceptor expectOne fails
Unit Testing Angular 12 HTTP Interceptor expectOne fails

Time:12-18

I have an Angular project using Firebase for authentication. In order to authenticate to our server, we need the JWT from Firebase. The project is using Angular Fire to accomplish this.

We have an HTTP interceptor that intercepts any requests to the server, and adds the required Authorization header. The interceptor seems to work perfectly, but we would like to unit test it as well.

Our AuthService is responsible for delegating actions to Firebase. The getCurrentUserToken method retrieves the JWT as a promise, as Angular Fire will refresh the JWT if needed.

This complicates the interceptor slightly, since we need to asynchronously get the token and wrap it into the Observable from the request.

token-interceptor.ts

@Injectable()
export class TokenInterceptor implements HttpInterceptor {
  constructor(private authService: AuthService) {}

  intercept(
    req: HttpRequest<unknown>,
    next: HttpHandler
  ): Observable<HttpEvent<unknown>> {
    if (req.url.includes(environment.apiRoot)) {
      return defer(async () => {
        const token = await this.authService.getCurrentUserToken();
        const request = req.clone({
          headers: this.getDefaultHeaders(token),
          withCredentials: true,
        });
        return next.handle(request);
      }).pipe(mergeAll());
    } else {
      return next.handle(req);
    }
  }

  private getDefaultHeaders(accessToken: string): HttpHeaders {
    return new HttpHeaders()
      .set(
        'Access-Control-Allow-Origin',
        `${window.location.protocol}//${window.location.host}`
      )
      .set(
        'Access-Control-Allow-Headers',
        'Access-Control-Allow-Origin,Access-Control-Request-Method,Access-Control-Request-Headers,Access-Control-Allow-Headers,Authorization,Accept,Content-Type,Origin,Host,Referer,X-Requested-With,X-CSRF-Token'
      )
      .set('Content-Type', 'application/json')
      .set('Accept', 'application/json')
      .set('Authorization', `Bearer ${accessToken}`);
  }
}

We provide the interceptor in our AppModule by importing HttpClientModule and providing the interceptor under the HTTP_INTERCEPTORS injection token with

  providers: [
    {
      provide: HTTP_INTERCEPTORS,
      useClass: TokenInterceptor,
      multi: true,
    },

The issue comes only in our unit test

token-interceptor.spec.ts

describe(`TokenInterceptor`, () => {
  let service: SomeService;
  let httpMock: HttpTestingController;
  let authServiceStub: AuthService;

  beforeEach(() => {
    authServiceStub = {
      getCurrentUserToken: () => Promise.resolve('some-token'),
    } as AuthService;

    TestBed.configureTestingModule({
      imports: [HttpClientTestingModule],
      providers: [
        {
          provide: HTTP_INTERCEPTORS,
          useClass: TokenInterceptor,
          multi: true,
        },
        { provide: AuthService, useValue: authServiceStub },
      ],
    });

    service = TestBed.inject(SomeService);
    httpMock = TestBed.inject(HttpTestingController);
  });

  afterEach(() => {
    httpMock.verify();
  });

  it('should add an Authorization header', () => {
    service.sendSomeRequestToServer().subscribe((response) => {
      expect(response).toBeTruthy();
    });

    const httpRequest = httpMock.expectOne({});

    expect(httpRequest.request.headers.has('Authorization')).toEqual(true);
    expect(httpRequest.request.headers.get('Authorization')).toBe(
      'Bearer some-token'
    );

    httpRequest.flush({ message: "SUCCESS" });

    httpMock.verify();
  });
});

The test fails at the httpMock.expectOne({}) statement with the message

Error: Expected one matching request for criteria "Match method: (any), URL: (any)", found none.

It seems that somehow our mock request was never sent at all.

However, if we remove the interceptor from the providers in the Testbed, the test instead fails at the expect statements, which shows that the mock request is being found.

        Error: Expected false to equal true.
            at <Jasmine>
            at UserContext.<anonymous> (src/app/http-interceptors/token-interceptor.spec.ts:57:62)
            at ZoneDelegate.invoke (node_modules/zone.js/fesm2015/zone.js:372:1)
            at ProxyZoneSpec.onInvoke (node_modules/zone.js/fesm2015/zone-testing.js:287:1)
        Error: Expected null to be 'Bearer some-token'.
            at <Jasmine>
            at UserContext.<anonymous> (src/app/http-interceptors/token-interceptor.spec.ts:58:62)
            at ZoneDelegate.invoke (node_modules/zone.js/fesm2015/zone.js:372:1)
            at ProxyZoneSpec.onInvoke (node_modules/zone.js/fesm2015/zone-testing.js:287:1)

Why doesn't expectOne find the request being sent by our service? Is the issue in the interceptor itself or the way the test is constructed?

CodePudding user response:

I think the issue is in the interceptor because we may be not waiting for the async task of const token = await this.authService.getCurrentUserToken(); to complete before going on to the assertion.

Add a console.log like this in the interceptor:

return defer(async () => {
        console.log('[Interceptor] Before token');
        const token = await this.authService.getCurrentUserToken();
        console.log('[Interceptor] After token');
        const request = req.clone({
          headers: this.getDefaultHeaders(token),
          withCredentials: true,
        });
        return next.handle(request);
      }).pipe(mergeAll());

Try using fakeAsync and tick:

it('should add an Authorization header', fakeAsync(() => {
    service.sendSomeRequestToServer().subscribe((response) => {
      expect(response).toBeTruthy();
    });

    // Add a tick here to tell Angular to finish all promises encountered
    // in the code before carrying forward with the tests.
    tick();
    console.log('[Test] Before expectation');
    const httpRequest = httpMock.expectOne({});

    expect(httpRequest.request.headers.has('Authorization')).toEqual(true);
    expect(httpRequest.request.headers.get('Authorization')).toBe(
      'Bearer some-token'
    );

    httpRequest.flush({ message: "SUCCESS" });

    httpMock.verify();
  }));

Without the tick, I suspect you would see [Interceptor] Before token and then [Test] Before expectation (wrong order) vs. with the tick you should see [Interceptor] Before token, [Interceptor] After token and then [Test] Before expectation (Right order)

  • Related