Reputation: 369
If searched around SO for answers and so far, everything I've tried produces the same missing information.
This is running Angular 10 with the latest version of Karma/Jasmine.
Essentially, I have an HTTP Interceptor that is looking at the content-type of the return object. If it's json, continue as normal, if it's html...then throw an error.
import {
HttpEvent,
HttpHandler,
HttpInterceptor,
HttpRequest,
HttpResponse
} from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { httpStatusCodes } from '../../../shared/enums/httpStatusCodes.enum';
import { errorResponse } from './../../../shared/models/errorResponse.model';
@Injectable()
export class WafErrorInterceptor implements HttpInterceptor {
intercept(
request: HttpRequest<any>,
next: HttpHandler
): Observable<HttpEvent<any>> {
return next.handle(request).pipe(
map((event: HttpEvent<any>) => {
console.log(event instanceof HttpResponse);
if (
event instanceof HttpResponse &&
event.headers.has('content-type') &&
event.headers.get('content-type') === 'application/json'
) {
return event;
}
const throwErrorResponse = new errorResponse(
httpStatusCodes.WAF_ERROR,
'99999',
event instanceof HttpResponse
? event.body
: 'unknown error occurred'
);
throw throwErrorResponse;
})
);
}
}
And then in my unit test I run this:
import {
HttpClient,
HttpHeaders,
HttpResponse,
HTTP_INTERCEPTORS
} from '@angular/common/http';
import {
HttpClientTestingModule,
HttpTestingController
} from '@angular/common/http/testing';
import { fakeAsync, TestBed, tick } from '@angular/core/testing';
import { errorResponse } from './../../../shared/models/errorResponse.model';
import { WafErrorInterceptor } from './waf-error.service';
describe('WafErrorInterceptor', () => {
let httpMock: HttpTestingController;
let httpClient: HttpClient;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
providers: [
{
provide: HTTP_INTERCEPTORS,
useClass: WafErrorInterceptor,
multi: true
}
]
});
httpMock = TestBed.get(HttpTestingController);
httpClient = TestBed.get(HttpClient);
});
afterEach(() => {
httpMock.verify();
});
it('intercept: when no error, then subscribe returns successfully', () => {
const testData: string = 'test';
httpClient.get<string>('https://secure.testurl.com/success').subscribe(
(data) => expect(data).toBeTruthy(),
(error: errorResponse) => {
console.log(error);
fail('error should not have been called');
}
);
tick();
let req = httpMock.expectOne('https://secure.testurl.com/success');
tick();
let httpHeaders = new HttpHeaders();
httpHeaders.set('content-type', 'application/json');
const expectedResponse = new HttpResponse<string>({
status: 200,
statusText: 'OK',
body: testData,
headers: httpHeaders
});
//req.flush(expectedResponse);
req.event(expectedResponse);
});
});
I've tried flush where I just send back data, where i send back data and the headers/status. Where I send back an httpresponse, etc. Each time, when it gets into the interceptor, the interceptor doesn't see the response as of type httpresponse and the console.log always returns false.
I've even just created a unit test where the unit test sends in a mock object...and even that has the same issue.
Thoughts?
UPDATED: So the below answer works for the Map, but still having issues testing the catchErrorInterceptor. The code works on my site. Our API returns an object where the errors contains an array of errors. So we grab the first one and use that.
@Injectable()
export class ErrorInterceptor implements HttpInterceptor {
intercept(
request: HttpRequest<any>,
next: HttpHandler
): Observable<HttpEvent<any>> {
return next.handle(request).pipe(
catchError((error: HttpErrorResponse) => {
let statusCode = httpStatusCodes.CONFLICT;
let errorMessage = '';
let errorCode = '99999';
statusCode =
error.status === 0
? httpStatusCodes.INTERNAL_SERVER_ERROR
: error.status;
if (error.error.errors && error.error.errors.length > 0) {
errorCode = error.error.errors[0].code;
errorMessage = error.error.errors[0].description;
} else {
errorMessage = error.message;
}
const throwErrorResponse = new errorResponse(
statusCode,
errorCode,
errorMessage
);
return throwError(throwErrorResponse);
})
);
}
}
Here is one of the tests:
it('intercept: when delibarate 409, then error returned', (done) => {
httpClient
.get<string>('https://secure.go2bank.com/error')
.pipe(skip(1))
.subscribe(
(data) => fail('should have failed with the 404 error'),
(error: errorResponse) => {
expect(error).toBeTruthy(); // check if executed
expect(error.httpStatusCodes).toBe(
httpStatusCodes.CONFLICT
);
expect(error.errorCode).toBe('1007');
expect(error.errorMessage).toBe(
'Validation error Authorization'
);
done();
}
);
const errorInitEvent: ErrorEventInit = {
message: null,
error: {
errors: [
{
code: '1007',
description: 'Validation error Authorization.',
message: null,
link: null,
additionalinfo: null
}
]
}),
lineno: null,
colno: null,
filename: null
};
let error = new ErrorEvent('ERROR', errorInitEvent);
httpMock.expectOne('https://secure.go2bank.com/error').error(error, {
status: httpStatusCodes.CONFLICT,
statusText: 'Conflict',
headers: new HttpHeaders().set('content-type', 'application/json')
});
});
The result of this test is always 99999 and not 1007. So I'm getting the error, it's catching the error. But when I look at it, error.error is ErrorEvent(isTrusted: //"), and it doesn't look like I have an array of Errors inside of Error.
Upvotes: 1
Views: 8405
Reputation: 214047
The main problem here is that in case of HttpClientTestingModule
Angular uses HttpClientTestingBackend
class instead of HttpXhrBackend in order to mock actual http request.
But there is one big difference in their implementations:
HttpClientTestingBackend
always sends { type: HttpEventType.Sent }
event while HttpXhrBackend usually sends HttpResponse
event.
This means that the event { type: HttpEventType.Sent }
is the first event that is handled inside your map
rxjs operator and it will fail.
So, you need to filter the response like:
interceptor.ts
import {
HttpEventType,
...
} from '@angular/common/http';
...
map((event: HttpEvent<any>) => {
if (event.type === HttpEventType.Sent) { <---------- add this
return event;
}
if (
event instanceof HttpResponse &&
event.headers.has('content-type') &&
event.headers.get('content-type') === 'application/json'
) {
return event;
}
...
Update: Or it would be even better to to use skip(1) rxjs operator in your test.
Now, let's come back to your test.
First, you need to remove all redundant tick()
calls.
Then flush
method has slightly different signature.
flush(body, opts?: {
headers?: HttpHeaders | {
[name: string]: string | string[];
};
status?: number;
statusText?: string;
})
But you tried to put everything into body
.
So, here's your test case:
interceptor.spec.ts
it('intercept: when no error, then subscribe returns successfully', () => {
const testData: string = 'test';
httpClient.get<string>('https://secure.testurl.com/success').subscribe(
(data) => expect(data).toBeTruthy(),
(error: errorResponse) => {
console.log(error);
fail('error should not have been called');
}
);
const req = httpMock.expectOne('https://secure.testurl.com/success');
req.flush(testData, {
status: 200,
statusText: 'OK',
headers: new HttpHeaders().set('content-type', 'application/json')
});
});
Upvotes: 1