feat: add subscription manager to handle subscription sharing (#1517)
* feat: add subscription manager to handle subscription sharing * fix(services): add missing methods to the interface * fix(services): add subscription manager specs * feat(snjs): add subscriptions e2e tests * fix(snjs): add wait in subscription cancelling test * fix(snjs): checking for canceled invitations in tests * fix(snjs): add e2e test for restored limit of subscription invitations * chore(lint): fix linter issues
This commit is contained in:
@@ -1,13 +1,13 @@
|
||||
import { ApplicationIdentifier, ContentType } from '@standardnotes/common'
|
||||
import { BackupFile, DecryptedItemInterface, ItemStream, PrefKey, PrefValue } from '@standardnotes/models'
|
||||
import { BackupFile, DecryptedItemInterface, ItemStream, Platform, PrefKey, PrefValue } from '@standardnotes/models'
|
||||
import { FilesClientInterface } from '@standardnotes/files'
|
||||
import { AlertService } from '../Alert/AlertService'
|
||||
|
||||
import { ComponentManagerInterface } from '../Component/ComponentManagerInterface'
|
||||
import { Platform } from '../Device/Environments'
|
||||
import { ApplicationEvent } from '../Event/ApplicationEvent'
|
||||
import { ApplicationEventCallback } from '../Event/ApplicationEventCallback'
|
||||
import { FeaturesClientInterface } from '../Feature/FeaturesClientInterface'
|
||||
import { SubscriptionClientInterface } from '../Subscription/SubscriptionClientInterface'
|
||||
import { ItemsClientInterface } from '../Item/ItemsClientInterface'
|
||||
import { MutatorClientInterface } from '../Mutator/MutatorClientInterface'
|
||||
import { StorageValueModes } from '../Storage/StorageTypes'
|
||||
@@ -48,6 +48,7 @@ export interface ApplicationInterface {
|
||||
get mutator(): MutatorClientInterface
|
||||
get user(): UserClientInterface
|
||||
get files(): FilesClientInterface
|
||||
get subscriptions(): SubscriptionClientInterface
|
||||
readonly identifier: ApplicationIdentifier
|
||||
readonly platform: Platform
|
||||
deviceInterface: DeviceInterface
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Environment } from '@standardnotes/models'
|
||||
|
||||
import { WebClientRequiresDesktopMethods } from './DesktopWebCommunication'
|
||||
import { DeviceInterface } from './DeviceInterface'
|
||||
import { Environment } from './Environments'
|
||||
import { WebOrDesktopDeviceInterface } from './WebOrDesktopDeviceInterface'
|
||||
|
||||
/* istanbul ignore file */
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { Environment } from './Environments'
|
||||
import { ApplicationIdentifier } from '@standardnotes/common'
|
||||
import {
|
||||
FullyFormedTransferPayload,
|
||||
TransferPayload,
|
||||
LegacyRawKeychainValue,
|
||||
NamespacedRootKeyInKeychain,
|
||||
Environment,
|
||||
} from '@standardnotes/models'
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
export enum Environment {
|
||||
Web = 1,
|
||||
Desktop = 2,
|
||||
Mobile = 3,
|
||||
NativeMobileWeb = 4,
|
||||
}
|
||||
|
||||
export enum Platform {
|
||||
Ios = 1,
|
||||
Android = 2,
|
||||
MacWeb = 3,
|
||||
MacDesktop = 4,
|
||||
WindowsWeb = 5,
|
||||
WindowsDesktop = 6,
|
||||
LinuxWeb = 7,
|
||||
LinuxDesktop = 8,
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
import { DeviceInterface } from './DeviceInterface'
|
||||
import { Environment } from './Environments'
|
||||
import { RawKeychainValue } from '@standardnotes/models'
|
||||
import { Environment, RawKeychainValue } from '@standardnotes/models'
|
||||
|
||||
export interface MobileDeviceInterface extends DeviceInterface {
|
||||
environment: Environment.Mobile
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Environment } from '@standardnotes/models'
|
||||
|
||||
import { DeviceInterface } from './DeviceInterface'
|
||||
import { Environment } from './Environments'
|
||||
import { MobileDeviceInterface } from './MobileDeviceInterface'
|
||||
import { isMobileDevice } from './TypeCheck'
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Environment } from './Environments'
|
||||
import { MobileDeviceInterface } from './MobileDeviceInterface'
|
||||
import { DeviceInterface } from './DeviceInterface'
|
||||
import { Environment } from '@standardnotes/models'
|
||||
|
||||
/* istanbul ignore file */
|
||||
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import { Uuid } from '@standardnotes/common'
|
||||
import { Invitation } from '@standardnotes/models'
|
||||
|
||||
export interface SubscriptionClientInterface {
|
||||
listSubscriptionInvitations(): Promise<Invitation[]>
|
||||
inviteToSubscription(inviteeEmail: string): Promise<boolean>
|
||||
cancelInvitation(inviteUuid: Uuid): Promise<boolean>
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
import { SubscriptionApiServiceInterface } from '@standardnotes/api'
|
||||
import { Invitation } from '@standardnotes/models'
|
||||
import { InternalEventBusInterface } from '..'
|
||||
import { SubscriptionManager } from './SubscriptionManager'
|
||||
|
||||
describe('SubscriptionManager', () => {
|
||||
let subscriptionApiService: SubscriptionApiServiceInterface
|
||||
let internalEventBus: InternalEventBusInterface
|
||||
|
||||
const createManager = () => new SubscriptionManager(subscriptionApiService, internalEventBus)
|
||||
|
||||
beforeEach(() => {
|
||||
subscriptionApiService = {} as jest.Mocked<SubscriptionApiServiceInterface>
|
||||
subscriptionApiService.cancelInvite = jest.fn()
|
||||
subscriptionApiService.invite = jest.fn()
|
||||
subscriptionApiService.listInvites = jest.fn()
|
||||
|
||||
internalEventBus = {} as jest.Mocked<InternalEventBusInterface>
|
||||
})
|
||||
|
||||
it('should invite user by email to a shared subscription', async () => {
|
||||
subscriptionApiService.invite = jest.fn().mockReturnValue({ data: { success: true } })
|
||||
|
||||
expect(await createManager().inviteToSubscription('test@test.te')).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should not invite user by email if the api fails to do so', async () => {
|
||||
subscriptionApiService.invite = jest.fn().mockReturnValue({ data: { error: 'foobar' } })
|
||||
|
||||
expect(await createManager().inviteToSubscription('test@test.te')).toBeFalsy()
|
||||
})
|
||||
|
||||
it('should not invite user by email if the api throws an error', async () => {
|
||||
subscriptionApiService.invite = jest.fn().mockImplementation(() => {
|
||||
throw new Error('Oops')
|
||||
})
|
||||
|
||||
expect(await createManager().inviteToSubscription('test@test.te')).toBeFalsy()
|
||||
})
|
||||
|
||||
it('should cancel invite to a shared subscription', async () => {
|
||||
subscriptionApiService.cancelInvite = jest.fn().mockReturnValue({ data: { success: true } })
|
||||
|
||||
expect(await createManager().cancelInvitation('1-2-3')).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should not cancel invite if the api fails to do so', async () => {
|
||||
subscriptionApiService.cancelInvite = jest.fn().mockReturnValue({ data: { error: 'foobar' } })
|
||||
|
||||
expect(await createManager().cancelInvitation('1-2-3')).toBeFalsy()
|
||||
})
|
||||
|
||||
it('should not cancel invite if the api throws an error', async () => {
|
||||
subscriptionApiService.cancelInvite = jest.fn().mockImplementation(() => {
|
||||
throw new Error('Oops')
|
||||
})
|
||||
|
||||
expect(await createManager().cancelInvitation('1-2-3')).toBeFalsy()
|
||||
})
|
||||
|
||||
it('should list invites to a shared subscription', async () => {
|
||||
const invitation = {
|
||||
uuid: '1-2-3',
|
||||
} as jest.Mocked<Invitation>
|
||||
subscriptionApiService.listInvites = jest.fn().mockReturnValue({ data: { invitations: [invitation] } })
|
||||
|
||||
expect(await createManager().listSubscriptionInvitations()).toEqual([invitation])
|
||||
})
|
||||
|
||||
it('should return an empty list of invites if the api fails to fetch them', async () => {
|
||||
subscriptionApiService.listInvites = jest.fn().mockReturnValue({ data: { error: 'foobar' } })
|
||||
|
||||
expect(await createManager().listSubscriptionInvitations()).toEqual([])
|
||||
})
|
||||
|
||||
it('should return an empty list of invites if the api throws an error', async () => {
|
||||
subscriptionApiService.listInvites = jest.fn().mockImplementation(() => {
|
||||
throw new Error('Oops')
|
||||
})
|
||||
|
||||
expect(await createManager().listSubscriptionInvitations()).toEqual([])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,45 @@
|
||||
import { Invitation } from '@standardnotes/models'
|
||||
import { SubscriptionApiServiceInterface } from '@standardnotes/api'
|
||||
import { InternalEventBusInterface } from '../Internal/InternalEventBusInterface'
|
||||
import { AbstractService } from '../Service/AbstractService'
|
||||
import { SubscriptionClientInterface } from './SubscriptionClientInterface'
|
||||
import { Uuid } from '@standardnotes/common'
|
||||
|
||||
export class SubscriptionManager extends AbstractService implements SubscriptionClientInterface {
|
||||
constructor(
|
||||
private subscriptionApiService: SubscriptionApiServiceInterface,
|
||||
protected override internalEventBus: InternalEventBusInterface,
|
||||
) {
|
||||
super(internalEventBus)
|
||||
}
|
||||
|
||||
async listSubscriptionInvitations(): Promise<Invitation[]> {
|
||||
try {
|
||||
const response = await this.subscriptionApiService.listInvites()
|
||||
|
||||
return response.data.invitations ?? []
|
||||
} catch (error) {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
async inviteToSubscription(inviteeEmail: string): Promise<boolean> {
|
||||
try {
|
||||
const result = await this.subscriptionApiService.invite(inviteeEmail)
|
||||
|
||||
return result.data.success === true
|
||||
} catch (error) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
async cancelInvitation(inviteUuid: Uuid): Promise<boolean> {
|
||||
try {
|
||||
const result = await this.subscriptionApiService.cancelInvite(inviteUuid)
|
||||
|
||||
return result.data.success === true
|
||||
} catch (error) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,6 @@ export * from './Device/DesktopDeviceInterface'
|
||||
export * from './Device/DesktopManagerInterface'
|
||||
export * from './Device/DesktopWebCommunication'
|
||||
export * from './Device/DeviceInterface'
|
||||
export * from './Device/Environments'
|
||||
export * from './Device/MobileDeviceInterface'
|
||||
export * from './Device/TypeCheck'
|
||||
export * from './Device/WebOrDesktopDeviceInterface'
|
||||
@@ -67,6 +66,8 @@ export * from './Storage/InMemoryStore'
|
||||
export * from './Storage/KeyValueStoreInterface'
|
||||
export * from './Storage/StorageServiceInterface'
|
||||
export * from './Storage/StorageTypes'
|
||||
export * from './Subscription/SubscriptionClientInterface'
|
||||
export * from './Subscription/SubscriptionManager'
|
||||
export * from './Sync/SyncMode'
|
||||
export * from './Sync/SyncOptions'
|
||||
export * from './Sync/SyncQueueStrategy'
|
||||
|
||||
Reference in New Issue
Block a user