Skip to content

Commit 323e95a

Browse files
committed
test: added tests for utils and callAPIMethod
1 parent dc50e4a commit 323e95a

File tree

6 files changed

+209
-46
lines changed

6 files changed

+209
-46
lines changed

src/callAPIMethod.js

Lines changed: 11 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
import { format as formatURL } from 'url';
2-
import { uri } from 'unity-utils';
31
import 'isomorphic-fetch';
2+
import { formatURL } from './utils';
43
import APIError from './error';
54

65
const defaults = {
@@ -14,23 +13,16 @@ const defaults = {
1413
}
1514
};
1615

17-
export function getAPIPrefix(APINamespace) {
18-
const reAbsolute = /^(\/|https?:\/\/)/ig;
19-
return reAbsolute.test(APINamespace) ? APINamespace : '/' + APINamespace;
20-
}
16+
export default function callAPI(
17+
APINamespace = defaults.APINamespace,
18+
fetchOptions = defaults.fetchOptions,
19+
namespace = '',
20+
methodOptions = {}
21+
) {
2122

22-
export function getFullPath(APINamespace, namespace, path=[]) {
23-
path = [].concat(path);
24-
return uri.join(getAPIPrefix(APINamespace), namespace, ...path).replace(':/', '://');
25-
}
23+
const { path=[], query={}, options={}, method='json' } = methodOptions;
2624

27-
function callAPI(APINamespace, fetchOptions, namespace = '', { path=[], query={}, options={}, method='json' }) {
28-
29-
APINamespace = APINamespace || defaults.APINamespace;
30-
31-
query = uri.query(query);
32-
33-
const url = formatURL({ query, pathname: getFullPath(APINamespace, namespace, path) });
25+
const url = formatURL(APINamespace, namespace, path, query);
3426

3527
return fetch(url, {...defaults.fetchOptions, ...fetchOptions, ...options})
3628
.then( result => {
@@ -41,7 +33,5 @@ function callAPI(APINamespace, fetchOptions, namespace = '', { path=[], query={}
4133

4234
return result[method]();
4335
})
44-
.catch( error => (error instanceof Error ? error : new Error(error)));
45-
}
46-
47-
export default callAPI;
36+
.catch( error => error);
37+
}

src/createAPI.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ const createAPI = (resources = {}, middleware = [], APINamespace = '', fetchOpti
88
api[resourceId] = Object.keys(resources[resourceId].methods).reduce( (resource, method) => {
99
resource[method] = (params, methodOptions) => {
1010
const apiParams = resources[resourceId].methods[method](params);
11-
const bindedCallAPIMethod = callAPIMethod.bind(null, APINamespace, fetchOptions, (resources[resourceId].namespace || resources[resourceId].prefix));
12-
return applyMiddleware(bindedCallAPIMethod, middleware, methodOptions, apiParams, resourceId, method);
11+
const boundCallAPIMethod = callAPIMethod.bind(null, APINamespace, fetchOptions, (resources[resourceId].namespace || resources[resourceId].prefix));
12+
return applyMiddleware(boundCallAPIMethod, middleware, methodOptions, apiParams, resourceId, method);
1313
};
1414
return resource;
1515
}, {});

src/utils.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { uri } from 'unity-utils';
2+
import { format } from 'url';
3+
4+
export function getAPIPrefix(APINamespace) {
5+
const reAbsolute = /^(\/|https?:\/\/)/ig;
6+
return reAbsolute.test(APINamespace) ? APINamespace : '/' + APINamespace;
7+
}
8+
9+
export function getFullPath(APINamespace, namespace, path=[]) {
10+
path = [].concat(path);
11+
return uri.join(getAPIPrefix(APINamespace), namespace, ...path).replace(':/', '://');
12+
}
13+
14+
export function formatURL(APINamespace = '', namespace = '', path=[], query={}) {
15+
return format({
16+
pathname: getFullPath(APINamespace, namespace, path),
17+
query: uri.query(query)
18+
});
19+
}

test/applyMiddlewate.spec.js

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import test from 'ava';
22
import sinon from 'sinon';
33
import applyMiddleware from '../src/applyMiddleware';
44

5+
const noop = _ => _;
6+
57
test('applyTo is required', t => {
68
t.throws(() => applyMiddleware());
79
});
@@ -12,16 +14,15 @@ test('applyTo is not a function', t => {
1214
});
1315

1416
test('no middleware returns default params', t => {
15-
const applyTo = a => a;
16-
const spyApplyTo = sinon.spy(applyTo);
17+
const spyApplyTo = sinon.spy(noop);
1718

1819
t.deepEqual(applyMiddleware(spyApplyTo), {});
1920
t.true(spyApplyTo.calledOnce);
2021
t.true(spyApplyTo.calledWithExactly({}));
2122
});
2223

2324
test('no functional middleware returns default params', t => {
24-
const applyTo = sinon.spy(a => a);
25+
const applyTo = sinon.spy(noop);
2526
const middlewares = [
2627
'test', undefined, {}, [], 123, null, NaN, /^/
2728
];
@@ -32,8 +33,8 @@ test('no functional middleware returns default params', t => {
3233
});
3334

3435
test('one middleware', t => {
35-
const applyTo = sinon.spy(a => a);
36-
const middleware = sinon.spy(a => a);
36+
const applyTo = sinon.spy(noop);
37+
const middleware = sinon.spy(noop);
3738

3839
const options = { options: 'options' };
3940
const params = { params: 'params' };
@@ -53,9 +54,9 @@ test('one middleware', t => {
5354
});
5455

5556
test('multiple middlewares', t => {
56-
const applyTo = sinon.spy(a => a);
57-
const middleware1 = sinon.spy(a => a);
58-
const middleware2 = sinon.spy(a => a);
57+
const applyTo = sinon.spy(noop);
58+
const middleware1 = sinon.spy(noop);
59+
const middleware2 = sinon.spy(noop);
5960

6061
const options = { options: 'options' };
6162
const params = { params: 'params' };

test/callAPIMethod.spec.js

Lines changed: 106 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,110 @@
11
import test from 'ava';
2-
import { getAPIPrefix, getFullPath } from '../src/callAPIMethod';
3-
4-
test('getAPIPrefix', t => {
5-
t.is(getAPIPrefix('api'), '/api', 'relative path');
6-
t.is(getAPIPrefix('/api'), '/api', 'absolute path');
7-
t.is(getAPIPrefix('http://api.example.com'), 'http://api.example.com', 'http url');
8-
t.is(getAPIPrefix('https://api.example.com'), 'https://api.example.com', 'http url');
9-
t.is(getAPIPrefix('//api.example.com'), '//api.example.com', 'protocol-less url');
10-
t.is(getAPIPrefix('https://localhost:8080'), 'https://localhost:8080', 'localhost');
2+
import sinon from 'sinon';
3+
import fetchMock from 'fetch-mock';
4+
import callAPIMethod from '../src/callAPIMethod';
5+
import APIError from '../src/error';
6+
7+
const matcher = '*';
8+
9+
const ResponseGood = new Response(
10+
JSON.stringify({ sample: 'data'}),
11+
{
12+
status: 200
13+
}
14+
);
15+
16+
const ResponseBad = new Response(null, {
17+
status: 500
18+
});
19+
20+
test.serial('default params', async t => {
21+
fetchMock.get(matcher, {});
22+
await callAPIMethod();
23+
24+
t.is(fetchMock.lastUrl(matcher), '/api', 'correct url');
25+
t.deepEqual(fetchMock.lastOptions(matcher), {
26+
cache: 'default',
27+
credentials: 'include',
28+
method: 'GET',
29+
mode: 'cors'
30+
}, 'correct options');
31+
32+
fetchMock.restore();
33+
});
34+
35+
test.serial('200 reponse', async t => {
36+
fetchMock.get(matcher, ResponseGood);
37+
const result = await callAPIMethod();
38+
39+
t.deepEqual(result, { sample: 'data' }, 'correct response body');
40+
41+
fetchMock.restore();
42+
});
43+
44+
45+
test.serial('500 reponse', async t => {
46+
fetchMock.get(matcher, ResponseBad);
47+
48+
const result = await callAPIMethod();
49+
50+
t.true(result instanceof Error, 'instance of Error');
51+
t.true(result instanceof APIError, 'instance of APIError');
52+
53+
fetchMock.restore();
54+
});
55+
56+
test.serial('fetch options', async t => {
57+
fetchMock.post(matcher, ResponseGood);
58+
59+
const APINamespace = 'rest-api';
60+
const namespace = 'user';
61+
const fetchOptions = { method: 'POST'};
62+
const methodOptions = {
63+
path: 'path',
64+
query: { edit: true },
65+
options: {
66+
credentials: 'omit'
67+
},
68+
method: 'text'
69+
};
70+
const spyResponseGood = sinon.spy(ResponseGood, 'text');
71+
72+
fetchMock.post(matcher, {});
73+
74+
await callAPIMethod(APINamespace, fetchOptions, namespace, methodOptions);
75+
76+
t.is(fetchMock.lastUrl(matcher), '/rest-api/user/path?edit=true', 'correct url');
77+
t.deepEqual(fetchMock.lastOptions(matcher), {
78+
cache: 'default',
79+
credentials: 'omit',
80+
method: 'POST',
81+
mode: 'cors'
82+
}), 'correct options';
83+
t.true(spyResponseGood.calledOnce);
84+
85+
spyResponseGood.restore();
86+
fetchMock.restore();
1187
});
1288

13-
test('getFullPath', t => {
14-
t.is(getFullPath('api'), '/api', 'relative path');
15-
t.is(getFullPath('/api'), '/api', 'absolute path');
16-
t.is(getFullPath('http://example.com', '/test'), 'http://example.com/test', 'url');
17-
t.is(getFullPath('http://example.com///', '/test/'), 'http://example.com/test/', 'url with slashes');
18-
t.is(getFullPath('https://localhost:8080', '/test///'), 'https://localhost:8080/test/', 'localhost');
89+
test.serial('unsupported Response method', async t => {
90+
fetchMock.get(matcher, ResponseGood);
91+
92+
const APINamespace = 'rest-api';
93+
const namespace = 'user';
94+
const fetchOptions = { method: 'POST'};
95+
const methodOptions = {
96+
path: 'path',
97+
options: {
98+
credentials: 'omit'
99+
},
100+
method: 'undefined'
101+
};
102+
103+
const result = await callAPIMethod(APINamespace, namespace, fetchOptions, methodOptions);
104+
105+
t.true(result instanceof Error);
106+
t.true(result instanceof TypeError);
107+
t.false(result instanceof APIError);
108+
109+
fetchMock.restore();
19110
});

test/utils.spec.js

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import test from 'ava';
2+
import { getAPIPrefix, getFullPath, formatURL } from '../src/utils';
3+
4+
test('getAPIPrefix', t => {
5+
t.is(getAPIPrefix('api'), '/api', 'relative path');
6+
t.is(getAPIPrefix('/api'), '/api', 'absolute path');
7+
t.is(getAPIPrefix('http://api.example.com'), 'http://api.example.com', 'http url');
8+
t.is(getAPIPrefix('https://api.example.com'), 'https://api.example.com', 'http url');
9+
t.is(getAPIPrefix('//api.example.com'), '//api.example.com', 'protocol-less url');
10+
t.is(getAPIPrefix('https://localhost:8080'), 'https://localhost:8080', 'localhost');
11+
});
12+
13+
test('getFullPath', t => {
14+
t.is(getFullPath('api'), '/api', 'relative path');
15+
t.is(getFullPath('/api'), '/api', 'absolute path');
16+
t.is(getFullPath('http://example.com', '/test'), 'http://example.com/test', 'url');
17+
t.is(getFullPath('http://example.com///', '/test/'), 'http://example.com/test/', 'url with slashes');
18+
t.is(getFullPath('https://localhost:8080', '/test///'), 'https://localhost:8080/test/', 'localhost');
19+
});
20+
21+
test('formatURL', t => {
22+
t.is(
23+
formatURL(),
24+
'/',
25+
'with no params passed'
26+
);
27+
t.is(
28+
formatURL(
29+
'APINamespace',
30+
),
31+
'/APINamespace',
32+
'with `APINamespace` param passed'
33+
);
34+
t.is(
35+
formatURL(
36+
'APINamespace',
37+
'namespace',
38+
),
39+
'/APINamespace/namespace',
40+
'with `APINamespace` and `namespace` param passed'
41+
);
42+
t.is(
43+
formatURL(
44+
'APINamespace',
45+
'namespace',
46+
['path', 'to', 'resource'],
47+
),
48+
'/APINamespace/namespace/path/to/resource',
49+
'with now `query` param passed'
50+
);
51+
t.is(
52+
formatURL(
53+
'APINamespace',
54+
'namespace',
55+
['path', 'to', 'resource'],
56+
{ edit: true, userId: 1}
57+
),
58+
'/APINamespace/namespace/path/to/resource?edit=true&userId=1',
59+
'with every param passed'
60+
);
61+
62+
});

0 commit comments

Comments
 (0)