mirror of
https://github.com/apache/superset.git
synced 2024-09-12 08:39:45 -04:00
Merge pull request #1 from apache-superset/chris--core-package
[SIP-4] add lerna monorepo and`@superset-ui/core` package with `SupersetClient`
This commit is contained in:
parent
01bcbefac6
commit
c4b946f965
9
superset-frontend/temporary_superset_ui/superset-ui/.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
9
superset-frontend/temporary_superset_ui/superset-ui/.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
@ -0,0 +1,9 @@
|
||||
💔 Breaking Changes
|
||||
|
||||
🏆 Enhancements
|
||||
|
||||
📜 Documentation
|
||||
|
||||
🐛 Bug Fix
|
||||
|
||||
🏠 Internal
|
21
superset-frontend/temporary_superset_ui/superset-ui/.gitignore
vendored
Normal file
21
superset-frontend/temporary_superset_ui/superset-ui/.gitignore
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
.babelrc
|
||||
.eslintcache
|
||||
.eslintignore
|
||||
.eslintrc.js
|
||||
.idea
|
||||
.npm
|
||||
.prettierignore
|
||||
.yarnclean
|
||||
|
||||
*.log
|
||||
*.map
|
||||
*.min.js
|
||||
|
||||
build/
|
||||
coverage/
|
||||
esm/
|
||||
jest.config.js
|
||||
lib/
|
||||
logs/
|
||||
node_modules/
|
||||
prettier.config.js
|
@ -0,0 +1,61 @@
|
||||
# `@superset-ui`
|
||||
|
||||
Collection of packages that power the Apache Superset UI, and can be used to craft custom data
|
||||
applications that leverage a Superset backend :chart_with_upwards_trend:
|
||||
|
||||
## Packages
|
||||
|
||||
[@superset-ui/core](https://github.com/apache-superset/superset-ui/tree/master/packages/superset-ui-core)
|
||||
[![Version](https://img.shields.io/npm/v/@superset-ui/core.svg?style=flat)](https://img.shields.io/npm/v/@superset-ui/core.svg?style=flat)
|
||||
|
||||
#### Coming :soon:
|
||||
|
||||
- Data providers
|
||||
- Embeddable charts
|
||||
- Chart collections
|
||||
- Demo storybook package
|
||||
|
||||
### Development
|
||||
|
||||
[lerna](https://github.com/lerna/lerna/) is used to manage versions and dependencies between
|
||||
packages in this monorepo.
|
||||
|
||||
```
|
||||
superset-ui/
|
||||
lerna.json
|
||||
package.json
|
||||
...
|
||||
packages/
|
||||
package1/
|
||||
package.json
|
||||
...
|
||||
src/
|
||||
test/
|
||||
...
|
||||
lib/
|
||||
esm/
|
||||
...
|
||||
...
|
||||
```
|
||||
|
||||
For easiest development
|
||||
|
||||
1. clone this repo
|
||||
2. install the root npm modules including lerna and yarn
|
||||
3. have lerna install package dependencies and manage the symlinking between packages for you
|
||||
|
||||
```sh
|
||||
git clone ...superset-ui && cd superset-ui
|
||||
npm install
|
||||
lerna bootstrap
|
||||
```
|
||||
|
||||
### Builds, linting, and testing
|
||||
|
||||
Each package defines its own build config, linting, and testing. You can have lerna run commands
|
||||
across all packages using the syntax `lerna exec test` from the root `@superset/monorepo` root
|
||||
directory.
|
||||
|
||||
### License
|
||||
|
||||
Apache-2.0
|
@ -0,0 +1,5 @@
|
||||
{
|
||||
"lerna": "3.2.1",
|
||||
"packages": ["packages/*"],
|
||||
"version": "0.0.0"
|
||||
}
|
@ -0,0 +1,39 @@
|
||||
{
|
||||
"name": "@superset-ui/monorepo",
|
||||
"version": "0.0.0",
|
||||
"description": "Superset UI",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "lerna run build",
|
||||
"jest": "lerna run test",
|
||||
"lint": "lerna run lint",
|
||||
"prerelease": "yarn run build",
|
||||
"prepare-release": "git checkout master && git pull --rebase origin master && yarn run test",
|
||||
"release": "yarn run prepare-release && lerna publish && lerna run gh-pages",
|
||||
"test": "lerna bootstrap && yarn run lint && yarn run jest"
|
||||
},
|
||||
"repository": "https://github.com/apache-superset/superset-ui.git",
|
||||
"keywords": [
|
||||
"apache",
|
||||
"superset",
|
||||
"data",
|
||||
"analytics",
|
||||
"analysis",
|
||||
"visualization",
|
||||
"react",
|
||||
"d3",
|
||||
"data-ui",
|
||||
"vx"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"devDependencies": {
|
||||
"lerna": "^3.2.1",
|
||||
"yarn": "^1.9.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.10.0"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
}
|
||||
}
|
@ -0,0 +1,90 @@
|
||||
## `@superset-ui/core`
|
||||
|
||||
[![Version](https://img.shields.io/npm/v/@superset-ui/core.svg?style=flat)](https://img.shields.io/npm/v/@superset-ui/core.svg?style=flat)
|
||||
|
||||
Core modules for Superset:
|
||||
|
||||
- `SupersetClient` requests and authentication
|
||||
- (future) `i18n` locales and translation
|
||||
|
||||
### SupersetClient
|
||||
|
||||
The `SupersetClient` handles all client-side requests to the Superset backend. It can be configured
|
||||
for use within the Superset application, or used to issue `CORS` requests in other applications. At
|
||||
a high-level it supports:
|
||||
|
||||
- `CSRF` token authentication
|
||||
- queues requests in the case that another request is made before the token is received
|
||||
- it checks for a token before every request, an external app that uses this can detect this by
|
||||
catching errors, or explicitly checking `SupersetClient.isAuthorized()`
|
||||
- supports `GET` and `POST` requests (no `PUT` or `DELETE`)
|
||||
- timeouts
|
||||
- query aborts through the `AbortController` API
|
||||
|
||||
#### Example usage
|
||||
|
||||
```javascript
|
||||
// appSetup.js
|
||||
import { SupersetClient } from `@superset-ui/core`;
|
||||
// or import SupersetClient from `@superset-ui/core/lib|esm/SupersetClient`;
|
||||
|
||||
SupersetClient.configure(...clientConfig);
|
||||
SupersetClient.init(); // CSRF auth, can also chain `.configure().init();
|
||||
|
||||
// anotherFile.js
|
||||
import { SupersetClient } from `@superset-ui/core`;
|
||||
|
||||
SupersetClient.post(...requestConfig)
|
||||
.then(({ request, json }) => ...)
|
||||
.catch((error) => ...);
|
||||
```
|
||||
|
||||
#### API
|
||||
|
||||
##### Client Configuration
|
||||
|
||||
The following flags can be passed in the client config call
|
||||
`SupersetClient.configure(...clientConfig);`
|
||||
|
||||
- `protocol = 'http'`
|
||||
- `host`
|
||||
- `headers`
|
||||
- `credentials = 'same-origin'` (set to `include` for non-Superset apps)
|
||||
- `mode = 'same-origin'` (set to `cors` for non-Superset apps)
|
||||
- `timeout`
|
||||
|
||||
##### Per-request Configuration
|
||||
|
||||
The following flags can be passed on a per-request call `SupersetClient.get/post(...requestConfig);`
|
||||
|
||||
- `url` or `endpoint`
|
||||
- `headers`
|
||||
- `body`
|
||||
- `timeout`
|
||||
- `signal` (for aborting, from `const { signal } = (new AbortController())`)
|
||||
- for `POST` requests
|
||||
- `postPayload` (key values are added to a `new FormData()`)
|
||||
- `stringify` whether to call `JSON.stringify` on `postPayload` values
|
||||
|
||||
##### Request aborting
|
||||
|
||||
Per-request aborting is implemented through the `AbortController` API:
|
||||
|
||||
```javascript
|
||||
import { SupersetClient } from '@superset-ui/core';
|
||||
import AbortController from 'abortcontroller-polyfill';
|
||||
|
||||
const controller = new AbortController();
|
||||
const { signal } = controller;
|
||||
|
||||
SupersetClient.get({ ..., signal }).then(...).catch(...);
|
||||
|
||||
if (IWantToCancelForSomeReason) {
|
||||
signal.abort(); // Promise is rejected, request `catch` is invoked
|
||||
}
|
||||
```
|
||||
|
||||
### Development
|
||||
|
||||
`@data-ui/build-config` is used to manage the build configuration for this package including babel
|
||||
builds, jest testing, eslint, and prettier.
|
@ -0,0 +1,73 @@
|
||||
{
|
||||
"name": "@superset-ui/core",
|
||||
"version": "0.0.0",
|
||||
"description": "Superset UI core 🤖",
|
||||
"sideEffects": false,
|
||||
"main": "lib/index.js",
|
||||
"module": "esm/index.js",
|
||||
"files": [
|
||||
"esm",
|
||||
"lib"
|
||||
],
|
||||
"scripts": {
|
||||
"build:cjs": "beemo babel ./src --out-dir lib/ --minify",
|
||||
"build:esm": "beemo babel ./src --out-dir esm/ --esm --minify",
|
||||
"build": "yarn run build:cjs && yarn run build:esm",
|
||||
"dev": "beemo babel --watch ./src --out-dir esm/ --esm",
|
||||
"jest": "beemo jest --color --coverage",
|
||||
"eslint": "beemo eslint \"./{src,test}/**/*.{js,jsx,json,md}\"",
|
||||
"lint": "yarn run prettier --write && yarn run eslint --fix",
|
||||
"test": "yarn run jest",
|
||||
"prettier": "beemo prettier \"./{src,test}/**/*.{js,jsx,json,md}\"",
|
||||
"sync:gitignore": "beemo sync-dotfiles --filter=gitignore",
|
||||
"prepublish": "yarn run build"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/apache-superset/superset-ui.git"
|
||||
},
|
||||
"keywords": [
|
||||
"superset",
|
||||
"client",
|
||||
"core",
|
||||
"data"
|
||||
],
|
||||
"author": "",
|
||||
"license": "Apache-2.0",
|
||||
"bugs": {
|
||||
"url": "https://github.com/apache-superset/superset-ui/issues"
|
||||
},
|
||||
"homepage": "https://github.com/apache-superset/superset-ui#readme",
|
||||
"devDependencies": {
|
||||
"@data-ui/build-config": "0.0.10",
|
||||
"fetch-mock": "^6.5.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"url-search-params-polyfill": "^4.0.1",
|
||||
"whatwg-fetch": "^2.0.4"
|
||||
},
|
||||
"beemo": {
|
||||
"module": "@data-ui/build-config",
|
||||
"drivers": [
|
||||
"babel",
|
||||
"eslint",
|
||||
{
|
||||
"driver": "jest",
|
||||
"env": {
|
||||
"NODE_ENV": "test"
|
||||
}
|
||||
},
|
||||
"prettier"
|
||||
],
|
||||
"eslint": {
|
||||
"rules": {
|
||||
"prefer-promise-reject-errors": "off"
|
||||
}
|
||||
},
|
||||
"jest": {
|
||||
"testPathIgnorePatterns": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,149 @@
|
||||
import callApi from './callApi';
|
||||
|
||||
class SupersetClient {
|
||||
constructor(config) {
|
||||
const {
|
||||
protocol = 'http',
|
||||
host = '',
|
||||
headers = {},
|
||||
mode = 'same-origin',
|
||||
timeout,
|
||||
credentials,
|
||||
} = config;
|
||||
|
||||
this.headers = headers;
|
||||
this.host = host;
|
||||
this.mode = mode;
|
||||
this.timeout = timeout;
|
||||
this.protocol = protocol;
|
||||
this.credentials = credentials;
|
||||
this.csrfToken = null;
|
||||
this.didAuthSuccessfully = false;
|
||||
this.csrfPromise = null;
|
||||
}
|
||||
|
||||
isAuthenticated() {
|
||||
return this.didAuthSuccessfully;
|
||||
}
|
||||
|
||||
init() {
|
||||
return this.getCSRFToken();
|
||||
}
|
||||
|
||||
getCSRFToken() {
|
||||
// If we can request this resource successfully, it means that the user has
|
||||
// authenticated. If not we throw an error prompting to authenticate.
|
||||
this.csrfPromise = callApi({
|
||||
credentials: this.credentials,
|
||||
headers: {
|
||||
...this.headers,
|
||||
},
|
||||
method: 'GET',
|
||||
mode: this.mode,
|
||||
timeout: this.timeout,
|
||||
url: this.getUrl({ endpoint: 'superset/csrf_token/', host: this.host }),
|
||||
}).then(response => {
|
||||
if (response.json) {
|
||||
this.csrfToken = response.json.csrf_token;
|
||||
this.headers = { ...this.headers, 'X-CSRFToken': this.csrfToken };
|
||||
this.didAuthSuccessfully = !!this.csrfToken;
|
||||
}
|
||||
|
||||
if (!this.csrfToken) {
|
||||
return Promise.reject({ error: 'Failed to fetch CSRF token' });
|
||||
}
|
||||
|
||||
return response;
|
||||
});
|
||||
|
||||
return this.csrfPromise;
|
||||
}
|
||||
|
||||
getUrl({ host = '', endpoint = '' }) {
|
||||
const cleanHost = host.slice(-1) === '/' ? host.slice(0, -1) : host; // no backslash
|
||||
|
||||
return `${this.protocol}://${cleanHost}/${endpoint[0] === '/' ? endpoint.slice(1) : endpoint}`;
|
||||
}
|
||||
|
||||
ensureAuth() {
|
||||
return (
|
||||
this.csrfPromise ||
|
||||
Promise.reject({
|
||||
error: `SupersetClient has no CSRF token, ensure it is initialized or
|
||||
try logging into the Superset instance at ${this.getUrl('/login')}`,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
get({ host, url, endpoint, mode, credentials, headers, body, timeout, signal }) {
|
||||
return this.ensureAuth().then(() =>
|
||||
callApi({
|
||||
body,
|
||||
credentials: credentials || this.credentials,
|
||||
headers: { ...this.headers, ...headers },
|
||||
method: 'GET',
|
||||
mode: mode || this.mode,
|
||||
signal,
|
||||
timeout: timeout || this.timeout,
|
||||
url: url || this.getUrl({ endpoint, host: host || this.host }),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
post({
|
||||
host,
|
||||
endpoint,
|
||||
url,
|
||||
mode,
|
||||
credentials,
|
||||
headers,
|
||||
postPayload,
|
||||
timeout,
|
||||
signal,
|
||||
stringify,
|
||||
}) {
|
||||
return this.ensureAuth().then(() =>
|
||||
callApi({
|
||||
credentials: credentials || this.credentials,
|
||||
headers: { ...this.headers, ...headers },
|
||||
method: 'POST',
|
||||
mode: mode || this.mode,
|
||||
postPayload,
|
||||
signal,
|
||||
stringify,
|
||||
timeout: timeout || this.timeout,
|
||||
url: url || this.getUrl({ endpoint, host: host || this.host }),
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let singletonClient;
|
||||
|
||||
function hasInstance() {
|
||||
if (!singletonClient) {
|
||||
throw new Error('You must call SupersetClient.configure(...) before calling other methods');
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
const PublicAPI = {
|
||||
configure: config => {
|
||||
singletonClient = new SupersetClient(config || {});
|
||||
|
||||
return singletonClient;
|
||||
},
|
||||
get: (...args) => hasInstance() && singletonClient.get(...args),
|
||||
init: () => hasInstance() && singletonClient.init(),
|
||||
isAuthenticated: () => hasInstance() && singletonClient.isAuthenticated(),
|
||||
post: (...args) => hasInstance() && singletonClient.post(...args),
|
||||
reAuthenticate: () => hasInstance() && singletonClient.getCSRFToken(),
|
||||
reset: () => {
|
||||
singletonClient = null;
|
||||
},
|
||||
};
|
||||
|
||||
export { SupersetClient };
|
||||
|
||||
export default PublicAPI;
|
@ -0,0 +1,50 @@
|
||||
import 'whatwg-fetch';
|
||||
import 'url-search-params-polyfill';
|
||||
|
||||
const DEFAULT_HEADERS = null;
|
||||
|
||||
// This function fetches an API response and returns the corresponding json
|
||||
export default function callApi({
|
||||
url,
|
||||
method = 'GET', // GET, POST, PUT, DELETE
|
||||
mode = 'same-origin', // no-cors, cors, same-origin
|
||||
cache = 'default', // default, no-cache, reload, force-cache, only-if-cached
|
||||
credentials = 'same-origin', // include, same-origin, omit
|
||||
headers: partialHeaders,
|
||||
body,
|
||||
postPayload,
|
||||
stringify = true,
|
||||
redirect = 'follow', // manual, follow, error
|
||||
timeoutId,
|
||||
signal, // used for aborting
|
||||
}) {
|
||||
let request = {
|
||||
body,
|
||||
cache,
|
||||
credentials,
|
||||
headers: { ...DEFAULT_HEADERS, ...partialHeaders },
|
||||
method,
|
||||
mode,
|
||||
redirect,
|
||||
signal,
|
||||
};
|
||||
|
||||
if (method === 'POST' && typeof postPayload === 'object') {
|
||||
// using FormData has the effect that Content-Type header is set to `multipart/form-data`,
|
||||
// not e.g., 'application/x-www-form-urlencoded'
|
||||
const formData = new FormData();
|
||||
Object.keys(postPayload).forEach(key => {
|
||||
const value = postPayload[key];
|
||||
if (typeof value !== 'undefined') {
|
||||
formData.append(key, stringify ? JSON.stringify(postPayload[key]) : postPayload[key]);
|
||||
}
|
||||
});
|
||||
|
||||
request = {
|
||||
...request,
|
||||
body: formData,
|
||||
};
|
||||
}
|
||||
|
||||
return fetch(url, request); // eslint-disable-line compat/compat
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
import callApi from './callApi';
|
||||
import rejectAfterTimeout from './rejectAfterTimeout';
|
||||
import parseResponse from './parseResponse';
|
||||
|
||||
export default function callApiAndParseWithTimeout({ timeout, ...rest }) {
|
||||
const apiPromise = callApi(rest);
|
||||
|
||||
const racedPromise =
|
||||
typeof timeout === 'number' && timeout > 0
|
||||
? Promise.race([rejectAfterTimeout(timeout), apiPromise])
|
||||
: apiPromise;
|
||||
|
||||
return parseResponse(racedPromise);
|
||||
}
|
@ -0,0 +1 @@
|
||||
export { default } from './callApiAndParseWithTimeout';
|
@ -0,0 +1,26 @@
|
||||
export default function parseResponse(apiPromise) {
|
||||
return apiPromise.then(apiResponse =>
|
||||
// first try to parse as json, and fall back to text (e.g., in the case of HTML stacktrace)
|
||||
// cannot fall back to .text() without cloning the response because body is single-use
|
||||
apiResponse
|
||||
.clone()
|
||||
.json()
|
||||
.catch(() => /* jsonParseError */ apiResponse.text().then(textPayload => ({ textPayload })))
|
||||
.then(maybeJson => ({
|
||||
json: maybeJson.textPayload ? undefined : maybeJson,
|
||||
response: apiResponse,
|
||||
text: maybeJson.textPayload,
|
||||
}))
|
||||
.then(({ response, json, text }) => {
|
||||
if (!response.ok) {
|
||||
return Promise.reject({
|
||||
error: response.error || (json && json.error) || text || 'An error occurred',
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
});
|
||||
}
|
||||
|
||||
return typeof text === 'undefined' ? { json, response } : { response, text };
|
||||
}),
|
||||
);
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
// returns a Promise that rejects after the specified timeout
|
||||
export default function rejectAfterTimeout(timeout) {
|
||||
return new Promise((resolve, reject) => {
|
||||
setTimeout(
|
||||
() =>
|
||||
reject({
|
||||
error: 'Request timed out',
|
||||
statusText: 'timeout',
|
||||
}),
|
||||
timeout,
|
||||
);
|
||||
});
|
||||
}
|
@ -0,0 +1,2 @@
|
||||
export { default as callApi } from './callApi';
|
||||
export { default as SupersetClient } from './SupersetClient';
|
@ -0,0 +1,471 @@
|
||||
/* eslint promise/no-callback-in-promise: 'off' */
|
||||
import fetchMock from 'fetch-mock';
|
||||
|
||||
import PublicAPI, { SupersetClient } from '../src/SupersetClient';
|
||||
|
||||
import throwIfCalled from './utils/throwIfCalled';
|
||||
import { LOGIN_GLOB } from './fixtures/constants';
|
||||
|
||||
describe('SupersetClient', () => {
|
||||
beforeAll(() => {
|
||||
fetchMock.get(LOGIN_GLOB, { csrf_token: '1234' });
|
||||
});
|
||||
|
||||
afterAll(fetchMock.restore);
|
||||
|
||||
afterEach(PublicAPI.reset);
|
||||
|
||||
describe('Public API', () => {
|
||||
it('exposes reset, configure, init, get, post, isAuthenticated, and reAuthenticate methods', () => {
|
||||
expect(PublicAPI.configure).toEqual(expect.any(Function));
|
||||
expect(PublicAPI.init).toEqual(expect.any(Function));
|
||||
expect(PublicAPI.get).toEqual(expect.any(Function));
|
||||
expect(PublicAPI.post).toEqual(expect.any(Function));
|
||||
expect(PublicAPI.isAuthenticated).toEqual(expect.any(Function));
|
||||
expect(PublicAPI.reAuthenticate).toEqual(expect.any(Function));
|
||||
expect(PublicAPI.reset).toEqual(expect.any(Function));
|
||||
});
|
||||
|
||||
it('throws if you call init, get, post, isAuthenticated, or reAuthenticate before configure', () => {
|
||||
expect(PublicAPI.init).toThrow();
|
||||
expect(PublicAPI.get).toThrow();
|
||||
expect(PublicAPI.post).toThrow();
|
||||
expect(PublicAPI.isAuthenticated).toThrow();
|
||||
expect(PublicAPI.reAuthenticate).toThrow();
|
||||
|
||||
expect(PublicAPI.configure).not.toThrow();
|
||||
});
|
||||
|
||||
// this also tests that the ^above doesn't throw if configure is called appropriately
|
||||
it('calls appropriate SupersetClient methods when configured', () => {
|
||||
const mockGetUrl = '/mock/get/url';
|
||||
const mockPostUrl = '/mock/post/url';
|
||||
const mockGetPayload = { get: 'payload' };
|
||||
const mockPostPayload = { post: 'payload' };
|
||||
fetchMock.get(mockGetUrl, mockGetPayload);
|
||||
fetchMock.post(mockPostUrl, mockPostPayload);
|
||||
|
||||
const initSpy = jest.spyOn(SupersetClient.prototype, 'init');
|
||||
const getSpy = jest.spyOn(SupersetClient.prototype, 'get');
|
||||
const postSpy = jest.spyOn(SupersetClient.prototype, 'post');
|
||||
const authenticatedSpy = jest.spyOn(SupersetClient.prototype, 'isAuthenticated');
|
||||
const csrfSpy = jest.spyOn(SupersetClient.prototype, 'getCSRFToken');
|
||||
|
||||
PublicAPI.configure({});
|
||||
PublicAPI.init();
|
||||
expect(csrfSpy).toHaveBeenCalledTimes(1);
|
||||
|
||||
PublicAPI.get({ url: mockGetUrl });
|
||||
PublicAPI.post({ url: mockPostUrl });
|
||||
PublicAPI.isAuthenticated();
|
||||
PublicAPI.reAuthenticate({});
|
||||
|
||||
expect(initSpy).toHaveBeenCalledTimes(1);
|
||||
expect(getSpy).toHaveBeenCalledTimes(1);
|
||||
expect(postSpy).toHaveBeenCalledTimes(1);
|
||||
expect(authenticatedSpy).toHaveBeenCalledTimes(1);
|
||||
expect(csrfSpy).toHaveBeenCalledTimes(2); // from init() + reAuthenticate()
|
||||
|
||||
initSpy.mockRestore();
|
||||
getSpy.mockRestore();
|
||||
postSpy.mockRestore();
|
||||
authenticatedSpy.mockRestore();
|
||||
csrfSpy.mockRestore();
|
||||
|
||||
fetchMock.reset();
|
||||
});
|
||||
});
|
||||
|
||||
describe('SupersetClient', () => {
|
||||
describe('CSRF', () => {
|
||||
afterEach(fetchMock.reset);
|
||||
|
||||
it('calls superset/csrf_token/ upon initialization', done => {
|
||||
expect.assertions(1);
|
||||
const client = new SupersetClient({});
|
||||
|
||||
client
|
||||
.init()
|
||||
.then(() => {
|
||||
expect(fetchMock.calls(LOGIN_GLOB)).toHaveLength(1);
|
||||
|
||||
return done();
|
||||
})
|
||||
.catch(throwIfCalled);
|
||||
});
|
||||
|
||||
it('isAuthenticated() returns true if there is a token and false if not', done => {
|
||||
expect.assertions(2);
|
||||
const client = new SupersetClient({});
|
||||
expect(client.isAuthenticated()).toBe(false);
|
||||
|
||||
client
|
||||
.init()
|
||||
.then(() => {
|
||||
expect(client.isAuthenticated()).toBe(true);
|
||||
|
||||
return done();
|
||||
})
|
||||
.catch(throwIfCalled);
|
||||
});
|
||||
|
||||
it('init() throws if superset/csrf_token/ returns an error', done => {
|
||||
expect.assertions(1);
|
||||
|
||||
fetchMock.get(LOGIN_GLOB, () => Promise.reject({ status: 403 }), {
|
||||
overwriteRoutes: true,
|
||||
});
|
||||
|
||||
const client = new SupersetClient({});
|
||||
|
||||
client
|
||||
.init()
|
||||
.then(throwIfCalled)
|
||||
.catch(error => {
|
||||
expect(error.status).toBe(403);
|
||||
|
||||
// reset
|
||||
fetchMock.get(
|
||||
LOGIN_GLOB,
|
||||
{ csrf_token: 1234 },
|
||||
{
|
||||
overwriteRoutes: true,
|
||||
},
|
||||
);
|
||||
|
||||
return done();
|
||||
});
|
||||
});
|
||||
|
||||
it('init() throws if superset/csrf_token/ does not return a token', done => {
|
||||
expect.assertions(1);
|
||||
fetchMock.get(LOGIN_GLOB, {}, { overwriteRoutes: true });
|
||||
|
||||
const client = new SupersetClient({});
|
||||
client
|
||||
.init()
|
||||
.then(throwIfCalled)
|
||||
.catch(error => {
|
||||
expect(error).toBeDefined();
|
||||
|
||||
// reset
|
||||
fetchMock.get(
|
||||
LOGIN_GLOB,
|
||||
{ csrf_token: 1234 },
|
||||
{
|
||||
overwriteRoutes: true,
|
||||
},
|
||||
);
|
||||
|
||||
return done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('CSRF queuing', () => {
|
||||
it(`client.ensureAuth() returns a promise that rejects init() has not been called`, done => {
|
||||
expect.assertions(2);
|
||||
|
||||
const client = new SupersetClient({});
|
||||
|
||||
client
|
||||
.ensureAuth()
|
||||
.then(throwIfCalled)
|
||||
.catch(error => {
|
||||
expect(error).toEqual(expect.objectContaining({ error: expect.any(String) }));
|
||||
expect(client.didAuthSuccessfully).toBe(false);
|
||||
|
||||
return done();
|
||||
});
|
||||
});
|
||||
|
||||
it('client.ensureAuth() returns a promise that resolves if client.init() resolves successfully', done => {
|
||||
expect.assertions(1);
|
||||
|
||||
const client = new SupersetClient({});
|
||||
|
||||
client
|
||||
.init()
|
||||
.then(() =>
|
||||
client
|
||||
.ensureAuth()
|
||||
.then(throwIfCalled)
|
||||
.catch(() => {
|
||||
expect(client.didAuthSuccessfully).toBe(true);
|
||||
|
||||
return done();
|
||||
}),
|
||||
)
|
||||
.catch(throwIfCalled);
|
||||
});
|
||||
|
||||
it(`client.ensureAuth() returns a promise that rejects if init() is unsuccessful`, done => {
|
||||
const rejectValue = { status: 403 };
|
||||
fetchMock.get(LOGIN_GLOB, () => Promise.reject(rejectValue), {
|
||||
overwriteRoutes: true,
|
||||
});
|
||||
|
||||
expect.assertions(3);
|
||||
|
||||
const client = new SupersetClient({});
|
||||
|
||||
client
|
||||
.init()
|
||||
.then(throwIfCalled)
|
||||
.catch(error => {
|
||||
expect(error).toEqual(expect.objectContaining(rejectValue));
|
||||
|
||||
return client
|
||||
.ensureAuth()
|
||||
.then(throwIfCalled)
|
||||
.catch(error2 => {
|
||||
expect(error2).toEqual(expect.objectContaining(rejectValue));
|
||||
expect(client.didAuthSuccessfully).toBe(false);
|
||||
|
||||
// reset
|
||||
fetchMock.get(
|
||||
LOGIN_GLOB,
|
||||
{ csrf_token: 1234 },
|
||||
{
|
||||
overwriteRoutes: true,
|
||||
},
|
||||
);
|
||||
|
||||
return done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('requests', () => {
|
||||
afterEach(fetchMock.reset);
|
||||
const protocol = 'PROTOCOL';
|
||||
const host = 'HOST';
|
||||
const mockGetEndpoint = '/get/url';
|
||||
const mockPostEndpoint = '/post/url';
|
||||
const mockGetUrl = `${protocol}://${host}${mockGetEndpoint}`;
|
||||
const mockPostUrl = `${protocol}://${host}${mockPostEndpoint}`;
|
||||
|
||||
fetchMock.get(mockGetUrl, 'Ok');
|
||||
fetchMock.post(mockPostUrl, 'Ok');
|
||||
|
||||
it('checks for authentication before every get and post request', done => {
|
||||
expect.assertions(3);
|
||||
const authSpy = jest.spyOn(SupersetClient.prototype, 'ensureAuth');
|
||||
const client = new SupersetClient({ protocol, host });
|
||||
|
||||
client
|
||||
.init()
|
||||
.then(() =>
|
||||
Promise.all([client.get({ url: mockGetUrl }), client.post({ url: mockPostUrl })])
|
||||
.then(() => {
|
||||
expect(fetchMock.calls(mockGetUrl)).toHaveLength(1);
|
||||
expect(fetchMock.calls(mockPostUrl)).toHaveLength(1);
|
||||
expect(authSpy).toHaveBeenCalledTimes(2);
|
||||
authSpy.mockRestore();
|
||||
|
||||
return done();
|
||||
})
|
||||
.catch(throwIfCalled),
|
||||
)
|
||||
.catch(throwIfCalled);
|
||||
});
|
||||
|
||||
it('sets protocol, host, headers, mode, and credentials from config', done => {
|
||||
expect.assertions(3);
|
||||
const clientConfig = {
|
||||
host,
|
||||
protocol,
|
||||
mode: 'a la mode',
|
||||
credentials: 'mad cred',
|
||||
headers: { my: 'header' },
|
||||
};
|
||||
|
||||
const client = new SupersetClient(clientConfig);
|
||||
client
|
||||
.init()
|
||||
.then(() =>
|
||||
client
|
||||
.get({ url: mockGetUrl })
|
||||
.then(() => {
|
||||
const fetchRequest = fetchMock.calls(mockGetUrl)[0][1];
|
||||
expect(fetchRequest.mode).toBe(clientConfig.mode);
|
||||
expect(fetchRequest.credentials).toBe(clientConfig.credentials);
|
||||
expect(fetchRequest.headers).toEqual(expect.objectContaining(clientConfig.headers));
|
||||
|
||||
return done();
|
||||
})
|
||||
.catch(throwIfCalled),
|
||||
)
|
||||
.catch(throwIfCalled);
|
||||
});
|
||||
|
||||
describe('GET', () => {
|
||||
it('makes a request using url or endpoint', done => {
|
||||
expect.assertions(1);
|
||||
const client = new SupersetClient({ protocol, host });
|
||||
client
|
||||
.init()
|
||||
.then(() =>
|
||||
Promise.all([
|
||||
client.get({ url: mockGetUrl }),
|
||||
client.get({ endpoint: mockGetEndpoint }),
|
||||
])
|
||||
.then(() => {
|
||||
expect(fetchMock.calls(mockGetUrl)).toHaveLength(2);
|
||||
|
||||
return done();
|
||||
})
|
||||
.catch(throwIfCalled),
|
||||
)
|
||||
.catch(throwIfCalled);
|
||||
});
|
||||
|
||||
it('allows overriding host, headers, mode, and credentials per-request', done => {
|
||||
expect.assertions(3);
|
||||
const clientConfig = {
|
||||
host,
|
||||
protocol,
|
||||
mode: 'a la mode',
|
||||
credentials: 'mad cred',
|
||||
headers: { my: 'header' },
|
||||
};
|
||||
|
||||
const overrideConfig = {
|
||||
host: 'override_host',
|
||||
mode: 'override mode',
|
||||
credentials: 'override credentials',
|
||||
headers: { my: 'override', another: 'header' },
|
||||
};
|
||||
|
||||
const client = new SupersetClient(clientConfig);
|
||||
client
|
||||
.init()
|
||||
.then(() =>
|
||||
client
|
||||
.get({ url: mockGetUrl, ...overrideConfig })
|
||||
.then(() => {
|
||||
const fetchRequest = fetchMock.calls(mockGetUrl)[0][1];
|
||||
expect(fetchRequest.mode).toBe(overrideConfig.mode);
|
||||
expect(fetchRequest.credentials).toBe(overrideConfig.credentials);
|
||||
expect(fetchRequest.headers).toEqual(
|
||||
expect.objectContaining(overrideConfig.headers),
|
||||
);
|
||||
|
||||
return done();
|
||||
})
|
||||
.catch(throwIfCalled),
|
||||
)
|
||||
.catch(throwIfCalled);
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST', () => {
|
||||
it('makes a request using url or endpoint', done => {
|
||||
expect.assertions(1);
|
||||
const client = new SupersetClient({ protocol, host });
|
||||
client
|
||||
.init()
|
||||
.then(() =>
|
||||
Promise.all([
|
||||
client.post({ url: mockPostUrl }),
|
||||
client.post({ endpoint: mockPostEndpoint }),
|
||||
])
|
||||
.then(() => {
|
||||
expect(fetchMock.calls(mockPostUrl)).toHaveLength(2);
|
||||
|
||||
return done();
|
||||
})
|
||||
.catch(throwIfCalled),
|
||||
)
|
||||
.catch(throwIfCalled);
|
||||
});
|
||||
|
||||
it('allows overriding host, headers, mode, and credentials per-request', done => {
|
||||
const clientConfig = {
|
||||
host,
|
||||
protocol,
|
||||
mode: 'a la mode',
|
||||
credentials: 'mad cred',
|
||||
headers: { my: 'header' },
|
||||
};
|
||||
|
||||
const overrideConfig = {
|
||||
host: 'override_host',
|
||||
mode: 'override mode',
|
||||
credentials: 'override credentials',
|
||||
headers: { my: 'override', another: 'header' },
|
||||
};
|
||||
|
||||
const client = new SupersetClient(clientConfig);
|
||||
client
|
||||
.init()
|
||||
.then(() =>
|
||||
client
|
||||
.post({ url: mockPostUrl, ...overrideConfig })
|
||||
.then(() => {
|
||||
const fetchRequest = fetchMock.calls(mockPostUrl)[0][1];
|
||||
expect(fetchRequest.mode).toBe(overrideConfig.mode);
|
||||
expect(fetchRequest.credentials).toBe(overrideConfig.credentials);
|
||||
expect(fetchRequest.headers).toEqual(
|
||||
expect.objectContaining(overrideConfig.headers),
|
||||
);
|
||||
|
||||
return done();
|
||||
})
|
||||
.catch(throwIfCalled),
|
||||
)
|
||||
.catch(throwIfCalled);
|
||||
});
|
||||
|
||||
it('passes postPayload key,values in the body', done => {
|
||||
expect.assertions(3);
|
||||
|
||||
const postPayload = { number: 123, array: [1, 2, 3] };
|
||||
const client = new SupersetClient({ protocol, host });
|
||||
client
|
||||
.init()
|
||||
.then(() =>
|
||||
client
|
||||
.post({ url: mockPostUrl, postPayload })
|
||||
.then(() => {
|
||||
const formData = fetchMock.calls(mockPostUrl)[0][1].body;
|
||||
expect(fetchMock.calls(mockPostUrl)).toHaveLength(1);
|
||||
Object.keys(postPayload).forEach(key => {
|
||||
expect(formData.get(key)).toBe(JSON.stringify(postPayload[key]));
|
||||
});
|
||||
|
||||
return done();
|
||||
})
|
||||
.catch(throwIfCalled),
|
||||
)
|
||||
.catch(throwIfCalled);
|
||||
});
|
||||
|
||||
it('respects the stringify parameter for postPayload key,values', done => {
|
||||
expect.assertions(3);
|
||||
const postPayload = { number: 123, array: [1, 2, 3] };
|
||||
const client = new SupersetClient({ protocol, host });
|
||||
|
||||
client
|
||||
.init()
|
||||
.then(() =>
|
||||
client
|
||||
.post({ url: mockPostUrl, postPayload, stringify: false })
|
||||
.then(() => {
|
||||
const formData = fetchMock.calls(mockPostUrl)[0][1].body;
|
||||
expect(fetchMock.calls(mockPostUrl)).toHaveLength(1);
|
||||
Object.keys(postPayload).forEach(key => {
|
||||
expect(formData.get(key)).toBe(String(postPayload[key]));
|
||||
});
|
||||
|
||||
return done();
|
||||
})
|
||||
.catch(throwIfCalled),
|
||||
)
|
||||
.catch(throwIfCalled);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,156 @@
|
||||
/* eslint promise/no-callback-in-promise: 'off' */
|
||||
import fetchMock from 'fetch-mock';
|
||||
import callApi from '../../src/callApi/callApi';
|
||||
|
||||
import { LOGIN_GLOB } from '../fixtures/constants';
|
||||
import throwIfCalled from '../utils/throwIfCalled';
|
||||
|
||||
describe('callApi()', () => {
|
||||
beforeAll(() => {
|
||||
fetchMock.get(LOGIN_GLOB, { csrf_token: '1234' });
|
||||
});
|
||||
|
||||
afterAll(fetchMock.restore);
|
||||
|
||||
const mockGetUrl = '/mock/get/url';
|
||||
const mockPostUrl = '/mock/post/url';
|
||||
const mockErrorUrl = '/mock/error/url';
|
||||
|
||||
const mockGetPayload = { get: 'payload' };
|
||||
const mockPostPayload = { post: 'payload' };
|
||||
const mockErrorPayload = { status: 500, statusText: 'Internal error' };
|
||||
|
||||
fetchMock.get(mockGetUrl, mockGetPayload);
|
||||
fetchMock.post(mockPostUrl, mockPostPayload);
|
||||
fetchMock.get(mockErrorUrl, () => Promise.reject(mockErrorPayload));
|
||||
|
||||
afterEach(fetchMock.reset);
|
||||
|
||||
describe('request config', () => {
|
||||
it('calls the right url with the specified method', done => {
|
||||
expect.assertions(2);
|
||||
|
||||
Promise.all([
|
||||
callApi({ url: mockGetUrl, method: 'GET' }),
|
||||
callApi({ url: mockPostUrl, method: 'POST' }),
|
||||
])
|
||||
.then(() => {
|
||||
expect(fetchMock.calls(mockGetUrl)).toHaveLength(1);
|
||||
expect(fetchMock.calls(mockPostUrl)).toHaveLength(1);
|
||||
|
||||
return done();
|
||||
})
|
||||
.catch(throwIfCalled);
|
||||
});
|
||||
|
||||
it('passes along mode, cache, credentials, headers, body, signal, and redirect parameters in the request', done => {
|
||||
expect.assertions(8);
|
||||
|
||||
const mockRequest = {
|
||||
url: mockGetUrl,
|
||||
mode: 'my-mode',
|
||||
cache: 'cash money',
|
||||
credentials: 'mad cred',
|
||||
headers: {
|
||||
custom: 'header',
|
||||
},
|
||||
redirect: 'no thanks',
|
||||
signal: () => {},
|
||||
body: 'BODY',
|
||||
};
|
||||
|
||||
callApi(mockRequest)
|
||||
.then(() => {
|
||||
const calls = fetchMock.calls(mockGetUrl);
|
||||
const fetchParams = calls[0][1];
|
||||
expect(calls).toHaveLength(1);
|
||||
expect(fetchParams.mode).toBe(mockRequest.mode);
|
||||
expect(fetchParams.cache).toBe(mockRequest.cache);
|
||||
expect(fetchParams.credentials).toBe(mockRequest.credentials);
|
||||
expect(fetchParams.headers).toEqual(expect.objectContaining(mockRequest.headers));
|
||||
expect(fetchParams.redirect).toBe(mockRequest.redirect);
|
||||
expect(fetchParams.signal).toBe(mockRequest.signal);
|
||||
expect(fetchParams.body).toBe(mockRequest.body);
|
||||
|
||||
return done();
|
||||
})
|
||||
.catch(throwIfCalled);
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST requests', () => {
|
||||
it('encodes key,value pairs from postPayload', done => {
|
||||
expect.assertions(3);
|
||||
const postPayload = { key: 'value', anotherKey: 1237 };
|
||||
|
||||
callApi({ url: mockPostUrl, method: 'POST', postPayload })
|
||||
.then(() => {
|
||||
const calls = fetchMock.calls(mockPostUrl);
|
||||
expect(calls).toHaveLength(1);
|
||||
|
||||
const fetchParams = calls[0][1];
|
||||
const { body } = fetchParams;
|
||||
|
||||
Object.keys(postPayload).forEach(key => {
|
||||
expect(body.get(key)).toBe(JSON.stringify(postPayload[key]));
|
||||
});
|
||||
|
||||
return done();
|
||||
})
|
||||
.catch(throwIfCalled);
|
||||
});
|
||||
|
||||
// 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', done => {
|
||||
expect.assertions(3);
|
||||
const postPayload = { key: 'value', noValue: undefined };
|
||||
|
||||
callApi({ url: mockPostUrl, method: 'POST', postPayload })
|
||||
.then(() => {
|
||||
const calls = fetchMock.calls(mockPostUrl);
|
||||
expect(calls).toHaveLength(1);
|
||||
|
||||
const fetchParams = calls[0][1];
|
||||
const { body } = fetchParams;
|
||||
expect(body.get('key')).toBe(JSON.stringify(postPayload.key));
|
||||
expect(body.get('noValue')).toBeNull();
|
||||
|
||||
return done();
|
||||
})
|
||||
.catch(throwIfCalled);
|
||||
});
|
||||
|
||||
it('respects the stringify flag in POST requests', done => {
|
||||
const postPayload = {
|
||||
string: 'value',
|
||||
number: 1237,
|
||||
array: [1, 2, 3],
|
||||
object: { a: 'a', 1: 1 },
|
||||
null: null,
|
||||
emptyString: '',
|
||||
};
|
||||
|
||||
expect.assertions(1 + 2 * Object.keys(postPayload).length);
|
||||
|
||||
Promise.all([
|
||||
callApi({ url: mockPostUrl, method: 'POST', postPayload }),
|
||||
callApi({ url: mockPostUrl, method: 'POST', postPayload, stringify: false }),
|
||||
])
|
||||
.then(() => {
|
||||
const calls = fetchMock.calls(mockPostUrl);
|
||||
expect(calls).toHaveLength(2);
|
||||
|
||||
const stringified = calls[0][1].body;
|
||||
const unstringified = calls[1][1].body;
|
||||
|
||||
Object.keys(postPayload).forEach(key => {
|
||||
expect(stringified.get(key)).toBe(JSON.stringify(postPayload[key]));
|
||||
expect(unstringified.get(key)).toBe(String(postPayload[key]));
|
||||
});
|
||||
|
||||
return done();
|
||||
})
|
||||
.catch(throwIfCalled);
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,102 @@
|
||||
/* eslint promise/no-callback-in-promise: 'off' */
|
||||
import fetchMock from 'fetch-mock';
|
||||
|
||||
import callApiAndParseWithTimeout from '../../src/callApi/callApiAndParseWithTimeout';
|
||||
|
||||
// we import these via * so that we can spy on the 'default' property of the object
|
||||
import * as callApi from '../../src/callApi/callApi';
|
||||
import * as parseResponse from '../../src/callApi/parseResponse';
|
||||
import * as rejectAfterTimeout from '../../src/callApi/rejectAfterTimeout';
|
||||
|
||||
import { LOGIN_GLOB } from '../fixtures/constants';
|
||||
import throwIfCalled from '../utils/throwIfCalled';
|
||||
|
||||
describe('callApiAndParseWithTimeout()', () => {
|
||||
beforeAll(() => {
|
||||
fetchMock.get(LOGIN_GLOB, { csrf_token: '1234' });
|
||||
});
|
||||
|
||||
afterAll(fetchMock.restore);
|
||||
|
||||
const mockGetUrl = '/mock/get/url';
|
||||
const mockGetPayload = { get: 'payload' };
|
||||
fetchMock.get(mockGetUrl, mockGetPayload);
|
||||
|
||||
afterEach(fetchMock.reset);
|
||||
|
||||
describe('callApi', () => {
|
||||
it('calls callApi()', () => {
|
||||
const callApiSpy = jest.spyOn(callApi, 'default');
|
||||
callApiAndParseWithTimeout({ url: mockGetUrl, method: 'GET' });
|
||||
|
||||
expect(callApiSpy).toHaveBeenCalledTimes(1);
|
||||
callApiSpy.mockClear();
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseResponse', () => {
|
||||
it('calls parseResponse()', () => {
|
||||
const parseSpy = jest.spyOn(parseResponse, 'default');
|
||||
callApiAndParseWithTimeout({ url: mockGetUrl, method: 'GET' });
|
||||
|
||||
expect(parseSpy).toHaveBeenCalledTimes(1);
|
||||
parseSpy.mockClear();
|
||||
});
|
||||
});
|
||||
|
||||
describe('timeout', () => {
|
||||
it('does not create a rejection timer if no timeout passed', () => {
|
||||
const rejectionSpy = jest.spyOn(rejectAfterTimeout, 'default');
|
||||
callApiAndParseWithTimeout({ url: mockGetUrl, method: 'GET' });
|
||||
|
||||
expect(rejectionSpy).toHaveBeenCalledTimes(0);
|
||||
rejectionSpy.mockClear();
|
||||
});
|
||||
|
||||
it('creates a rejection timer if a timeout passed', () => {
|
||||
jest.useFakeTimers(); // prevents the timeout from rejecting + failing test
|
||||
const rejectionSpy = jest.spyOn(rejectAfterTimeout, 'default');
|
||||
callApiAndParseWithTimeout({ url: mockGetUrl, method: 'GET', timeout: 10 });
|
||||
|
||||
expect(rejectionSpy).toHaveBeenCalledTimes(1);
|
||||
rejectionSpy.mockClear();
|
||||
});
|
||||
|
||||
it('rejects if the request exceeds the timeout', done => {
|
||||
expect.assertions(4);
|
||||
jest.useFakeTimers();
|
||||
|
||||
const mockTimeoutUrl = '/mock/timeout/url';
|
||||
const unresolvingPromise = new Promise(() => {});
|
||||
fetchMock.get(mockTimeoutUrl, () => unresolvingPromise);
|
||||
|
||||
callApiAndParseWithTimeout({ url: mockTimeoutUrl, method: 'GET', timeout: 1 })
|
||||
.then(throwIfCalled)
|
||||
.catch(timeoutError => {
|
||||
expect(setTimeout).toHaveBeenCalledTimes(1);
|
||||
expect(fetchMock.calls(mockTimeoutUrl)).toHaveLength(1);
|
||||
expect(Object.keys(timeoutError)).toEqual(
|
||||
expect.arrayContaining(['error', 'statusText']),
|
||||
);
|
||||
expect(timeoutError.statusText).toBe('timeout');
|
||||
|
||||
return done();
|
||||
});
|
||||
|
||||
jest.runOnlyPendingTimers();
|
||||
});
|
||||
|
||||
it('resolves if the request does not exceed the timeout', done => {
|
||||
expect.assertions(1);
|
||||
jest.useFakeTimers();
|
||||
|
||||
callApiAndParseWithTimeout({ url: mockGetUrl, method: 'GET', timeout: 100 })
|
||||
.then(response => {
|
||||
expect(response.json).toEqual(expect.objectContaining(mockGetPayload));
|
||||
|
||||
return done();
|
||||
})
|
||||
.catch(throwIfCalled);
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,84 @@
|
||||
/* eslint promise/no-callback-in-promise: 'off' */
|
||||
import fetchMock from 'fetch-mock';
|
||||
import callApi from '../../src/callApi/callApi';
|
||||
import parseResponse from '../../src/callApi/parseResponse';
|
||||
|
||||
import { LOGIN_GLOB } from '../fixtures/constants';
|
||||
import throwIfCalled from '../utils/throwIfCalled';
|
||||
|
||||
describe('parseResponse()', () => {
|
||||
beforeAll(() => {
|
||||
fetchMock.get(LOGIN_GLOB, { csrf_token: '1234' });
|
||||
});
|
||||
|
||||
afterAll(fetchMock.restore);
|
||||
|
||||
const mockGetUrl = '/mock/get/url';
|
||||
const mockPostUrl = '/mock/post/url';
|
||||
const mockErrorUrl = '/mock/error/url';
|
||||
|
||||
const mockGetPayload = { get: 'payload' };
|
||||
const mockPostPayload = { post: 'payload' };
|
||||
const mockErrorPayload = { status: 500, statusText: 'Internal error' };
|
||||
|
||||
fetchMock.get(mockGetUrl, mockGetPayload);
|
||||
fetchMock.post(mockPostUrl, mockPostPayload);
|
||||
fetchMock.get(mockErrorUrl, () => Promise.reject(mockErrorPayload));
|
||||
|
||||
afterEach(fetchMock.reset);
|
||||
|
||||
it('returns a Promise', () => {
|
||||
const apiPromise = callApi({ url: mockGetUrl, method: 'GET' });
|
||||
const parsedResponsePromise = parseResponse(apiPromise);
|
||||
expect(parsedResponsePromise).toEqual(expect.any(Promise));
|
||||
});
|
||||
|
||||
it('resolves to { json, response } if the request succeeds', done => {
|
||||
expect.assertions(3);
|
||||
const apiPromise = callApi({ url: mockGetUrl, method: 'GET' });
|
||||
|
||||
parseResponse(apiPromise)
|
||||
.then(args => {
|
||||
expect(fetchMock.calls(mockGetUrl)).toHaveLength(1);
|
||||
expect(Object.keys(args)).toEqual(expect.arrayContaining(['response', 'json']));
|
||||
expect(args.json).toEqual(expect.objectContaining(mockGetPayload));
|
||||
|
||||
return done();
|
||||
})
|
||||
.catch(throwIfCalled);
|
||||
});
|
||||
|
||||
it('resolves to { text, response } if the request succeeds with text response', done => {
|
||||
expect.assertions(3);
|
||||
|
||||
const mockTextUrl = '/mock/text/url';
|
||||
const mockTextResponse =
|
||||
'<html><head></head><body>I could be a stack trace or something</body></html>';
|
||||
fetchMock.get(mockTextUrl, mockTextResponse);
|
||||
|
||||
const apiPromise = callApi({ url: mockTextUrl, method: 'GET' });
|
||||
parseResponse(apiPromise)
|
||||
.then(args => {
|
||||
expect(fetchMock.calls(mockTextUrl)).toHaveLength(1);
|
||||
expect(Object.keys(args)).toEqual(expect.arrayContaining(['response', 'text']));
|
||||
expect(args.text).toBe(mockTextResponse);
|
||||
|
||||
return done();
|
||||
})
|
||||
.catch(throwIfCalled);
|
||||
});
|
||||
|
||||
it('rejects if the request throws', done => {
|
||||
expect.assertions(3);
|
||||
|
||||
callApi({ url: mockErrorUrl, method: 'GET' })
|
||||
.then(throwIfCalled)
|
||||
.catch(error => {
|
||||
expect(fetchMock.calls(mockErrorUrl)).toHaveLength(1);
|
||||
expect(error.status).toBe(mockErrorPayload.status);
|
||||
expect(error.statusText).toBe(mockErrorPayload.statusText);
|
||||
|
||||
return done();
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,20 @@
|
||||
/* eslint promise/no-callback-in-promise: 'off' */
|
||||
import rejectAfterTimeout from '../../src/callApi/rejectAfterTimeout';
|
||||
import throwIfCalled from '../utils/throwIfCalled';
|
||||
|
||||
describe('rejectAfterTimeout()', () => {
|
||||
it('returns a promise that rejects after the specified timeout', done => {
|
||||
expect.assertions(1);
|
||||
jest.useFakeTimers();
|
||||
|
||||
rejectAfterTimeout(10)
|
||||
.then(throwIfCalled)
|
||||
.catch(() => {
|
||||
expect(setTimeout).toHaveBeenCalledTimes(1);
|
||||
|
||||
return done();
|
||||
});
|
||||
|
||||
jest.runOnlyPendingTimers();
|
||||
});
|
||||
});
|
@ -0,0 +1 @@
|
||||
export const LOGIN_GLOB = 'glob:*superset/csrf_token/*'; // eslint-disable-line import/prefer-default-export
|
@ -0,0 +1,3 @@
|
||||
export default function throwIfCalled() {
|
||||
throw new Error('Unexpected call to throwIfCalled()');
|
||||
}
|
Loading…
Reference in New Issue
Block a user