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:
Karol Sójko
2022-09-13 10:28:11 +02:00
committed by GitHub
parent 2b830c0fae
commit 55b1409a80
55 changed files with 512 additions and 91 deletions

View File

@@ -35,12 +35,11 @@
"ts-jest": "^28.0.5"
},
"dependencies": {
"@standardnotes/common": "^1.23.1",
"@standardnotes/common": "^1.32.0",
"@standardnotes/encryption": "workspace:*",
"@standardnotes/models": "workspace:*",
"@standardnotes/responses": "workspace:*",
"@standardnotes/security": "^1.1.0",
"@standardnotes/services": "workspace:*",
"@standardnotes/utils": "workspace:*",
"reflect-metadata": "^0.1.13"
}

View File

@@ -1,4 +1,5 @@
import { Environment } from '@standardnotes/services'
import { Environment } from '@standardnotes/models'
import { HttpResponseMeta } from './HttpResponseMeta'
import { HttpService } from './HttpService'
@@ -24,4 +25,14 @@ describe('HttpService', () => {
expect(service['host']).toEqual('http://foo')
})
it('should set and use the authorization token', () => {
const service = createService()
expect(service['authorizationToken']).toBeUndefined()
service.setAuthorizationToken('foo-bar')
expect(service['authorizationToken']).toEqual('foo-bar')
})
})

View File

@@ -1,5 +1,5 @@
import { isString, joinPaths } from '@standardnotes/utils'
import { Environment } from '@standardnotes/services'
import { Environment } from '@standardnotes/models'
import { HttpRequestParams } from './HttpRequestParams'
import { HttpVerb } from './HttpVerb'
import { HttpRequest } from './HttpRequest'
@@ -12,6 +12,8 @@ import { HttpResponseMeta } from './HttpResponseMeta'
import { HttpErrorResponseBody } from './HttpErrorResponseBody'
export class HttpService implements HttpServiceInterface {
private authorizationToken?: string
constructor(
private environment: Environment,
private appVersion: string,
@@ -20,28 +22,57 @@ export class HttpService implements HttpServiceInterface {
private updateMetaCallback: (meta: HttpResponseMeta) => void,
) {}
setAuthorizationToken(authorizationToken: string): void {
this.authorizationToken = authorizationToken
}
setHost(host: string): void {
this.host = host
}
async get(path: string, params?: HttpRequestParams, authentication?: string): Promise<HttpResponse> {
return this.runHttp({ url: joinPaths(this.host, path), params, verb: HttpVerb.Get, authentication })
return this.runHttp({
url: joinPaths(this.host, path),
params,
verb: HttpVerb.Get,
authentication: authentication ?? this.authorizationToken,
})
}
async post(path: string, params?: HttpRequestParams, authentication?: string): Promise<HttpResponse> {
return this.runHttp({ url: joinPaths(this.host, path), params, verb: HttpVerb.Post, authentication })
return this.runHttp({
url: joinPaths(this.host, path),
params,
verb: HttpVerb.Post,
authentication: authentication ?? this.authorizationToken,
})
}
async put(path: string, params?: HttpRequestParams, authentication?: string): Promise<HttpResponse> {
return this.runHttp({ url: joinPaths(this.host, path), params, verb: HttpVerb.Put, authentication })
return this.runHttp({
url: joinPaths(this.host, path),
params,
verb: HttpVerb.Put,
authentication: authentication ?? this.authorizationToken,
})
}
async patch(path: string, params: HttpRequestParams, authentication?: string): Promise<HttpResponse> {
return this.runHttp({ url: joinPaths(this.host, path), params, verb: HttpVerb.Patch, authentication })
return this.runHttp({
url: joinPaths(this.host, path),
params,
verb: HttpVerb.Patch,
authentication: authentication ?? this.authorizationToken,
})
}
async delete(path: string, params?: HttpRequestParams, authentication?: string): Promise<HttpResponse> {
return this.runHttp({ url: joinPaths(this.host, path), params, verb: HttpVerb.Delete, authentication })
return this.runHttp({
url: joinPaths(this.host, path),
params,
verb: HttpVerb.Delete,
authentication: authentication ?? this.authorizationToken,
})
}
private async runHttp(httpRequest: HttpRequest): Promise<HttpResponse> {

View File

@@ -3,6 +3,7 @@ import { HttpResponse } from './HttpResponse'
export interface HttpServiceInterface {
setHost(host: string): void
setAuthorizationToken(authorizationToken: string): void
get(path: string, params?: HttpRequestParams, authentication?: string): Promise<HttpResponse>
post(path: string, params?: HttpRequestParams, authentication?: string): Promise<HttpResponse>
put(path: string, params?: HttpRequestParams, authentication?: string): Promise<HttpResponse>

View File

@@ -1,7 +1,9 @@
import { Either } from '@standardnotes/common'
import { HttpErrorResponseBody } from '../../Http/HttpErrorResponseBody'
import { HttpResponse } from '../../Http/HttpResponse'
import { SubscriptionInviteAcceptResponseBody } from './SubscriptionInviteAcceptResponseBody'
export interface SubscriptionInviteAcceptResponse extends HttpResponse {
data: SubscriptionInviteAcceptResponseBody | HttpErrorResponseBody
data: Either<SubscriptionInviteAcceptResponseBody, HttpErrorResponseBody>
}

View File

@@ -1,7 +1,9 @@
import { Either } from '@standardnotes/common'
import { HttpErrorResponseBody } from '../../Http/HttpErrorResponseBody'
import { HttpResponse } from '../../Http/HttpResponse'
import { SubscriptionInviteCancelResponseBody } from './SubscriptionInviteCancelResponseBody'
export interface SubscriptionInviteCancelResponse extends HttpResponse {
data: SubscriptionInviteCancelResponseBody | HttpErrorResponseBody
data: Either<SubscriptionInviteCancelResponseBody, HttpErrorResponseBody>
}

View File

@@ -1,7 +1,9 @@
import { Either } from '@standardnotes/common'
import { HttpErrorResponseBody } from '../../Http/HttpErrorResponseBody'
import { HttpResponse } from '../../Http/HttpResponse'
import { SubscriptionInviteDeclineResponseBody } from './SubscriptionInviteDeclineResponseBody'
export interface SubscriptionInviteDeclineResponse extends HttpResponse {
data: SubscriptionInviteDeclineResponseBody | HttpErrorResponseBody
data: Either<SubscriptionInviteDeclineResponseBody, HttpErrorResponseBody>
}

View File

@@ -1,7 +1,9 @@
import { Either } from '@standardnotes/common'
import { HttpErrorResponseBody } from '../../Http/HttpErrorResponseBody'
import { HttpResponse } from '../../Http/HttpResponse'
import { SubscriptionInviteListResponseBody } from './SubscriptionInviteListResponseBody'
export interface SubscriptionInviteListResponse extends HttpResponse {
data: SubscriptionInviteListResponseBody | HttpErrorResponseBody
data: Either<SubscriptionInviteListResponseBody, HttpErrorResponseBody>
}

View File

@@ -1,7 +1,9 @@
import { Either } from '@standardnotes/common'
import { HttpErrorResponseBody } from '../../Http/HttpErrorResponseBody'
import { HttpResponse } from '../../Http/HttpResponse'
import { SubscriptionInviteResponseBody } from './SubscriptionInviteResponseBody'
export interface SubscriptionInviteResponse extends HttpResponse {
data: SubscriptionInviteResponseBody | HttpErrorResponseBody
data: Either<SubscriptionInviteResponseBody, HttpErrorResponseBody>
}

View File

@@ -1,3 +1,4 @@
export * from './User/Paths'
export * from './Subscription/SubscriptionServer'
export * from './Subscription/SubscriptionServerInterface'
export * from './User/UserServer'
export * from './User/UserServerInterface'

View File

@@ -35,7 +35,7 @@
"typescript": "*"
},
"dependencies": {
"@standardnotes/common": "^1.23.1",
"@standardnotes/common": "^1.32.0",
"@standardnotes/models": "workspace:*",
"@standardnotes/responses": "workspace:*",
"@standardnotes/sncrypto-common": "workspace:*",

View File

@@ -26,7 +26,7 @@
},
"dependencies": {
"@standardnotes/auth": "^3.19.4",
"@standardnotes/common": "^1.23.1",
"@standardnotes/common": "^1.32.0",
"@standardnotes/security": "^1.2.0",
"reflect-metadata": "^0.1.13"
},

View File

@@ -31,7 +31,7 @@
"ts-node": "^10.5.0"
},
"dependencies": {
"@standardnotes/common": "^1.23.1",
"@standardnotes/common": "^1.32.0",
"@standardnotes/files": "workspace:*",
"@standardnotes/utils": "workspace:*",
"@types/wicg-file-system-access": "^2020.9.5",

View File

@@ -30,7 +30,7 @@
"ts-jest": "^28.0.5"
},
"dependencies": {
"@standardnotes/common": "^1.23.1",
"@standardnotes/common": "^1.32.0",
"@standardnotes/encryption": "workspace:*",
"@standardnotes/models": "workspace:*",
"@standardnotes/responses": "workspace:*",

View File

@@ -32,7 +32,7 @@
"typescript": "*"
},
"dependencies": {
"@standardnotes/common": "^1.23.1",
"@standardnotes/common": "^1.32.0",
"@standardnotes/features": "workspace:*",
"@standardnotes/responses": "workspace:*",
"@standardnotes/utils": "workspace:*",

View File

@@ -0,0 +1,6 @@
export enum Environment {
Web = 1,
Desktop = 2,
Mobile = 3,
NativeMobileWeb = 4,
}

View File

@@ -1,10 +1,3 @@
export enum Environment {
Web = 1,
Desktop = 2,
Mobile = 3,
NativeMobileWeb = 4,
}
export enum Platform {
Ios = 1,
Android = 2,

View File

@@ -28,6 +28,8 @@ export * from './Api/Subscription/Invitation'
export * from './Api/Subscription/InvitationStatus'
export * from './Api/Subscription/InviteeIdentifierType'
export * from './Api/Subscription/InviterIdentifierType'
export * from './Device/Environment'
export * from './Device/Platform'
export * from './Local/KeyParams/RootKeyParamsInterface'
export * from './Local/RootKey/KeychainTypes'
export * from './Local/RootKey/RootKeyContent'

View File

@@ -31,7 +31,7 @@
"ts-jest": "^28.0.5"
},
"dependencies": {
"@standardnotes/common": "^1.23.1",
"@standardnotes/common": "^1.32.0",
"@standardnotes/features": "workspace:*",
"@standardnotes/security": "^1.1.0",
"reflect-metadata": "^0.1.13"

View File

@@ -11,8 +11,8 @@ module.exports = {
coverageThreshold: {
global: {
branches: 9,
functions: 9,
lines: 16,
functions: 10,
lines: 17,
statements: 16
}
}

View File

@@ -20,11 +20,13 @@
"prebuild": "yarn clean",
"build": "tsc -p tsconfig.json",
"lint": "eslint . --ext .ts",
"lint:fix": "eslint . --ext .ts --fix",
"test": "jest spec --coverage"
},
"dependencies": {
"@standardnotes/api": "workspace:^",
"@standardnotes/auth": "^3.19.4",
"@standardnotes/common": "^1.30.0",
"@standardnotes/common": "^1.32.0",
"@standardnotes/encryption": "workspace:^",
"@standardnotes/files": "workspace:^",
"@standardnotes/models": "workspace:^",
@@ -37,6 +39,7 @@
"@types/jest": "^28.1.5",
"@typescript-eslint/eslint-plugin": "^5.30.0",
"@typescript-eslint/parser": "^5.12.1",
"eslint": "^8.23.1",
"eslint-plugin-prettier": "*",
"jest": "^28.1.2",
"ts-jest": "^28.0.5"

View File

@@ -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

View File

@@ -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 */

View File

@@ -1,10 +1,10 @@
import { Environment } from './Environments'
import { ApplicationIdentifier } from '@standardnotes/common'
import {
FullyFormedTransferPayload,
TransferPayload,
LegacyRawKeychainValue,
NamespacedRootKeyInKeychain,
Environment,
} from '@standardnotes/models'
/**

View File

@@ -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

View File

@@ -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'

View File

@@ -1,6 +1,6 @@
import { Environment } from './Environments'
import { MobileDeviceInterface } from './MobileDeviceInterface'
import { DeviceInterface } from './DeviceInterface'
import { Environment } from '@standardnotes/models'
/* istanbul ignore file */

View 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>
}

View File

@@ -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([])
})
})

View File

@@ -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
}
}
}

View File

@@ -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'

View File

@@ -1,13 +1,7 @@
import { SNLog } from './../Log'
import { PureCryptoInterface } from '@standardnotes/sncrypto-common'
import {
AlertService,
DeviceInterface,
Environment,
namespacedKey,
Platform,
RawStorageKey,
} from '@standardnotes/services'
import { AlertService, DeviceInterface, namespacedKey, RawStorageKey } from '@standardnotes/services'
import { Environment, Platform } from '@standardnotes/models'
import { SNApplication } from './Application'
describe('application', () => {

View File

@@ -1,6 +1,10 @@
import {
HttpService,
HttpServiceInterface,
SubscriptionApiService,
SubscriptionApiServiceInterface,
SubscriptionServer,
SubscriptionServerInterface,
UserApiService,
UserApiServiceInterface,
UserRegistrationResponseBody,
@@ -23,9 +27,7 @@ import {
ChallengeValidation,
ComponentManagerInterface,
DiagnosticInfo,
Environment,
isDesktopDevice,
Platform,
ChallengeValue,
StorageKey,
ChallengeReason,
@@ -37,11 +39,20 @@ import {
EncryptionServiceEvent,
FilesBackupService,
FileService,
SubscriptionClientInterface,
SubscriptionManager,
} from '@standardnotes/services'
import { FilesClientInterface } from '@standardnotes/files'
import { ComputePrivateWorkspaceIdentifier } from '@standardnotes/encryption'
import { useBoolean } from '@standardnotes/utils'
import { BackupFile, DecryptedItemInterface, EncryptedItemInterface, ItemStream } from '@standardnotes/models'
import {
BackupFile,
DecryptedItemInterface,
EncryptedItemInterface,
Environment,
ItemStream,
Platform,
} from '@standardnotes/models'
import { ClientDisplayableError } from '@standardnotes/responses'
import { SnjsVersion } from './../Version'
@@ -92,6 +103,9 @@ export class SNApplication
private apiService!: InternalServices.SNApiService
private declare userApiService: UserApiServiceInterface
private declare userServer: UserServerInterface
private declare subscriptionApiService: SubscriptionApiServiceInterface
private declare subscriptionServer: SubscriptionServerInterface
private declare subscriptionManager: SubscriptionClientInterface
private sessionManager!: InternalServices.SNSessionManager
private syncService!: InternalServices.SNSyncService
private challengeService!: InternalServices.ChallengeService
@@ -187,6 +201,10 @@ export class SNApplication
this.defineInternalEventHandlers()
}
get subscriptions(): ExternalServices.SubscriptionClientInterface {
return this.subscriptionManager
}
public get files(): FilesClientInterface {
return this.fileService
}
@@ -1031,6 +1049,9 @@ export class SNApplication
this.createHttpService()
this.createUserServer()
this.createUserApiService()
this.createSubscriptionServer()
this.createSubscriptionApiService()
this.createSubscriptionManager()
this.createWebSocketsService()
this.createSessionManager()
this.createHistoryManager()
@@ -1069,6 +1090,9 @@ export class SNApplication
;(this.apiService as unknown) = undefined
;(this.userApiService as unknown) = undefined
;(this.userServer as unknown) = undefined
;(this.subscriptionApiService as unknown) = undefined
;(this.subscriptionServer as unknown) = undefined
;(this.subscriptionManager as unknown) = undefined
;(this.sessionManager as unknown) = undefined
;(this.syncService as unknown) = undefined
;(this.challengeService as unknown) = undefined
@@ -1262,6 +1286,18 @@ export class SNApplication
this.userServer = new UserServer(this.httpService)
}
private createSubscriptionApiService() {
this.subscriptionApiService = new SubscriptionApiService(this.subscriptionServer)
}
private createSubscriptionServer() {
this.subscriptionServer = new SubscriptionServer(this.httpService)
}
private createSubscriptionManager() {
this.subscriptionManager = new SubscriptionManager(this.subscriptionApiService, this.internalEventBus)
}
private createItemManager() {
this.itemManager = new InternalServices.ItemManager(this.payloadManager, this.options, this.internalEventBus)
this.services.push(this.itemManager)
@@ -1377,6 +1413,7 @@ export class SNApplication
this.protocolService,
this.challengeService,
this.webSocketsService,
this.httpService,
this.internalEventBus,
)
this.serviceObservers.push(

View File

@@ -1,5 +1,6 @@
import { Environment, Platform } from '@standardnotes/models'
import { ApplicationIdentifier } from '@standardnotes/common'
import { AlertService, DeviceInterface, Environment, Platform } from '@standardnotes/services'
import { AlertService, DeviceInterface } from '@standardnotes/services'
import { PureCryptoInterface } from '@standardnotes/sncrypto-common'
export interface RequiredApplicationOptions {

View File

@@ -1,4 +1,4 @@
import { Environment, Platform } from '@standardnotes/services'
import { Environment, Platform } from '@standardnotes/models'
export function platformFromString(string: string) {
const map: Record<string, Platform> = {

View File

@@ -1,7 +1,9 @@
import { Environment } from '@standardnotes/models'
import { DeviceInterface, InternalEventBusInterface, EncryptionService } from '@standardnotes/services'
import { SNSessionManager } from '../Services/Session/SessionManager'
import { ApplicationIdentifier } from '@standardnotes/common'
import { ItemManager } from '@Lib/Services/Items/ItemManager'
import { DeviceInterface, InternalEventBusInterface, Environment, EncryptionService } from '@standardnotes/services'
import { ChallengeService, SNSingletonManager, SNFeaturesService, DiskStorageService } from '@Lib/Services'
export type MigrationServices = {

View File

@@ -1,6 +1,7 @@
import { ApplicationIdentifier } from '@standardnotes/common'
import { Environment } from '@standardnotes/models'
import { compareSemVersions, isRightVersionGreaterThanLeft } from '@Lib/Version'
import { DeviceInterface, Environment } from '@standardnotes/services'
import { DeviceInterface } from '@standardnotes/services'
import { StorageReader } from './Reader'
import * as ReaderClasses from './Versions'

View File

@@ -1,5 +1,6 @@
import { Environment } from '@standardnotes/models'
import { ApplicationIdentifier } from '@standardnotes/common'
import { DeviceInterface, Environment } from '@standardnotes/services'
import { DeviceInterface } from '@standardnotes/services'
/**
* A storage reader reads storage via a device interface

View File

@@ -2,7 +2,8 @@ import { API_MESSAGE_RATE_LIMITED, UNKNOWN_ERROR } from './Messages'
import { HttpResponse, StatusCode } from '@standardnotes/responses'
import { isString } from '@standardnotes/utils'
import { SnjsVersion } from '@Lib/Version'
import { AbstractService, InternalEventBusInterface, Environment } from '@standardnotes/services'
import { AbstractService, InternalEventBusInterface } from '@standardnotes/services'
import { Environment } from '@standardnotes/models'
export enum HttpVerb {
Get = 'GET',

View File

@@ -11,14 +11,8 @@ import {
FeatureIdentifier,
} from '@standardnotes/features'
import { ContentType } from '@standardnotes/common'
import { GenericItem, SNComponent } from '@standardnotes/models'
import {
DesktopManagerInterface,
InternalEventBusInterface,
Environment,
Platform,
AlertService,
} from '@standardnotes/services'
import { GenericItem, SNComponent, Environment, Platform } from '@standardnotes/models'
import { DesktopManagerInterface, InternalEventBusInterface, AlertService } from '@standardnotes/services'
import { ItemManager } from '@Lib/Services/Items/ItemManager'
import { SNFeaturesService } from '@Lib/Services/Features/FeaturesService'
import { SNComponentManager } from './ComponentManager'

View File

@@ -11,6 +11,8 @@ import {
ComponentMutator,
PayloadEmitSource,
PermissionDialog,
Environment,
Platform,
} from '@standardnotes/models'
import { SNSyncService } from '@Lib/Services/Sync/SyncService'
import find from 'lodash/find'
@@ -26,8 +28,6 @@ import {
ComponentViewerInterface,
DesktopManagerInterface,
InternalEventBusInterface,
Environment,
Platform,
AlertService,
} from '@standardnotes/services'

View File

@@ -2,10 +2,8 @@ import { SNPreferencesService } from '../Preferences/PreferencesService'
import {
ComponentViewerInterface,
ComponentViewerError,
Environment,
FeatureStatus,
FeaturesEvent,
Platform,
AlertService,
} from '@standardnotes/services'
import { SNFeaturesService } from '@Lib/Services'
@@ -34,6 +32,8 @@ import {
PayloadTimestampDefaults,
IncomingComponentItemPayload,
MessageData,
Environment,
Platform,
} from '@standardnotes/models'
import find from 'lodash/find'
import uniq from 'lodash/uniq'

View File

@@ -38,6 +38,7 @@ import {
ApiCallError,
ErrorMessage,
HttpErrorResponseBody,
HttpServiceInterface,
UserApiServiceInterface,
UserRegistrationResponseBody,
} from '@standardnotes/api'
@@ -77,6 +78,7 @@ export class SNSessionManager extends AbstractService<SessionEvent> implements S
private protocolService: EncryptionService,
private challengeService: ChallengeService,
private webSocketsService: SNWebSocketsService,
private httpService: HttpServiceInterface,
protected override internalEventBus: InternalEventBusInterface,
) {
super(internalEventBus)
@@ -617,6 +619,10 @@ export class SNSessionManager extends AbstractService<SessionEvent> implements S
void this.apiService.setHost(host)
this.httpService.setHost(host)
this.httpService.setAuthorizationToken(session.authorizationValue)
await this.setSession(session)
this.webSocketsService.startWebSocketConnection(session.authorizationValue)

View File

@@ -4,7 +4,7 @@ import { SNLog } from '../../Log'
import { isErrorDecryptingParameters, SNRootKey } from '@standardnotes/encryption'
import * as Encryption from '@standardnotes/encryption'
import * as Services from '@standardnotes/services'
import { DiagnosticInfo, Environment } from '@standardnotes/services'
import { DiagnosticInfo } from '@standardnotes/services'
import {
CreateDecryptedLocalStorageContextPayload,
CreateDeletedLocalStorageContextPayload,
@@ -19,6 +19,7 @@ import {
DeletedPayloadInterface,
PayloadTimestampDefaults,
LocalStorageEncryptedContextualPayload,
Environment,
} from '@standardnotes/models'
/**

View File

@@ -0,0 +1,110 @@
import * as Factory from './lib/factory.js'
chai.use(chaiAsPromised)
const expect = chai.expect
describe('subscriptions', function () {
this.timeout(Factory.TwentySecondTimeout)
let application
let context
let subscriptionManager
afterEach(async function () {
await Factory.safeDeinit(application)
localStorage.clear()
})
beforeEach(async function () {
localStorage.clear()
context = await Factory.createAppContextWithFakeCrypto()
await context.launch()
application = context.application
subscriptionManager = context.application.subscriptionManager
const result = await Factory.registerUserToApplication({
application: application,
email: context.email,
password: context.password,
})
await Factory.publishMockedEvent('SUBSCRIPTION_PURCHASED', {
userEmail: context.email,
subscriptionId: 1,
subscriptionName: 'PRO_PLAN',
subscriptionExpiresAt: (new Date().getTime() + 3_600_000) * 1_000,
timestamp: Date.now(),
offline: false,
})
await Factory.sleep(0.25)
})
it('should invite a user by email to a shared subscription', async () => {
await subscriptionManager.inviteToSubscription('test@test.te')
const existingInvites = await subscriptionManager.listSubscriptionInvitations()
const newlyCreatedInvite = existingInvites.find(invite => invite.inviteeIdentifier === 'test@test.te')
expect(newlyCreatedInvite.status).to.equal('sent')
})
it('should not invite a user by email if the limit of shared subscription is breached', async () => {
await subscriptionManager.inviteToSubscription('test1@test.te')
await subscriptionManager.inviteToSubscription('test2@test.te')
await subscriptionManager.inviteToSubscription('test3@test.te')
await subscriptionManager.inviteToSubscription('test4@test.te')
await subscriptionManager.inviteToSubscription('test5@test.te')
let existingInvites = await subscriptionManager.listSubscriptionInvitations()
expect(existingInvites.length).to.equal(5)
expect(await subscriptionManager.inviteToSubscription('test6@test.te')).to.equal(false)
existingInvites = await subscriptionManager.listSubscriptionInvitations()
expect(existingInvites.length).to.equal(5)
})
it('should cancel a user invitation to a shared subscription', async () => {
await subscriptionManager.inviteToSubscription('test@test.te')
await subscriptionManager.inviteToSubscription('test2@test.te')
let existingInvites = await subscriptionManager.listSubscriptionInvitations()
expect (existingInvites.length).to.equal(2)
const newlyCreatedInvite = existingInvites.find(invite => invite.inviteeIdentifier === 'test@test.te')
await subscriptionManager.cancelInvitation(newlyCreatedInvite.uuid)
existingInvites = await subscriptionManager.listSubscriptionInvitations()
expect (existingInvites.length).to.equal(2)
expect(existingInvites.filter(invite => invite.status === 'canceled').length).to.equal(1)
})
it('should invite a user by email if the limit of shared subscription is restored', async () => {
await subscriptionManager.inviteToSubscription('test1@test.te')
await subscriptionManager.inviteToSubscription('test2@test.te')
await subscriptionManager.inviteToSubscription('test3@test.te')
await subscriptionManager.inviteToSubscription('test4@test.te')
await subscriptionManager.inviteToSubscription('test5@test.te')
let existingInvites = await subscriptionManager.listSubscriptionInvitations()
expect(existingInvites.length).to.equal(5)
await subscriptionManager.cancelInvitation(existingInvites[0].uuid)
expect(await subscriptionManager.inviteToSubscription('test6@test.te')).to.equal(true)
existingInvites = await subscriptionManager.listSubscriptionInvitations()
expect(existingInvites.find(invite => invite.inviteeIdentifier === 'test6@test.te')).not.to.equal(undefined)
})
})

View File

@@ -91,6 +91,7 @@
<script type="module" src="session-sharing.test.js"></script>
<script type="module" src="files.test.js"></script>
<script type="module" src="session.test.js"></script>
<script type="module" src="subscriptions.test.js"></script>
<script type="module">
mocha.run();
</script>

View File

@@ -24,6 +24,7 @@
"tsc": "tsc --project lib/tsconfig.json && tscpaths -p lib/tsconfig.json -s lib -o dist/@types",
"lint": "yarn lint:tsc && yarn lint:eslint",
"lint:eslint": "eslint --ext .ts lib/",
"lint:fix": "eslint --fix --ext .ts lib/",
"lint:tsc": "tsc --noEmit --emitDeclarationOnly false --project lib/tsconfig.json",
"test": "jest spec --coverage",
"test:debug": "node --inspect-brk node_modules/.bin/jest --runInBand"
@@ -67,7 +68,7 @@
},
"dependencies": {
"@standardnotes/api": "workspace:*",
"@standardnotes/common": "^1.25.0",
"@standardnotes/common": "^1.32.0",
"@standardnotes/domain-events": "^2.39.0",
"@standardnotes/encryption": "workspace:*",
"@standardnotes/features": "workspace:*",

View File

@@ -23,7 +23,7 @@
"test": "jest spec --coverage --passWithNoTests"
},
"dependencies": {
"@standardnotes/common": "^1.30.0",
"@standardnotes/common": "^1.32.0",
"@standardnotes/filepicker": "workspace:^",
"@standardnotes/services": "workspace:^",
"@standardnotes/styles": "workspace:^",

View File

@@ -25,7 +25,7 @@
"test": "jest spec"
},
"dependencies": {
"@standardnotes/common": "^1.23.1",
"@standardnotes/common": "^1.32.0",
"dompurify": "^2.3.8",
"lodash": "^4.17.21",
"reflect-metadata": "^0.1.13"