From f6f1abd03369038046e9d34bb9aaabfdc94cdc2f Mon Sep 17 00:00:00 2001 From: Adam Biagianti Date: Wed, 19 Jul 2017 12:39:31 -0400 Subject: [PATCH] AB+LB: Allow org managers to invite users to spaces Pass in entitytype to UserInvite component Fix lint errors, only show invites when org manager is current user Fixup unit tests Add specs, remove unused sandbox refs * Use component for invite fallback message Store users before merging roles Linting an spec fixes Address PR feedback --- package.json | 2 + static_src/actions/user_actions.js | 75 ++++---- static_src/components/user_list.jsx | 1 + static_src/components/users.jsx | 167 ++++++++++-------- static_src/components/users_invite.jsx | 98 +++++----- static_src/stores/base_store.js | 1 + static_src/stores/user_store.js | 22 ++- .../test/unit/actions/user_actions.spec.js | 60 +++---- .../user_role_list_control.spec.jsx | 10 +- .../test/unit/components/users.spec.jsx | 83 ++++++--- .../unit/components/users_invite.spec.jsx | 66 ++++--- .../test/unit/stores/user_store.spec.js | 39 ++-- static_src/test/unit/util/cf_api.spec.js | 33 +--- static_src/util/cf_api.js | 30 +--- 14 files changed, 369 insertions(+), 318 deletions(-) diff --git a/package.json b/package.json index 7f6b54ab..40e67c86 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,8 @@ "build-style": "cd node_modules/cloudgov-style && npm install && npm run build", "check-style": "/bin/bash -c '[[ $(npm ls cloudgov-style) =~ \"git://github.com\" ]] && npm run build-style || exit 0'", "clean": "rm -rf ./static/assets/*", + "dev-start": "cf dev start && docker-compose up -d app frontend watch", + "dev-stop": "cf dev stop && docker-compose stop", "lint": "eslint --ext .js --ext .jsx ./static_src", "test": "npm run lint && npm run test-unit && npm run test-functional", "test-functional": "NODE_ENV=test npm run testing-server-run -- wdio wdio.conf.js", diff --git a/static_src/actions/user_actions.js b/static_src/actions/user_actions.js index 13a4de62..b6c7dd6a 100644 --- a/static_src/actions/user_actions.js +++ b/static_src/actions/user_actions.js @@ -125,7 +125,7 @@ const userActions = { addUserRoles(roles, apiKey, userGuid, entityGuid, entityType) { const apiMethodMap = { - org: cfApi.putOrgUserPermissions, + organization: cfApi.putOrgUserPermissions, space: cfApi.putSpaceUserPermissions }; const api = apiMethodMap[entityType]; @@ -163,7 +163,7 @@ const userActions = { deleteUserRoles(roles, apiKey, userGuid, entityGuid, entityType) { const apiMethodMap = { - org: cfApi.deleteOrgUserPermissions, + organization: cfApi.deleteOrgUserPermissions, space: cfApi.deleteSpaceUserPermissions }; const api = apiMethodMap[entityType]; @@ -296,52 +296,55 @@ const userActions = { }, createUserAndAssociate(userGuid) { - let entityGuid; const entityType = UserStore.currentlyViewedType; + const orgGuid = OrgStore.currentOrgGuid; + let cfApiRequest; + let entityGuid; if (entityType === ORG_NAME) { - entityGuid = OrgStore.currentOrgGuid; + entityGuid = orgGuid; + cfApiRequest = cfApi.putAssociateUserToOrganization.bind(cfApi, userGuid, entityGuid); + + AppDispatcher.handleViewAction({ + type: userActionTypes.USER_ORG_ASSOCIATE, + userGuid, + entityType, + entityGuid + }); } else { entityGuid = SpaceStore.currentSpaceGuid; + cfApiRequest = cfApi.putAssociateUserToSpace.bind(cfApi, userGuid, orgGuid, entityGuid); } - AppDispatcher.handleViewAction({ - type: userActionTypes.USER_ORG_ASSOCIATE, - userGuid, - entityType, - entityGuid - }); - - return cfApi.putAssociateUserToEntity(userGuid, entityGuid, entityType) + return cfApiRequest() .then(() => userActions.fetchEntityUsers(entityGuid, entityType)) - .then(entityUsers => userActions.createdUserAndAssociated(userGuid, entityGuid, entityUsers)); + .then(entityUsers => { + userActions.createdUserAndAssociated(userGuid, entityGuid, entityUsers, entityType); + }); }, fetchEntityUsers(entityGuid, entityType) { - let entityUsers; - if (entityType === ORG_NAME) { - entityUsers = cfApi.fetchOrgUsers(entityGuid); - } else { - entityUsers = cfApi.fetchSpaceUserRoles(entityGuid); - } - return Promise.resolve(entityUsers); + return cfApi[(entityType === ORG_NAME) ? 'fetchOrgUsers' : 'fetchSpaceUserRoles'](entityGuid); }, - createdUserAndAssociated(userGuid, entityGuid, entityUsers) { - const user = entityUsers.filter((entityUser) => entityUser.guid === userGuid); + createdUserAndAssociated(userGuid, entityGuid, entityUsers, entityType) { + const user = entityUsers.filter(entityUser => entityUser.guid === userGuid)[0]; - if (!user[0]) { + if (!user) { const err = new Error('User was not associated'); const message = `The user ${userGuid} was not associated in ${entityGuid}.`; return Promise.resolve(userActions.userInviteCreateError(err, message)); } - AppDispatcher.handleViewAction({ - type: userActionTypes.USER_ORG_ASSOCIATED, - userGuid, - entityGuid, - user - }); + if (entityType === ORG_NAME) { + AppDispatcher.handleViewAction({ + type: userActionTypes.USER_ORG_ASSOCIATED, + userGuid, + entityGuid, + user + }); + } + return Promise.resolve(user); }, @@ -433,12 +436,14 @@ const userActions = { // TODO add error action return userActions .fetchCurrentUserInfo() - .then(userInfo => - Promise.all([ - userActions.fetchUser(userInfo.user_id), - userActions.fetchCurrentUserUaaInfo(userInfo.user_id) - ]) - ) + .then(userInfo => { + const userId = userInfo.user_id; + + return Promise.all([ + userActions.fetchUser(userId), + userActions.fetchCurrentUserUaaInfo(userId) + ]); + }) // Grab user from store with all merged properties .then(() => UserStore.currentUser) .then(userActions.receivedCurrentUser); diff --git a/static_src/components/user_list.jsx b/static_src/components/user_list.jsx index de89940e..df46eb04 100644 --- a/static_src/components/user_list.jsx +++ b/static_src/components/user_list.jsx @@ -162,6 +162,7 @@ export default class UserList extends React.Component { ); } + return ( { user.username } diff --git a/static_src/components/users.jsx b/static_src/components/users.jsx index 7cb1063d..f213fd40 100644 --- a/static_src/components/users.jsx +++ b/static_src/components/users.jsx @@ -13,11 +13,16 @@ import UserList from './user_list.jsx'; import UsersInvite from './users_invite.jsx'; import Notification from './notification.jsx'; import UserStore from '../stores/user_store.js'; - import ErrorMessage from './error_message.jsx'; +import PanelDocumentation from './panel_documentation.jsx'; +const propTypes = {}; const SPACE_NAME = SpaceStore.cfName; const ORG_NAME = OrgStore.cfName; +const ORG_MANAGER = 'org_manager'; +const SPACE_MANAGER = 'space_manager'; +const ORG_ENTITY = 'organization'; +const SPACE_ENTITY = 'space'; function stateSetter() { const currentOrgGuid = OrgStore.currentOrgGuid; @@ -35,15 +40,16 @@ function stateSetter() { users = UserStore.getAllInSpace(currentSpaceGuid); entityGuid = currentSpaceGuid; currentUserAccess = UserStore.hasRole(currentUser.guid, currentSpaceGuid, - 'space_manager'); + SPACE_MANAGER); } else { users = UserStore.getAllInOrg(currentOrgGuid); entityGuid = currentOrgGuid; currentUserAccess = UserStore.hasRole(currentUser.guid, currentOrgGuid, - 'org_manager'); + ORG_MANAGER); } return { + currentUser, error: UserStore.getError(), inviteDisabled, currentUserAccess, @@ -67,7 +73,7 @@ export default class Users extends React.Component { this.state = stateSetter(); this._onChange = this._onChange.bind(this); - this.handleRemove = this.handleRemove.bind(this); + this.handleRemoveUser = this.handleRemoveUser.bind(this); this.handleAddPermissions = this.handleAddPermissions.bind(this); this.handleRemovePermissions = this.handleRemovePermissions.bind(this); } @@ -86,105 +92,122 @@ export default class Users extends React.Component { } handleAddPermissions(roleKey, apiKey, userGuid) { - userActions.addUserRoles(roleKey, - apiKey, - userGuid, - this.entityGuid, - this.entityType); + userActions.addUserRoles( + roleKey, + apiKey, + userGuid, + this.entityGuid, + this.entityType + ); } handleRemovePermissions(roleKey, apiKey, userGuid) { - userActions.deleteUserRoles(roleKey, - apiKey, - userGuid, - this.entityGuid, - this.entityType); + userActions.deleteUserRoles( + roleKey, + apiKey, + userGuid, + this.entityGuid, + this.entityType + ); } - handleRemove(userGuid, ev) { + handleRemoveUser(userGuid, ev) { ev.preventDefault(); userActions.deleteUser(userGuid, this.state.currentOrgGuid); } get entityType() { - return this.state.currentType === ORG_NAME ? 'org' : 'space'; + return this.isOrganization ? ORG_ENTITY : SPACE_ENTITY; + } + + get isOrganization() { + return this.state.currentType === ORG_NAME; } get entityGuid() { - const entityGuid = this.state.currentType === ORG_NAME ? + const entityGuid = this.isOrganization ? this.state.currentOrgGuid : this.state.currentSpaceGuid; return entityGuid; } - _onChange() { - this.setState(stateSetter()); + get currentUserIsOrgManager() { + const { currentUser } = this.state; + const { currentOrgGuid } = OrgStore; + + return UserStore.hasRole(currentUser.guid, currentOrgGuid, ORG_MANAGER); } - render() { - let removeHandler; + get notification() { + const { userListNotices } = this.state; - if (this.state.currentType === ORG_NAME) { - removeHandler = this.handleRemove; - } + if (!userListNotices.description) return null; - let content = (); - - let notification; - let userInvite; - - if (this.state.userListNotices.description) { - const notice = this.state.userListNotices; - notification = ( - + return ( + + ); + } + + get userInvite() { + if (!this.currentUserIsOrgManager) { + const cfAPILink = 'https://cloud.gov/docs/getting-started/setup/#set-up-the-command-line'; + + return ( + + {`Currently, only an org manager can invite users to this ${this.entityType} + via the dashboard. If the user you want to add is already a cloud.gov member, + you can invite them using the cloud foundry api. + Speak to your org manager if you need to add a user to this ${this.entityType} who + is not a member of cloud.gov`} + ); - } else { - // If there's nothing, let's reset the notification to null. - notification = null; } - if (this.state.currentType === ORG_NAME) { - userInvite = ( -
- -
- ); + return ( + + ); + } + + _onChange() { + this.setState(stateSetter()); + } + + render() { + let removeHandler; + + if (this.isOrganization) { + removeHandler = this.handleRemoveUser; } return (
- { userInvite } - { notification } -
-
- { content } -
-
+ { this.userInvite } + { this.notification } +
); } } -Users.propTypes = {}; - -Users.defaultProps = {}; +Users.propTypes = propTypes; diff --git a/static_src/components/users_invite.jsx b/static_src/components/users_invite.jsx index 04fc7ad3..00c7b17d 100644 --- a/static_src/components/users_invite.jsx +++ b/static_src/components/users_invite.jsx @@ -1,27 +1,22 @@ - -import PropTypes from 'prop-types'; /** * Renders a form that allows org users to invite new users * to cloud.gov */ +import PropTypes from 'prop-types'; import React from 'react'; - import Action from './action.jsx'; import FormStore from '../stores/form_store'; import { Form, FormText } from './form'; import PanelDocumentation from './panel_documentation.jsx'; import userActions from '../actions/user_actions'; - import { validateEmail } from '../util/validators'; -import createStyler from '../util/create_styler'; -import style from 'cloudgov-style/css/cloudgov-style.css'; - const USERS_INVITE_FORM_GUID = 'users-invite-form'; const propTypes = { inviteDisabled: PropTypes.bool, + inviteEntityType: PropTypes.string.isRequired, currentUserAccess: PropTypes.bool, error: PropTypes.object }; @@ -44,8 +39,6 @@ export default class UsersInvite extends React.Component { this.state = stateSetter(props); - this.styler = createStyler(style); - this.validateEmail = validateEmail().bind(this); this._onValidForm = this._onValidForm.bind(this); } @@ -65,55 +58,60 @@ export default class UsersInvite extends React.Component { } get errorMessage() { - const err = this.props.error; - if (!err) return undefined; - const message = err.contextualMessage; - if (err.message) { - return `${message}: ${err.message}.`; + const { error } = this.props; + + if (!error) return ''; + + const message = error.contextualMessage; + + if (error.message) { + return `${message}: ${error.message}.`; } + return message; } + get invitationMessage() { + const entity = this.props.inviteEntityType; + + return `Invite a new user to cloud.gov and this ${entity}` + + ` or add an existing user to this ${entity}.`; + } + render() { - let content; - - if (this.props.currentUserAccess) { - content = ( -
- -

Invite new user to cloud.gov and this organization, or add an existing user to this - organization.

-
-
- - - Add user to this organization - - -
- ); - } else { - content = ''; + const { inviteDisabled } = this.props; + + if (!this.props.currentUserAccess) { + return null; } + return (
- {content} + +

{ this.invitationMessage }

+
+
+ + + Add user to this { this.state.inviteEntityType } + +
); } diff --git a/static_src/stores/base_store.js b/static_src/stores/base_store.js index 2cc518c9..115ddbe7 100644 --- a/static_src/stores/base_store.js +++ b/static_src/stores/base_store.js @@ -160,6 +160,7 @@ export default class BaseStore extends EventEmitter { } else { this._data = this._data.push(toMerge); } + return cb(true); } diff --git a/static_src/stores/user_store.js b/static_src/stores/user_store.js index 18520812..0526d28a 100644 --- a/static_src/stores/user_store.js +++ b/static_src/stores/user_store.js @@ -54,11 +54,14 @@ export class UserStore extends BaseStore { } case userActionTypes.SPACE_USER_ROLES_RECEIVED: { - const updatedUsers = this.mergeRoles(action.users, action.spaceGuid, - 'space_roles'); + const { users, spaceGuid } = action; - this.mergeMany('guid', updatedUsers, () => { }); + // Force an update to the cached list of users, otherwise mergeRoles + // won't be able to find the new user + this.mergeMany('guid', users, () => {}); + this.mergeMany('guid', this.mergeRoles(users, spaceGuid, 'space_roles'), () => {}); this.emitChange(); + break; } @@ -70,16 +73,20 @@ export class UserStore extends BaseStore { } case userActionTypes.USER_ORG_ASSOCIATED: { - const user = Object.assign({}, - { guid: action.userGuid, roles: { [action.entityGuid]: [] } }, - action.user); + const user = Object.assign({}, { + guid: action.userGuid, + roles: { [action.entityGuid]: [] } + }, action.user); this._inviteInputActive = true; + if (!this.get(user.guid)) { this.push(user); } else { this.merge('guid', user, () => {}); } + this.emitChange(); + break; } @@ -152,6 +159,7 @@ export class UserStore extends BaseStore { break; } + // TODO: this should not be happening in the user store case userActionTypes.USER_DELETE: { // Nothing should happen. break; @@ -303,6 +311,7 @@ export class UserStore extends BaseStore { const usersInOrg = this._data.filter((user) => !!user.get('roles') && !!user.get('roles').get(orgGuid) ); + return usersInOrg.toJS(); } @@ -378,7 +387,6 @@ export class UserStore extends BaseStore { get currentlyViewedType() { return this._currentViewedType; } - } const _UserStore = new UserStore(); diff --git a/static_src/test/unit/actions/user_actions.spec.js b/static_src/test/unit/actions/user_actions.spec.js index d91dc573..1bcaa5d6 100644 --- a/static_src/test/unit/actions/user_actions.spec.js +++ b/static_src/test/unit/actions/user_actions.spec.js @@ -512,34 +512,34 @@ describe('userActions', function() { }); }); - describe('createdUserAndAssociated', function () { - let userGuid; - let orgUsers; - let orgGuid; - let expectedParams; + describe('createdUserAndAssociated', () => { + const userGuid = "fake-udid"; + const orgGuid = "fake-org-udid"; + const user = { + guid: userGuid, + username: 'asdf' + }; + const orgUsers = [user, {guid: 'wrong-udid'}, {guid: 'wrong-udid-2'}]; let spy; - let user; - beforeEach(function (done) { - userGuid = "fake-udid"; - orgGuid = "fake-org-udid"; - user = { - guid: userGuid, - username: 'asdf' - }; - orgUsers = [user, {userGuid: 'wrong-udid'}, {userGuid: 'wrong-udid-2'}]; - expectedParams = { - userGuid, - orgGuid, - user: orgUsers[0] - }; + beforeEach(() => { spy = setupViewSpy(sandbox); - userActions.createdUserAndAssociated(userGuid, orgGuid, orgUsers) - .then(done, done.fail); }); - it('should dispatch USER_ORG_ASSOCIATED notice with user and org', function() { - assertAction(spy, userActionTypes.USER_ORG_ASSOCIATED); + describe('when entityType is org_users', () => { + it('should dispatch USER_ORG_ASSOCIATED notice with user and org', (done) => { + userActions.createdUserAndAssociated(userGuid, orgGuid, orgUsers, 'org_users') + .then(done, done.fail); + assertAction(spy, userActionTypes.USER_ORG_ASSOCIATED); + }); + }); + + describe('when entityType is not org_users', () => { + it('should not dispatch USER_ORG_ASSOCIATED notice with user and org', (done) => { + userActions.createdUserAndAssociated(userGuid, orgGuid, orgUsers) + .then(done, done.fail); + expect(spy.called).toBe(false); + }); }); }); @@ -550,7 +550,7 @@ describe('userActions', function() { expectedApiKey = 'managers', expectedUserGuid = 'akdfjadzxcvzxcvzxvzx', expectedGuid = '2eve2v2vadsfa', - expectedType = 'org'; + expectedType = 'organization'; // expectedParams for the params dispatched with userActionTypes.USER_ROLES_ADD. // the apiKey is not sent with it. @@ -600,7 +600,7 @@ describe('userActions', function() { apiKey, userGuid, orgGuid, - 'org' + 'organization' ).then(done, done.fail); }); @@ -619,7 +619,7 @@ describe('userActions', function() { roles, userGuid, orgGuid, - 'org' + 'organization' )); }); }); @@ -653,7 +653,7 @@ describe('userActions', function() { apiKey, userGuid, orgGuid, - 'org' + 'organization' ).then(done, done.fail); }); @@ -839,7 +839,7 @@ describe('userActions', function() { apiKey, userGuid, orgGuid, - 'org' + 'organization' ).then(done, done.fail); }); @@ -858,7 +858,7 @@ describe('userActions', function() { roles, userGuid, orgGuid, - 'org' + 'organization' )); }); @@ -896,7 +896,7 @@ describe('userActions', function() { apiKey, userGuid, orgGuid, - 'org' + 'organization' ).then(done, done.fail); }); diff --git a/static_src/test/unit/components/user_role_list_control.spec.jsx b/static_src/test/unit/components/user_role_list_control.spec.jsx index 28ddcc4e..176253f8 100644 --- a/static_src/test/unit/components/user_role_list_control.spec.jsx +++ b/static_src/test/unit/components/user_role_list_control.spec.jsx @@ -6,15 +6,7 @@ import { shallow } from 'enzyme'; import UserRoleListControl from '../../../components/user_role_list_control.jsx'; describe('', function () { - let userRoleListControl, sandbox; - - beforeEach(function () { - sandbox = sinon.sandbox.create(); - }); - - afterEach(function () { - sandbox.restore(); - }); + let userRoleListControl; describe('with an org user', function () { const orgGuid = 'org-123'; diff --git a/static_src/test/unit/components/users.spec.jsx b/static_src/test/unit/components/users.spec.jsx index 7e57e66e..28ff3863 100644 --- a/static_src/test/unit/components/users.spec.jsx +++ b/static_src/test/unit/components/users.spec.jsx @@ -1,52 +1,83 @@ import '../../global_setup.js'; import React from 'react'; +import Immutable from 'immutable'; +import sinon from 'sinon'; import { shallow } from 'enzyme'; import Users from '../../../components/users.jsx'; +import PanelDocumentation from '../../../components/panel_documentation.jsx'; +import UsersInvite from '../../../components/users_invite.jsx'; import UserStore from '../../../stores/user_store'; +import SpaceStore from '../../../stores/space_store'; -describe('', function () { - let users, sandbox; +const buildRoles = (spaceGuid, roles = []) => { + const obj = {}; + obj[spaceGuid] = roles; - beforeEach(function () { - sandbox = sinon.sandbox.create(); - }); + return obj; +}; - afterEach(function () { - sandbox.restore(); - }); +describe('', () => { + const userGuid = 'a-user-guid'; + const spaceGuid = 'space-guid'; + const user = { + guid: userGuid, + roles: buildRoles(spaceGuid, ['org_manager']) + }; + + let users; - describe('with a user', function () { - beforeEach(function () { - const userGuid = 'a-user-guid'; - const user = { - guid: userGuid - }; - UserStore._currentUserGuid = userGuid; - UserStore.push(user); + SpaceStore._currentSpaceGuid = spaceGuid; + UserStore._currentUserGuid = userGuid; + describe('with a user', () => { + beforeEach(() => { + UserStore._data = Immutable.fromJS([user]); users = shallow(); }); - describe('when at org level', function () { - beforeEach(function () { + describe('when at org level', () => { + it('has an `entityType` of organization', () => { users.setState({ currentType: 'org_users' }); + const actual = users.instance().entityType; + + expect(actual).toEqual('organization'); }); + }); - it('doesnt have permissions to edit users', function () { + describe('when at space level', () => { + it('has an `entityType` of space', () => { + users.setState({ currentType: 'space_users' }); const actual = users.instance().entityType; - expect(actual).toEqual('org'); + + expect(actual).toEqual('space'); }); }); - describe('when at space level', function () { - beforeEach(function () { - users.setState({ currentType: 'space_users' }); + describe('when a user is an org manager', () => { + afterEach(() => { + sinon.restore(UserStore); }); - it('doesnt have permissions to edit users', function () { - const actual = users.instance().entityType; - expect(actual).toEqual('space'); + it('renders a component', () => { + sinon.stub(UserStore, 'hasRole').returns(true); + users = shallow(); + + expect(users.find(UsersInvite).length).toBe(1); + }); + }); + + describe('when a user is not an org manager', () => { + it('renders message telling user to ask an org manager to add users', () => { + const spaceUser = Object.assign({}, user, { + roles: buildRoles(spaceGuid, ['space_manager']) + }); + + UserStore._data = Immutable.fromJS([spaceUser]); + users = shallow(); + + expect(users.find(UsersInvite).length).toBe(0); + expect(users.find(PanelDocumentation).length).toBe(1); }); }); }); diff --git a/static_src/test/unit/components/users_invite.spec.jsx b/static_src/test/unit/components/users_invite.spec.jsx index 5ad24609..6d4edb45 100644 --- a/static_src/test/unit/components/users_invite.spec.jsx +++ b/static_src/test/unit/components/users_invite.spec.jsx @@ -4,32 +4,58 @@ import React from 'react'; import { shallow } from 'enzyme'; import { Form } from '../../../components/form'; +import PanelDocumentation from '../../../components/panel_documentation.jsx'; import UsersInvite from '../../../components/users_invite.jsx'; import Action from '../../../components/action.jsx'; describe('', function () { - let sandbox; - - beforeEach(function () { - sandbox = sinon.sandbox.create(); - }); - - afterEach(function () { - sandbox.restore(); + const entityType = 'space'; + const props = { + inviteEntityType: entityType, + currentUserAccess: true + }; + let wrapper; + + describe('when user has access to inviting other users', () => { + beforeEach(() => { + wrapper = shallow(); + }); + + it('renders one
component', () => { + expect(wrapper.find(Form).length).toEqual(1); + }); + + it('renders one component', () => { + expect(wrapper.find(Action).length).toEqual(1); + }); + + describe('conditional documentation based on inviteEntityType', () => { + it('refers to `space` when type is space', () => { + const doc = 'Invite a new user to cloud.gov and this space' + + ' or add an existing user to this space.'; + + expect(wrapper.find(PanelDocumentation).find('p').text()).toBe(doc); + }); + + it('refers to `organization` when type is organization', () => { + const doc = 'Invite a new user to cloud.gov and this organization' + + ' or add an existing user to this organization.'; + const orgProps = Object.assign({}, props, { + inviteEntityType: 'organization' + }); + wrapper = shallow(); + + expect(wrapper.find(PanelDocumentation).find('p').text()).toBe(doc); + }); + }); }); - it('doesnt renders components if currentUser doesnt have access', () => { - const userInvite = shallow(); - expect(userInvite.find(Form).length).toEqual(1); - }); - - it('renders one components', () => { - const userInvite = shallow(); - expect(userInvite.find(Form).length).toEqual(1); - }); + describe('when user does not have ability to invite other users', () => { + it('does not render component', () => { + const noAccessProps = Object.assign({}, props, { currentUserAccess: false }); + wrapper = shallow(); - it('renders one components', () => { - const userInvite = shallow(); - expect(userInvite.find(Action).length).toEqual(1); + expect(wrapper.find(Form).length).toEqual(0); + }); }); }); diff --git a/static_src/test/unit/stores/user_store.spec.js b/static_src/test/unit/stores/user_store.spec.js index 8bb480a1..08d0ddf9 100644 --- a/static_src/test/unit/stores/user_store.spec.js +++ b/static_src/test/unit/stores/user_store.spec.js @@ -108,13 +108,14 @@ describe('UserStore', function () { }); }); - describe('on space user roles received', function() { - let expectedUsers; + describe('on space user roles received', () => { const userGuidA = 'user-a'; const userGuidB = 'user-b'; const spaceGuid = 'space-123'; + let expectedUsers; + let spy; - beforeEach(function() { + beforeEach(() => { const spaceUserRoles = [ { guid: userGuidA, @@ -134,22 +135,25 @@ describe('UserStore', function () { expectedUsers = [ { guid: userGuidA, - roles: { [spaceGuid]: ['space_developer'] } + roles: { [spaceGuid]: ['space_developer'] }, + space_roles: [ 'space_developer' ] }, { guid: userGuidB, - roles: { [spaceGuid]: ['space_developer', 'space_manager'] } + roles: { [spaceGuid]: ['space_developer', 'space_manager'] }, + space_roles: [ 'space_developer', 'space_manager' ] } - ] + ]; - UserStore.push(currentUsers[0]); - sandbox.spy(UserStore, 'emitChange'); + UserStore._data = Immutable.fromJS(currentUsers); + spy = sandbox.spy(UserStore, 'emitChange'); userActions.receivedSpaceUserRoles(spaceUserRoles, spaceGuid); }); - afterEach(function() { + afterEach(() => { UserStore._data = []; + spy.restore(UserStore); }); it('should emit a change event', function() { @@ -180,8 +184,7 @@ describe('UserStore', function () { expect(spy).toHaveBeenCalledOnce(); }); - it('should merge and update new users with existing users in data', - function() { + it('should merge and update new users with existing users in data', () => { const userGuid = 'user-75384'; const orgGuid = 'org-534789'; const currentUsers = [ @@ -224,19 +227,19 @@ describe('UserStore', function () { }); }); - describe('on org user associated', function() { + describe('on org user associated', () => { const userGuid = 'user-543'; const orgGuid = 'org-abc'; - let orgUsers - beforeEach(function() { - UserStore._data = Immutable.List(); - sandbox.spy(UserStore, 'emitChange'); + let orgUsers; + + beforeEach(() => { const user = { guid: userGuid, username: 'person@person.com' }; orgUsers = [user, {userGuid: 'wrong-udid'}, {userGuid: 'wrong-udid-2'}]; - userActions.createdUserAndAssociated(userGuid, orgGuid, orgUsers); + sandbox.spy(UserStore, 'emitChange'); + userActions.createdUserAndAssociated(userGuid, orgGuid, orgUsers, 'org_users'); }); it('should emit a change', function() { @@ -318,7 +321,7 @@ describe('UserStore', function () { expectedApiKey, expectedUserGuid, expectedOrgGuid, - 'org' + 'organization' ); expect(spy).toHaveBeenCalledOnce(); diff --git a/static_src/test/unit/util/cf_api.spec.js b/static_src/test/unit/util/cf_api.spec.js index ce8423e9..d4ced85e 100644 --- a/static_src/test/unit/util/cf_api.spec.js +++ b/static_src/test/unit/util/cf_api.spec.js @@ -117,35 +117,20 @@ describe('cfApi', function() { }); }); - describe('putAssociateUserToEntity()', function() { - it('associate a user to org when entityType is org_users', function(done) { - const entityType = 'org_users'; - const entityGuid = 'fake-org-guid'; - const userGuid = 'fake-user-guid'; - const spy = sandbox.stub(http, 'put'); - const users = [{ userGuid: 'user-guid' }]; - sandbox.stub(cfApi, 'fetchEntityName').returns(entityType); - sandbox.stub(userActions, 'fetchEntityUsers').returns(users); - spy.returns(createPromise({ data: {}})); - cfApi.putAssociateUserToEntity(userGuid, entityGuid, entityType).then(() => { - const args = spy.getCall(0).args; - expect(spy).toHaveBeenCalledOnce(); - expect(args[0]).toMatch(`/organizations/${entityGuid}/users/${userGuid}`); - done(); - }); - }); - - it('associate a user to space when entityType is space_users', function(done) { - const entityType = 'space_users'; + describe('putAssociateUserToSpace()', () => { + it('associates a user to a space', () => { + const orgGuid = 'an-org'; const entityGuid = 'fake-space-guid'; const userGuid = 'fake-user-guid'; const spy = sandbox.stub(http, 'put'); - const users = [{ userGuid: 'user-guid' }]; - sandbox.stub(cfApi, 'fetchEntityName').returns(entityType); - sandbox.stub(userActions, 'fetchEntityUsers').returns(users); + const putUserToOrgSpy = sandbox.stub(cfApi, 'putAssociateUserToOrganization'); + spy.returns(createPromise({ data: {}})); - cfApi.putAssociateUserToEntity(userGuid, entityGuid, entityType).then(() => { + putUserToOrgSpy.returns(createPromise()); + + cfApi.putAssociateUserToSpace(userGuid, orgGuid, entityGuid).then(() => { const args = spy.getCall(0).args; + expect(putUserToOrgSpy).toHaveBeenCalledOnce(); expect(spy).toHaveBeenCalledOnce(); expect(args[0]).toMatch(`/spaces/${entityGuid}/auditors/${userGuid}`); done(); diff --git a/static_src/util/cf_api.js b/static_src/util/cf_api.js index d326b294..573d03c9 100644 --- a/static_src/util/cf_api.js +++ b/static_src/util/cf_api.js @@ -1,8 +1,6 @@ import http from 'axios'; import { noticeError } from '../util/analytics.js'; -import SpaceStore from '../stores/space_store.js'; -import OrgStore from '../stores/org_store.js'; import domainActions from '../actions/domain_actions.js'; import errorActions from '../actions/error_actions.js'; import quotaActions from '../actions/quota_actions.js'; @@ -10,8 +8,6 @@ import routeActions from '../actions/route_actions.js'; import userActions from '../actions/user_actions.js'; const APIV = '/v2'; -const ORG_NAME = OrgStore.cfName; -const SPACE_NAME = SpaceStore.cfName; // An error from the CF v2 API function CfApiV2Error(response) { @@ -432,24 +428,14 @@ export default { }); }, - putAssociateUserToEntity(userGuid, entityGuid, entityType) { - let resp; - const entityName = this.fetchEntityName(entityType); - if (ORG_NAME === entityName) { - resp = this.putAssociateUserToOrganization(userGuid, entityGuid); - } else { - resp = this.putAssociateUserToSpace(userGuid, entityGuid); - } - return Promise.resolve(resp); - }, - putAssociateUserToOrganization(userGuid, orgGuid) { return http.put(`${APIV}/organizations/${orgGuid}/users/${userGuid}`) .then((res) => this.formatSplitResponse(res.data)); }, - putAssociateUserToSpace(userGuid, spaceGuid) { - return http.put(`${APIV}/spaces/${spaceGuid}/auditors/${userGuid}`) + putAssociateUserToSpace(userGuid, orgGuid, spaceGuid) { + return this.putAssociateUserToOrganization(userGuid, orgGuid) + .then(() => http.put(`${APIV}/spaces/${spaceGuid}/auditors/${userGuid}`)) .then((res) => this.formatSplitResponse(res.data)); }, @@ -603,16 +589,6 @@ export default { }); }, - fetchEntityName(entityType) { - let entityName; - if (entityType === 'org_users') { - entityName = ORG_NAME; - } else { - entityName = SPACE_NAME; - } - return entityName; - }, - fetchUser(userGuid) { return this.fetchOne(`/users/${userGuid}`); },