| /** |
| * @license |
| * Copyright 2019 Google LLC |
| * SPDX-License-Identifier: Apache-2.0 |
| */ |
| import '../../../../test/common-test-setup'; |
| import { |
| SiteBasedCache, |
| FetchPromisesCache, |
| GrRestApiHelper, |
| } from './gr-rest-api-helper'; |
| import {assertFails, waitEventLoop} from '../../../../test/test-utils'; |
| import {FakeScheduler} from '../../../../services/scheduler/fake-scheduler'; |
| import {RetryScheduler} from '../../../../services/scheduler/retry-scheduler'; |
| import {ParsedJSON} from '../../../../types/common'; |
| import {HttpMethod} from '../../../../api/rest-api'; |
| import {SinonFakeTimers} from 'sinon'; |
| import {assert} from '@open-wc/testing'; |
| import {AuthService} from '../../../../services/gr-auth/gr-auth'; |
| import {GrAuthMock} from '../../../../services/gr-auth/gr-auth_mock'; |
| |
| function makeParsedJSON<T>(val: T): ParsedJSON { |
| return val as unknown as ParsedJSON; |
| } |
| |
| suite('gr-rest-api-helper tests', () => { |
| let clock: SinonFakeTimers; |
| let helper: GrRestApiHelper; |
| |
| let cache: SiteBasedCache; |
| let fetchPromisesCache: FetchPromisesCache; |
| let originalCanonicalPath: string | undefined; |
| let authFetchStub: sinon.SinonStub; |
| let readScheduler: FakeScheduler<Response>; |
| let writeScheduler: FakeScheduler<Response>; |
| let authService: AuthService; |
| |
| setup(() => { |
| clock = sinon.useFakeTimers(); |
| cache = new SiteBasedCache(); |
| fetchPromisesCache = new FetchPromisesCache(); |
| |
| originalCanonicalPath = window.CANONICAL_PATH; |
| window.CANONICAL_PATH = 'testhelper'; |
| |
| const testJSON = ')]}\'\n{"hello": "bonjour"}'; |
| authService = new GrAuthMock(); |
| authFetchStub = sinon.stub(authService, 'fetch').returns( |
| Promise.resolve({ |
| ...new Response(), |
| ok: true, |
| text() { |
| return Promise.resolve(testJSON); |
| }, |
| }) |
| ); |
| |
| readScheduler = new FakeScheduler<Response>(); |
| writeScheduler = new FakeScheduler<Response>(); |
| |
| helper = new GrRestApiHelper( |
| cache, |
| authService, |
| fetchPromisesCache, |
| readScheduler, |
| writeScheduler |
| ); |
| }); |
| |
| teardown(() => { |
| window.CANONICAL_PATH = originalCanonicalPath; |
| }); |
| |
| async function assertReadRequest() { |
| assert.equal(readScheduler.scheduled.length, 1); |
| await readScheduler.resolve(); |
| await waitEventLoop(); |
| } |
| |
| async function assertWriteRequest() { |
| assert.equal(writeScheduler.scheduled.length, 1); |
| await writeScheduler.resolve(); |
| await waitEventLoop(); |
| } |
| |
| suite('send()', () => { |
| setup(() => { |
| authFetchStub.returns( |
| Promise.resolve({ |
| ...new Response(), |
| ok: true, |
| text() { |
| return Promise.resolve('Yay'); |
| }, |
| }) |
| ); |
| }); |
| |
| test('GET are sent to readScheduler', async () => { |
| const promise = helper.send({ |
| method: HttpMethod.GET, |
| url: '/dummy/url', |
| parseResponse: false, |
| }); |
| assert.equal(writeScheduler.scheduled.length, 0); |
| await assertReadRequest(); |
| const res: Response = await promise; |
| assert.equal(await res.text(), 'Yay'); |
| }); |
| |
| test('PUT are sent to writeScheduler', async () => { |
| const promise = helper.send({ |
| method: HttpMethod.PUT, |
| url: '/dummy/url', |
| parseResponse: false, |
| }); |
| assert.equal(readScheduler.scheduled.length, 0); |
| await assertWriteRequest(); |
| const res: Response = await promise; |
| assert.equal(await res.text(), 'Yay'); |
| }); |
| }); |
| |
| suite('fetchJSON()', () => { |
| test('Sets header to accept application/json', async () => { |
| helper.fetchJSON({url: '/dummy/url'}); |
| assert.isFalse(authFetchStub.called); |
| await assertReadRequest(); |
| assert.isTrue(authFetchStub.called); |
| assert.equal( |
| authFetchStub.lastCall.args[1].headers.get('Accept'), |
| 'application/json' |
| ); |
| }); |
| |
| test('Use header option accept when provided', async () => { |
| const headers = new Headers(); |
| headers.append('Accept', '*/*'); |
| const fetchOptions = {headers}; |
| helper.fetchJSON({url: '/dummy/url', fetchOptions}); |
| assert.isFalse(authFetchStub.called); |
| await assertReadRequest(); |
| assert.isTrue(authFetchStub.called); |
| assert.equal(authFetchStub.lastCall.args[1].headers.get('Accept'), '*/*'); |
| }); |
| |
| test('JSON prefix is properly removed', async () => { |
| const promise = helper.fetchJSON({url: '/dummy/url'}); |
| await assertReadRequest(); |
| const obj = await promise; |
| assert.deepEqual(obj, makeParsedJSON({hello: 'bonjour'})); |
| }); |
| }); |
| |
| test('cached results', () => { |
| let n = 0; |
| sinon |
| .stub(helper, 'fetchJSON') |
| .callsFake(() => Promise.resolve(makeParsedJSON(++n))); |
| const promises = []; |
| promises.push(helper.fetchCacheURL({url: '/foo'})); |
| promises.push(helper.fetchCacheURL({url: '/foo'})); |
| promises.push(helper.fetchCacheURL({url: '/foo'})); |
| |
| return Promise.all(promises).then(results => { |
| assert.deepEqual(results, [ |
| makeParsedJSON(1), |
| makeParsedJSON(1), |
| makeParsedJSON(1), |
| ]); |
| return helper.fetchCacheURL({url: '/foo'}).then(foo => { |
| assert.equal(foo, makeParsedJSON(1)); |
| }); |
| }); |
| }); |
| |
| test('cache invalidation', async () => { |
| cache.set('/foo/bar', makeParsedJSON(1)); |
| cache.set('/bar', makeParsedJSON(2)); |
| fetchPromisesCache.set('/foo/bar', Promise.resolve(makeParsedJSON(3))); |
| fetchPromisesCache.set('/bar', Promise.resolve(makeParsedJSON(4))); |
| helper.invalidateFetchPromisesPrefix('/foo/'); |
| assert.isFalse(cache.has('/foo/bar')); |
| assert.isTrue(cache.has('/bar')); |
| assert.isUndefined(fetchPromisesCache.get('/foo/bar')); |
| assert.strictEqual(makeParsedJSON(4), await fetchPromisesCache.get('/bar')); |
| }); |
| |
| test('params are properly encoded', () => { |
| let url = helper.urlWithParams('/path/', { |
| sp: 'hola', |
| gr: 'guten tag', |
| noval: null, |
| }); |
| assert.equal( |
| url, |
| `${window.CANONICAL_PATH}/path/?sp=hola&gr=guten%20tag&noval` |
| ); |
| |
| url = helper.urlWithParams('/path/', { |
| sp: 'hola', |
| en: ['hey', 'hi'], |
| }); |
| assert.equal(url, `${window.CANONICAL_PATH}/path/?sp=hola&en=hey&en=hi`); |
| |
| // Order must be maintained with array params. |
| url = helper.urlWithParams('/path/', { |
| l: ['c', 'b', 'a'], |
| }); |
| assert.equal(url, `${window.CANONICAL_PATH}/path/?l=c&l=b&l=a`); |
| }); |
| |
| test('request callbacks can be canceled', async () => { |
| let cancelCalled = false; |
| authFetchStub.returns( |
| Promise.resolve({ |
| body: { |
| cancel() { |
| cancelCalled = true; |
| }, |
| }, |
| }) |
| ); |
| const cancelCondition = () => true; |
| const promise = helper.fetchJSON({url: '/dummy/url', cancelCondition}); |
| await assertReadRequest(); |
| const obj = await promise; |
| assert.isUndefined(obj); |
| assert.isTrue(cancelCalled); |
| }); |
| |
| suite('throwing in errFn', () => { |
| function throwInPromise(response?: Response | null, _?: Error) { |
| return response?.text().then(text => { |
| throw new Error(text); |
| }); |
| } |
| |
| function throwImmediately(_1?: Response | null, _2?: Error) { |
| throw new Error('Error Callback error'); |
| } |
| |
| setup(() => { |
| authFetchStub.returns( |
| Promise.resolve({ |
| ...new Response(), |
| status: 400, |
| ok: false, |
| text() { |
| return Promise.resolve('Nope'); |
| }, |
| }) |
| ); |
| }); |
| |
| test('errFn with Promise throw cause send to reject on error', async () => { |
| const promise = helper.send({ |
| method: HttpMethod.GET, |
| url: '/dummy/url', |
| parseResponse: false, |
| errFn: throwInPromise, |
| }); |
| await assertReadRequest(); |
| |
| const err = await assertFails(promise); |
| assert.equal((err as Error).message, 'Nope'); |
| }); |
| |
| test('errFn with Promise throw cause fetchJSON to reject on error', async () => { |
| const promise = helper.fetchJSON({ |
| url: '/dummy/url', |
| errFn: throwInPromise, |
| }); |
| await assertReadRequest(); |
| |
| const err = await assertFails(promise); |
| assert.equal((err as Error).message, 'Nope'); |
| }); |
| |
| test('errFn with immediate throw cause send to reject on error', async () => { |
| const promise = helper.send({ |
| method: HttpMethod.GET, |
| url: '/dummy/url', |
| parseResponse: false, |
| errFn: throwImmediately, |
| }); |
| await assertReadRequest(); |
| |
| const err = await assertFails(promise); |
| assert.equal((err as Error).message, 'Error Callback error'); |
| }); |
| |
| test('errFn with immediate Promise cause fetchJSON to reject on error', async () => { |
| const promise = helper.fetchJSON({ |
| url: '/dummy/url', |
| errFn: throwImmediately, |
| }); |
| await assertReadRequest(); |
| |
| const err = await assertFails(promise); |
| assert.equal((err as Error).message, 'Error Callback error'); |
| }); |
| }); |
| |
| suite('429 errors', () => { |
| setup(() => { |
| authFetchStub.returns( |
| Promise.resolve({ |
| ...new Response(), |
| status: 429, |
| ok: false, |
| }) |
| ); |
| }); |
| |
| test('still call errFn when not retried', async () => { |
| const errFn = sinon.stub(); |
| const promise = helper.send({ |
| method: HttpMethod.GET, |
| url: '/dummy/url', |
| parseResponse: false, |
| errFn, |
| }); |
| await assertReadRequest(); |
| |
| // But we expect the result from the network to return a 429 error when |
| // it's no longer being retried. |
| await promise; |
| assert.isTrue(errFn.called); |
| }); |
| |
| test('still pass through correctly when not retried', async () => { |
| const promise = helper.send({ |
| method: HttpMethod.GET, |
| url: '/dummy/url', |
| parseResponse: false, |
| }); |
| await assertReadRequest(); |
| |
| // But we expect the result from the network to return a 429 error when |
| // it's no longer being retried. |
| const res: Response = await promise; |
| assert.equal(res.status, 429); |
| }); |
| |
| test('are retried', async () => { |
| helper = new GrRestApiHelper( |
| cache, |
| authService, |
| fetchPromisesCache, |
| new RetryScheduler<Response>(readScheduler, 1, 50), |
| writeScheduler |
| ); |
| const promise = helper.send({ |
| method: HttpMethod.GET, |
| url: '/dummy/url', |
| parseResponse: false, |
| }); |
| await assertReadRequest(); |
| authFetchStub.returns( |
| Promise.resolve({ |
| ...new Response(), |
| ok: true, |
| text() { |
| return Promise.resolve('Yay'); |
| }, |
| }) |
| ); |
| // Flush the retry scheduler |
| clock.tick(50); |
| await waitEventLoop(); |
| // We expect a retry. |
| await assertReadRequest(); |
| const res: Response = await promise; |
| assert.equal(await res.text(), 'Yay'); |
| }); |
| }); |
| }); |