Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add fetch tests #23

Merged
merged 4 commits into from
May 31, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 19 additions & 8 deletions src/fetch.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,29 @@
import { API_ROOT } from './constants';
import _fetch from 'isomorphic-fetch';
import * as isomorphicFetch from 'isomorphic-fetch';

export function fetch(token, _input, _init) {
const init = {
/*
* Sinon cannot stub out a function in a function-only module.
* https://github.com/sinonjs/sinon/issues/664
*/
export function rawFetch(...args) {
return isomorphicFetch['default'](...args);
}

export function fetch(token, _path, _options) {
/*
* Get updated reference in case rawFetch is a stub (during testing).
* See comment on rawFetch.
*/
const fetchRef = module.exports.rawFetch;
const options = {
mode: 'cors',
..._init,
..._options,
headers: {
Accept: 'application/json',
Authorization: `token ${token}`,
'Content-Type': 'application/json',
},
};
const input = API_ROOT + _input;
return _fetch(input, init);
const path = API_ROOT + _path;
return fetchRef(path, options);
}

export { _fetch };
4 changes: 2 additions & 2 deletions src/layouts/OAuth.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { setToken } from '../actions/authentication';
import { clientId, clientSecret } from '../secrets';
import { pushPath } from 'redux-simple-router';
import { LOGIN_ROOT } from '../constants';
import { _fetch as fetch } from '../fetch';
import { rawFetch } from '../fetch';

export class OAuthCallbackPage extends Component {
async componentDidMount() {
Expand All @@ -18,7 +18,7 @@ export class OAuthCallbackPage extends Component {
data.append('client_secret', clientSecret);
data.append('code', code);

const resp = await fetch(`${LOGIN_ROOT}/oauth/token`, {
const resp = await rawFetch(`${LOGIN_ROOT}/oauth/token`, {
method: 'POST',
body: data,
});
Expand Down
8 changes: 4 additions & 4 deletions test/actions/api/linodes.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
rebootLinode,
UPDATE_LINODE,
} from '../../../src/actions/api/linodes';
import { mockContext } from '../../mocks';
import { mockFetchContext } from '../../contexts';

describe('actions/linodes/power', sinon.test(() => {
const mockBootingResponse = {
Expand All @@ -25,7 +25,7 @@ describe('actions/linodes/power', sinon.test(() => {
});

it('returns linode power boot status', async () => {
await mockContext(sandbox, async ({
await mockFetchContext(sandbox, async ({
auth, dispatch, getState, fetchStub,
}) => {
const f = powerOnLinode('foo');
Expand All @@ -47,7 +47,7 @@ describe('actions/linodes/power', sinon.test(() => {
};

it('returns linode power shutdown status', async () => {
await mockContext(sandbox, async ({
await mockFetchContext(sandbox, async ({
auth, dispatch, getState, fetchStub,
}) => {
const f = powerOffLinode('foo');
Expand All @@ -69,7 +69,7 @@ describe('actions/linodes/power', sinon.test(() => {
};

it('returns linode power reboot status', async () => {
await mockContext(sandbox, async ({
await mockFetchContext(sandbox, async ({
auth, dispatch, getState, fetchStub,
}) => {
const f = rebootLinode('foo');
Expand Down
10 changes: 5 additions & 5 deletions test/api-store.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import makeApiList, {
makeUpdateItem,
makeDeleteItem,
} from '../src/api-store';
import { mockContext } from './mocks';
import { mockFetchContext } from './contexts';

const mockFoobarsResponse = {
foobars: [
Expand Down Expand Up @@ -209,7 +209,7 @@ describe('api-store', () => {
});

it('fetches a page of items from the API', async () => {
await mockContext(sandbox, async ({
await mockFetchContext(sandbox, async ({
auth, dispatch, getState, fetchStub,
}) => {
const f = makeFetchPage('FETCH_FOOBARS', 'foobars');
Expand All @@ -227,7 +227,7 @@ describe('api-store', () => {
});

it('fetches the requested page', async () => {
await mockContext(sandbox, async ({
await mockFetchContext(sandbox, async ({
auth, dispatch, getState, fetchStub,
}) => {
const f = makeFetchPage('FETCH_FOOBARS', 'foobars');
Expand All @@ -249,7 +249,7 @@ describe('api-store', () => {
});

it('fetches an item from the API', async () => {
await mockContext(sandbox, async ({
await mockFetchContext(sandbox, async ({
auth, dispatch, getState, fetchStub,
}) => {
const f = makeUpdateItem('UPDATE_FOOBAR', 'foobars', 'foobar');
Expand All @@ -276,7 +276,7 @@ describe('api-store', () => {

const emptyResponse = {};
it('performs the API request', async () => {
await mockContext(sandbox, async ({
await mockFetchContext(sandbox, async ({
auth, dispatch, getState, fetchStub,
}) => {
const f = makeDeleteItem('DELETE_FOOBAR', 'foobars');
Expand Down
40 changes: 40 additions & 0 deletions test/contexts.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import sinon from 'sinon';
import * as fetch from '~/fetch';

export const mockContext = async (sandbox, stubInfo, f) => {
const stubs = stubInfo.map(({ obj, accessor, rsp }) =>
sinon.stub(obj, accessor).returns(rsp));

await f(stubs);

stubs.forEach(stub => {
stub.restore();
});
};

export const mockFetchContext = async (sandbox, f, rsp, state = {}) => {
const auth = { token: 'token' };
const getState = sinon.stub().returns({
authentication: auth,
...state,
});
const dispatch = sinon.spy();
const stubInfo = [
{ obj: fetch, accessor: 'fetch', rsp: { json: () => rsp } },
];

const fCurried = async ([fetchStub]) => {
await f({ auth, getState, dispatch, fetchStub });
};
return mockContext(sandbox, stubInfo, fCurried, state);
};

export const mockInternalFetchContext = async (sandbox, f) => {
const stubInfo = [
{ obj: fetch, accessor: 'rawFetch', rsp: null },
];
const fCurried = async ([fetchStub]) => {
await f({ fetchStub });
};
return mockContext(sandbox, stubInfo, fCurried);
};
64 changes: 64 additions & 0 deletions test/fetch.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import sinon from 'sinon';
import { expect } from 'chai';
import { mockInternalFetchContext } from './contexts';
import { fetch } from '~/fetch';
import { API_ROOT } from '~/constants';

describe('fetch', () => {
let sandbox = null;

beforeEach(() => {
sandbox = sinon.sandbox.create();
});

afterEach(() => {
sandbox.restore();
});

const token = 'my token';
const defaultHeaders = {
mode: 'cors',
headers: {
Accept: 'application/json',
Authorization: `token ${token}`,
'Content-Type': 'application/json',
},
};

it('should default to cors mode and headers for token', async () => {
await mockInternalFetchContext(sandbox, async ({ fetchStub }) => {
await fetch(token, '');

expect(fetchStub.calledWith(
API_ROOT,
defaultHeaders,
)).to.equal(true);
});
});

it('should fetch the correct path', async () => {
await mockInternalFetchContext(sandbox, async ({ fetchStub }) => {
await fetch(token, 'path');

expect(fetchStub.calledWith(
`${API_ROOT}path`,
defaultHeaders,
));
});
});

it('should include data', async () => {
await mockInternalFetchContext(sandbox, async ({ fetchStub }) => {
const data = { data: { foo: 'bar' } };
await fetch(token, '', data);

expect(fetchStub.calledWith(
API_ROOT,
{
...defaultHeaders,
...data,
}
));
});
});
});
6 changes: 3 additions & 3 deletions test/layouts/OAuth.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ describe('layouts/OAuth', () => {
};

it('exchanges the code for an OAuth token', async () => {
const fetchStub = sandbox.stub(fetch, '_fetch').returns({
const fetchStub = sandbox.stub(fetch, 'rawFetch').returns({
json: () => exchangeResponse,
});
const dispatch = sandbox.spy();
Expand All @@ -65,7 +65,7 @@ describe('layouts/OAuth', () => {
});

it('dispatches a setToken action', async () => {
sandbox.stub(fetch, '_fetch').returns({
sandbox.stub(fetch, 'rawFetch').returns({
json: () => exchangeResponse,
});
const dispatch = sandbox.spy();
Expand All @@ -88,7 +88,7 @@ describe('layouts/OAuth', () => {
});

it('supports the return query string option', async () => {
sandbox.stub(fetch, '_fetch').returns({
sandbox.stub(fetch, 'rawFetch').returns({
json: () => exchangeResponse,
});
const dispatch = sandbox.spy();
Expand Down
16 changes: 8 additions & 8 deletions test/linodes/actions/create.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { expect } from 'chai';
import sinon from 'sinon';
import * as actions from '~/linodes/actions/create';
import * as linodeActions from '~/actions/api/linodes';
import { mockContext } from '~/../test/mocks';
import { mockFetchContext } from '~/../test/contexts';
import { pushPath } from 'redux-simple-router';

describe('linodes/actions/create', () => {
Expand Down Expand Up @@ -112,15 +112,15 @@ describe('linodes/actions/create', () => {
});

it('should call getState() once', async () => {
await mockContext(sandbox, async ({ dispatch, getState }) => {
await mockFetchContext(sandbox, async ({ dispatch, getState }) => {
const func = actions.createLinode();
await func(dispatch, getState);
expect(getState.calledOnce).to.equal(true);
}, response, state);
});

it('should dispatch a TOGGLE_CREATING action', async () => {
await mockContext(sandbox, async ({ dispatch, getState }) => {
await mockFetchContext(sandbox, async ({ dispatch, getState }) => {
const func = actions.createLinode();
await func(dispatch, getState);
expect(dispatch.calledWith({
Expand All @@ -130,7 +130,7 @@ describe('linodes/actions/create', () => {
});

it('should perform an HTTP POST to /linodes', async () => {
await mockContext(sandbox, async ({ dispatch, getState, fetchStub }) => {
await mockFetchContext(sandbox, async ({ dispatch, getState, fetchStub }) => {
const func = actions.createLinode();
await func(dispatch, getState);
expect(fetchStub.calledWith(
Expand All @@ -149,7 +149,7 @@ describe('linodes/actions/create', () => {
});

it('should dispatch an UPDATE_LINODE action with the new linode', async () => {
await mockContext(sandbox, async ({ dispatch, getState }) => {
await mockFetchContext(sandbox, async ({ dispatch, getState }) => {
const func = actions.createLinode();
await func(dispatch, getState);
expect(dispatch.calledWith({
Expand All @@ -160,7 +160,7 @@ describe('linodes/actions/create', () => {
});

it('should dispatch a routing action to navigate to the detail page', async () => {
await mockContext(sandbox, async ({ dispatch, getState }) => {
await mockFetchContext(sandbox, async ({ dispatch, getState }) => {
const func = actions.createLinode();
await func(dispatch, getState);
expect(dispatch.calledWith(
Expand All @@ -170,15 +170,15 @@ describe('linodes/actions/create', () => {
});

it('should dispatch a CLEAR_FORM action', async () => {
await mockContext(sandbox, async ({ dispatch, getState }) => {
await mockFetchContext(sandbox, async ({ dispatch, getState }) => {
const func = actions.createLinode();
await func(dispatch, getState);
expect(dispatch.calledWith({ type: actions.CLEAR_FORM })).to.equal(true);
}, response, state);
});

it('should update the linode until it finishes provisioning', async () => {
await mockContext(sandbox, async ({ dispatch, getState }) => {
await mockFetchContext(sandbox, async ({ dispatch, getState }) => {
const func = actions.createLinode();
const update = sandbox.spy(() => { });
sandbox.stub(linodeActions, 'updateLinodeUntil', update);
Expand Down
15 changes: 0 additions & 15 deletions test/mocks.js

This file was deleted.