2020-07-02 20:46:54 -04:00
|
|
|
/**
|
|
|
|
* Licensed to the Apache Software Foundation (ASF) under one
|
|
|
|
* or more contributor license agreements. See the NOTICE file
|
|
|
|
* distributed with this work for additional information
|
|
|
|
* regarding copyright ownership. The ASF licenses this file
|
|
|
|
* to you under the Apache License, Version 2.0 (the
|
|
|
|
* "License"); you may not use this file except in compliance
|
|
|
|
* with the License. You may obtain a copy of the License at
|
|
|
|
*
|
|
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
*
|
|
|
|
* Unless required by applicable law or agreed to in writing,
|
|
|
|
* software distributed under the License is distributed on an
|
|
|
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
|
|
* KIND, either express or implied. See the License for the
|
|
|
|
* specific language governing permissions and limitations
|
|
|
|
* under the License.
|
|
|
|
*/
|
2018-09-07 21:36:47 -04:00
|
|
|
import fetchMock from 'fetch-mock';
|
2022-01-07 20:16:24 -05:00
|
|
|
import { CallApi, JsonObject } from '@superset-ui/core';
|
|
|
|
import * as constants from '../../../src/connection/constants';
|
|
|
|
import callApi from '../../../src/connection/callApi/callApi';
|
2018-09-07 21:36:47 -04:00
|
|
|
|
|
|
|
import { LOGIN_GLOB } from '../fixtures/constants';
|
|
|
|
|
2022-08-10 13:33:47 -04:00
|
|
|
// missing the toString function causing method to error out when casting to String
|
|
|
|
class BadObject {}
|
|
|
|
const corruptObject = new BadObject();
|
|
|
|
/* @ts-expect-error */
|
|
|
|
BadObject.prototype.toString = undefined;
|
|
|
|
|
2018-09-07 21:36:47 -04:00
|
|
|
describe('callApi()', () => {
|
|
|
|
beforeAll(() => {
|
2021-03-01 16:46:25 -05:00
|
|
|
fetchMock.get(LOGIN_GLOB, { result: '1234' });
|
2018-09-07 21:36:47 -04:00
|
|
|
});
|
|
|
|
|
|
|
|
afterAll(fetchMock.restore);
|
|
|
|
|
|
|
|
const mockGetUrl = '/mock/get/url';
|
|
|
|
const mockPostUrl = '/mock/post/url';
|
2019-03-04 16:25:33 -05:00
|
|
|
const mockPutUrl = '/mock/put/url';
|
|
|
|
const mockPatchUrl = '/mock/patch/url';
|
2019-04-02 13:45:07 -04:00
|
|
|
const mockCacheUrl = '/mock/cache/url';
|
2019-09-04 00:04:10 -04:00
|
|
|
const mockNotFound = '/mock/notfound';
|
2020-04-20 21:37:13 -04:00
|
|
|
const mockErrorUrl = '/mock/error/url';
|
|
|
|
const mock503 = '/mock/503';
|
2018-09-07 21:36:47 -04:00
|
|
|
|
|
|
|
const mockGetPayload = { get: 'payload' };
|
|
|
|
const mockPostPayload = { post: 'payload' };
|
2019-03-04 16:25:33 -05:00
|
|
|
const mockPutPayload = { post: 'payload' };
|
|
|
|
const mockPatchPayload = { post: 'payload' };
|
2019-04-02 13:45:07 -04:00
|
|
|
const mockCachePayload = {
|
|
|
|
status: 200,
|
|
|
|
body: 'BODY',
|
|
|
|
headers: { Etag: 'etag' },
|
|
|
|
};
|
2020-04-20 21:37:13 -04:00
|
|
|
const mockErrorPayload = { status: 500, statusText: 'Internal error' };
|
2018-09-07 21:36:47 -04:00
|
|
|
|
|
|
|
fetchMock.get(mockGetUrl, mockGetPayload);
|
|
|
|
fetchMock.post(mockPostUrl, mockPostPayload);
|
2019-03-04 16:25:33 -05:00
|
|
|
fetchMock.put(mockPutUrl, mockPutPayload);
|
|
|
|
fetchMock.patch(mockPatchUrl, mockPatchPayload);
|
2019-04-02 13:45:07 -04:00
|
|
|
fetchMock.get(mockCacheUrl, mockCachePayload);
|
2019-09-04 00:04:10 -04:00
|
|
|
fetchMock.get(mockNotFound, { status: 404 });
|
2020-04-20 21:37:13 -04:00
|
|
|
fetchMock.get(mock503, { status: 503 });
|
|
|
|
fetchMock.get(mockErrorUrl, () => Promise.reject(mockErrorPayload));
|
2018-09-07 21:36:47 -04:00
|
|
|
|
|
|
|
afterEach(fetchMock.reset);
|
|
|
|
|
|
|
|
describe('request config', () => {
|
2020-07-02 20:46:54 -04:00
|
|
|
it('calls the right url with the specified method', async () => {
|
2019-03-04 16:25:33 -05:00
|
|
|
expect.assertions(4);
|
2020-07-02 20:46:54 -04:00
|
|
|
await Promise.all([
|
2018-09-07 21:36:47 -04:00
|
|
|
callApi({ url: mockGetUrl, method: 'GET' }),
|
|
|
|
callApi({ url: mockPostUrl, method: 'POST' }),
|
2019-03-04 16:25:33 -05:00
|
|
|
callApi({ url: mockPutUrl, method: 'PUT' }),
|
|
|
|
callApi({ url: mockPatchUrl, method: 'PATCH' }),
|
2020-07-02 20:46:54 -04:00
|
|
|
]);
|
|
|
|
expect(fetchMock.calls(mockGetUrl)).toHaveLength(1);
|
|
|
|
expect(fetchMock.calls(mockPostUrl)).toHaveLength(1);
|
|
|
|
expect(fetchMock.calls(mockPutUrl)).toHaveLength(1);
|
|
|
|
expect(fetchMock.calls(mockPatchUrl)).toHaveLength(1);
|
2018-09-07 21:36:47 -04:00
|
|
|
});
|
|
|
|
|
2020-07-02 20:46:54 -04:00
|
|
|
it('passes along mode, cache, credentials, headers, body, signal, and redirect parameters in the request', async () => {
|
2018-09-07 21:36:47 -04:00
|
|
|
expect.assertions(8);
|
2018-11-29 12:48:18 -05:00
|
|
|
const mockRequest: CallApi = {
|
2018-09-07 21:36:47 -04:00
|
|
|
url: mockGetUrl,
|
2018-11-29 12:48:18 -05:00
|
|
|
mode: 'cors',
|
|
|
|
cache: 'default',
|
|
|
|
credentials: 'include',
|
2018-09-07 21:36:47 -04:00
|
|
|
headers: {
|
|
|
|
custom: 'header',
|
|
|
|
},
|
2018-11-29 12:48:18 -05:00
|
|
|
redirect: 'follow',
|
|
|
|
signal: undefined,
|
2018-09-07 21:36:47 -04:00
|
|
|
body: 'BODY',
|
|
|
|
};
|
|
|
|
|
2020-07-02 20:46:54 -04:00
|
|
|
await callApi(mockRequest);
|
|
|
|
const calls = fetchMock.calls(mockGetUrl);
|
2022-03-23 06:57:35 -04:00
|
|
|
const fetchParams = calls[0][1] as RequestInit;
|
2020-07-02 20:46:54 -04:00
|
|
|
expect(calls).toHaveLength(1);
|
|
|
|
expect(fetchParams.mode).toBe(mockRequest.mode);
|
|
|
|
expect(fetchParams.cache).toBe(mockRequest.cache);
|
|
|
|
expect(fetchParams.credentials).toBe(mockRequest.credentials);
|
|
|
|
expect(fetchParams.headers).toEqual(
|
2021-11-09 07:42:28 -05:00
|
|
|
expect.objectContaining(
|
|
|
|
mockRequest.headers,
|
|
|
|
) as typeof fetchParams.headers,
|
2020-07-02 20:46:54 -04:00
|
|
|
);
|
|
|
|
expect(fetchParams.redirect).toBe(mockRequest.redirect);
|
|
|
|
expect(fetchParams.signal).toBe(mockRequest.signal);
|
|
|
|
expect(fetchParams.body).toBe(mockRequest.body);
|
2018-09-07 21:36:47 -04:00
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('POST requests', () => {
|
2020-07-02 20:46:54 -04:00
|
|
|
it('encodes key,value pairs from postPayload', async () => {
|
2018-09-07 21:36:47 -04:00
|
|
|
expect.assertions(3);
|
2020-05-07 15:53:36 -04:00
|
|
|
const postPayload = { key: 'value', anotherKey: 1237 };
|
2018-09-07 21:36:47 -04:00
|
|
|
|
2020-07-02 20:46:54 -04:00
|
|
|
await callApi({ url: mockPostUrl, method: 'POST', postPayload });
|
|
|
|
const calls = fetchMock.calls(mockPostUrl);
|
|
|
|
expect(calls).toHaveLength(1);
|
2018-09-07 21:36:47 -04:00
|
|
|
|
2022-03-23 06:57:35 -04:00
|
|
|
const fetchParams = calls[0][1] as RequestInit;
|
2020-07-02 20:46:54 -04:00
|
|
|
const body = fetchParams.body as FormData;
|
2018-09-07 21:36:47 -04:00
|
|
|
|
2020-07-02 20:46:54 -04:00
|
|
|
Object.entries(postPayload).forEach(([key, value]) => {
|
|
|
|
expect(body.get(key)).toBe(JSON.stringify(value));
|
2018-09-18 00:53:14 -04:00
|
|
|
});
|
2018-09-07 21:36:47 -04:00
|
|
|
});
|
|
|
|
|
|
|
|
// the reason for this is to omit strings like 'undefined' from making their way to the backend
|
2020-07-02 20:46:54 -04:00
|
|
|
it('omits key,value pairs from postPayload that have undefined values (POST)', async () => {
|
2018-09-07 21:36:47 -04:00
|
|
|
expect.assertions(3);
|
|
|
|
const postPayload = { key: 'value', noValue: undefined };
|
|
|
|
|
2020-07-02 20:46:54 -04:00
|
|
|
await callApi({ url: mockPostUrl, method: 'POST', postPayload });
|
|
|
|
const calls = fetchMock.calls(mockPostUrl);
|
|
|
|
expect(calls).toHaveLength(1);
|
2018-09-07 21:36:47 -04:00
|
|
|
|
2022-03-23 06:57:35 -04:00
|
|
|
const fetchParams = calls[0][1] as RequestInit;
|
2020-07-02 20:46:54 -04:00
|
|
|
const body = fetchParams.body as FormData;
|
|
|
|
expect(body.get('key')).toBe(JSON.stringify(postPayload.key));
|
|
|
|
expect(body.get('noValue')).toBeNull();
|
2018-09-07 21:36:47 -04:00
|
|
|
});
|
|
|
|
|
2020-07-02 20:46:54 -04:00
|
|
|
it('respects the stringify flag in POST requests', async () => {
|
2018-09-07 21:36:47 -04:00
|
|
|
const postPayload = {
|
|
|
|
string: 'value',
|
|
|
|
number: 1237,
|
|
|
|
array: [1, 2, 3],
|
|
|
|
object: { a: 'a', 1: 1 },
|
|
|
|
null: null,
|
|
|
|
emptyString: '',
|
2020-05-07 15:53:36 -04:00
|
|
|
};
|
2018-09-07 21:36:47 -04:00
|
|
|
|
2020-07-02 20:26:03 -04:00
|
|
|
expect.assertions(1 + 3 * Object.keys(postPayload).length);
|
2018-09-07 21:36:47 -04:00
|
|
|
|
2020-07-02 20:46:54 -04:00
|
|
|
await Promise.all([
|
2018-09-07 21:36:47 -04:00
|
|
|
callApi({ url: mockPostUrl, method: 'POST', postPayload }),
|
2021-11-09 07:42:28 -05:00
|
|
|
callApi({
|
|
|
|
url: mockPostUrl,
|
|
|
|
method: 'POST',
|
|
|
|
postPayload,
|
|
|
|
stringify: false,
|
|
|
|
}),
|
2020-07-02 20:26:03 -04:00
|
|
|
callApi({ url: mockPostUrl, method: 'POST', jsonPayload: postPayload }),
|
2020-07-02 20:46:54 -04:00
|
|
|
]);
|
|
|
|
const calls = fetchMock.calls(mockPostUrl);
|
|
|
|
expect(calls).toHaveLength(3);
|
|
|
|
|
2022-03-23 06:57:35 -04:00
|
|
|
const stringified = (calls[0][1] as RequestInit).body as FormData;
|
|
|
|
const unstringified = (calls[1][1] as RequestInit).body as FormData;
|
2021-11-09 07:42:28 -05:00
|
|
|
const jsonRequestBody = JSON.parse(
|
2022-03-23 06:57:35 -04:00
|
|
|
(calls[2][1] as RequestInit).body as string,
|
2021-11-09 07:42:28 -05:00
|
|
|
) as JsonObject;
|
2020-07-02 20:46:54 -04:00
|
|
|
|
|
|
|
Object.entries(postPayload).forEach(([key, value]) => {
|
|
|
|
expect(stringified.get(key)).toBe(JSON.stringify(value));
|
|
|
|
expect(unstringified.get(key)).toBe(String(value));
|
|
|
|
expect(jsonRequestBody[key]).toEqual(value);
|
2019-03-04 16:25:33 -05:00
|
|
|
});
|
|
|
|
});
|
2022-08-10 13:33:47 -04:00
|
|
|
|
|
|
|
it('removes corrupt value when building formData with stringify = false', async () => {
|
|
|
|
/*
|
|
|
|
There has been a case when 'stringify' is false an object value on one of the
|
|
|
|
attributes was missing a toString function making the cast to String() fail
|
|
|
|
and causing entire method call to fail. The new logic skips corrupt values that fail cast to String()
|
|
|
|
and allows all valid attributes to be added as key / value pairs to the formData
|
|
|
|
instance. This test case replicates a corrupt object missing the .toString method
|
|
|
|
representing a real bug report.
|
|
|
|
*/
|
|
|
|
const postPayload = {
|
|
|
|
string: 'value',
|
|
|
|
number: 1237,
|
|
|
|
array: [1, 2, 3],
|
|
|
|
object: { a: 'a', 1: 1 },
|
|
|
|
null: null,
|
|
|
|
emptyString: '',
|
|
|
|
// corruptObject has no toString method and will fail cast to String()
|
|
|
|
corrupt: [corruptObject],
|
|
|
|
};
|
|
|
|
jest.spyOn(console, 'error').mockImplementation();
|
|
|
|
|
|
|
|
await callApi({
|
|
|
|
url: mockPostUrl,
|
|
|
|
method: 'POST',
|
|
|
|
postPayload,
|
|
|
|
stringify: false,
|
|
|
|
});
|
|
|
|
|
|
|
|
const calls = fetchMock.calls(mockPostUrl);
|
|
|
|
expect(calls).toHaveLength(1);
|
|
|
|
const unstringified = (calls[0][1] as RequestInit).body as FormData;
|
|
|
|
const hasCorruptKey = unstringified.has('corrupt');
|
|
|
|
expect(hasCorruptKey).toBeFalsy();
|
|
|
|
// When a corrupt attribute is encountred, a console.error call is made with info about the corrupt attribute
|
|
|
|
// eslint-disable-next-line no-console
|
|
|
|
expect(console.error).toHaveBeenCalledTimes(1);
|
|
|
|
});
|
2019-03-04 16:25:33 -05:00
|
|
|
});
|
|
|
|
|
|
|
|
describe('PUT requests', () => {
|
2020-07-02 20:46:54 -04:00
|
|
|
it('encodes key,value pairs from postPayload', async () => {
|
2019-03-04 16:25:33 -05:00
|
|
|
expect.assertions(3);
|
2020-05-07 15:53:36 -04:00
|
|
|
const postPayload = { key: 'value', anotherKey: 1237 };
|
2019-03-04 16:25:33 -05:00
|
|
|
|
2020-07-02 20:46:54 -04:00
|
|
|
await callApi({ url: mockPutUrl, method: 'PUT', postPayload });
|
|
|
|
const calls = fetchMock.calls(mockPutUrl);
|
|
|
|
expect(calls).toHaveLength(1);
|
2019-03-04 16:25:33 -05:00
|
|
|
|
2022-03-23 06:57:35 -04:00
|
|
|
const fetchParams = calls[0][1] as RequestInit;
|
2020-07-02 20:46:54 -04:00
|
|
|
const body = fetchParams.body as FormData;
|
2019-03-04 16:25:33 -05:00
|
|
|
|
2020-07-02 20:46:54 -04:00
|
|
|
Object.entries(postPayload).forEach(([key, value]) => {
|
|
|
|
expect(body.get(key)).toBe(JSON.stringify(value));
|
2019-03-04 16:25:33 -05:00
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
// the reason for this is to omit strings like 'undefined' from making their way to the backend
|
2020-07-02 20:46:54 -04:00
|
|
|
it('omits key,value pairs from postPayload that have undefined values (PUT)', async () => {
|
2019-03-04 16:25:33 -05:00
|
|
|
expect.assertions(3);
|
|
|
|
const postPayload = { key: 'value', noValue: undefined };
|
|
|
|
|
2020-07-02 20:46:54 -04:00
|
|
|
await callApi({ url: mockPutUrl, method: 'PUT', postPayload });
|
|
|
|
const calls = fetchMock.calls(mockPutUrl);
|
|
|
|
expect(calls).toHaveLength(1);
|
2019-03-04 16:25:33 -05:00
|
|
|
|
2022-03-23 06:57:35 -04:00
|
|
|
const fetchParams = calls[0][1] as RequestInit;
|
2020-07-02 20:46:54 -04:00
|
|
|
const body = fetchParams.body as FormData;
|
|
|
|
expect(body.get('key')).toBe(JSON.stringify(postPayload.key));
|
|
|
|
expect(body.get('noValue')).toBeNull();
|
2019-03-04 16:25:33 -05:00
|
|
|
});
|
|
|
|
|
2020-07-02 20:46:54 -04:00
|
|
|
it('respects the stringify flag in PUT requests', async () => {
|
2019-03-04 16:25:33 -05:00
|
|
|
const postPayload = {
|
|
|
|
string: 'value',
|
|
|
|
number: 1237,
|
|
|
|
array: [1, 2, 3],
|
|
|
|
object: { a: 'a', 1: 1 },
|
|
|
|
null: null,
|
|
|
|
emptyString: '',
|
2020-05-07 15:53:36 -04:00
|
|
|
};
|
2019-03-04 16:25:33 -05:00
|
|
|
|
|
|
|
expect.assertions(1 + 2 * Object.keys(postPayload).length);
|
|
|
|
|
2020-07-02 20:46:54 -04:00
|
|
|
await Promise.all([
|
2019-03-04 16:25:33 -05:00
|
|
|
callApi({ url: mockPutUrl, method: 'PUT', postPayload }),
|
2021-11-09 07:42:28 -05:00
|
|
|
callApi({
|
|
|
|
url: mockPutUrl,
|
|
|
|
method: 'PUT',
|
|
|
|
postPayload,
|
|
|
|
stringify: false,
|
|
|
|
}),
|
2020-07-02 20:46:54 -04:00
|
|
|
]);
|
|
|
|
const calls = fetchMock.calls(mockPutUrl);
|
|
|
|
expect(calls).toHaveLength(2);
|
2019-03-04 16:25:33 -05:00
|
|
|
|
2022-03-23 06:57:35 -04:00
|
|
|
const stringified = (calls[0][1] as RequestInit).body as FormData;
|
|
|
|
const unstringified = (calls[1][1] as RequestInit).body as FormData;
|
2019-03-04 16:25:33 -05:00
|
|
|
|
2020-07-02 20:46:54 -04:00
|
|
|
Object.entries(postPayload).forEach(([key, value]) => {
|
|
|
|
expect(stringified.get(key)).toBe(JSON.stringify(value));
|
|
|
|
expect(unstringified.get(key)).toBe(String(value));
|
2019-03-04 16:25:33 -05:00
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('PATCH requests', () => {
|
2020-07-02 20:46:54 -04:00
|
|
|
it('encodes key,value pairs from postPayload', async () => {
|
2019-03-04 16:25:33 -05:00
|
|
|
expect.assertions(3);
|
2020-05-07 15:53:36 -04:00
|
|
|
const postPayload = { key: 'value', anotherKey: 1237 };
|
2019-03-04 16:25:33 -05:00
|
|
|
|
2020-07-02 20:46:54 -04:00
|
|
|
await callApi({ url: mockPatchUrl, method: 'PATCH', postPayload });
|
|
|
|
const calls = fetchMock.calls(mockPatchUrl);
|
|
|
|
expect(calls).toHaveLength(1);
|
2019-03-04 16:25:33 -05:00
|
|
|
|
2022-03-23 06:57:35 -04:00
|
|
|
const fetchParams = calls[0][1] as RequestInit;
|
2020-07-02 20:46:54 -04:00
|
|
|
const body = fetchParams.body as FormData;
|
2019-03-04 16:25:33 -05:00
|
|
|
|
2020-07-02 20:46:54 -04:00
|
|
|
Object.entries(postPayload).forEach(([key, value]) => {
|
|
|
|
expect(body.get(key)).toBe(JSON.stringify(value));
|
2019-03-04 16:25:33 -05:00
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
// the reason for this is to omit strings like 'undefined' from making their way to the backend
|
2020-07-02 20:46:54 -04:00
|
|
|
it('omits key,value pairs from postPayload that have undefined values (PATCH)', async () => {
|
2019-03-04 16:25:33 -05:00
|
|
|
expect.assertions(3);
|
|
|
|
const postPayload = { key: 'value', noValue: undefined };
|
|
|
|
|
2020-07-02 20:46:54 -04:00
|
|
|
await callApi({ url: mockPatchUrl, method: 'PATCH', postPayload });
|
|
|
|
const calls = fetchMock.calls(mockPatchUrl);
|
|
|
|
expect(calls).toHaveLength(1);
|
2019-03-04 16:25:33 -05:00
|
|
|
|
2022-03-23 06:57:35 -04:00
|
|
|
const fetchParams = calls[0][1] as RequestInit;
|
2020-07-02 20:46:54 -04:00
|
|
|
const body = fetchParams.body as FormData;
|
|
|
|
expect(body.get('key')).toBe(JSON.stringify(postPayload.key));
|
|
|
|
expect(body.get('noValue')).toBeNull();
|
2019-03-04 16:25:33 -05:00
|
|
|
});
|
|
|
|
|
2020-07-02 20:46:54 -04:00
|
|
|
it('respects the stringify flag in PATCH requests', async () => {
|
2019-03-04 16:25:33 -05:00
|
|
|
const postPayload = {
|
|
|
|
string: 'value',
|
|
|
|
number: 1237,
|
|
|
|
array: [1, 2, 3],
|
|
|
|
object: { a: 'a', 1: 1 },
|
|
|
|
null: null,
|
|
|
|
emptyString: '',
|
2020-05-07 15:53:36 -04:00
|
|
|
};
|
2019-03-04 16:25:33 -05:00
|
|
|
|
|
|
|
expect.assertions(1 + 2 * Object.keys(postPayload).length);
|
|
|
|
|
2020-07-02 20:46:54 -04:00
|
|
|
await Promise.all([
|
2019-03-04 16:25:33 -05:00
|
|
|
callApi({ url: mockPatchUrl, method: 'PATCH', postPayload }),
|
2021-11-09 07:42:28 -05:00
|
|
|
callApi({
|
|
|
|
url: mockPatchUrl,
|
|
|
|
method: 'PATCH',
|
|
|
|
postPayload,
|
|
|
|
stringify: false,
|
|
|
|
}),
|
2020-07-02 20:46:54 -04:00
|
|
|
]);
|
|
|
|
const calls = fetchMock.calls(mockPatchUrl);
|
|
|
|
expect(calls).toHaveLength(2);
|
2018-09-18 00:53:14 -04:00
|
|
|
|
2022-03-23 06:57:35 -04:00
|
|
|
const stringified = (calls[0][1] as RequestInit).body as FormData;
|
|
|
|
const unstringified = (calls[1][1] as RequestInit).body as FormData;
|
2018-09-18 00:53:14 -04:00
|
|
|
|
2020-07-02 20:46:54 -04:00
|
|
|
Object.entries(postPayload).forEach(([key, value]) => {
|
|
|
|
expect(stringified.get(key)).toBe(JSON.stringify(value));
|
|
|
|
expect(unstringified.get(key)).toBe(String(value));
|
2018-09-18 00:53:14 -04:00
|
|
|
});
|
2018-09-07 21:36:47 -04:00
|
|
|
});
|
|
|
|
});
|
2018-09-18 00:53:14 -04:00
|
|
|
|
2019-07-23 19:03:40 -04:00
|
|
|
describe('caching', () => {
|
2020-12-10 16:58:10 -05:00
|
|
|
const origLocation = window.location;
|
2019-07-23 19:03:40 -04:00
|
|
|
|
|
|
|
beforeAll(() => {
|
2020-12-10 16:58:10 -05:00
|
|
|
Object.defineProperty(window, 'location', { value: {} });
|
2019-07-23 19:03:40 -04:00
|
|
|
});
|
|
|
|
|
|
|
|
afterAll(() => {
|
2020-12-10 16:58:10 -05:00
|
|
|
Object.defineProperty(window, 'location', { value: origLocation });
|
2019-07-23 19:03:40 -04:00
|
|
|
});
|
|
|
|
|
2020-12-10 16:58:10 -05:00
|
|
|
beforeEach(async () => {
|
|
|
|
window.location.protocol = 'https:';
|
|
|
|
await caches.delete(constants.CACHE_KEY);
|
2019-07-23 19:03:40 -04:00
|
|
|
});
|
|
|
|
|
2020-07-02 20:46:54 -04:00
|
|
|
it('caches requests with ETags', async () => {
|
|
|
|
expect.assertions(2);
|
|
|
|
await callApi({ url: mockCacheUrl, method: 'GET' });
|
|
|
|
const calls = fetchMock.calls(mockCacheUrl);
|
|
|
|
expect(calls).toHaveLength(1);
|
|
|
|
const supersetCache = await caches.open(constants.CACHE_KEY);
|
|
|
|
const cachedResponse = await supersetCache.match(mockCacheUrl);
|
|
|
|
expect(cachedResponse).toBeDefined();
|
|
|
|
});
|
2019-07-23 19:03:40 -04:00
|
|
|
|
2020-07-02 20:46:54 -04:00
|
|
|
it('will not use cache when running off an insecure connection', async () => {
|
|
|
|
expect.assertions(2);
|
2020-12-10 16:58:10 -05:00
|
|
|
window.location.protocol = 'http:';
|
2019-07-23 19:03:40 -04:00
|
|
|
|
2020-07-02 20:46:54 -04:00
|
|
|
await callApi({ url: mockCacheUrl, method: 'GET' });
|
|
|
|
const calls = fetchMock.calls(mockCacheUrl);
|
|
|
|
expect(calls).toHaveLength(1);
|
2019-07-23 19:03:40 -04:00
|
|
|
|
2020-07-02 20:46:54 -04:00
|
|
|
const supersetCache = await caches.open(constants.CACHE_KEY);
|
|
|
|
const cachedResponse = await supersetCache.match(mockCacheUrl);
|
|
|
|
expect(cachedResponse).toBeUndefined();
|
2019-07-23 19:03:40 -04:00
|
|
|
});
|
|
|
|
|
|
|
|
it('works when the Cache API is disabled', async () => {
|
2020-07-02 20:46:54 -04:00
|
|
|
expect.assertions(5);
|
2020-01-28 18:15:31 -05:00
|
|
|
// eslint-disable-next-line no-import-assign
|
2019-07-23 19:03:40 -04:00
|
|
|
Object.defineProperty(constants, 'CACHE_AVAILABLE', { value: false });
|
|
|
|
|
|
|
|
const firstResponse = await callApi({ url: mockCacheUrl, method: 'GET' });
|
2019-04-02 13:45:07 -04:00
|
|
|
const calls = fetchMock.calls(mockCacheUrl);
|
|
|
|
expect(calls).toHaveLength(1);
|
2019-07-23 19:03:40 -04:00
|
|
|
const firstBody = await firstResponse.text();
|
|
|
|
expect(firstBody).toEqual('BODY');
|
|
|
|
|
2021-11-09 07:42:28 -05:00
|
|
|
const secondResponse = await callApi({
|
|
|
|
url: mockCacheUrl,
|
|
|
|
method: 'GET',
|
|
|
|
});
|
2022-03-23 06:57:35 -04:00
|
|
|
const fetchParams = calls[1][1] as RequestInit;
|
2019-07-23 19:03:40 -04:00
|
|
|
expect(calls).toHaveLength(2);
|
|
|
|
// second call should not have If-None-Match header
|
|
|
|
expect(fetchParams.headers).toBeUndefined();
|
|
|
|
const secondBody = await secondResponse.text();
|
|
|
|
expect(secondBody).toEqual('BODY');
|
|
|
|
|
2020-01-28 18:15:31 -05:00
|
|
|
// eslint-disable-next-line no-import-assign
|
2019-07-23 19:03:40 -04:00
|
|
|
Object.defineProperty(constants, 'CACHE_AVAILABLE', { value: true });
|
|
|
|
});
|
2019-04-02 13:45:07 -04:00
|
|
|
|
2020-07-02 20:46:54 -04:00
|
|
|
it('sends known ETags in the If-None-Match header', async () => {
|
|
|
|
expect.assertions(3);
|
2019-07-23 19:03:40 -04:00
|
|
|
// first call sets the cache
|
2020-07-02 20:46:54 -04:00
|
|
|
await callApi({ url: mockCacheUrl, method: 'GET' });
|
|
|
|
const calls = fetchMock.calls(mockCacheUrl);
|
|
|
|
expect(calls).toHaveLength(1);
|
2019-04-02 13:45:07 -04:00
|
|
|
|
2020-07-02 20:46:54 -04:00
|
|
|
// second call sends the Etag in the If-None-Match header
|
|
|
|
await callApi({ url: mockCacheUrl, method: 'GET' });
|
2022-03-23 06:57:35 -04:00
|
|
|
const fetchParams = calls[1][1] as RequestInit;
|
2020-07-02 20:46:54 -04:00
|
|
|
const headers = { 'If-None-Match': 'etag' };
|
|
|
|
expect(calls).toHaveLength(2);
|
|
|
|
expect(fetchParams.headers).toEqual(
|
|
|
|
expect.objectContaining(headers) as typeof fetchParams.headers,
|
|
|
|
);
|
|
|
|
});
|
2019-04-02 13:45:07 -04:00
|
|
|
|
2019-07-23 19:03:40 -04:00
|
|
|
it('reuses cached responses on 304 status', async () => {
|
2020-07-02 20:46:54 -04:00
|
|
|
expect.assertions(3);
|
2019-07-23 19:03:40 -04:00
|
|
|
// first call sets the cache
|
|
|
|
await callApi({ url: mockCacheUrl, method: 'GET' });
|
2019-04-02 13:45:07 -04:00
|
|
|
const calls = fetchMock.calls(mockCacheUrl);
|
|
|
|
expect(calls).toHaveLength(1);
|
2019-07-23 19:03:40 -04:00
|
|
|
// second call reuses the cached payload on a 304
|
|
|
|
const mockCachedPayload = { status: 304 };
|
|
|
|
fetchMock.get(mockCacheUrl, mockCachedPayload, { overwriteRoutes: true });
|
|
|
|
|
2021-11-09 07:42:28 -05:00
|
|
|
const secondResponse = await callApi({
|
|
|
|
url: mockCacheUrl,
|
|
|
|
method: 'GET',
|
|
|
|
});
|
2019-07-23 19:03:40 -04:00
|
|
|
expect(calls).toHaveLength(2);
|
|
|
|
const secondBody = await secondResponse.text();
|
|
|
|
expect(secondBody).toEqual('BODY');
|
|
|
|
});
|
2019-04-02 13:45:07 -04:00
|
|
|
|
2020-07-02 20:46:54 -04:00
|
|
|
it('throws error when cache fails on 304', async () => {
|
|
|
|
expect.assertions(2);
|
|
|
|
|
2019-07-23 19:03:40 -04:00
|
|
|
// this should never happen, since a 304 is only returned if we have
|
|
|
|
// the cached response and sent the If-None-Match header
|
|
|
|
const mockUncachedUrl = '/mock/uncached/url';
|
|
|
|
const mockCachedPayload = { status: 304 };
|
2021-01-13 01:09:58 -05:00
|
|
|
let error;
|
2019-07-23 19:03:40 -04:00
|
|
|
fetchMock.get(mockUncachedUrl, mockCachedPayload);
|
2019-04-02 13:45:07 -04:00
|
|
|
|
2020-07-02 20:46:54 -04:00
|
|
|
try {
|
|
|
|
await callApi({ url: mockUncachedUrl, method: 'GET' });
|
2021-01-13 01:09:58 -05:00
|
|
|
} catch (err) {
|
|
|
|
error = err;
|
|
|
|
} finally {
|
2020-07-02 20:46:54 -04:00
|
|
|
const calls = fetchMock.calls(mockUncachedUrl);
|
|
|
|
expect(calls).toHaveLength(1);
|
|
|
|
expect((error as { message: string }).message).toEqual(
|
|
|
|
'Received 304 but no content is cached!',
|
|
|
|
);
|
|
|
|
}
|
2019-04-02 13:45:07 -04:00
|
|
|
});
|
2019-09-04 00:04:10 -04:00
|
|
|
|
|
|
|
it('returns original response if no Etag', async () => {
|
2020-07-02 20:46:54 -04:00
|
|
|
expect.assertions(3);
|
2019-09-04 00:04:10 -04:00
|
|
|
const url = mockGetUrl;
|
|
|
|
const response = await callApi({ url, method: 'GET' });
|
|
|
|
const calls = fetchMock.calls(url);
|
|
|
|
expect(calls).toHaveLength(1);
|
|
|
|
expect(response.status).toEqual(200);
|
|
|
|
const body = await response.json();
|
2020-05-07 15:53:36 -04:00
|
|
|
expect(body as typeof mockGetPayload).toEqual(mockGetPayload);
|
2019-09-04 00:04:10 -04:00
|
|
|
});
|
|
|
|
|
|
|
|
it('returns original response if status not 304 or 200', async () => {
|
2020-07-02 20:46:54 -04:00
|
|
|
expect.assertions(2);
|
2019-09-04 00:04:10 -04:00
|
|
|
const url = mockNotFound;
|
|
|
|
const response = await callApi({ url, method: 'GET' });
|
|
|
|
const calls = fetchMock.calls(url);
|
|
|
|
expect(calls).toHaveLength(1);
|
|
|
|
expect(response.status).toEqual(404);
|
|
|
|
});
|
2019-04-02 13:45:07 -04:00
|
|
|
});
|
|
|
|
|
2020-07-02 20:46:54 -04:00
|
|
|
it('rejects after retrying thrice if the request throws', async () => {
|
2020-04-20 21:37:13 -04:00
|
|
|
expect.assertions(3);
|
2021-01-13 01:09:58 -05:00
|
|
|
let error;
|
2020-07-02 20:46:54 -04:00
|
|
|
try {
|
|
|
|
await callApi({
|
2022-01-07 20:16:24 -05:00
|
|
|
fetchRetryOptions: constants.DEFAULT_FETCH_RETRY_OPTIONS,
|
2020-07-02 20:46:54 -04:00
|
|
|
url: mockErrorUrl,
|
|
|
|
method: 'GET',
|
2020-04-20 21:37:13 -04:00
|
|
|
});
|
2021-01-13 01:09:58 -05:00
|
|
|
} catch (err) {
|
|
|
|
error = err;
|
|
|
|
} finally {
|
2020-07-02 20:46:54 -04:00
|
|
|
const err = error as { status: number; statusText: string };
|
|
|
|
expect(fetchMock.calls(mockErrorUrl)).toHaveLength(4);
|
|
|
|
expect(err.status).toBe(mockErrorPayload.status);
|
|
|
|
expect(err.statusText).toBe(mockErrorPayload.statusText);
|
|
|
|
}
|
2020-04-20 21:37:13 -04:00
|
|
|
});
|
|
|
|
|
2020-07-02 20:46:54 -04:00
|
|
|
it('rejects without retries if the config is set to 0 retries', async () => {
|
2018-09-18 00:53:14 -04:00
|
|
|
expect.assertions(3);
|
2021-01-13 01:09:58 -05:00
|
|
|
let error;
|
2020-07-02 20:46:54 -04:00
|
|
|
try {
|
|
|
|
await callApi({
|
|
|
|
fetchRetryOptions: { retries: 0 },
|
|
|
|
url: mockErrorUrl,
|
|
|
|
method: 'GET',
|
2018-09-18 00:53:14 -04:00
|
|
|
});
|
2021-01-13 01:09:58 -05:00
|
|
|
} catch (err) {
|
|
|
|
error = err as { status: number; statusText: string };
|
|
|
|
} finally {
|
2020-07-02 20:46:54 -04:00
|
|
|
expect(fetchMock.calls(mockErrorUrl)).toHaveLength(1);
|
2021-01-13 01:09:58 -05:00
|
|
|
expect(error?.status).toBe(mockErrorPayload.status);
|
|
|
|
expect(error?.statusText).toBe(mockErrorPayload.statusText);
|
2020-07-02 20:46:54 -04:00
|
|
|
}
|
2018-09-18 00:53:14 -04:00
|
|
|
});
|
2020-04-20 21:37:13 -04:00
|
|
|
|
|
|
|
it('rejects after retrying thrice if the request returns a 503', async () => {
|
2020-07-02 20:46:54 -04:00
|
|
|
expect.assertions(2);
|
2020-04-20 21:37:13 -04:00
|
|
|
const url = mock503;
|
|
|
|
const response = await callApi({
|
2022-01-07 20:16:24 -05:00
|
|
|
fetchRetryOptions: constants.DEFAULT_FETCH_RETRY_OPTIONS,
|
2020-04-20 21:37:13 -04:00
|
|
|
url,
|
|
|
|
method: 'GET',
|
|
|
|
});
|
|
|
|
const calls = fetchMock.calls(url);
|
|
|
|
expect(calls).toHaveLength(4);
|
|
|
|
expect(response.status).toEqual(503);
|
|
|
|
});
|
2020-07-02 20:26:03 -04:00
|
|
|
|
2020-07-02 20:46:54 -04:00
|
|
|
it('invalid json for postPayload should thrown error', async () => {
|
|
|
|
expect.assertions(2);
|
2021-01-13 01:09:58 -05:00
|
|
|
let error;
|
2020-07-02 20:46:54 -04:00
|
|
|
try {
|
|
|
|
await callApi({
|
2020-07-02 20:26:03 -04:00
|
|
|
url: mockPostUrl,
|
|
|
|
method: 'POST',
|
|
|
|
postPayload: 'haha',
|
|
|
|
});
|
2021-01-13 01:09:58 -05:00
|
|
|
} catch (err) {
|
|
|
|
error = err;
|
|
|
|
} finally {
|
2020-07-02 20:46:54 -04:00
|
|
|
expect(error).toBeInstanceOf(Error);
|
|
|
|
expect(error.message).toEqual('Invalid payload:\n\nhaha');
|
|
|
|
}
|
2020-07-02 20:26:03 -04:00
|
|
|
});
|
2020-07-07 20:44:36 -04:00
|
|
|
|
|
|
|
it('should accept search params object', async () => {
|
|
|
|
expect.assertions(3);
|
|
|
|
window.location.href = 'http://localhost';
|
|
|
|
fetchMock.get(`glob:*/get-search*`, { yes: 'ok' });
|
|
|
|
const response = await callApi({
|
|
|
|
url: '/get-search',
|
|
|
|
searchParams: {
|
|
|
|
abc: 1,
|
|
|
|
},
|
|
|
|
method: 'GET',
|
|
|
|
});
|
|
|
|
const result = await response.json();
|
|
|
|
expect(response.status).toEqual(200);
|
|
|
|
expect(result).toEqual({ yes: 'ok' });
|
|
|
|
expect(fetchMock.lastUrl()).toEqual(`http://localhost/get-search?abc=1`);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should accept URLSearchParams', async () => {
|
|
|
|
expect.assertions(2);
|
|
|
|
window.location.href = 'http://localhost';
|
|
|
|
fetchMock.post(`glob:*/post-search*`, { yes: 'ok' });
|
|
|
|
await callApi({
|
|
|
|
url: '/post-search',
|
|
|
|
searchParams: new URLSearchParams({
|
|
|
|
abc: '1',
|
|
|
|
}),
|
|
|
|
method: 'POST',
|
|
|
|
jsonPayload: { request: 'ok' },
|
|
|
|
});
|
|
|
|
expect(fetchMock.lastUrl()).toEqual(`http://localhost/post-search?abc=1`);
|
|
|
|
expect(fetchMock.lastOptions()).toEqual(
|
|
|
|
expect.objectContaining({
|
|
|
|
body: JSON.stringify({ request: 'ok' }),
|
|
|
|
}),
|
|
|
|
);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should throw when both payloads provided', async () => {
|
|
|
|
expect.assertions(1);
|
|
|
|
fetchMock.post('/post-both-payload', {});
|
2021-01-13 01:09:58 -05:00
|
|
|
|
|
|
|
let error;
|
2020-07-07 20:44:36 -04:00
|
|
|
try {
|
|
|
|
await callApi({
|
|
|
|
url: '/post-both-payload',
|
|
|
|
method: 'POST',
|
|
|
|
postPayload: { a: 1 },
|
|
|
|
jsonPayload: '{}',
|
|
|
|
});
|
2021-01-13 01:09:58 -05:00
|
|
|
} catch (err) {
|
|
|
|
error = err;
|
|
|
|
} finally {
|
2021-11-09 07:42:28 -05:00
|
|
|
expect((error as Error).message).toContain(
|
|
|
|
'provide only one of jsonPayload or postPayload',
|
|
|
|
);
|
2020-07-07 20:44:36 -04:00
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should accept FormData as postPayload', async () => {
|
|
|
|
expect.assertions(1);
|
|
|
|
fetchMock.post('/post-formdata', {});
|
|
|
|
const payload = new FormData();
|
|
|
|
await callApi({
|
|
|
|
url: '/post-formdata',
|
|
|
|
method: 'POST',
|
|
|
|
postPayload: payload,
|
|
|
|
});
|
2022-03-23 06:57:35 -04:00
|
|
|
expect(fetchMock.lastOptions()?.body).toBe(payload);
|
2020-07-07 20:44:36 -04:00
|
|
|
});
|
|
|
|
|
|
|
|
it('should ignore "null" postPayload string', async () => {
|
|
|
|
expect.assertions(1);
|
|
|
|
fetchMock.post('/post-null-postpayload', {});
|
|
|
|
await callApi({
|
|
|
|
url: '/post-formdata',
|
|
|
|
method: 'POST',
|
|
|
|
postPayload: 'null',
|
|
|
|
});
|
2022-03-23 06:57:35 -04:00
|
|
|
expect(fetchMock.lastOptions()?.body).toBeUndefined();
|
2020-07-07 20:44:36 -04:00
|
|
|
});
|
2018-09-07 21:36:47 -04:00
|
|
|
});
|