From 91f5694a025a26e9b7878fc4c78a22c7f2f1dcef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Karol=20S=C3=B3jko?= Date: Wed, 31 Aug 2022 19:43:43 +0200 Subject: [PATCH] feat(api): add client methods for listing and canceling subscription invites (#1471) * feat(api): add subscription server and client services and interfaces * feat(api): add subscriptions invitation operations on server side * fix(api): linter issues * feat(api): add client methods for listing and canceling subscription invites * fix(api): imports --- .../Subscription/SubscriptionApiOperations.ts | 5 + .../SubscriptionApiService.spec.ts | 104 +++++++++++++++++- .../Subscription/SubscriptionApiService.ts | 60 ++++++++-- .../SubscriptionApiServiceInterface.ts | 6 + packages/api/src/Domain/Client/index.ts | 1 + packages/api/src/Domain/Error/ErrorMessage.ts | 2 +- .../Subscription/SubscriptionServer.spec.ts | 1 + 7 files changed, 169 insertions(+), 10 deletions(-) create mode 100644 packages/api/src/Domain/Client/Subscription/SubscriptionApiOperations.ts diff --git a/packages/api/src/Domain/Client/Subscription/SubscriptionApiOperations.ts b/packages/api/src/Domain/Client/Subscription/SubscriptionApiOperations.ts new file mode 100644 index 000000000..81c3c5cb6 --- /dev/null +++ b/packages/api/src/Domain/Client/Subscription/SubscriptionApiOperations.ts @@ -0,0 +1,5 @@ +export enum SubscriptionApiOperations { + Inviting, + CancelingInvite, + ListingInvites, +} diff --git a/packages/api/src/Domain/Client/Subscription/SubscriptionApiService.spec.ts b/packages/api/src/Domain/Client/Subscription/SubscriptionApiService.spec.ts index 1467f92de..82a718bef 100644 --- a/packages/api/src/Domain/Client/Subscription/SubscriptionApiService.spec.ts +++ b/packages/api/src/Domain/Client/Subscription/SubscriptionApiService.spec.ts @@ -1,6 +1,11 @@ +import { Invitation } from '@standardnotes/models' + +import { SubscriptionInviteCancelResponse } from '../../Response/Subscription/SubscriptionInviteCancelResponse' +import { SubscriptionInviteListResponse } from '../../Response/Subscription/SubscriptionInviteListResponse' import { SubscriptionInviteResponse } from '../../Response/Subscription/SubscriptionInviteResponse' import { SubscriptionServerInterface } from '../../Server/Subscription/SubscriptionServerInterface' +import { SubscriptionApiOperations } from './SubscriptionApiOperations' import { SubscriptionApiService } from './SubscriptionApiService' describe('SubscriptionApiService', () => { @@ -13,6 +18,12 @@ describe('SubscriptionApiService', () => { subscriptionServer.invite = jest.fn().mockReturnValue({ data: { success: true, sharedSubscriptionInvitationUuid: '1-2-3' }, } as jest.Mocked) + subscriptionServer.cancelInvite = jest.fn().mockReturnValue({ + data: { success: true }, + } as jest.Mocked) + subscriptionServer.listInvites = jest.fn().mockReturnValue({ + data: { invitations: [{} as jest.Mocked] }, + } as jest.Mocked) }) it('should invite a user', async () => { @@ -32,8 +43,8 @@ describe('SubscriptionApiService', () => { it('should not invite a user if it is already inviting', async () => { const service = createService() - Object.defineProperty(service, 'inviting', { - get: () => true, + Object.defineProperty(service, 'operationsInProgress', { + get: () => new Map([[SubscriptionApiOperations.Inviting, true]]), }) let error = null @@ -60,4 +71,93 @@ describe('SubscriptionApiService', () => { expect(error).not.toBeNull() }) + + it('should cancel an invite', async () => { + const response = await createService().cancelInvite('1-2-3') + + expect(response).toEqual({ + data: { + success: true, + }, + }) + expect(subscriptionServer.cancelInvite).toHaveBeenCalledWith({ + api: '20200115', + inviteUuid: '1-2-3', + }) + }) + + it('should not cancel an invite if it is already canceling', async () => { + const service = createService() + Object.defineProperty(service, 'operationsInProgress', { + get: () => new Map([[SubscriptionApiOperations.CancelingInvite, true]]), + }) + + let error = null + try { + await service.cancelInvite('1-2-3') + } catch (caughtError) { + error = caughtError + } + + expect(error).not.toBeNull() + }) + + it('should not cancel an invite if the server fails', async () => { + subscriptionServer.cancelInvite = jest.fn().mockImplementation(() => { + throw new Error('Oops') + }) + + let error = null + try { + await createService().cancelInvite('1-2-3') + } catch (caughtError) { + error = caughtError + } + + expect(error).not.toBeNull() + }) + + it('should list invites', async () => { + const response = await createService().listInvites() + + expect(response).toEqual({ + data: { + invitations: [{} as jest.Mocked], + }, + }) + expect(subscriptionServer.listInvites).toHaveBeenCalledWith({ + api: '20200115', + }) + }) + + it('should not list invitations if it is already listing', async () => { + const service = createService() + Object.defineProperty(service, 'operationsInProgress', { + get: () => new Map([[SubscriptionApiOperations.ListingInvites, true]]), + }) + + let error = null + try { + await service.listInvites() + } catch (caughtError) { + error = caughtError + } + + expect(error).not.toBeNull() + }) + + it('should not list invites if the server fails', async () => { + subscriptionServer.listInvites = jest.fn().mockImplementation(() => { + throw new Error('Oops') + }) + + let error = null + try { + await createService().listInvites() + } catch (caughtError) { + error = caughtError + } + + expect(error).not.toBeNull() + }) }) diff --git a/packages/api/src/Domain/Client/Subscription/SubscriptionApiService.ts b/packages/api/src/Domain/Client/Subscription/SubscriptionApiService.ts index d8abf0152..8e48970f7 100644 --- a/packages/api/src/Domain/Client/Subscription/SubscriptionApiService.ts +++ b/packages/api/src/Domain/Client/Subscription/SubscriptionApiService.ts @@ -2,22 +2,68 @@ import { ErrorMessage } from '../../Error/ErrorMessage' import { ApiCallError } from '../../Error/ApiCallError' import { ApiVersion } from '../../Api/ApiVersion' import { ApiEndpointParam } from '../../Request/ApiEndpointParam' -import { SubscriptionApiServiceInterface } from './SubscriptionApiServiceInterface' import { SubscriptionServerInterface } from '../../Server/Subscription/SubscriptionServerInterface' import { SubscriptionInviteResponse } from '../../Response/Subscription/SubscriptionInviteResponse' +import { SubscriptionInviteListResponse } from '../../Response/Subscription/SubscriptionInviteListResponse' +import { SubscriptionInviteCancelResponse } from '../../Response/Subscription/SubscriptionInviteCancelResponse' + +import { SubscriptionApiServiceInterface } from './SubscriptionApiServiceInterface' +import { SubscriptionApiOperations } from './SubscriptionApiOperations' export class SubscriptionApiService implements SubscriptionApiServiceInterface { - private inviting: boolean + private operationsInProgress: Map constructor(private subscriptionServer: SubscriptionServerInterface) { - this.inviting = false + this.operationsInProgress = new Map() + } + + async listInvites(): Promise { + if (this.operationsInProgress.get(SubscriptionApiOperations.ListingInvites)) { + throw new ApiCallError(ErrorMessage.GenericInProgress) + } + + this.operationsInProgress.set(SubscriptionApiOperations.ListingInvites, true) + + try { + const response = await this.subscriptionServer.listInvites({ + [ApiEndpointParam.ApiVersion]: ApiVersion.v0, + }) + + this.operationsInProgress.set(SubscriptionApiOperations.ListingInvites, false) + + return response + } catch (error) { + throw new ApiCallError(ErrorMessage.GenericFail) + } + } + + async cancelInvite(inviteUuid: string): Promise { + if (this.operationsInProgress.get(SubscriptionApiOperations.CancelingInvite)) { + throw new ApiCallError(ErrorMessage.GenericInProgress) + } + + this.operationsInProgress.set(SubscriptionApiOperations.CancelingInvite, true) + + try { + const response = await this.subscriptionServer.cancelInvite({ + [ApiEndpointParam.ApiVersion]: ApiVersion.v0, + inviteUuid, + }) + + this.operationsInProgress.set(SubscriptionApiOperations.CancelingInvite, false) + + return response + } catch (error) { + throw new ApiCallError(ErrorMessage.GenericFail) + } } async invite(inviteeEmail: string): Promise { - if (this.inviting) { - throw new ApiCallError(ErrorMessage.InvitingInProgress) + if (this.operationsInProgress.get(SubscriptionApiOperations.Inviting)) { + throw new ApiCallError(ErrorMessage.GenericInProgress) } - this.inviting = true + + this.operationsInProgress.set(SubscriptionApiOperations.Inviting, true) try { const response = await this.subscriptionServer.invite({ @@ -25,7 +71,7 @@ export class SubscriptionApiService implements SubscriptionApiServiceInterface { identifier: inviteeEmail, }) - this.inviting = false + this.operationsInProgress.set(SubscriptionApiOperations.Inviting, false) return response } catch (error) { diff --git a/packages/api/src/Domain/Client/Subscription/SubscriptionApiServiceInterface.ts b/packages/api/src/Domain/Client/Subscription/SubscriptionApiServiceInterface.ts index 32ba7b76b..e65c2a02f 100644 --- a/packages/api/src/Domain/Client/Subscription/SubscriptionApiServiceInterface.ts +++ b/packages/api/src/Domain/Client/Subscription/SubscriptionApiServiceInterface.ts @@ -1,5 +1,11 @@ +import { Uuid } from '@standardnotes/common' + +import { SubscriptionInviteCancelResponse } from '../../Response/Subscription/SubscriptionInviteCancelResponse' +import { SubscriptionInviteListResponse } from '../../Response/Subscription/SubscriptionInviteListResponse' import { SubscriptionInviteResponse } from '../../Response/Subscription/SubscriptionInviteResponse' export interface SubscriptionApiServiceInterface { invite(inviteeEmail: string): Promise + listInvites(): Promise + cancelInvite(inviteUuid: Uuid): Promise } diff --git a/packages/api/src/Domain/Client/index.ts b/packages/api/src/Domain/Client/index.ts index f79730600..beb912853 100644 --- a/packages/api/src/Domain/Client/index.ts +++ b/packages/api/src/Domain/Client/index.ts @@ -1,3 +1,4 @@ +export * from './Subscription/SubscriptionApiOperations' export * from './Subscription/SubscriptionApiService' export * from './Subscription/SubscriptionApiServiceInterface' export * from './User/UserApiService' diff --git a/packages/api/src/Domain/Error/ErrorMessage.ts b/packages/api/src/Domain/Error/ErrorMessage.ts index c11fcfbd9..ab35cfcc8 100644 --- a/packages/api/src/Domain/Error/ErrorMessage.ts +++ b/packages/api/src/Domain/Error/ErrorMessage.ts @@ -1,9 +1,9 @@ export enum ErrorMessage { - InvitingInProgress = 'An existing invitation request is already in progress.', RegistrationInProgress = 'An existing registration request is already in progress.', GenericRegistrationFail = 'A server error occurred while trying to register. Please try again.', RateLimited = 'Too many successive server requests. Please wait a few minutes and try again.', InsufficientPasswordMessage = 'Your password must be at least %LENGTH% characters in length. For your security, please choose a longer password or, ideally, a passphrase, and try again.', PasscodeRequired = 'Your passcode is required in order to register for an account.', + GenericInProgress = 'An existing request is already in progress.', GenericFail = 'A server error occurred. Please try again.', } diff --git a/packages/api/src/Domain/Server/Subscription/SubscriptionServer.spec.ts b/packages/api/src/Domain/Server/Subscription/SubscriptionServer.spec.ts index 8e7c446e3..8f235b7cc 100644 --- a/packages/api/src/Domain/Server/Subscription/SubscriptionServer.spec.ts +++ b/packages/api/src/Domain/Server/Subscription/SubscriptionServer.spec.ts @@ -7,6 +7,7 @@ import { SubscriptionInviteAcceptResponse } from '../../Response/Subscription/Su import { SubscriptionInviteCancelResponse } from '../../Response/Subscription/SubscriptionInviteCancelResponse' import { SubscriptionInviteDeclineResponse } from '../../Response/Subscription/SubscriptionInviteDeclineResponse' import { SubscriptionInviteListResponse } from '../../Response/Subscription/SubscriptionInviteListResponse' + import { SubscriptionServer } from './SubscriptionServer' describe('SubscriptionServer', () => {