Skip to content
This repository has been archived by the owner on Jun 19, 2023. It is now read-only.

Commit

Permalink
Merge 767ecd0 into c5a6330
Browse files Browse the repository at this point in the history
  • Loading branch information
mmcgahan committed Aug 18, 2017
2 parents c5a6330 + 767ecd0 commit 7c5212a
Show file tree
Hide file tree
Showing 23 changed files with 257 additions and 183 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ In general, application-specific code will live outside of this package.
- [API State module](src/api-state/README.md)
- ['Query': structuring data requests](src/api-state/Queries.md) -
GET/POST/PATCH/DELETE requests to REST API
- [Redux store modules](src/store/README.md) - browser and server

# Releases

Expand Down
4 changes: 4 additions & 0 deletions flow-typed/api-proxy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
declare type ProxyResponseSuccess = {| responses: Array<QueryResponse> |};
declare type ProxyResponseError = {| error: string, message: string |};

declare type ProxyResponse = ProxyResponseSuccess | ProxyResponseError;
4 changes: 4 additions & 0 deletions flow-typed/store.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
declare type ParsedQueryResponses = {
successes: Array<QueryState>,
errors: Array<QueryState>,
};
15 changes: 0 additions & 15 deletions src/middleware/catch.js

This file was deleted.

21 changes: 20 additions & 1 deletion src/plugins/requestAuthPlugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import 'rxjs/add/operator/mergeMap';
import 'rxjs/add/operator/map';

import logger from '../util/logger';
import { tryJSON } from '../util/fetchUtils';
import { MEMBER_COOKIE } from '../util/cookieUtils';
import {
applyAuthState,
Expand All @@ -21,6 +20,26 @@ import {
* @module requestAuthPlugin
*/

/**
* Attempt to JSON parse a Response object from a fetch call
*
* @param {String} reqUrl the URL that was requested
* @param {Response} response the fetch Response object
* @return {Promise} a Promise that resolves with the JSON-parsed text
*/
export const tryJSON = reqUrl => response => {
const { status, statusText } = response;
if (status >= 400) {
// status always 200: bugzilla #52128
return Promise.reject(
new Error(
`Request to ${reqUrl} responded with error code ${status}: ${statusText}`
)
);
}
return response.text().then(text => JSON.parse(text));
};

const verifyAuth = logger => auth => {
if (!Object.keys(auth).length) {
const errorMessage = 'No auth token(s) provided';
Expand Down
40 changes: 40 additions & 0 deletions src/plugins/requestAuthPlugin.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import register, {
getAnonymousCode$,
getAccessToken$,
applyRequestAuthorizer$,
tryJSON,
} from './requestAuthPlugin';

const MOCK_REPLY_FN = () => {};
Expand Down Expand Up @@ -351,3 +352,42 @@ describe('getAuthenticate', () => {
);
});
});

describe('tryJSON', () => {
const reqUrl = 'http://example.com';
const goodResponse = { foo: 'bar' };
const goodFetchResponse = {
text: () => Promise.resolve(JSON.stringify(goodResponse)),
};
const errorResponse = {
status: 400,
statusText: '400 Bad Request',
text: () => Promise.resolve('There was a problem'),
};
const badJSON = 'totally not JSON';
const badJSONResponse = {
text: () => Promise.resolve(badJSON),
};
it('returns a Promise that resolves to the parsed fetch response JSON', () => {
const theFetch = tryJSON(reqUrl)(goodFetchResponse);
expect(theFetch).toEqual(jasmine.any(Promise));

return theFetch.then(response => expect(response).toEqual(goodResponse));
});
it('returns a rejected Promise with Error when response has 400+ status', () => {
const theFetch = tryJSON(reqUrl)(errorResponse);
expect(theFetch).toEqual(jasmine.any(Promise));
return theFetch.then(
response => expect(true).toBe(false), // should not run - promise should be rejected
err => expect(err).toEqual(jasmine.any(Error))
);
});
it('returns a rejected Promise with Error when response fails JSON parsing', () => {
const theFetch = tryJSON(reqUrl)(badJSONResponse);
expect(theFetch).toEqual(jasmine.any(Promise));
return theFetch.then(
response => expect(true).toBe(false), // should not run - promise should be rejected
err => expect(err).toEqual(jasmine.any(Error))
);
});
});
5 changes: 1 addition & 4 deletions src/renderers/browser-render.jsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
// @flow
import React from 'react';
import ReactDOM from 'react-dom';
import {
getInitialState,
getBrowserCreateStore,
} from '../util/createStoreBrowser';
import { getInitialState, getBrowserCreateStore } from '../store/browser';
import { getRouteResolver } from '../router/util';
import BrowserApp from '../render/components/BrowserApp';

Expand Down
7 changes: 3 additions & 4 deletions src/renderers/server-render.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,10 @@ import ReactDOMServer from 'react-dom/server';
import Helmet from 'react-helmet';
import { API_ROUTE_PATH } from '../plugins/api-proxy'; // mwp-api-proxy
import { Forbidden, NotFound, Redirect, SERVER_RENDER } from '../router'; // mwp-router
import { getServerCreateStore } from '../store/server'; // mwp-store
import Dom from '../render/components/Dom'; // mwp-render/components/Dom
import ServerApp from '../render/components/ServerApp'; // mwp-render/components/ServerApp

import Dom from '../render/components/Dom';
import ServerApp from '../render/components/ServerApp';

import { getServerCreateStore } from '../util/createStoreServer';
import configure from '../actions/configActionCreators';

// Ensure global Intl for use with FormatJS
Expand Down
15 changes: 15 additions & 0 deletions src/store/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Store

The Redux `createStore` helpers for MWP applications - `store/browser` and
`store/server`. Both modules are implemented as 'getters' with a similar
signature - the main difference is that the `store/server` function takes an
additional `request` parameter corresponding to a Hapi request.

## Dependencies

- js-cookie
- rison
- rxjs (peer)
- mwp-api-proxy-plugin
- mwp-api-state
- mwp-tracking/util/clickWriter
54 changes: 4 additions & 50 deletions src/util/fetchUtils.js → src/store/browser/fetchQueries.js
Original file line number Diff line number Diff line change
@@ -1,40 +1,12 @@
import JSCookie from 'js-cookie';
import rison from 'rison';
import { setClickCookie } from '../plugins/tracking/util/clickState';
import { setClickCookie } from '../../plugins/tracking/util/clickState'; // mwp-tracking-plugin

/**
* A module for middleware that would like to make external calls through `fetch`
* @module fetchUtils
*/
import { parseQueryResponse } from '../util/fetchUtils';

export const CSRF_HEADER = 'x-csrf-jwt';
export const CSRF_HEADER_COOKIE = 'x-csrf-jwt-header';

export const parseQueryResponse = queries => ({
responses,
error,
message,
}) => {
if (error) {
throw new Error(JSON.stringify({ error, message })); // treat like an API error
}
responses = responses || [];
if (queries.length !== responses.length) {
throw new Error('Responses do not match requests');
}

return responses.reduce(
(categorized, response, i) => {
const targetArray = response.error
? categorized.errors
: categorized.successes;
targetArray.push({ response, query: queries[i] });
return categorized;
},
{ successes: [], errors: [] }
);
};

/**
* that the request will have the required OAuth and CSRF credentials and constructs
* the `fetch` call arguments based on the request method. It also records the
Expand Down Expand Up @@ -120,7 +92,7 @@ export const getFetchArgs = (apiUrl, queries, meta) => {
* click tracking data
* @return {Promise} resolves with a `{queries, responses}` object
*/
export const fetchQueries = apiUrl => (queries, meta) => {
const fetchQueries = apiUrl => (queries, meta) => {
if (
typeof window === 'undefined' &&
typeof test === 'undefined' // not in browser // not in testing env (global set by Jest)
Expand All @@ -147,22 +119,4 @@ export const fetchQueries = apiUrl => (queries, meta) => {
});
};

/**
* Attempt to JSON parse a Response object from a fetch call
*
* @param {String} reqUrl the URL that was requested
* @param {Response} response the fetch Response object
* @return {Promise} a Promise that resolves with the JSON-parsed text
*/
export const tryJSON = reqUrl => response => {
const { status, statusText } = response;
if (status >= 400) {
// status always 200: bugzilla #52128
return Promise.reject(
new Error(
`Request to ${reqUrl} responded with error code ${status}: ${statusText}`
)
);
}
return response.text().then(text => JSON.parse(text));
};
export default fetchQueries;
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { mockQuery } from 'meetup-web-mocks/lib/app';
import { MOCK_GROUP } from 'meetup-web-mocks/lib/api';
import * as fetchUtils from './fetchUtils';
import * as clickState from '../../plugins/tracking/util/clickState'; // mwp-tracking/util/clickState
import { URLSearchParams } from 'url';
import * as clickState from '../plugins/tracking/util/clickState';
import fetchQueries, { CSRF_HEADER_COOKIE } from './fetchQueries';
clickState.setClickCookie = jest.fn();

global.FormData = function() {};
Expand All @@ -23,7 +23,7 @@ jest.mock('js-cookie', () => {

describe('fetchQueries', () => {
const API_URL = new URL('http://api.example.com/');
const csrfJwt = `${fetchUtils.CSRF_HEADER_COOKIE} value`;
const csrfJwt = `${CSRF_HEADER_COOKIE} value`;
const getQueries = [mockQuery({ params: {} })];
const POSTQueries = [
{ ...mockQuery({ params: {} }), meta: { method: 'POST' } },
Expand Down Expand Up @@ -55,9 +55,7 @@ describe('fetchQueries', () => {
it('returns an object with successes and errors arrays', () => {
spyOn(global, 'fetch').and.callFake(fakeSuccess);

return fetchUtils.fetchQueries(API_URL.toString())(
getQueries
).then(response => {
return fetchQueries(API_URL.toString())(getQueries).then(response => {
expect(response.successes).toEqual([
{ query: getQueries[0], response: responses[0] },
]);
Expand All @@ -67,7 +65,7 @@ describe('fetchQueries', () => {
it('returns a promise that will reject when response contains error prop', () => {
spyOn(global, 'fetch').and.callFake(fakeSuccessError);

return fetchUtils.fetchQueries(API_URL.toString())(getQueries).then(
return fetchQueries(API_URL.toString())(getQueries).then(
response => expect(true).toBe(false),
err => expect(err).toEqual(jasmine.any(Error))
);
Expand All @@ -79,9 +77,7 @@ describe('fetchQueries', () => {

const methodTest = method => () => {
query.meta = { method };
return fetchUtils.fetchQueries(API_URL.toString())(
queries
).then(response => {
return fetchQueries(API_URL.toString())(queries).then(response => {
const [, config] = global.fetch.calls.mostRecent().args;
expect(config.method).toEqual(method);
});
Expand All @@ -94,7 +90,7 @@ describe('fetchQueries', () => {
it('GET calls fetch with API url and queries, metadata', () => {
spyOn(global, 'fetch').and.callFake(fakeSuccess);

return fetchUtils.fetchQueries(API_URL.toString())(getQueries, {
return fetchQueries(API_URL.toString())(getQueries, {
...meta,
}).then(() => {
const calledWith = global.fetch.calls.mostRecent().args;
Expand All @@ -112,7 +108,7 @@ describe('fetchQueries', () => {
};
spyOn(global, 'fetch').and.callFake(fakeSuccess);

return fetchUtils.fetchQueries(API_URL.toString())(getQueries, {
return fetchQueries(API_URL.toString())(getQueries, {
...meta,
clickTracking,
logout: true,
Expand All @@ -124,9 +120,7 @@ describe('fetchQueries', () => {
it('GET without meta calls fetch without metadata querystring params', () => {
spyOn(global, 'fetch').and.callFake(fakeSuccess);

return fetchUtils.fetchQueries(API_URL.toString())(
getQueries
).then(() => {
return fetchQueries(API_URL.toString())(getQueries).then(() => {
const calledWith = global.fetch.calls.mostRecent().args;
const url = new URL(calledWith[0]);
expect(url.origin).toBe(API_URL.origin);
Expand All @@ -140,10 +134,7 @@ describe('fetchQueries', () => {
it('POST calls fetch with API url; csrf header; queries and metadata body params', () => {
spyOn(global, 'fetch').and.callFake(fakeSuccess);

return fetchUtils.fetchQueries(API_URL.toString())(
POSTQueries,
meta
).then(() => {
return fetchQueries(API_URL.toString())(POSTQueries, meta).then(() => {
const calledWith = global.fetch.calls.mostRecent().args;
const url = new URL(calledWith[0]);
const options = calledWith[1];
Expand All @@ -159,9 +150,7 @@ describe('fetchQueries', () => {
it('POST without meta calls fetch without metadata body params', () => {
spyOn(global, 'fetch').and.callFake(fakeSuccess);

return fetchUtils.fetchQueries(API_URL.toString())(
POSTQueries
).then(() => {
return fetchQueries(API_URL.toString())(POSTQueries).then(() => {
const calledWith = global.fetch.calls.mostRecent().args;
const url = new URL(calledWith[0]);
const options = calledWith[1];
Expand All @@ -188,9 +177,7 @@ describe('fetchQueries', () => {
];
spyOn(global, 'fetch').and.callFake(fakeSuccess);

return fetchUtils.fetchQueries(API_URL.toString())(
formQueries
).then(() => {
return fetchQueries(API_URL.toString())(formQueries).then(() => {
const calledWith = global.fetch.calls.mostRecent().args;
const url = new URL(calledWith[0]);
const options = calledWith[1];
Expand All @@ -202,42 +189,3 @@ describe('fetchQueries', () => {
});
});
});

describe('tryJSON', () => {
const reqUrl = 'http://example.com';
const goodResponse = { foo: 'bar' };
const goodFetchResponse = {
text: () => Promise.resolve(JSON.stringify(goodResponse)),
};
const errorResponse = {
status: 400,
statusText: '400 Bad Request',
text: () => Promise.resolve('There was a problem'),
};
const badJSON = 'totally not JSON';
const badJSONResponse = {
text: () => Promise.resolve(badJSON),
};
it('returns a Promise that resolves to the parsed fetch response JSON', () => {
const theFetch = fetchUtils.tryJSON(reqUrl)(goodFetchResponse);
expect(theFetch).toEqual(jasmine.any(Promise));

return theFetch.then(response => expect(response).toEqual(goodResponse));
});
it('returns a rejected Promise with Error when response has 400+ status', () => {
const theFetch = fetchUtils.tryJSON(reqUrl)(errorResponse);
expect(theFetch).toEqual(jasmine.any(Promise));
return theFetch.then(
response => expect(true).toBe(false), // should not run - promise should be rejected
err => expect(err).toEqual(jasmine.any(Error))
);
});
it('returns a rejected Promise with Error when response fails JSON parsing', () => {
const theFetch = fetchUtils.tryJSON(reqUrl)(badJSONResponse);
expect(theFetch).toEqual(jasmine.any(Promise));
return theFetch.then(
response => expect(true).toBe(false), // should not run - promise should be rejected
err => expect(err).toEqual(jasmine.any(Error))
);
});
});

0 comments on commit 7c5212a

Please sign in to comment.