mirror of
https://github.com/apache/superset.git
synced 2024-09-19 20:19:37 -04:00
feat(connection): optimize typing and API for SupersetClient (#635)
This commit is contained in:
parent
c9cc22ca1e
commit
ba8c619c2e
@ -1,6 +1,6 @@
|
|||||||
/* eslint-disable max-classes-per-file */
|
/* eslint-disable max-classes-per-file */
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { QueryFormData } from '@superset-ui/query/src';
|
import { QueryFormData } from '@superset-ui/query';
|
||||||
import { ChartMetadata, ChartPlugin } from '../../src';
|
import { ChartMetadata, ChartPlugin } from '../../src';
|
||||||
|
|
||||||
const DIMENSION_STYLE = {
|
const DIMENSION_STYLE = {
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import SupersetClientClass from './SupersetClientClass';
|
import SupersetClientClass from './SupersetClientClass';
|
||||||
import { ClientConfig, RequestConfig, SupersetClientInterface } from './types';
|
import { SupersetClientInterface } from './types';
|
||||||
|
|
||||||
let singletonClient: SupersetClientClass | undefined;
|
let singletonClient: SupersetClientClass | undefined;
|
||||||
|
|
||||||
@ -7,28 +7,26 @@ function getInstance(): SupersetClientClass {
|
|||||||
if (!singletonClient) {
|
if (!singletonClient) {
|
||||||
throw new Error('You must call SupersetClient.configure(...) before calling other methods');
|
throw new Error('You must call SupersetClient.configure(...) before calling other methods');
|
||||||
}
|
}
|
||||||
|
|
||||||
return singletonClient;
|
return singletonClient;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SupersetClient: SupersetClientInterface = {
|
const SupersetClient: SupersetClientInterface = {
|
||||||
configure: (config?: ClientConfig): SupersetClientClass => {
|
configure: config => {
|
||||||
singletonClient = new SupersetClientClass(config);
|
singletonClient = new SupersetClientClass(config);
|
||||||
|
|
||||||
return singletonClient;
|
return singletonClient;
|
||||||
},
|
},
|
||||||
delete: (request: RequestConfig) => getInstance().delete(request),
|
|
||||||
get: (request: RequestConfig) => getInstance().get(request),
|
|
||||||
getInstance,
|
|
||||||
init: (force?: boolean) => getInstance().init(force),
|
|
||||||
isAuthenticated: () => getInstance().isAuthenticated(),
|
|
||||||
post: (request: RequestConfig) => getInstance().post(request),
|
|
||||||
put: (request: RequestConfig) => getInstance().put(request),
|
|
||||||
reAuthenticate: () => getInstance().init(/* force = */ true),
|
|
||||||
request: (request: RequestConfig) => getInstance().request(request),
|
|
||||||
reset: () => {
|
reset: () => {
|
||||||
singletonClient = undefined;
|
singletonClient = undefined;
|
||||||
},
|
},
|
||||||
|
getInstance,
|
||||||
|
delete: request => getInstance().delete(request),
|
||||||
|
get: request => getInstance().get(request),
|
||||||
|
init: force => getInstance().init(force),
|
||||||
|
isAuthenticated: () => getInstance().isAuthenticated(),
|
||||||
|
post: request => getInstance().post(request),
|
||||||
|
put: request => getInstance().put(request),
|
||||||
|
reAuthenticate: () => getInstance().reAuthenticate(),
|
||||||
|
request: request => getInstance().request(request),
|
||||||
};
|
};
|
||||||
|
|
||||||
export default SupersetClient;
|
export default SupersetClient;
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import callApi from './callApi';
|
import callApiAndParseWithTimeout from './callApi/callApiAndParseWithTimeout';
|
||||||
import {
|
import {
|
||||||
ClientConfig,
|
ClientConfig,
|
||||||
ClientTimeout,
|
ClientTimeout,
|
||||||
@ -11,7 +11,7 @@ import {
|
|||||||
Mode,
|
Mode,
|
||||||
Protocol,
|
Protocol,
|
||||||
RequestConfig,
|
RequestConfig,
|
||||||
SupersetClientResponse,
|
ParseMethod,
|
||||||
} from './types';
|
} from './types';
|
||||||
import { DEFAULT_FETCH_RETRY_OPTIONS } from './constants';
|
import { DEFAULT_FETCH_RETRY_OPTIONS } from './constants';
|
||||||
|
|
||||||
@ -20,6 +20,7 @@ export default class SupersetClientClass {
|
|||||||
csrfToken?: CsrfToken;
|
csrfToken?: CsrfToken;
|
||||||
csrfPromise?: CsrfPromise;
|
csrfPromise?: CsrfPromise;
|
||||||
fetchRetryOptions?: FetchRetryOptions;
|
fetchRetryOptions?: FetchRetryOptions;
|
||||||
|
baseUrl: string;
|
||||||
protocol: Protocol;
|
protocol: Protocol;
|
||||||
host: Host;
|
host: Host;
|
||||||
headers: Headers;
|
headers: Headers;
|
||||||
@ -27,8 +28,9 @@ export default class SupersetClientClass {
|
|||||||
timeout: ClientTimeout;
|
timeout: ClientTimeout;
|
||||||
|
|
||||||
constructor({
|
constructor({
|
||||||
protocol = 'http:',
|
baseUrl = 'http://localhost',
|
||||||
host = 'localhost',
|
host,
|
||||||
|
protocol,
|
||||||
headers = {},
|
headers = {},
|
||||||
fetchRetryOptions = {},
|
fetchRetryOptions = {},
|
||||||
mode = 'same-origin',
|
mode = 'same-origin',
|
||||||
@ -36,52 +38,57 @@ export default class SupersetClientClass {
|
|||||||
credentials = undefined,
|
credentials = undefined,
|
||||||
csrfToken = undefined,
|
csrfToken = undefined,
|
||||||
}: ClientConfig = {}) {
|
}: ClientConfig = {}) {
|
||||||
|
const url = new URL(
|
||||||
|
host || protocol ? `${protocol || 'https:'}//${host || 'localhost'}` : baseUrl,
|
||||||
|
);
|
||||||
|
this.baseUrl = url.href.replace(/\/+$/, ''); // always strip trailing slash
|
||||||
|
this.host = url.host;
|
||||||
|
this.protocol = url.protocol as Protocol;
|
||||||
this.headers = { ...headers };
|
this.headers = { ...headers };
|
||||||
this.host = host;
|
|
||||||
this.mode = mode;
|
this.mode = mode;
|
||||||
this.timeout = timeout;
|
this.timeout = timeout;
|
||||||
this.protocol = protocol;
|
|
||||||
this.credentials = credentials;
|
this.credentials = credentials;
|
||||||
this.csrfToken = csrfToken;
|
this.csrfToken = csrfToken;
|
||||||
this.csrfPromise = undefined;
|
|
||||||
this.fetchRetryOptions = { ...DEFAULT_FETCH_RETRY_OPTIONS, ...fetchRetryOptions };
|
this.fetchRetryOptions = { ...DEFAULT_FETCH_RETRY_OPTIONS, ...fetchRetryOptions };
|
||||||
|
|
||||||
if (typeof this.csrfToken === 'string') {
|
if (typeof this.csrfToken === 'string') {
|
||||||
this.headers = { ...this.headers, 'X-CSRFToken': this.csrfToken };
|
this.headers = { ...this.headers, 'X-CSRFToken': this.csrfToken };
|
||||||
this.csrfPromise = Promise.resolve(this.csrfToken);
|
this.csrfPromise = Promise.resolve(this.csrfToken);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
init(force: boolean = false): CsrfPromise {
|
async init(force: boolean = false): CsrfPromise {
|
||||||
if (this.isAuthenticated() && !force) {
|
if (this.isAuthenticated() && !force) {
|
||||||
return this.csrfPromise as CsrfPromise;
|
return this.csrfPromise as CsrfPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.getCSRFToken();
|
return this.getCSRFToken();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async reAuthenticate() {
|
||||||
|
return this.init(true);
|
||||||
|
}
|
||||||
|
|
||||||
isAuthenticated(): boolean {
|
isAuthenticated(): boolean {
|
||||||
// if CSRF protection is disabled in the Superset app, the token may be an empty string
|
// if CSRF protection is disabled in the Superset app, the token may be an empty string
|
||||||
return this.csrfToken !== null && this.csrfToken !== undefined;
|
return this.csrfToken !== null && this.csrfToken !== undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
async get(requestConfig: RequestConfig): Promise<SupersetClientResponse> {
|
async get<T extends ParseMethod = 'json'>(requestConfig: RequestConfig & { parseMethod?: T }) {
|
||||||
return this.request({ ...requestConfig, method: 'GET' });
|
return this.request({ ...requestConfig, method: 'GET' });
|
||||||
}
|
}
|
||||||
|
|
||||||
async delete(requestConfig: RequestConfig): Promise<SupersetClientResponse> {
|
async delete<T extends ParseMethod = 'json'>(requestConfig: RequestConfig & { parseMethod?: T }) {
|
||||||
return this.request({ ...requestConfig, method: 'DELETE' });
|
return this.request({ ...requestConfig, method: 'DELETE' });
|
||||||
}
|
}
|
||||||
|
|
||||||
async put(requestConfig: RequestConfig): Promise<SupersetClientResponse> {
|
async put<T extends ParseMethod = 'json'>(requestConfig: RequestConfig & { parseMethod?: T }) {
|
||||||
return this.request({ ...requestConfig, method: 'PUT' });
|
return this.request({ ...requestConfig, method: 'PUT' });
|
||||||
}
|
}
|
||||||
|
|
||||||
async post(requestConfig: RequestConfig): Promise<SupersetClientResponse> {
|
async post<T extends ParseMethod = 'json'>(requestConfig: RequestConfig & { parseMethod?: T }) {
|
||||||
return this.request({ ...requestConfig, method: 'POST' });
|
return this.request({ ...requestConfig, method: 'POST' });
|
||||||
}
|
}
|
||||||
|
|
||||||
async request({
|
async request<T extends ParseMethod = 'json'>({
|
||||||
body,
|
body,
|
||||||
credentials,
|
credentials,
|
||||||
endpoint,
|
endpoint,
|
||||||
@ -97,9 +104,9 @@ export default class SupersetClientClass {
|
|||||||
stringify,
|
stringify,
|
||||||
timeout,
|
timeout,
|
||||||
url,
|
url,
|
||||||
}: RequestConfig): Promise<SupersetClientResponse> {
|
}: RequestConfig & { parseMethod?: T }) {
|
||||||
return this.ensureAuth().then(() =>
|
await this.ensureAuth();
|
||||||
callApi({
|
return callApiAndParseWithTimeout({
|
||||||
body,
|
body,
|
||||||
credentials: credentials ?? this.credentials,
|
credentials: credentials ?? this.credentials,
|
||||||
fetchRetryOptions,
|
fetchRetryOptions,
|
||||||
@ -113,28 +120,25 @@ export default class SupersetClientClass {
|
|||||||
stringify,
|
stringify,
|
||||||
timeout: timeout ?? this.timeout,
|
timeout: timeout ?? this.timeout,
|
||||||
url: this.getUrl({ endpoint, host, url }),
|
url: this.getUrl({ endpoint, host, url }),
|
||||||
}),
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ensureAuth(): CsrfPromise {
|
async ensureAuth(): CsrfPromise {
|
||||||
return (
|
return (
|
||||||
this.csrfPromise ??
|
this.csrfPromise ??
|
||||||
Promise.reject({
|
Promise.reject({
|
||||||
error: `SupersetClient has no CSRF token, ensure it is initialized or
|
error: `SupersetClient has not been provided a CSRF token, ensure it is
|
||||||
try logging into the Superset instance at ${this.getUrl({
|
initialized with \`client.getCSRFToken()\` or try logging in at
|
||||||
endpoint: '/login',
|
${this.getUrl({ endpoint: '/login' })}`,
|
||||||
})}`,
|
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getCSRFToken(): CsrfPromise {
|
async getCSRFToken() {
|
||||||
this.csrfToken = undefined;
|
this.csrfToken = undefined;
|
||||||
|
|
||||||
// If we can request this resource successfully, it means that the user has
|
// If we can request this resource successfully, it means that the user has
|
||||||
// authenticated. If not we throw an error prompting to authenticate.
|
// authenticated. If not we throw an error prompting to authenticate.
|
||||||
this.csrfPromise = callApi({
|
this.csrfPromise = callApiAndParseWithTimeout({
|
||||||
credentials: this.credentials,
|
credentials: this.credentials,
|
||||||
headers: {
|
headers: {
|
||||||
...this.headers,
|
...this.headers,
|
||||||
@ -143,19 +147,19 @@ export default class SupersetClientClass {
|
|||||||
mode: this.mode,
|
mode: this.mode,
|
||||||
timeout: this.timeout,
|
timeout: this.timeout,
|
||||||
url: this.getUrl({ endpoint: 'superset/csrf_token/' }),
|
url: this.getUrl({ endpoint: 'superset/csrf_token/' }),
|
||||||
}).then(response => {
|
parseMethod: 'json',
|
||||||
if (typeof response.json === 'object') {
|
}).then(({ json }) => {
|
||||||
this.csrfToken = response.json.csrf_token as string;
|
if (typeof json === 'object') {
|
||||||
|
this.csrfToken = json.csrf_token as string;
|
||||||
if (typeof this.csrfToken === 'string') {
|
if (typeof this.csrfToken === 'string') {
|
||||||
this.headers = { ...this.headers, 'X-CSRFToken': this.csrfToken };
|
this.headers = { ...this.headers, 'X-CSRFToken': this.csrfToken };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!this.isAuthenticated()) {
|
if (this.isAuthenticated()) {
|
||||||
return Promise.reject({ error: 'Failed to fetch CSRF token' });
|
|
||||||
}
|
|
||||||
return this.csrfToken;
|
return this.csrfToken;
|
||||||
|
}
|
||||||
|
return Promise.reject({ error: 'Failed to fetch CSRF token' });
|
||||||
});
|
});
|
||||||
|
|
||||||
return this.csrfPromise;
|
return this.csrfPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -10,7 +10,7 @@ import { CACHE_AVAILABLE, CACHE_KEY, HTTP_STATUS_NOT_MODIFIED, HTTP_STATUS_OK }
|
|||||||
* @param {Payload} jsonPayload json payload to post, will automatically add Content-Type header
|
* @param {Payload} jsonPayload json payload to post, will automatically add Content-Type header
|
||||||
* @param {string} stringify whether to stringify field values when post as formData
|
* @param {string} stringify whether to stringify field values when post as formData
|
||||||
*/
|
*/
|
||||||
export default function callApi({
|
export default async function callApi({
|
||||||
body,
|
body,
|
||||||
cache = 'default',
|
cache = 'default',
|
||||||
credentials = 'same-origin',
|
credentials = 'same-origin',
|
||||||
@ -40,39 +40,32 @@ export default function callApi({
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
method === 'GET' &&
|
method === 'GET' &&
|
||||||
|
cache !== 'no-store' &&
|
||||||
|
cache !== 'reload' &&
|
||||||
CACHE_AVAILABLE &&
|
CACHE_AVAILABLE &&
|
||||||
(self.location && self.location.protocol) === 'https:'
|
(self.location && self.location.protocol) === 'https:'
|
||||||
) {
|
) {
|
||||||
return caches.open(CACHE_KEY).then(supersetCache =>
|
const supersetCache = await caches.open(CACHE_KEY);
|
||||||
supersetCache
|
const cachedResponse = await supersetCache.match(url);
|
||||||
.match(url)
|
|
||||||
.then(cachedResponse => {
|
|
||||||
if (cachedResponse) {
|
if (cachedResponse) {
|
||||||
// if we have a cached response, send its ETag in the
|
// if we have a cached response, send its ETag in the
|
||||||
// `If-None-Match` header in a conditional request
|
// `If-None-Match` header in a conditional request
|
||||||
const etag = cachedResponse.headers.get('Etag') as string;
|
const etag = cachedResponse.headers.get('Etag') as string;
|
||||||
request.headers = { ...request.headers, 'If-None-Match': etag };
|
request.headers = { ...request.headers, 'If-None-Match': etag };
|
||||||
}
|
}
|
||||||
|
const response = await fetchWithRetry(url, request);
|
||||||
return fetchWithRetry(url, request);
|
|
||||||
})
|
|
||||||
.then(response => {
|
|
||||||
if (response.status === HTTP_STATUS_NOT_MODIFIED) {
|
if (response.status === HTTP_STATUS_NOT_MODIFIED) {
|
||||||
return supersetCache.match(url).then(cachedResponse => {
|
const cachedFullResponse = await supersetCache.match(url);
|
||||||
if (cachedResponse) {
|
if (cachedFullResponse) {
|
||||||
return cachedResponse.clone();
|
return cachedFullResponse.clone();
|
||||||
}
|
}
|
||||||
throw new Error('Received 304 but no content is cached!');
|
throw new Error('Received 304 but no content is cached!');
|
||||||
});
|
|
||||||
}
|
}
|
||||||
if (response.status === HTTP_STATUS_OK && response.headers.get('Etag')) {
|
if (response.status === HTTP_STATUS_OK && response.headers.get('Etag')) {
|
||||||
supersetCache.delete(url);
|
supersetCache.delete(url);
|
||||||
supersetCache.put(url, response.clone());
|
supersetCache.put(url, response.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (method === 'POST' || method === 'PATCH' || method === 'PUT') {
|
if (method === 'POST' || method === 'PATCH' || method === 'PUT') {
|
||||||
@ -80,13 +73,13 @@ export default function callApi({
|
|||||||
try {
|
try {
|
||||||
return JSON.parse(payloadString) as JsonObject;
|
return JSON.parse(payloadString) as JsonObject;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new Error(`Invalid postPayload:\n\n${payloadString}`);
|
throw new Error(`Invalid payload:\n\n${payloadString}`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// override request body with post payload
|
// override request body with post payload
|
||||||
const payload: JsonObject | undefined =
|
const payload: JsonObject | undefined =
|
||||||
typeof postPayload === 'string' ? tryParsePayload(postPayload) : postPayload;
|
typeof postPayload === 'string' ? tryParsePayload(postPayload) : postPayload;
|
||||||
|
|
||||||
if (typeof payload === 'object') {
|
if (typeof payload === 'object') {
|
||||||
// using FormData has the effect that Content-Type header is set to `multipart/form-data`,
|
// using FormData has the effect that Content-Type header is set to `multipart/form-data`,
|
||||||
// not e.g., 'application/x-www-form-urlencoded'
|
// not e.g., 'application/x-www-form-urlencoded'
|
||||||
|
@ -1,20 +1,17 @@
|
|||||||
import callApi from './callApi';
|
import callApi from './callApi';
|
||||||
import rejectAfterTimeout from './rejectAfterTimeout';
|
import rejectAfterTimeout from './rejectAfterTimeout';
|
||||||
import parseResponse from './parseResponse';
|
import parseResponse from './parseResponse';
|
||||||
import { CallApi, ClientTimeout, SupersetClientResponse, ParseMethod } from '../types';
|
import { CallApi, ClientTimeout, ParseMethod } from '../types';
|
||||||
|
|
||||||
export default function callApiAndParseWithTimeout({
|
export default async function callApiAndParseWithTimeout<T extends ParseMethod = 'json'>({
|
||||||
timeout,
|
timeout,
|
||||||
parseMethod,
|
parseMethod,
|
||||||
...rest
|
...rest
|
||||||
}: { timeout?: ClientTimeout; parseMethod?: ParseMethod } & CallApi): Promise<
|
}: { timeout?: ClientTimeout; parseMethod?: T } & CallApi) {
|
||||||
SupersetClientResponse
|
|
||||||
> {
|
|
||||||
const apiPromise = callApi(rest);
|
const apiPromise = callApi(rest);
|
||||||
|
|
||||||
const racedPromise =
|
const racedPromise =
|
||||||
typeof timeout === 'number' && timeout > 0
|
typeof timeout === 'number' && timeout > 0
|
||||||
? Promise.race([rejectAfterTimeout<Response>(timeout), apiPromise])
|
? Promise.race([apiPromise, rejectAfterTimeout<Response>(timeout)])
|
||||||
: apiPromise;
|
: apiPromise;
|
||||||
|
|
||||||
return parseResponse(racedPromise, parseMethod);
|
return parseResponse(racedPromise, parseMethod);
|
||||||
|
@ -1,26 +1,43 @@
|
|||||||
import { ParseMethod, SupersetClientResponse } from '../types';
|
import { ParseMethod, TextResponse, JsonResponse } from '../types';
|
||||||
|
|
||||||
function rejectIfNotOkay(response: Response): Promise<Response> {
|
export default async function parseResponse<T extends ParseMethod = 'json'>(
|
||||||
if (!response.ok) return Promise.reject<Response>(response);
|
apiPromise: Promise<Response>,
|
||||||
|
parseMethod?: T,
|
||||||
return Promise.resolve<Response>(response);
|
) {
|
||||||
|
type ReturnType = T extends 'raw' | null
|
||||||
|
? Response
|
||||||
|
: T extends 'json' | undefined
|
||||||
|
? JsonResponse
|
||||||
|
: T extends 'text'
|
||||||
|
? TextResponse
|
||||||
|
: never;
|
||||||
|
const response = await apiPromise;
|
||||||
|
// reject failed HTTP requests with the raw response
|
||||||
|
if (!response.ok) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-throw-literal
|
||||||
|
throw response;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function parseResponse(
|
if (parseMethod === null || parseMethod === 'raw') {
|
||||||
apiPromise: Promise<Response>,
|
return response as ReturnType;
|
||||||
parseMethod: ParseMethod = 'json',
|
|
||||||
): Promise<SupersetClientResponse> {
|
|
||||||
const checkedPromise = apiPromise.then(rejectIfNotOkay);
|
|
||||||
|
|
||||||
if (parseMethod === null) {
|
|
||||||
return apiPromise.then(rejectIfNotOkay);
|
|
||||||
}
|
}
|
||||||
if (parseMethod === 'text') {
|
if (parseMethod === 'text') {
|
||||||
return checkedPromise.then(response => response.text().then(text => ({ response, text })));
|
const text = await response.text();
|
||||||
|
const result: TextResponse = {
|
||||||
|
response,
|
||||||
|
text,
|
||||||
|
};
|
||||||
|
return result as ReturnType;
|
||||||
}
|
}
|
||||||
if (parseMethod === 'json') {
|
// by default treat this as json
|
||||||
return checkedPromise.then(response => response.json().then(json => ({ json, response })));
|
if (parseMethod === undefined || parseMethod === 'json') {
|
||||||
|
const json = await response.json();
|
||||||
|
const result: JsonResponse = {
|
||||||
|
json,
|
||||||
|
response,
|
||||||
|
};
|
||||||
|
return result as ReturnType;
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error(`Expected parseResponse=null|json|text, got '${parseMethod}'.`);
|
throw new Error(`Expected parseResponse=json|text|raw|null, got '${parseMethod}'.`);
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
// returns a Promise that rejects after the specified timeout
|
// returns a Promise that rejects after the specified timeout
|
||||||
export default function rejectAfterTimeout<T>(timeout: number): Promise<T> {
|
export default function rejectAfterTimeout<T>(timeout: number) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise<T>((resolve, reject) => {
|
||||||
setTimeout(
|
setTimeout(
|
||||||
() =>
|
() =>
|
||||||
reject({
|
reject({
|
||||||
|
@ -44,7 +44,7 @@ export type Method = RequestInit['method'];
|
|||||||
export type Mode = RequestInit['mode'];
|
export type Mode = RequestInit['mode'];
|
||||||
export type Redirect = RequestInit['redirect'];
|
export type Redirect = RequestInit['redirect'];
|
||||||
export type ClientTimeout = number | undefined;
|
export type ClientTimeout = number | undefined;
|
||||||
export type ParseMethod = 'json' | 'text' | null;
|
export type ParseMethod = 'json' | 'text' | 'raw' | null | undefined;
|
||||||
export type Signal = RequestInit['signal'];
|
export type Signal = RequestInit['signal'];
|
||||||
export type Stringify = boolean;
|
export type Stringify = boolean;
|
||||||
export type Url = string;
|
export type Url = string;
|
||||||
@ -57,7 +57,6 @@ export interface RequestBase {
|
|||||||
host?: Host;
|
host?: Host;
|
||||||
mode?: Mode;
|
mode?: Mode;
|
||||||
method?: Method;
|
method?: Method;
|
||||||
parseMethod?: ParseMethod;
|
|
||||||
postPayload?: Payload;
|
postPayload?: Payload;
|
||||||
jsonPayload?: Payload;
|
jsonPayload?: Payload;
|
||||||
signal?: Signal;
|
signal?: Signal;
|
||||||
@ -84,10 +83,14 @@ export interface RequestWithUrl extends RequestBase {
|
|||||||
// this make sure at least one of `url` or `endpoint` is set
|
// this make sure at least one of `url` or `endpoint` is set
|
||||||
export type RequestConfig = RequestWithEndpoint | RequestWithUrl;
|
export type RequestConfig = RequestWithEndpoint | RequestWithUrl;
|
||||||
|
|
||||||
export interface JsonTextResponse {
|
export interface JsonResponse {
|
||||||
json?: JsonObject;
|
|
||||||
response: Response;
|
response: Response;
|
||||||
text?: string;
|
json: JsonObject;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TextResponse {
|
||||||
|
response: Response;
|
||||||
|
text: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CsrfToken = string;
|
export type CsrfToken = string;
|
||||||
@ -95,28 +98,25 @@ export type CsrfPromise = Promise<string | undefined>;
|
|||||||
export type Protocol = 'http:' | 'https:';
|
export type Protocol = 'http:' | 'https:';
|
||||||
|
|
||||||
export interface ClientConfig {
|
export interface ClientConfig {
|
||||||
|
baseUrl?: string;
|
||||||
|
host?: Host;
|
||||||
|
protocol?: Protocol;
|
||||||
credentials?: Credentials;
|
credentials?: Credentials;
|
||||||
csrfToken?: CsrfToken;
|
csrfToken?: CsrfToken;
|
||||||
fetchRetryOptions?: FetchRetryOptions;
|
fetchRetryOptions?: FetchRetryOptions;
|
||||||
headers?: Headers;
|
headers?: Headers;
|
||||||
host?: Host;
|
|
||||||
protocol?: Protocol;
|
|
||||||
mode?: Mode;
|
mode?: Mode;
|
||||||
timeout?: ClientTimeout;
|
timeout?: ClientTimeout;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SupersetClientInterface {
|
export interface SupersetClientInterface
|
||||||
|
extends Pick<
|
||||||
|
SupersetClientClass,
|
||||||
|
'delete' | 'get' | 'post' | 'put' | 'request' | 'init' | 'isAuthenticated' | 'reAuthenticate'
|
||||||
|
> {
|
||||||
configure: (config?: ClientConfig) => SupersetClientClass;
|
configure: (config?: ClientConfig) => SupersetClientClass;
|
||||||
delete: (request: RequestConfig) => Promise<SupersetClientResponse>;
|
|
||||||
get: (request: RequestConfig) => Promise<SupersetClientResponse>;
|
|
||||||
getInstance: (maybeClient?: SupersetClientClass) => SupersetClientClass;
|
getInstance: (maybeClient?: SupersetClientClass) => SupersetClientClass;
|
||||||
init: (force?: boolean) => Promise<string | undefined>;
|
|
||||||
isAuthenticated: () => boolean;
|
|
||||||
post: (request: RequestConfig) => Promise<SupersetClientResponse>;
|
|
||||||
put: (request: RequestConfig) => Promise<SupersetClientResponse>;
|
|
||||||
reAuthenticate: () => Promise<string | undefined>;
|
|
||||||
request: (request: RequestConfig) => Promise<SupersetClientResponse>;
|
|
||||||
reset: () => void;
|
reset: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SupersetClientResponse = Response | JsonTextResponse;
|
export type SupersetClientResponse = Response | JsonResponse | TextResponse;
|
||||||
|
@ -1,3 +1,21 @@
|
|||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
import fetchMock from 'fetch-mock';
|
import fetchMock from 'fetch-mock';
|
||||||
|
|
||||||
import { SupersetClient, SupersetClientClass } from '../src';
|
import { SupersetClient, SupersetClientClass } from '../src';
|
||||||
@ -30,12 +48,12 @@ describe('SupersetClient', () => {
|
|||||||
expect(SupersetClient.isAuthenticated).toThrow();
|
expect(SupersetClient.isAuthenticated).toThrow();
|
||||||
expect(SupersetClient.reAuthenticate).toThrow();
|
expect(SupersetClient.reAuthenticate).toThrow();
|
||||||
expect(SupersetClient.request).toThrow();
|
expect(SupersetClient.request).toThrow();
|
||||||
|
|
||||||
expect(SupersetClient.configure).not.toThrow();
|
expect(SupersetClient.configure).not.toThrow();
|
||||||
});
|
});
|
||||||
|
|
||||||
// this also tests that the ^above doesn't throw if configure is called appropriately
|
// this also tests that the ^above doesn't throw if configure is called appropriately
|
||||||
it('calls appropriate SupersetClient methods when configured', () => {
|
it('calls appropriate SupersetClient methods when configured', async () => {
|
||||||
|
expect.assertions(10);
|
||||||
const mockGetUrl = '/mock/get/url';
|
const mockGetUrl = '/mock/get/url';
|
||||||
const mockPostUrl = '/mock/post/url';
|
const mockPostUrl = '/mock/post/url';
|
||||||
const mockRequestUrl = '/mock/request/url';
|
const mockRequestUrl = '/mock/request/url';
|
||||||
@ -43,8 +61,13 @@ describe('SupersetClient', () => {
|
|||||||
const mockDeleteUrl = '/mock/delete/url';
|
const mockDeleteUrl = '/mock/delete/url';
|
||||||
const mockGetPayload = { get: 'payload' };
|
const mockGetPayload = { get: 'payload' };
|
||||||
const mockPostPayload = { post: 'payload' };
|
const mockPostPayload = { post: 'payload' };
|
||||||
|
const mockDeletePayload = { delete: 'ok' };
|
||||||
|
const mockPutPayload = { put: 'ok' };
|
||||||
fetchMock.get(mockGetUrl, mockGetPayload);
|
fetchMock.get(mockGetUrl, mockGetPayload);
|
||||||
fetchMock.post(mockPostUrl, mockPostPayload);
|
fetchMock.post(mockPostUrl, mockPostPayload);
|
||||||
|
fetchMock.delete(mockDeleteUrl, mockDeletePayload);
|
||||||
|
fetchMock.put(mockPutUrl, mockPutPayload);
|
||||||
|
fetchMock.get(mockRequestUrl, mockGetPayload);
|
||||||
|
|
||||||
const initSpy = jest.spyOn(SupersetClientClass.prototype, 'init');
|
const initSpy = jest.spyOn(SupersetClientClass.prototype, 'init');
|
||||||
const getSpy = jest.spyOn(SupersetClientClass.prototype, 'get');
|
const getSpy = jest.spyOn(SupersetClientClass.prototype, 'get');
|
||||||
@ -56,19 +79,19 @@ describe('SupersetClient', () => {
|
|||||||
const requestSpy = jest.spyOn(SupersetClientClass.prototype, 'request');
|
const requestSpy = jest.spyOn(SupersetClientClass.prototype, 'request');
|
||||||
|
|
||||||
SupersetClient.configure({});
|
SupersetClient.configure({});
|
||||||
SupersetClient.init();
|
await SupersetClient.init();
|
||||||
|
|
||||||
expect(initSpy).toHaveBeenCalledTimes(1);
|
expect(initSpy).toHaveBeenCalledTimes(1);
|
||||||
expect(authenticatedSpy).toHaveBeenCalledTimes(1);
|
expect(authenticatedSpy).toHaveBeenCalledTimes(2);
|
||||||
expect(csrfSpy).toHaveBeenCalledTimes(1);
|
expect(csrfSpy).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
SupersetClient.get({ url: mockGetUrl });
|
await SupersetClient.get({ url: mockGetUrl });
|
||||||
SupersetClient.post({ url: mockPostUrl });
|
await SupersetClient.post({ url: mockPostUrl });
|
||||||
SupersetClient.delete({ url: mockDeleteUrl });
|
await SupersetClient.delete({ url: mockDeleteUrl });
|
||||||
SupersetClient.put({ url: mockPutUrl });
|
await SupersetClient.put({ url: mockPutUrl });
|
||||||
SupersetClient.request({ url: mockRequestUrl });
|
await SupersetClient.request({ url: mockRequestUrl });
|
||||||
SupersetClient.isAuthenticated();
|
SupersetClient.isAuthenticated();
|
||||||
SupersetClient.reAuthenticate();
|
await SupersetClient.reAuthenticate();
|
||||||
|
|
||||||
expect(initSpy).toHaveBeenCalledTimes(2);
|
expect(initSpy).toHaveBeenCalledTimes(2);
|
||||||
expect(deleteSpy).toHaveBeenCalledTimes(1);
|
expect(deleteSpy).toHaveBeenCalledTimes(1);
|
||||||
|
@ -1,6 +1,23 @@
|
|||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
import fetchMock from 'fetch-mock';
|
import fetchMock from 'fetch-mock';
|
||||||
import { SupersetClientClass, ClientConfig } from '../src';
|
import { SupersetClientClass, ClientConfig } from '../src';
|
||||||
import throwIfCalled from './utils/throwIfCalled';
|
|
||||||
import { LOGIN_GLOB } from './fixtures/constants';
|
import { LOGIN_GLOB } from './fixtures/constants';
|
||||||
|
|
||||||
describe('SupersetClientClass', () => {
|
describe('SupersetClientClass', () => {
|
||||||
@ -15,6 +32,11 @@ describe('SupersetClientClass', () => {
|
|||||||
expect(client).toBeInstanceOf(SupersetClientClass);
|
expect(client).toBeInstanceOf(SupersetClientClass);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('fallback protocol to https when setting only host', () => {
|
||||||
|
const client = new SupersetClientClass({ host: 'TEST-HOST' });
|
||||||
|
expect(client.baseUrl).toEqual('https://test-host');
|
||||||
|
});
|
||||||
|
|
||||||
describe('.getUrl()', () => {
|
describe('.getUrl()', () => {
|
||||||
let client = new SupersetClientClass();
|
let client = new SupersetClientClass();
|
||||||
|
|
||||||
@ -36,12 +58,12 @@ describe('SupersetClientClass', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('constructs a valid url from config.host + endpoint if host is omitted', () => {
|
it('constructs a valid url from config.host + endpoint if host is omitted', () => {
|
||||||
expect(client.getUrl({ endpoint: '/test' })).toBe('https://CONFIG_HOST/test');
|
expect(client.getUrl({ endpoint: '/test' })).toBe('https://config_host/test');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not throw if url, endpoint, and host are', () => {
|
it('does not throw if url, endpoint, and host are all empty', () => {
|
||||||
client = new SupersetClientClass({ protocol: 'https:', host: '' });
|
client = new SupersetClientClass({ protocol: 'https:', host: '' });
|
||||||
expect(client.getUrl()).toBe('https:///');
|
expect(client.getUrl()).toBe('https://localhost/');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -52,174 +74,135 @@ describe('SupersetClientClass', () => {
|
|||||||
fetchMock.get(LOGIN_GLOB, { csrf_token: 1234 }, { overwriteRoutes: true });
|
fetchMock.get(LOGIN_GLOB, { csrf_token: 1234 }, { overwriteRoutes: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('calls superset/csrf_token/ when init() is called if no CSRF token is passed', () => {
|
it('calls superset/csrf_token/ when init() is called if no CSRF token is passed', async () => {
|
||||||
expect.assertions(1);
|
expect.assertions(1);
|
||||||
|
await new SupersetClientClass().init();
|
||||||
return new SupersetClientClass().init().then(() => {
|
|
||||||
expect(fetchMock.calls(LOGIN_GLOB)).toHaveLength(1);
|
expect(fetchMock.calls(LOGIN_GLOB)).toHaveLength(1);
|
||||||
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does NOT call superset/csrf_token/ when init() is called if a CSRF token is passed', () => {
|
it('does NOT call superset/csrf_token/ when init() is called if a CSRF token is passed', async () => {
|
||||||
expect.assertions(1);
|
expect.assertions(1);
|
||||||
|
await new SupersetClientClass({ csrfToken: 'abc' }).init();
|
||||||
return new SupersetClientClass({ csrfToken: 'abc' }).init().then(() => {
|
|
||||||
expect(fetchMock.calls(LOGIN_GLOB)).toHaveLength(0);
|
expect(fetchMock.calls(LOGIN_GLOB)).toHaveLength(0);
|
||||||
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('calls superset/csrf_token/ when init(force=true) is called even if a CSRF token is passed', () => {
|
it('calls superset/csrf_token/ when init(force=true) is called even if a CSRF token is passed', async () => {
|
||||||
expect.assertions(4);
|
expect.assertions(4);
|
||||||
const initialToken = 'initial_token';
|
const initialToken = 'initial_token';
|
||||||
const client = new SupersetClientClass({ csrfToken: initialToken });
|
const client = new SupersetClientClass({ csrfToken: initialToken });
|
||||||
|
|
||||||
return client.init().then(() => {
|
await client.init();
|
||||||
expect(fetchMock.calls(LOGIN_GLOB)).toHaveLength(0);
|
expect(fetchMock.calls(LOGIN_GLOB)).toHaveLength(0);
|
||||||
expect(client.csrfToken).toBe(initialToken);
|
expect(client.csrfToken).toBe(initialToken);
|
||||||
|
|
||||||
return client.init(true).then(() => {
|
await client.init(true);
|
||||||
expect(fetchMock.calls(LOGIN_GLOB)).toHaveLength(1);
|
expect(fetchMock.calls(LOGIN_GLOB)).toHaveLength(1);
|
||||||
expect(client.csrfToken).not.toBe(initialToken);
|
expect(client.csrfToken).not.toBe(initialToken);
|
||||||
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('throws if superset/csrf_token/ returns an error', () => {
|
it('throws if superset/csrf_token/ returns an error', async () => {
|
||||||
expect.assertions(1);
|
expect.assertions(1);
|
||||||
|
const rejectError = { status: 403 };
|
||||||
fetchMock.get(LOGIN_GLOB, () => Promise.reject({ status: 403 }), {
|
fetchMock.get(LOGIN_GLOB, () => Promise.reject(rejectError), {
|
||||||
overwriteRoutes: true,
|
overwriteRoutes: true,
|
||||||
});
|
});
|
||||||
|
try {
|
||||||
return new SupersetClientClass({})
|
await new SupersetClientClass({}).init();
|
||||||
.init()
|
} catch (error) {
|
||||||
.then(throwIfCalled)
|
expect(error as typeof rejectError).toEqual(rejectError);
|
||||||
.catch((error: { status: number }) => {
|
}
|
||||||
expect(error.status).toBe(403);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('throws if superset/csrf_token/ does not return a token', () => {
|
const invalidCsrfTokenError = { error: 'Failed to fetch CSRF token' };
|
||||||
|
|
||||||
|
it('throws if superset/csrf_token/ does not return a token', async () => {
|
||||||
expect.assertions(1);
|
expect.assertions(1);
|
||||||
fetchMock.get(LOGIN_GLOB, {}, { overwriteRoutes: true });
|
fetchMock.get(LOGIN_GLOB, {}, { overwriteRoutes: true });
|
||||||
|
try {
|
||||||
return new SupersetClientClass({})
|
await new SupersetClientClass({}).init();
|
||||||
.init()
|
} catch (error) {
|
||||||
.then(throwIfCalled)
|
expect(error as typeof invalidCsrfTokenError).toEqual(invalidCsrfTokenError);
|
||||||
.catch((error: unknown) => {
|
}
|
||||||
expect(error).toBeDefined();
|
|
||||||
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not set csrfToken if response is not json', () => {
|
it('does not set csrfToken if response is not json', async () => {
|
||||||
|
expect.assertions(1);
|
||||||
fetchMock.get(LOGIN_GLOB, '123', {
|
fetchMock.get(LOGIN_GLOB, '123', {
|
||||||
overwriteRoutes: true,
|
overwriteRoutes: true,
|
||||||
});
|
});
|
||||||
|
try {
|
||||||
return new SupersetClientClass({})
|
await new SupersetClientClass({}).init();
|
||||||
.init()
|
} catch (error) {
|
||||||
.then(throwIfCalled)
|
expect(error as typeof invalidCsrfTokenError).toEqual(invalidCsrfTokenError);
|
||||||
.catch((error: unknown) => {
|
}
|
||||||
expect(error).toBeDefined();
|
|
||||||
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('.isAuthenticated()', () => {
|
describe('.isAuthenticated()', () => {
|
||||||
afterEach(fetchMock.reset);
|
afterEach(fetchMock.reset);
|
||||||
|
|
||||||
it('returns true if there is a token and false if not', () => {
|
it('returns true if there is a token and false if not', async () => {
|
||||||
expect.assertions(2);
|
expect.assertions(2);
|
||||||
const client = new SupersetClientClass({});
|
const client = new SupersetClientClass({});
|
||||||
expect(client.isAuthenticated()).toBe(false);
|
expect(client.isAuthenticated()).toBe(false);
|
||||||
|
await client.init();
|
||||||
return client.init().then(() => {
|
|
||||||
expect(client.isAuthenticated()).toBe(true);
|
expect(client.isAuthenticated()).toBe(true);
|
||||||
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns true if a token is passed at configuration', () => {
|
it('returns true if a token is passed at configuration', () => {
|
||||||
expect.assertions(2);
|
expect.assertions(2);
|
||||||
const clientWithoutToken = new SupersetClientClass({ csrfToken: undefined });
|
const clientWithoutToken = new SupersetClientClass({ csrfToken: undefined });
|
||||||
const clientWithToken = new SupersetClientClass({ csrfToken: 'token' });
|
const clientWithToken = new SupersetClientClass({ csrfToken: 'token' });
|
||||||
|
|
||||||
expect(clientWithoutToken.isAuthenticated()).toBe(false);
|
expect(clientWithoutToken.isAuthenticated()).toBe(false);
|
||||||
expect(clientWithToken.isAuthenticated()).toBe(true);
|
expect(clientWithToken.isAuthenticated()).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('.ensureAuth()', () => {
|
describe('.ensureAuth()', () => {
|
||||||
it(`returns a promise that rejects if .init() has not been called`, () => {
|
it(`returns a promise that rejects if .init() has not been called`, async () => {
|
||||||
expect.assertions(2);
|
expect.assertions(2);
|
||||||
|
|
||||||
const client = new SupersetClientClass({});
|
const client = new SupersetClientClass({});
|
||||||
|
try {
|
||||||
return client
|
await client.ensureAuth();
|
||||||
.ensureAuth()
|
} catch (error) {
|
||||||
.then(throwIfCalled)
|
expect(error).toEqual({ error: expect.any(String) });
|
||||||
.catch((error: { error: string }) => {
|
}
|
||||||
expect(error).toEqual(
|
|
||||||
expect.objectContaining({ error: expect.any(String) }) as typeof error,
|
|
||||||
);
|
|
||||||
expect(client.isAuthenticated()).toBe(false);
|
expect(client.isAuthenticated()).toBe(false);
|
||||||
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns a promise that resolves if .init() resolves successfully', () => {
|
it('returns a promise that resolves if .init() resolves successfully', async () => {
|
||||||
expect.assertions(1);
|
expect.assertions(1);
|
||||||
|
|
||||||
const client = new SupersetClientClass({});
|
const client = new SupersetClientClass({});
|
||||||
|
await client.init();
|
||||||
|
await client.ensureAuth();
|
||||||
|
|
||||||
return client.init().then(() =>
|
|
||||||
client
|
|
||||||
.ensureAuth()
|
|
||||||
.then(throwIfCalled)
|
|
||||||
.catch(() => {
|
|
||||||
expect(client.isAuthenticated()).toBe(true);
|
expect(client.isAuthenticated()).toBe(true);
|
||||||
|
|
||||||
return true;
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it(`returns a promise that rejects if .init() is unsuccessful`, () => {
|
it(`returns a promise that rejects if .init() is unsuccessful`, async () => {
|
||||||
|
expect.assertions(4);
|
||||||
|
|
||||||
const rejectValue = { status: 403 };
|
const rejectValue = { status: 403 };
|
||||||
fetchMock.get(LOGIN_GLOB, () => Promise.reject(rejectValue), {
|
fetchMock.get(LOGIN_GLOB, () => Promise.reject(rejectValue), {
|
||||||
overwriteRoutes: true,
|
overwriteRoutes: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect.assertions(3);
|
|
||||||
|
|
||||||
const client = new SupersetClientClass({});
|
const client = new SupersetClientClass({});
|
||||||
|
|
||||||
return client
|
try {
|
||||||
.init()
|
await client.init();
|
||||||
.then(throwIfCalled)
|
} catch (error) {
|
||||||
.catch((error: unknown) => {
|
expect(error).toEqual(expect.objectContaining(rejectValue));
|
||||||
expect(error).toEqual(expect.objectContaining(rejectValue) as unknown);
|
|
||||||
|
|
||||||
return client
|
|
||||||
.ensureAuth()
|
|
||||||
.then(throwIfCalled)
|
|
||||||
.catch((error2: unknown) => {
|
|
||||||
expect(error2).toEqual(expect.objectContaining(rejectValue) as unknown);
|
|
||||||
expect(client.isAuthenticated()).toBe(false);
|
expect(client.isAuthenticated()).toBe(false);
|
||||||
|
try {
|
||||||
|
await client.ensureAuth();
|
||||||
|
} catch (error2) {
|
||||||
|
expect(error2).toEqual(expect.objectContaining(rejectValue));
|
||||||
|
expect(client.isAuthenticated()).toBe(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// reset
|
// reset
|
||||||
fetchMock.get(
|
fetchMock.get(
|
||||||
@ -229,17 +212,13 @@ describe('SupersetClientClass', () => {
|
|||||||
overwriteRoutes: true,
|
overwriteRoutes: true,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('requests', () => {
|
describe('requests', () => {
|
||||||
afterEach(fetchMock.reset);
|
afterEach(fetchMock.reset);
|
||||||
const protocol = 'https:';
|
const protocol = 'https:';
|
||||||
const host = 'HOST';
|
const host = 'host';
|
||||||
const mockGetEndpoint = '/get/url';
|
const mockGetEndpoint = '/get/url';
|
||||||
const mockRequestEndpoint = '/request/url';
|
const mockRequestEndpoint = '/request/url';
|
||||||
const mockPostEndpoint = '/post/url';
|
const mockPostEndpoint = '/post/url';
|
||||||
@ -263,34 +242,32 @@ describe('SupersetClientClass', () => {
|
|||||||
fetchMock.get(mockTextUrl, mockTextJsonResponse);
|
fetchMock.get(mockTextUrl, mockTextJsonResponse);
|
||||||
fetchMock.post(mockTextUrl, mockTextJsonResponse);
|
fetchMock.post(mockTextUrl, mockTextJsonResponse);
|
||||||
|
|
||||||
it('checks for authentication before every get and post request', () => {
|
it('checks for authentication before every get and post request', async () => {
|
||||||
expect.assertions(6);
|
expect.assertions(6);
|
||||||
|
|
||||||
const authSpy = jest.spyOn(SupersetClientClass.prototype, 'ensureAuth');
|
const authSpy = jest.spyOn(SupersetClientClass.prototype, 'ensureAuth');
|
||||||
const client = new SupersetClientClass({ protocol, host });
|
const client = new SupersetClientClass({ protocol, host });
|
||||||
|
|
||||||
return client.init().then(() =>
|
await client.init();
|
||||||
Promise.all([
|
await client.get({ url: mockGetUrl });
|
||||||
client.get({ url: mockGetUrl }),
|
await client.post({ url: mockPostUrl });
|
||||||
client.post({ url: mockPostUrl }),
|
await client.put({ url: mockPutUrl });
|
||||||
client.put({ url: mockPutUrl }),
|
await client.delete({ url: mockDeleteUrl });
|
||||||
client.delete({ url: mockDeleteUrl }),
|
await client.request({ url: mockRequestUrl, method: 'DELETE' });
|
||||||
client.request({ url: mockRequestUrl, method: 'DELETE' }),
|
|
||||||
]).then(() => {
|
|
||||||
expect(fetchMock.calls(mockGetUrl)).toHaveLength(1);
|
expect(fetchMock.calls(mockGetUrl)).toHaveLength(1);
|
||||||
expect(fetchMock.calls(mockPostUrl)).toHaveLength(1);
|
expect(fetchMock.calls(mockPostUrl)).toHaveLength(1);
|
||||||
expect(fetchMock.calls(mockDeleteUrl)).toHaveLength(1);
|
expect(fetchMock.calls(mockDeleteUrl)).toHaveLength(1);
|
||||||
expect(fetchMock.calls(mockPutUrl)).toHaveLength(1);
|
expect(fetchMock.calls(mockPutUrl)).toHaveLength(1);
|
||||||
expect(fetchMock.calls(mockRequestUrl)).toHaveLength(1);
|
expect(fetchMock.calls(mockRequestUrl)).toHaveLength(1);
|
||||||
|
|
||||||
expect(authSpy).toHaveBeenCalledTimes(5);
|
expect(authSpy).toHaveBeenCalledTimes(5);
|
||||||
authSpy.mockRestore();
|
authSpy.mockRestore();
|
||||||
|
|
||||||
return true;
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('sets protocol, host, headers, mode, and credentials from config', () => {
|
it('sets protocol, host, headers, mode, and credentials from config', async () => {
|
||||||
expect.assertions(3);
|
expect.assertions(3);
|
||||||
|
|
||||||
const clientConfig: ClientConfig = {
|
const clientConfig: ClientConfig = {
|
||||||
host,
|
host,
|
||||||
protocol,
|
protocol,
|
||||||
@ -300,60 +277,43 @@ describe('SupersetClientClass', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const client = new SupersetClientClass(clientConfig);
|
const client = new SupersetClientClass(clientConfig);
|
||||||
|
await client.init();
|
||||||
|
await client.get({ url: mockGetUrl });
|
||||||
|
|
||||||
return client.init().then(() =>
|
|
||||||
client.get({ url: mockGetUrl }).then(() => {
|
|
||||||
const fetchRequest = fetchMock.calls(mockGetUrl)[0][1];
|
const fetchRequest = fetchMock.calls(mockGetUrl)[0][1];
|
||||||
expect(fetchRequest.mode).toBe(clientConfig.mode);
|
expect(fetchRequest.mode).toBe(clientConfig.mode);
|
||||||
expect(fetchRequest.credentials).toBe(clientConfig.credentials);
|
expect(fetchRequest.credentials).toBe(clientConfig.credentials);
|
||||||
expect(fetchRequest.headers).toEqual(
|
expect(fetchRequest.headers).toEqual(
|
||||||
expect.objectContaining(clientConfig.headers) as typeof fetchRequest.headers,
|
expect.objectContaining(clientConfig.headers) as typeof fetchRequest.headers,
|
||||||
);
|
);
|
||||||
|
|
||||||
return true;
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('.get()', () => {
|
describe('.get()', () => {
|
||||||
it('makes a request using url or endpoint', () => {
|
it('makes a request using url or endpoint', async () => {
|
||||||
expect.assertions(1);
|
expect.assertions(2);
|
||||||
|
|
||||||
const client = new SupersetClientClass({ protocol, host });
|
const client = new SupersetClientClass({ protocol, host });
|
||||||
|
await client.init();
|
||||||
|
|
||||||
return client.init().then(() =>
|
await client.get({ url: mockGetUrl });
|
||||||
Promise.all([
|
expect(fetchMock.calls(mockGetUrl)).toHaveLength(1);
|
||||||
client.get({ url: mockGetUrl }),
|
|
||||||
client.get({ endpoint: mockGetEndpoint }),
|
await client.get({ endpoint: mockGetEndpoint });
|
||||||
]).then(() => {
|
|
||||||
expect(fetchMock.calls(mockGetUrl)).toHaveLength(2);
|
expect(fetchMock.calls(mockGetUrl)).toHaveLength(2);
|
||||||
|
|
||||||
return true;
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('supports parsing a response as text', () => {
|
it('supports parsing a response as text', async () => {
|
||||||
expect.assertions(2);
|
expect.assertions(2);
|
||||||
const client = new SupersetClientClass({ protocol, host });
|
const client = new SupersetClientClass({ protocol, host });
|
||||||
|
await client.init();
|
||||||
return client
|
const { text } = await client.get({ url: mockTextUrl, parseMethod: 'text' });
|
||||||
.init()
|
|
||||||
.then(() =>
|
|
||||||
client
|
|
||||||
.get({ url: mockTextUrl, parseMethod: 'text' })
|
|
||||||
.then(({ text }) => {
|
|
||||||
expect(fetchMock.calls(mockTextUrl)).toHaveLength(1);
|
expect(fetchMock.calls(mockTextUrl)).toHaveLength(1);
|
||||||
expect(text).toBe(mockTextJsonResponse);
|
expect(text).toBe(mockTextJsonResponse);
|
||||||
|
|
||||||
return true;
|
|
||||||
})
|
|
||||||
.catch(throwIfCalled),
|
|
||||||
)
|
|
||||||
.catch(throwIfCalled);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('allows overriding host, headers, mode, and credentials per-request', () => {
|
it('allows overriding host, headers, mode, and credentials per-request', async () => {
|
||||||
expect.assertions(3);
|
expect.assertions(3);
|
||||||
|
|
||||||
const clientConfig: ClientConfig = {
|
const clientConfig: ClientConfig = {
|
||||||
host,
|
host,
|
||||||
protocol,
|
protocol,
|
||||||
@ -361,7 +321,6 @@ describe('SupersetClientClass', () => {
|
|||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
headers: { my: 'header' },
|
headers: { my: 'header' },
|
||||||
};
|
};
|
||||||
|
|
||||||
const overrideConfig: ClientConfig = {
|
const overrideConfig: ClientConfig = {
|
||||||
host: 'override_host',
|
host: 'override_host',
|
||||||
mode: 'no-cors',
|
mode: 'no-cors',
|
||||||
@ -370,46 +329,34 @@ describe('SupersetClientClass', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const client = new SupersetClientClass(clientConfig);
|
const client = new SupersetClientClass(clientConfig);
|
||||||
|
await client.init();
|
||||||
|
await client.get({ url: mockGetUrl, ...overrideConfig });
|
||||||
|
|
||||||
return client
|
|
||||||
.init()
|
|
||||||
.then(() =>
|
|
||||||
client
|
|
||||||
.get({ url: mockGetUrl, ...overrideConfig })
|
|
||||||
.then(() => {
|
|
||||||
const fetchRequest = fetchMock.calls(mockGetUrl)[0][1];
|
const fetchRequest = fetchMock.calls(mockGetUrl)[0][1];
|
||||||
expect(fetchRequest.mode).toBe(overrideConfig.mode);
|
expect(fetchRequest.mode).toBe(overrideConfig.mode);
|
||||||
expect(fetchRequest.credentials).toBe(overrideConfig.credentials);
|
expect(fetchRequest.credentials).toBe(overrideConfig.credentials);
|
||||||
expect(fetchRequest.headers).toEqual(
|
expect(fetchRequest.headers).toEqual(
|
||||||
expect.objectContaining(overrideConfig.headers) as typeof fetchRequest.headers,
|
expect.objectContaining(overrideConfig.headers) as typeof fetchRequest.headers,
|
||||||
);
|
);
|
||||||
|
|
||||||
return true;
|
|
||||||
})
|
|
||||||
.catch(throwIfCalled),
|
|
||||||
)
|
|
||||||
.catch(throwIfCalled);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('.post()', () => {
|
describe('.post()', () => {
|
||||||
it('makes a request using url or endpoint', () => {
|
it('makes a request using url or endpoint', async () => {
|
||||||
expect.assertions(1);
|
expect.assertions(2);
|
||||||
|
|
||||||
const client = new SupersetClientClass({ protocol, host });
|
const client = new SupersetClientClass({ protocol, host });
|
||||||
|
await client.init();
|
||||||
|
|
||||||
return client.init().then(() =>
|
await client.post({ url: mockPostUrl });
|
||||||
Promise.all([
|
expect(fetchMock.calls(mockPostUrl)).toHaveLength(1);
|
||||||
client.post({ url: mockPostUrl }),
|
|
||||||
client.post({ endpoint: mockPostEndpoint }),
|
await client.post({ endpoint: mockPostEndpoint });
|
||||||
]).then(() => {
|
|
||||||
expect(fetchMock.calls(mockPostUrl)).toHaveLength(2);
|
expect(fetchMock.calls(mockPostUrl)).toHaveLength(2);
|
||||||
|
|
||||||
return true;
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('allows overriding host, headers, mode, and credentials per-request', () => {
|
it('allows overriding host, headers, mode, and credentials per-request', async () => {
|
||||||
|
expect.assertions(3);
|
||||||
const clientConfig: ClientConfig = {
|
const clientConfig: ClientConfig = {
|
||||||
host,
|
host,
|
||||||
protocol,
|
protocol,
|
||||||
@ -417,7 +364,6 @@ describe('SupersetClientClass', () => {
|
|||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
headers: { my: 'header' },
|
headers: { my: 'header' },
|
||||||
};
|
};
|
||||||
|
|
||||||
const overrideConfig: ClientConfig = {
|
const overrideConfig: ClientConfig = {
|
||||||
host: 'override_host',
|
host: 'override_host',
|
||||||
mode: 'no-cors',
|
mode: 'no-cors',
|
||||||
@ -426,70 +372,57 @@ describe('SupersetClientClass', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const client = new SupersetClientClass(clientConfig);
|
const client = new SupersetClientClass(clientConfig);
|
||||||
|
await client.init();
|
||||||
|
await client.post({ url: mockPostUrl, ...overrideConfig });
|
||||||
|
|
||||||
return client.init().then(() =>
|
|
||||||
client.post({ url: mockPostUrl, ...overrideConfig }).then(() => {
|
|
||||||
const fetchRequest = fetchMock.calls(mockPostUrl)[0][1];
|
const fetchRequest = fetchMock.calls(mockPostUrl)[0][1];
|
||||||
|
|
||||||
expect(fetchRequest.mode).toBe(overrideConfig.mode);
|
expect(fetchRequest.mode).toBe(overrideConfig.mode);
|
||||||
expect(fetchRequest.credentials).toBe(overrideConfig.credentials);
|
expect(fetchRequest.credentials).toBe(overrideConfig.credentials);
|
||||||
expect(fetchRequest.headers).toEqual(
|
expect(fetchRequest.headers).toEqual(
|
||||||
expect.objectContaining(overrideConfig.headers) as typeof fetchRequest.headers,
|
expect.objectContaining(overrideConfig.headers) as typeof fetchRequest.headers,
|
||||||
);
|
);
|
||||||
|
|
||||||
return true;
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('supports parsing a response as text', () => {
|
it('supports parsing a response as text', async () => {
|
||||||
expect.assertions(2);
|
expect.assertions(2);
|
||||||
const client = new SupersetClientClass({ protocol, host });
|
const client = new SupersetClientClass({ protocol, host });
|
||||||
|
await client.init();
|
||||||
return client.init().then(() =>
|
const { text } = await client.post({ url: mockTextUrl, parseMethod: 'text' });
|
||||||
client.post({ url: mockTextUrl, parseMethod: 'text' }).then(({ text }) => {
|
|
||||||
expect(fetchMock.calls(mockTextUrl)).toHaveLength(1);
|
expect(fetchMock.calls(mockTextUrl)).toHaveLength(1);
|
||||||
expect(text).toBe(mockTextJsonResponse);
|
expect(text).toBe(mockTextJsonResponse);
|
||||||
|
|
||||||
return true;
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('passes postPayload key,values in the body', () => {
|
it('passes postPayload key,values in the body', async () => {
|
||||||
expect.assertions(3);
|
expect.assertions(3);
|
||||||
|
|
||||||
const postPayload = { number: 123, array: [1, 2, 3] };
|
const postPayload = { number: 123, array: [1, 2, 3] };
|
||||||
const client = new SupersetClientClass({ protocol, host });
|
const client = new SupersetClientClass({ protocol, host });
|
||||||
|
await client.init();
|
||||||
|
await client.post({ url: mockPostUrl, postPayload });
|
||||||
|
|
||||||
return client.init().then(() =>
|
|
||||||
client.post({ url: mockPostUrl, postPayload }).then(() => {
|
|
||||||
const formData = fetchMock.calls(mockPostUrl)[0][1].body as FormData;
|
const formData = fetchMock.calls(mockPostUrl)[0][1].body as FormData;
|
||||||
|
|
||||||
expect(fetchMock.calls(mockPostUrl)).toHaveLength(1);
|
expect(fetchMock.calls(mockPostUrl)).toHaveLength(1);
|
||||||
Object.entries(postPayload).forEach(([key, value]) => {
|
Object.entries(postPayload).forEach(([key, value]) => {
|
||||||
expect(formData.get(key)).toBe(JSON.stringify(value));
|
expect(formData.get(key)).toBe(JSON.stringify(value));
|
||||||
});
|
});
|
||||||
|
|
||||||
return true;
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('respects the stringify parameter for postPayload key,values', () => {
|
it('respects the stringify parameter for postPayload key,values', async () => {
|
||||||
expect.assertions(3);
|
expect.assertions(3);
|
||||||
|
|
||||||
const postPayload = { number: 123, array: [1, 2, 3] };
|
const postPayload = { number: 123, array: [1, 2, 3] };
|
||||||
const client = new SupersetClientClass({ protocol, host });
|
const client = new SupersetClientClass({ protocol, host });
|
||||||
|
await client.init();
|
||||||
|
await client.post({ url: mockPostUrl, postPayload, stringify: false });
|
||||||
|
|
||||||
return client.init().then(() =>
|
|
||||||
client.post({ url: mockPostUrl, postPayload, stringify: false }).then(() => {
|
|
||||||
const formData = fetchMock.calls(mockPostUrl)[0][1].body as FormData;
|
const formData = fetchMock.calls(mockPostUrl)[0][1].body as FormData;
|
||||||
|
|
||||||
expect(fetchMock.calls(mockPostUrl)).toHaveLength(1);
|
expect(fetchMock.calls(mockPostUrl)).toHaveLength(1);
|
||||||
Object.entries(postPayload).forEach(([key, value]) => {
|
Object.entries(postPayload).forEach(([key, value]) => {
|
||||||
expect(formData.get(key)).toBe(String(value));
|
expect(formData.get(key)).toBe(String(value));
|
||||||
});
|
});
|
||||||
|
|
||||||
return true;
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,10 +1,26 @@
|
|||||||
/* eslint promise/no-callback-in-promise: 'off' */
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
import fetchMock from 'fetch-mock';
|
import fetchMock from 'fetch-mock';
|
||||||
import callApi from '../../src/callApi/callApi';
|
import callApi from '../../src/callApi/callApi';
|
||||||
import * as constants from '../../src/constants';
|
import * as constants from '../../src/constants';
|
||||||
|
|
||||||
import { LOGIN_GLOB } from '../fixtures/constants';
|
import { LOGIN_GLOB } from '../fixtures/constants';
|
||||||
import throwIfCalled from '../utils/throwIfCalled';
|
|
||||||
import { CallApi, JsonObject } from '../../src/types';
|
import { CallApi, JsonObject } from '../../src/types';
|
||||||
import { DEFAULT_FETCH_RETRY_OPTIONS } from '../../src/constants';
|
import { DEFAULT_FETCH_RETRY_OPTIONS } from '../../src/constants';
|
||||||
|
|
||||||
@ -47,27 +63,22 @@ describe('callApi()', () => {
|
|||||||
afterEach(fetchMock.reset);
|
afterEach(fetchMock.reset);
|
||||||
|
|
||||||
describe('request config', () => {
|
describe('request config', () => {
|
||||||
it('calls the right url with the specified method', () => {
|
it('calls the right url with the specified method', async () => {
|
||||||
expect.assertions(4);
|
expect.assertions(4);
|
||||||
|
await Promise.all([
|
||||||
return Promise.all([
|
|
||||||
callApi({ url: mockGetUrl, method: 'GET' }),
|
callApi({ url: mockGetUrl, method: 'GET' }),
|
||||||
callApi({ url: mockPostUrl, method: 'POST' }),
|
callApi({ url: mockPostUrl, method: 'POST' }),
|
||||||
callApi({ url: mockPutUrl, method: 'PUT' }),
|
callApi({ url: mockPutUrl, method: 'PUT' }),
|
||||||
callApi({ url: mockPatchUrl, method: 'PATCH' }),
|
callApi({ url: mockPatchUrl, method: 'PATCH' }),
|
||||||
]).then(() => {
|
]);
|
||||||
expect(fetchMock.calls(mockGetUrl)).toHaveLength(1);
|
expect(fetchMock.calls(mockGetUrl)).toHaveLength(1);
|
||||||
expect(fetchMock.calls(mockPostUrl)).toHaveLength(1);
|
expect(fetchMock.calls(mockPostUrl)).toHaveLength(1);
|
||||||
expect(fetchMock.calls(mockPutUrl)).toHaveLength(1);
|
expect(fetchMock.calls(mockPutUrl)).toHaveLength(1);
|
||||||
expect(fetchMock.calls(mockPatchUrl)).toHaveLength(1);
|
expect(fetchMock.calls(mockPatchUrl)).toHaveLength(1);
|
||||||
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('passes along mode, cache, credentials, headers, body, signal, and redirect parameters in the request', () => {
|
it('passes along mode, cache, credentials, headers, body, signal, and redirect parameters in the request', async () => {
|
||||||
expect.assertions(8);
|
expect.assertions(8);
|
||||||
|
|
||||||
const mockRequest: CallApi = {
|
const mockRequest: CallApi = {
|
||||||
url: mockGetUrl,
|
url: mockGetUrl,
|
||||||
mode: 'cors',
|
mode: 'cors',
|
||||||
@ -81,7 +92,7 @@ describe('callApi()', () => {
|
|||||||
body: 'BODY',
|
body: 'BODY',
|
||||||
};
|
};
|
||||||
|
|
||||||
return callApi(mockRequest).then(() => {
|
await callApi(mockRequest);
|
||||||
const calls = fetchMock.calls(mockGetUrl);
|
const calls = fetchMock.calls(mockGetUrl);
|
||||||
const fetchParams = calls[0][1];
|
const fetchParams = calls[0][1];
|
||||||
expect(calls).toHaveLength(1);
|
expect(calls).toHaveLength(1);
|
||||||
@ -94,18 +105,15 @@ describe('callApi()', () => {
|
|||||||
expect(fetchParams.redirect).toBe(mockRequest.redirect);
|
expect(fetchParams.redirect).toBe(mockRequest.redirect);
|
||||||
expect(fetchParams.signal).toBe(mockRequest.signal);
|
expect(fetchParams.signal).toBe(mockRequest.signal);
|
||||||
expect(fetchParams.body).toBe(mockRequest.body);
|
expect(fetchParams.body).toBe(mockRequest.body);
|
||||||
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('POST requests', () => {
|
describe('POST requests', () => {
|
||||||
it('encodes key,value pairs from postPayload', () => {
|
it('encodes key,value pairs from postPayload', async () => {
|
||||||
expect.assertions(3);
|
expect.assertions(3);
|
||||||
const postPayload = { key: 'value', anotherKey: 1237 };
|
const postPayload = { key: 'value', anotherKey: 1237 };
|
||||||
|
|
||||||
return callApi({ url: mockPostUrl, method: 'POST', postPayload }).then(() => {
|
await callApi({ url: mockPostUrl, method: 'POST', postPayload });
|
||||||
const calls = fetchMock.calls(mockPostUrl);
|
const calls = fetchMock.calls(mockPostUrl);
|
||||||
expect(calls).toHaveLength(1);
|
expect(calls).toHaveLength(1);
|
||||||
|
|
||||||
@ -115,17 +123,14 @@ describe('callApi()', () => {
|
|||||||
Object.entries(postPayload).forEach(([key, value]) => {
|
Object.entries(postPayload).forEach(([key, value]) => {
|
||||||
expect(body.get(key)).toBe(JSON.stringify(value));
|
expect(body.get(key)).toBe(JSON.stringify(value));
|
||||||
});
|
});
|
||||||
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// the reason for this is to omit strings like 'undefined' from making their way to the backend
|
// the reason for this is to omit strings like 'undefined' from making their way to the backend
|
||||||
it('omits key,value pairs from postPayload that have undefined values (POST)', () => {
|
it('omits key,value pairs from postPayload that have undefined values (POST)', async () => {
|
||||||
expect.assertions(3);
|
expect.assertions(3);
|
||||||
const postPayload = { key: 'value', noValue: undefined };
|
const postPayload = { key: 'value', noValue: undefined };
|
||||||
|
|
||||||
return callApi({ url: mockPostUrl, method: 'POST', postPayload }).then(() => {
|
await callApi({ url: mockPostUrl, method: 'POST', postPayload });
|
||||||
const calls = fetchMock.calls(mockPostUrl);
|
const calls = fetchMock.calls(mockPostUrl);
|
||||||
expect(calls).toHaveLength(1);
|
expect(calls).toHaveLength(1);
|
||||||
|
|
||||||
@ -133,12 +138,9 @@ describe('callApi()', () => {
|
|||||||
const body = fetchParams.body as FormData;
|
const body = fetchParams.body as FormData;
|
||||||
expect(body.get('key')).toBe(JSON.stringify(postPayload.key));
|
expect(body.get('key')).toBe(JSON.stringify(postPayload.key));
|
||||||
expect(body.get('noValue')).toBeNull();
|
expect(body.get('noValue')).toBeNull();
|
||||||
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('respects the stringify flag in POST requests', () => {
|
it('respects the stringify flag in POST requests', async () => {
|
||||||
const postPayload = {
|
const postPayload = {
|
||||||
string: 'value',
|
string: 'value',
|
||||||
number: 1237,
|
number: 1237,
|
||||||
@ -150,11 +152,11 @@ describe('callApi()', () => {
|
|||||||
|
|
||||||
expect.assertions(1 + 3 * Object.keys(postPayload).length);
|
expect.assertions(1 + 3 * Object.keys(postPayload).length);
|
||||||
|
|
||||||
return Promise.all([
|
await Promise.all([
|
||||||
callApi({ url: mockPostUrl, method: 'POST', postPayload }),
|
callApi({ url: mockPostUrl, method: 'POST', postPayload }),
|
||||||
callApi({ url: mockPostUrl, method: 'POST', postPayload, stringify: false }),
|
callApi({ url: mockPostUrl, method: 'POST', postPayload, stringify: false }),
|
||||||
callApi({ url: mockPostUrl, method: 'POST', jsonPayload: postPayload }),
|
callApi({ url: mockPostUrl, method: 'POST', jsonPayload: postPayload }),
|
||||||
]).then(() => {
|
]);
|
||||||
const calls = fetchMock.calls(mockPostUrl);
|
const calls = fetchMock.calls(mockPostUrl);
|
||||||
expect(calls).toHaveLength(3);
|
expect(calls).toHaveLength(3);
|
||||||
|
|
||||||
@ -167,18 +169,15 @@ describe('callApi()', () => {
|
|||||||
expect(unstringified.get(key)).toBe(String(value));
|
expect(unstringified.get(key)).toBe(String(value));
|
||||||
expect(jsonRequestBody[key]).toEqual(value);
|
expect(jsonRequestBody[key]).toEqual(value);
|
||||||
});
|
});
|
||||||
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('PUT requests', () => {
|
describe('PUT requests', () => {
|
||||||
it('encodes key,value pairs from postPayload', () => {
|
it('encodes key,value pairs from postPayload', async () => {
|
||||||
expect.assertions(3);
|
expect.assertions(3);
|
||||||
const postPayload = { key: 'value', anotherKey: 1237 };
|
const postPayload = { key: 'value', anotherKey: 1237 };
|
||||||
|
|
||||||
return callApi({ url: mockPutUrl, method: 'PUT', postPayload }).then(() => {
|
await callApi({ url: mockPutUrl, method: 'PUT', postPayload });
|
||||||
const calls = fetchMock.calls(mockPutUrl);
|
const calls = fetchMock.calls(mockPutUrl);
|
||||||
expect(calls).toHaveLength(1);
|
expect(calls).toHaveLength(1);
|
||||||
|
|
||||||
@ -188,17 +187,14 @@ describe('callApi()', () => {
|
|||||||
Object.entries(postPayload).forEach(([key, value]) => {
|
Object.entries(postPayload).forEach(([key, value]) => {
|
||||||
expect(body.get(key)).toBe(JSON.stringify(value));
|
expect(body.get(key)).toBe(JSON.stringify(value));
|
||||||
});
|
});
|
||||||
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// the reason for this is to omit strings like 'undefined' from making their way to the backend
|
// the reason for this is to omit strings like 'undefined' from making their way to the backend
|
||||||
it('omits key,value pairs from postPayload that have undefined values (PUT)', () => {
|
it('omits key,value pairs from postPayload that have undefined values (PUT)', async () => {
|
||||||
expect.assertions(3);
|
expect.assertions(3);
|
||||||
const postPayload = { key: 'value', noValue: undefined };
|
const postPayload = { key: 'value', noValue: undefined };
|
||||||
|
|
||||||
return callApi({ url: mockPutUrl, method: 'PUT', postPayload }).then(() => {
|
await callApi({ url: mockPutUrl, method: 'PUT', postPayload });
|
||||||
const calls = fetchMock.calls(mockPutUrl);
|
const calls = fetchMock.calls(mockPutUrl);
|
||||||
expect(calls).toHaveLength(1);
|
expect(calls).toHaveLength(1);
|
||||||
|
|
||||||
@ -206,12 +202,9 @@ describe('callApi()', () => {
|
|||||||
const body = fetchParams.body as FormData;
|
const body = fetchParams.body as FormData;
|
||||||
expect(body.get('key')).toBe(JSON.stringify(postPayload.key));
|
expect(body.get('key')).toBe(JSON.stringify(postPayload.key));
|
||||||
expect(body.get('noValue')).toBeNull();
|
expect(body.get('noValue')).toBeNull();
|
||||||
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('respects the stringify flag in PUT requests', () => {
|
it('respects the stringify flag in PUT requests', async () => {
|
||||||
const postPayload = {
|
const postPayload = {
|
||||||
string: 'value',
|
string: 'value',
|
||||||
number: 1237,
|
number: 1237,
|
||||||
@ -223,10 +216,10 @@ describe('callApi()', () => {
|
|||||||
|
|
||||||
expect.assertions(1 + 2 * Object.keys(postPayload).length);
|
expect.assertions(1 + 2 * Object.keys(postPayload).length);
|
||||||
|
|
||||||
return Promise.all([
|
await Promise.all([
|
||||||
callApi({ url: mockPutUrl, method: 'PUT', postPayload }),
|
callApi({ url: mockPutUrl, method: 'PUT', postPayload }),
|
||||||
callApi({ url: mockPutUrl, method: 'PUT', postPayload, stringify: false }),
|
callApi({ url: mockPutUrl, method: 'PUT', postPayload, stringify: false }),
|
||||||
]).then(() => {
|
]);
|
||||||
const calls = fetchMock.calls(mockPutUrl);
|
const calls = fetchMock.calls(mockPutUrl);
|
||||||
expect(calls).toHaveLength(2);
|
expect(calls).toHaveLength(2);
|
||||||
|
|
||||||
@ -237,18 +230,15 @@ describe('callApi()', () => {
|
|||||||
expect(stringified.get(key)).toBe(JSON.stringify(value));
|
expect(stringified.get(key)).toBe(JSON.stringify(value));
|
||||||
expect(unstringified.get(key)).toBe(String(value));
|
expect(unstringified.get(key)).toBe(String(value));
|
||||||
});
|
});
|
||||||
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('PATCH requests', () => {
|
describe('PATCH requests', () => {
|
||||||
it('encodes key,value pairs from postPayload', () => {
|
it('encodes key,value pairs from postPayload', async () => {
|
||||||
expect.assertions(3);
|
expect.assertions(3);
|
||||||
const postPayload = { key: 'value', anotherKey: 1237 };
|
const postPayload = { key: 'value', anotherKey: 1237 };
|
||||||
|
|
||||||
return callApi({ url: mockPatchUrl, method: 'PATCH', postPayload }).then(() => {
|
await callApi({ url: mockPatchUrl, method: 'PATCH', postPayload });
|
||||||
const calls = fetchMock.calls(mockPatchUrl);
|
const calls = fetchMock.calls(mockPatchUrl);
|
||||||
expect(calls).toHaveLength(1);
|
expect(calls).toHaveLength(1);
|
||||||
|
|
||||||
@ -258,17 +248,14 @@ describe('callApi()', () => {
|
|||||||
Object.entries(postPayload).forEach(([key, value]) => {
|
Object.entries(postPayload).forEach(([key, value]) => {
|
||||||
expect(body.get(key)).toBe(JSON.stringify(value));
|
expect(body.get(key)).toBe(JSON.stringify(value));
|
||||||
});
|
});
|
||||||
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// the reason for this is to omit strings like 'undefined' from making their way to the backend
|
// the reason for this is to omit strings like 'undefined' from making their way to the backend
|
||||||
it('omits key,value pairs from postPayload that have undefined values (PATCH)', () => {
|
it('omits key,value pairs from postPayload that have undefined values (PATCH)', async () => {
|
||||||
expect.assertions(3);
|
expect.assertions(3);
|
||||||
const postPayload = { key: 'value', noValue: undefined };
|
const postPayload = { key: 'value', noValue: undefined };
|
||||||
|
|
||||||
return callApi({ url: mockPatchUrl, method: 'PATCH', postPayload }).then(() => {
|
await callApi({ url: mockPatchUrl, method: 'PATCH', postPayload });
|
||||||
const calls = fetchMock.calls(mockPatchUrl);
|
const calls = fetchMock.calls(mockPatchUrl);
|
||||||
expect(calls).toHaveLength(1);
|
expect(calls).toHaveLength(1);
|
||||||
|
|
||||||
@ -276,12 +263,9 @@ describe('callApi()', () => {
|
|||||||
const body = fetchParams.body as FormData;
|
const body = fetchParams.body as FormData;
|
||||||
expect(body.get('key')).toBe(JSON.stringify(postPayload.key));
|
expect(body.get('key')).toBe(JSON.stringify(postPayload.key));
|
||||||
expect(body.get('noValue')).toBeNull();
|
expect(body.get('noValue')).toBeNull();
|
||||||
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('respects the stringify flag in PATCH requests', () => {
|
it('respects the stringify flag in PATCH requests', async () => {
|
||||||
const postPayload = {
|
const postPayload = {
|
||||||
string: 'value',
|
string: 'value',
|
||||||
number: 1237,
|
number: 1237,
|
||||||
@ -293,10 +277,10 @@ describe('callApi()', () => {
|
|||||||
|
|
||||||
expect.assertions(1 + 2 * Object.keys(postPayload).length);
|
expect.assertions(1 + 2 * Object.keys(postPayload).length);
|
||||||
|
|
||||||
return Promise.all([
|
await Promise.all([
|
||||||
callApi({ url: mockPatchUrl, method: 'PATCH', postPayload }),
|
callApi({ url: mockPatchUrl, method: 'PATCH', postPayload }),
|
||||||
callApi({ url: mockPatchUrl, method: 'PATCH', postPayload, stringify: false }),
|
callApi({ url: mockPatchUrl, method: 'PATCH', postPayload, stringify: false }),
|
||||||
]).then(() => {
|
]);
|
||||||
const calls = fetchMock.calls(mockPatchUrl);
|
const calls = fetchMock.calls(mockPatchUrl);
|
||||||
expect(calls).toHaveLength(2);
|
expect(calls).toHaveLength(2);
|
||||||
|
|
||||||
@ -307,9 +291,6 @@ describe('callApi()', () => {
|
|||||||
expect(stringified.get(key)).toBe(JSON.stringify(value));
|
expect(stringified.get(key)).toBe(JSON.stringify(value));
|
||||||
expect(unstringified.get(key)).toBe(String(value));
|
expect(unstringified.get(key)).toBe(String(value));
|
||||||
});
|
});
|
||||||
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -326,42 +307,34 @@ describe('callApi()', () => {
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
self.location.protocol = 'https:';
|
self.location.protocol = 'https:';
|
||||||
|
|
||||||
return caches.delete(constants.CACHE_KEY);
|
return caches.delete(constants.CACHE_KEY);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('caches requests with ETags', () =>
|
it('caches requests with ETags', async () => {
|
||||||
callApi({ url: mockCacheUrl, method: 'GET' }).then(() => {
|
expect.assertions(2);
|
||||||
|
await callApi({ url: mockCacheUrl, method: 'GET' });
|
||||||
const calls = fetchMock.calls(mockCacheUrl);
|
const calls = fetchMock.calls(mockCacheUrl);
|
||||||
expect(calls).toHaveLength(1);
|
expect(calls).toHaveLength(1);
|
||||||
|
const supersetCache = await caches.open(constants.CACHE_KEY);
|
||||||
return caches.open(constants.CACHE_KEY).then(supersetCache =>
|
const cachedResponse = await supersetCache.match(mockCacheUrl);
|
||||||
supersetCache.match(mockCacheUrl).then(cachedResponse => {
|
|
||||||
expect(cachedResponse).toBeDefined();
|
expect(cachedResponse).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
return true;
|
it('will not use cache when running off an insecure connection', async () => {
|
||||||
}),
|
expect.assertions(2);
|
||||||
);
|
|
||||||
}));
|
|
||||||
|
|
||||||
it('will not use cache when running off an insecure connection', () => {
|
|
||||||
self.location.protocol = 'http:';
|
self.location.protocol = 'http:';
|
||||||
|
|
||||||
return callApi({ url: mockCacheUrl, method: 'GET' }).then(() => {
|
await callApi({ url: mockCacheUrl, method: 'GET' });
|
||||||
const calls = fetchMock.calls(mockCacheUrl);
|
const calls = fetchMock.calls(mockCacheUrl);
|
||||||
expect(calls).toHaveLength(1);
|
expect(calls).toHaveLength(1);
|
||||||
|
|
||||||
return caches.open(constants.CACHE_KEY).then(supersetCache =>
|
const supersetCache = await caches.open(constants.CACHE_KEY);
|
||||||
supersetCache.match(mockCacheUrl).then(cachedResponse => {
|
const cachedResponse = await supersetCache.match(mockCacheUrl);
|
||||||
expect(cachedResponse).toBeUndefined();
|
expect(cachedResponse).toBeUndefined();
|
||||||
|
|
||||||
return true;
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('works when the Cache API is disabled', async () => {
|
it('works when the Cache API is disabled', async () => {
|
||||||
|
expect.assertions(5);
|
||||||
// eslint-disable-next-line no-import-assign
|
// eslint-disable-next-line no-import-assign
|
||||||
Object.defineProperty(constants, 'CACHE_AVAILABLE', { value: false });
|
Object.defineProperty(constants, 'CACHE_AVAILABLE', { value: false });
|
||||||
|
|
||||||
@ -383,26 +356,25 @@ describe('callApi()', () => {
|
|||||||
Object.defineProperty(constants, 'CACHE_AVAILABLE', { value: true });
|
Object.defineProperty(constants, 'CACHE_AVAILABLE', { value: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('sends known ETags in the If-None-Match header', () =>
|
it('sends known ETags in the If-None-Match header', async () => {
|
||||||
|
expect.assertions(3);
|
||||||
// first call sets the cache
|
// first call sets the cache
|
||||||
callApi({ url: mockCacheUrl, method: 'GET' }).then(() => {
|
await callApi({ url: mockCacheUrl, method: 'GET' });
|
||||||
const calls = fetchMock.calls(mockCacheUrl);
|
const calls = fetchMock.calls(mockCacheUrl);
|
||||||
expect(calls).toHaveLength(1);
|
expect(calls).toHaveLength(1);
|
||||||
|
|
||||||
// second call sends the Etag in the If-None-Match header
|
// second call sends the Etag in the If-None-Match header
|
||||||
return callApi({ url: mockCacheUrl, method: 'GET' }).then(() => {
|
await callApi({ url: mockCacheUrl, method: 'GET' });
|
||||||
const fetchParams = calls[1][1];
|
const fetchParams = calls[1][1];
|
||||||
const headers = { 'If-None-Match': 'etag' };
|
const headers = { 'If-None-Match': 'etag' };
|
||||||
expect(calls).toHaveLength(2);
|
expect(calls).toHaveLength(2);
|
||||||
expect(fetchParams.headers).toEqual(
|
expect(fetchParams.headers).toEqual(
|
||||||
expect.objectContaining(headers) as typeof fetchParams.headers,
|
expect.objectContaining(headers) as typeof fetchParams.headers,
|
||||||
);
|
);
|
||||||
|
|
||||||
return true;
|
|
||||||
});
|
});
|
||||||
}));
|
|
||||||
|
|
||||||
it('reuses cached responses on 304 status', async () => {
|
it('reuses cached responses on 304 status', async () => {
|
||||||
|
expect.assertions(3);
|
||||||
// first call sets the cache
|
// first call sets the cache
|
||||||
await callApi({ url: mockCacheUrl, method: 'GET' });
|
await callApi({ url: mockCacheUrl, method: 'GET' });
|
||||||
const calls = fetchMock.calls(mockCacheUrl);
|
const calls = fetchMock.calls(mockCacheUrl);
|
||||||
@ -417,23 +389,28 @@ describe('callApi()', () => {
|
|||||||
expect(secondBody).toEqual('BODY');
|
expect(secondBody).toEqual('BODY');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('throws error when cache fails on 304', () => {
|
it('throws error when cache fails on 304', async () => {
|
||||||
|
expect.assertions(2);
|
||||||
|
|
||||||
// this should never happen, since a 304 is only returned if we have
|
// this should never happen, since a 304 is only returned if we have
|
||||||
// the cached response and sent the If-None-Match header
|
// the cached response and sent the If-None-Match header
|
||||||
const mockUncachedUrl = '/mock/uncached/url';
|
const mockUncachedUrl = '/mock/uncached/url';
|
||||||
const mockCachedPayload = { status: 304 };
|
const mockCachedPayload = { status: 304 };
|
||||||
fetchMock.get(mockUncachedUrl, mockCachedPayload);
|
fetchMock.get(mockUncachedUrl, mockCachedPayload);
|
||||||
|
|
||||||
return callApi({ url: mockUncachedUrl, method: 'GET' }).catch(
|
try {
|
||||||
(error: { message: string }) => {
|
await callApi({ url: mockUncachedUrl, method: 'GET' });
|
||||||
|
} catch (error) {
|
||||||
const calls = fetchMock.calls(mockUncachedUrl);
|
const calls = fetchMock.calls(mockUncachedUrl);
|
||||||
expect(calls).toHaveLength(1);
|
expect(calls).toHaveLength(1);
|
||||||
expect(error.message).toEqual('Received 304 but no content is cached!');
|
expect((error as { message: string }).message).toEqual(
|
||||||
},
|
'Received 304 but no content is cached!',
|
||||||
);
|
);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns original response if no Etag', async () => {
|
it('returns original response if no Etag', async () => {
|
||||||
|
expect.assertions(3);
|
||||||
const url = mockGetUrl;
|
const url = mockGetUrl;
|
||||||
const response = await callApi({ url, method: 'GET' });
|
const response = await callApi({ url, method: 'GET' });
|
||||||
const calls = fetchMock.calls(url);
|
const calls = fetchMock.calls(url);
|
||||||
@ -444,6 +421,7 @@ describe('callApi()', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('returns original response if status not 304 or 200', async () => {
|
it('returns original response if status not 304 or 200', async () => {
|
||||||
|
expect.assertions(2);
|
||||||
const url = mockNotFound;
|
const url = mockNotFound;
|
||||||
const response = await callApi({ url, method: 'GET' });
|
const response = await callApi({ url, method: 'GET' });
|
||||||
const calls = fetchMock.calls(url);
|
const calls = fetchMock.calls(url);
|
||||||
@ -452,39 +430,40 @@ describe('callApi()', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('rejects after retrying thrice if the request throws', () => {
|
it('rejects after retrying thrice if the request throws', async () => {
|
||||||
expect.assertions(3);
|
expect.assertions(3);
|
||||||
|
try {
|
||||||
return callApi({
|
await callApi({
|
||||||
fetchRetryOptions: DEFAULT_FETCH_RETRY_OPTIONS,
|
fetchRetryOptions: DEFAULT_FETCH_RETRY_OPTIONS,
|
||||||
url: mockErrorUrl,
|
url: mockErrorUrl,
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
})
|
});
|
||||||
.then(throwIfCalled)
|
} catch (error) {
|
||||||
.catch((error: { status: number; statusText: string }) => {
|
const err = error as { status: number; statusText: string };
|
||||||
expect(fetchMock.calls(mockErrorUrl)).toHaveLength(4);
|
expect(fetchMock.calls(mockErrorUrl)).toHaveLength(4);
|
||||||
expect(error.status).toBe(mockErrorPayload.status);
|
expect(err.status).toBe(mockErrorPayload.status);
|
||||||
expect(error.statusText).toBe(mockErrorPayload.statusText);
|
expect(err.statusText).toBe(mockErrorPayload.statusText);
|
||||||
});
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it('rejects without retries if the config is set to 0 retries', () => {
|
it('rejects without retries if the config is set to 0 retries', async () => {
|
||||||
expect.assertions(3);
|
expect.assertions(3);
|
||||||
|
try {
|
||||||
return callApi({
|
await callApi({
|
||||||
fetchRetryOptions: { retries: 0 },
|
fetchRetryOptions: { retries: 0 },
|
||||||
url: mockErrorUrl,
|
url: mockErrorUrl,
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
})
|
|
||||||
.then(throwIfCalled)
|
|
||||||
.catch((error: { status: number; statusText: string }) => {
|
|
||||||
expect(fetchMock.calls(mockErrorUrl)).toHaveLength(1);
|
|
||||||
expect(error.status).toBe(mockErrorPayload.status);
|
|
||||||
expect(error.statusText).toBe(mockErrorPayload.statusText);
|
|
||||||
});
|
});
|
||||||
|
} catch (error) {
|
||||||
|
const err = error as { status: number; statusText: string };
|
||||||
|
expect(fetchMock.calls(mockErrorUrl)).toHaveLength(1);
|
||||||
|
expect(err.status).toBe(mockErrorPayload.status);
|
||||||
|
expect(err.statusText).toBe(mockErrorPayload.statusText);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it('rejects after retrying thrice if the request returns a 503', async () => {
|
it('rejects after retrying thrice if the request returns a 503', async () => {
|
||||||
|
expect.assertions(2);
|
||||||
const url = mock503;
|
const url = mock503;
|
||||||
const response = await callApi({
|
const response = await callApi({
|
||||||
fetchRetryOptions: DEFAULT_FETCH_RETRY_OPTIONS,
|
fetchRetryOptions: DEFAULT_FETCH_RETRY_OPTIONS,
|
||||||
@ -496,13 +475,17 @@ describe('callApi()', () => {
|
|||||||
expect(response.status).toEqual(503);
|
expect(response.status).toEqual(503);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('invalid json for postPayload should thrown error', () => {
|
it('invalid json for postPayload should thrown error', async () => {
|
||||||
expect(() => {
|
expect.assertions(2);
|
||||||
callApi({
|
try {
|
||||||
|
await callApi({
|
||||||
url: mockPostUrl,
|
url: mockPostUrl,
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
postPayload: 'haha',
|
postPayload: 'haha',
|
||||||
});
|
});
|
||||||
}).toThrow('Invalid postPayload:\n\nhaha');
|
} catch (error) {
|
||||||
|
expect(error).toBeInstanceOf(Error);
|
||||||
|
expect(error.message).toEqual('Invalid payload:\n\nhaha');
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,3 +1,21 @@
|
|||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
import fetchMock from 'fetch-mock';
|
import fetchMock from 'fetch-mock';
|
||||||
|
|
||||||
import callApiAndParseWithTimeout from '../../src/callApi/callApiAndParseWithTimeout';
|
import callApiAndParseWithTimeout from '../../src/callApi/callApiAndParseWithTimeout';
|
||||||
@ -8,7 +26,6 @@ import * as parseResponse from '../../src/callApi/parseResponse';
|
|||||||
import * as rejectAfterTimeout from '../../src/callApi/rejectAfterTimeout';
|
import * as rejectAfterTimeout from '../../src/callApi/rejectAfterTimeout';
|
||||||
|
|
||||||
import { LOGIN_GLOB } from '../fixtures/constants';
|
import { LOGIN_GLOB } from '../fixtures/constants';
|
||||||
import throwIfCalled from '../utils/throwIfCalled';
|
|
||||||
|
|
||||||
describe('callApiAndParseWithTimeout()', () => {
|
describe('callApiAndParseWithTimeout()', () => {
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
@ -64,40 +81,39 @@ describe('callApiAndParseWithTimeout()', () => {
|
|||||||
rejectionSpy.mockClear();
|
rejectionSpy.mockClear();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('rejects if the request exceeds the timeout', () => {
|
it('rejects if the request exceeds the timeout', async () => {
|
||||||
return new Promise(done => {
|
expect.assertions(2);
|
||||||
expect.assertions(3);
|
|
||||||
jest.useFakeTimers();
|
jest.useFakeTimers();
|
||||||
|
|
||||||
const mockTimeoutUrl = '/mock/timeout/url';
|
const mockTimeoutUrl = '/mock/timeout/url';
|
||||||
const unresolvingPromise = new Promise(() => {});
|
const unresolvingPromise = new Promise(() => {});
|
||||||
fetchMock.get(mockTimeoutUrl, () => unresolvingPromise);
|
fetchMock.get(mockTimeoutUrl, () => unresolvingPromise);
|
||||||
|
|
||||||
callApiAndParseWithTimeout({ url: mockTimeoutUrl, method: 'GET', timeout: 1 })
|
try {
|
||||||
.then(throwIfCalled)
|
const promise = callApiAndParseWithTimeout({
|
||||||
.catch((error: { error: string; statusText: string }) => {
|
url: mockTimeoutUrl,
|
||||||
expect(fetchMock.calls(mockTimeoutUrl)).toHaveLength(1);
|
method: 'GET',
|
||||||
expect(Object.keys(error)).toEqual(['error', 'statusText']);
|
timeout: 1,
|
||||||
expect(error.statusText).toBe('timeout');
|
|
||||||
|
|
||||||
return done(); // eslint-disable-line promise/no-callback-in-promise
|
|
||||||
});
|
});
|
||||||
|
|
||||||
jest.advanceTimersByTime(2);
|
jest.advanceTimersByTime(2);
|
||||||
|
await promise;
|
||||||
|
} catch (error) {
|
||||||
|
expect(fetchMock.calls(mockTimeoutUrl)).toHaveLength(1);
|
||||||
|
expect(error).toEqual({
|
||||||
|
error: 'Request timed out',
|
||||||
|
statusText: 'timeout',
|
||||||
});
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it('resolves if the request does not exceed the timeout', () => {
|
it('resolves if the request does not exceed the timeout', async () => {
|
||||||
expect.assertions(1);
|
expect.assertions(1);
|
||||||
|
const { json } = await callApiAndParseWithTimeout({
|
||||||
return callApiAndParseWithTimeout({ url: mockGetUrl, method: 'GET', timeout: 100 }).then(
|
url: mockGetUrl,
|
||||||
response => {
|
method: 'GET',
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
timeout: 100,
|
||||||
expect(response.json).toEqual(expect.objectContaining(mockGetPayload));
|
});
|
||||||
|
expect(json).toEqual(mockGetPayload);
|
||||||
return true;
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,10 +1,26 @@
|
|||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
import fetchMock from 'fetch-mock';
|
import fetchMock from 'fetch-mock';
|
||||||
import callApi from '../../src/callApi/callApi';
|
import callApi from '../../src/callApi/callApi';
|
||||||
import parseResponse from '../../src/callApi/parseResponse';
|
import parseResponse from '../../src/callApi/parseResponse';
|
||||||
|
|
||||||
import { LOGIN_GLOB } from '../fixtures/constants';
|
import { LOGIN_GLOB } from '../fixtures/constants';
|
||||||
import throwIfCalled from '../utils/throwIfCalled';
|
|
||||||
import { SupersetClientResponse } from '../../src';
|
|
||||||
|
|
||||||
describe('parseResponse()', () => {
|
describe('parseResponse()', () => {
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
@ -35,22 +51,17 @@ describe('parseResponse()', () => {
|
|||||||
expect(parsedResponsePromise).toBeInstanceOf(Promise);
|
expect(parsedResponsePromise).toBeInstanceOf(Promise);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('resolves to { json, response } if the request succeeds', () => {
|
it('resolves to { json, response } if the request succeeds', async () => {
|
||||||
expect.assertions(4);
|
expect.assertions(4);
|
||||||
const apiPromise = callApi({ url: mockGetUrl, method: 'GET' });
|
const args = await parseResponse(callApi({ url: mockGetUrl, method: 'GET' }));
|
||||||
|
|
||||||
return parseResponse(apiPromise).then(args => {
|
|
||||||
expect(fetchMock.calls(mockGetUrl)).toHaveLength(1);
|
expect(fetchMock.calls(mockGetUrl)).toHaveLength(1);
|
||||||
const keys = Object.keys(args);
|
const keys = Object.keys(args);
|
||||||
expect(keys).toContain('response');
|
expect(keys).toContain('response');
|
||||||
expect(keys).toContain('json');
|
expect(keys).toContain('json');
|
||||||
expect(args.json).toEqual(expect.objectContaining(mockGetPayload) as typeof args.json);
|
expect(args.json).toEqual(expect.objectContaining(mockGetPayload) as typeof args.json);
|
||||||
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('throws if `parseMethod=json` and .json() fails', () => {
|
it('throws if `parseMethod=json` and .json() fails', async () => {
|
||||||
expect.assertions(3);
|
expect.assertions(3);
|
||||||
|
|
||||||
const mockTextUrl = '/mock/text/url';
|
const mockTextUrl = '/mock/text/url';
|
||||||
@ -58,20 +69,17 @@ describe('parseResponse()', () => {
|
|||||||
'<html><head></head><body>I could be a stack trace or something</body></html>';
|
'<html><head></head><body>I could be a stack trace or something</body></html>';
|
||||||
fetchMock.get(mockTextUrl, mockTextResponse);
|
fetchMock.get(mockTextUrl, mockTextResponse);
|
||||||
|
|
||||||
const apiPromise = callApi({ url: mockTextUrl, method: 'GET' });
|
try {
|
||||||
|
await parseResponse(callApi({ url: mockTextUrl, method: 'GET' }));
|
||||||
return parseResponse(apiPromise, 'json')
|
} catch (error) {
|
||||||
.then(throwIfCalled)
|
const err = error as Error;
|
||||||
.catch((error: { stack: unknown; message: string }) => {
|
|
||||||
expect(fetchMock.calls(mockTextUrl)).toHaveLength(1);
|
expect(fetchMock.calls(mockTextUrl)).toHaveLength(1);
|
||||||
expect(error.stack).toBeDefined();
|
expect(err.stack).toBeDefined();
|
||||||
expect(error.message).toContain('Unexpected token');
|
expect(err.message).toContain('Unexpected token');
|
||||||
|
}
|
||||||
return true;
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('resolves to { text, response } if the `parseMethod=text`', () => {
|
it('resolves to { text, response } if the `parseMethod=text`', async () => {
|
||||||
expect.assertions(4);
|
expect.assertions(4);
|
||||||
|
|
||||||
// test with json + bigint to ensure that it was not first parsed as json
|
// test with json + bigint to ensure that it was not first parsed as json
|
||||||
@ -79,53 +87,49 @@ describe('parseResponse()', () => {
|
|||||||
const mockTextJsonResponse = '{ "value": 9223372036854775807 }';
|
const mockTextJsonResponse = '{ "value": 9223372036854775807 }';
|
||||||
fetchMock.get(mockTextParseUrl, mockTextJsonResponse);
|
fetchMock.get(mockTextParseUrl, mockTextJsonResponse);
|
||||||
|
|
||||||
const apiPromise = callApi({ url: mockTextParseUrl, method: 'GET' });
|
const args = await parseResponse(callApi({ url: mockTextParseUrl, method: 'GET' }), 'text');
|
||||||
|
|
||||||
return parseResponse(apiPromise, 'text').then(args => {
|
|
||||||
expect(fetchMock.calls(mockTextParseUrl)).toHaveLength(1);
|
expect(fetchMock.calls(mockTextParseUrl)).toHaveLength(1);
|
||||||
const keys = Object.keys(args);
|
const keys = Object.keys(args);
|
||||||
expect(keys).toContain('response');
|
expect(keys).toContain('response');
|
||||||
expect(keys).toContain('text');
|
expect(keys).toContain('text');
|
||||||
expect(args.text).toBe(mockTextJsonResponse);
|
expect(args.text).toBe(mockTextJsonResponse);
|
||||||
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('throws if parseMethod is not null|json|text', () => {
|
it('throws if parseMethod is not null|json|text', async () => {
|
||||||
const apiPromise = callApi({ url: mockNoParseUrl, method: 'GET' });
|
expect.assertions(1);
|
||||||
|
try {
|
||||||
// @ts-ignore - 'something-else' is *intentionally* an invalid type
|
await parseResponse(
|
||||||
expect(() => parseResponse(apiPromise, 'something-else')).toThrow();
|
callApi({ url: mockNoParseUrl, method: 'GET' }),
|
||||||
|
'something-else' as never,
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
expect(error.message).toEqual(expect.stringContaining('Expected parseResponse=json'));
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it('resolves to the unmodified `Response` object if `parseMethod=null`', () => {
|
it('resolves to unmodified `Response` object if `parseMethod=null|raw`', async () => {
|
||||||
expect.assertions(2);
|
expect.assertions(3);
|
||||||
|
const responseNull = await parseResponse(callApi({ url: mockNoParseUrl, method: 'GET' }), null);
|
||||||
const apiPromise = callApi({ url: mockNoParseUrl, method: 'GET' });
|
const responseRaw = await parseResponse(callApi({ url: mockNoParseUrl, method: 'GET' }), 'raw');
|
||||||
|
expect(fetchMock.calls(mockNoParseUrl)).toHaveLength(2);
|
||||||
return parseResponse(apiPromise, null).then((clientResponse: SupersetClientResponse) => {
|
expect(responseNull.bodyUsed).toBe(false);
|
||||||
const response = clientResponse as Response;
|
expect(responseRaw.bodyUsed).toBe(false);
|
||||||
expect(fetchMock.calls(mockNoParseUrl)).toHaveLength(1);
|
|
||||||
expect(response.bodyUsed).toBe(false);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('rejects if request.ok=false', () => {
|
it('rejects if request.ok=false', async () => {
|
||||||
|
expect.assertions(3);
|
||||||
const mockNotOkayUrl = '/mock/notokay/url';
|
const mockNotOkayUrl = '/mock/notokay/url';
|
||||||
fetchMock.get(mockNotOkayUrl, 404); // 404s result in not response.ok=false
|
fetchMock.get(mockNotOkayUrl, 404); // 404s result in not response.ok=false
|
||||||
|
|
||||||
expect.assertions(3);
|
|
||||||
const apiPromise = callApi({ url: mockNotOkayUrl, method: 'GET' });
|
const apiPromise = callApi({ url: mockNotOkayUrl, method: 'GET' });
|
||||||
|
|
||||||
return parseResponse(apiPromise)
|
try {
|
||||||
.then(throwIfCalled)
|
await parseResponse(apiPromise);
|
||||||
.catch((error: { ok: boolean; status: number }) => {
|
} catch (error) {
|
||||||
|
const err = error as { ok: boolean; status: number };
|
||||||
expect(fetchMock.calls(mockNotOkayUrl)).toHaveLength(1);
|
expect(fetchMock.calls(mockNotOkayUrl)).toHaveLength(1);
|
||||||
expect(error.ok).toBe(false);
|
expect(err.ok).toBe(false);
|
||||||
expect(error.status).toBe(404);
|
expect(err.status).toBe(404);
|
||||||
});
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,23 +1,34 @@
|
|||||||
/* eslint promise/no-callback-in-promise: 'off' */
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
import rejectAfterTimeout from '../../src/callApi/rejectAfterTimeout';
|
import rejectAfterTimeout from '../../src/callApi/rejectAfterTimeout';
|
||||||
import throwIfCalled from '../utils/throwIfCalled';
|
|
||||||
|
|
||||||
describe('rejectAfterTimeout()', () => {
|
describe('rejectAfterTimeout()', () => {
|
||||||
it('returns a promise that rejects after the specified timeout', () => {
|
it('returns a promise that rejects after the specified timeout', async () => {
|
||||||
return new Promise(done => {
|
|
||||||
expect.assertions(1);
|
expect.assertions(1);
|
||||||
jest.useFakeTimers();
|
jest.useFakeTimers();
|
||||||
|
try {
|
||||||
rejectAfterTimeout(10)
|
const promise = rejectAfterTimeout(10);
|
||||||
.then(throwIfCalled)
|
|
||||||
.catch((error: Error) => {
|
|
||||||
expect(error).toBeDefined();
|
|
||||||
|
|
||||||
return done();
|
|
||||||
});
|
|
||||||
|
|
||||||
jest.advanceTimersByTime(11);
|
jest.advanceTimersByTime(11);
|
||||||
|
await promise;
|
||||||
|
} catch (error) {
|
||||||
|
expect(error).toBeDefined();
|
||||||
|
}
|
||||||
jest.useRealTimers();
|
jest.useRealTimers();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
@ -1 +1,19 @@
|
|||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
export const LOGIN_GLOB = 'glob:*superset/csrf_token/*'; // eslint-disable-line import/prefer-default-export
|
export const LOGIN_GLOB = 'glob:*superset/csrf_token/*'; // eslint-disable-line import/prefer-default-export
|
||||||
|
@ -1,3 +0,0 @@
|
|||||||
export default function throwIfCalled(args: unknown) {
|
|
||||||
throw new Error(`Unexpected call to throwIfCalled(): ${JSON.stringify(args)}`);
|
|
||||||
}
|
|
@ -5,12 +5,14 @@ export type Props = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default function ErrorMessage({ error }: Props) {
|
export default function ErrorMessage({ error }: Props) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.error(error);
|
||||||
return (
|
return (
|
||||||
<div className="alert alert-danger">
|
<pre className="alert alert-danger">
|
||||||
{error.stack || error.message}
|
{error.stack || error.message}
|
||||||
{!error.message &&
|
{!error.message &&
|
||||||
!error.stack &&
|
!error.stack &&
|
||||||
(typeof error === 'object' ? JSON.stringify(error) : String(error))}
|
(typeof error === 'object' ? JSON.stringify(error, null, 2) : String(error))}
|
||||||
</div>
|
</pre>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { text, select } from '@storybook/addon-knobs';
|
import { text, select, withKnobs } from '@storybook/addon-knobs';
|
||||||
|
|
||||||
import { SuperChart, ChartDataProvider } from '@superset-ui/chart';
|
import { SuperChart, ChartDataProvider } from '@superset-ui/chart';
|
||||||
import { SupersetClient } from '@superset-ui/connection';
|
import { SupersetClient } from '@superset-ui/connection';
|
||||||
@ -26,13 +26,12 @@ const WORD_CLOUD_LEGACY = wordCloudFormData.viz_type;
|
|||||||
const WORD_CLOUD = 'new_word_cloud';
|
const WORD_CLOUD = 'new_word_cloud';
|
||||||
|
|
||||||
new LegacyBigNumberPlugin().configure({ key: BIG_NUMBER }).register();
|
new LegacyBigNumberPlugin().configure({ key: BIG_NUMBER }).register();
|
||||||
// @ts-ignore
|
// eslint-disable-next-line
|
||||||
new LegacySankeyPlugin().configure({ key: SANKEY }).register();
|
new LegacySankeyPlugin().configure({ key: SANKEY }).register();
|
||||||
// @ts-ignore
|
// eslint-disable-next-line
|
||||||
new LegacySunburstPlugin().configure({ key: SUNBURST }).register();
|
new LegacySunburstPlugin().configure({ key: SUNBURST }).register();
|
||||||
// @ts-ignore
|
// eslint-disable-next-line
|
||||||
new LegacyWordCloudPlugin().configure({ key: WORD_CLOUD_LEGACY }).register();
|
new LegacyWordCloudPlugin().configure({ key: WORD_CLOUD_LEGACY }).register();
|
||||||
// @ts-ignore
|
|
||||||
new WordCloudChartPlugin().configure({ key: WORD_CLOUD }).register();
|
new WordCloudChartPlugin().configure({ key: WORD_CLOUD }).register();
|
||||||
|
|
||||||
const VIS_TYPES = [BIG_NUMBER, SANKEY, SUNBURST, WORD_CLOUD, WORD_CLOUD_LEGACY];
|
const VIS_TYPES = [BIG_NUMBER, SANKEY, SUNBURST, WORD_CLOUD, WORD_CLOUD_LEGACY];
|
||||||
@ -46,14 +45,19 @@ const FORM_DATA_LOOKUP = {
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
title: 'Core Packages|@superset-ui/chart',
|
title: 'Core Packages|@superset-ui/chart',
|
||||||
|
decorators: [
|
||||||
|
withKnobs({
|
||||||
|
escapeHTML: false,
|
||||||
|
}),
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
export const dataProvider = () => {
|
export const dataProvider = () => {
|
||||||
const host = text('Set Superset App host for CORS request', 'localhost:9000');
|
const host = text('Set Superset App host for CORS request', 'localhost:8088');
|
||||||
const visType = select('Chart Plugin Type', VIS_TYPES, VIS_TYPES[0]);
|
const visType = select('Chart Plugin Type', VIS_TYPES, VIS_TYPES[0]);
|
||||||
const formData = text('Override formData', JSON.stringify(FORM_DATA_LOOKUP[visType]));
|
|
||||||
const width = text('Vis width', '500');
|
const width = text('Vis width', '500');
|
||||||
const height = text('Vis height', '300');
|
const height = text('Vis height', '300');
|
||||||
|
const formData = text('Override formData', JSON.stringify(FORM_DATA_LOOKUP[visType]));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ margin: 16 }}>
|
<div style={{ margin: 16 }}>
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { select, text, withKnobs } from '@storybook/addon-knobs';
|
import { select, text, withKnobs } from '@storybook/addon-knobs';
|
||||||
import { sankeyFormData } from '@superset-ui/chart/test/fixtures/formData';
|
import { bigNumberFormData } from '@superset-ui/chart/test/fixtures/formData';
|
||||||
|
|
||||||
import VerifyCORS, { Props as VerifyCORSProps } from '../../shared/components/VerifyCORS';
|
import VerifyCORS, { Props as VerifyCORSProps } from '../../shared/components/VerifyCORS';
|
||||||
import Expandable from '../../shared/components/Expandable';
|
import Expandable from '../../shared/components/Expandable';
|
||||||
@ -21,14 +21,14 @@ export default {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const configureCORS = () => {
|
export const configureCORS = () => {
|
||||||
const host = text('Superset App host for CORS request', 'localhost:9000');
|
const host = text('Superset App host for CORS request', 'localhost:8088');
|
||||||
const selectEndpoint = select('Endpoint', ENDPOINTS, '');
|
const selectEndpoint = select('Endpoint', ENDPOINTS, '');
|
||||||
const customEndpoint = text('Custom Endpoint (override above)', '');
|
const customEndpoint = text('Custom Endpoint (override above)', '');
|
||||||
const endpoint = customEndpoint || selectEndpoint;
|
const endpoint = customEndpoint || selectEndpoint;
|
||||||
const method = endpoint ? select('Request method', REQUEST_METHODS, 'POST') : undefined;
|
const method = endpoint ? select('Request method', REQUEST_METHODS, 'POST') : undefined;
|
||||||
const postPayload =
|
const postPayload =
|
||||||
endpoint && method === 'POST'
|
endpoint && method === 'POST'
|
||||||
? text('POST payload', JSON.stringify({ form_data: sankeyFormData }, null, 2))
|
? text('POST payload', JSON.stringify({ form_data: bigNumberFormData }))
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
Loading…
Reference in New Issue
Block a user