/** * @license * Copyright Google Inc. All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ import { describe, it, beforeEach } from 'razmin'; import { expect } from 'chai'; import { Observable } from 'rxjs'; import { toArray } from 'rxjs/operators'; import { HttpRequest } from '../request'; import { HttpDownloadProgressEvent, HttpErrorResponse, HttpEvent, HttpEventType, HttpHeaderResponse, HttpResponse, HttpResponseBase, HttpUploadProgressEvent } from '../response'; import { HttpXhrBackend } from '../xhr'; import { MockXhrFactory } from './xhr.mock'; function trackEvents(obs: Observable>): HttpEvent[] { const events: HttpEvent[] = []; obs.subscribe(event => events.push(event), err => events.push(err)); return events; } const TEST_POST = new HttpRequest('POST', '/test', 'some body', { responseType: 'text', }); const XSSI_PREFIX = ')]}\'\n'; { describe('XhrBackend', () => { let factory: MockXhrFactory = null!; let backend: HttpXhrBackend = null!; beforeEach(() => { factory = new MockXhrFactory(); backend = new HttpXhrBackend(factory); }); it('emits status immediately', () => { const events = trackEvents(backend.handle(TEST_POST)); expect(events.length).to.equal(1); expect(events[0].type).to.equal(HttpEventType.Sent); }); it('sets method, url, and responseType correctly', () => { backend.handle(TEST_POST).subscribe(); expect(factory.mock.method).to.equal('POST'); expect(factory.mock.responseType).to.equal('text'); expect(factory.mock.url).to.equal('/test'); }); it('sets outgoing body correctly', () => { backend.handle(TEST_POST).subscribe(); expect(factory.mock.body).to.equal('some body'); }); it('sets outgoing headers, including default headers', () => { const post = TEST_POST.clone({ setHeaders: { 'Test': 'Test header', }, }); backend.handle(post).subscribe(); expect(factory.mock.mockHeaders).to.eql({ 'Test': 'Test header', 'Accept': 'application/json, text/plain, */*', 'Content-Type': 'text/plain', }); }); it('sets outgoing headers, including overriding defaults', () => { const setHeaders = { 'Test': 'Test header', 'Accept': 'text/html', 'Content-Type': 'text/css', }; backend.handle(TEST_POST.clone({ setHeaders })).subscribe(); expect(factory.mock.mockHeaders).to.eql(setHeaders); }); it('passes withCredentials through', () => { backend.handle(TEST_POST.clone({ withCredentials: true })).subscribe(); expect(factory.mock.withCredentials).to.equal(true); }); it('handles a text response', () => { const events = trackEvents(backend.handle(TEST_POST)); factory.mock.mockFlush(200, 'OK', 'some response'); expect(events.length).to.equal(2); expect(events[1].type).to.equal(HttpEventType.Response); expect(events[1] instanceof HttpResponse).to.be.ok; const res = events[1] as HttpResponse; expect(res.body).to.equal('some response'); expect(res.status).to.equal(200); expect(res.statusText).to.equal('OK'); }); it('handles a json response', () => { const events = trackEvents(backend.handle(TEST_POST.clone({ responseType: 'json' }))); factory.mock.mockFlush(200, 'OK', JSON.stringify({ data: 'some data' })); expect(events.length).to.equal(2); const res = events[1] as HttpResponse<{ data: string }>; expect(res.body!.data).to.equal('some data'); }); it('handles a blank json response', () => { const events = trackEvents(backend.handle(TEST_POST.clone({ responseType: 'json' }))); factory.mock.mockFlush(200, 'OK', ''); expect(events.length).to.equal(2); const res = events[1] as HttpResponse<{ data: string }>; expect(res.body).to.be.null; }); it('handles a json error response', () => { const events = trackEvents(backend.handle(TEST_POST.clone({ responseType: 'json' }))); factory.mock.mockFlush(500, 'Error', JSON.stringify({ data: 'some data' })); expect(events.length).to.equal(2); const res = events[1] as any as HttpErrorResponse; expect(res.error!.data).to.equal('some data'); }); it('handles a json error response with XSSI prefix', () => { const events = trackEvents(backend.handle(TEST_POST.clone({ responseType: 'json' }))); factory.mock.mockFlush(500, 'Error', XSSI_PREFIX + JSON.stringify({ data: 'some data' })); expect(events.length).to.equal(2); const res = events[1] as any as HttpErrorResponse; expect(res.error!.data).to.equal('some data'); }); it('handles a json string response', () => { const events = trackEvents(backend.handle(TEST_POST.clone({ responseType: 'json' }))); expect(factory.mock.responseType).to.equal('text'); factory.mock.mockFlush(200, 'OK', JSON.stringify('this is a string')); expect(events.length).to.equal(2); const res = events[1] as HttpResponse; expect(res.body).to.equal('this is a string'); }); it('handles a json response with an XSSI prefix', () => { const events = trackEvents(backend.handle(TEST_POST.clone({ responseType: 'json' }))); factory.mock.mockFlush(200, 'OK', XSSI_PREFIX + JSON.stringify({ data: 'some data' })); expect(events.length).to.equal(2); const res = events[1] as HttpResponse<{ data: string }>; expect(res.body!.data).to.equal('some data'); }); it('emits unsuccessful responses via the error path', done => { backend.handle(TEST_POST).subscribe(undefined, (err: HttpErrorResponse) => { expect(err instanceof HttpErrorResponse).to.equal(true); expect(err.error).to.equal('this is the error'); done!(); }); factory.mock.mockFlush(400, 'Bad Request', 'this is the error'); }); it('emits real errors via the error path', done => { backend.handle(TEST_POST).subscribe(undefined, (err: HttpErrorResponse) => { expect(err instanceof HttpErrorResponse).to.equal(true); expect(err.error instanceof Error); done!(); }); factory.mock.mockErrorEvent(new Error('blah')); }); describe('progress events', () => { it('are emitted for download progress', done => { backend.handle(TEST_POST.clone({ reportProgress: true })) .pipe(toArray()) .subscribe(events => { expect(events.map(event => event.type)).to.eql([ HttpEventType.Sent, HttpEventType.ResponseHeader, HttpEventType.DownloadProgress, HttpEventType.DownloadProgress, HttpEventType.Response, ]); const [progress1, progress2, response] = [ events[2] as HttpDownloadProgressEvent, events[3] as HttpDownloadProgressEvent, events[4] as HttpResponse ]; expect(progress1.partialText).to.equal('down'); expect(progress1.loaded).to.equal(100); expect(progress1.total).to.equal(300); expect(progress2.partialText).to.equal('download'); expect(progress2.loaded).to.equal(200); expect(progress2.total).to.equal(300); expect(response.body).to.equal('downloaded'); done!(); }); factory.mock.responseText = 'down'; factory.mock.mockDownloadProgressEvent(100, 300); factory.mock.responseText = 'download'; factory.mock.mockDownloadProgressEvent(200, 300); factory.mock.mockFlush(200, 'OK', 'downloaded'); }); it('are emitted for upload progress', done => { backend.handle(TEST_POST.clone({ reportProgress: true })) .pipe(toArray()) .subscribe(events => { expect(events.map(event => event.type)).to.eql([ HttpEventType.Sent, HttpEventType.UploadProgress, HttpEventType.UploadProgress, HttpEventType.Response, ]); const [progress1, progress2] = [ events[1] as HttpUploadProgressEvent, events[2] as HttpUploadProgressEvent, ]; expect(progress1.loaded).to.equal(100); expect(progress1.total).to.equal(300); expect(progress2.loaded).to.equal(200); expect(progress2.total).to.equal(300); done!(); }); factory.mock.mockUploadProgressEvent(100, 300); factory.mock.mockUploadProgressEvent(200, 300); factory.mock.mockFlush(200, 'OK', 'Done'); }); it('are emitted when both upload and download progress are available', done => { backend.handle(TEST_POST.clone({ reportProgress: true })) .pipe(toArray()) .subscribe(events => { expect(events.map(event => event.type)).to.eql([ HttpEventType.Sent, HttpEventType.UploadProgress, HttpEventType.ResponseHeader, HttpEventType.DownloadProgress, HttpEventType.Response, ]); done!(); }); factory.mock.mockUploadProgressEvent(100, 300); factory.mock.mockDownloadProgressEvent(200, 300); factory.mock.mockFlush(200, 'OK', 'Done'); }); it('are emitted even if length is not computable', done => { backend.handle(TEST_POST.clone({ reportProgress: true })) .pipe(toArray()) .subscribe(events => { expect(events.map(event => event.type)).to.eql([ HttpEventType.Sent, HttpEventType.UploadProgress, HttpEventType.ResponseHeader, HttpEventType.DownloadProgress, HttpEventType.Response, ]); done!(); }); factory.mock.mockUploadProgressEvent(100); factory.mock.mockDownloadProgressEvent(200); factory.mock.mockFlush(200, 'OK', 'Done'); }); it('include ResponseHeader with headers and status', done => { backend.handle(TEST_POST.clone({ reportProgress: true })) .pipe(toArray()) .subscribe(events => { expect(events.map(event => event.type)).to.eql([ HttpEventType.Sent, HttpEventType.ResponseHeader, HttpEventType.DownloadProgress, HttpEventType.Response, ]); const partial = events[1] as HttpHeaderResponse; expect(partial.headers.get('Content-Type')).to.equal('text/plain'); expect(partial.headers.get('Test')).to.equal('Test header'); done!(); }); factory.mock.mockResponseHeaders = 'Test: Test header\nContent-Type: text/plain\n'; factory.mock.mockDownloadProgressEvent(200); factory.mock.mockFlush(200, 'OK', 'Done'); }); it('are unsubscribed along with the main request', () => { const sub = backend.handle(TEST_POST.clone({ reportProgress: true })).subscribe(); expect(factory.mock.listeners.progress).not.to.be.undefined; sub.unsubscribe(); expect(factory.mock.listeners.progress).to.be.undefined; }); it('do not cause headers to be re-parsed on main response', done => { backend.handle(TEST_POST.clone({ reportProgress: true })) .pipe(toArray()) .subscribe(events => { events .filter( event => event.type === HttpEventType.Response || event.type === HttpEventType.ResponseHeader) .map(event => event as HttpResponseBase) .forEach(event => { expect(event.status).to.equal(203); expect(event.headers.get('Test')).to.equal('This is a test'); }); done!(); }); factory.mock.mockResponseHeaders = 'Test: This is a test\n'; factory.mock.status = 203; factory.mock.mockDownloadProgressEvent(100, 300); factory.mock.mockResponseHeaders = 'Test: should never be read\n'; factory.mock.mockFlush(203, 'OK', 'Testing 1 2 3'); }); }); describe('gets response URL', () => { it('from XHR.responsesURL', done => { backend.handle(TEST_POST).pipe(toArray()).subscribe(events => { expect(events.length).to.equal(2); expect(events[1].type).to.equal(HttpEventType.Response); const response = events[1] as HttpResponse; expect(response.url).to.equal('/response/url'); done!(); }); factory.mock.responseURL = '/response/url'; factory.mock.mockFlush(200, 'OK', 'Test'); }); it('from X-Request-URL header if XHR.responseURL is not present', done => { backend.handle(TEST_POST).pipe(toArray()).subscribe(events => { expect(events.length).to.equal(2); expect(events[1].type).to.equal(HttpEventType.Response); const response = events[1] as HttpResponse; expect(response.url).to.equal('/response/url'); done!(); }); factory.mock.mockResponseHeaders = 'X-Request-URL: /response/url\n'; factory.mock.mockFlush(200, 'OK', 'Test'); }); it('falls back on Request.url if neither are available', done => { backend.handle(TEST_POST).pipe(toArray()).subscribe(events => { expect(events.length).to.equal(2); expect(events[1].type).to.equal(HttpEventType.Response); const response = events[1] as HttpResponse; expect(response.url).to.equal('/test'); done!(); }); factory.mock.mockFlush(200, 'OK', 'Test'); }); }); describe('corrects for quirks', () => { it('by normalizing 1223 status to 204', done => { backend.handle(TEST_POST).pipe(toArray()).subscribe(events => { expect(events.length).to.equal(2); expect(events[1].type).to.equal(HttpEventType.Response); const response = events[1] as HttpResponse; expect(response.status).to.equal(204); done!(); }); factory.mock.mockFlush(1223, 'IE Special Status', 'Test'); }); it('by normalizing 0 status to 200 if a body is present', done => { backend.handle(TEST_POST).pipe(toArray()).subscribe(events => { expect(events.length).to.equal(2); expect(events[1].type).to.equal(HttpEventType.Response); const response = events[1] as HttpResponse; expect(response.status).to.equal(200); done!(); }); factory.mock.mockFlush(0, 'CORS 0 status', 'Test'); }); it('by leaving 0 status as 0 if a body is not present', done => { backend.handle(TEST_POST).pipe(toArray()).subscribe( undefined, (error: HttpErrorResponse) => { expect(error.status).to.equal(0); done!(); }); factory.mock.mockFlush(0, 'CORS 0 status'); }); }); }); }