/
githubmirror
/
angular
Обзор
Документация
Войти
/
githubmirror
/
angular
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
packages/common/http/test/fetch_spec.ts
933 строки
30 KB
SkyZeroZx
fix(http): always decode JSON responses as UTF-8
08 авг 2026, 01:54
08 авг 2026, 01:54
09bc900
Код
Авторство
О чём код?
/** * @license * Copyright Google LLC 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.dev/license */ import {TestBed} from '@angular/core/testing'; import {Observable, of, Subject} from 'rxjs'; import {catchError, retry, scan, skip, take, toArray} from 'rxjs/operators'; import {HttpEvent, HttpEventType, HttpRequest, HttpResponse} from '../index'; import { HttpClient, HttpDownloadProgressEvent, HttpErrorResponse, HttpHeaderResponse, HttpParams, HttpStatusCode, provideHttpClient, } from '../public_api'; import {RuntimeErrorCode} from '../src/errors'; import {FetchBackend, FetchFactory, HTTP_FETCH_MAX_RESPONSE_SIZE} from '../src/fetch'; function trackEvents(obs: Observable<any>): Promise<any[]> { return obs .pipe( // We don't want the promise to fail on HttpErrorResponse catchError((e) => of(e)), scan((acc, event) => { acc.push(event); return acc; }, [] as any[]), ) .toPromise() as Promise<any[]>; } const TEST_POST = new HttpRequest('POST', '/test', 'some body', { responseType: 'text', timeout: 1000, }); const TEST_POST_WITH_JSON_BODY = new HttpRequest( 'POST', '/test', {'some': 'body'}, { responseType: 'text', }, ); const XSSI_PREFIX = ")]}'\n"; describe('FetchBackend', () => { let fetchMock: MockFetchFactory = null!; let backend: FetchBackend = null!; let fetchSpy: jasmine.Spy<typeof fetch>; function callFetchAndFlush(req: HttpRequest<any>): void { backend.handle(req).pipe(take(1)).subscribe(); fetchMock.mockFlush(HttpStatusCode.Ok, 'OK', 'some response'); } beforeEach(() => { TestBed.configureTestingModule({ providers: [{provide: FetchFactory, useClass: MockFetchFactory}, FetchBackend], }); fetchMock = TestBed.inject(FetchFactory) as MockFetchFactory; fetchSpy = spyOn(fetchMock, 'fetch').and.callThrough(); backend = TestBed.inject(FetchBackend); }); it('emits status immediately', () => { let event!: HttpEvent<any>; // subscribe is sync backend .handle(TEST_POST) .pipe(take(1)) .subscribe((e) => (event = e)); fetchMock.mockFlush(HttpStatusCode.Ok, 'OK', 'some response'); expect(event.type).toBe(HttpEventType.Sent); }); it('should not call fetch without a subscribe', () => { const handle = backend.handle(TEST_POST); expect(fetchSpy).not.toHaveBeenCalled(); handle.subscribe(); fetchMock.mockFlush(HttpStatusCode.Ok, 'OK', 'some response'); expect(fetchSpy).toHaveBeenCalled(); }); it('should be able to retry', (done) => { const handle = backend.handle(TEST_POST); // Skipping both HttpSentEvent (from the 1st subscription + retry) handle.pipe(retry(1), skip(2)).subscribe((response) => { expect(response.type).toBe(HttpEventType.Response); expect((response as HttpResponse<any>).body).toBe('some response'); done(); }); fetchMock.mockErrorEvent('Error 1'); fetchMock.resetFetchPromise(); fetchMock.mockFlush(HttpStatusCode.Ok, 'OK', 'some response'); }); it('sets method, url, and responseType correctly', () => { callFetchAndFlush(TEST_POST); expect(fetchMock.request.method).toBe('POST'); expect(fetchMock.request.url).toBe('/test'); }); it('use query params from request', () => { const requestWithQuery = new HttpRequest('GET', '/test', 'some body', { params: new HttpParams({fromObject: {query: 'foobar'}}), responseType: 'text', }); callFetchAndFlush(requestWithQuery); expect(fetchMock.request.method).toBe('GET'); expect(fetchMock.request.url).toBe('/test?query=foobar'); }); it('sets outgoing body correctly', () => { callFetchAndFlush(TEST_POST); expect(fetchMock.request.body).toBe('some body'); }); it('sets outgoing body correctly when request payload is json', () => { callFetchAndFlush(TEST_POST_WITH_JSON_BODY); expect(fetchMock.request.body).toBe('{"some":"body"}'); }); it('sets outgoing headers, including default headers', () => { const post = TEST_POST.clone({ setHeaders: { 'Test': 'Test header', }, }); callFetchAndFlush(post); expect(fetchMock.request.headers).toEqual({ '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', }; callFetchAndFlush(TEST_POST.clone({setHeaders})); expect(fetchMock.request.headers).toEqual(setHeaders); }); it('should be case insensitive for Content-Type & Accept', () => { const setHeaders = { 'accept': 'text/html', 'content-type': 'text/css', }; callFetchAndFlush(TEST_POST.clone({setHeaders})); expect(fetchMock.request.headers).toEqual(setHeaders); }); it('passes withCredentials through', () => { callFetchAndFlush(TEST_POST.clone({withCredentials: true})); expect(fetchMock.request.credentials).toBe('include'); }); it('handles a text response', async () => { const promise = trackEvents(backend.handle(TEST_POST)); fetchMock.mockFlush(HttpStatusCode.Ok, 'OK', 'some response'); const events = await promise; expect(events.length).toBe(2); expect(events[1].type).toBe(HttpEventType.Response); expect(events[1] instanceof HttpResponse).toBeTruthy(); const res = events[1] as HttpResponse<string>; expect(res.body).toBe('some response'); expect(res.status).toBe(HttpStatusCode.Ok); expect(res.statusText).toBe('OK'); }); it('handles a text response with iso-8859-1 charset', async () => { const promise = trackEvents(backend.handle(TEST_POST)); // "café" encoded in ISO-8859-1 (0x63, 0x61, 0x66, 0xE9) const isoBytes = new Uint8Array([0x63, 0x61, 0x66, 0xe9]); fetchMock.mockFlush(HttpStatusCode.Ok, 'OK', isoBytes, { 'Content-Type': 'text/html; charset=iso-8859-1', }); const events = await promise; expect(events.length).toBe(2); const res = events[1] as HttpResponse<string>; expect(res.body).toBe('café'); }); it('handles a text response with windows-1252 charset in quotes', async () => { const promise = trackEvents(backend.handle(TEST_POST)); // "€" encoded in Windows-1252 (0x80) const windowsBytes = new Uint8Array([0x80]); fetchMock.mockFlush(HttpStatusCode.Ok, 'OK', windowsBytes, { 'Content-Type': 'text/plain; charset="windows-1252"', }); const events = await promise; expect(events.length).toBe(2); const res = events[1] as HttpResponse<string>; expect(res.body).toBe('€'); }); it('falls back to default utf-8 when given an invalid charset', async () => { const promise = trackEvents(backend.handle(TEST_POST)); // "café" encoded in UTF-8 const utf8Bytes = new Uint8Array([0x63, 0x61, 0x66, 0xc3, 0xa9]); fetchMock.mockFlush(HttpStatusCode.Ok, 'OK', utf8Bytes, { 'Content-Type': 'text/plain; charset=invalid-encoding-label', }); const events = await promise; expect(events.length).toBe(2); const res = events[1] as HttpResponse<string>; expect(res.body).toBe('café'); }); it('handles a json response', async () => { const promise = trackEvents(backend.handle(TEST_POST.clone({responseType: 'json'}))); fetchMock.mockFlush(HttpStatusCode.Ok, 'OK', JSON.stringify({data: 'some data'})); const events = await promise; expect(events.length).toBe(2); const res = events[1] as HttpResponse<{data: string}>; expect(res.body!.data).toBe('some data'); }); it('decodes a json response as UTF-8 regardless of its declared charset', async () => { const promise = trackEvents(backend.handle(TEST_POST.clone({responseType: 'json'}))); fetchMock.mockFlush(HttpStatusCode.Ok, 'OK', JSON.stringify({data: 'café'}), { 'Content-Type': 'application/json; charset=windows-1251', }); const events = await promise; const res = events[1] as HttpResponse<{data: string}>; expect(res.body!.data).toBe('café'); }); it('handles a blank json response', async () => { const promise = trackEvents(backend.handle(TEST_POST.clone({responseType: 'json'}))); fetchMock.mockFlush(HttpStatusCode.Ok, 'OK', ''); const events = await promise; expect(events.length).toBe(2); const res = events[1] as HttpResponse<{data: string}>; expect(res.body).toBeNull(); }); it('handles a json error response', async () => { const promise = trackEvents(backend.handle(TEST_POST.clone({responseType: 'json'}))); fetchMock.mockFlush( HttpStatusCode.InternalServerError, 'Error', JSON.stringify({data: 'some data'}), ); const events = await promise; expect(events.length).toBe(2); const res = events[1] as any as HttpErrorResponse; expect(res.error.data).toBe('some data'); }); it('handles a text error response when a json success response was expected', async () => { const promise = trackEvents(backend.handle(TEST_POST.clone({responseType: 'json'}))); fetchMock.mockFlush(HttpStatusCode.InternalServerError, 'Error', 'simple text error'); const events = await promise; expect(events.length).toBe(2); const res = events[1] as any as HttpErrorResponse; expect(res.error).toBe('simple text error'); }); it('handles a json error response with XSSI prefix', async () => { const promise = trackEvents(backend.handle(TEST_POST.clone({responseType: 'json'}))); fetchMock.mockFlush( HttpStatusCode.InternalServerError, 'Error', XSSI_PREFIX + JSON.stringify({data: 'some data'}), ); const events = await promise; expect(events.length).toBe(2); const res = events[1] as any as HttpErrorResponse; expect(res.error.data).toBe('some data'); }); it('handles a text error response with XSSI prefix when a json success response was expected', async () => { const promise = trackEvents(backend.handle(TEST_POST.clone({responseType: 'json'}))); fetchMock.mockFlush( HttpStatusCode.InternalServerError, 'Error', XSSI_PREFIX + 'simple text error', ); const events = await promise; expect(events.length).toBe(2); const res = events[1] as any as HttpErrorResponse; expect(res.error).toBe('simple text error'); }); it('handles a json string response', async () => { const promise = trackEvents(backend.handle(TEST_POST.clone({responseType: 'json'}))); fetchMock.mockFlush(HttpStatusCode.Ok, 'OK', JSON.stringify('this is a string')); const events = await promise; expect(events.length).toBe(2); const res = events[1] as HttpResponse<string>; expect(res.body).toEqual('this is a string'); }); it('handles a json response with an XSSI prefix', async () => { const promise = trackEvents(backend.handle(TEST_POST.clone({responseType: 'json'}))); fetchMock.mockFlush(HttpStatusCode.Ok, 'OK', XSSI_PREFIX + JSON.stringify({data: 'some data'})); const events = await promise; expect(events.length).toBe(2); const res = events[1] as HttpResponse<{data: string}>; expect(res.body!.data).toBe('some data'); }); it('handles a blob with a mime type', async () => { const promise = trackEvents(backend.handle(TEST_POST.clone({responseType: 'blob'}))); const type = 'application/pdf'; fetchMock.mockFlush(HttpStatusCode.Ok, 'OK', new Blob(), {'Content-Type': type}); const events = await promise; expect(events.length).toBe(2); const res = events[1] as HttpResponse<Blob>; expect(res.body?.type).toBe(type); }); it('emits unsuccessful responses via the error path', (done) => { backend.handle(TEST_POST).subscribe({ error: (err: HttpErrorResponse) => { expect(err instanceof HttpErrorResponse).toBe(true); expect(err.error).toBe('this is the error'); done(); }, }); fetchMock.mockFlush(HttpStatusCode.BadRequest, 'Bad Request', 'this is the error'); }); it('emits real errors via the error path', (done) => { // Skipping the HttpEventType.Sent that is sent first backend .handle(TEST_POST) .pipe(skip(1)) .subscribe({ error: (err: HttpErrorResponse) => { expect(err instanceof HttpErrorResponse).toBe(true); expect(err.error instanceof Error).toBeTrue(); expect(err.url).toBe('/test'); done(); }, }); fetchMock.mockErrorEvent(new Error('blah')); }); it('emits an error when browser cancels a request', (done) => { backend.handle(TEST_POST).subscribe({ error: (err: HttpErrorResponse) => { expect(err instanceof HttpErrorResponse).toBe(true); expect(err.error instanceof DOMException).toBeTrue(); expect((err.error as DOMException).name).toBe('AbortError'); done(); }, }); fetchMock.mockAbortEvent(); }); it('should pass keepalive option to fetch', () => { const req = new HttpRequest('GET', '/test', {keepalive: true}); backend.handle(req).subscribe(); expect(fetchSpy).toHaveBeenCalledWith( '/test', jasmine.objectContaining({ keepalive: true, }), ); fetchMock.mockFlush(HttpStatusCode.Ok, 'OK'); }); it('should pass priority option to fetch', () => { const req = new HttpRequest('GET', '/test', {priority: 'high'}); backend.handle(req).subscribe(); expect(fetchSpy).toHaveBeenCalledWith( '/test', jasmine.objectContaining({ priority: 'high', }), ); fetchMock.mockFlush(HttpStatusCode.Ok, 'OK'); }); it('should pass cache option to fetch', () => { const req = new HttpRequest('GET', '/test', {cache: 'only-if-cached'}); backend.handle(req).subscribe(); expect(fetchSpy).toHaveBeenCalledWith( '/test', jasmine.objectContaining({ cache: 'only-if-cached', }), ); fetchMock.mockFlush(HttpStatusCode.Ok, 'OK'); }); it('should pass referrerPolicy option to fetch', () => { const req = new HttpRequest('GET', '/test', {referrerPolicy: 'no-referrer'}); backend.handle(req).subscribe(); expect(fetchSpy).toHaveBeenCalledWith( '/test', jasmine.objectContaining({ referrerPolicy: 'no-referrer', }), ); fetchMock.mockFlush(HttpStatusCode.Ok, 'OK'); }); it('emits an error when a request times out', (done) => { backend.handle(TEST_POST).subscribe({ error: (err: HttpErrorResponse) => { expect(err instanceof HttpErrorResponse).toBe(true); expect(err.error instanceof DOMException).toBeTrue(); expect((err.error as DOMException).name).toBe('TimeoutError'); done(); }, }); fetchMock.mockTimeoutEvent(); }); it('should pass mode option to fetch', () => { const req = new HttpRequest('GET', '/test', {mode: 'cors'}); backend.handle(req).subscribe(); expect(fetchSpy).toHaveBeenCalledWith( '/test', jasmine.objectContaining({ mode: 'cors', }), ); fetchMock.mockFlush(HttpStatusCode.Ok, 'OK'); }); it('should pass redirect option to fetch', () => { const req = new HttpRequest('GET', '/test', {redirect: 'follow'}); backend.handle(req).subscribe(); expect(fetchSpy).toHaveBeenCalledWith( '/test', jasmine.objectContaining({ redirect: 'follow', }), ); fetchMock.mockFlush(HttpStatusCode.Ok, 'OK'); }); it('should pass credentials option to fetch', () => { const req = new HttpRequest('GET', '/test', {credentials: 'omit'}); backend.handle(req).subscribe(); expect(fetchSpy).toHaveBeenCalledWith( '/test', jasmine.objectContaining({ credentials: 'omit', }), ); fetchMock.mockFlush(HttpStatusCode.Ok, 'OK'); }); it('should pass referrer option to fetch', () => { const req = new HttpRequest('GET', '/test', {referrer: 'about:client'}); backend.handle(req).subscribe(); expect(fetchSpy).toHaveBeenCalledWith( '/test', jasmine.objectContaining({ referrer: 'about:client', }), ); fetchMock.mockFlush(HttpStatusCode.Ok, 'OK'); }); it('should pass integrity option to fetch', () => { const req = new HttpRequest('GET', '/test', {integrity: 'sha256-...'}); backend.handle(req).subscribe(); expect(fetchSpy).toHaveBeenCalledWith( '/test', jasmine.objectContaining({ integrity: 'sha256-...', }), ); fetchMock.mockFlush(HttpStatusCode.Ok, 'OK'); }); 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)).toEqual([ HttpEventType.Sent, HttpEventType.ResponseHeader, HttpEventType.DownloadProgress, HttpEventType.DownloadProgress, HttpEventType.DownloadProgress, HttpEventType.Response, ]); const [progress1, progress2, response] = [ events[2] as HttpDownloadProgressEvent, events[3] as HttpDownloadProgressEvent, events[5] as HttpResponse<string>, ]; expect(progress1.partialText).toBe('down'); expect(progress1.loaded).toBe(4); expect(progress1.total).toBe(10); expect(progress2.partialText).toBe('download'); expect(progress2.loaded).toBe(8); expect(progress2.total).toBe(10); expect(response.body).toBe('downloaded'); done(); }); fetchMock.mockProgressEvent(4); fetchMock.mockProgressEvent(8); fetchMock.mockFlush(HttpStatusCode.Ok, 'OK', 'downloaded'); }); it('are emitted when only download progress is requested', (done) => { backend .handle(TEST_POST.clone({reportDownloadProgress: true})) .pipe(toArray()) .subscribe((events) => { expect(events.map((event) => event.type)).toEqual([ HttpEventType.Sent, HttpEventType.ResponseHeader, HttpEventType.DownloadProgress, HttpEventType.DownloadProgress, HttpEventType.Response, ]); done(); }); fetchMock.mockProgressEvent(4); fetchMock.mockFlush(HttpStatusCode.Ok, 'OK', 'downloaded'); }); it('errors when upload progress is requested', (done) => { backend.handle(TEST_POST.clone({reportUploadProgress: true})).subscribe({ error: (error: HttpErrorResponse) => { expect(error.error.code).toBe(RuntimeErrorCode.FETCH_UPLOAD_PROGRESS_NOT_SUPPORTED); expect(error.error.message).toContain('does not support upload progress reporting'); 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)).toEqual([ HttpEventType.Sent, HttpEventType.ResponseHeader, HttpEventType.DownloadProgress, HttpEventType.DownloadProgress, HttpEventType.Response, ]); const partial = events[1] as HttpHeaderResponse; expect(partial.type).toEqual(HttpEventType.ResponseHeader); expect(partial.headers.get('Content-Type')).toEqual('text/plain'); expect(partial.headers.get('Test')).toEqual('Test header'); done(); }); fetchMock.response.headers = {'Test': 'Test header', 'Content-Type': 'text/plain'}; fetchMock.mockProgressEvent(200); fetchMock.mockFlush(HttpStatusCode.Ok, 'OK', 'Done'); }); }); describe('gets response URL', () => { it('from the response URL', (done) => { backend .handle(TEST_POST) .pipe(toArray()) .subscribe((events) => { expect(events.length).toBe(2); expect(events[1].type).toBe(HttpEventType.Response); const response = events[1] as HttpResponse<string>; expect(response.url).toBe('/response/url'); done(); }); fetchMock.response.url = '/response/url'; fetchMock.mockFlush(HttpStatusCode.Ok, 'OK', 'Test'); }); it('falls back on Request.url if neither are available', (done) => { backend .handle(TEST_POST) .pipe(toArray()) .subscribe((events) => { expect(events.length).toBe(2); expect(events[1].type).toBe(HttpEventType.Response); const response = events[1] as HttpResponse<string>; expect(response.url).toBe('/test'); done(); }); fetchMock.mockFlush(HttpStatusCode.Ok, 'OK', 'Test'); }); }); describe('corrects for quirks', () => { it('by normalizing 0 status to 200 if a body is present', (done) => { backend .handle(TEST_POST) .pipe(toArray()) .subscribe((events) => { expect(events.length).toBe(2); expect(events[1].type).toBe(HttpEventType.Response); const response = events[1] as HttpResponse<string>; expect(response.status).toBe(HttpStatusCode.Ok); done(); }); fetchMock.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({ error: (error: HttpErrorResponse) => { expect(error.status).toBe(0); done(); }, }); fetchMock.mockFlush(0, 'CORS 0 status'); }); }); describe('dynamic global fetch', () => { beforeEach(() => { TestBed.resetTestingModule(); TestBed.configureTestingModule({ providers: [provideHttpClient()], }); }); it('should use the current implementation of the global fetch', async () => { const originalFetch = globalThis.fetch; try { const fakeFetch = jasmine .createSpy('', () => Promise.resolve(new Response(JSON.stringify({foo: 'bar'})))) .and.callThrough(); globalThis.fetch = fakeFetch; const client = TestBed.inject(HttpClient); expect(fakeFetch).not.toHaveBeenCalled(); let response = await client.get<unknown>('').toPromise(); expect(fakeFetch).toHaveBeenCalled(); expect(response).toEqual({foo: 'bar'}); // We dynamicaly change the implementation of fetch const fakeFetch2 = jasmine .createSpy('', () => Promise.resolve(new Response(JSON.stringify({foo: 'baz'})))) .and.callThrough(); globalThis.fetch = fakeFetch2; response = await client.get<unknown>('').toPromise(); expect(response).toEqual({foo: 'baz'}); } finally { // We need to restore the original fetch implementation, else the tests might become flaky globalThis.fetch = originalFetch; } }); }); describe('response buffering limits', () => { let infiniteStreamFactory: InfiniteStreamFetchFactory = null!; beforeEach(() => { TestBed.resetTestingModule(); TestBed.configureTestingModule({ providers: [ {provide: FetchFactory, useClass: InfiniteStreamFetchFactory}, {provide: HTTP_FETCH_MAX_RESPONSE_SIZE, useValue: 2048}, FetchBackend, ], }); backend = TestBed.inject(FetchBackend); infiniteStreamFactory = TestBed.inject(FetchFactory) as InfiniteStreamFetchFactory; }); it('aborts a never-ending response stream once the configured size limit is exceeded', async () => { const req = new HttpRequest('GET', '/test', {responseType: 'text'}); const events = await trackEvents(backend.handle(req)); const error = events[1] as HttpErrorResponse; expect(events.length).toBe(2); expect(error instanceof HttpErrorResponse).toBeTrue(); expect(error.error.code).toBe(RuntimeErrorCode.FETCH_RESPONSE_BODY_TOO_LARGE); expect(infiniteStreamFactory.cancelCount).toBe(1); }); it('allows disabling the size limit via dependency injection', async () => { TestBed.resetTestingModule(); TestBed.configureTestingModule({ providers: [ {provide: FetchFactory, useClass: FiniteChunkFetchFactory}, {provide: HTTP_FETCH_MAX_RESPONSE_SIZE, useValue: null}, FetchBackend, ], }); backend = TestBed.inject(FetchBackend); const req = new HttpRequest('GET', '/test', {responseType: 'text'}); const events = await trackEvents(backend.handle(req)); expect(events.length).toBe(2); expect(events[1].type).toBe(HttpEventType.Response); expect((events[1] as HttpResponse<string>).body).toBe('ok'); }); }); describe('cancellation behavior', () => { it('should not abort signal when the request completes successfully', async () => { const req = new HttpRequest('GET', '/test', {responseType: 'text'}); const promise = trackEvents(backend.handle(req)); fetchMock.mockFlush(HttpStatusCode.Ok, 'OK', 'ok'); const events = await promise; expect(events.length).toBe(2); expect(fetchMock.request.signal?.aborted).toBeFalse(); }); it('should abort signal when unsubscribed before request completion', () => { const req = new HttpRequest('GET', '/test', {responseType: 'text'}); const sub = backend.handle(req).subscribe(); expect(fetchMock.request.signal?.aborted).toBeFalse(); sub.unsubscribe(); expect(fetchMock.request.signal?.aborted).toBeTrue(); }); }); }); export class MockFetchFactory extends FetchFactory { public readonly response = new MockFetchResponse(); public readonly request = new MockFetchRequest(); private resolve!: Function; private reject!: Function; private clearWarningTimeout?: VoidFunction; private promise = new Promise<Response>((resolve, reject) => { this.resolve = resolve; this.reject = reject; }); override fetch = (input: RequestInfo | URL, init?: RequestInit): Promise<Response> => { this.request.method = init?.method; this.request.url = input; this.request.body = init?.body; this.request.headers = init?.headers; this.request.credentials = init?.credentials; this.request.signal = init?.signal; if (init?.signal) { init?.signal.addEventListener('abort', () => { this.reject(); this.clearWarningTimeout?.(); }); } // Fetch uses a Macrotask to keep the NgZone unstable during the fetch // If the promise is not resolved/rejected the unit will succeed but the test suite will // fail with a timeout const timeoutId = setTimeout(() => { console.error('********* You forgot to resolve/reject the promise ********* '); this.reject(); }, 5000); this.clearWarningTimeout = () => clearTimeout(timeoutId); return this.promise; }; mockFlush( status: number, statusText: string, body?: string | Blob | Uint8Array, headers?: Record<string, string>, ): void { this.clearWarningTimeout?.(); if (typeof body === 'string') { this.response.setupBodyStream(body); } else { this.response.setBody(body); } const response = new Response(this.response.stream, { statusText, headers: {...this.response.headers, ...(headers ?? {})}, }); // Have to be set outside the constructor because it might throw // RangeError: init["status"] must be in the range of 200 to 599, inclusive Object.defineProperty(response, 'status', {value: status}); if (this.response.url) { // url is readonly Object.defineProperty(response, 'url', {value: this.response.url}); } this.resolve(response); } mockProgressEvent(loaded: number): void { this.response.progress.push(loaded); } mockErrorEvent(error: any) { this.reject(error); } mockAbortEvent() { this.reject(new DOMException('', 'AbortError')); } mockTimeoutEvent() { this.reject(new DOMException('', 'TimeoutError')); } resetFetchPromise() { this.promise = new Promise<Response>((resolve, reject) => { this.resolve = resolve; this.reject = reject; }); } } class MockFetchRequest { public url!: RequestInfo | URL; public method?: string; public body: any; public credentials?: RequestCredentials; public headers?: HeadersInit; public signal?: AbortSignal | null; } class InfiniteStreamFetchFactory extends FetchFactory { public cancelCount = 0; override fetch = async (_input: RequestInfo | URL, _init?: RequestInit): Promise<Response> => { const stream = new ReadableStream<Uint8Array>({ pull: (controller) => { controller.enqueue(new Uint8Array(1024)); }, cancel: () => { this.cancelCount++; }, }); return new Response(stream, { status: HttpStatusCode.Ok, statusText: 'OK', headers: {'Content-Type': 'text/plain'}, }); }; } class FiniteChunkFetchFactory extends FetchFactory { override fetch = async (_input: RequestInfo | URL, _init?: RequestInit): Promise<Response> => { const stream = new ReadableStream<Uint8Array>({ start: (controller) => { controller.enqueue(new TextEncoder().encode('ok')); controller.close(); }, }); return new Response(stream, { status: HttpStatusCode.Ok, statusText: 'OK', headers: {'Content-Type': 'text/plain'}, }); }; } class MockFetchResponse { public url?: string; public headers: Record<string, string> = {}; public progress: number[] = []; private sub$ = new Subject<any>(); public stream = new ReadableStream({ start: (controller) => { this.sub$.subscribe({ next: (val) => { controller.enqueue(val instanceof Uint8Array ? val : new TextEncoder().encode(val)); }, complete: () => { controller.close(); }, }); }, }); public setBody(body: any) { this.sub$.next(body); this.sub$.complete(); } public setupBodyStream(body?: string) { if (body && this.progress.length) { this.headers['content-length'] = `${body.length}`; let shift = 0; this.progress.forEach((loaded) => { this.sub$.next(body.substring(shift, loaded)); shift = loaded; }); this.sub$.next(body.substring(shift, body.length)); } else { this.sub$.next(body); } this.sub$.complete(); } }