From 80aa6dcfa1b0297df4f71f191c32ce91c5e62d64 Mon Sep 17 00:00:00 2001 From: mjuniper Date: Mon, 8 Oct 2018 12:53:06 -0600 Subject: [PATCH] feat(users): add user invitation functions AFFECTS PACKAGES: @esri/arcgis-rest-common-types @esri/arcgis-rest-users --- .../arcgis-rest-common-types/src/group.ts | 2 +- packages/arcgis-rest-users/src/index.ts | 1 + packages/arcgis-rest-users/src/invitation.ts | 102 +++++++++++ .../arcgis-rest-users/test/invitation.test.ts | 169 ++++++++++++++++++ .../arcgis-rest-users/test/mocks/responses.ts | 70 ++++++++ 5 files changed, 343 insertions(+), 1 deletion(-) create mode 100644 packages/arcgis-rest-users/src/invitation.ts create mode 100644 packages/arcgis-rest-users/test/invitation.test.ts diff --git a/packages/arcgis-rest-common-types/src/group.ts b/packages/arcgis-rest-common-types/src/group.ts index 22df6750f8..8eeedf2af4 100644 --- a/packages/arcgis-rest-common-types/src/group.ts +++ b/packages/arcgis-rest-common-types/src/group.ts @@ -42,7 +42,7 @@ export interface IGroup extends IGroupAdd { protected: boolean; isInvitationOnly: boolean; isViewOnly: boolean; - isOpenData: boolean; + isOpenData?: boolean; isFav: boolean; autoJoin: boolean; userMembership?: { diff --git a/packages/arcgis-rest-users/src/index.ts b/packages/arcgis-rest-users/src/index.ts index 2b19f5656f..be959a8cb5 100644 --- a/packages/arcgis-rest-users/src/index.ts +++ b/packages/arcgis-rest-users/src/index.ts @@ -3,3 +3,4 @@ export * from "./get"; export * from "./notification"; +export * from "./invitation"; diff --git a/packages/arcgis-rest-users/src/invitation.ts b/packages/arcgis-rest-users/src/invitation.ts new file mode 100644 index 0000000000..7fbb33770e --- /dev/null +++ b/packages/arcgis-rest-users/src/invitation.ts @@ -0,0 +1,102 @@ +/* Copyright (c) 2018 Environmental Systems Research Institute, Inc. + * Apache-2.0 */ + +import { request, getPortalUrl } from "@esri/arcgis-rest-request"; + +import { IUserRequestOptions } from "@esri/arcgis-rest-auth"; +import { IGroup } from "@esri/arcgis-rest-common-types"; + +export interface IInvitation { + id: string; + targetType: string; + targetId: string; + received: number; + accepted: boolean; + mustApprove: boolean; + email: string; + role: string; + type: string; + dateAccepted: number; + expiration: number; + created: number; + username: string; + fromUsername: { + username: string; + fullname?: string; + }; + group?: IGroup; + groupId?: string; +} + +export interface IInvitationResult { + userInvitations: IInvitation[]; +} + +/** + * Get invitations for a user. + * + * @param requestOptions - options to pass through in the request + * @returns A Promise that will resolve with the user's invitations + */ +export function getUserInvitations( + requestOptions: IUserRequestOptions +): Promise { + let options = { httpMethod: "GET" } as IUserRequestOptions; + + const username = encodeURIComponent(requestOptions.authentication.username); + const portalUrl = getPortalUrl(requestOptions); + const url = `${portalUrl}/community/users/${username}/invitations`; + options = { ...requestOptions, ...options }; + + // send the request + return request(url, options); +} + +/** + * Get an invitation for a user by id. + * + * @param requestOptions - options to pass through in the request + * @returns A Promise that will resolve with the invitation + */ +export function getUserInvitation( + id: string, + requestOptions: IUserRequestOptions +): Promise { + let options = { httpMethod: "GET" } as IUserRequestOptions; + + const username = encodeURIComponent(requestOptions.authentication.username); + const portalUrl = getPortalUrl(requestOptions); + const url = `${portalUrl}/community/users/${username}/invitations/${id}`; + options = { ...requestOptions, ...options }; + + // send the request + return request(url, options); +} + +/** + * Accept an invitation. + * + * @param requestOptions - Options for the request + * @returns A Promise that will resolve with the success/failure status of the request + */ +export function acceptInvitation( + id: string, + requestOptions: IUserRequestOptions +): Promise { + const username = encodeURIComponent(requestOptions.authentication.username); + const portalUrl = getPortalUrl(requestOptions); + const url = `${portalUrl}/community/users/${username}/invitations/${id}/accept`; + + return request(url, requestOptions); +} + +export function declineInvitation( + id: string, + requestOptions: IUserRequestOptions +): Promise { + const username = encodeURIComponent(requestOptions.authentication.username); + const portalUrl = getPortalUrl(requestOptions); + const url = `${portalUrl}/community/users/${username}/invitations/${id}/decline`; + + return request(url, requestOptions); +} diff --git a/packages/arcgis-rest-users/test/invitation.test.ts b/packages/arcgis-rest-users/test/invitation.test.ts new file mode 100644 index 0000000000..a5286cc01e --- /dev/null +++ b/packages/arcgis-rest-users/test/invitation.test.ts @@ -0,0 +1,169 @@ +/* Copyright (c) 2018 Environmental Systems Research Institute, Inc. + * Apache-2.0 */ + +import { + getUserInvitations, + getUserInvitation, + acceptInvitation, + declineInvitation +} from "../src/index"; + +import { + UserInvitationsResponse, + UserInvitationResponse +} from "./mocks/responses"; + +import { encodeParam } from "@esri/arcgis-rest-request"; +import { UserSession } from "@esri/arcgis-rest-auth"; +import * as fetchMock from "fetch-mock"; + +const TOMORROW = (function() { + const now = new Date(); + now.setDate(now.getDate() + 1); + return now; +})(); + +describe("invitations", () => { + afterEach(fetchMock.restore); + + const session = new UserSession({ + username: "c@sey", + password: "123456", + portal: "https://myorg.maps.arcgis.com/sharing/rest" + }); + + fetchMock.postOnce( + "https://myorg.maps.arcgis.com/sharing/rest/generateToken", + { + token: "fake-token", + expires: TOMORROW.getTime(), + username: "c@sey" + } + ); + + describe("getUserInvitations", () => { + session.refreshSession(); + + it("should make an authenticated request for user invitations", done => { + fetchMock.once("*", UserInvitationsResponse); + + getUserInvitations({ authentication: session }) + .then(response => { + expect(fetchMock.called()).toEqual(true); + const [url, options]: [string, RequestInit] = fetchMock.lastCall("*"); + expect(url).toEqual( + "https://myorg.maps.arcgis.com/sharing/rest/community/users/c%40sey/invitations?f=json&token=fake-token" + ); + expect(options.method).toBe("GET"); + expect(response.userInvitations.length).toEqual(1); + done(); + }) + .catch(e => { + fail(e); + }); + }); + }); + + describe("getUserInvitation", () => { + fetchMock.postOnce( + "https://myorg.maps.arcgis.com/sharing/rest/generateToken", + { + token: "fake-token", + expires: TOMORROW.getTime(), + username: "c@sey" + } + ); + + session.refreshSession(); + + it("should make an authenticated request for a user invitation", done => { + fetchMock.once("*", UserInvitationResponse); + + getUserInvitation("3ef", { authentication: session }) + .then(response => { + expect(fetchMock.called()).toEqual(true); + const [url, options]: [string, RequestInit] = fetchMock.lastCall("*"); + expect(url).toEqual( + "https://myorg.maps.arcgis.com/sharing/rest/community/users/c%40sey/invitations/3ef?f=json&token=fake-token" + ); + expect(options.method).toBe("GET"); + expect(response.id).toEqual("G45ad52e7560e470598815499003c13f6"); + expect(response.id).toEqual("G45ad52e7560e470598815499003c13f6"); + done(); + }) + .catch(e => { + fail(e); + }); + }); + }); + + describe("acceptInvitation", () => { + fetchMock.postOnce( + "https://myorg.maps.arcgis.com/sharing/rest/generateToken", + { + token: "fake-token", + expires: TOMORROW.getTime(), + username: "c@sey" + } + ); + + session.refreshSession(); + + it("should accept an invitation", done => { + fetchMock.once("*", { success: true }); + + acceptInvitation("3ef", { authentication: session }) + .then(response => { + expect(fetchMock.called()).toEqual(true); + const [url, options]: [string, RequestInit] = fetchMock.lastCall("*"); + expect(url).toEqual( + "https://myorg.maps.arcgis.com/sharing/rest/community/users/c%40sey/invitations/3ef/accept" + ); + expect(options.method).toBe("POST"); + expect(options.body).toContain(encodeParam("f", "json")); + expect(options.body).toContain(encodeParam("token", "fake-token")); + expect(response.success).toEqual(true); + // expect(response.notificationId).toBe("3ef"); + done(); + }) + .catch(e => { + fail(e); + }); + }); + }); + + describe("declineInvitation", () => { + fetchMock.postOnce( + "https://myorg.maps.arcgis.com/sharing/rest/generateToken", + { + token: "fake-token", + expires: TOMORROW.getTime(), + username: "c@sey" + } + ); + + session.refreshSession(); + + it("should decline an invitation", done => { + fetchMock.once("*", { success: true }); + + declineInvitation("3ef", { authentication: session }) + .then(response => { + expect(fetchMock.called()).toEqual(true); + const [url, options]: [string, RequestInit] = fetchMock.lastCall("*"); + expect(url).toEqual( + "https://myorg.maps.arcgis.com/sharing/rest/community/users/c%40sey/invitations/3ef/decline" + ); + expect(options.method).toBe("POST"); + expect(options.body).toContain(encodeParam("f", "json")); + expect(options.body).toContain(encodeParam("token", "fake-token")); + expect(response.success).toEqual(true); + // expect(response.notificationId).toBe("3ef"); + done(); + }) + .catch(e => { + fail(e); + }); + }); + }); +}); diff --git a/packages/arcgis-rest-users/test/mocks/responses.ts b/packages/arcgis-rest-users/test/mocks/responses.ts index 759e2d6bc3..cb02511047 100644 --- a/packages/arcgis-rest-users/test/mocks/responses.ts +++ b/packages/arcgis-rest-users/test/mocks/responses.ts @@ -3,6 +3,7 @@ import { IUser } from "@esri/arcgis-rest-common-types"; import { INotificationResult } from "../../src/notification"; +import { IInvitation, IInvitationResult } from "../../src/invitation"; export const AnonUserResponse: IUser = { username: "jsmith", @@ -205,3 +206,72 @@ export const IDeleteSuccessResponse: any = { success: true, notificationId: "3ef" }; + +export const UserInvitationsResponse: IInvitationResult = { + userInvitations: [ + { + id: "G45ad52e7560e470598815499003c13f6", + targetType: "group", + targetId: "5d780fcf924e4e7ab1952a71472bc950", + received: 1538516323000, + accepted: false, + mustApprove: false, + email: null, + role: "group_member", + type: "user", + dateAccepted: -1, + expiration: 1539725923000, + created: 1538516323000, + username: "mjuniper_dcqa", + fromUsername: { + username: "dcadminqa" + }, + groupId: "5d780fcf924e4e7ab1952a71472bc950" + } + ] +}; + +export const UserInvitationResponse: IInvitation = { + id: "G45ad52e7560e470598815499003c13f6", + targetType: "group", + targetId: "5d780fcf924e4e7ab1952a71472bc950", + received: 1538516323000, + accepted: false, + mustApprove: false, + email: null, + role: "group_member", + type: "user", + dateAccepted: -1, + expiration: 1539725923000, + created: 1538516323000, + username: "mjuniper_dcqa", + fromUsername: { + username: "dcadminqa", + fullname: "DcAdminQA QaExtDc" + }, + group: { + id: "5d780fcf924e4e7ab1952a71472bc950", + title: "Jupe test group 2", + isInvitationOnly: true, + owner: "dcadminqa", + description: null, + snippet: null, + tags: ["hub"], + phone: null, + sortField: "title", + sortOrder: "asc", + isViewOnly: false, + thumbnail: null, + created: 1538516296000, + modified: 1538516296000, + access: "public", + capabilities: [], + isFav: false, + isReadOnly: false, + protected: false, + autoJoin: false, + notificationsEnabled: false, + provider: null, + providerGroupName: null + } +};