diff --git a/package-lock.json b/package-lock.json index 423c79c419..9849fe7495 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10782,9 +10782,9 @@ "dev": true }, "path-to-regexp": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.7.0.tgz", - "integrity": "sha1-Wf3g9DW62suhA6hOnTvGTpa5k30=", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", + "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", "dev": true, "requires": { "isarray": "0.0.1" diff --git a/packages/arcgis-rest-feature-layer/test/attachments.test.ts b/packages/arcgis-rest-feature-layer/test/attachments.test.ts index ca054b31bd..71a5a8b1e3 100644 --- a/packages/arcgis-rest-feature-layer/test/attachments.test.ts +++ b/packages/arcgis-rest-feature-layer/test/attachments.test.ts @@ -53,9 +53,7 @@ describe("attachment methods", () => { expect(fetchMock.called()).toBeTruthy(); const [url, options]: [string, RequestInit] = fetchMock.lastCall("*"); expect(url).toEqual( - `${requestOptions.url}/${ - requestOptions.featureId - }/attachments?f=json&gdbVersion=SDE.DEFAULT` + `${requestOptions.url}/${requestOptions.featureId}/attachments?f=json&gdbVersion=SDE.DEFAULT` ); expect(options.method).toBe("GET"); expect(getAttachmentsResponse.attachmentInfos.length).toEqual(2); diff --git a/packages/arcgis-rest-feature-layer/test/query.test.ts b/packages/arcgis-rest-feature-layer/test/query.test.ts index 7ce341817f..403581bf99 100644 --- a/packages/arcgis-rest-feature-layer/test/query.test.ts +++ b/packages/arcgis-rest-feature-layer/test/query.test.ts @@ -106,9 +106,7 @@ describe("getFeature() and queryFeatures()", () => { expect(fetchMock.called()).toBeTruthy(); const [url, options]: [string, RequestInit] = fetchMock.lastCall("*"); expect(url).toEqual( - `${ - requestOptions.url - }/query?f=json&where=Condition%3D'Poor'&outFields=FID%2CTree_ID%2CCmn_Name%2CCondition&geometry=%7B%7D&geometryType=esriGeometryPolygon&orderByFields=test` + `${requestOptions.url}/query?f=json&where=Condition%3D'Poor'&outFields=FID%2CTree_ID%2CCmn_Name%2CCondition&geometry=%7B%7D&geometryType=esriGeometryPolygon&orderByFields=test` ); expect(options.method).toBe("GET"); // expect(response.attributes.FID).toEqual(42); @@ -129,9 +127,7 @@ describe("getFeature() and queryFeatures()", () => { expect(fetchMock.called()).toBeTruthy(); const [url, options]: [string, RequestInit] = fetchMock.lastCall("*"); expect(url).toEqual( - `${ - requestOptions.url - }/queryRelatedRecords?f=json&definitionExpression=1%3D1&outFields=*&relationshipId=0` + `${requestOptions.url}/queryRelatedRecords?f=json&definitionExpression=1%3D1&outFields=*&relationshipId=0` ); expect(options.method).toBe("GET"); done(); diff --git a/packages/arcgis-rest-portal/src/groups/add-users.ts b/packages/arcgis-rest-portal/src/groups/add-users.ts index 9d2f1936ee..b25e4dfd92 100644 --- a/packages/arcgis-rest-portal/src/groups/add-users.ts +++ b/packages/arcgis-rest-portal/src/groups/add-users.ts @@ -9,6 +9,7 @@ import { import { getPortalUrl } from "../util/get-portal-url"; import { chunk } from "../util/array"; +import { IUserRequestOptions } from "@esri/arcgis-rest-auth"; export interface IAddGroupUsersOptions extends IRequestOptions { /** diff --git a/packages/arcgis-rest-portal/src/groups/update-user-membership.ts b/packages/arcgis-rest-portal/src/groups/update-user-membership.ts new file mode 100644 index 0000000000..bcb3378005 --- /dev/null +++ b/packages/arcgis-rest-portal/src/groups/update-user-membership.ts @@ -0,0 +1,63 @@ +/* Copyright (c) 2017-2018 Environmental Systems Research Institute, Inc. + * Apache-2.0 */ +import { IUserRequestOptions } from "@esri/arcgis-rest-auth"; +import { getPortalUrl } from "../util/get-portal-url"; +import { request } from "@esri/arcgis-rest-request"; + +export interface IUpdateGroupUsersResult { + /** + * Array of results + */ + results: any[]; +} + +export interface IUpdateGroupUsersOptions extends IUserRequestOptions { + /** + * Group ID + */ + id: string; + /** + * An array of usernames to be updated + */ + users: string[]; + /** + * Membership Type to update to + */ + newMemberType: "member" | "admin"; +} + +/** + * ```js + * import { updateUserMemberships } from "@esri/arcgis-rest-portal"; + * // + * updateUserMemberships({ + * id: groupId, + * admins: ["username3"], + * authentication + * }) + * .then(response); + * ``` + * Change the user membership levels of existing users in a group + * + * @param requestOptions - Options for the request + * @returns A Promise + */ +export function updateUserMemberships( + requestOptions: IUpdateGroupUsersOptions +): Promise { + const url = `${getPortalUrl(requestOptions)}/community/groups/${ + requestOptions.id + }/updateUsers`; + const opts: any = { + authentication: requestOptions.authentication, + params: {} + }; + // add the correct params depending on the type of membership we are changing to + if (requestOptions.newMemberType === "admin") { + opts.params.admins = requestOptions.users; + } else { + opts.params.users = requestOptions.users; + } + // make the request + return request(url, opts); +} diff --git a/packages/arcgis-rest-portal/src/index.ts b/packages/arcgis-rest-portal/src/index.ts index ba8b0ae081..ef7a501b01 100644 --- a/packages/arcgis-rest-portal/src/index.ts +++ b/packages/arcgis-rest-portal/src/index.ts @@ -21,6 +21,7 @@ export * from "./groups/protect"; export * from "./groups/remove"; export * from "./groups/search"; export * from "./groups/update"; +export * from "./groups/update-user-membership"; export * from "./groups/join"; export * from "./users/get-user"; diff --git a/packages/arcgis-rest-portal/src/sharing/access.ts b/packages/arcgis-rest-portal/src/sharing/access.ts index fe29764e16..8e5b9c6ef5 100644 --- a/packages/arcgis-rest-portal/src/sharing/access.ts +++ b/packages/arcgis-rest-portal/src/sharing/access.ts @@ -49,9 +49,7 @@ export function setItemAccess( } else { // if neither, updating the sharing isnt possible throw Error( - `This item can not be shared by ${ - requestOptions.authentication.username - }. They are neither the item owner nor an organization admin.` + `This item can not be shared by ${requestOptions.authentication.username}. They are neither the item owner nor an organization admin.` ); } }); diff --git a/packages/arcgis-rest-portal/src/sharing/group-sharing.ts b/packages/arcgis-rest-portal/src/sharing/group-sharing.ts index 405babb7be..16b7392619 100644 --- a/packages/arcgis-rest-portal/src/sharing/group-sharing.ts +++ b/packages/arcgis-rest-portal/src/sharing/group-sharing.ts @@ -1,8 +1,8 @@ /* Copyright (c) 2018 Environmental Systems Research Institute, Inc. * Apache-2.0 */ -import { request } from "@esri/arcgis-rest-request"; -import { IItem } from "@esri/arcgis-rest-types"; +import { request, setDefaultRequestOptions } from "@esri/arcgis-rest-request"; +import { IItem, IUser } from "@esri/arcgis-rest-types"; import { getPortalUrl } from "../util/get-portal-url"; import { IGroupSharingOptions, @@ -10,6 +10,13 @@ import { isOrgAdmin, getUserMembership } from "./helpers"; +import { IUserRequestOptions } from "@esri/arcgis-rest-auth"; +import { searchGroupUsers } from "../groups/get"; +import { getUser } from "../users/get-user"; +import { addGroupUsers, IAddGroupUsersResult } from "../groups/add-users"; +import { updateUserMemberships } from "../groups/update-user-membership"; +import { searchItems } from "../items/search"; +import { ISearchOptions } from "../util/search"; interface IGroupSharingUnsharingOptions extends IGroupSharingOptions { action: "share" | "unshare"; @@ -75,7 +82,7 @@ function changeGroupSharing( const itemOwner = requestOptions.owner || username; const isSharedEditingGroup = requestOptions.confirmItemControl || false; - return isOrgAdmin(requestOptions).then(admin => { + return isOrgAdmin(requestOptions).then(isAdmin => { const resultProp = requestOptions.action === "share" ? "notSharedWith" : "notUnsharedFrom"; // check if the item has already been shared with the group... @@ -96,59 +103,51 @@ function changeGroupSharing( // next check to ensure the user is a member of the group return getUserMembership(requestOptions) .then(membership => { - // if user is not a member of the group and not an admin - if (membership === "none" && !admin) { + // Stack all the exception conditions up top so we can + // strealine the promise chain + // if user is not a member of the group and not an orgAdmin + if (membership === "none" && !isAdmin) { // abort and reject promise throw Error( `This item can not be ${requestOptions.action}d by ${username} as they are not a member of the specified group ${requestOptions.groupId}.` ); - } else { - // ...they are some level of membership or org-admin - // if the current user does not own the item... - if (itemOwner !== username) { - // only item owners can share/unshare items w/ shared editing groups - if (isSharedEditingGroup) { - throw Error( - `This item can not be ${requestOptions.action}d to shared editing group ${requestOptions.groupId} by ${username} as they not the item owner.` - ); - } - // only item-owners, group-admin's, group-owners can unshare an item from a group - if ( - requestOptions.action === "unshare" && - membership !== "admin" && // not group admin - membership !== "owner" // not group owner - ) { - throw Error( - `This item can not be ${requestOptions.action}d from group ${requestOptions.groupId} by ${username} as they not the item owner, group admin or group owner.` - ); - } - } - - // at this point, the user *should* be able to take the action - - // only question is what url to use + } + // it's a sharedEditing Group and user is not owner, org orgAdmin + if (isSharedEditingGroup && itemOwner !== username && !isAdmin) { + // abort and reject promise + throw Error( + `This item can not be ${requestOptions.action}d to shared editing group ${requestOptions.groupId} by ${username} as they not the item owner.` + ); + } - // default to the non-owner url... - let url = `${getPortalUrl(requestOptions)}/content/items/${ - requestOptions.id - }/${requestOptions.action}`; + // only item-owners, group-admin's, group-owners can unshare an item from a group + if ( + requestOptions.action === "unshare" && + itemOwner !== username && // not item owner + membership !== "admin" && // not group admin + membership !== "owner" // not group owner + ) { + throw Error( + `This item can not be ${requestOptions.action}d from group ${requestOptions.groupId} by ${username} as they not the item owner, group admin or group owner.` + ); + } - // but if they are the owner, we use a different path... - if (itemOwner === username) { - url = `${getPortalUrl( - requestOptions - )}/content/users/${itemOwner}/items/${requestOptions.id}/${ - requestOptions.action - }`; + // if it's a sharedEditing Group, and the current user is not the owner, but an OrgAdmin + // then we can let call shareToGroupAsAdmin which will add the owner to the group + // and then share the item to the group + if (isSharedEditingGroup && itemOwner !== username && isAdmin) { + return shareToGroupAsAdmin(requestOptions); + } else { + // if the current user is a member of the target group + if (membership !== "none") { + // we let the sharing call go + return shareToGroup(requestOptions); + } else { + // otherwise - even if they are org_admin - we throw staying the current user must be a member of the group + throw Error( + `This item can not be ${requestOptions.action}d by ${username} as they are not a member of the specified group ${requestOptions.groupId}.` + ); } - - // now its finally time to do the sharing - requestOptions.params = { - groups: requestOptions.groupId, - confirmItemControl: requestOptions.confirmItemControl - }; - - return request(url, requestOptions); } }) .then(sharingResponse => { @@ -166,54 +165,135 @@ function changeGroupSharing( }); } +function shareToGroupAsAdmin( + requestOptions: IGroupSharingUnsharingOptions +): Promise { + const username = requestOptions.authentication.username; + const itemOwner = requestOptions.owner; + + return getUser({ + username: itemOwner, + authentication: requestOptions.authentication + }) + .then(userDetails => { + const userGroups = userDetails.groups; + const group = userGroups.find(g => { + return g.id === requestOptions.groupId; + }); + if (group) { + // they are in the group... + // check member type + if (group.userMembership.memberType !== "admin") { + // promote them + return updateUserMemberships({ + id: requestOptions.groupId, + users: [itemOwner], + newMemberType: "admin", + authentication: requestOptions.authentication + }).then(response => { + // convert the result into the right type + const notAdded = response.results.reduce( + (acc: any[], entry: any) => { + if (!entry.success) { + acc.push(entry.username); + } + return acc; + }, + [] + ); + // and return it + return { + notAdded + } as IAddGroupUsersResult; + }); + } else { + // they are already an admin in the group + // return the same response the API would if we added them + return { notAdded: [] } as IAddGroupUsersResult; + } + } else { + // add user to group as an admin + return addGroupUsers({ + id: requestOptions.groupId, + admins: [itemOwner], + authentication: requestOptions.authentication + }); + } + }) + .then(membershipResponse => { + if (membershipResponse.notAdded.length) { + throw Error( + `Error adding user ${itemOwner} to group ${requestOptions.groupId}. Consequently item ${requestOptions.id} was not shared to the group.` + ); + } else { + // then make the sharing call + return shareToGroup(requestOptions); + } + }); +} + +function shareToGroup( + requestOptions: IGroupSharingUnsharingOptions +): Promise { + const username = requestOptions.authentication.username; + const itemOwner = requestOptions.owner || username; + // decide what url to use + // default to the non-owner url... + let url = `${getPortalUrl(requestOptions)}/content/items/${ + requestOptions.id + }/${requestOptions.action}`; + + // but if they are the owner, we use a different path... + if (itemOwner === username) { + url = `${getPortalUrl(requestOptions)}/content/users/${itemOwner}/items/${ + requestOptions.id + }/${requestOptions.action}`; + } + + // now its finally time to do the sharing + requestOptions.params = { + groups: requestOptions.groupId, + confirmItemControl: requestOptions.confirmItemControl + }; + + return request(url, requestOptions); +} + /** + * ```js + * import { isItemSharedWithGroup } from "@esri/arcgis-rest-portal"; + * // + * isItemSharedWithGroup({ + * groupId: 'bc3, + * itemId: 'f56, + * authentication + * }) + * .then(isShared => {}) + * ``` * Find out whether or not an item is already shared with a group. * * @param requestOptions - Options for the request. NOTE: `rawResponse` is not supported by this operation. - * @returns A Promise that will resolve with the data from the response. + * @returns Promise that will resolve with true/false */ -function isItemSharedWithGroup( +export function isItemSharedWithGroup( requestOptions: IGroupSharingOptions ): Promise { - const query = { + const searchOpts = { q: `id: ${requestOptions.id} AND group: ${requestOptions.groupId}`, start: 1, num: 10, - sortField: "title" - }; + sortField: "title", + authentication: requestOptions.authentication, + httpMethod: "POST" + } as ISearchOptions; - // we need to append some params into requestOptions, so make a clone - // instead of mutating the params on the inbound requestOptions object - const options = { ...requestOptions, rawResponse: false }; - // instead of calling out to "@esri/arcgis-rest-items, make the request manually to forgoe another dependency - options.params = { - ...query, - ...requestOptions.params - }; - - const url = `${getPortalUrl(options)}/search`; - - // to do: just call searchItems now that its in the same package - return request(url, options).then(searchResponse => { - // if there are no search results at all, we know the item hasnt already been shared with the group - if (searchResponse.total === 0) { - return false; - } else { - let sharedItem: IItem; - // otherwise loop through and search for the id - searchResponse.results.some((item: IItem) => { - const matchedItem = item.id === requestOptions.id; - if (matchedItem) { - sharedItem = item; - } - return matchedItem; + return searchItems(searchOpts).then(searchResponse => { + let result = false; + if (searchResponse.total > 0) { + result = !!searchResponse.results.find((itm: any) => { + return itm.id === requestOptions.id; }); - - if (sharedItem) { - return true; - } else { - return false; - } + return result; } }); } diff --git a/packages/arcgis-rest-portal/src/users/invitation.ts b/packages/arcgis-rest-portal/src/users/invitation.ts index 7e8022a145..8c03cdd724 100644 --- a/packages/arcgis-rest-portal/src/users/invitation.ts +++ b/packages/arcgis-rest-portal/src/users/invitation.ts @@ -84,9 +84,7 @@ export function getUserInvitation( ): Promise { const username = encodeURIComponent(requestOptions.authentication.username); const portalUrl = getPortalUrl(requestOptions); - const url = `${portalUrl}/community/users/${username}/invitations/${ - requestOptions.invitationId - }`; + const url = `${portalUrl}/community/users/${username}/invitations/${requestOptions.invitationId}`; let options = { httpMethod: "GET" } as IGetUserInvitationOptions; options = { ...requestOptions, ...options }; @@ -120,9 +118,7 @@ export function acceptInvitation( }> { const username = encodeURIComponent(requestOptions.authentication.username); const portalUrl = getPortalUrl(requestOptions); - const url = `${portalUrl}/community/users/${username}/invitations/${ - requestOptions.invitationId - }/accept`; + const url = `${portalUrl}/community/users/${username}/invitations/${requestOptions.invitationId}/accept`; const options: IGetUserInvitationOptions = { ...requestOptions }; return request(url, options); @@ -153,9 +149,7 @@ export function declineInvitation( }> { const username = encodeURIComponent(requestOptions.authentication.username); const portalUrl = getPortalUrl(requestOptions); - const url = `${portalUrl}/community/users/${username}/invitations/${ - requestOptions.invitationId - }/decline`; + const url = `${portalUrl}/community/users/${username}/invitations/${requestOptions.invitationId}/decline`; const options: IGetUserInvitationOptions = { ...requestOptions }; return request(url, options); diff --git a/packages/arcgis-rest-portal/src/users/notification.ts b/packages/arcgis-rest-portal/src/users/notification.ts index 1e04fbca2e..0cd85fa40b 100644 --- a/packages/arcgis-rest-portal/src/users/notification.ts +++ b/packages/arcgis-rest-portal/src/users/notification.ts @@ -62,9 +62,7 @@ export function removeNotification( ): Promise<{ success: boolean; notificationId: string }> { const username = encodeURIComponent(requestOptions.authentication.username); const portalUrl = getPortalUrl(requestOptions); - const url = `${portalUrl}/community/users/${username}/notifications/${ - requestOptions.id - }/delete`; + const url = `${portalUrl}/community/users/${username}/notifications/${requestOptions.id}/delete`; return request(url, requestOptions); } diff --git a/packages/arcgis-rest-portal/test/groups/update-user-membership.test.ts b/packages/arcgis-rest-portal/test/groups/update-user-membership.test.ts new file mode 100644 index 0000000000..1661155d34 --- /dev/null +++ b/packages/arcgis-rest-portal/test/groups/update-user-membership.test.ts @@ -0,0 +1,62 @@ +/* Copyright (c) 2018 Environmental Systems Research Institute, Inc. + * Apache-2.0 */ + +import * as fetchMock from "fetch-mock"; + +import { MOCK_USER_SESSION } from "../mocks/sharing/sharing"; +import { TOMORROW } from "@esri/arcgis-rest-auth/test/utils"; +import { updateUserMemberships } from "../../src/groups/update-user-membership"; + +describe("udpate-user-membership", () => { + beforeEach(done => { + fetchMock.post("https://myorg.maps.arcgis.com/sharing/rest/generateToken", { + token: "fake-token", + expires: TOMORROW.getTime(), + username: "jsmith" + }); + + // make sure session doesnt cache metadata + MOCK_USER_SESSION.refreshSession() + .then(() => done()) + .catch(); + }); + + afterEach(fetchMock.restore); + + it("converts member to admin", done => { + fetchMock.post( + "https://myorg.maps.arcgis.com/sharing/rest/community/groups/3ef/updateUsers", + { results: [{ username: "casey", success: true }] } + ); + return updateUserMemberships({ + authentication: MOCK_USER_SESSION, + id: "3ef", + users: ["larry", "curly", "moe"], + newMemberType: "admin" + }).then(() => { + const opts: RequestInit = fetchMock.lastOptions( + "https://myorg.maps.arcgis.com/sharing/rest/community/groups/3ef/updateUsers" + ); + expect(opts.body).toContain("admins=larry%2Ccurly%2Cmoe"); + done(); + }); + }); + it("converts admin to member", done => { + fetchMock.post( + "https://myorg.maps.arcgis.com/sharing/rest/community/groups/3ef/updateUsers", + { results: [{ username: "casey", success: true }] } + ); + return updateUserMemberships({ + authentication: MOCK_USER_SESSION, + id: "3ef", + users: ["larry", "curly", "moe"], + newMemberType: "member" + }).then(() => { + const opts: RequestInit = fetchMock.lastOptions( + "https://myorg.maps.arcgis.com/sharing/rest/community/groups/3ef/updateUsers" + ); + expect(opts.body).toContain("users=larry%2Ccurly%2Cmoe"); + done(); + }); + }); +}); diff --git a/packages/arcgis-rest-portal/test/sharing/group-sharing.test.ts b/packages/arcgis-rest-portal/test/sharing/group-sharing.test.ts index 47c0941526..c7bee8ed21 100644 --- a/packages/arcgis-rest-portal/test/sharing/group-sharing.test.ts +++ b/packages/arcgis-rest-portal/test/sharing/group-sharing.test.ts @@ -165,6 +165,33 @@ describe("shareItemWithGroup() ::", () => { fail(e); }); }); + it("should throw is owner is not member of the group", done => { + // this is used when isOrgAdmin is called... + fetchMock + .once( + "https://myorg.maps.arcgis.com/sharing/rest/community/users/jsmith?f=json&token=fake-token", + OrgAdminUserResponse + ) + .once("https://myorg.maps.arcgis.com/sharing/rest/search", SearchResponse) + .get( + "https://myorg.maps.arcgis.com/sharing/rest/community/groups/t6b?f=json&token=fake-token", + GroupNonMemberResponse + ); + + shareItemWithGroup({ + authentication: MOCK_USER_SESSION, + id: "n3v", + groupId: "t6b" + }).catch(e => { + expect(fetchMock.done()).toBeTruthy( + "All fetchMocks should have been called" + ); + expect(e.message).toBe( + "This item can not be shared by jsmith as they are not a member of the specified group t6b." + ); + done(); + }); + }); it("should fail to share an item with a group if the request is made by a non-org admin and non-group member", done => { fetchMock.once( @@ -245,40 +272,6 @@ describe("shareItemWithGroup() ::", () => { }); }); - it("should fail share an item with a group by org administrator and pass through confirmItemControl", done => { - fetchMock.once( - "https://myorg.maps.arcgis.com/sharing/rest/community/users/jsmith?f=json&token=fake-token", - OrgAdminUserResponse - ); - - fetchMock.once( - "https://myorg.maps.arcgis.com/sharing/rest/search", - NoResultsSearchResponse - ); - - // called when we determine if the user is a member of the group - fetchMock.get( - "https://myorg.maps.arcgis.com/sharing/rest/community/groups/t6b?f=json&token=fake-token", - GroupOwnerResponse - ); - - shareItemWithGroup({ - authentication: MOCK_USER_SESSION, - id: "n3v", - groupId: "t6b", - owner: "casey", - confirmItemControl: true - }).catch(e => { - expect(fetchMock.done()).toBeTruthy( - "All fetchMocks should have been called" - ); - expect(e.message).toBe( - "This item can not be shared to shared editing group t6b by jsmith as they not the item owner." - ); - done(); - }); - }); - it("should fail unshare an item with a group by org administrator thats not a group member ", done => { fetchMock.once( "https://myorg.maps.arcgis.com/sharing/rest/community/users/jsmith?f=json&token=fake-token", @@ -449,7 +442,7 @@ describe("shareItemWithGroup() ::", () => { }); }); - it("should throw is non-owner tries to share to shared editing group", done => { + it("should throw if non-owner tries to share to shared editing group", done => { fetchMock.once( "https://myorg.maps.arcgis.com/sharing/rest/community/users/jsmith?f=json&token=fake-token", GroupMemberUserResponse @@ -516,6 +509,249 @@ describe("shareItemWithGroup() ::", () => { done(); }); }); + + describe("share item as org admin on behalf of other user ::", () => { + it("should add user to group then share item", done => { + fetchMock + .once( + "https://myorg.maps.arcgis.com/sharing/rest/community/users/jsmith?f=json&token=fake-token", + OrgAdminUserResponse + ) + .once( + "https://myorg.maps.arcgis.com/sharing/rest/search", + NoResultsSearchResponse + ) + .get( + "https://myorg.maps.arcgis.com/sharing/rest/community/groups/t6b?f=json&token=fake-token", + GroupOwnerResponse + ) + .once( + "https://myorg.maps.arcgis.com/sharing/rest/community/users/casey?f=json&token=fake-token", + { + username: "casey", + groups: [] as any[] + } + ) + .post( + "https://myorg.maps.arcgis.com/sharing/rest/community/groups/t6b/addUsers", + { notAdded: [] } + ) + .post( + "https://myorg.maps.arcgis.com/sharing/rest/content/items/n3v/share", + { notSharedWith: [], itemId: "n3v" } + ); + + shareItemWithGroup({ + authentication: MOCK_USER_SESSION, + id: "n3v", + groupId: "t6b", + owner: "casey", + confirmItemControl: true + }) + .then(result => { + expect(fetchMock.done()).toBeTruthy( + "All fetchMocks should have been called" + ); + // verify we added casey to t6b + const addUsersOptions: RequestInit = fetchMock.lastOptions( + "https://myorg.maps.arcgis.com/sharing/rest/community/groups/t6b/addUsers" + ); + expect(addUsersOptions.body).toContain("admins=casey"); + // verify we shared the item + const shareOptions: RequestInit = fetchMock.lastOptions( + "https://myorg.maps.arcgis.com/sharing/rest/content/items/n3v/share" + ); + expect(shareOptions.body).toContain("groups=t6b"); + expect(shareOptions.body).toContain("confirmItemControl=true"); + + done(); + }) + .catch(e => { + fail(); + }); + }); + it("should upgrade user to admin then share item", done => { + fetchMock + .once( + "https://myorg.maps.arcgis.com/sharing/rest/community/users/jsmith?f=json&token=fake-token", + OrgAdminUserResponse + ) + .once( + "https://myorg.maps.arcgis.com/sharing/rest/search", + NoResultsSearchResponse + ) + .get( + "https://myorg.maps.arcgis.com/sharing/rest/community/groups/t6b?f=json&token=fake-token", + GroupOwnerResponse + ) + .once( + "https://myorg.maps.arcgis.com/sharing/rest/community/users/casey?f=json&token=fake-token", + { + username: "casey", + groups: [ + { + id: "t6b", + userMembership: { + memberType: "member" + } + } + ] as any[] + } + ) + .post( + "https://myorg.maps.arcgis.com/sharing/rest/community/groups/t6b/updateUsers", + { results: [{ username: "casey", success: true }] } + ) + .post( + "https://myorg.maps.arcgis.com/sharing/rest/content/items/n3v/share", + { notSharedWith: [], itemId: "n3v" } + ); + + shareItemWithGroup({ + authentication: MOCK_USER_SESSION, + id: "n3v", + groupId: "t6b", + owner: "casey", + confirmItemControl: true + }) + .then(result => { + expect(fetchMock.done()).toBeTruthy( + "All fetchMocks should have been called" + ); + // verify we added casey to t6b + const addUsersOptions: RequestInit = fetchMock.lastOptions( + "https://myorg.maps.arcgis.com/sharing/rest/community/groups/t6b/updateUsers" + ); + expect(addUsersOptions.body).toContain("admins=casey"); + // verify we shared the item + const shareOptions: RequestInit = fetchMock.lastOptions( + "https://myorg.maps.arcgis.com/sharing/rest/content/items/n3v/share" + ); + expect(shareOptions.body).toContain("groups=t6b"); + expect(shareOptions.body).toContain("confirmItemControl=true"); + + done(); + }) + .catch(e => { + fail(); + }); + }); + it("should share item if user is already admin in group", done => { + fetchMock + .once( + "https://myorg.maps.arcgis.com/sharing/rest/community/users/jsmith?f=json&token=fake-token", + OrgAdminUserResponse + ) + .once( + "https://myorg.maps.arcgis.com/sharing/rest/search", + NoResultsSearchResponse + ) + .get( + "https://myorg.maps.arcgis.com/sharing/rest/community/groups/t6b?f=json&token=fake-token", + GroupOwnerResponse + ) + .once( + "https://myorg.maps.arcgis.com/sharing/rest/community/users/casey?f=json&token=fake-token", + { + username: "casey", + groups: [ + { + id: "t6b", + userMembership: { + memberType: "admin" + } + } + ] as any[] + } + ) + .post( + "https://myorg.maps.arcgis.com/sharing/rest/content/items/n3v/share", + { notSharedWith: [], itemId: "n3v" } + ); + + shareItemWithGroup({ + authentication: MOCK_USER_SESSION, + id: "n3v", + groupId: "t6b", + owner: "casey", + confirmItemControl: true + }) + .then(result => { + expect(fetchMock.done()).toBeTruthy( + "All fetchMocks should have been called" + ); + // verify we shared the item + const shareOptions: RequestInit = fetchMock.lastOptions( + "https://myorg.maps.arcgis.com/sharing/rest/content/items/n3v/share" + ); + expect(shareOptions.body).toContain("groups=t6b"); + expect(shareOptions.body).toContain("confirmItemControl=true"); + + done(); + }) + .catch(e => { + fail(); + }); + }); + it("should throw if we can not upgrade user membership", done => { + fetchMock + .once( + "https://myorg.maps.arcgis.com/sharing/rest/community/users/jsmith?f=json&token=fake-token", + OrgAdminUserResponse + ) + .once( + "https://myorg.maps.arcgis.com/sharing/rest/search", + NoResultsSearchResponse + ) + .get( + "https://myorg.maps.arcgis.com/sharing/rest/community/groups/t6b?f=json&token=fake-token", + GroupOwnerResponse + ) + .once( + "https://myorg.maps.arcgis.com/sharing/rest/community/users/casey?f=json&token=fake-token", + { + username: "casey", + groups: [ + { + id: "t6b", + userMembership: { + memberType: "member" + } + } + ] as any[] + } + ) + .post( + "https://myorg.maps.arcgis.com/sharing/rest/community/groups/t6b/updateUsers", + { results: [{ username: "casey", success: false }] } + ); + + return shareItemWithGroup({ + authentication: MOCK_USER_SESSION, + id: "n3v", + groupId: "t6b", + owner: "casey", + confirmItemControl: true + }) + .then(() => { + expect("").toBe("Should Throw, but it returned"); + fail(); + }) + .catch(e => { + expect(fetchMock.done()).toBeTruthy( + "All fetchMocks should have been called" + ); + const addUsersOptions: RequestInit = fetchMock.lastOptions( + "https://myorg.maps.arcgis.com/sharing/rest/community/groups/t6b/updateUsers" + ); + expect(addUsersOptions.body).toContain("admins=casey"); + expect(e.message).toBe( + "Error adding user casey to group t6b. Consequently item n3v was not shared to the group." + ); + done(); + }); + }); + }); }); describe("unshareItemWithGroup() ::", () => { diff --git a/packages/arcgis-rest-service-admin/test/create.test.ts b/packages/arcgis-rest-service-admin/test/create.test.ts index 6b3b97a20d..22bb5b81e6 100644 --- a/packages/arcgis-rest-service-admin/test/create.test.ts +++ b/packages/arcgis-rest-service-admin/test/create.test.ts @@ -356,9 +356,7 @@ describe("create feature service", () => { RequestInit ] = fetchMock.lastCall("end:move"); expect(urlMove).toEqual( - `https://myorg.maps.arcgis.com/sharing/rest/content/users/casey/items/${ - FeatureServiceResponse.serviceItemId - }/move` + `https://myorg.maps.arcgis.com/sharing/rest/content/users/casey/items/${FeatureServiceResponse.serviceItemId}/move` ); expect(optionsMove.method).toBe("POST"); expect(optionsMove.body).toContain("folder=" + folderId);