Skip to content

Commit

Permalink
[MM-12738] Migrate OAuth app actions from user_actions.jsx and admin_…
Browse files Browse the repository at this point in the history
…actions.jsx to redux (mattermost#2387)

component tests
  • Loading branch information
mmaksitaliev authored and deanwhillier committed Feb 28, 2019
1 parent e4c2403 commit 6e3b7b6
Show file tree
Hide file tree
Showing 8 changed files with 262 additions and 94 deletions.
45 changes: 11 additions & 34 deletions actions/admin_actions.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import * as AdminActions from 'mattermost-redux/actions/admin';
import * as UserActions from 'mattermost-redux/actions/users';
import {Client4} from 'mattermost-redux/client';
import {bindClientFunc} from 'mattermost-redux/actions/helpers';

import {emitUserLoggedOutEvent} from 'actions/global_actions.jsx';
import {getOnNavigationConfirmed} from 'selectors/views/admin';
Expand Down Expand Up @@ -105,42 +106,18 @@ export async function samlCertificateStatus(success, error) {
}
}

export function getOAuthAppInfo(clientId, success, error) {
Client4.getOAuthAppInfo(clientId).then(
(data) => {
if (success) {
success(data);
}
}
).catch(
(err) => {
if (error) {
error(err);
}
}
);
export function getOAuthAppInfo(clientId) {
return bindClientFunc({
clientFunc: Client4.getOAuthAppInfo,
params: [clientId],
});
}

export function allowOAuth2(params, success, error) {
const responseType = params.get('response_type');
const clientId = params.get('client_id');
const redirectUri = params.get('redirect_uri');
const state = params.get('state');
const scope = params.get('scope');

Client4.authorizeOAuthApp(responseType, clientId, redirectUri, state, scope).then(
(data) => {
if (success) {
success(data);
}
}
).catch(
(err) => {
if (error) {
error(err);
}
}
);
export function allowOAuth2({responseType, clientId, redirectUri, state, scope}) {
return bindClientFunc({
clientFunc: Client4.authorizeOAuthApp,
params: [responseType, clientId, redirectUri, state, scope],
});
}

export async function emailToLdap(loginId, password, token, ldapId, ldapPassword, success, error) {
Expand Down
44 changes: 16 additions & 28 deletions actions/user_actions.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {deletePreferences as deletePreferencesRedux, savePreferences as savePref
import {getTeamMembersByIds} from 'mattermost-redux/actions/teams';
import * as UserActions from 'mattermost-redux/actions/users';
import {Client4} from 'mattermost-redux/client';
import {bindClientFunc} from 'mattermost-redux/actions/helpers';
import {Preferences as PreferencesRedux} from 'mattermost-redux/constants';
import {
getChannel,
Expand Down Expand Up @@ -385,36 +386,23 @@ export async function resendVerification(email, success, error) {
}
}

export function getAuthorizedApps(success, error) {
Client4.getAuthorizedOAuthApps(getState().entities.users.currentUserId).then(
(authorizedApps) => {
if (success) {
success(authorizedApps);
}
}
).catch(
(err) => {
if (error) {
error(err);
}
}
);
export function getAuthorizedApps() {
return (doDispatch, doGetState) => {
const currentUserId = Selectors.getCurrentUserId(doGetState());
const getAuthAppsAction = bindClientFunc({
clientFunc: Client4.getAuthorizedOAuthApps,
params: [currentUserId],
});

return doDispatch(getAuthAppsAction);
};
}

export function deauthorizeOAuthApp(appId, success, error) {
Client4.deauthorizeOAuthApp(appId).then(
() => {
if (success) {
success();
}
}
).catch(
(err) => {
if (error) {
error(err);
}
}
);
export function deauthorizeOAuthApp(appId) {
return bindClientFunc({
clientFunc: Client4.deauthorizeOAuthApp,
params: [appId],
});
}

export async function loadProfiles(page, perPage, options = {}, success) {
Expand Down
38 changes: 23 additions & 15 deletions components/authorize.jsx → components/authorize/authorize.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import PropTypes from 'prop-types';
import React from 'react';
import {FormattedMessage} from 'react-intl';

import {allowOAuth2, getOAuthAppInfo} from 'actions/admin_actions.jsx';
import icon50 from 'images/icon50x50.png';
import FormError from 'components/form_error.jsx';
import {browserHistory} from 'utils/browser_history';
Expand All @@ -16,7 +15,10 @@ export default class Authorize extends React.Component {
static get propTypes() {
return {
location: PropTypes.object.isRequired,
params: PropTypes.object.isRequired,
actions: PropTypes.shape({
getOAuthAppInfo: PropTypes.func.isRequired,
allowOAuth2: PropTypes.func.isRequired,
}).isRequired,
};
}

Expand All @@ -35,12 +37,12 @@ export default class Authorize extends React.Component {
return;
}

getOAuthAppInfo(
clientId,
(app) => {
this.setState({app});
}
);
this.props.actions.getOAuthAppInfo(clientId).then(
({data}) => {
if (data) {
this.setState({app: data});
}
});
}

componentDidMount() {
Expand All @@ -52,16 +54,22 @@ export default class Authorize extends React.Component {
}

handleAllow() {
const params = new URLSearchParams(this.props.location.search);
const searchParams = new URLSearchParams(this.props.location.search);
const params = {
responseType: searchParams.get('response_type'),
clientId: searchParams.get('client_id'),
redirectUri: searchParams.get('redirect_uri'),
state: searchParams.get('state'),
scope: searchParams.get('store'),
};

allowOAuth2(params,
(data) => {
if (data.redirect) {
this.props.actions.allowOAuth2(params).then(
({data, error}) => {
if (data && data.redirect) {
window.location.href = data.redirect;
} else if (error) {
this.setState({error: error.message});
}
},
(err) => {
this.setState({error: err.message});
}
);
}
Expand Down
71 changes: 71 additions & 0 deletions components/authorize/authorize.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.

import React from 'react';
import {shallow} from 'enzyme';

import Authorize from './authorize';

describe('components/user_settings/display/UserSettingsDisplay', () => {
const requiredProps = {
location: {search: ''},
actions: {
getOAuthAppInfo: jest.fn().mockResolvedValue({data: true}),
allowOAuth2: jest.fn().mockResolvedValue({data: true}),
},
};

test('UNSAFE_componentWillMount() should have called getOAuthAppInfo', () => {
const props = {...requiredProps, location: {search: 'client_id=1234abcd'}};

shallow(<Authorize {...props}/>);

expect(requiredProps.actions.getOAuthAppInfo).toHaveBeenCalled();
expect(requiredProps.actions.getOAuthAppInfo).toHaveBeenCalledWith('1234abcd');
});

test('UNSAFE_componentWillMount() should have updated state.app', async () => {
const expected = {};
const promise = Promise.resolve({data: expected});
const actions = {...requiredProps.actions, getOAuthAppInfo: () => promise};
const props = {...requiredProps, actions, location: {search: 'client_id=1234abcd'}};

const wrapper = shallow(<Authorize {...props}/>);

await promise;

expect(wrapper.state().app).toEqual(expected);
});

test('handleAllow() should have called allowOAuth2', () => {
const props = {...requiredProps, location: {search: 'client_id=1234abcd'}};

const wrapper = shallow(<Authorize {...props}/>);

wrapper.instance().handleAllow();

const expected = {
clientId: '1234abcd',
responseType: null,
redirectUri: null,
state: null,
scope: null,
};
expect(requiredProps.actions.allowOAuth2).toHaveBeenCalled();
expect(requiredProps.actions.allowOAuth2).toHaveBeenCalledWith(expected);
});

test('handleAllow() should updated state.error', async () => {
const error = {message: 'error'};
const promise = Promise.resolve({error});
const actions = {...requiredProps.actions, allowOAuth2: () => promise};
const props = {...requiredProps, actions, location: {search: 'client_id=1234abcd'}};

const wrapper = shallow(<Authorize {...props}/>);

wrapper.instance().handleAllow();
await promise;

expect(wrapper.state().error).toEqual(error.message);
});
});
20 changes: 20 additions & 0 deletions components/authorize/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.

import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';

import {allowOAuth2, getOAuthAppInfo} from 'actions/admin_actions.jsx';

import Authorize from './authorize';

function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
getOAuthAppInfo,
allowOAuth2,
}, dispatch),
};
}

export default connect(null, mapDispatchToProps)(Authorize);
3 changes: 3 additions & 0 deletions components/user_settings/security/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import * as UserUtils from 'mattermost-redux/utils/user_utils';
import {getConfig} from 'mattermost-redux/selectors/entities/general';
import {getBool} from 'mattermost-redux/selectors/entities/preferences';

import {getAuthorizedApps, deauthorizeOAuthApp} from 'actions/user_actions.jsx';
import {getPasswordConfig} from 'utils/utils.jsx';
import {Preferences} from 'utils/constants';

Expand Down Expand Up @@ -48,6 +49,8 @@ function mapDispatchToProps(dispatch) {
actions: bindActionCreators({
getMe,
updateUserPassword,
getAuthorizedApps,
deauthorizeOAuthApp,
}, dispatch),
};
}
Expand Down
36 changes: 19 additions & 17 deletions components/user_settings/security/user_settings_security.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import React from 'react';
import {FormattedDate, FormattedMessage, FormattedTime} from 'react-intl';
import {Link} from 'react-router-dom';

import {deauthorizeOAuthApp, getAuthorizedApps} from 'actions/user_actions.jsx';
import Constants from 'utils/constants.jsx';
import * as Utils from 'utils/utils.jsx';
import icon50 from 'images/icon50x50.png';
Expand Down Expand Up @@ -69,6 +68,8 @@ export default class SecurityTab extends React.Component {
actions: PropTypes.shape({
getMe: PropTypes.func.isRequired,
updateUserPassword: PropTypes.func.isRequired,
getAuthorizedApps: PropTypes.func.isRequired,
deauthorizeOAuthApp: PropTypes.func.isRequired,
}).isRequired,
}

Expand All @@ -93,12 +94,13 @@ export default class SecurityTab extends React.Component {

componentDidMount() {
if (this.props.enableOAuthServiceProvider) {
getAuthorizedApps(
(authorizedApps) => {
this.setState({authorizedApps, serverError: null}); //eslint-disable-line react/no-did-mount-set-state
},
(err) => {
this.setState({serverError: err.message}); //eslint-disable-line react/no-did-mount-set-state
this.props.actions.getAuthorizedApps().then(
({data, error}) => {
if (data) {
this.setState({authorizedApps: data, serverError: null}); //eslint-disable-line react/no-did-mount-set-state
} else if (error) {
this.setState({serverError: error.message}); //eslint-disable-line react/no-did-mount-set-state
}
}
);
}
Expand Down Expand Up @@ -169,17 +171,17 @@ export default class SecurityTab extends React.Component {
deauthorizeApp = (e) => {
e.preventDefault();
const appId = e.currentTarget.getAttribute('data-app');
deauthorizeOAuthApp(
appId,
() => {
const authorizedApps = this.state.authorizedApps.filter((app) => {
return app.id !== appId;
});
this.props.actions.deauthorizeOAuthApp(appId).then(
({data, error}) => {
if (data) {
const authorizedApps = this.state.authorizedApps.filter((app) => {
return app.id !== appId;
});

this.setState({authorizedApps, serverError: null});
},
(err) => {
this.setState({serverError: err.message});
this.setState({authorizedApps, serverError: null});
} else if (error) {
this.setState({serverError: error.message});
}
}
);
}
Expand Down
Loading

0 comments on commit 6e3b7b6

Please sign in to comment.