Decouple oauth api calls into separate module. Simple tests for oauth

actions
This commit is contained in:
SleepWalker 2016-07-27 21:27:21 +03:00
parent eb3d436843
commit 1b8333b006
3 changed files with 228 additions and 82 deletions

View File

@ -3,6 +3,7 @@ import { routeActions } from 'react-router-redux';
import { updateUser, logout as logoutUser, changePassword as changeUserPassword, authenticate } from 'components/user/actions';
import request from 'services/request';
import authentication from 'services/api/authentication';
import oauth from 'services/api/oauth';
export function login({login = '', password = '', rememberMe = false}) {
const PASSWORD_REQUIRED = 'error.password_required';
@ -146,61 +147,27 @@ export function logout() {
// TODO: move to oAuth actions?
// test request: /oauth?client_id=ely&redirect_uri=http%3A%2F%2Fely.by&response_type=code&scope=minecraft_server_session
export function oAuthValidate(oauth) {
export function oAuthValidate(oauthData) {
return wrapInLoader((dispatch) =>
request.get(
'/api/oauth/validate',
getOAuthRequest(oauth)
)
oauth.validate(oauthData)
.then((resp) => {
dispatch(setClient(resp.client));
dispatch(setOAuthRequest(resp.oAuth));
dispatch(setScopes(resp.session.scopes));
})
.catch((resp = {}) => { // TODO
handleOauthParamsValidation(resp);
})
.catch(handleOauthParamsValidation)
);
}
export function oAuthComplete(params = {}) {
return wrapInLoader((dispatch, getState) => {
const oauth = getState().auth.oauth;
const query = request.buildQuery(getOAuthRequest(oauth));
return request.post(
`/api/oauth/complete?${query}`,
typeof params.accept === 'undefined' ? {} : {accept: params.accept}
)
.catch((resp = {}) => { // TODO
if (resp.statusCode === 401 && resp.error === 'access_denied') {
// user declined permissions
return {
success: false,
redirectUri: resp.redirectUri
};
}
handleOauthParamsValidation(resp);
if (resp.status === 401 && resp.name === 'Unauthorized') {
const error = new Error('Unauthorized');
error.unauthorized = true;
throw error;
}
if (resp.statusCode === 401 && resp.error === 'accept_required') {
const error = new Error('Permissions accept required');
error.acceptRequired = true;
dispatch(requirePermissionsAccept());
throw error;
}
})
return wrapInLoader((dispatch, getState) =>
oauth.complete(getState().auth.oauth, params)
.then((resp) => {
if (resp.redirectUri.startsWith('static_page')) {
resp.code = resp.redirectUri.match(/code=(.+)&/)[1];
resp.redirectUri = resp.redirectUri.match(/^(.+)\?/)[1];
resp.displayCode = resp.redirectUri === 'static_page_with_code';
dispatch(setOAuthCode({
success: resp.success,
code: resp.code,
@ -209,37 +176,21 @@ export function oAuthComplete(params = {}) {
}
return resp;
});
});
}
}, (resp) => {
if (resp.acceptRequired) {
dispatch(requirePermissionsAccept());
}
function getOAuthRequest(oauth) {
return {
client_id: oauth.clientId,
redirect_uri: oauth.redirectUrl,
response_type: oauth.responseType,
scope: oauth.scope,
state: oauth.state
};
return handleOauthParamsValidation(resp);
})
);
}
function handleOauthParamsValidation(resp = {}) {
let userMessage;
if (resp.statusCode === 400 && resp.error === 'invalid_request') {
userMessage = `Invalid request (${resp.parameter} required).`;
} else if (resp.statusCode === 400 && resp.error === 'unsupported_response_type') {
userMessage = `Invalid response type '${resp.parameter}'.`;
} else if (resp.statusCode === 400 && resp.error === 'invalid_scope') {
userMessage = `Invalid scope '${resp.parameter}'.`;
} else if (resp.statusCode === 401 && resp.error === 'invalid_client') {
userMessage = 'Can not find application you are trying to authorize.';
} else {
return;
}
/* eslint no-alert: "off" */
alert(userMessage);
throw new Error('Error completing request');
resp.userMessage && alert(resp.userMessage);
return Promise.reject(resp);
}
export const SET_CLIENT = 'set_client';

69
src/services/api/oauth.js Normal file
View File

@ -0,0 +1,69 @@
import request from 'services/request';
export default {
validate(oauthData) {
return request.get(
'/api/oauth/validate',
getOAuthRequest(oauthData)
).catch(handleOauthParamsValidation);
},
complete(oauthData, params = {}) {
const query = request.buildQuery(getOAuthRequest(oauthData));
return request.post(
`/api/oauth/complete?${query}`,
typeof params.accept === 'undefined' ? {} : {accept: params.accept}
).catch((resp = {}) => {
if (resp.statusCode === 401 && resp.error === 'access_denied') {
// user declined permissions
return {
success: false,
redirectUri: resp.redirectUri
};
}
if (resp.status === 401 && resp.name === 'Unauthorized') {
const error = new Error('Unauthorized');
error.unauthorized = true;
throw error;
}
if (resp.statusCode === 401 && resp.error === 'accept_required') {
const error = new Error('Permissions accept required');
error.acceptRequired = true;
throw error;
}
return handleOauthParamsValidation(resp);
});
}
};
function getOAuthRequest(oauthData) {
return {
client_id: oauthData.clientId,
redirect_uri: oauthData.redirectUrl,
response_type: oauthData.responseType,
scope: oauthData.scope,
state: oauthData.state
};
}
function handleOauthParamsValidation(resp = {}) {
let userMessage;
if (resp.statusCode === 400 && resp.error === 'invalid_request') {
userMessage = `Invalid request (${resp.parameter} required).`;
} else if (resp.statusCode === 400 && resp.error === 'unsupported_response_type') {
userMessage = `Invalid response type '${resp.parameter}'.`;
} else if (resp.statusCode === 400 && resp.error === 'invalid_scope') {
userMessage = `Invalid scope '${resp.parameter}'.`;
} else if (resp.statusCode === 401 && resp.error === 'invalid_client') {
userMessage = 'Can not find application you are trying to authorize.';
} else {
return;
}
return Promise.reject(userMessage);
}

View File

@ -0,0 +1,126 @@
import request from 'services/request';
import {
oAuthValidate,
oAuthComplete,
setClient,
setOAuthRequest,
setScopes,
setOAuthCode,
requirePermissionsAccept
} from 'components/auth/actions';
const oauthData = {
clientId: '',
redirectUrl: '',
responseType: '',
scope: '',
state: ''
};
describe('components/auth/actions', () => {
const dispatch = sinon.stub();
const getState = sinon.stub();
const callThunk = function(fn, ...args) {
const thunk = fn(...args);
return thunk(dispatch, getState);
};
beforeEach(() => {
dispatch.reset();
getState.reset();
getState.returns({});
sinon.stub(request, 'get');
sinon.stub(request, 'post');
});
afterEach(() => {
request.get.restore();
request.post.restore();
});
describe('#oAuthValidate()', () => {
it('should dispatch setClient, setOAuthRequest and setScopes', () => {
// TODO: the assertions may be splitted up to one per test
const resp = {
client: {id: 123},
oAuth: {state: 123},
session: {
scopes: ['scopes']
}
};
request.get.returns(Promise.resolve(resp));
return callThunk(oAuthValidate, oauthData).then(() => {
sinon.assert.calledWith(request.get, '/api/oauth/validate');
sinon.assert.calledWith(dispatch, setClient(resp.client));
sinon.assert.calledWith(dispatch, setOAuthRequest(resp.oAuth));
sinon.assert.calledWith(dispatch, setScopes(resp.session.scopes));
});
});
});
describe('#oAuthComplete()', () => {
beforeEach(() => {
getState.returns({
auth: {
oauth: oauthData
}
});
});
it('should dispatch setOAuthCode for static_page redirect', () => {
// TODO: it may be split on separate url and dispatch tests
const resp = {
success: true,
redirectUri: 'static_page?code=123&state='
};
request.post.returns(Promise.resolve(resp));
return callThunk(oAuthComplete).then(() => {
sinon.assert.calledWithMatch(request.post, /\/api\/oauth\/complete/);
sinon.assert.calledWith(dispatch, setOAuthCode({
success: true,
code: '123',
displayCode: false
}));
});
});
it('should resolve to with success false and redirectUri for access_denied', () => {
const resp = {
statusCode: 401,
error: 'access_denied',
redirectUri: 'redirectUri'
};
request.post.returns(Promise.reject(resp));
return callThunk(oAuthComplete).then((resp) => {
expect(resp).to.be.deep.equal({
success: false,
redirectUri: 'redirectUri'
});
});
});
it('should dispatch requirePermissionsAccept if accept_required', () => {
const resp = {
statusCode: 401,
error: 'accept_required'
};
request.post.returns(Promise.reject(resp));
return callThunk(oAuthComplete).catch((resp) => {
expect(resp.acceptRequired).to.be.true;
sinon.assert.calledWith(dispatch, requirePermissionsAccept());
});
});
});
});