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

User invite error handling #1105

Merged
merged 16 commits into from
May 30, 2017
23 changes: 20 additions & 3 deletions static_src/actions/user_actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,9 @@ const userActions = {
});

return uaaApi.inviteUaaUser(email)
.then(data => userActions.receiveUserInvite(data));
.then(data => userActions.receiveUserInvite(data))
.catch(err => userActions.userInviteError(err, `There was a problem
inviting ${email}`));
},

receiveUserInvite(inviteData) {
Expand All @@ -136,9 +138,22 @@ const userActions = {
});

const userGuid = inviteData.new_invites[0].userId;
const userEmail = inviteData.new_invites[0].email;

return cfApi.postCreateNewUserWithGuid(userGuid)
.then(user => userActions.receiveUserForCF(user, inviteData));
.then(user => userActions.receiveUserForCF(user, inviteData))
.catch(err => userActions.userInviteError(err, `There was a problem
inviting ${userEmail}`));
},

userInviteError(err, contextualMessage) {
AppDispatcher.handleServerAction({
type: userActionTypes.USER_INVITE_ERROR,
err,
contextualMessage
});

return Promise.resolve(err);
},

receiveUserForCF(user, inviteData) {
Expand Down Expand Up @@ -168,7 +183,9 @@ const userActions = {
const orgGuid = OrgStore.currentOrgGuid;

return cfApi.putAssociateUserToOrganization(user.guid, orgGuid)
.then(userActions.associatedUserToOrg(user, orgGuid));
.then(userActions.associatedUserToOrg(user, orgGuid))
.catch(err => userActions.userInviteError(err, `Unable to associate user to
organization`));
},

associatedUserToOrg(user, orgGuid) {
Expand Down
7 changes: 5 additions & 2 deletions static_src/components/form/form.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,9 @@ export default class Form extends React.Component {
let errorMsg;
const classes = classNames(...this.props.classes);

if (this.state.errors.length) {
if (this.props.errorOverride) {
errorMsg = <FormError message={ this.props.errorOverride } />;
} else if (this.state.errors.length) {
errorMsg = <FormError message="There were errors submitting the form." />;
}

Expand All @@ -92,7 +94,8 @@ Form.propTypes = {
classes: React.PropTypes.array,
guid: React.PropTypes.string.isRequired,
method: React.PropTypes.string,
onSubmit: React.PropTypes.func
onSubmit: React.PropTypes.func,
errorOverride: React.PropTypes.string
};

Form.defaultProps = {
Expand Down
5 changes: 3 additions & 2 deletions static_src/components/users.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ function stateSetter() {
currentType,
loading: UserStore.loading,
empty: !UserStore.loading && !users.length,
users
users,
userInviteError: UserStore.getInviteError()
};
}

Expand Down Expand Up @@ -135,7 +136,7 @@ export default class Users extends React.Component {
{ content }
</div>
</div>
<UsersInvite />
<UsersInvite error={ this.state.userInviteError } />
</div>
);
}
Expand Down
21 changes: 16 additions & 5 deletions static_src/components/users_invite.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@ import userActions from '../actions/user_actions';
import { validateString } from '../util/validators';

const USERS_INVITE_FORM_GUID = 'users-invite-form';
const propTypes = {};

const propTypes = {
error: React.PropTypes.object
};
const defaultProps = {};

export default class UsersInvite extends React.Component {
Expand All @@ -30,22 +33,30 @@ export default class UsersInvite extends React.Component {
userActions.fetchUserInvite(values.email.value);
}

get errorMessage() {
const err = this.props.error;
if (!err) return undefined;
const message = err.contextualMessage;
if (err.message) {
return `${message}: ${err.message}.`;
}
return message;
}

render() {
return (
<div className="test-users-invite">
<h2>User invite</h2>
<PanelDocumentation>
<PanelDocumentation description>
<p>Organizational Managers can add new users below.</p>
</PanelDocumentation>
<Form
guid={ USERS_INVITE_FORM_GUID }
classes={ ['users_invite_form'] }
ref="form"
onSubmit={ this._onValidForm }
errorOverride={ this.errorMessage }
>
<legend>
Invite a new user
</legend>
<FormText
formGuid={ USERS_INVITE_FORM_GUID }
classes={ ['test-users_invite_name'] }
Expand Down
2 changes: 2 additions & 0 deletions static_src/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,8 @@ const userActionTypes = keymirror({
USER_ORG_ASSOCIATE: null,
// Action to associate user to organization on the server.
USER_ORG_ASSOCIATED: null,
// Action when something goes wrong in user invite and email process.
USER_INVITE_ERROR: null,
// Action to delete a user from an org.
USER_DELETE: null,
// Action when a user was deleted from an org on the server.
Expand Down
18 changes: 18 additions & 0 deletions static_src/stores/user_store.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,12 @@ export class UserStore extends BaseStore {
break;
}

case userActionTypes.USER_INVITE_FETCH: {
this._inviteError = null;
this.emitChange();
break;
}

case userActionTypes.USER_ORG_ASSOCIATED: {
const user = Object.assign({}, { orgGuid: action.orgGuid }, action.user);
this._inviteInputActive = true;
Expand Down Expand Up @@ -207,6 +213,14 @@ export class UserStore extends BaseStore {
break;
}

case userActionTypes.USER_INVITE_ERROR: {
this._inviteError = Object.assign({}, action.err, {
contextualMessage: action.contextualMessage
});
this.emitChange();
break;
}

case userActionTypes.CURRENT_USER_INFO_RECEIVED: {
const guid = action.currentUser.user_id;
const userInfo = Object.assign(
Expand Down Expand Up @@ -376,6 +390,10 @@ export class UserStore extends BaseStore {
return this._currentUserIsAdmin;
}

getInviteError() {
return this._inviteError;
}

get currentUser() {
return this.get(this._currentUserGuid);
}
Expand Down
11 changes: 10 additions & 1 deletion static_src/test/functional/pageobjects/user_invite.element.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ export default class UserInviteElement extends BaseElement {
const existingUserCount = this.countNumberOfUsers();
this.element('[type="submit"]').click();
browser.waitUntil(() =>
this.countNumberOfUsers() > existingUserCount
this.countNumberOfUsers() > existingUserCount ||
browser.isExisting('.test-users-invite .error_message')
, 10000);
}

Expand All @@ -35,4 +36,12 @@ export default class UserInviteElement extends BaseElement {
return browser.elements(`.test-users .complex_list-item:nth-child(${idx})`)
.value[0];
}

getErrorMessage() {
const errorEl = this.element('.error_message');
if (errorEl) {
return errorEl.getText();
}
return null;
}
}
8 changes: 8 additions & 0 deletions static_src/test/functional/user_invite.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,5 +53,13 @@ describe('User roles', function () {
const user = userInviteElement.getUserByIndex(currentUserCount - 1);
expect(user.getText()).toMatch(/[email protected]/);
});

it('should display an error message if the email address is invalid',
function () {
const invalidEmail = '123';
userInviteElement.inputToInviteForm(invalidEmail);
userInviteElement.submitInviteForm();
expect(userInviteElement.getErrorMessage()).toMatch(invalidEmail);
});
});
});
5 changes: 4 additions & 1 deletion static_src/test/server/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -97,10 +97,13 @@ module.exports = function api(smocks) {
const email = req.payload.emails[0];
if (email && userInviteResponses[email]){
userInviteResponse = userInviteResponses[email];
reply(userInviteResponse);
} else if (!email.length || !/(.+)@(.+){2,}\.(.+){2,}/.test(email)) {
reply({ message: 'Invalid email'}).code(500);
} else {
userInviteResponse = userInviteResponses['default'];
reply(userInviteResponse);
}
reply(userInviteResponse);
}
});

Expand Down
81 changes: 81 additions & 0 deletions static_src/test/unit/actions/user_actions.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,47 @@ describe('userActions', function() {
it('calls uaaApi inviteUaaUser', function () {
expect(uaaApi.inviteUaaUser).toHaveBeenCalledWith(email);
});

describe('when request fails', function() {
beforeEach(function (done) {
uaaApi.inviteUaaUser.returns(Promise.reject({}));
sandbox.spy(userActions, 'userInviteError');

userActions.fetchUserInvite(email).then(done, done.fail);
});

it('should call user invite error action handler', function() {
expect(userActions.userInviteError).toHaveBeenCalledOnce();
});

it('should provide contextual message about invite', function() {
const arg = userActions.userInviteError.getCall(0).args[1];
expect(arg.length).toBeGreaterThan(0);
expect(arg).toMatch('invit');
});
});
});

describe('userInviteError()', function() {
let err;
let message;

beforeEach(function(done) {
err = { status: 502 };
message = 'something happened when invititing';
sandbox.stub(AppDispatcher, 'handleServerAction');

userActions.userInviteError(err, message).then(done, done.fail);
});

it('should dispatch server action of type user error with error and optional message',
function() {
expect(AppDispatcher.handleServerAction).toHaveBeenCalledWith(sinon.match({
type: userActionTypes.USER_INVITE_ERROR,
err,
contextualMessage: message
}));
});
});

describe('receiveUserInvite', function () {
Expand All @@ -241,6 +282,26 @@ describe('userActions', function() {
it('calls cfApi postCreateNewUserWithGuid', function () {
expect(cfApi.postCreateNewUserWithGuid).toHaveBeenCalledWith(userGuid);
});

describe('when request fails', function() {
beforeEach(function (done) {
cfApi.postCreateNewUserWithGuid.returns(Promise.reject({}));
sandbox.spy(userActions, 'userInviteError');

userActions.receiveUserInvite(inviteData)
.then(done, done.fail);
});

it('should call user invite error action handler', function() {
expect(userActions.userInviteError).toHaveBeenCalledOnce();
});

it('should provide contextual message about invite', function() {
const arg = userActions.userInviteError.getCall(0).args[1];
expect(arg.length).toBeGreaterThan(0);
expect(arg).toMatch('invit');
});
});
});

describe('receiveUserForCF', function () {
Expand Down Expand Up @@ -325,6 +386,26 @@ describe('userActions', function() {
it('should call associatedUser confirmation after', function () {
expect(userActions.associatedUserToOrg).toHaveBeenCalledOnce();
});

describe('when the request fails', function() {
beforeEach(function (done) {
cfApi.putAssociateUserToOrganization.returns(Promise.reject({}));
sandbox.spy(userActions, 'userInviteError');
userActions.associateUserToOrg(user)
.then(done, done.fail);
});

it('should call global user error action', function() {
expect(userActions.userInviteError).toHaveBeenCalledOnce();
});

it('should provide a message about the user not being added to the org',
function() {
const arg = userActions.userInviteError.getCall(0).args[1];
expect(arg.length).toBeGreaterThan(0);
expect(arg).toMatch('associate user');
});
});
});

describe('associatedUserToOrg', function () {
Expand Down
41 changes: 41 additions & 0 deletions static_src/test/unit/stores/user_store.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -673,6 +673,47 @@ describe('UserStore', function () {
});
});

describe('on USER_INVITE_FETCH', function() {
beforeEach(function() {
UserStore._inviteError = { message: 'something wrong' };
sandbox.spy(UserStore, 'emitChange');

userActions.fetchUserInvite();
});

it('should unset the user invite error', function() {
expect(UserStore.getInviteError()).toBeNull();
});

it('should emit a change event', function() {
expect(UserStore.emitChange).toHaveBeenCalledOnce();
});
});

describe('on USER_INVITE_ERROR', function() {
let error;
let message;

beforeEach(function() {
UserStore._inviteError = null;
message = 'Inviting user did not work';
error = { status: 500, message: 'CF said this' };
sandbox.spy(UserStore, 'emitChange');

userActions.userInviteError(error, message);
});

it('should add a error message to the user invite error field', function() {
expect(UserStore.getInviteError()).toBeDefined();
expect(UserStore.getInviteError().contextualMessage).toEqual(message);
expect(UserStore.getInviteError().message).toEqual(error.message);
});

it('should emit a change event', function() {
expect(UserStore.emitChange).toHaveBeenCalledOnce();
});
});

describe('isAdmin()', function () {
describe('user with _currentUserIsAdmin', function () {
let user, space, org, actual;
Expand Down
Loading