diff --git a/package-lock.json b/package-lock.json index 2e74024dc2d..09f0cea6962 100644 --- a/package-lock.json +++ b/package-lock.json @@ -64966,7 +64966,7 @@ }, "packages/common": { "name": "@esri/hub-common", - "version": "14.0.0", + "version": "14.3.1", "license": "Apache-2.0", "dependencies": { "abab": "^2.0.5", @@ -65090,7 +65090,7 @@ }, "packages/sites": { "name": "@esri/hub-sites", - "version": "14.0.0", + "version": "14.0.1", "license": "Apache-2.0", "dependencies": { "tslib": "^1.13.0" diff --git a/packages/common/src/groups/HubGroups.ts b/packages/common/src/groups/HubGroups.ts index 7b599f9a35a..0e0b2fabaf3 100644 --- a/packages/common/src/groups/HubGroups.ts +++ b/packages/common/src/groups/HubGroups.ts @@ -3,7 +3,7 @@ import { fetchGroupEnrichments } from "./_internal/enrichments"; import { getProp, setProp } from "../objects"; import { getGroupThumbnailUrl, IHubSearchResult } from "../search"; import { parseInclude } from "../search/_internal/parseInclude"; -import { IHubRequestOptions, IModel } from "../types"; +import { IHubRequestOptions } from "../types"; import { getGroupHomeUrl } from "../urls"; import { unique } from "../util"; import { mapBy } from "../utils"; diff --git a/packages/common/src/groups/_internal/AddOrInviteUsersToGroupUtils.ts b/packages/common/src/groups/_internal/AddOrInviteUsersToGroupUtils.ts new file mode 100644 index 00000000000..a824bcd64d8 --- /dev/null +++ b/packages/common/src/groups/_internal/AddOrInviteUsersToGroupUtils.ts @@ -0,0 +1,231 @@ +import { + IAddOrInviteContext, + IAddOrInviteResponse, + IUserOrgRelationship, + IUserWithOrgType, +} from "../types"; + +import { processAutoAddUsers } from "./processAutoAddUsers"; +import { processInviteUsers } from "./processInviteUsers"; + +// Add or invite flow based on the type of user begins here + +/** + * @private + * Handles add/invite logic for collaboration coordinators inside partnered orgs. + * This is intentionally split out from the invitation of partnered org normal members, + * because the two types of partnered org usres (regular and collaboration coordinator) + * always come from the same 'bucket', however have distinctly different add paths Invite vs auto add. + * It returns either an empty instance of the addOrInviteResponse + * object, or their response from auto adding users. + * + * @export + * @param {IAddOrInviteContext} context context object + * @return {IAddOrInviteResponse} response object + */ +export async function addOrInviteCollaborationCoordinators( + context: IAddOrInviteContext +): Promise { + // If there are no org users return handling no users + if ( + !context.collaborationCoordinator || + context.collaborationCoordinator.length === 0 + ) { + // we return an empty object because + // if you leave out any of the props + // from the final object and you are concatting together arrays you can concat + // an undeifined inside an array which will throw off array lengths. + return handleNoUsers(); + } + return processAutoAddUsers(context, "collaborationCoordinator"); +} + +/** + * @private + * Handles add/invite logic for community users + * It returns either an empty instance of the addOrInviteResponse + * object, or either ther esponse from processing auto adding + * users or inviting users. If an email has been passed in it also notifies + * processAutoAddUsers that emails should be sent. + * + * @export + * @param {IAddOrInviteContext} context context object + * @return {IAddOrInviteResponse} response object + */ +export async function addOrInviteCommunityUsers( + context: IAddOrInviteContext +): Promise { + // We default to handleNoUsers + // we return an empty object because + // if you leave out any of the props + // from the final object and you are concatting together arrays you can concat + // an undeifined inside an array which will throw off array lengths. + let fnToCall = handleNoUsers; + let shouldEmail = false; + + // If community users were passed in... + if (context.community && context.community.length > 0) { + // Default to either autoAdd or invite based on canAutoAddUser. + fnToCall = context.canAutoAddUser + ? processAutoAddUsers + : processInviteUsers; + // If we have an email object + // Then we will auto add... + // But whether or not we email is still in question + if (context.email) { + // If the email object has the groupId property... + if (context.email.hasOwnProperty("groupId")) { + // If the email objects groupId property is the same as the current groupId in context... + // (This function is part of a flow that could work for N groupIds) + if (context.email.groupId === context.groupId) { + // Then we auto add and send email + fnToCall = processAutoAddUsers; + shouldEmail = true; + } // ELSE if the groupId's do NOT match, we will fall back + // To autoAdd or invite as per line 32. + // We are doing the above logic (lines 43 - 47) because + // We wish to add users to core groups, followers, and content groups + // but only to email the core group. + } else { + // If it does not have a groupId at all then we will autoAdd and email. + fnToCall = processAutoAddUsers; + shouldEmail = true; + } + } + } + // Return/call the function + return fnToCall(context, "community", shouldEmail); +} + +/** + * @private + * Handles add/invite logic for Org users + * It returns either an empty instance of the addOrInviteResponse + * object, or either ther esponse from processing auto adding a users or inviting a user + * + * @export + * @param {IAddOrInviteContext} context context object + * @return {IAddOrInviteResponse} response object + */ +export async function addOrInviteOrgUsers( + context: IAddOrInviteContext +): Promise { + // If there are no org users return handling no users + if (!context.org || context.org.length === 0) { + // we return an empty object because + // if you leave out any of the props + // from the final object and you are concatting together arrays you can concat + // an undeifined inside an array which will throw off array lengths. + return handleNoUsers(); + } + // for org user if you have assignUsers then auto add the user + // if not then invite the user + return context.canAutoAddUser + ? processAutoAddUsers(context, "org") + : processInviteUsers(context, "org"); +} + +/** + * @private + * Handles add/invite logic for partnered org users. + * It returns either an empty instance of the addOrInviteResponse + * object, or their response from inviting users. + * + * @export + * @param {IAddOrInviteContext} context context object + * @return {IAddOrInviteResponse} response object + */ +export async function addOrInvitePartneredUsers( + context: IAddOrInviteContext +): Promise { + // If there are no org users return handling no users + if (!context.partnered || context.partnered.length === 0) { + // we return an empty object because + // if you leave out any of the props + // from the final object and you are concatting together arrays you can concat + // an undeifined inside an array which will throw off array lengths. + return handleNoUsers(); + } + // process invite + return processInviteUsers(context, "partnered"); +} + +/** + * @private + * Handles add/invite logic for world users + * It either returns an empty instance of the add/invite response + * object, or a populated version from processInviteUsers + * + * @export + * @param {IAddOrInviteContext} context Context object + * @return {IAddOrInviteResponse} Response object + */ +export async function addOrInviteWorldUsers( + context: IAddOrInviteContext +): Promise { + // If there are no world users return handling no users + if (!context.world || context.world.length === 0) { + // we return an empty object because + // if you leave out any of the props + // from the final object and you are concatting together arrays you can concat + // an undeifined inside an array which will throw off array lengths. + return handleNoUsers(); + } + // process invite + return processInviteUsers(context, "world"); +} + +// Add or invite flow based on the type of user ends here + +/** + * @private + * Returns an empty instance of the addorinviteresponse object. + * We are using this because if you leave out any of the props + * from the final object and you are concatting together arrays you can concat + * an undeifined inside an array which will throw off array lengths. + * + * @export + * @return {IAddOrInviteResponse} + */ +export async function handleNoUsers( + context?: IAddOrInviteContext, + userType?: "world" | "org" | "community" | "partnered", + shouldEmail?: boolean +): Promise { + return { + notAdded: [], + notEmailed: [], + notInvited: [], + users: [], + errors: [], + }; +} + +/** + * @private + * Takes users array and sorts them into an object by the type of user they are + * based on the orgType prop (world|org|community) + * + * @export + * @param {IUserWithOrgType[]} users array of users + * @return {IUserOrgRelationship} Object of users sorted by type (world, org, community) + */ +export function groupUsersByOrgRelationship( + users: IUserWithOrgType[] +): IUserOrgRelationship { + return users.reduce( + (acc, user) => { + // keyof needed to make bracket notation work without TS throwing a wobbly. + const orgType = user.orgType as keyof IUserOrgRelationship; + acc[orgType].push(user); + return acc; + }, + { + world: [], + org: [], + community: [], + partnered: [], + collaborationCoordinator: [], + } + ); +} diff --git a/packages/common/src/groups/_internal/autoAddUsersAsAdmins.ts b/packages/common/src/groups/_internal/autoAddUsersAsAdmins.ts new file mode 100644 index 00000000000..a3f534197e6 --- /dev/null +++ b/packages/common/src/groups/_internal/autoAddUsersAsAdmins.ts @@ -0,0 +1,33 @@ +import { + IAddGroupUsersResult, + IUser, + addGroupUsers, +} from "@esri/arcgis-rest-portal"; +import { IAuthenticationManager } from "@esri/arcgis-rest-request"; + +/** + * @private + * Auto add N users to a single group, with users added as admins of that group + * + * @export + * @param {string} id Group ID + * @param {IUser[]} admins array of users to add to group as admin + * @param {IAuthenticationManager} authentication authentication manager + * @return {IAddGroupUsersResult} Result of the transaction (null if no users are passed in) + */ +export function autoAddUsersAsAdmins( + id: string, + admins: IUser[], + authentication: IAuthenticationManager +): Promise { + let response = Promise.resolve(null); + if (admins.length) { + const args = { + id, + admins: admins.map((a) => a.username), + authentication, + }; + response = addGroupUsers(args); + } + return response; +} diff --git a/packages/common/src/groups/_internal/processAutoAddUsers.ts b/packages/common/src/groups/_internal/processAutoAddUsers.ts new file mode 100644 index 00000000000..55e9c18dab2 --- /dev/null +++ b/packages/common/src/groups/_internal/processAutoAddUsers.ts @@ -0,0 +1,82 @@ +import { IUser } from "@esri/arcgis-rest-types"; +import { IAddOrInviteContext, IAddOrInviteResponse } from "../types"; +import { getProp } from "../../objects/get-prop"; +import { ArcGISRequestError } from "@esri/arcgis-rest-request"; +import { autoAddUsers } from "../autoAddUsers"; +import { processEmailUsers } from "./processEmailUsers"; +import { autoAddUsersAsAdmins } from "./autoAddUsersAsAdmins"; + +/** + * @private + * Governs logic for automatically adding N users to a group. + * Users are added as either a regular user OR as an administrator of the group + * depending on the addUserAsGroupAdmin prop on the IAddOrInviteContext. + * If there is an email object on the IAddOrInviteContext, then email notifications are sent. + * + * @export + * @param {IAddOrInviteContext} context context object + * @param {string} userType what type of user is it: org | world | community + * @param {boolean} [shouldEmail=false] should the user be emailed? + * @return {IAddOrInviteResponse} response object + */ +export async function processAutoAddUsers( + context: IAddOrInviteContext, + userType: + | "world" + | "org" + | "community" + | "partnered" + | "collaborationCoordinator", + shouldEmail: boolean = false +): Promise { + // fetch users out of context object + const users: IUser[] = getProp(context, userType); + let autoAddResponse; + let emailResponse; + let notAdded: string[] = []; + let errors: ArcGISRequestError[] = []; + // fetch addUserAsGroupAdmin out of context + const { addUserAsGroupAdmin } = context; + + if (addUserAsGroupAdmin) { + // if is core group we elevate user to admin + autoAddResponse = await autoAddUsersAsAdmins( + getProp(context, "groupId"), + users, + getProp(context, "primaryRO") + ); + } else { + // if not then we are just auto adding them + autoAddResponse = await autoAddUsers( + getProp(context, "groupId"), + users, + getProp(context, "primaryRO") + ); + } + // handle notAdded users + if (autoAddResponse.notAdded) { + notAdded = notAdded.concat(autoAddResponse.notAdded); + } + // Merge errors into empty array + if (autoAddResponse.errors) { + errors = errors.concat(autoAddResponse.errors); + } + // run email process + if (shouldEmail) { + emailResponse = await processEmailUsers(context); + // merge errors in to overall errors array to keep things flat + if (emailResponse.errors && emailResponse.errors.length > 0) { + errors = errors.concat(emailResponse.errors); + } + } + // if you leave out any of the props + // from the final object and you are concatting together arrays you can concat + // an undeifined inside an array which will throw off array lengths. + return { + users: users.map((u) => u.username), + notAdded, + errors, + notEmailed: emailResponse?.notEmailed || [], + notInvited: [], + }; +} diff --git a/packages/common/src/groups/_internal/processEmailUsers.ts b/packages/common/src/groups/_internal/processEmailUsers.ts new file mode 100644 index 00000000000..1e5764a5c29 --- /dev/null +++ b/packages/common/src/groups/_internal/processEmailUsers.ts @@ -0,0 +1,60 @@ +import { IUser } from "@esri/arcgis-rest-types"; +import { IAddOrInviteContext, IAddOrInviteResponse } from "../types"; +import { getProp } from "../../objects/get-prop"; +import { ArcGISRequestError } from "@esri/arcgis-rest-request"; +import { emailOrgUsers } from "../emailOrgUsers"; + +/** + * @private + * Governs the logic for emailing N users. It acts under the assumption + * that all the 'community' users are the ones being emailed (this is due to platform rules we conform to) + * Function is called upstream depending on if an email object is attached to the context. + * Email object contains its own auth as it'll require the community admin to send the email itself. + * An individual email call goes out for each user due to how the response of multiple users in a single call works. + * + * @export + * @param {IAddOrInviteContext} context context object + * @return {IAddOrInviteResponse} response object + */ +export async function processEmailUsers( + context: IAddOrInviteContext +): Promise { + // Fetch users out of context. We only email community users so we are + // explicit about that + const users: IUser[] = getProp(context, "community"); + const notEmailed: string[] = []; + let errors: ArcGISRequestError[] = []; + // iterate through users as we want a distinct email call per user due to how + // batch email will only respond with success: true/false + // and if there is an error then it gets priority even though successes do still go through + for (const user of users) { + // Make email call... + const emailResponse = await emailOrgUsers( + [user], + getProp(context, "email.message"), + getProp(context, "email.auth"), + true + ); + // If it's just a failed email + // then add username to notEmailed array + if (!emailResponse.success) { + notEmailed.push(user.username); + // If there was a legit error + // Then only the error returns from + // online. Add error AND include username in notEmailed array. + if (emailResponse.errors) { + errors = errors.concat(emailResponse.errors); + } + } + } + // if you leave out any of the props + // from the final object and you are concatting together arrays you can concat + // an undeifined inside an array which will throw off array lengths. + return { + users: users.map((u) => u.username), + notEmailed, + errors, + notInvited: [], + notAdded: [], + }; +} diff --git a/packages/common/src/groups/_internal/processInviteUsers.ts b/packages/common/src/groups/_internal/processInviteUsers.ts new file mode 100644 index 00000000000..c930475dbb4 --- /dev/null +++ b/packages/common/src/groups/_internal/processInviteUsers.ts @@ -0,0 +1,60 @@ +import { IUser } from "@esri/arcgis-rest-types"; +import { IAddOrInviteContext, IAddOrInviteResponse } from "../types"; +import { getProp } from "../../objects/get-prop"; +import { ArcGISRequestError } from "@esri/arcgis-rest-request"; +import { inviteUsers } from "../inviteUsers"; +/** + * @private + * Governs the logic for inviting N users to a single group. + * An individual invite call goes out for each user and the results are consolidated. + * See comment in function about the for...of loop which explains reasoning. + * + * @export + * @param {IAddOrInviteContext} context context object + * @param {string} userType what type of user is it: org | world | community + * @return {IAddOrInviteResponse} response object + */ +export async function processInviteUsers( + context: IAddOrInviteContext, + userType: "world" | "org" | "community" | "partnered" +): Promise { + // Fetch users out of context based on userType + const users: IUser[] = getProp(context, userType); + const notInvited: string[] = []; + let errors: ArcGISRequestError[] = []; + const { addUserAsGroupAdmin } = context; + // iterate through users as we want a distinct invite call per user due to how + // batch invites will only respond with success: true/false + // and if there is an error then it gets priority even though successes do still go through + for (const user of users) { + // Invite users call + const inviteResponse = await inviteUsers( + getProp(context, "groupId"), + [user], + getProp(context, "primaryRO"), + 20160, // timeout + addUserAsGroupAdmin ? "group_admin" : "group_member" // if we are in a core group we want to invite them as a group admin, otherwise a group member + ); + // If it's just a failed invite then + // add username to notInvited array + if (!inviteResponse.success) { + notInvited.push(user.username); + // If there was a legit error + // Then only the error returns from + // online. Add error AND include username in notInvited array. + if (inviteResponse.errors) { + errors = errors.concat(inviteResponse.errors); + } + } + } + // if you leave out any of the props + // from the final object and you are concatting together arrays you can concat + // an undeifined inside an array which will throw off array lengths. + return { + users: users.map((u) => u.username), + notInvited, + errors, + notEmailed: [], + notAdded: [], + }; +} diff --git a/packages/common/src/groups/add-users-workflow/add-users-to-group.ts b/packages/common/src/groups/add-users-workflow/add-users-to-group.ts index 544a9d64742..897450a7fb6 100644 --- a/packages/common/src/groups/add-users-workflow/add-users-to-group.ts +++ b/packages/common/src/groups/add-users-workflow/add-users-to-group.ts @@ -3,7 +3,7 @@ import { getProp } from "../../objects/get-prop"; import { getWithDefault } from "../../objects/get-with-default"; import { IHubRequestOptions } from "../../types"; import { cloneObject } from "../../util"; -import { IConsolidatedResult, IEmail } from "./interfaces"; +import { IConsolidatedResult } from "./interfaces"; import { _consolidateResults } from "./output-processors/_consolidate-results"; import { _processAutoAdd } from "./output-processors/_process-auto-add"; import { _processInvite } from "./output-processors/_process-invite"; @@ -12,6 +12,7 @@ import { _processSecondaryEmail } from "./output-processors/_process-secondary-e import { _getAutoAddUsers } from "./utils/_get-auto-add-users"; import { _getEmailUsers } from "./utils/_get-email-users"; import { _getInviteUsers } from "./utils/_get-invite-users"; +import { IEmail } from "../types"; /** * Adds, invites or emails users about joining a group @@ -72,7 +73,7 @@ export function addUsersToGroup( allUsers, requestingUser, getProp(email, "copyMe") - ) + ), }; return _processAutoAdd(context) diff --git a/packages/common/src/groups/add-users-workflow/index.ts b/packages/common/src/groups/add-users-workflow/index.ts index 76a9af01394..202ec49b4a1 100644 --- a/packages/common/src/groups/add-users-workflow/index.ts +++ b/packages/common/src/groups/add-users-workflow/index.ts @@ -1,5 +1,4 @@ export * from "./output-processors"; export * from "./utils"; -export * from "./workflow-sections"; export * from "./add-users-to-group"; export * from "./interfaces"; diff --git a/packages/common/src/groups/add-users-workflow/interfaces.ts b/packages/common/src/groups/add-users-workflow/interfaces.ts index ebe3c43d04f..a6b1782f801 100644 --- a/packages/common/src/groups/add-users-workflow/interfaces.ts +++ b/packages/common/src/groups/add-users-workflow/interfaces.ts @@ -1,16 +1,11 @@ import { ICreateOrgNotificationResult, IInviteGroupUsersResult, - IUser + IUser, } from "@esri/arcgis-rest-portal"; import { ArcGISRequestError } from "@esri/arcgis-rest-request"; import { IHubRequestOptions } from "../../types"; - -export interface IEmail { - subject?: string; - body?: string; - copyMe?: boolean; -} +import { IEmail } from "../types"; export interface IConsolidatedResult { success: boolean; diff --git a/packages/common/src/groups/add-users-workflow/output-processors/_process-auto-add.ts b/packages/common/src/groups/add-users-workflow/output-processors/_process-auto-add.ts index 75788eddb2a..08db6bde952 100644 --- a/packages/common/src/groups/add-users-workflow/output-processors/_process-auto-add.ts +++ b/packages/common/src/groups/add-users-workflow/output-processors/_process-auto-add.ts @@ -1,6 +1,6 @@ import { getProp } from "../../../objects/get-prop"; import { IAddMemberContext } from "../interfaces"; -import { autoAddUsers } from "../workflow-sections/auto-add-users"; +import { autoAddUsers } from "../../autoAddUsers"; import { _formatAutoAddResponse } from "./_format-auto-add-response"; /** @@ -13,5 +13,5 @@ export function _processAutoAdd( getProp(context, "groupId"), getProp(context, "usersToAutoAdd"), getProp(context, "primaryRO.authentication") - ).then(rawResponse => _formatAutoAddResponse(rawResponse, context)); + ).then((rawResponse) => _formatAutoAddResponse(rawResponse, context)); } diff --git a/packages/common/src/groups/add-users-workflow/output-processors/_process-invite.ts b/packages/common/src/groups/add-users-workflow/output-processors/_process-invite.ts index cc68b5736f7..c3f00037e4f 100644 --- a/packages/common/src/groups/add-users-workflow/output-processors/_process-invite.ts +++ b/packages/common/src/groups/add-users-workflow/output-processors/_process-invite.ts @@ -1,6 +1,6 @@ import { getProp } from "../../../objects/get-prop"; import { IAddMemberContext } from "../interfaces"; -import { inviteUsers } from "../workflow-sections/invite-users"; +import { inviteUsers } from "../../inviteUsers"; /** * @private @@ -12,7 +12,7 @@ export function _processInvite( getProp(context, "groupId"), getProp(context, "usersToInvite"), getProp(context, "primaryRO.authentication") - ).then(result => { + ).then((result) => { context.inviteResult = result; return context; }); diff --git a/packages/common/src/groups/add-users-workflow/output-processors/_process-primary-email.ts b/packages/common/src/groups/add-users-workflow/output-processors/_process-primary-email.ts index 892217a241f..57173b2521a 100644 --- a/packages/common/src/groups/add-users-workflow/output-processors/_process-primary-email.ts +++ b/packages/common/src/groups/add-users-workflow/output-processors/_process-primary-email.ts @@ -1,7 +1,7 @@ import { getProp } from "../../../objects/get-prop"; import { IAddMemberContext } from "../interfaces"; import { _isOrgAdmin } from "../utils/_is-org-admin"; -import { emailOrgUsers } from "../workflow-sections/email-org-users"; +import { emailOrgUsers } from "../../emailOrgUsers"; /** * @private @@ -20,7 +20,7 @@ export function _processPrimaryEmail( context.email, context.primaryRO.authentication, _isOrgAdmin(context.requestingUser) - ).then(result => { + ).then((result) => { context.primaryEmailResult = result; return context; }); diff --git a/packages/common/src/groups/add-users-workflow/output-processors/_process-secondary-email.ts b/packages/common/src/groups/add-users-workflow/output-processors/_process-secondary-email.ts index 8748702443e..c909aa7e74d 100644 --- a/packages/common/src/groups/add-users-workflow/output-processors/_process-secondary-email.ts +++ b/packages/common/src/groups/add-users-workflow/output-processors/_process-secondary-email.ts @@ -3,7 +3,7 @@ import { _canEmailUser } from "../../add-users-workflow/utils/_can-email-user"; import { _isOrgAdmin } from "../../add-users-workflow/utils/_is-org-admin"; import { getProp, getWithDefault } from "../../../objects"; import { IAddMemberContext } from "../interfaces"; -import { emailOrgUsers } from "../workflow-sections/email-org-users"; +import { emailOrgUsers } from "../../emailOrgUsers"; /** * @private @@ -31,7 +31,7 @@ export function _processSecondaryEmail( "secondaryRO.portalSelf.user", {} ); - const secondaryOrgUsersToEmail = context.usersToInvite.filter(u => + const secondaryOrgUsersToEmail = context.usersToInvite.filter((u) => _canEmailUser(u, secondaryUser) ); response = emailOrgUsers( @@ -39,7 +39,7 @@ export function _processSecondaryEmail( context.email, context.secondaryRO.authentication, _isOrgAdmin(secondaryUser) - ).then(result => { + ).then((result) => { context.secondaryEmailResult = result; return context; }); diff --git a/packages/common/src/groups/add-users-workflow/workflow-sections/index.ts b/packages/common/src/groups/add-users-workflow/workflow-sections/index.ts deleted file mode 100644 index 67c3046bd76..00000000000 --- a/packages/common/src/groups/add-users-workflow/workflow-sections/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from "./auto-add-users"; -export * from "./email-org-users"; -export * from "./invite-users"; diff --git a/packages/common/src/groups/addOrInviteUsersToGroup.ts b/packages/common/src/groups/addOrInviteUsersToGroup.ts new file mode 100644 index 00000000000..47f2912a0c9 --- /dev/null +++ b/packages/common/src/groups/addOrInviteUsersToGroup.ts @@ -0,0 +1,91 @@ +import { IAuthenticationManager } from "@esri/arcgis-rest-request"; +import { + IAddOrInviteContext, + IAddOrInviteEmail, + IAddOrInviteToGroupResult, + IUserOrgRelationship, + IUserWithOrgType, +} from "./types"; +import { + addOrInviteCollaborationCoordinators, + addOrInviteCommunityUsers, + addOrInviteOrgUsers, + addOrInvitePartneredUsers, + addOrInviteWorldUsers, + groupUsersByOrgRelationship, +} from "./_internal/AddOrInviteUsersToGroupUtils"; + +/** + * Add or invite N users to a single group + * Org|community|world logic flows are run even if there are no users applicable for that particular path. + * Results from each path are consolidated and surfaced in the return object as failures and errors are of + * more importance than successes. + * + * @export + * @param {string} groupId Group we are adding users to + * @param {IUserWithOrgType[]} users array of users to add + * @param {IAuthenticationManager} primaryRO primary requestOptions + * @param {boolean} canAutoAddUser Can we automatically add a user to the group? + * @param {boolean} addUserAsGroupAdmin Should the user be added as a group administrator + * @param {IAddOrInviteEmail} email Email object + * @return {IAddOrInviteToGroupResult} Result object + */ +export async function addOrInviteUsersToGroup( + groupId: string, + users: IUserWithOrgType[], + primaryRO: IAuthenticationManager, + canAutoAddUser: boolean, + addUserAsGroupAdmin: boolean, + email: IAddOrInviteEmail +): Promise { + // Group users by their org relationship + const parsedUsers: IUserOrgRelationship = groupUsersByOrgRelationship(users); + // build up params for the context + const inputParams = { + groupId, // The group ID that the users shall be added/invited to. + primaryRO, // requestOptions required to auth for all the various add/invite logic except email + allUsers: users, // All users. + canAutoAddUser, // can the user be automatically added to the group rather than just invited? + addUserAsGroupAdmin, // Should they be added to the group as a group Admin vs a normal group member + email, // Either undefined or an object that contains both message/subject of the email and the auth for the email request + }; + // create context from params and parsed users + const context: IAddOrInviteContext = Object.assign(inputParams, parsedUsers); + // result obj by org relationship + const result: IAddOrInviteToGroupResult = { + community: await addOrInviteCommunityUsers(context), + org: await addOrInviteOrgUsers(context), + world: await addOrInviteWorldUsers(context), + partnered: await addOrInvitePartneredUsers(context), + collaborationCoordinator: await addOrInviteCollaborationCoordinators( + context + ), + notAdded: [], + notInvited: [], + notEmailed: [], + errors: [], + groupId, + }; + // Bring not added / invited / emailed / errors up to the top level + result.notAdded = [ + ...result.community.notAdded, + ...result.org.notAdded, + ...result.world.notAdded, + ]; + result.notInvited = [ + ...result.community.notInvited, + ...result.org.notInvited, + ...result.world.notInvited, + ]; + result.notEmailed = [ + ...result.community.notEmailed, + ...result.org.notEmailed, + ...result.world.notEmailed, + ]; + result.errors = [ + ...result.community.errors, + ...result.org.errors, + ...result.world.errors, + ]; + return result; +} diff --git a/packages/common/src/groups/addOrInviteUsersToGroups.ts b/packages/common/src/groups/addOrInviteUsersToGroups.ts new file mode 100644 index 00000000000..ce8c86f0f44 --- /dev/null +++ b/packages/common/src/groups/addOrInviteUsersToGroups.ts @@ -0,0 +1,80 @@ +import { + ArcGISRequestError, + IAuthenticationManager, +} from "@esri/arcgis-rest-request"; +import { + IAddOrInviteEmail, + IAddOrInviteToGroupResult, + IUserWithOrgType, +} from "./types"; +import { addOrInviteUsersToGroup } from "./addOrInviteUsersToGroup"; + +/** + * addOrInviteUsersToGroups adds/invites N users to N groups + * Initial entry point function for add/invite members flow + * when dealing with multiple groups. + * Responses from each group are then consolidated into the final returned object. + * + * @export + * @param {string[]} groupIds array of groups we are adding users to + * @param {IUserWithOrgType[]} users array of users to add to those groups + * @param {IAuthenticationManager} primaryRO primary requestOptions + * @param {boolean} [canAutoAddUser=false] Can we automatically add a user to the group? + * @param {boolean} [addUserAsGroupAdmin=false] Can the user be added to a group as an administrator of that group? + * @param {IAddOrInviteEmail} [email] Email object contains auth for the email && the email object itself + * @return {*} {Promise<{ + * notAdded: string[]; + * notInvited: string[]; + * notEmailed: string[]; + * errors: ArcGISRequestError[]; + * responses: IAddOrInviteToGroupResult[]; + * }>} Results object + */ +export async function addOrInviteUsersToGroups( + groupIds: string[], + users: IUserWithOrgType[], + primaryRO: IAuthenticationManager, + canAutoAddUser: boolean = false, + addUserAsGroupAdmin: boolean = false, + email?: IAddOrInviteEmail +): Promise<{ + notAdded: string[]; + notInvited: string[]; + notEmailed: string[]; + errors: ArcGISRequestError[]; + responses: IAddOrInviteToGroupResult[]; +}> { + let notAdded: string[] = []; + let notInvited: string[] = []; + let notEmailed: string[] = []; + let errors: ArcGISRequestError[] = []; + const responses: IAddOrInviteToGroupResult[] = []; + // need to for..of loop this as a reduce will overwrite promises during execution + // this way we get an object of each group id nicely. + for (const groupId of groupIds) { + // For each group we'll add the users to them. + const result = await addOrInviteUsersToGroup( + groupId, + users, + primaryRO, + canAutoAddUser, + addUserAsGroupAdmin, + email + ); + // attach each groups results + responses.push(result); + // surface results to the top of the stack... + notAdded = notAdded.concat(result.notAdded); + errors = errors.concat(result.errors); + notInvited = notInvited.concat(result.notInvited); + notEmailed = notEmailed.concat(result.notEmailed); + } + // Return built up result object. + return { + notAdded, + notInvited, + notEmailed, + errors, + responses, + }; +} diff --git a/packages/common/src/groups/add-users-workflow/workflow-sections/auto-add-users.ts b/packages/common/src/groups/autoAddUsers.ts similarity index 78% rename from packages/common/src/groups/add-users-workflow/workflow-sections/auto-add-users.ts rename to packages/common/src/groups/autoAddUsers.ts index 776abebbc97..888e157dc0c 100644 --- a/packages/common/src/groups/add-users-workflow/workflow-sections/auto-add-users.ts +++ b/packages/common/src/groups/autoAddUsers.ts @@ -2,7 +2,7 @@ import { IUser } from "@esri/arcgis-rest-auth"; import { IAddGroupUsersResult, IAddGroupUsersOptions, - addGroupUsers + addGroupUsers, } from "@esri/arcgis-rest-portal"; import { IAuthenticationManager } from "@esri/arcgis-rest-request"; @@ -20,13 +20,13 @@ export function autoAddUsers( id: string, users: IUser[], authentication: IAuthenticationManager -): Promise { - let response: Promise = Promise.resolve(null); +): Promise { + let response: Promise = Promise.resolve(null); if (users.length) { const args: IAddGroupUsersOptions = { id, - users: users.map(u => u.username), - authentication + users: users.map((u) => u.username), + authentication, }; response = addGroupUsers(args); } diff --git a/packages/common/src/groups/add-users-workflow/workflow-sections/email-org-users.ts b/packages/common/src/groups/emailOrgUsers.ts similarity index 82% rename from packages/common/src/groups/add-users-workflow/workflow-sections/email-org-users.ts rename to packages/common/src/groups/emailOrgUsers.ts index 66e7392189a..40d76690eb9 100644 --- a/packages/common/src/groups/add-users-workflow/workflow-sections/email-org-users.ts +++ b/packages/common/src/groups/emailOrgUsers.ts @@ -2,10 +2,10 @@ import { IUser } from "@esri/arcgis-rest-auth"; import { ICreateOrgNotificationResult, ICreateOrgNotificationOptions, - createOrgNotification + createOrgNotification, } from "@esri/arcgis-rest-portal"; import { IAuthenticationManager } from "@esri/arcgis-rest-request"; -import { IEmail } from "../interfaces"; +import { IEmail } from "./types"; /** * Attempts to email members of the requesting user's organization. @@ -22,15 +22,16 @@ export function emailOrgUsers( email: IEmail, authentication: IAuthenticationManager, isOrgAdmin: boolean -): Promise { - let response: Promise = Promise.resolve(null); +): Promise { + let response: Promise = + Promise.resolve(null); if (users.length) { const args: ICreateOrgNotificationOptions = { authentication, message: email.body, subject: email.subject, notificationChannelType: "email", - users: users.map(u => u.username) + users: users.map((u) => u.username), }; if (!isOrgAdmin) { args.batchSize = 1; diff --git a/packages/common/src/groups/index.ts b/packages/common/src/groups/index.ts index 6cee26960f1..dffd5eed1a4 100644 --- a/packages/common/src/groups/index.ts +++ b/packages/common/src/groups/index.ts @@ -3,3 +3,11 @@ export * from "./add-users-workflow"; export * from "./types"; export * from "./HubGroups"; export * from "./HubGroup"; +export * from "./addOrInviteUsersToGroup"; +export * from "./addOrInviteUsersToGroups"; +// TODO: The below are being used in hub-teams. When we deprecate that package we can move +// The below into _internal and remove the exports from here. They were previously in +// the add-users-workflow directory +export * from "./autoAddUsers"; +export * from "./inviteUsers"; +export * from "./emailOrgUsers"; diff --git a/packages/common/src/groups/add-users-workflow/workflow-sections/invite-users.ts b/packages/common/src/groups/inviteUsers.ts similarity index 90% rename from packages/common/src/groups/add-users-workflow/workflow-sections/invite-users.ts rename to packages/common/src/groups/inviteUsers.ts index 926ee6621da..9d09beb53e4 100644 --- a/packages/common/src/groups/add-users-workflow/workflow-sections/invite-users.ts +++ b/packages/common/src/groups/inviteUsers.ts @@ -24,8 +24,8 @@ export function inviteUsers( authentication: IAuthenticationManager, expiration = 20160, // default to 2 week expiration TODO: is this actually 2 weeks? role: "group_member" | "group_admin" = "group_member" // default to group member, but allow for team_admin as well -): Promise { - let response: Promise = Promise.resolve(null); +): Promise { + let response: Promise = Promise.resolve(null); if (users.length) { const args: IInviteGroupUsersOptions = { id, diff --git a/packages/common/src/groups/types/index.ts b/packages/common/src/groups/types/index.ts index 1bf77929e62..3c892185e2a 100644 --- a/packages/common/src/groups/types/index.ts +++ b/packages/common/src/groups/types/index.ts @@ -1 +1,2 @@ export * from "./IGroupMembershipSummary"; +export * from "./types"; diff --git a/packages/common/src/groups/types/types.ts b/packages/common/src/groups/types/types.ts new file mode 100644 index 00000000000..663e0082c0f --- /dev/null +++ b/packages/common/src/groups/types/types.ts @@ -0,0 +1,86 @@ +import { + ArcGISRequestError, + IAuthenticationManager, +} from "@esri/arcgis-rest-request"; +import { IUser } from "@esri/arcgis-rest-types"; + +export interface IEmail { + subject?: string; + body?: string; + copyMe?: boolean; +} +/** + * User object returned from add users modal in ember application + * It extends the IUser interface with an additional property + * that denotes what org relationship the user might have (world|org|community|partnered|collaborationCoordinator) + */ +export interface IUserWithOrgType extends IUser { + orgType: + | "world" + | "org" + | "community" + | "partnered" + | "collaborationCoordinator"; +} + +/** + * User org relationship interface + * Object contains users parsed by their org relationship (world|org|community|partnered|collaborationCoordinator) + */ +export interface IUserOrgRelationship { + world: IUserWithOrgType[]; + org: IUserWithOrgType[]; + community: IUserWithOrgType[]; + partnered: IUserWithOrgType[]; + collaborationCoordinator: IUserWithOrgType[]; +} + +/** + * Interface governing the add or invite response out of process auto add / invite / emailing users + */ +export interface IAddOrInviteResponse { + users: string[]; + errors: ArcGISRequestError[]; + notAdded: string[]; + notInvited: string[]; + notEmailed: string[]; +} + +/** + * Email input object for add/invite flow + * contains both the IEmail object and auth for the email. + */ +export interface IAddOrInviteEmail { + message: IEmail; + auth: IAuthenticationManager; + groupId?: string; +} + +/** + * Add or invite flow context - object that contains all the needed + * inputs for org/world/community users + */ +export interface IAddOrInviteContext extends IUserOrgRelationship { + groupId: string; + primaryRO: IAuthenticationManager; + allUsers: IUserWithOrgType[]; + canAutoAddUser: boolean; + addUserAsGroupAdmin: boolean; + email: IAddOrInviteEmail; +} + +/** + * Interface for result object out of addOrInviteUsersToGroup. + */ +export interface IAddOrInviteToGroupResult { + errors: ArcGISRequestError[]; + notAdded: string[]; + notInvited: string[]; + notEmailed: string[]; + community: IAddOrInviteResponse; + org: IAddOrInviteResponse; + world: IAddOrInviteResponse; + partnered: IAddOrInviteResponse; + collaborationCoordinator: IAddOrInviteResponse; + groupId: string; +} diff --git a/packages/common/test/groups/HubGroups.test.ts b/packages/common/test/groups/HubGroups.test.ts index 969781e3332..69971abe550 100644 --- a/packages/common/test/groups/HubGroups.test.ts +++ b/packages/common/test/groups/HubGroups.test.ts @@ -5,7 +5,6 @@ import { cloneObject, enrichGroupSearchResult, IHubRequestOptions, - setProp, } from "../../src"; import * as HubGroupsModule from "../../src/groups/HubGroups"; import * as FetchEnrichments from "../../src/groups/_internal/enrichments"; @@ -18,10 +17,10 @@ const TEST_GROUP: IGroup = { isInvitationOnly: false, owner: "dev_pre_hub_admin", description: "dev followers Content summary", - snippet: null, + snippet: undefined, tags: ["Hub Initiative Group", "Open Data"], typeKeywords: [], - phone: null, + phone: undefined, sortField: "title", sortOrder: "asc", isViewOnly: false, @@ -104,11 +103,11 @@ describe("HubGroups Module:", () => { expect(chk.updatedDate).toEqual(new Date(GRP.modified)); expect(chk.updatedDateSource).toEqual("group.modified"); expect(chk.family).toEqual("team"); - expect(chk.links.self).toEqual( + expect(chk.links?.self).toEqual( `https://some-server.com/gis/home/group.html?id=${GRP.id}` ); - expect(chk.links.siteRelative).toEqual(`/teams/${GRP.id}`); - expect(chk.links.thumbnail).toEqual( + expect(chk.links?.siteRelative).toEqual(`/teams/${GRP.id}`); + expect(chk.links?.thumbnail).toEqual( `${hubRo.portal}/community/groups/${GRP.id}/info/${GRP.thumbnail}` ); // Group Specific Props diff --git a/packages/common/test/groups/_internal/AddOrInviteUsersToGroupUtils.test.ts b/packages/common/test/groups/_internal/AddOrInviteUsersToGroupUtils.test.ts new file mode 100644 index 00000000000..051f1efb7e8 --- /dev/null +++ b/packages/common/test/groups/_internal/AddOrInviteUsersToGroupUtils.test.ts @@ -0,0 +1,426 @@ +// import * as AddOrInviteUtilsModule from "../../../src/groups/_internal/AddOrInviteUsersToGroupUtils"; +import { + IAddOrInviteContext, + IAddOrInviteEmail, + IUserWithOrgType, +} from "../../../src/groups/types"; +import { + addOrInviteCollaborationCoordinators, + addOrInviteCommunityUsers, + addOrInviteOrgUsers, + addOrInvitePartneredUsers, + addOrInviteWorldUsers, + handleNoUsers, + groupUsersByOrgRelationship, +} from "../../../src/groups/_internal/AddOrInviteUsersToGroupUtils"; +import { MOCK_AUTH } from "../../mocks/mock-auth"; +import * as processAutoAddUsersModule from "../../../src/groups/_internal/processAutoAddUsers"; +import * as processInviteUsersModule from "../../../src/groups/_internal/processInviteUsers"; + +describe("AddOrInviteUsersToGroupUtilsModule", () => { + describe("groupUsersByOrgRelationship: ", () => { + it("properly groups users by org relationship", () => { + const users: IUserWithOrgType[] = [ + { orgType: "world" }, + { orgType: "org" }, + { orgType: "community" }, + { orgType: "org" }, + { orgType: "partnered" }, + { orgType: "collaborationCoordinator" }, + ]; + const result = groupUsersByOrgRelationship(users); + expect(result.world.length).toEqual(1); + expect(result.org.length).toEqual(2); + expect(result.community.length).toEqual(1); + expect(result.partnered.length).toEqual(1); + expect(result.collaborationCoordinator.length).toEqual(1); + }); + }); + + describe("addOrInviteCollaborationCoordinators: ", () => { + it("Properly delegates to handleNoUsers when no users supplied", async () => { + const context: IAddOrInviteContext = { + groupId: "abc123", + primaryRO: MOCK_AUTH, + allUsers: [], + canAutoAddUser: false, + addUserAsGroupAdmin: false, + email: undefined as unknown as IAddOrInviteEmail, + world: [], + org: [], + community: [], + partnered: [], + collaborationCoordinator: [], + }; + + const actual = await addOrInviteCollaborationCoordinators(context); + expect(actual).toEqual({ + notAdded: [], + notInvited: [], + notEmailed: [], + users: [], + errors: [], + }); + }); + it("Properly autoAdds when canAutoAdd is supplied", async () => { + const context: IAddOrInviteContext = { + groupId: "abc123", + primaryRO: MOCK_AUTH, + allUsers: [{ orgType: "collaborationCoordinator" }], + canAutoAddUser: true, + addUserAsGroupAdmin: false, + email: undefined as unknown as IAddOrInviteEmail, + world: [], + org: [], + community: [], + partnered: [], + collaborationCoordinator: [{ orgType: "collaborationCoordinator" }], + }; + const processAutoAddUsersSpy = spyOn( + processAutoAddUsersModule, + "processAutoAddUsers" + ).and.callFake(() => { + Promise.resolve(); + }); + + const actual = await addOrInviteCollaborationCoordinators(context); + expect(processAutoAddUsersSpy).toHaveBeenCalled(); + }); + }); + + describe("addOrInviteCommunityUsers:", () => { + it("Properly delegates to handleNoUsers when no users supplied", async () => { + const context: IAddOrInviteContext = { + groupId: "abc123", + primaryRO: MOCK_AUTH, + allUsers: [], + canAutoAddUser: false, + addUserAsGroupAdmin: false, + email: { message: {}, auth: MOCK_AUTH }, + world: [], + org: [], + partnered: [], + collaborationCoordinator: [], + community: [], + }; + + const actual = await addOrInviteCommunityUsers(context); + expect(actual).toEqual({ + notAdded: [], + notInvited: [], + notEmailed: [], + users: [], + errors: [], + }); + }); + it("Properly autoAdds when an email is supplied", async () => { + const context: IAddOrInviteContext = { + groupId: "abc123", + primaryRO: MOCK_AUTH, + allUsers: [{ orgType: "community" }], + canAutoAddUser: false, + addUserAsGroupAdmin: false, + email: { message: {}, auth: MOCK_AUTH }, + world: [], + org: [], + partnered: [], + collaborationCoordinator: [], + community: [{ orgType: "community" }], + }; + const processAutoAddUsersSpy = spyOn( + processAutoAddUsersModule, + "processAutoAddUsers" + ).and.callFake(() => { + Promise.resolve(); + }); + + const actual = await addOrInviteCommunityUsers(context); + expect(processAutoAddUsersSpy).toHaveBeenCalled(); + }); + it("Properly autoAdds when an email is supplied for a given groupID", async () => { + const context: IAddOrInviteContext = { + groupId: "abc123", + primaryRO: MOCK_AUTH, + allUsers: [{ orgType: "community" }], + canAutoAddUser: true, + addUserAsGroupAdmin: false, + email: { message: {}, auth: MOCK_AUTH, groupId: "abc123" }, + world: [], + org: [], + partnered: [], + collaborationCoordinator: [], + community: [{ orgType: "community" }], + }; + const processAutoAddUsersSpy = spyOn( + processAutoAddUsersModule, + "processAutoAddUsers" + ).and.callFake(() => { + Promise.resolve(); + }); + + const actual = await addOrInviteCommunityUsers(context); + expect(processAutoAddUsersSpy).toHaveBeenCalled(); + expect(processAutoAddUsersSpy.calls.count()).toEqual(1); + }); + it("Properly invites when an email is supplied, but groups dont match", async () => { + const context: IAddOrInviteContext = { + groupId: "abc123", + primaryRO: MOCK_AUTH, + allUsers: [{ orgType: "community" }], + canAutoAddUser: false, + addUserAsGroupAdmin: false, + email: { message: {}, auth: MOCK_AUTH, groupId: "def456" }, + world: [], + org: [], + partnered: [], + collaborationCoordinator: [], + community: [{ orgType: "community" }], + }; + const processInviteUsersSpy = spyOn( + processInviteUsersModule, + "processInviteUsers" + ).and.callFake(() => { + Promise.resolve(); + }); + + const actual = await addOrInviteCommunityUsers(context); + expect(processInviteUsersSpy).toHaveBeenCalled(); + }); + it("Properly autoAdds when canAutoAdd is supplied", async () => { + const context: IAddOrInviteContext = { + groupId: "abc123", + primaryRO: MOCK_AUTH, + allUsers: [{ orgType: "community" }], + canAutoAddUser: true, + addUserAsGroupAdmin: false, + email: undefined as unknown as IAddOrInviteEmail, + world: [], + org: [], + partnered: [], + collaborationCoordinator: [], + community: [{ orgType: "community" }], + }; + const processAutoAddUsersSpy = spyOn( + processAutoAddUsersModule, + "processAutoAddUsers" + ).and.callFake(() => { + Promise.resolve(); + }); + + const actual = await addOrInviteCommunityUsers(context); + expect(processAutoAddUsersSpy).toHaveBeenCalled(); + }); + it("Properly falls back to inviting users", async () => { + const context: IAddOrInviteContext = { + groupId: "abc123", + primaryRO: MOCK_AUTH, + allUsers: [{ orgType: "community" }], + canAutoAddUser: false, + addUserAsGroupAdmin: false, + email: undefined as unknown as IAddOrInviteEmail, + world: [], + org: [], + partnered: [], + collaborationCoordinator: [], + community: [{ orgType: "community" }], + }; + const processInviteUsersSpy = spyOn( + processInviteUsersModule, + "processInviteUsers" + ).and.callFake(() => { + Promise.resolve(); + }); + + const actual = await addOrInviteCommunityUsers(context); + expect(processInviteUsersSpy).toHaveBeenCalled(); + }); + }); + + describe("addOrInviteOrgUsers: ", () => { + it("Properly delegates to handleNoUsers when no users supplied", async () => { + const context: IAddOrInviteContext = { + groupId: "abc123", + primaryRO: MOCK_AUTH, + allUsers: [], + canAutoAddUser: false, + addUserAsGroupAdmin: false, + email: undefined as unknown as IAddOrInviteEmail, + world: [], + org: [], + community: [], + partnered: [], + collaborationCoordinator: [], + }; + + const actual = await addOrInviteOrgUsers(context); + expect(actual).toEqual({ + notAdded: [], + notInvited: [], + notEmailed: [], + users: [], + errors: [], + }); + }); + it("Properly autoAdds when canAutoAdd is supplied", async () => { + const context: IAddOrInviteContext = { + groupId: "abc123", + primaryRO: MOCK_AUTH, + allUsers: [{ orgType: "org" }], + canAutoAddUser: true, + addUserAsGroupAdmin: false, + email: undefined as unknown as IAddOrInviteEmail, + world: [], + org: [{ orgType: "org" }], + community: [], + partnered: [], + collaborationCoordinator: [], + }; + const processAutoAddUsersSpy = spyOn( + processAutoAddUsersModule, + "processAutoAddUsers" + ).and.callFake(() => { + Promise.resolve(); + }); + + const actual = await addOrInviteOrgUsers(context); + expect(processAutoAddUsersSpy).toHaveBeenCalled(); + }); + it("Properly falls back to inviting users", async () => { + const context: IAddOrInviteContext = { + groupId: "abc123", + primaryRO: MOCK_AUTH, + allUsers: [{ orgType: "org" }], + canAutoAddUser: false, + addUserAsGroupAdmin: false, + email: undefined as unknown as IAddOrInviteEmail, + world: [], + org: [{ orgType: "org" }], + community: [], + partnered: [], + collaborationCoordinator: [], + }; + const processInviteUsersSpy = spyOn( + processInviteUsersModule, + "processInviteUsers" + ).and.callFake(() => { + Promise.resolve(); + }); + + const actual = await addOrInviteOrgUsers(context); + expect(processInviteUsersSpy).toHaveBeenCalled(); + }); + }); + + describe("addOrInvitePartneredUsers: ", () => { + it("Properly delegates to handleNoUsers when no users supplied", async () => { + const context: IAddOrInviteContext = { + groupId: "abc123", + primaryRO: MOCK_AUTH, + allUsers: [], + canAutoAddUser: false, + addUserAsGroupAdmin: false, + email: undefined as unknown as IAddOrInviteEmail, + world: [], + org: [], + community: [], + partnered: [], + collaborationCoordinator: [], + }; + + const actual = await addOrInvitePartneredUsers(context); + expect(actual).toEqual({ + notAdded: [], + notInvited: [], + notEmailed: [], + users: [], + errors: [], + }); + }); + it("Properly falls back to inviting users", async () => { + const context: IAddOrInviteContext = { + groupId: "abc123", + primaryRO: MOCK_AUTH, + allUsers: [{ orgType: "partnered" }], + canAutoAddUser: false, + addUserAsGroupAdmin: false, + email: undefined as unknown as IAddOrInviteEmail, + world: [], + org: [], + community: [], + partnered: [{ orgType: "partnered" }], + collaborationCoordinator: [], + }; + const processInviteUsersSpy = spyOn( + processInviteUsersModule, + "processInviteUsers" + ).and.callFake(() => { + Promise.resolve(); + }); + + const actual = await addOrInvitePartneredUsers(context); + expect(processInviteUsersSpy).toHaveBeenCalled(); + }); + }); + + describe("addOrInviteWorldUsers: ", () => { + it("Properly delegates to handleNoUsers when no users supplied", async () => { + const context: IAddOrInviteContext = { + groupId: "abc123", + primaryRO: MOCK_AUTH, + allUsers: [], + canAutoAddUser: false, + addUserAsGroupAdmin: false, + email: undefined as unknown as IAddOrInviteEmail, + world: [], + org: [], + community: [], + partnered: [], + collaborationCoordinator: [], + }; + + const actual = await addOrInviteWorldUsers(context); + expect(actual).toEqual({ + notAdded: [], + notInvited: [], + notEmailed: [], + users: [], + errors: [], + }); + }); + it("Properly falls back to inviting users", async () => { + const context: IAddOrInviteContext = { + groupId: "abc123", + primaryRO: MOCK_AUTH, + allUsers: [{ orgType: "world" }], + canAutoAddUser: false, + addUserAsGroupAdmin: false, + email: undefined as unknown as IAddOrInviteEmail, + world: [{ orgType: "world" }], + org: [], + community: [], + partnered: [], + collaborationCoordinator: [], + }; + const processInviteUsersSpy = spyOn( + processInviteUsersModule, + "processInviteUsers" + ).and.callFake(() => { + Promise.resolve(); + }); + + const actual = await addOrInviteWorldUsers(context); + expect(processInviteUsersSpy).toHaveBeenCalled(); + }); + }); + + describe("handleNoUsers: ", () => { + it("returns expected empty addOrInviteReponse object", async () => { + const result = await handleNoUsers(); + expect(result.notAdded.length).toBe(0); + expect(result.notEmailed.length).toBe(0); + expect(result.notInvited.length).toBe(0); + expect(result.users.length).toBe(0); + expect(result.errors.length).toBe(0); + }); + }); +}); diff --git a/packages/common/test/groups/_internal/autoAddUsersAsAdmin.test.ts b/packages/common/test/groups/_internal/autoAddUsersAsAdmin.test.ts new file mode 100644 index 00000000000..f9d22562700 --- /dev/null +++ b/packages/common/test/groups/_internal/autoAddUsersAsAdmin.test.ts @@ -0,0 +1,42 @@ +import { autoAddUsersAsAdmins } from "../../../src/groups/_internal/autoAddUsersAsAdmins"; +import { MOCK_AUTH } from "../../mocks/mock-auth"; +import * as restPortalModule from "@esri/arcgis-rest-portal"; + +describe("autoAddUsersAsAdmin", function () { + let addSpy: jasmine.Spy; + const admins: restPortalModule.IUser[] = [ + { username: "luke" }, + { username: "leia" }, + { username: "han" }, + ]; + const groupId = "rebel_alliance"; + + beforeEach(() => { + addSpy = spyOn(restPortalModule, "addGroupUsers"); + }); + afterEach(() => { + addSpy.calls.reset(); + }); + + it("Properly delegates to addGroupUsers", async () => { + addSpy.and.callFake(() => Promise.resolve({ notAdded: [] })); + const result = await autoAddUsersAsAdmins(groupId, admins, MOCK_AUTH); + expect(result).toEqual({ notAdded: [] }); + expect(addSpy).toHaveBeenCalled(); + const actualArgs = addSpy.calls.first().args; + const expectedArgs = [ + { + authentication: MOCK_AUTH, + id: groupId, + admins: admins.map((u) => u.username), + }, + ]; + expect(actualArgs).toEqual(expectedArgs); + }); + + it("Resolves to null when users array is empty", async () => { + const result = await autoAddUsersAsAdmins(groupId, [], MOCK_AUTH); + expect(result).toEqual(null); + expect(addSpy).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/common/test/groups/_internal/processAutoAddUsers.test.ts b/packages/common/test/groups/_internal/processAutoAddUsers.test.ts new file mode 100644 index 00000000000..a8678543414 --- /dev/null +++ b/packages/common/test/groups/_internal/processAutoAddUsers.test.ts @@ -0,0 +1,185 @@ +import { ArcGISRequestError } from "@esri/arcgis-rest-request"; +import { processAutoAddUsers } from "../../../src/groups/_internal/processAutoAddUsers"; +import { + IAddOrInviteContext, + IAddOrInviteEmail, + IAddOrInviteResponse, +} from "../../../src/groups/types"; +import { MOCK_AUTH } from "../../mocks/mock-auth"; +import * as autoAddUsersAsAdminsModule from "../../../src/groups/_internal/autoAddUsersAsAdmins"; +import * as processEmailUsersModule from "../../../src/groups/_internal/processEmailUsers"; +import * as autoAddUsersModule from "../../../src/groups/autoAddUsers"; + +describe("processAutoAddUsers: ", () => { + let autoAddUsersSpy: jasmine.Spy; + let autoAddUsersAsAdminsSpy: jasmine.Spy; + let processEmailUsersSpy: jasmine.Spy; + + beforeEach(() => { + autoAddUsersSpy = spyOn(autoAddUsersModule, "autoAddUsers"); + autoAddUsersAsAdminsSpy = spyOn( + autoAddUsersAsAdminsModule, + "autoAddUsersAsAdmins" + ); + processEmailUsersSpy = spyOn(processEmailUsersModule, "processEmailUsers"); + }); + afterEach(() => { + autoAddUsersSpy.calls.reset(); + autoAddUsersAsAdminsSpy.calls.reset(); + processEmailUsersSpy.calls.reset(); + }); + + it("flows through happy path as admin without email", async () => { + const context: IAddOrInviteContext = { + groupId: "abc123", + primaryRO: MOCK_AUTH, + allUsers: [ + { orgType: "org", username: "bob" }, + { orgType: "org", username: "frank" }, + ], + canAutoAddUser: false, + addUserAsGroupAdmin: true, + email: undefined as unknown as IAddOrInviteEmail, + world: [], + org: [ + { orgType: "org", username: "bob" }, + { orgType: "org", username: "frank" }, + ], + community: [], + partnered: [], + collaborationCoordinator: [], + }; + autoAddUsersSpy.and.callFake(() => Promise.resolve({ success: true })); + autoAddUsersAsAdminsSpy.and.callFake(() => + Promise.resolve({ success: true }) + ); + const result = await processAutoAddUsers(context, "org"); + expect(autoAddUsersAsAdminsSpy).toHaveBeenCalled(); + expect(autoAddUsersSpy).not.toHaveBeenCalled(); + expect(result.users.length).toEqual(2); + expect(result.notAdded.length).toEqual(0); + expect(result.errors.length).toEqual(0); + expect(result.notEmailed.length).toEqual(0); + }); + it("flows through happy path not as admin without email", async () => { + const context: IAddOrInviteContext = { + groupId: "abc123", + primaryRO: MOCK_AUTH, + allUsers: [ + { orgType: "org", username: "bob" }, + { orgType: "org", username: "frank" }, + ], + canAutoAddUser: false, + addUserAsGroupAdmin: false, + email: undefined as unknown as IAddOrInviteEmail, + world: [], + org: [ + { orgType: "org", username: "bob" }, + { orgType: "org", username: "frank" }, + ], + community: [], + partnered: [], + collaborationCoordinator: [], + }; + autoAddUsersSpy.and.callFake(() => Promise.resolve({ success: true })); + autoAddUsersAsAdminsSpy.and.callFake(() => + Promise.resolve({ success: true }) + ); + const result = await processAutoAddUsers(context, "org"); + expect(autoAddUsersAsAdminsSpy).not.toHaveBeenCalled(); + expect(autoAddUsersSpy).toHaveBeenCalled(); + expect(result.users.length).toEqual(2); + expect(result.notAdded.length).toEqual(0); + expect(result.errors.length).toEqual(0); + expect(result.notEmailed.length).toEqual(0); + }); + it("flows through happy path not as admin with email", async () => { + const context: IAddOrInviteContext = { + groupId: "abc123", + primaryRO: MOCK_AUTH, + allUsers: [ + { orgType: "org", username: "bob" }, + { orgType: "org", username: "frank" }, + ], + canAutoAddUser: false, + addUserAsGroupAdmin: false, + email: { message: {}, auth: MOCK_AUTH }, + world: [], + org: [ + { orgType: "org", username: "bob" }, + { orgType: "org", username: "frank" }, + ], + community: [], + partnered: [], + collaborationCoordinator: [], + }; + autoAddUsersSpy.and.callFake(() => Promise.resolve({ success: true })); + autoAddUsersAsAdminsSpy.and.callFake(() => + Promise.resolve({ success: true }) + ); + processEmailUsersSpy.and.callFake(() => { + const responseObj: IAddOrInviteResponse = { + users: [], + notEmailed: [], + errors: [], + notInvited: [], + notAdded: [], + }; + return Promise.resolve(responseObj); + }); + const result = await processAutoAddUsers(context, "org", true); + expect(autoAddUsersAsAdminsSpy).not.toHaveBeenCalled(); + expect(autoAddUsersSpy).toHaveBeenCalled(); + expect(processEmailUsersSpy).toHaveBeenCalled(); + expect(result.users.length).toEqual(2); + expect(result.notAdded.length).toEqual(0); + expect(result.errors.length).toEqual(0); + expect(result.notEmailed.length).toEqual(0); + }); + it("handles errors", async () => { + const context: IAddOrInviteContext = { + groupId: "abc123", + primaryRO: MOCK_AUTH, + allUsers: [ + { orgType: "org", username: "bob" }, + { orgType: "org", username: "frank" }, + ], + canAutoAddUser: false, + addUserAsGroupAdmin: false, + email: { message: {}, auth: MOCK_AUTH }, + world: [], + org: [ + { orgType: "org", username: "bob" }, + { orgType: "org", username: "frank" }, + ], + community: [], + partnered: [], + collaborationCoordinator: [], + }; + const error = new ArcGISRequestError("Email not sent"); + autoAddUsersSpy.and.callFake(() => + Promise.resolve({ notAdded: ["bob", "frank"], errors: [error] }) + ); + autoAddUsersAsAdminsSpy.and.callFake(() => + Promise.resolve({ success: true }) + ); + processEmailUsersSpy.and.callFake(() => { + const responseObj: IAddOrInviteResponse = { + users: ["bob", "frank"], + notEmailed: ["bob", "frank"], + errors: [error, error], + notInvited: [], + notAdded: [], + }; + return Promise.resolve(responseObj); + }); + const result = await processAutoAddUsers(context, "org", true); + expect(autoAddUsersAsAdminsSpy).not.toHaveBeenCalled(); + expect(autoAddUsersSpy).toHaveBeenCalled(); + expect(processEmailUsersSpy).toHaveBeenCalled(); + expect(result.users.length).toEqual(2); + expect(result.notAdded.length).toEqual(2); + expect(result.errors.length).toEqual(3); + expect(result.notEmailed.length).toEqual(2); + }); +}); diff --git a/packages/common/test/groups/_internal/processEmailUsers.test.ts b/packages/common/test/groups/_internal/processEmailUsers.test.ts new file mode 100644 index 00000000000..8c788c3f3d8 --- /dev/null +++ b/packages/common/test/groups/_internal/processEmailUsers.test.ts @@ -0,0 +1,103 @@ +import { ArcGISRequestError } from "@esri/arcgis-rest-request"; +import { processEmailUsers } from "../../../src/groups/_internal/processEmailUsers"; +import { IAddOrInviteContext } from "../../../src/groups/types"; +import { MOCK_AUTH } from "../../mocks/mock-auth"; +import * as emailOrgUsersModule from "../../../src/groups/emailOrgUsers"; + +describe("processEmailUsers: ", () => { + let emailOrgUsersSpy: jasmine.Spy; + + beforeEach(() => { + emailOrgUsersSpy = spyOn(emailOrgUsersModule, "emailOrgUsers"); + }); + afterEach(() => { + emailOrgUsersSpy.calls.reset(); + }); + it("flows through happy path...", async () => { + const context: IAddOrInviteContext = { + groupId: "abc123", + primaryRO: MOCK_AUTH, + allUsers: [ + { orgType: "community", username: "bob" }, + { orgType: "community", username: "frank" }, + ], + canAutoAddUser: false, + addUserAsGroupAdmin: false, + email: { message: {}, auth: MOCK_AUTH }, + world: [], + org: [], + community: [ + { orgType: "community", username: "bob" }, + { orgType: "community", username: "frank" }, + ], + partnered: [], + collaborationCoordinator: [], + }; + emailOrgUsersSpy.and.callFake(() => Promise.resolve({ success: true })); + const result = await processEmailUsers(context); + expect(emailOrgUsersSpy).toHaveBeenCalled(); + expect(emailOrgUsersSpy.calls.count()).toEqual(2); + expect(result.users.length).toEqual(2); + expect(result.notEmailed.length).toEqual(0); + expect(result.errors.length).toEqual(0); + }); + it("handles matters when success is false", async () => { + const context: IAddOrInviteContext = { + groupId: "abc123", + primaryRO: MOCK_AUTH, + allUsers: [ + { orgType: "community", username: "bob" }, + { orgType: "community", username: "frank" }, + ], + canAutoAddUser: false, + addUserAsGroupAdmin: false, + email: { message: {}, auth: MOCK_AUTH }, + world: [], + org: [], + community: [ + { orgType: "community", username: "bob" }, + { orgType: "community", username: "frank" }, + ], + partnered: [], + collaborationCoordinator: [], + }; + emailOrgUsersSpy.and.callFake(() => Promise.resolve({ success: false })); + const result = await processEmailUsers(context); + expect(emailOrgUsersSpy).toHaveBeenCalled(); + expect(emailOrgUsersSpy.calls.count()).toEqual(2); + expect(result.users.length).toEqual(2); + expect(result.notEmailed.length).toEqual(2); + expect(result.errors.length).toEqual(0); + }); + it("handles matters when errors are returned", async () => { + const context: IAddOrInviteContext = { + groupId: "abc123", + primaryRO: MOCK_AUTH, + allUsers: [ + { orgType: "community", username: "bob" }, + { orgType: "community", username: "frank" }, + ], + canAutoAddUser: false, + addUserAsGroupAdmin: false, + email: { message: {}, auth: MOCK_AUTH }, + world: [], + org: [], + community: [ + { orgType: "community", username: "bob" }, + { orgType: "community", username: "frank" }, + ], + partnered: [], + collaborationCoordinator: [], + }; + const error = new ArcGISRequestError("Email not sent"); + emailOrgUsersSpy.and.callFake(() => + Promise.resolve({ success: false, errors: [error] }) + ); + const result = await processEmailUsers(context); + expect(emailOrgUsersSpy).toHaveBeenCalled(); + expect(emailOrgUsersSpy.calls.count()).toEqual(2); + expect(result.users.length).toEqual(2, "two people in users array"); + expect(result.notEmailed.length).toEqual(2, "two people not emailed"); + expect(result.errors.length).toEqual(2, "two errors returned"); + }); +}); diff --git a/packages/common/test/groups/_internal/processInviteUsers.test.ts b/packages/common/test/groups/_internal/processInviteUsers.test.ts new file mode 100644 index 00000000000..26e2455da4f --- /dev/null +++ b/packages/common/test/groups/_internal/processInviteUsers.test.ts @@ -0,0 +1,158 @@ +import { + IAddOrInviteContext, + IAddOrInviteEmail, +} from "../../../src/groups/types"; +import { MOCK_AUTH } from "../../mocks/mock-auth"; +import * as inviteUsersModule from "../../../src/groups/inviteUsers"; +import { processInviteUsers } from "../../../src/groups/_internal/processInviteUsers"; +import { ArcGISRequestError } from "@esri/arcgis-rest-request"; + +describe("processInviteUsers: ", () => { + let inviteUsersSpy: jasmine.Spy; + + beforeEach(() => { + inviteUsersSpy = spyOn(inviteUsersModule, "inviteUsers"); + }); + afterEach(() => { + inviteUsersSpy.calls.reset(); + }); + it("flows through happy path...", async () => { + const context: IAddOrInviteContext = { + groupId: "abc123", + primaryRO: MOCK_AUTH, + allUsers: [ + { orgType: "world", username: "bob" }, + { orgType: "world", username: "frank" }, + { orgType: "community", username: "bob" }, + { orgType: "community", username: "frank" }, + ], + canAutoAddUser: false, + addUserAsGroupAdmin: false, + email: undefined as unknown as IAddOrInviteEmail, + world: [ + { orgType: "world", username: "bob" }, + { orgType: "world", username: "frank" }, + ], + org: [], + community: [ + { orgType: "community", username: "bob" }, + { orgType: "community", username: "frank" }, + ], + partnered: [], + collaborationCoordinator: [], + }; + inviteUsersSpy.and.callFake(() => Promise.resolve({ success: true })); + const result = await processInviteUsers(context, "community"); + expect(inviteUsersSpy).toHaveBeenCalled(); + expect(inviteUsersSpy.calls.count()).toEqual(2); + expect(inviteUsersSpy.calls.argsFor(0)[4]).toEqual("group_member"); + expect(inviteUsersSpy.calls.argsFor(1)[4]).toEqual("group_member"); + expect(result.users.length).toEqual(2); + expect(result.notInvited.length).toEqual(0); + expect(result.errors.length).toEqual(0); + }); + it("flows through happy path...when inviting as admin", async () => { + const context: IAddOrInviteContext = { + groupId: "abc123", + primaryRO: MOCK_AUTH, + allUsers: [ + { orgType: "world", username: "bob" }, + { orgType: "world", username: "frank" }, + { orgType: "community", username: "bob" }, + { orgType: "community", username: "frank" }, + ], + canAutoAddUser: false, + addUserAsGroupAdmin: true, + email: undefined as unknown as IAddOrInviteEmail, + world: [ + { orgType: "world", username: "bob" }, + { orgType: "world", username: "frank" }, + ], + org: [], + community: [ + { orgType: "community", username: "bob" }, + { orgType: "community", username: "frank" }, + ], + partnered: [], + collaborationCoordinator: [], + }; + inviteUsersSpy.and.callFake(() => Promise.resolve({ success: true })); + const result = await processInviteUsers(context, "community"); + expect(inviteUsersSpy).toHaveBeenCalled(); + expect(inviteUsersSpy.calls.count()).toEqual(2); + expect(inviteUsersSpy.calls.argsFor(0)[4]).toEqual("group_admin"); + expect(inviteUsersSpy.calls.argsFor(1)[4]).toEqual("group_admin"); + expect(result.users.length).toEqual(2); + expect(result.notInvited.length).toEqual(0); + expect(result.errors.length).toEqual(0); + }); + it("handles matters when success is false.", async () => { + const context: IAddOrInviteContext = { + groupId: "abc123", + primaryRO: MOCK_AUTH, + allUsers: [ + { orgType: "world", username: "bob" }, + { orgType: "world", username: "frank" }, + { orgType: "community", username: "bob" }, + { orgType: "community", username: "frank" }, + ], + canAutoAddUser: false, + addUserAsGroupAdmin: false, + email: undefined as unknown as IAddOrInviteEmail, + world: [ + { orgType: "world", username: "bob" }, + { orgType: "world", username: "frank" }, + ], + org: [], + community: [ + { orgType: "community", username: "bob" }, + { orgType: "community", username: "frank" }, + ], + partnered: [], + collaborationCoordinator: [], + }; + inviteUsersSpy.and.callFake(() => Promise.resolve({ success: false })); + const result = await processInviteUsers(context, "community"); + expect(inviteUsersSpy).toHaveBeenCalled(); + expect(inviteUsersSpy.calls.count()).toEqual(2); + expect(result.users.length).toEqual(2); + expect(result.notInvited.length).toEqual(2); + expect(result.errors.length).toEqual(0); + }); + it("handles matters when errors are returned.", async () => { + const context: IAddOrInviteContext = { + groupId: "abc123", + primaryRO: MOCK_AUTH, + allUsers: [ + { orgType: "community", username: "bob" }, + { orgType: "community", username: "frank" }, + { orgType: "world", username: "bob" }, + { orgType: "world", username: "frank" }, + ], + canAutoAddUser: false, + addUserAsGroupAdmin: false, + email: undefined as unknown as IAddOrInviteEmail, + world: [ + { orgType: "world", username: "bob" }, + { orgType: "world", username: "frank" }, + ], + org: [], + community: [ + { orgType: "community", username: "bob" }, + { orgType: "community", username: "frank" }, + ], + partnered: [], + collaborationCoordinator: [], + }; + const error = new ArcGISRequestError("Email not sent"); + inviteUsersSpy.and.callFake(() => + Promise.resolve({ success: false, errors: [error] }) + ); + const result = await processInviteUsers(context, "community"); + expect(inviteUsersSpy).toHaveBeenCalled(); + expect(inviteUsersSpy.calls.count()).toEqual(2); + expect(result.users.length).toEqual(2); + expect(result.notInvited.length).toEqual(2); + expect(result.errors.length).toEqual(2); + }); +}); diff --git a/packages/common/test/groups/add-users-workflow/add-users-to-group.test.ts b/packages/common/test/groups/add-users-workflow/add-users-to-group.test.ts index 91cbc138f4f..d64904011ec 100644 --- a/packages/common/test/groups/add-users-workflow/add-users-to-group.test.ts +++ b/packages/common/test/groups/add-users-workflow/add-users-to-group.test.ts @@ -11,8 +11,8 @@ import { addUsersToGroup } from "../../../src/groups/add-users-workflow/add-user import { IAddMemberContext, IConsolidatedResult, - IEmail } from "../../../src/groups/add-users-workflow/interfaces"; +import { IEmail } from "../../../src/groups/types"; import { IUser } from "@esri/arcgis-rest-portal"; import { IHubRequestOptions } from "../../../src/types"; import { cloneObject } from "../../../src/util"; @@ -43,44 +43,44 @@ describe("add-users-to-group", () => { const requestingUser: IUser = { username: "Marcus", orgId: eOrgId, - privileges: [] + privileges: [], }; const communityAdmin: IUser = { username: "Adam", - orgId: cOrgId + orgId: cOrgId, }; const users: IUser[] = [ { username: "Baird", - orgId: eOrgId + orgId: eOrgId, }, { username: "Dom", - orgId: eOrgId + orgId: eOrgId, }, { username: "Cole", - orgId: eOrgId + orgId: eOrgId, }, { username: "Anya", - orgId: cOrgId + orgId: cOrgId, }, { username: "Myrrah", - orgId: otherOrgId - } + orgId: otherOrgId, + }, ]; const usersToAutoAdd: IUser[] = []; const usersToInvite: IUser[] = users; const usersToEmail: IUser[] = users.filter( - u => cOrgId === u.orgId || eOrgId === u.orgId + (u) => cOrgId === u.orgId || eOrgId === u.orgId ); const email: IEmail = { subject: "Time's up", - body: "Let's do this" + body: "Let's do this", }; const primaryRO: IHubRequestOptions = { @@ -96,12 +96,12 @@ describe("add-users-to-group", () => { hub: { settings: { communityOrg: { - orgId: cOrgId - } - } - } - } - } + orgId: cOrgId, + }, + }, + }, + }, + }, }; const secondaryRO: IHubRequestOptions = { @@ -112,8 +112,8 @@ describe("add-users-to-group", () => { isPortal: false, id: "a different portal id", name: "a different portal name", - user: communityAdmin - } + user: communityAdmin, + }, }; ////////////////////////////////// @@ -129,7 +129,7 @@ describe("add-users-to-group", () => { requestingUser: Object.assign(cloneObject(requestingUser), { cOrgId }), email, primaryRO, - secondaryRO + secondaryRO, }; const autoAddResultContext: IAddMemberContext = cloneObject(baseContext); @@ -150,11 +150,11 @@ describe("add-users-to-group", () => { success: true, autoAdd: undefined, invite: { - success: true + success: true, }, email: { - success: true - } + success: true, + }, }; //////////////////////////////////// diff --git a/packages/common/test/groups/add-users-workflow/output-processors/_process-auto-add.test.ts b/packages/common/test/groups/add-users-workflow/output-processors/_process-auto-add.test.ts index 03ab005a8e4..0047ce00fd6 100644 --- a/packages/common/test/groups/add-users-workflow/output-processors/_process-auto-add.test.ts +++ b/packages/common/test/groups/add-users-workflow/output-processors/_process-auto-add.test.ts @@ -1,7 +1,7 @@ import { IAddGroupUsersResult } from "@esri/arcgis-rest-portal"; import { cloneObject, IAddMemberContext } from "../../../../src"; import * as formatResponseModule from "../../../../src/groups/add-users-workflow/output-processors/_format-auto-add-response"; -import * as autoAddModule from "../../../../src/groups/add-users-workflow/workflow-sections/auto-add-users"; +import * as autoAddModule from "../../../../src/groups/autoAddUsers"; import { _processAutoAdd } from "../../../../src/groups/add-users-workflow/output-processors/_process-auto-add"; describe("_process_auto_add", () => { @@ -16,14 +16,14 @@ describe("_process_auto_add", () => { primaryRO: { authentication: null, isPortal: false, - hubApiUrl: "" - } + hubApiUrl: "", + }, }; const autoAddResult: IAddGroupUsersResult = { notAdded: [] }; const formattedResult: IAddMemberContext = Object.assign( { - autoAddResult: { success: true } + autoAddResult: { success: true }, }, cloneObject(context) ); diff --git a/packages/common/test/groups/add-users-workflow/output-processors/_process-invite.test.ts b/packages/common/test/groups/add-users-workflow/output-processors/_process-invite.test.ts index ed1d03153fb..7f5b63b686b 100644 --- a/packages/common/test/groups/add-users-workflow/output-processors/_process-invite.test.ts +++ b/packages/common/test/groups/add-users-workflow/output-processors/_process-invite.test.ts @@ -1,9 +1,9 @@ import { IAddGroupUsersResult, - IInviteGroupUsersResult + IInviteGroupUsersResult, } from "@esri/arcgis-rest-portal"; import { IAddMemberContext, _processInvite } from "../../../../src"; -import * as inviteModule from "../../../../src/groups/add-users-workflow/workflow-sections/invite-users"; +import * as inviteModule from "../../../../src/groups/inviteUsers"; describe("_process_auto_add", () => { it("Delegates properly to inviteUsers and modifies context", async () => { @@ -17,8 +17,8 @@ describe("_process_auto_add", () => { primaryRO: { authentication: null, isPortal: false, - hubApiUrl: "" - } + hubApiUrl: "", + }, }; const inviteResult: IInviteGroupUsersResult = { success: true }; diff --git a/packages/common/test/groups/add-users-workflow/output-processors/_process-primary-email.test.ts b/packages/common/test/groups/add-users-workflow/output-processors/_process-primary-email.test.ts index fcd9a8deb03..b7e482db72d 100644 --- a/packages/common/test/groups/add-users-workflow/output-processors/_process-primary-email.test.ts +++ b/packages/common/test/groups/add-users-workflow/output-processors/_process-primary-email.test.ts @@ -3,10 +3,10 @@ import { cloneObject, IAddMemberContext, IEmail, - IHubRequestOptions + IHubRequestOptions, } from "../../../../src"; import { _processPrimaryEmail } from "../../../../src/groups/add-users-workflow/output-processors/_process-primary-email"; -import * as emailModule from "../../../../src/groups/add-users-workflow/workflow-sections/email-org-users"; +import * as emailModule from "../../../../src/groups/emailOrgUsers"; import * as isAdminModule from "../../../../src/groups/add-users-workflow/utils/_is-org-admin"; describe("_processPrimaryEmail", () => { const orgId = "the_swamp"; @@ -14,7 +14,7 @@ describe("_processPrimaryEmail", () => { const users: restPortalModule.IUser[] = [ { username: "Shrek", orgId }, { username: "Fiona", orgId }, - { username: "Donkey", orgId } + { username: "Donkey", orgId }, ]; const baseContext: IAddMemberContext = { @@ -24,7 +24,7 @@ describe("_processPrimaryEmail", () => { usersToEmail: users, usersToInvite: [], requestingUser: null, - primaryRO: null + primaryRO: null, }; let emailSpy: jasmine.Spy; @@ -53,15 +53,15 @@ describe("_processPrimaryEmail", () => { it("Doesn't modify context if inviteResult is unsuccessful/non-existent", async () => { const email: IEmail = { subject: "subject", - body: "body" + body: "body", }; const context = Object.assign(cloneObject(baseContext), { - email: cloneObject(email) + email: cloneObject(email), }); const actual = await _processPrimaryEmail(context); const expected = Object.assign(cloneObject(baseContext), { - email: cloneObject(email) + email: cloneObject(email), }); expect(emailSpy).not.toHaveBeenCalled(); expect(actual).toEqual(expected); @@ -70,7 +70,7 @@ describe("_processPrimaryEmail", () => { it("Delegates to emailOrgUser and modifies the context object", async () => { const email: IEmail = { subject: "subject", - body: "body" + body: "body", }; const primaryRO: IHubRequestOptions = { @@ -81,8 +81,8 @@ describe("_processPrimaryEmail", () => { isPortal: false, id: "portalId", name: "a name", - user: { username: "Mr. Rooney", orgId } - } + user: { username: "Mr. Rooney", orgId }, + }, }; const inviteResult = { success: true }; @@ -91,14 +91,14 @@ describe("_processPrimaryEmail", () => { const context = Object.assign(cloneObject(baseContext), { email: cloneObject(email), primaryRO: cloneObject(primaryRO), - inviteResult: cloneObject(inviteResult) + inviteResult: cloneObject(inviteResult), }); const expected = Object.assign(cloneObject(baseContext), { email: cloneObject(email), primaryRO: cloneObject(primaryRO), inviteResult: cloneObject(inviteResult), - primaryEmailResult: cloneObject(primaryEmailResult) + primaryEmailResult: cloneObject(primaryEmailResult), }); const actual = await _processPrimaryEmail(context); diff --git a/packages/common/test/groups/add-users-workflow/output-processors/_process-secondary-email.test.ts b/packages/common/test/groups/add-users-workflow/output-processors/_process-secondary-email.test.ts index b7111094d42..4e7704dda74 100644 --- a/packages/common/test/groups/add-users-workflow/output-processors/_process-secondary-email.test.ts +++ b/packages/common/test/groups/add-users-workflow/output-processors/_process-secondary-email.test.ts @@ -1,10 +1,8 @@ import * as restPortalModule from "@esri/arcgis-rest-portal"; -import { - IAddMemberContext, - IEmail -} from "../../../../src/groups/add-users-workflow/interfaces"; +import { IAddMemberContext } from "../../../../src/groups/add-users-workflow/interfaces"; +import { IEmail } from "../../../../src/groups/types"; import { _processSecondaryEmail } from "../../../../src/groups/add-users-workflow/output-processors/_process-secondary-email"; -import * as emailModule from "../../../../src/groups/add-users-workflow/workflow-sections/email-org-users"; +import * as emailModule from "../../../../src/groups/emailOrgUsers"; import * as isAdminModule from "../../../../src/groups/add-users-workflow/utils/_is-org-admin"; import { IHubRequestOptions } from "../../../../src/types"; import { cloneObject } from "../../../../src/util"; @@ -14,7 +12,7 @@ describe("_processSecondaryEmail", () => { const users: restPortalModule.IUser[] = [ { username: "Ferris", orgId }, { username: "Cameron", orgId }, - { username: "Sloane", orgId } + { username: "Sloane", orgId }, ]; const baseContext: IAddMemberContext = { @@ -24,7 +22,7 @@ describe("_processSecondaryEmail", () => { usersToEmail: [], usersToInvite: users, requestingUser: null, - primaryRO: null + primaryRO: null, }; let emailSpy: jasmine.Spy; @@ -53,14 +51,14 @@ describe("_processSecondaryEmail", () => { it("Doesn't modify context if no secondaryRO object provided", async () => { const email: IEmail = { subject: "subject", - body: "body" + body: "body", }; const context = Object.assign(cloneObject(baseContext), { - email: cloneObject(email) + email: cloneObject(email), }); const actual = await _processSecondaryEmail(context); const expected = Object.assign(cloneObject(baseContext), { - email: cloneObject(email) + email: cloneObject(email), }); expect(emailSpy).not.toHaveBeenCalled(); expect(actual).toEqual(expected); @@ -69,7 +67,7 @@ describe("_processSecondaryEmail", () => { it("Doesn't modify context if inviteResult is unsuccessful/non-existent", async () => { const email: IEmail = { subject: "subject", - body: "body" + body: "body", }; const secondaryRO: IHubRequestOptions = { @@ -80,17 +78,17 @@ describe("_processSecondaryEmail", () => { isPortal: false, id: "portalId", name: "a name", - user: { username: "Mr. Rooney", orgId } - } + user: { username: "Mr. Rooney", orgId }, + }, }; const context = Object.assign(cloneObject(baseContext), { email: cloneObject(email), - secondaryRO: cloneObject(secondaryRO) + secondaryRO: cloneObject(secondaryRO), }); const actual = await _processSecondaryEmail(context); const expected = Object.assign(cloneObject(baseContext), { email: cloneObject(email), - secondaryRO: cloneObject(secondaryRO) + secondaryRO: cloneObject(secondaryRO), }); expect(emailSpy).not.toHaveBeenCalled(); expect(actual).toEqual(expected); @@ -99,7 +97,7 @@ describe("_processSecondaryEmail", () => { it("Delegates to emailOrgUser and modifies the context object", async () => { const email: IEmail = { subject: "subject", - body: "body" + body: "body", }; const secondaryRO: IHubRequestOptions = { @@ -110,8 +108,8 @@ describe("_processSecondaryEmail", () => { isPortal: false, id: "portalId", name: "a name", - user: { username: "Mr. Rooney", orgId } - } + user: { username: "Mr. Rooney", orgId }, + }, }; const inviteResult = { success: true }; @@ -120,14 +118,14 @@ describe("_processSecondaryEmail", () => { const context = Object.assign(cloneObject(baseContext), { email: cloneObject(email), secondaryRO: cloneObject(secondaryRO), - inviteResult: cloneObject(inviteResult) + inviteResult: cloneObject(inviteResult), }); const expected = Object.assign(cloneObject(baseContext), { email: cloneObject(email), secondaryRO: cloneObject(secondaryRO), inviteResult: cloneObject(inviteResult), - secondaryEmailResult: cloneObject(secondaryEmailResult) + secondaryEmailResult: cloneObject(secondaryEmailResult), }); const actual = await _processSecondaryEmail(context); diff --git a/packages/common/test/groups/addOrInviteUsersToGroup.test.ts b/packages/common/test/groups/addOrInviteUsersToGroup.test.ts new file mode 100644 index 00000000000..94f9639c05f --- /dev/null +++ b/packages/common/test/groups/addOrInviteUsersToGroup.test.ts @@ -0,0 +1,88 @@ +import { ArcGISRequestError } from "@esri/arcgis-rest-request"; +import { + IAddOrInviteEmail, + IAddOrInviteResponse, + IUserWithOrgType, +} from "../../src/groups/types"; +import { MOCK_AUTH } from "../mocks/mock-auth"; +import { addOrInviteUsersToGroup } from "../../src/groups/addOrInviteUsersToGroup"; +import * as utilsModule from "../../src/groups/_internal/AddOrInviteUsersToGroupUtils"; + +describe("addOrInviteUsersToGroup: ", () => { + let addOrInviteCommunityUsersSpy: jasmine.Spy; + let addOrInviteOrgUsersSpy: jasmine.Spy; + let addOrInviteWorldUsersSpy: jasmine.Spy; + + beforeEach(() => { + addOrInviteCommunityUsersSpy = spyOn( + utilsModule, + "addOrInviteCommunityUsers" + ); + addOrInviteOrgUsersSpy = spyOn(utilsModule, "addOrInviteOrgUsers"); + addOrInviteWorldUsersSpy = spyOn(utilsModule, "addOrInviteWorldUsers"); + }); + + afterEach(() => { + addOrInviteCommunityUsersSpy.calls.reset(); + addOrInviteOrgUsersSpy.calls.reset(); + addOrInviteWorldUsersSpy.calls.reset(); + }); + + it("all works...", async () => { + const users: IUserWithOrgType[] = [ + { orgType: "world", username: "bob" }, + { orgType: "world", username: "bobb" }, + { orgType: "world", username: "bobbb" }, + { orgType: "org", username: "frank" }, + { orgType: "org", username: "frankk" }, + { orgType: "community", username: "dobby" }, + ]; + const error = new ArcGISRequestError("error in addOrInviteUsersToGroup"); + addOrInviteCommunityUsersSpy.and.callFake(() => { + const result: IAddOrInviteResponse = { + users: [], + notInvited: [], + notAdded: ["dobby"], + notEmailed: ["dobby"], + errors: [error, error], + }; + return Promise.resolve(result); + }); + addOrInviteOrgUsersSpy.and.callFake(() => { + const result: IAddOrInviteResponse = { + users: [], + notInvited: [], + notAdded: ["frank"], + notEmailed: [], + errors: [], + }; + return Promise.resolve(result); + }); + addOrInviteWorldUsersSpy.and.callFake(() => { + const result: IAddOrInviteResponse = { + users: [], + notInvited: ["bob", "bobb"], + notAdded: [], + notEmailed: [], + errors: [error, error], + }; + return Promise.resolve(result); + }); + const response = await addOrInviteUsersToGroup( + "abc123", + users, + MOCK_AUTH, + false, + false, + undefined as unknown as IAddOrInviteEmail + ); + expect(addOrInviteCommunityUsersSpy).toHaveBeenCalled(); + expect(addOrInviteOrgUsersSpy).toHaveBeenCalled(); + expect(addOrInviteWorldUsersSpy).toHaveBeenCalled(); + expect(response.notAdded.length).toEqual(2); + expect(response.notInvited.length).toEqual(2); + expect(response.notEmailed.length).toEqual(1); + expect(response.errors.length).toEqual(4); + expect(response.groupId).toEqual("abc123"); + }); +}); diff --git a/packages/common/test/groups/addOrInviteUsersToGroups.test.ts b/packages/common/test/groups/addOrInviteUsersToGroups.test.ts new file mode 100644 index 00000000000..9ab6020a13d --- /dev/null +++ b/packages/common/test/groups/addOrInviteUsersToGroups.test.ts @@ -0,0 +1,85 @@ +import { ArcGISRequestError } from "@esri/arcgis-rest-request"; +import { + IAddOrInviteToGroupResult, + IUserWithOrgType, +} from "../../src/groups/types"; +import { MOCK_AUTH } from "../mocks/mock-auth"; +import * as addOrInviteUsersToGroupModule from "../../src/groups/addOrInviteUsersToGroup"; +import { addOrInviteUsersToGroups } from "../../src/groups/addOrInviteUsersToGroups"; + +describe("addOrInviteUsersToGroups: ", () => { + it("all works...", async () => { + const users: IUserWithOrgType[] = [ + { orgType: "world", username: "bob" }, + { orgType: "world", username: "bobb" }, + { orgType: "world", username: "bobbb" }, + { orgType: "org", username: "frank" }, + { orgType: "org", username: "frankk" }, + { orgType: "community", username: "dobby" }, + { orgType: "partnered", username: "randy" }, + { orgType: "partnered", username: "jupe" }, + { orgType: "collaborationCoordinator", username: "freddy" }, + ]; + const error = new ArcGISRequestError("error in addOrInviteUsersToGroups"); + const addOrInviteUsersToGroupSpy = spyOn( + addOrInviteUsersToGroupModule, + "addOrInviteUsersToGroup" + ).and.callFake(() => { + const response: IAddOrInviteToGroupResult = { + groupId: "abc123", + notAdded: ["dobby", "frank"], + notInvited: ["bob", "bobb"], + notEmailed: ["dobby"], + errors: [error, error, error, error], + community: { + users: [], + notInvited: [], + notAdded: ["dobby"], + notEmailed: ["dobby"], + errors: [error, error], + }, + org: { + users: [], + notInvited: [], + notAdded: ["frank"], + notEmailed: [], + errors: [], + }, + world: { + users: [], + notInvited: ["bob", "bobb"], + notAdded: [], + notEmailed: [], + errors: [error, error], + }, + partnered: { + users: ["randy", "jupe"], + notInvited: [], + notAdded: [], + notEmailed: [], + errors: [], + }, + collaborationCoordinator: { + users: ["freddy"], + notInvited: [], + notAdded: [], + notEmailed: [], + errors: [], + }, + }; + return Promise.resolve(response); + }); + const result = await addOrInviteUsersToGroups( + ["abc123", "def456", "ghi789"], + users, + MOCK_AUTH + ); + expect(addOrInviteUsersToGroupSpy).toHaveBeenCalled(); + expect(addOrInviteUsersToGroupSpy.calls.count()).toEqual(3); + expect(result.responses.length).toEqual(3); + expect(result.notAdded.length).toEqual(6); + expect(result.notInvited.length).toEqual(6); + expect(result.notEmailed.length).toEqual(3); + expect(result.errors.length).toEqual(12); + }); +}); diff --git a/packages/common/test/groups/add-users-workflow/workflow-sections/auto-add-users.test.ts b/packages/common/test/groups/autoAddUsers.test.ts similarity index 80% rename from packages/common/test/groups/add-users-workflow/workflow-sections/auto-add-users.test.ts rename to packages/common/test/groups/autoAddUsers.test.ts index bad9bf4c221..d12000783e0 100644 --- a/packages/common/test/groups/add-users-workflow/workflow-sections/auto-add-users.test.ts +++ b/packages/common/test/groups/autoAddUsers.test.ts @@ -1,13 +1,13 @@ import * as restPortalModule from "@esri/arcgis-rest-portal"; -import { autoAddUsers } from "../../../../src/groups/add-users-workflow/workflow-sections/auto-add-users"; -import { MOCK_AUTH } from "../fixtures"; +import { autoAddUsers } from "../../src/groups/autoAddUsers"; +import { MOCK_AUTH } from "./add-users-workflow/fixtures"; -describe("auto-add-users", function() { +describe("auto-add-users", function () { let addSpy: jasmine.Spy; const users: restPortalModule.IUser[] = [ { username: "luke" }, { username: "leia" }, - { username: "han" } + { username: "han" }, ]; const groupId = "rebel_alliance"; @@ -28,8 +28,8 @@ describe("auto-add-users", function() { { authentication: MOCK_AUTH, id: groupId, - users: users.map(u => u.username) - } + users: users.map((u) => u.username), + }, ]; expect(actualArgs).toEqual(expectedArgs); }); diff --git a/packages/common/test/groups/add-users-workflow/workflow-sections/email-org-users.test.ts b/packages/common/test/groups/emailOrgUsers.test.ts similarity index 82% rename from packages/common/test/groups/add-users-workflow/workflow-sections/email-org-users.test.ts rename to packages/common/test/groups/emailOrgUsers.test.ts index d5a499b9c09..d435ef804f3 100644 --- a/packages/common/test/groups/add-users-workflow/workflow-sections/email-org-users.test.ts +++ b/packages/common/test/groups/emailOrgUsers.test.ts @@ -1,17 +1,18 @@ import * as restPortalModule from "@esri/arcgis-rest-portal"; -import { IEmail, emailOrgUsers } from "../../../../src"; -import { MOCK_AUTH } from "../fixtures"; +import { emailOrgUsers } from "../../src/groups/emailOrgUsers"; +import { IEmail } from "../../src/groups/types"; +import { MOCK_AUTH } from "./add-users-workflow/fixtures"; -describe("email-org-users", function() { +describe("email-org-users", function () { let notificationSpy: jasmine.Spy; const users: restPortalModule.IUser[] = [ { username: "huey" }, { username: "dewey" }, - { username: "louie" } + { username: "louie" }, ]; const email: IEmail = { subject: "subject", - body: "body" + body: "body", }; beforeEach(() => { @@ -33,8 +34,8 @@ describe("email-org-users", function() { message: email.body, subject: email.subject, notificationChannelType: "email", - users: users.map(u => u.username) - } + users: users.map((u) => u.username), + }, ]; expect(actualArgs).toEqual(expectedArgs); }); @@ -51,9 +52,9 @@ describe("email-org-users", function() { message: email.body, subject: email.subject, notificationChannelType: "email", - users: users.map(u => u.username), - batchSize: 1 - } + users: users.map((u) => u.username), + batchSize: 1, + }, ]; expect(actualArgs).toEqual(expectedArgs); }); diff --git a/packages/common/test/groups/add-users-workflow/workflow-sections/invite-users.test.ts b/packages/common/test/groups/inviteUsers.test.ts similarity index 82% rename from packages/common/test/groups/add-users-workflow/workflow-sections/invite-users.test.ts rename to packages/common/test/groups/inviteUsers.test.ts index b81a5428dda..dbc638d04f7 100644 --- a/packages/common/test/groups/add-users-workflow/workflow-sections/invite-users.test.ts +++ b/packages/common/test/groups/inviteUsers.test.ts @@ -1,13 +1,13 @@ import * as restPortalModule from "@esri/arcgis-rest-portal"; -import { inviteUsers } from "../../../../src/groups/add-users-workflow/workflow-sections/invite-users"; -import { MOCK_AUTH } from "../fixtures"; +import { inviteUsers } from "../../src/groups/inviteUsers"; +import { MOCK_AUTH } from "./add-users-workflow/fixtures"; -describe("invite-users", function() { +describe("invite-users", function () { let invitationSpy: jasmine.Spy; const users: restPortalModule.IUser[] = [ { username: "harry" }, { username: "ron" }, - { username: "hermione" } + { username: "hermione" }, ]; const groupId = "gryffindor"; @@ -28,10 +28,10 @@ describe("invite-users", function() { { authentication: MOCK_AUTH, id: groupId, - users: users.map(u => u.username), + users: users.map((u) => u.username), role: "group_member", - expiration: 20160 - } + expiration: 20160, + }, ]; expect(actualArgs).toEqual(expectedArgs); }); @@ -46,10 +46,10 @@ describe("invite-users", function() { { authentication: MOCK_AUTH, id: groupId, - users: users.map(u => u.username), + users: users.map((u) => u.username), role: "group_member", - expiration: 9001 - } + expiration: 9001, + }, ]; expect(actualArgs).toEqual(expectedArgs); });