redux-api-middleware
Version:
Redux middleware for calling an API.
896 lines (874 loc) • 23.9 kB
JavaScript
function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); keys.push.apply(keys, symbols); } return keys; }
function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(Object(source), true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; }
function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
function _objectWithoutProperties(source, excluded) { if (source == null) return {}; var target = _objectWithoutPropertiesLoose(source, excluded); var key, i; if (Object.getOwnPropertySymbols) { var sourceSymbolKeys = Object.getOwnPropertySymbols(source); for (i = 0; i < sourceSymbolKeys.length; i++) { key = sourceSymbolKeys[i]; if (excluded.indexOf(key) >= 0) continue; if (!Object.prototype.propertyIsEnumerable.call(source, key)) continue; target[key] = source[key]; } } return target; }
function _objectWithoutPropertiesLoose(source, excluded) { if (source == null) return {}; var target = {}; var sourceKeys = Object.keys(source); var key, i; for (i = 0; i < sourceKeys.length; i++) { key = sourceKeys[i]; if (excluded.indexOf(key) >= 0) continue; target[key] = source[key]; } return target; }
// Public package exports
import { RSAA, apiMiddleware, createMiddleware, InternalError } from 'redux-api-middleware';
const fetchMockSnapshotMatcher = {
invocationCallOrder: expect.any(Object)
}; // const fetchMockSnapshotMatcher = {};
const doTestMiddleware = async ({
response,
action
}) => {
if (response) {
const {
body
} = response,
mockConfig = _objectWithoutProperties(response, ["body"]);
fetch.mockResponseOnce(body, mockConfig);
}
const doGetState = jest.fn();
doGetState.mockImplementation(() => {});
const doNext = jest.fn();
doNext.mockImplementation(it => it);
const nextHandler = apiMiddleware({
getState: doGetState
});
const actionHandler = nextHandler(doNext);
const result = actionHandler(action);
if (result) {
const final = await result;
if (final) {
expect(final).toMatchSnapshot({}, 'final result');
}
}
if (doNext.mock.calls.length) {
expect(doNext).toMatchSnapshot({}, 'next mock');
}
if (fetch.mock.calls.length) {
expect(fetch.mock).toMatchSnapshot({
invocationCallOrder: expect.any(Object)
}, 'fetch mock');
}
return {
doGetState,
nextHandler,
doNext,
actionHandler,
result
};
};
describe('#createMiddleware', () => {
it('returns a redux middleware', () => {
const doGetState = () => {};
const middleware = createMiddleware();
const nextHandler = middleware({
getState: doGetState
});
const doNext = () => {};
const actionHandler = nextHandler(doNext);
expect(typeof middleware).toEqual('function');
expect(middleware).toHaveLength(1);
expect(typeof nextHandler).toEqual('function');
expect(nextHandler).toHaveLength(1);
expect(typeof actionHandler).toEqual('function');
expect(actionHandler).toHaveLength(1);
});
});
describe('#apiMiddleware', () => {
it('is a redux middleware', () => {
const doGetState = () => {};
const nextHandler = apiMiddleware({
getState: doGetState
});
const doNext = () => {};
const actionHandler = nextHandler(doNext);
expect(typeof apiMiddleware).toEqual('function');
expect(apiMiddleware).toHaveLength(1);
expect(typeof nextHandler).toEqual('function');
expect(nextHandler).toHaveLength(1);
expect(typeof actionHandler).toEqual('function');
expect(actionHandler).toHaveLength(1);
});
it('must pass actions without an [RSAA] property to the next handler', async () => {
const action = {};
const {
doNext
} = await doTestMiddleware({
action
});
expect(doNext).toHaveBeenCalledWith(action);
});
it("mustn't return a promise on actions without a [RSAA] property", async () => {
const action = {};
const {
result
} = await doTestMiddleware({
action
});
expect(result.then).toBeUndefined();
});
it('must return a promise on actions without a [RSAA] property', async () => {
const action = {
[RSAA]: {}
};
const {
result
} = await doTestMiddleware({
action
});
expect(typeof result.then).toEqual('function');
});
it('must dispatch an error request FSA for an invalid RSAA with a string request type', async () => {
const action = {
[RSAA]: {
types: ['REQUEST']
}
};
await doTestMiddleware({
action
});
});
it('must dispatch an error request FSA for an invalid RSAA with a descriptor request type', async () => {
const action = {
[RSAA]: {
types: [{
type: 'REQUEST'
}]
}
};
await doTestMiddleware({
action
});
});
it('must do nothing for an invalid RSAA without a request type', async () => {
const action = {
[RSAA]: {}
};
const {
doNext
} = await doTestMiddleware({
action
});
expect(doNext).not.toHaveBeenCalled();
});
it('must dispatch an error request FSA when [RSAA].bailout fails', async () => {
const action = {
[RSAA]: {
endpoint: '',
method: 'GET',
bailout: () => {
throw new Error();
},
types: [{
type: 'REQUEST',
payload: () => 'ignoredPayload',
meta: () => 'someMeta'
}, 'SUCCESS', 'FAILURE']
}
};
await doTestMiddleware({
action
});
});
it('must dispatch an error request FSA when [RSAA].body fails', async () => {
const action = {
[RSAA]: {
endpoint: 'http://127.0.0.1/api/users/1',
body: () => {
throw new Error();
},
method: 'GET',
types: [{
type: 'REQUEST',
payload: 'ignoredPayload',
meta: 'someMeta'
}, 'SUCCESS', 'FAILURE']
}
};
await doTestMiddleware({
action
});
});
it('must dispatch an error request FSA when [RSAA].endpoint fails', async () => {
const action = {
[RSAA]: {
endpoint: () => {
throw new Error();
},
method: 'GET',
types: [{
type: 'REQUEST',
payload: 'ignoredPayload',
meta: 'someMeta'
}, 'SUCCESS', 'FAILURE']
}
};
await doTestMiddleware({
action
});
});
it('must dispatch an error request FSA when [RSAA].headers fails', async () => {
const action = {
[RSAA]: {
endpoint: '',
method: 'GET',
headers: () => {
throw new Error();
},
types: [{
type: 'REQUEST',
payload: 'ignoredPayload',
meta: 'someMeta'
}, 'SUCCESS', 'FAILURE']
}
};
await doTestMiddleware({
action
});
});
it('must dispatch an error request FSA when [RSAA].options fails', async () => {
const action = {
[RSAA]: {
endpoint: '',
method: 'GET',
options: () => {
throw new Error();
},
types: [{
type: 'REQUEST',
payload: 'ignoredPayload',
meta: 'someMeta'
}, 'SUCCESS', 'FAILURE']
}
};
await doTestMiddleware({
action
});
});
it('must dispatch an error request FSA when [RSAA].ok fails', async () => {
const action = {
[RSAA]: {
endpoint: 'http://127.0.0.1/api/users/1',
method: 'GET',
ok: () => {
throw new Error();
},
types: ['REQUEST', 'SUCCESS', 'FAILURE']
}
};
await doTestMiddleware({
action,
response: {
body: JSON.stringify({
data: '12345'
}),
status: 200,
headers: {
'Content-Type': 'application/json'
}
}
});
});
it('must dispatch a failure FSA with an error on a request error', async () => {
fetch.mockRejectOnce(new Error('Test request error'));
const action = {
[RSAA]: {
endpoint: 'http://127.0.0.1/api/users/1',
method: 'GET',
types: [{
type: 'REQUEST',
payload: 'ignoredPayload',
meta: 'someMeta'
}, 'SUCCESS', 'FAILURE']
}
};
await doTestMiddleware({
action
});
});
it('must use an [RSAA].bailout boolean when present', async () => {
const action = {
[RSAA]: {
endpoint: 'http://127.0.0.1/api/users/1',
method: 'GET',
types: ['REQUEST', 'SUCCESS', 'FAILURE'],
bailout: true
}
};
await doTestMiddleware({
action
});
});
it('must use an [RSAA].bailout function when present', async () => {
const bailout = jest.fn();
bailout.mockReturnValue(true);
const action = {
[RSAA]: {
endpoint: 'http://127.0.0.1/api/users/1',
method: 'GET',
types: ['REQUEST', 'SUCCESS', 'FAILURE'],
bailout
}
};
const {
doNext
} = await doTestMiddleware({
action,
response: {
body: JSON.stringify({
data: '12345'
}),
status: 200,
headers: {
'Content-Type': 'application/json'
}
}
});
expect(bailout).toMatchSnapshot({}, 'bailout()');
expect(doNext).not.toHaveBeenCalled();
});
it('must use an [RSAA].body function when present', async () => {
const body = jest.fn();
body.mockReturnValue('mockBody');
const action = {
[RSAA]: {
endpoint: 'http://127.0.0.1/api/users/1',
method: 'GET',
types: ['REQUEST', 'SUCCESS', 'FAILURE'],
body
}
};
await doTestMiddleware({
action,
response: {
body: JSON.stringify({
data: '12345'
}),
status: 200,
headers: {
'Content-Type': 'application/json'
}
}
});
expect(body).toMatchSnapshot({}, 'body()');
});
it('must use an async [RSAA].body function when present', async () => {
const body = jest.fn();
body.mockResolvedValue('mockBody');
const action = {
[RSAA]: {
endpoint: 'http://127.0.0.1/api/users/1',
method: 'GET',
types: ['REQUEST', 'SUCCESS', 'FAILURE'],
body
}
};
await doTestMiddleware({
action,
response: {
body: JSON.stringify({
data: '12345'
}),
status: 200,
headers: {
'Content-Type': 'application/json'
}
}
});
expect(body).toMatchSnapshot({}, 'body()');
});
it('must use an [RSAA].endpoint function when present', async () => {
const endpoint = jest.fn();
endpoint.mockReturnValue('http://127.0.0.1/api/users/1');
const action = {
[RSAA]: {
endpoint,
method: 'GET',
types: ['REQUEST', 'SUCCESS', 'FAILURE']
}
};
await doTestMiddleware({
action,
response: {
body: JSON.stringify({
data: '12345'
}),
status: 200,
headers: {
'Content-Type': 'application/json'
}
}
});
expect(endpoint).toMatchSnapshot({}, 'endpoint()');
});
it('must use an async [RSAA].endpoint function when present', async () => {
const endpoint = jest.fn();
endpoint.mockResolvedValue('http://127.0.0.1/api/users/1');
const action = {
[RSAA]: {
endpoint,
method: 'GET',
types: ['REQUEST', 'SUCCESS', 'FAILURE']
}
};
await doTestMiddleware({
action,
response: {
body: JSON.stringify({
data: '12345'
}),
status: 200,
headers: {
'Content-Type': 'application/json'
}
}
});
expect(endpoint).toMatchSnapshot({}, 'endpoint()');
});
it('must use an [RSAA].headers function when present', async () => {
const headers = jest.fn();
headers.mockReturnValue({
'Test-Header': 'test'
});
const action = {
[RSAA]: {
endpoint: 'http://127.0.0.1/api/users/1',
method: 'GET',
types: ['REQUEST', 'SUCCESS', 'FAILURE'],
headers
}
};
await doTestMiddleware({
action,
response: {
body: JSON.stringify({
data: '12345'
}),
status: 200,
headers: {
'Content-Type': 'application/json'
}
}
});
expect(headers).toMatchSnapshot({}, 'headers()');
});
it('must use an async [RSAA].headers function when present', async () => {
const headers = jest.fn();
headers.mockResolvedValue({
'Test-Header': 'test'
});
const action = {
[RSAA]: {
endpoint: 'http://127.0.0.1/api/users/1',
method: 'GET',
types: ['REQUEST', 'SUCCESS', 'FAILURE'],
headers
}
};
await doTestMiddleware({
action,
response: {
body: JSON.stringify({
data: '12345'
}),
status: 200,
headers: {
'Content-Type': 'application/json'
}
}
});
expect(headers).toMatchSnapshot({}, 'headers()');
});
it('must use an [RSAA].options function when present', async () => {
const options = jest.fn();
options.mockReturnValue({});
const action = {
[RSAA]: {
endpoint: 'http://127.0.0.1/api/users/1',
method: 'GET',
types: ['REQUEST', 'SUCCESS', 'FAILURE'],
options
}
};
await doTestMiddleware({
action,
response: {
body: JSON.stringify({
data: '12345'
}),
status: 200,
headers: {
'Content-Type': 'application/json'
}
}
});
expect(options).toMatchSnapshot({}, 'options()');
});
it('must use an async [RSAA].options function when present', async () => {
const options = jest.fn();
options.mockResolvedValue({});
const action = {
[RSAA]: {
endpoint: 'http://127.0.0.1/api/users/1',
method: 'GET',
types: ['REQUEST', 'SUCCESS', 'FAILURE'],
options
}
};
await doTestMiddleware({
action,
response: {
body: JSON.stringify({
data: '12345'
}),
status: 200,
headers: {
'Content-Type': 'application/json'
}
}
});
expect(options).toMatchSnapshot({}, 'options()');
});
it('must use an [RSAA].ok function when present', async () => {
const ok = jest.fn();
ok.mockReturnValue(true);
const action = {
[RSAA]: {
endpoint: 'http://127.0.0.1/api/users/1',
method: 'GET',
types: ['REQUEST', 'SUCCESS', 'FAILURE'],
ok
}
};
await doTestMiddleware({
action,
response: {
body: JSON.stringify({
data: '12345'
}),
status: 200,
headers: {
'Content-Type': 'application/json'
}
}
});
expect(ok).toMatchSnapshot({}, 'ok()');
});
it('must dispatch a failure FSA when [RSAA].ok returns false on a successful request', async () => {
const ok = jest.fn();
ok.mockReturnValue(false);
const action = {
[RSAA]: {
endpoint: 'http://127.0.0.1/api/users/1',
method: 'GET',
types: ['REQUEST', 'SUCCESS', 'FAILURE'],
ok
}
};
await doTestMiddleware({
action,
response: {
body: JSON.stringify({
data: '12345'
}),
status: 200,
headers: {
'Content-Type': 'application/json'
}
}
});
expect(ok).toMatchSnapshot({}, 'ok()');
});
it('must use a [RSAA].fetch custom fetch wrapper when present', async () => {
const myFetch = async (endpoint, opts) => {
const res = await fetch(endpoint, opts);
const json = await res.json();
return new Response(JSON.stringify(_objectSpread({}, json, {
foo: 'bar'
})), {
// Example of custom `res.ok`
status: json.error ? 500 : 200,
headers: {
'Content-Type': 'application/json'
}
});
};
const action = {
[RSAA]: {
endpoint: 'http://127.0.0.1/api/users/1',
method: 'GET',
types: ['REQUEST', 'SUCCESS', 'FAILURE'],
fetch: myFetch
}
};
await doTestMiddleware({
action,
response: {
body: JSON.stringify({
id: 1,
name: 'Alan',
error: false
}),
status: 200,
headers: {
'Content-Type': 'application/json'
}
}
});
});
it('must dispatch correct error payload when [RSAA].fetch wrapper returns an error response', async () => {
const myFetch = async (endpoint, opts) => {
return new Response(JSON.stringify({
foo: 'bar'
}), {
status: 500,
headers: {
'Content-Type': 'application/json'
}
});
};
const action = {
[RSAA]: {
endpoint: 'http://127.0.0.1/api/users/1',
method: 'GET',
types: ['REQUEST', 'SUCCESS', 'FAILURE'],
fetch: myFetch
}
};
await doTestMiddleware({
action
});
});
it('must use payload property of request type descriptor when it is a function', async () => {
const payload = jest.fn();
payload.mockReturnValue('requestPayload');
const action = {
[RSAA]: {
endpoint: 'http://127.0.0.1/api/users/1',
method: 'GET',
types: [{
type: 'REQUEST',
meta: 'requestMeta',
payload
}, 'SUCCESS', 'FAILURE']
}
};
await doTestMiddleware({
action,
response: {
body: JSON.stringify({
data: '12345'
}),
status: 200,
headers: {
'Content-Type': 'application/json'
}
}
});
expect(payload).toMatchSnapshot({}, 'payload()');
});
it('must use meta property of request type descriptor when it is a function', async () => {
const meta = jest.fn();
meta.mockReturnValue('requestMeta');
const action = {
[RSAA]: {
endpoint: 'http://127.0.0.1/api/users/1',
method: 'GET',
types: [{
type: 'REQUEST',
meta,
payload: 'requestPayload'
}, 'SUCCESS', 'FAILURE']
}
};
await doTestMiddleware({
action,
response: {
body: JSON.stringify({
data: '12345'
}),
status: 200,
headers: {
'Content-Type': 'application/json'
}
}
});
expect(meta).toMatchSnapshot({}, 'meta()');
});
it('must dispatch a success FSA on a successful API call with a non-empty JSON response', async () => {
const action = {
[RSAA]: {
endpoint: 'http://127.0.0.1/api/users/1',
method: 'GET',
types: [{
type: 'REQUEST',
payload: 'requestPayload',
meta: 'requestMeta'
}, {
type: 'SUCCESS',
meta: 'successMeta'
}, 'FAILURE']
}
};
await doTestMiddleware({
action,
response: {
body: JSON.stringify({
username: 'Alice'
}),
status: 200,
headers: {
'Content-Type': 'application/json'
}
}
});
});
it('must dispatch a success FSA on a successful API call with an empty JSON response', async () => {
const action = {
[RSAA]: {
endpoint: 'http://127.0.0.1/api/users/1',
method: 'GET',
types: [{
type: 'REQUEST',
payload: 'requestPayload',
meta: 'requestMeta'
}, {
type: 'SUCCESS',
meta: 'successMeta'
}, 'FAILURE']
}
};
await doTestMiddleware({
action,
response: {
body: JSON.stringify({}),
status: 200,
headers: {
'Content-Type': 'application/json'
}
}
});
});
it('must dispatch a success FSA with an error state on a successful API call with an invalid JSON response', async () => {
const action = {
[RSAA]: {
endpoint: 'http://127.0.0.1/api/users/1',
method: 'GET',
types: [{
type: 'REQUEST',
payload: 'requestPayload',
meta: 'requestMeta'
}, {
type: 'SUCCESS',
meta: 'successMeta',
payload: () => {
throw new InternalError('Expected error - simulating invalid JSON');
}
}, 'FAILURE']
}
};
await doTestMiddleware({
action,
response: {
body: '',
status: 200,
headers: {
'Content-Type': 'application/json'
}
}
});
});
it('must dispatch a success FSA on a successful API call with a non-JSON response', async () => {
const action = {
[RSAA]: {
endpoint: 'http://127.0.0.1/api/users/1',
method: 'GET',
types: [{
type: 'REQUEST',
payload: 'requestPayload',
meta: 'requestMeta'
}, {
type: 'SUCCESS',
meta: 'successMeta'
}, 'FAILURE']
}
};
await doTestMiddleware({
action,
response: {
body: null,
status: 200
}
});
});
it('must dispatch a failure FSA on an unsuccessful API call with a non-empty JSON response', async () => {
const action = {
[RSAA]: {
endpoint: 'http://127.0.0.1/api/users/1',
method: 'GET',
types: [{
type: 'REQUEST',
payload: 'requestPayload',
meta: 'requestMeta'
}, 'SUCCESS', {
type: 'FAILURE',
meta: 'failureMeta'
}]
}
};
await doTestMiddleware({
action,
response: {
body: JSON.stringify({
error: 'Resource not found'
}),
status: 404,
headers: {
'Content-Type': 'application/json'
}
}
});
});
it('must dispatch a failure FSA on an unsuccessful API call with an empty JSON response', async () => {
const action = {
[RSAA]: {
endpoint: 'http://127.0.0.1/api/users/1',
method: 'GET',
types: [{
type: 'REQUEST',
payload: 'requestPayload',
meta: 'requestMeta'
}, 'SUCCESS', {
type: 'FAILURE',
meta: 'failureMeta'
}]
}
};
await doTestMiddleware({
action,
response: {
body: JSON.stringify({}),
status: 404,
headers: {
'Content-Type': 'application/json'
}
}
});
});
it('must dispatch a failure FSA on an unsuccessful API call with a non-JSON response', async () => {
const action = {
[RSAA]: {
endpoint: 'http://127.0.0.1/api/users/1',
method: 'GET',
types: [{
type: 'REQUEST',
payload: 'requestPayload',
meta: 'requestMeta'
}, 'SUCCESS', {
type: 'FAILURE',
meta: 'failureMeta'
}]
}
};
await doTestMiddleware({
action,
response: {
body: '',
status: 404
}
});
});
});