internal: incomplete vault systems behind feature flag (#2340)
This commit is contained in:
@@ -42,27 +42,29 @@ Object.assign(window, SNLibrary);
|
||||
|
||||
#### Prerequisites
|
||||
|
||||
To run a stable server environment for E2E tests that is up to date with production, clone the [self-hosted repository](https://github.com/standardnotes/self-hosted). Make sure you have everything set up configuration wise as in self-hosting docs. In particular, make sure the env files are created and proper values for keys are set up.
|
||||
To run a stable server environment for E2E tests that is up to date with production, [setup a local self-hosted server](https://standardnotes.com/help/self-hosting/docker).
|
||||
|
||||
Make sure you have the following value in the env vars mentioned below. It's important to have low token TTLs for the purpose of the suite.
|
||||
|
||||
Make sure you have the following value in the env vars mentioned below. It's important to have low token TTLs for the purpose of the suite. For the most up to date values it's best to check `self-hosted` github workflows. At the moment of writting the recommended values are:
|
||||
```
|
||||
# docker/auth.env
|
||||
...
|
||||
ACCESS_TOKEN_AGE=4
|
||||
REFRESH_TOKEN_AGE=10
|
||||
EPHEMERAL_SESSION_AGE=300
|
||||
|
||||
# .env
|
||||
...
|
||||
REVISIONS_FREQUENCY=5
|
||||
AUTH_SERVER_ACCESS_TOKEN_AGE=4
|
||||
AUTH_SERVER_REFRESH_TOKEN_AGE=10
|
||||
AUTH_SERVER_EPHEMERAL_SESSION_AGE=300
|
||||
SYNCING_SERVER_REVISIONS_FREQUENCY=5
|
||||
```
|
||||
|
||||
#### Start Server For Tests (SELF-HOSTED)
|
||||
Edit `docker-compose.yml` ports and change keypath services.server.ports[0] from port 3000 to 3123.
|
||||
|
||||
If running server without docker and as individual node processes, and you need a valid subscription for a test (such as uploading files), you'll need to clone the [mock-event-publisher](https://github.com/standardnotes/mock-event-publisher) and run it locally on port 3124. In the Container.ts file, comment out any SNS_ENDPOINT related lines for running locally.
|
||||
|
||||
#### Start Server For Tests
|
||||
|
||||
In the `self-hosted` folder run:
|
||||
|
||||
```
|
||||
EXPOSED_PORT=3123 ./server.sh start && ./server.sh wait-for-startup
|
||||
docker compose pull && docker compose up
|
||||
```
|
||||
|
||||
Wait for the services to be up.
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
//@ts-ignore
|
||||
global['__VERSION__'] = global['SnjsVersion'] = require('./package.json').version
|
||||
global['__IS_DEV__'] = global['isDev'] = process.env.NODE_ENV !== 'production'
|
||||
|
||||
@@ -149,6 +149,7 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
|
||||
private declare subscriptionManager: SubscriptionClientInterface
|
||||
private declare webSocketApiService: WebSocketApiServiceInterface
|
||||
private declare webSocketServer: WebSocketServerInterface
|
||||
|
||||
private sessionManager!: InternalServices.SNSessionManager
|
||||
private syncService!: InternalServices.SNSyncService
|
||||
private challengeService!: InternalServices.ChallengeService
|
||||
@@ -171,6 +172,13 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
|
||||
private integrityService!: ExternalServices.IntegrityService
|
||||
private statusService!: ExternalServices.StatusService
|
||||
private filesBackupService?: FilesBackupService
|
||||
private vaultService!: ExternalServices.VaultServiceInterface
|
||||
private contactService!: ExternalServices.ContactServiceInterface
|
||||
private sharedVaultService!: ExternalServices.SharedVaultServiceInterface
|
||||
private userEventService!: ExternalServices.UserEventService
|
||||
private asymmetricMessageService!: ExternalServices.AsymmetricMessageService
|
||||
private keySystemKeyManager!: ExternalServices.KeySystemKeyManager
|
||||
|
||||
private declare sessionStorageMapper: MapperInterface<Session, Record<string, unknown>>
|
||||
private declare legacySessionStorageMapper: MapperInterface<LegacySession, Record<string, unknown>>
|
||||
private declare authenticatorManager: AuthenticatorClientInterface
|
||||
@@ -313,7 +321,7 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
|
||||
return this.featuresService
|
||||
}
|
||||
|
||||
public get items(): ExternalServices.ItemsClientInterface {
|
||||
public get items(): ExternalServices.ItemManagerInterface {
|
||||
return this.itemManager
|
||||
}
|
||||
|
||||
@@ -373,6 +381,18 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
|
||||
return this.challengeService
|
||||
}
|
||||
|
||||
public get vaults(): ExternalServices.VaultServiceInterface {
|
||||
return this.vaultService
|
||||
}
|
||||
|
||||
public get contacts(): ExternalServices.ContactServiceInterface {
|
||||
return this.contactService
|
||||
}
|
||||
|
||||
public get sharedVaults(): ExternalServices.SharedVaultServiceInterface {
|
||||
return this.sharedVaultService
|
||||
}
|
||||
|
||||
public computePrivateUsername(username: string): Promise<string | undefined> {
|
||||
return ComputePrivateUsername(this.options.crypto, username)
|
||||
}
|
||||
@@ -534,6 +554,11 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
|
||||
for (const service of this.services) {
|
||||
await service.handleApplicationStage(stage)
|
||||
}
|
||||
|
||||
this.internalEventBus.publish({
|
||||
type: ApplicationEvent.ApplicationStageChanged,
|
||||
payload: { stage } as ExternalServices.ApplicationStageChangedEventPayload,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -587,11 +612,13 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
|
||||
} else if (event === ApplicationEvent.Launched) {
|
||||
this.onLaunch()
|
||||
}
|
||||
|
||||
for (const observer of this.eventHandlers.slice()) {
|
||||
if ((observer.singleEvent && observer.singleEvent === event) || !observer.singleEvent) {
|
||||
await observer.callback(event, data || {})
|
||||
}
|
||||
}
|
||||
|
||||
void this.migrationService.handleApplicationEvent(event)
|
||||
}
|
||||
|
||||
@@ -637,6 +664,9 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
|
||||
public async getAvailableSubscriptions(): Promise<
|
||||
Responses.AvailableSubscriptions | Responses.ClientDisplayableError
|
||||
> {
|
||||
if (this.isThirdPartyHostUsed()) {
|
||||
return ClientDisplayableError.FromString('Third party hosts do not support subscriptions.')
|
||||
}
|
||||
return this.sessionManager.getAvailableSubscriptions()
|
||||
}
|
||||
|
||||
@@ -827,8 +857,8 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
|
||||
return this.diskStorageService.setValue(key, value, mode)
|
||||
}
|
||||
|
||||
public getValue(key: string, mode?: ExternalServices.StorageValueModes): unknown {
|
||||
return this.diskStorageService.getValue(key, mode)
|
||||
public getValue<T>(key: string, mode?: ExternalServices.StorageValueModes): T {
|
||||
return this.diskStorageService.getValue<T>(key, mode)
|
||||
}
|
||||
|
||||
public async removeValue(key: string, mode?: ExternalServices.StorageValueModes): Promise<void> {
|
||||
@@ -863,7 +893,7 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
|
||||
}
|
||||
}
|
||||
|
||||
public addChallengeObserver(challenge: Challenge, observer: InternalServices.ChallengeObserver): () => void {
|
||||
public addChallengeObserver(challenge: Challenge, observer: ExternalServices.ChallengeObserver): () => void {
|
||||
return this.challengeService.addChallengeObserver(challenge, observer)
|
||||
}
|
||||
|
||||
@@ -980,6 +1010,53 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
|
||||
})
|
||||
}
|
||||
|
||||
public async changeAndSaveItem<M extends Models.DecryptedItemMutator = Models.DecryptedItemMutator>(
|
||||
itemToLookupUuidFor: DecryptedItemInterface,
|
||||
mutate: (mutator: M) => void,
|
||||
updateTimestamps = true,
|
||||
emitSource?: Models.PayloadEmitSource,
|
||||
syncOptions?: ExternalServices.SyncOptions,
|
||||
): Promise<DecryptedItemInterface | undefined> {
|
||||
await this.mutator.changeItems(
|
||||
[itemToLookupUuidFor],
|
||||
mutate,
|
||||
updateTimestamps ? Models.MutationType.UpdateUserTimestamps : Models.MutationType.NoUpdateUserTimestamps,
|
||||
emitSource,
|
||||
)
|
||||
await this.syncService.sync(syncOptions)
|
||||
return this.itemManager.findItem(itemToLookupUuidFor.uuid)
|
||||
}
|
||||
|
||||
public async changeAndSaveItems<M extends Models.DecryptedItemMutator = Models.DecryptedItemMutator>(
|
||||
itemsToLookupUuidsFor: DecryptedItemInterface[],
|
||||
mutate: (mutator: M) => void,
|
||||
updateTimestamps = true,
|
||||
emitSource?: Models.PayloadEmitSource,
|
||||
syncOptions?: ExternalServices.SyncOptions,
|
||||
): Promise<void> {
|
||||
await this.mutator.changeItems(
|
||||
itemsToLookupUuidsFor,
|
||||
mutate,
|
||||
updateTimestamps ? Models.MutationType.UpdateUserTimestamps : Models.MutationType.NoUpdateUserTimestamps,
|
||||
emitSource,
|
||||
)
|
||||
await this.syncService.sync(syncOptions)
|
||||
}
|
||||
|
||||
public async importData(data: BackupFile, awaitSync = false): Promise<ExternalServices.ImportDataReturnType> {
|
||||
const usecase = new ExternalServices.ImportDataUseCase(
|
||||
this.itemManager,
|
||||
this.syncService,
|
||||
this.protectionService,
|
||||
this.protocolService,
|
||||
this.payloadManager,
|
||||
this.challengeService,
|
||||
this.historyManager,
|
||||
)
|
||||
|
||||
return usecase.execute(data, awaitSync)
|
||||
}
|
||||
|
||||
private async handleRevokedSession(): Promise<void> {
|
||||
/**
|
||||
* Because multiple API requests can come back at the same time
|
||||
@@ -1148,9 +1225,16 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
|
||||
this.createMappers()
|
||||
this.createPayloadManager()
|
||||
this.createItemManager()
|
||||
this.createMutatorService()
|
||||
|
||||
this.createDiskStorageManager()
|
||||
this.createUserEventService()
|
||||
|
||||
this.createInMemoryStorageManager()
|
||||
|
||||
this.createKeySystemKeyManager()
|
||||
this.createProtocolService()
|
||||
|
||||
this.diskStorageService.provideEncryptionProvider(this.protocolService)
|
||||
this.createChallengeService()
|
||||
this.createLegacyHttpManager()
|
||||
@@ -1185,7 +1269,7 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
|
||||
this.createFileService()
|
||||
|
||||
this.createIntegrityService()
|
||||
this.createMutatorService()
|
||||
|
||||
this.createListedService()
|
||||
this.createActionsManager()
|
||||
this.createAuthenticatorManager()
|
||||
@@ -1193,6 +1277,10 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
|
||||
this.createRevisionManager()
|
||||
|
||||
this.createUseCases()
|
||||
this.createContactService()
|
||||
this.createVaultService()
|
||||
this.createSharedVaultService()
|
||||
this.createAsymmetricMessageService()
|
||||
}
|
||||
|
||||
private clearServices() {
|
||||
@@ -1249,6 +1337,12 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
|
||||
;(this._listRevisions as unknown) = undefined
|
||||
;(this._getRevision as unknown) = undefined
|
||||
;(this._deleteRevision as unknown) = undefined
|
||||
;(this.vaultService as unknown) = undefined
|
||||
;(this.contactService as unknown) = undefined
|
||||
;(this.sharedVaultService as unknown) = undefined
|
||||
;(this.userEventService as unknown) = undefined
|
||||
;(this.asymmetricMessageService as unknown) = undefined
|
||||
;(this.keySystemKeyManager as unknown) = undefined
|
||||
|
||||
this.services = []
|
||||
}
|
||||
@@ -1270,6 +1364,71 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
|
||||
;(this.internalEventBus as unknown) = undefined
|
||||
}
|
||||
|
||||
private createUserEventService(): void {
|
||||
this.userEventService = new ExternalServices.UserEventService(this.internalEventBus)
|
||||
this.services.push(this.userEventService)
|
||||
}
|
||||
|
||||
private createAsymmetricMessageService() {
|
||||
this.asymmetricMessageService = new ExternalServices.AsymmetricMessageService(
|
||||
this.httpService,
|
||||
this.protocolService,
|
||||
this.contacts,
|
||||
this.itemManager,
|
||||
this.mutator,
|
||||
this.syncService,
|
||||
this.internalEventBus,
|
||||
)
|
||||
this.services.push(this.asymmetricMessageService)
|
||||
}
|
||||
|
||||
private createContactService(): void {
|
||||
this.contactService = new ExternalServices.ContactService(
|
||||
this.syncService,
|
||||
this.itemManager,
|
||||
this.mutator,
|
||||
this.sessionManager,
|
||||
this.options.crypto,
|
||||
this.user,
|
||||
this.protocolService,
|
||||
this.singletonManager,
|
||||
this.internalEventBus,
|
||||
)
|
||||
|
||||
this.services.push(this.contactService)
|
||||
}
|
||||
|
||||
private createSharedVaultService(): void {
|
||||
this.sharedVaultService = new ExternalServices.SharedVaultService(
|
||||
this.httpService,
|
||||
this.syncService,
|
||||
this.itemManager,
|
||||
this.mutator,
|
||||
this.protocolService,
|
||||
this.sessions,
|
||||
this.contactService,
|
||||
this.files,
|
||||
this.vaults,
|
||||
this.storage,
|
||||
this.internalEventBus,
|
||||
)
|
||||
this.services.push(this.sharedVaultService)
|
||||
}
|
||||
|
||||
private createVaultService(): void {
|
||||
this.vaultService = new ExternalServices.VaultService(
|
||||
this.syncService,
|
||||
this.itemManager,
|
||||
this.mutator,
|
||||
this.protocolService,
|
||||
this.files,
|
||||
this.alertService,
|
||||
this.internalEventBus,
|
||||
)
|
||||
|
||||
this.services.push(this.vaultService)
|
||||
}
|
||||
|
||||
private createListedService(): void {
|
||||
this.listedService = new InternalServices.ListedService(
|
||||
this.apiService,
|
||||
@@ -1278,6 +1437,7 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
|
||||
this.deprecatedHttpService,
|
||||
this.protectionService,
|
||||
this.mutator,
|
||||
this.sync,
|
||||
this.internalEventBus,
|
||||
)
|
||||
this.services.push(this.listedService)
|
||||
@@ -1286,10 +1446,11 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
|
||||
private createFileService() {
|
||||
this.fileService = new FileService(
|
||||
this.apiService,
|
||||
this.itemManager,
|
||||
this.mutator,
|
||||
this.syncService,
|
||||
this.protocolService,
|
||||
this.challengeService,
|
||||
this.httpService,
|
||||
this.alertService,
|
||||
this.options.crypto,
|
||||
this.internalEventBus,
|
||||
@@ -1315,6 +1476,7 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
|
||||
this.diskStorageService,
|
||||
this.apiService,
|
||||
this.itemManager,
|
||||
this.mutator,
|
||||
this.webSocketsService,
|
||||
this.settingsService,
|
||||
this.userService,
|
||||
@@ -1366,6 +1528,7 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
|
||||
sessionManager: this.sessionManager,
|
||||
challengeService: this.challengeService,
|
||||
itemManager: this.itemManager,
|
||||
mutator: this.mutator,
|
||||
singletonManager: this.singletonManager,
|
||||
featuresService: this.featuresService,
|
||||
environment: this.environment,
|
||||
@@ -1453,6 +1616,7 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
|
||||
private createComponentManager() {
|
||||
this.componentManagerService = new InternalServices.SNComponentManager(
|
||||
this.itemManager,
|
||||
this.mutator,
|
||||
this.syncService,
|
||||
this.featuresService,
|
||||
this.preferencesService,
|
||||
@@ -1508,6 +1672,7 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
|
||||
private createSingletonManager() {
|
||||
this.singletonManager = new InternalServices.SNSingletonManager(
|
||||
this.itemManager,
|
||||
this.mutator,
|
||||
this.payloadManager,
|
||||
this.syncService,
|
||||
this.internalEventBus,
|
||||
@@ -1531,9 +1696,11 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
|
||||
private createProtocolService() {
|
||||
this.protocolService = new EncryptionService(
|
||||
this.itemManager,
|
||||
this.mutator,
|
||||
this.payloadManager,
|
||||
this.deviceInterface,
|
||||
this.diskStorageService,
|
||||
this.keySystemKeyManager,
|
||||
this.identifier,
|
||||
this.options.crypto,
|
||||
this.internalEventBus,
|
||||
@@ -1548,6 +1715,17 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
|
||||
this.services.push(this.protocolService)
|
||||
}
|
||||
|
||||
private createKeySystemKeyManager() {
|
||||
this.keySystemKeyManager = new ExternalServices.KeySystemKeyManager(
|
||||
this.itemManager,
|
||||
this.mutator,
|
||||
this.storage,
|
||||
this.internalEventBus,
|
||||
)
|
||||
|
||||
this.services.push(this.keySystemKeyManager)
|
||||
}
|
||||
|
||||
private createKeyRecoveryService() {
|
||||
this.keyRecoveryService = new InternalServices.SNKeyRecoveryService(
|
||||
this.itemManager,
|
||||
@@ -1582,7 +1760,7 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
|
||||
this.serviceObservers.push(
|
||||
this.sessionManager.addEventObserver(async (event) => {
|
||||
switch (event) {
|
||||
case InternalServices.SessionEvent.Restored: {
|
||||
case ExternalServices.SessionEvent.Restored: {
|
||||
void (async () => {
|
||||
await this.sync.sync({ sourceDescription: 'Session restored pre key creation' })
|
||||
if (this.protocolService.needsNewRootKeyBasedItemsKey()) {
|
||||
@@ -1593,10 +1771,12 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
|
||||
})()
|
||||
break
|
||||
}
|
||||
case InternalServices.SessionEvent.Revoked: {
|
||||
case ExternalServices.SessionEvent.Revoked: {
|
||||
await this.handleRevokedSession()
|
||||
break
|
||||
}
|
||||
case ExternalServices.SessionEvent.UserKeyPairChanged:
|
||||
break
|
||||
default: {
|
||||
Utils.assertUnreachable(event)
|
||||
}
|
||||
@@ -1655,6 +1835,7 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
|
||||
private createProtectionService() {
|
||||
this.protectionService = new InternalServices.SNProtectionService(
|
||||
this.protocolService,
|
||||
this.mutator,
|
||||
this.challengeService,
|
||||
this.diskStorageService,
|
||||
this.internalEventBus,
|
||||
@@ -1701,6 +1882,7 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
|
||||
this.preferencesService = new InternalServices.SNPreferencesService(
|
||||
this.singletonManager,
|
||||
this.itemManager,
|
||||
this.mutator,
|
||||
this.syncService,
|
||||
this.internalEventBus,
|
||||
)
|
||||
@@ -1734,13 +1916,8 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
|
||||
private createMutatorService() {
|
||||
this.mutatorService = new InternalServices.MutatorService(
|
||||
this.itemManager,
|
||||
this.syncService,
|
||||
this.protectionService,
|
||||
this.protocolService,
|
||||
this.payloadManager,
|
||||
this.challengeService,
|
||||
this.componentManagerService,
|
||||
this.historyManager,
|
||||
this.alertService,
|
||||
this.internalEventBus,
|
||||
)
|
||||
this.services.push(this.mutatorService)
|
||||
|
||||
@@ -5,7 +5,7 @@ export function applicationEventForSyncEvent(syncEvent: SyncEvent) {
|
||||
return (
|
||||
{
|
||||
[SyncEvent.SyncCompletedWithAllItemsUploaded]: ApplicationEvent.CompletedFullSync,
|
||||
[SyncEvent.SingleRoundTripSyncCompleted]: ApplicationEvent.CompletedIncrementalSync,
|
||||
[SyncEvent.PaginatedSyncRequestCompleted]: ApplicationEvent.CompletedIncrementalSync,
|
||||
[SyncEvent.SyncError]: ApplicationEvent.FailedSync,
|
||||
[SyncEvent.SyncTakingTooLong]: ApplicationEvent.HighLatencySync,
|
||||
[SyncEvent.EnterOutOfSync]: ApplicationEvent.EnteredOutOfSync,
|
||||
@@ -14,7 +14,7 @@ export function applicationEventForSyncEvent(syncEvent: SyncEvent) {
|
||||
[SyncEvent.MajorDataChange]: ApplicationEvent.MajorDataChange,
|
||||
[SyncEvent.LocalDataIncrementalLoad]: ApplicationEvent.LocalDataIncrementalLoad,
|
||||
[SyncEvent.StatusChanged]: ApplicationEvent.SyncStatusChanged,
|
||||
[SyncEvent.SyncWillBegin]: ApplicationEvent.WillSync,
|
||||
[SyncEvent.SyncDidBeginProcessing]: ApplicationEvent.WillSync,
|
||||
[SyncEvent.InvalidSession]: ApplicationEvent.InvalidSyncSession,
|
||||
[SyncEvent.DatabaseReadError]: ApplicationEvent.LocalDatabaseReadError,
|
||||
[SyncEvent.DatabaseWriteError]: ApplicationEvent.LocalDatabaseWriteError,
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { DecryptedItemInterface } from '@standardnotes/models'
|
||||
import { SNApplication } from './Application'
|
||||
import { ApplicationInterface } from '@standardnotes/services'
|
||||
|
||||
/** Keeps an item reference up to date with changes */
|
||||
export class LiveItem<T extends DecryptedItemInterface> {
|
||||
public item: T
|
||||
private removeObserver: () => void
|
||||
|
||||
constructor(uuid: string, application: SNApplication, onChange?: (item: T) => void) {
|
||||
constructor(uuid: string, application: ApplicationInterface, onChange?: (item: T) => void) {
|
||||
this.item = application.items.findSureItem(uuid)
|
||||
|
||||
onChange && onChange(this.item)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { ServerItemResponse } from '@standardnotes/responses'
|
||||
import { RevisionClientInterface } from '@standardnotes/services'
|
||||
import { Result, UseCaseInterface, Uuid } from '@standardnotes/domain-core'
|
||||
import {
|
||||
@@ -50,6 +51,8 @@ export class GetRevision implements UseCaseInterface<HistoryEntry> {
|
||||
content_type: revision.content_type as ContentType,
|
||||
updated_at: new Date(revision.updated_at),
|
||||
created_at: new Date(revision.created_at),
|
||||
key_system_identifier: revision.key_system_identifier ?? undefined,
|
||||
shared_vault_uuid: revision.shared_vault_uuid ?? undefined,
|
||||
waitingForKey: false,
|
||||
errorDecrypting: false,
|
||||
})
|
||||
@@ -67,7 +70,7 @@ export class GetRevision implements UseCaseInterface<HistoryEntry> {
|
||||
uuid: sourceItemUuid || revision.item_uuid,
|
||||
})
|
||||
|
||||
if (!isRemotePayloadAllowed(payload)) {
|
||||
if (!isRemotePayloadAllowed(payload as ServerItemResponse)) {
|
||||
return Result.fail(`Remote payload is disallowed: ${JSON.stringify(payload)}`)
|
||||
}
|
||||
|
||||
|
||||
3
packages/snjs/lib/IsDev.ts
Normal file
3
packages/snjs/lib/IsDev.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
/** Declared in webpack config */
|
||||
declare const __IS_DEV__: boolean
|
||||
export const isDev = __IS_DEV__
|
||||
@@ -1,31 +1,37 @@
|
||||
import { ItemManager } from '@Lib/Services'
|
||||
import { TagsToFoldersMigrationApplicator } from './TagsToFolders'
|
||||
import { MutatorClientInterface } from '@standardnotes/services'
|
||||
|
||||
describe('folders component to hierarchy', () => {
|
||||
let itemManager: ItemManager
|
||||
let mutator: MutatorClientInterface
|
||||
let changeItemMock: jest.Mock
|
||||
let findOrCreateTagParentChainMock: jest.Mock
|
||||
|
||||
const itemManagerMock = (tagTitles: string[]) => {
|
||||
const mockTag = (title: string) => ({
|
||||
title,
|
||||
uuid: title,
|
||||
parentId: undefined,
|
||||
})
|
||||
beforeEach(() => {
|
||||
itemManager = {} as unknown as jest.Mocked<ItemManager>
|
||||
|
||||
const mock = {
|
||||
getItems: jest.fn().mockReturnValue(tagTitles.map(mockTag)),
|
||||
findOrCreateTagParentChain: jest.fn(),
|
||||
changeItem: jest.fn(),
|
||||
}
|
||||
mutator = {} as unknown as jest.Mocked<MutatorClientInterface>
|
||||
|
||||
return mock
|
||||
}
|
||||
changeItemMock = mutator.changeItem = jest.fn()
|
||||
findOrCreateTagParentChainMock = mutator.findOrCreateTagParentChain = jest.fn()
|
||||
})
|
||||
|
||||
describe('folders component to hierarchy', () => {
|
||||
it('should produce a valid hierarchy in the simple case', async () => {
|
||||
const titles = ['a', 'a.b', 'a.b.c']
|
||||
itemManager.getItems
|
||||
|
||||
const itemManager = itemManagerMock(titles)
|
||||
await TagsToFoldersMigrationApplicator.run(itemManager as unknown as ItemManager)
|
||||
itemManager.getItems = jest.fn().mockReturnValue(titles.map(mockTag))
|
||||
|
||||
const findOrCreateTagParentChainCalls = itemManager.findOrCreateTagParentChain.mock.calls
|
||||
const changeItemCalls = itemManager.changeItem.mock.calls
|
||||
await TagsToFoldersMigrationApplicator.run(itemManager, mutator)
|
||||
|
||||
const findOrCreateTagParentChainCalls = findOrCreateTagParentChainMock.mock.calls
|
||||
const changeItemCalls = changeItemMock.mock.calls
|
||||
|
||||
expect(findOrCreateTagParentChainCalls.length).toEqual(2)
|
||||
expect(findOrCreateTagParentChainCalls[0][0]).toEqual(['a'])
|
||||
@@ -39,11 +45,11 @@ describe('folders component to hierarchy', () => {
|
||||
it('should not touch flat hierarchies', async () => {
|
||||
const titles = ['a', 'x', 'y', 'z']
|
||||
|
||||
const itemManager = itemManagerMock(titles)
|
||||
await TagsToFoldersMigrationApplicator.run(itemManager as unknown as ItemManager)
|
||||
itemManager.getItems = jest.fn().mockReturnValue(titles.map(mockTag))
|
||||
await TagsToFoldersMigrationApplicator.run(itemManager, mutator)
|
||||
|
||||
const findOrCreateTagParentChainCalls = itemManager.findOrCreateTagParentChain.mock.calls
|
||||
const changeItemCalls = itemManager.changeItem.mock.calls
|
||||
const findOrCreateTagParentChainCalls = findOrCreateTagParentChainMock.mock.calls
|
||||
const changeItemCalls = changeItemMock.mock.calls
|
||||
|
||||
expect(findOrCreateTagParentChainCalls.length).toEqual(0)
|
||||
|
||||
@@ -53,11 +59,11 @@ describe('folders component to hierarchy', () => {
|
||||
it('should work despite cloned tags', async () => {
|
||||
const titles = ['a.b', 'c', 'a.b']
|
||||
|
||||
const itemManager = itemManagerMock(titles)
|
||||
await TagsToFoldersMigrationApplicator.run(itemManager as unknown as ItemManager)
|
||||
itemManager.getItems = jest.fn().mockReturnValue(titles.map(mockTag))
|
||||
await TagsToFoldersMigrationApplicator.run(itemManager, mutator)
|
||||
|
||||
const findOrCreateTagParentChainCalls = itemManager.findOrCreateTagParentChain.mock.calls
|
||||
const changeItemCalls = itemManager.changeItem.mock.calls
|
||||
const findOrCreateTagParentChainCalls = findOrCreateTagParentChainMock.mock.calls
|
||||
const changeItemCalls = changeItemMock.mock.calls
|
||||
|
||||
expect(findOrCreateTagParentChainCalls.length).toEqual(2)
|
||||
expect(findOrCreateTagParentChainCalls[0][0]).toEqual(['a'])
|
||||
@@ -71,11 +77,11 @@ describe('folders component to hierarchy', () => {
|
||||
it('should produce a valid hierarchy cases with missing intermediate tags or unordered', async () => {
|
||||
const titles = ['y.2', 'w.3', 'y']
|
||||
|
||||
const itemManager = itemManagerMock(titles)
|
||||
await TagsToFoldersMigrationApplicator.run(itemManager as unknown as ItemManager)
|
||||
itemManager.getItems = jest.fn().mockReturnValue(titles.map(mockTag))
|
||||
await TagsToFoldersMigrationApplicator.run(itemManager, mutator)
|
||||
|
||||
const findOrCreateTagParentChainCalls = itemManager.findOrCreateTagParentChain.mock.calls
|
||||
const changeItemCalls = itemManager.changeItem.mock.calls
|
||||
const findOrCreateTagParentChainCalls = findOrCreateTagParentChainMock.mock.calls
|
||||
const changeItemCalls = changeItemMock.mock.calls
|
||||
|
||||
expect(findOrCreateTagParentChainCalls.length).toEqual(2)
|
||||
expect(findOrCreateTagParentChainCalls[0][0]).toEqual(['w'])
|
||||
@@ -89,11 +95,11 @@ describe('folders component to hierarchy', () => {
|
||||
it('skip prefixed names', async () => {
|
||||
const titles = ['.something', '.something...something']
|
||||
|
||||
const itemManager = itemManagerMock(titles)
|
||||
await TagsToFoldersMigrationApplicator.run(itemManager as unknown as ItemManager)
|
||||
itemManager.getItems = jest.fn().mockReturnValue(titles.map(mockTag))
|
||||
await TagsToFoldersMigrationApplicator.run(itemManager, mutator)
|
||||
|
||||
const findOrCreateTagParentChainCalls = itemManager.findOrCreateTagParentChain.mock.calls
|
||||
const changeItemCalls = itemManager.changeItem.mock.calls
|
||||
const findOrCreateTagParentChainCalls = findOrCreateTagParentChainMock.mock.calls
|
||||
const changeItemCalls = changeItemMock.mock.calls
|
||||
|
||||
expect(findOrCreateTagParentChainCalls.length).toEqual(0)
|
||||
expect(changeItemCalls.length).toEqual(0)
|
||||
@@ -109,11 +115,11 @@ describe('folders component to hierarchy', () => {
|
||||
'something..another.thing..anyway',
|
||||
]
|
||||
|
||||
const itemManager = itemManagerMock(titles)
|
||||
await TagsToFoldersMigrationApplicator.run(itemManager as unknown as ItemManager)
|
||||
itemManager.getItems = jest.fn().mockReturnValue(titles.map(mockTag))
|
||||
await TagsToFoldersMigrationApplicator.run(itemManager, mutator)
|
||||
|
||||
const findOrCreateTagParentChainCalls = itemManager.findOrCreateTagParentChain.mock.calls
|
||||
const changeItemCalls = itemManager.changeItem.mock.calls
|
||||
const findOrCreateTagParentChainCalls = findOrCreateTagParentChainMock.mock.calls
|
||||
const changeItemCalls = changeItemMock.mock.calls
|
||||
|
||||
expect(findOrCreateTagParentChainCalls.length).toEqual(1)
|
||||
expect(findOrCreateTagParentChainCalls[0][0]).toEqual(['a', 'b'])
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { MutatorClientInterface } from '@standardnotes/services'
|
||||
import { SNTag, TagMutator, TagFolderDelimitter } from '@standardnotes/models'
|
||||
import { ItemManager } from '@Lib/Services'
|
||||
import { lastElement, sortByKey, withoutLastElement } from '@standardnotes/utils'
|
||||
@@ -15,7 +16,7 @@ export class TagsToFoldersMigrationApplicator {
|
||||
return false
|
||||
}
|
||||
|
||||
public static async run(itemManager: ItemManager): Promise<void> {
|
||||
public static async run(itemManager: ItemManager, mutator: MutatorClientInterface): Promise<void> {
|
||||
const tags = itemManager.getItems(ContentType.Tag) as SNTag[]
|
||||
const sortedTags = sortByKey(tags, 'title')
|
||||
|
||||
@@ -36,9 +37,9 @@ export class TagsToFoldersMigrationApplicator {
|
||||
return
|
||||
}
|
||||
|
||||
const parent = await itemManager.findOrCreateTagParentChain(parents)
|
||||
const parent = await mutator.findOrCreateTagParentChain(parents)
|
||||
|
||||
await itemManager.changeItem(tag, (mutator: TagMutator) => {
|
||||
await mutator.changeItem(tag, (mutator: TagMutator) => {
|
||||
mutator.title = newTitle
|
||||
|
||||
if (parent) {
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import { AnyKeyParamsContent, KeyParamsContent004 } from '@standardnotes/common'
|
||||
import { EncryptedPayload, EncryptedTransferPayload, isErrorDecryptingPayload } from '@standardnotes/models'
|
||||
import {
|
||||
EncryptedPayload,
|
||||
EncryptedTransferPayload,
|
||||
isErrorDecryptingPayload,
|
||||
ContentTypeUsesRootKeyEncryption,
|
||||
} from '@standardnotes/models'
|
||||
import { PreviousSnjsVersion1_0_0, PreviousSnjsVersion2_0_0, SnjsVersion } from '../Version'
|
||||
import { Migration } from '@Lib/Migrations/Migration'
|
||||
import {
|
||||
@@ -16,7 +21,6 @@ import {
|
||||
import { assert } from '@standardnotes/utils'
|
||||
import { CreateReader } from './StorageReaders/Functions'
|
||||
import { StorageReader } from './StorageReaders/Reader'
|
||||
import { ContentTypeUsesRootKeyEncryption } from '@standardnotes/encryption'
|
||||
|
||||
/** A key that was briefly present in Snjs version 2.0.0 but removed in 2.0.1 */
|
||||
const LastMigrationTimeStampKey2_0_0 = 'last_migration_timestamp'
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import { BackupServiceInterface } from '@standardnotes/files'
|
||||
import { Environment, Platform } from '@standardnotes/models'
|
||||
import { DeviceInterface, InternalEventBusInterface, EncryptionService } from '@standardnotes/services'
|
||||
import {
|
||||
DeviceInterface,
|
||||
InternalEventBusInterface,
|
||||
EncryptionService,
|
||||
MutatorClientInterface,
|
||||
} from '@standardnotes/services'
|
||||
import { SNSessionManager } from '../Services/Session/SessionManager'
|
||||
import { ApplicationIdentifier } from '@standardnotes/common'
|
||||
import { ItemManager } from '@Lib/Services/Items/ItemManager'
|
||||
@@ -15,6 +20,7 @@ export type MigrationServices = {
|
||||
sessionManager: SNSessionManager
|
||||
backups?: BackupServiceInterface
|
||||
itemManager: ItemManager
|
||||
mutator: MutatorClientInterface
|
||||
singletonManager: SNSingletonManager
|
||||
featuresService: SNFeaturesService
|
||||
environment: Environment
|
||||
|
||||
@@ -20,7 +20,7 @@ export class Migration2_20_0 extends Migration {
|
||||
|
||||
for (const item of items) {
|
||||
this.services.itemManager.removeItemLocally(item)
|
||||
await this.services.storageService.deletePayloadWithId(item.uuid)
|
||||
await this.services.storageService.deletePayloadWithUuid(item.uuid)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ export class Migration2_36_0 extends Migration {
|
||||
|
||||
for (const item of items) {
|
||||
this.services.itemManager.removeItemLocally(item)
|
||||
await this.services.storageService.deletePayloadWithId(item.uuid)
|
||||
await this.services.storageService.deletePayloadWithUuid(item.uuid)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ export class Migration2_42_0 extends Migration {
|
||||
})
|
||||
|
||||
for (const theme of themes) {
|
||||
await this.services.itemManager.setItemToBeDeleted(theme)
|
||||
await this.services.mutator.setItemToBeDeleted(theme)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ export class Migration2_7_0 extends Migration {
|
||||
const batchMgrSingleton = this.services.singletonManager.findSingleton(ContentType.Component, batchMgrPred)
|
||||
|
||||
if (batchMgrSingleton) {
|
||||
await this.services.itemManager.setItemToBeDeleted(batchMgrSingleton)
|
||||
await this.services.mutator.setItemToBeDeleted(batchMgrSingleton)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
ItemsServerInterface,
|
||||
StorageKey,
|
||||
ApiServiceEvent,
|
||||
DiagnosticInfo,
|
||||
KeyValueStoreInterface,
|
||||
API_MESSAGE_GENERIC_SYNC_FAIL,
|
||||
API_MESSAGE_GENERIC_TOKEN_REFRESH_FAIL,
|
||||
@@ -30,8 +29,8 @@ import {
|
||||
API_MESSAGE_TOKEN_REFRESH_IN_PROGRESS,
|
||||
ApiServiceEventData,
|
||||
} from '@standardnotes/services'
|
||||
import { FilesApiInterface } from '@standardnotes/files'
|
||||
import { ServerSyncPushContextualPayload, SNFeatureRepo, FileContent } from '@standardnotes/models'
|
||||
import { DownloadFileParams, FileOwnershipType, FilesApiInterface } from '@standardnotes/files'
|
||||
import { ServerSyncPushContextualPayload, SNFeatureRepo } from '@standardnotes/models'
|
||||
import {
|
||||
User,
|
||||
HttpStatusCode,
|
||||
@@ -72,6 +71,8 @@ import {
|
||||
HttpErrorResponse,
|
||||
HttpSuccessResponse,
|
||||
isErrorResponse,
|
||||
ValetTokenOperation,
|
||||
MoveFileResponse,
|
||||
} from '@standardnotes/responses'
|
||||
import { LegacySession, MapperInterface, Session, SessionToken } from '@standardnotes/domain-core'
|
||||
import { HttpServiceInterface } from '@standardnotes/api'
|
||||
@@ -103,7 +104,6 @@ export class SNApiService
|
||||
{
|
||||
private session: Session | LegacySession | null
|
||||
public user?: User
|
||||
private registering = false
|
||||
private authenticating = false
|
||||
private changing = false
|
||||
private refreshingSession = false
|
||||
@@ -210,7 +210,7 @@ export class SNApiService
|
||||
}
|
||||
|
||||
private errorResponseWithFallbackMessage(response: HttpErrorResponse, message: string): HttpErrorResponse {
|
||||
if (!response.data.error.message) {
|
||||
if (response.data.error && !response.data.error.message) {
|
||||
response.data.error.message = message
|
||||
}
|
||||
|
||||
@@ -369,9 +369,10 @@ export class SNApiService
|
||||
|
||||
async sync(
|
||||
payloads: ServerSyncPushContextualPayload[],
|
||||
lastSyncToken: string,
|
||||
paginationToken: string,
|
||||
lastSyncToken: string | undefined,
|
||||
paginationToken: string | undefined,
|
||||
limit: number,
|
||||
sharedVaultUuids?: string[],
|
||||
): Promise<HttpResponse<RawSyncResponse>> {
|
||||
const preprocessingError = this.preprocessingError()
|
||||
if (preprocessingError) {
|
||||
@@ -383,6 +384,7 @@ export class SNApiService
|
||||
[ApiEndpointParam.LastSyncToken]: lastSyncToken,
|
||||
[ApiEndpointParam.PaginationToken]: paginationToken,
|
||||
[ApiEndpointParam.SyncDlLimit]: limit,
|
||||
[ApiEndpointParam.SharedVaultUuids]: sharedVaultUuids,
|
||||
})
|
||||
const response = await this.httpService.post<RawSyncResponse>(path, params, this.getSessionAccessToken())
|
||||
|
||||
@@ -686,12 +688,12 @@ export class SNApiService
|
||||
})
|
||||
}
|
||||
|
||||
public async createFileValetToken(
|
||||
public async createUserFileValetToken(
|
||||
remoteIdentifier: string,
|
||||
operation: 'write' | 'read' | 'delete',
|
||||
operation: ValetTokenOperation,
|
||||
unencryptedFileSize?: number,
|
||||
): Promise<string | ClientDisplayableError> {
|
||||
const url = joinPaths(this.host, Paths.v1.createFileValetToken)
|
||||
const url = joinPaths(this.host, Paths.v1.createUserFileValetToken)
|
||||
|
||||
const params: CreateValetTokenPayload = {
|
||||
operation,
|
||||
@@ -717,40 +719,60 @@ export class SNApiService
|
||||
return response.data?.valetToken
|
||||
}
|
||||
|
||||
public async startUploadSession(apiToken: string): Promise<HttpResponse<StartUploadSessionResponse>> {
|
||||
const url = joinPaths(this.getFilesHost(), Paths.v1.startUploadSession)
|
||||
public async startUploadSession(
|
||||
valetToken: string,
|
||||
ownershipType: FileOwnershipType,
|
||||
): Promise<HttpResponse<StartUploadSessionResponse>> {
|
||||
const url = joinPaths(
|
||||
this.getFilesHost(),
|
||||
ownershipType === 'user' ? Paths.v1.startUploadSession : Paths.v1.startSharedVaultUploadSession,
|
||||
)
|
||||
|
||||
return this.tokenRefreshableRequest({
|
||||
verb: HttpVerb.Post,
|
||||
url,
|
||||
customHeaders: [{ key: 'x-valet-token', value: apiToken }],
|
||||
customHeaders: [{ key: 'x-valet-token', value: valetToken }],
|
||||
fallbackErrorMessage: Strings.Network.Files.FailedStartUploadSession,
|
||||
})
|
||||
}
|
||||
|
||||
public async deleteFile(apiToken: string): Promise<HttpResponse<StartUploadSessionResponse>> {
|
||||
const url = joinPaths(this.getFilesHost(), Paths.v1.deleteFile)
|
||||
public async deleteFile(
|
||||
valetToken: string,
|
||||
ownershipType: FileOwnershipType,
|
||||
): Promise<HttpResponse<StartUploadSessionResponse>> {
|
||||
const url = joinPaths(
|
||||
this.getFilesHost(),
|
||||
ownershipType === 'user' ? Paths.v1.deleteFile : Paths.v1.deleteSharedVaultFile,
|
||||
)
|
||||
|
||||
return this.tokenRefreshableRequest({
|
||||
verb: HttpVerb.Delete,
|
||||
url,
|
||||
customHeaders: [{ key: 'x-valet-token', value: apiToken }],
|
||||
customHeaders: [{ key: 'x-valet-token', value: valetToken }],
|
||||
fallbackErrorMessage: Strings.Network.Files.FailedDeleteFile,
|
||||
})
|
||||
}
|
||||
|
||||
public async uploadFileBytes(apiToken: string, chunkId: number, encryptedBytes: Uint8Array): Promise<boolean> {
|
||||
public async uploadFileBytes(
|
||||
valetToken: string,
|
||||
ownershipType: FileOwnershipType,
|
||||
chunkId: number,
|
||||
encryptedBytes: Uint8Array,
|
||||
): Promise<boolean> {
|
||||
if (chunkId === 0) {
|
||||
throw Error('chunkId must start with 1')
|
||||
}
|
||||
const url = joinPaths(this.getFilesHost(), Paths.v1.uploadFileChunk)
|
||||
const url = joinPaths(
|
||||
this.getFilesHost(),
|
||||
ownershipType === 'user' ? Paths.v1.uploadFileChunk : Paths.v1.uploadSharedVaultFileChunk,
|
||||
)
|
||||
|
||||
const response = await this.tokenRefreshableRequest<UploadFileChunkResponse>({
|
||||
verb: HttpVerb.Post,
|
||||
url,
|
||||
rawBytes: encryptedBytes,
|
||||
customHeaders: [
|
||||
{ key: 'x-valet-token', value: apiToken },
|
||||
{ key: 'x-valet-token', value: valetToken },
|
||||
{ key: 'x-chunk-id', value: chunkId.toString() },
|
||||
{ key: 'Content-Type', value: 'application/octet-stream' },
|
||||
],
|
||||
@@ -764,13 +786,16 @@ export class SNApiService
|
||||
return response.data.success
|
||||
}
|
||||
|
||||
public async closeUploadSession(apiToken: string): Promise<boolean> {
|
||||
const url = joinPaths(this.getFilesHost(), Paths.v1.closeUploadSession)
|
||||
public async closeUploadSession(valetToken: string, ownershipType: FileOwnershipType): Promise<boolean> {
|
||||
const url = joinPaths(
|
||||
this.getFilesHost(),
|
||||
ownershipType === 'user' ? Paths.v1.closeUploadSession : Paths.v1.closeSharedVaultUploadSession,
|
||||
)
|
||||
|
||||
const response = await this.tokenRefreshableRequest<CloseUploadSessionResponse>({
|
||||
verb: HttpVerb.Post,
|
||||
url,
|
||||
customHeaders: [{ key: 'x-valet-token', value: apiToken }],
|
||||
customHeaders: [{ key: 'x-valet-token', value: valetToken }],
|
||||
fallbackErrorMessage: Strings.Network.Files.FailedCloseUploadSession,
|
||||
})
|
||||
|
||||
@@ -781,33 +806,61 @@ export class SNApiService
|
||||
return response.data.success
|
||||
}
|
||||
|
||||
public getFilesDownloadUrl(): string {
|
||||
return joinPaths(this.getFilesHost(), Paths.v1.downloadFileChunk)
|
||||
public async moveFile(valetToken: string): Promise<boolean> {
|
||||
const url = joinPaths(this.getFilesHost(), Paths.v1.moveFile)
|
||||
|
||||
const response = await this.tokenRefreshableRequest<MoveFileResponse>({
|
||||
verb: HttpVerb.Post,
|
||||
url,
|
||||
customHeaders: [{ key: 'x-valet-token', value: valetToken }],
|
||||
fallbackErrorMessage: Strings.Network.Files.FailedCloseUploadSession,
|
||||
})
|
||||
|
||||
if (isErrorResponse(response)) {
|
||||
return false
|
||||
}
|
||||
|
||||
return response.data.success
|
||||
}
|
||||
|
||||
public async downloadFile(
|
||||
file: { encryptedChunkSizes: FileContent['encryptedChunkSizes'] },
|
||||
chunkIndex = 0,
|
||||
apiToken: string,
|
||||
contentRangeStart: number,
|
||||
onBytesReceived: (bytes: Uint8Array) => Promise<void>,
|
||||
): Promise<ClientDisplayableError | undefined> {
|
||||
const url = this.getFilesDownloadUrl()
|
||||
public getFilesDownloadUrl(ownershipType: FileOwnershipType): string {
|
||||
if (ownershipType === 'user') {
|
||||
return joinPaths(this.getFilesHost(), Paths.v1.downloadFileChunk)
|
||||
} else if (ownershipType === 'shared-vault') {
|
||||
return joinPaths(this.getFilesHost(), Paths.v1.downloadSharedVaultFileChunk)
|
||||
} else {
|
||||
throw Error('Invalid download type')
|
||||
}
|
||||
}
|
||||
|
||||
public async downloadFile({
|
||||
file,
|
||||
chunkIndex,
|
||||
valetToken,
|
||||
ownershipType,
|
||||
contentRangeStart,
|
||||
onBytesReceived,
|
||||
}: DownloadFileParams): Promise<ClientDisplayableError | undefined> {
|
||||
const url = this.getFilesDownloadUrl(ownershipType)
|
||||
const pullChunkSize = file.encryptedChunkSizes[chunkIndex]
|
||||
|
||||
const response = await this.tokenRefreshableRequest<DownloadFileChunkResponse>({
|
||||
const request: HttpRequest = {
|
||||
verb: HttpVerb.Get,
|
||||
url,
|
||||
customHeaders: [
|
||||
{ key: 'x-valet-token', value: apiToken },
|
||||
{ key: 'x-valet-token', value: valetToken },
|
||||
{
|
||||
key: 'x-chunk-size',
|
||||
value: pullChunkSize.toString(),
|
||||
},
|
||||
{ key: 'range', value: `bytes=${contentRangeStart}-` },
|
||||
],
|
||||
fallbackErrorMessage: Strings.Network.Files.FailedDownloadFileChunk,
|
||||
responseType: 'arraybuffer',
|
||||
}
|
||||
|
||||
const response = await this.tokenRefreshableRequest<DownloadFileChunkResponse>({
|
||||
...request,
|
||||
fallbackErrorMessage: Strings.Network.Files.FailedDownloadFileChunk,
|
||||
})
|
||||
|
||||
if (isErrorResponse(response)) {
|
||||
@@ -833,7 +886,14 @@ export class SNApiService
|
||||
await onBytesReceived(bytesReceived)
|
||||
|
||||
if (rangeEnd < totalSize - 1) {
|
||||
return this.downloadFile(file, ++chunkIndex, apiToken, rangeStart + pullChunkSize, onBytesReceived)
|
||||
return this.downloadFile({
|
||||
file,
|
||||
chunkIndex: ++chunkIndex,
|
||||
valetToken,
|
||||
ownershipType,
|
||||
contentRangeStart: rangeStart + pullChunkSize,
|
||||
onBytesReceived,
|
||||
})
|
||||
}
|
||||
|
||||
return undefined
|
||||
@@ -889,19 +949,4 @@ export class SNApiService
|
||||
|
||||
return this.session.accessToken
|
||||
}
|
||||
|
||||
override getDiagnostics(): Promise<DiagnosticInfo | undefined> {
|
||||
return Promise.resolve({
|
||||
api: {
|
||||
hasSession: this.session != undefined,
|
||||
user: this.user,
|
||||
registering: this.registering,
|
||||
authenticating: this.authenticating,
|
||||
changing: this.changing,
|
||||
refreshingSession: this.refreshingSession,
|
||||
filesHost: this.filesHost,
|
||||
host: this.host,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,22 @@
|
||||
const FilesPaths = {
|
||||
closeUploadSession: '/v1/files/upload/close-session',
|
||||
createFileValetToken: '/v1/files/valet-tokens',
|
||||
createUserFileValetToken: '/v1/files/valet-tokens',
|
||||
deleteFile: '/v1/files',
|
||||
downloadFileChunk: '/v1/files',
|
||||
downloadVaultFileChunk: '/v1/vaults/files',
|
||||
startUploadSession: '/v1/files/upload/create-session',
|
||||
uploadFileChunk: '/v1/files/upload/chunk',
|
||||
}
|
||||
|
||||
const SharedVaultFilesPaths = {
|
||||
closeSharedVaultUploadSession: '/v1/shared-vault/files/upload/close-session',
|
||||
deleteSharedVaultFile: '/v1/shared-vault/files',
|
||||
downloadSharedVaultFileChunk: '/v1/shared-vault/files',
|
||||
startSharedVaultUploadSession: '/v1/shared-vault/files/upload/create-session',
|
||||
uploadSharedVaultFileChunk: '/v1/shared-vault/files/upload/chunk',
|
||||
moveFile: '/v1/shared-vault/files/move',
|
||||
}
|
||||
|
||||
const UserPaths = {
|
||||
changeCredentials: (userUuid: string) => `/v1/users/${userUuid}/attributes/credentials`,
|
||||
deleteAccount: (userUuid: string) => `/v1/users/${userUuid}`,
|
||||
@@ -58,6 +68,7 @@ const ListedPaths = {
|
||||
export const Paths = {
|
||||
v1: {
|
||||
...FilesPaths,
|
||||
...SharedVaultFilesPaths,
|
||||
...ItemsPaths,
|
||||
...ListedPaths,
|
||||
...SettingsPaths,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Challenge, ChallengeValue, ChallengeArtifacts } from '@standardnotes/services'
|
||||
import { Challenge, ChallengeValue, ChallengeArtifacts, ChallengeValueCallback } from '@standardnotes/services'
|
||||
import { ChallengeResponse } from './ChallengeResponse'
|
||||
import { removeFromArray } from '@standardnotes/utils'
|
||||
import { ValueCallback } from './ChallengeService'
|
||||
|
||||
/**
|
||||
* A challenge operation stores user-submitted values and callbacks.
|
||||
@@ -15,8 +14,8 @@ export class ChallengeOperation {
|
||||
|
||||
constructor(
|
||||
public challenge: Challenge,
|
||||
public onValidValue: ValueCallback,
|
||||
public onInvalidValue: ValueCallback,
|
||||
public onValidValue: ChallengeValueCallback,
|
||||
public onInvalidValue: ChallengeValueCallback,
|
||||
public onNonvalidatedSubmit: (response: ChallengeResponse) => void,
|
||||
public onComplete: (response: ChallengeResponse) => void,
|
||||
public onCancel: () => void,
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
ChallengePrompt,
|
||||
EncryptionService,
|
||||
ChallengeStrings,
|
||||
ChallengeObserver,
|
||||
} from '@standardnotes/services'
|
||||
import { ChallengeResponse } from './ChallengeResponse'
|
||||
import { ChallengeOperation } from './ChallengeOperation'
|
||||
@@ -25,16 +26,6 @@ type ChallengeValidationResponse = {
|
||||
artifacts?: ChallengeArtifacts
|
||||
}
|
||||
|
||||
export type ValueCallback = (value: ChallengeValue) => void
|
||||
|
||||
export type ChallengeObserver = {
|
||||
onValidValue?: ValueCallback
|
||||
onInvalidValue?: ValueCallback
|
||||
onNonvalidatedSubmit?: (response: ChallengeResponse) => void
|
||||
onComplete?: (response: ChallengeResponse) => void
|
||||
onCancel?: () => void
|
||||
}
|
||||
|
||||
const clearChallengeObserver = (observer: ChallengeObserver) => {
|
||||
observer.onCancel = undefined
|
||||
observer.onComplete = undefined
|
||||
@@ -112,7 +103,7 @@ export class ChallengeService extends AbstractService implements ChallengeServic
|
||||
return value.value as string
|
||||
}
|
||||
|
||||
async promptForAccountPassword(): Promise<boolean> {
|
||||
async promptForAccountPassword(): Promise<string | null> {
|
||||
if (!this.protocolService.hasAccount()) {
|
||||
throw Error('Requiring account password for challenge with no account')
|
||||
}
|
||||
@@ -126,11 +117,7 @@ export class ChallengeService extends AbstractService implements ChallengeServic
|
||||
),
|
||||
)
|
||||
|
||||
if (response) {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
return response?.getValueForType(ChallengeValidation.AccountPassword)?.value as string
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -175,7 +162,7 @@ export class ChallengeService extends AbstractService implements ChallengeServic
|
||||
return this.protocolService.isPasscodeLocked()
|
||||
}
|
||||
|
||||
public addChallengeObserver(challenge: Challenge, observer: ChallengeObserver) {
|
||||
public addChallengeObserver(challenge: Challenge, observer: ChallengeObserver): () => void {
|
||||
const observers = this.challengeObservers[challenge.id] || []
|
||||
|
||||
observers.push(observer)
|
||||
@@ -303,11 +290,11 @@ export class ChallengeService extends AbstractService implements ChallengeServic
|
||||
}
|
||||
|
||||
public setValidationStatusForChallenge(
|
||||
challenge: Challenge,
|
||||
challenge: ChallengeInterface,
|
||||
value: ChallengeValue,
|
||||
valid: boolean,
|
||||
artifacts?: ChallengeArtifacts,
|
||||
) {
|
||||
): void {
|
||||
const operation = this.getChallengeOperation(challenge)
|
||||
operation.setValueStatus(value, valid, artifacts)
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
InternalEventBusInterface,
|
||||
AlertService,
|
||||
DeviceInterface,
|
||||
MutatorClientInterface,
|
||||
} from '@standardnotes/services'
|
||||
import { ItemManager } from '@Lib/Services/Items/ItemManager'
|
||||
import { SNFeaturesService } from '@Lib/Services/Features/FeaturesService'
|
||||
@@ -27,6 +28,7 @@ import { SNSyncService } from '../Sync/SyncService'
|
||||
|
||||
describe('featuresService', () => {
|
||||
let itemManager: ItemManager
|
||||
let mutator: MutatorClientInterface
|
||||
let featureService: SNFeaturesService
|
||||
let alertService: AlertService
|
||||
let syncService: SNSyncService
|
||||
@@ -52,6 +54,7 @@ describe('featuresService', () => {
|
||||
|
||||
const manager = new SNComponentManager(
|
||||
itemManager,
|
||||
mutator,
|
||||
syncService,
|
||||
featureService,
|
||||
prefsService,
|
||||
@@ -71,12 +74,14 @@ describe('featuresService', () => {
|
||||
|
||||
itemManager = {} as jest.Mocked<ItemManager>
|
||||
itemManager.getItems = jest.fn().mockReturnValue([])
|
||||
itemManager.createItem = jest.fn()
|
||||
itemManager.changeComponent = jest.fn().mockReturnValue({} as jest.Mocked<GenericItem>)
|
||||
itemManager.setItemsToBeDeleted = jest.fn()
|
||||
itemManager.addObserver = jest.fn()
|
||||
itemManager.changeItem = jest.fn()
|
||||
itemManager.changeFeatureRepo = jest.fn()
|
||||
|
||||
mutator = {} as jest.Mocked<MutatorClientInterface>
|
||||
mutator.createItem = jest.fn()
|
||||
mutator.changeComponent = jest.fn().mockReturnValue({} as jest.Mocked<GenericItem>)
|
||||
mutator.setItemsToBeDeleted = jest.fn()
|
||||
mutator.changeItem = jest.fn()
|
||||
mutator.changeFeatureRepo = jest.fn()
|
||||
|
||||
featureService = {} as jest.Mocked<SNFeaturesService>
|
||||
|
||||
|
||||
@@ -39,6 +39,7 @@ import {
|
||||
AlertService,
|
||||
DeviceInterface,
|
||||
isMobileDevice,
|
||||
MutatorClientInterface,
|
||||
} from '@standardnotes/services'
|
||||
|
||||
const DESKTOP_URL_PREFIX = 'sn://'
|
||||
@@ -78,6 +79,7 @@ export class SNComponentManager
|
||||
|
||||
constructor(
|
||||
private itemManager: ItemManager,
|
||||
private mutator: MutatorClientInterface,
|
||||
private syncService: SNSyncService,
|
||||
private featuresService: SNFeaturesService,
|
||||
private preferencesSerivce: SNPreferencesService,
|
||||
@@ -162,6 +164,7 @@ export class SNComponentManager
|
||||
const viewer = new ComponentViewer(
|
||||
component,
|
||||
this.itemManager,
|
||||
this.mutator,
|
||||
this.syncService,
|
||||
this.alertService,
|
||||
this.preferencesSerivce,
|
||||
@@ -482,7 +485,7 @@ export class SNComponentManager
|
||||
}
|
||||
}
|
||||
|
||||
await this.itemManager.changeItem(component, (m) => {
|
||||
await this.mutator.changeItem(component, (m) => {
|
||||
const mutator = m as ComponentMutator
|
||||
mutator.permissions = componentPermissions
|
||||
})
|
||||
@@ -546,14 +549,14 @@ export class SNComponentManager
|
||||
|
||||
const theme = this.findComponent(uuid) as SNTheme
|
||||
if (theme.active) {
|
||||
await this.itemManager.changeComponent(theme, (mutator) => {
|
||||
await this.mutator.changeComponent(theme, (mutator) => {
|
||||
mutator.active = false
|
||||
})
|
||||
} else {
|
||||
const activeThemes = this.getActiveThemes()
|
||||
|
||||
/* Activate current before deactivating others, so as not to flicker */
|
||||
await this.itemManager.changeComponent(theme, (mutator) => {
|
||||
await this.mutator.changeComponent(theme, (mutator) => {
|
||||
mutator.active = true
|
||||
})
|
||||
|
||||
@@ -562,13 +565,15 @@ export class SNComponentManager
|
||||
await sleep(10)
|
||||
for (const candidate of activeThemes) {
|
||||
if (candidate && !candidate.isLayerable()) {
|
||||
await this.itemManager.changeComponent(candidate, (mutator) => {
|
||||
await this.mutator.changeComponent(candidate, (mutator) => {
|
||||
mutator.active = false
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void this.syncService.sync()
|
||||
}
|
||||
|
||||
async toggleComponent(uuid: UuidString): Promise<void> {
|
||||
@@ -580,9 +585,11 @@ export class SNComponentManager
|
||||
return
|
||||
}
|
||||
|
||||
await this.itemManager.changeComponent(component, (mutator) => {
|
||||
await this.mutator.changeComponent(component, (mutator) => {
|
||||
mutator.active = !(mutator.getItem() as SNComponent).active
|
||||
})
|
||||
|
||||
void this.syncService.sync()
|
||||
}
|
||||
|
||||
isComponentActive(component: SNComponent): boolean {
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
FeatureStatus,
|
||||
FeaturesEvent,
|
||||
AlertService,
|
||||
MutatorClientInterface,
|
||||
} from '@standardnotes/services'
|
||||
import { SNFeaturesService } from '@Lib/Services'
|
||||
import {
|
||||
@@ -109,6 +110,7 @@ export class ComponentViewer implements ComponentViewerInterface {
|
||||
constructor(
|
||||
public readonly component: SNComponent,
|
||||
private itemManager: ItemManager,
|
||||
private mutator: MutatorClientInterface,
|
||||
private syncService: SNSyncService,
|
||||
private alertService: AlertService,
|
||||
private preferencesSerivce: SNPreferencesService,
|
||||
@@ -719,7 +721,7 @@ export class ComponentViewer implements ComponentViewerInterface {
|
||||
...contextualPayload,
|
||||
})
|
||||
const template = CreateDecryptedItemFromPayload(payload)
|
||||
await this.itemManager.insertItem(template)
|
||||
await this.mutator.insertItem(template)
|
||||
} else {
|
||||
if (contextualPayload.content_type !== item.content_type) {
|
||||
throw Error('Extension is trying to modify content type of item.')
|
||||
@@ -727,7 +729,7 @@ export class ComponentViewer implements ComponentViewerInterface {
|
||||
}
|
||||
}
|
||||
|
||||
await this.itemManager.changeItems(
|
||||
await this.mutator.changeItems(
|
||||
items.filter(isNotUndefined),
|
||||
(mutator) => {
|
||||
const contextualPayload = sureSearchArray(contextualPayloads, {
|
||||
@@ -798,9 +800,9 @@ export class ComponentViewer implements ComponentViewerInterface {
|
||||
})
|
||||
|
||||
const template = CreateDecryptedItemFromPayload(payload)
|
||||
const item = await this.itemManager.insertItem(template)
|
||||
const item = await this.mutator.insertItem(template)
|
||||
|
||||
await this.itemManager.changeItem(
|
||||
await this.mutator.changeItem(
|
||||
item,
|
||||
(mutator) => {
|
||||
if (responseItem.clientData) {
|
||||
@@ -857,7 +859,7 @@ export class ComponentViewer implements ComponentViewerInterface {
|
||||
void this.alertService.alert('The item you are trying to delete cannot be found.')
|
||||
continue
|
||||
}
|
||||
await this.itemManager.setItemToBeDeleted(item, PayloadEmitSource.ComponentRetrieved)
|
||||
await this.mutator.setItemToBeDeleted(item, PayloadEmitSource.ComponentRetrieved)
|
||||
}
|
||||
|
||||
void this.syncService.sync()
|
||||
@@ -875,7 +877,7 @@ export class ComponentViewer implements ComponentViewerInterface {
|
||||
handleSetComponentDataMessage(message: ComponentMessage): void {
|
||||
const noPermissionsRequired: ComponentPermission[] = []
|
||||
this.componentManagerFunctions.runWithPermissions(this.component.uuid, noPermissionsRequired, async () => {
|
||||
await this.itemManager.changeComponent(this.component, (mutator) => {
|
||||
await this.mutator.changeComponent(this.component, (mutator) => {
|
||||
mutator.componentData = message.data.componentData || {}
|
||||
})
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
FeaturesEvent,
|
||||
FeatureStatus,
|
||||
InternalEventBusInterface,
|
||||
MutatorClientInterface,
|
||||
StorageKey,
|
||||
UserService,
|
||||
} from '@standardnotes/services'
|
||||
@@ -25,6 +26,7 @@ describe('featuresService', () => {
|
||||
let storageService: DiskStorageService
|
||||
let apiService: SNApiService
|
||||
let itemManager: ItemManager
|
||||
let mutator: MutatorClientInterface
|
||||
let webSocketsService: SNWebSocketsService
|
||||
let settingsService: SNSettingsService
|
||||
let userService: UserService
|
||||
@@ -46,6 +48,7 @@ describe('featuresService', () => {
|
||||
storageService,
|
||||
apiService,
|
||||
itemManager,
|
||||
mutator,
|
||||
webSocketsService,
|
||||
settingsService,
|
||||
userService,
|
||||
@@ -95,13 +98,15 @@ describe('featuresService', () => {
|
||||
|
||||
itemManager = {} as jest.Mocked<ItemManager>
|
||||
itemManager.getItems = jest.fn().mockReturnValue(items)
|
||||
itemManager.createItem = jest.fn()
|
||||
itemManager.createTemplateItem = jest.fn().mockReturnValue({})
|
||||
itemManager.changeComponent = jest.fn().mockReturnValue({} as jest.Mocked<ItemInterface>)
|
||||
itemManager.setItemsToBeDeleted = jest.fn()
|
||||
itemManager.addObserver = jest.fn()
|
||||
itemManager.changeItem = jest.fn()
|
||||
itemManager.changeFeatureRepo = jest.fn()
|
||||
|
||||
mutator = {} as jest.Mocked<MutatorClientInterface>
|
||||
mutator.createItem = jest.fn()
|
||||
mutator.changeComponent = jest.fn().mockReturnValue({} as jest.Mocked<ItemInterface>)
|
||||
mutator.setItemsToBeDeleted = jest.fn()
|
||||
mutator.changeItem = jest.fn()
|
||||
mutator.changeFeatureRepo = jest.fn()
|
||||
|
||||
webSocketsService = {} as jest.Mocked<SNWebSocketsService>
|
||||
webSocketsService.addEventObserver = jest.fn()
|
||||
@@ -173,7 +178,7 @@ describe('featuresService', () => {
|
||||
const { didChangeRoles } = await featuresService.updateOnlineRoles(newRoles)
|
||||
await featuresService.fetchFeatures('123', didChangeRoles)
|
||||
|
||||
expect(itemManager.createItem).not.toHaveBeenCalled()
|
||||
expect(mutator.createItem).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('does create a component for enabled experimental feature', async () => {
|
||||
@@ -196,7 +201,7 @@ describe('featuresService', () => {
|
||||
const { didChangeRoles } = await featuresService.updateOnlineRoles(newRoles)
|
||||
await featuresService.fetchFeatures('123', didChangeRoles)
|
||||
|
||||
expect(itemManager.createItem).toHaveBeenCalled()
|
||||
expect(mutator.createItem).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -300,8 +305,8 @@ describe('featuresService', () => {
|
||||
const { didChangeRoles } = await featuresService.updateOnlineRoles(newRoles)
|
||||
await featuresService.fetchFeatures('123', didChangeRoles)
|
||||
|
||||
expect(itemManager.createItem).toHaveBeenCalledTimes(2)
|
||||
expect(itemManager.createItem).toHaveBeenCalledWith(
|
||||
expect(mutator.createItem).toHaveBeenCalledTimes(2)
|
||||
expect(mutator.createItem).toHaveBeenCalledWith(
|
||||
ContentType.Theme,
|
||||
expect.objectContaining({
|
||||
package_info: expect.objectContaining({
|
||||
@@ -312,7 +317,7 @@ describe('featuresService', () => {
|
||||
}),
|
||||
true,
|
||||
)
|
||||
expect(itemManager.createItem).toHaveBeenCalledWith(
|
||||
expect(mutator.createItem).toHaveBeenCalledWith(
|
||||
ContentType.Component,
|
||||
expect.objectContaining({
|
||||
package_info: expect.objectContaining({
|
||||
@@ -346,7 +351,7 @@ describe('featuresService', () => {
|
||||
const { didChangeRoles } = await featuresService.updateOnlineRoles(newRoles)
|
||||
await featuresService.fetchFeatures('123', didChangeRoles)
|
||||
|
||||
expect(itemManager.changeComponent).toHaveBeenCalledWith(existingItem, expect.any(Function))
|
||||
expect(mutator.changeComponent).toHaveBeenCalledWith(existingItem, expect.any(Function))
|
||||
})
|
||||
|
||||
it('creates items for expired components if they do not exist', async () => {
|
||||
@@ -373,7 +378,7 @@ describe('featuresService', () => {
|
||||
const { didChangeRoles } = await featuresService.updateOnlineRoles(newRoles)
|
||||
await featuresService.fetchFeatures('123', didChangeRoles)
|
||||
|
||||
expect(itemManager.createItem).toHaveBeenCalledWith(
|
||||
expect(mutator.createItem).toHaveBeenCalledWith(
|
||||
ContentType.Component,
|
||||
expect.objectContaining({
|
||||
package_info: expect.objectContaining({
|
||||
@@ -403,7 +408,7 @@ describe('featuresService', () => {
|
||||
const now = new Date()
|
||||
const yesterday = now.setDate(now.getDate() - 1)
|
||||
|
||||
itemManager.changeComponent = jest.fn().mockReturnValue(existingItem)
|
||||
mutator.changeComponent = jest.fn().mockReturnValue(existingItem)
|
||||
storageService.getValue = jest.fn().mockReturnValue(roles)
|
||||
itemManager.getItems = jest.fn().mockReturnValue([existingItem])
|
||||
apiService.getUserFeatures = jest.fn().mockReturnValue({
|
||||
@@ -422,7 +427,7 @@ describe('featuresService', () => {
|
||||
const { didChangeRoles } = await featuresService.updateOnlineRoles(newRoles)
|
||||
await featuresService.fetchFeatures('123', didChangeRoles)
|
||||
|
||||
expect(itemManager.setItemsToBeDeleted).toHaveBeenCalledWith([existingItem])
|
||||
expect(mutator.setItemsToBeDeleted).toHaveBeenCalledWith([existingItem])
|
||||
})
|
||||
|
||||
it('does not create an item for a feature without content type', async () => {
|
||||
@@ -447,7 +452,7 @@ describe('featuresService', () => {
|
||||
const { didChangeRoles } = await featuresService.updateOnlineRoles(newRoles)
|
||||
await featuresService.fetchFeatures('123', didChangeRoles)
|
||||
|
||||
expect(itemManager.createItem).not.toHaveBeenCalled()
|
||||
expect(mutator.createItem).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('does not create an item for deprecated features', async () => {
|
||||
@@ -472,7 +477,7 @@ describe('featuresService', () => {
|
||||
const { didChangeRoles } = await featuresService.updateOnlineRoles(newRoles)
|
||||
await featuresService.fetchFeatures('123', didChangeRoles)
|
||||
|
||||
expect(itemManager.createItem).not.toHaveBeenCalled()
|
||||
expect(mutator.createItem).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('does nothing after initial update if roles have not changed', async () => {
|
||||
|
||||
@@ -48,6 +48,7 @@ import {
|
||||
SetOfflineFeaturesFunctionResponse,
|
||||
StorageKey,
|
||||
UserService,
|
||||
MutatorClientInterface,
|
||||
} from '@standardnotes/services'
|
||||
import { FeatureIdentifier } from '@standardnotes/features'
|
||||
|
||||
@@ -72,7 +73,8 @@ export class SNFeaturesService
|
||||
private storageService: DiskStorageService,
|
||||
private apiService: SNApiService,
|
||||
private itemManager: ItemManager,
|
||||
private webSocketsService: SNWebSocketsService,
|
||||
private mutator: MutatorClientInterface,
|
||||
webSocketsService: SNWebSocketsService,
|
||||
private settingsService: SNSettingsService,
|
||||
private userService: UserService,
|
||||
private syncService: SNSyncService,
|
||||
@@ -188,7 +190,7 @@ export class SNFeaturesService
|
||||
if (existingItem) {
|
||||
const hasChange = JSON.stringify(feature) !== JSON.stringify(existingItem.package_info)
|
||||
if (hasChange) {
|
||||
await this.itemManager.changeComponent(existingItem, (mutator) => {
|
||||
await this.mutator.changeComponent(existingItem, (mutator) => {
|
||||
mutator.package_info = feature
|
||||
})
|
||||
}
|
||||
@@ -196,7 +198,7 @@ export class SNFeaturesService
|
||||
continue
|
||||
}
|
||||
|
||||
await this.itemManager.createItem(
|
||||
await this.mutator.createItem(
|
||||
feature.content_type,
|
||||
this.componentContentForNativeFeatureDescription(feature),
|
||||
true,
|
||||
@@ -230,7 +232,7 @@ export class SNFeaturesService
|
||||
return
|
||||
}
|
||||
|
||||
void this.itemManager.setItemToBeDeleted(component).then(() => {
|
||||
void this.mutator.setItemToBeDeleted(component).then(() => {
|
||||
void this.syncService.sync()
|
||||
})
|
||||
void this.notifyEvent(FeaturesEvent.FeaturesUpdated)
|
||||
@@ -270,7 +272,7 @@ export class SNFeaturesService
|
||||
return result
|
||||
}
|
||||
|
||||
const offlineRepo = (await this.itemManager.createItem(
|
||||
const offlineRepo = (await this.mutator.createItem(
|
||||
ContentType.ExtensionRepo,
|
||||
FillItemContent({
|
||||
offlineFeaturesUrl: result.featuresUrl,
|
||||
@@ -298,7 +300,7 @@ export class SNFeaturesService
|
||||
public async deleteOfflineFeatureRepo(): Promise<void> {
|
||||
const repo = this.getOfflineRepo()
|
||||
if (repo) {
|
||||
await this.itemManager.setItemToBeDeleted(repo)
|
||||
await this.mutator.setItemToBeDeleted(repo)
|
||||
void this.syncService.sync()
|
||||
}
|
||||
await this.storageService.removeValue(StorageKey.UserFeatures)
|
||||
@@ -346,7 +348,7 @@ export class SNFeaturesService
|
||||
userKey,
|
||||
true,
|
||||
)
|
||||
await this.itemManager.changeFeatureRepo(item, (m) => {
|
||||
await this.mutator.changeFeatureRepo(item, (m) => {
|
||||
m.migratedToUserSetting = true
|
||||
})
|
||||
}
|
||||
@@ -371,7 +373,7 @@ export class SNFeaturesService
|
||||
const userKeyMatch = repoUrl.match(/\w{32,64}/)
|
||||
if (userKeyMatch && userKeyMatch.length > 0) {
|
||||
const userKey = userKeyMatch[0]
|
||||
const updatedRepo = await this.itemManager.changeFeatureRepo(item, (m) => {
|
||||
const updatedRepo = await this.mutator.changeFeatureRepo(item, (m) => {
|
||||
m.offlineFeaturesUrl = PROD_OFFLINE_FEATURES_URL
|
||||
m.offlineKey = userKey
|
||||
m.migratedToOfflineEntitlements = true
|
||||
@@ -647,7 +649,7 @@ export class SNFeaturesService
|
||||
}
|
||||
}
|
||||
|
||||
await this.itemManager.setItemsToBeDeleted(itemsToDelete)
|
||||
await this.mutator.setItemsToBeDeleted(itemsToDelete)
|
||||
|
||||
if (hasChanges) {
|
||||
void this.syncService.sync()
|
||||
@@ -704,7 +706,7 @@ export class SNFeaturesService
|
||||
const hasChange = hasChangeInPackageInfo || hasChangeInExpiration
|
||||
|
||||
if (hasChange) {
|
||||
resultingItem = await this.itemManager.changeComponent(existingItem, (mutator) => {
|
||||
resultingItem = await this.mutator.changeComponent(existingItem, (mutator) => {
|
||||
mutator.package_info = feature
|
||||
mutator.valid_until = featureExpiresAt
|
||||
})
|
||||
@@ -714,7 +716,7 @@ export class SNFeaturesService
|
||||
resultingItem = existingItem
|
||||
}
|
||||
} else if (!expired || feature.content_type === ContentType.Component) {
|
||||
resultingItem = (await this.itemManager.createItem(
|
||||
resultingItem = (await this.mutator.createItem(
|
||||
feature.content_type,
|
||||
this.componentContentForNativeFeatureDescription(feature),
|
||||
true,
|
||||
@@ -835,7 +837,7 @@ export class SNFeaturesService
|
||||
;(this.storageService as unknown) = undefined
|
||||
;(this.apiService as unknown) = undefined
|
||||
;(this.itemManager as unknown) = undefined
|
||||
;(this.webSocketsService as unknown) = undefined
|
||||
;(this.mutator as unknown) = undefined
|
||||
;(this.settingsService as unknown) = undefined
|
||||
;(this.userService as unknown) = undefined
|
||||
;(this.syncService as unknown) = undefined
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { ContentType } from '@standardnotes/common'
|
||||
import { InternalEventBusInterface, ItemRelationshipDirection } from '@standardnotes/services'
|
||||
import { AlertService, InternalEventBusInterface, ItemRelationshipDirection } from '@standardnotes/services'
|
||||
import { ItemManager } from './ItemManager'
|
||||
import { PayloadManager } from '../Payloads/PayloadManager'
|
||||
import { UuidGenerator } from '@standardnotes/utils'
|
||||
import { UuidGenerator, assert } from '@standardnotes/utils'
|
||||
import * as Models from '@standardnotes/models'
|
||||
import {
|
||||
DecryptedPayload,
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
SystemViewId,
|
||||
} from '@standardnotes/models'
|
||||
import { createNoteWithTitle } from '../../Spec/SpecUtils'
|
||||
import { MutatorService } from '../Mutator'
|
||||
|
||||
const setupRandomUuid = () => {
|
||||
UuidGenerator.SetGenerator(() => String(Math.random()))
|
||||
@@ -43,15 +44,11 @@ const LongTextPredicate = Models.predicateFromJson<Models.SNTag>({
|
||||
})
|
||||
|
||||
describe('itemManager', () => {
|
||||
let mutator: MutatorService
|
||||
let payloadManager: PayloadManager
|
||||
let itemManager: ItemManager
|
||||
let items: Models.DecryptedItemInterface[]
|
||||
let internalEventBus: InternalEventBusInterface
|
||||
|
||||
const createService = () => {
|
||||
return new ItemManager(payloadManager, internalEventBus)
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
setupRandomUuid()
|
||||
|
||||
@@ -59,16 +56,9 @@ describe('itemManager', () => {
|
||||
internalEventBus.publish = jest.fn()
|
||||
|
||||
payloadManager = new PayloadManager(internalEventBus)
|
||||
itemManager = new ItemManager(payloadManager, internalEventBus)
|
||||
|
||||
items = [] as jest.Mocked<Models.DecryptedItemInterface[]>
|
||||
itemManager = {} as jest.Mocked<ItemManager>
|
||||
itemManager.getItems = jest.fn().mockReturnValue(items)
|
||||
itemManager.createItem = jest.fn()
|
||||
itemManager.changeComponent = jest.fn().mockReturnValue({} as jest.Mocked<Models.DecryptedItemInterface>)
|
||||
itemManager.setItemsToBeDeleted = jest.fn()
|
||||
itemManager.addObserver = jest.fn()
|
||||
itemManager.changeItem = jest.fn()
|
||||
itemManager.changeFeatureRepo = jest.fn()
|
||||
mutator = new MutatorService(itemManager, payloadManager, {} as jest.Mocked<AlertService>, internalEventBus)
|
||||
})
|
||||
|
||||
const createTag = (title: string) => {
|
||||
@@ -99,8 +89,6 @@ describe('itemManager', () => {
|
||||
|
||||
describe('item emit', () => {
|
||||
it('deleted payloads should map to removed items', async () => {
|
||||
itemManager = createService()
|
||||
|
||||
const payload = new DeletedPayload({
|
||||
uuid: String(Math.random()),
|
||||
content_type: ContentType.Note,
|
||||
@@ -120,8 +108,6 @@ describe('itemManager', () => {
|
||||
})
|
||||
|
||||
it('decrypted items who become encrypted should be removed from ui', async () => {
|
||||
itemManager = createService()
|
||||
|
||||
const decrypted = new DecryptedPayload({
|
||||
uuid: String(Math.random()),
|
||||
content_type: ContentType.Note,
|
||||
@@ -154,11 +140,10 @@ describe('itemManager', () => {
|
||||
|
||||
describe('note display criteria', () => {
|
||||
it('viewing notes with tag', async () => {
|
||||
itemManager = createService()
|
||||
const tag = createTag('parent')
|
||||
const note = createNoteWithTitle('note')
|
||||
await itemManager.insertItems([tag, note])
|
||||
await itemManager.addTagToNote(note, tag, false)
|
||||
await mutator.insertItems([tag, note])
|
||||
await mutator.addTagToNote(note, tag, false)
|
||||
|
||||
itemManager.setPrimaryItemDisplayOptions({
|
||||
tags: [tag],
|
||||
@@ -171,21 +156,19 @@ describe('itemManager', () => {
|
||||
})
|
||||
|
||||
it('viewing trashed notes smart view should include archived notes', async () => {
|
||||
itemManager = createService()
|
||||
|
||||
const archivedNote = createNoteWithTitle('archived')
|
||||
const trashedNote = createNoteWithTitle('trashed')
|
||||
const archivedAndTrashedNote = createNoteWithTitle('archived&trashed')
|
||||
|
||||
await itemManager.insertItems([archivedNote, trashedNote, archivedAndTrashedNote])
|
||||
await mutator.insertItems([archivedNote, trashedNote, archivedAndTrashedNote])
|
||||
|
||||
await itemManager.changeItem<Models.NoteMutator>(archivedNote, (m) => {
|
||||
await mutator.changeItem<Models.NoteMutator>(archivedNote, (m) => {
|
||||
m.archived = true
|
||||
})
|
||||
await itemManager.changeItem<Models.NoteMutator>(trashedNote, (m) => {
|
||||
await mutator.changeItem<Models.NoteMutator>(trashedNote, (m) => {
|
||||
m.trashed = true
|
||||
})
|
||||
await itemManager.changeItem<Models.NoteMutator>(archivedAndTrashedNote, (m) => {
|
||||
await mutator.changeItem<Models.NoteMutator>(archivedAndTrashedNote, (m) => {
|
||||
m.trashed = true
|
||||
m.archived = true
|
||||
})
|
||||
@@ -206,58 +189,53 @@ describe('itemManager', () => {
|
||||
|
||||
describe('tag relationships', () => {
|
||||
it('updates parentId of child tag', async () => {
|
||||
itemManager = createService()
|
||||
const parent = createTag('parent')
|
||||
const child = createTag('child')
|
||||
await itemManager.insertItems([parent, child])
|
||||
await itemManager.setTagParent(parent, child)
|
||||
await mutator.insertItems([parent, child])
|
||||
await mutator.setTagParent(parent, child)
|
||||
|
||||
const changedChild = itemManager.findItem(child.uuid) as Models.SNTag
|
||||
expect(changedChild.parentId).toBe(parent.uuid)
|
||||
})
|
||||
|
||||
it('forbids a tag to be its own parent', async () => {
|
||||
itemManager = createService()
|
||||
const tag = createTag('tag')
|
||||
await itemManager.insertItems([tag])
|
||||
await mutator.insertItems([tag])
|
||||
|
||||
expect(() => itemManager.setTagParent(tag, tag)).toThrow()
|
||||
await expect(mutator.setTagParent(tag, tag)).rejects.toThrow()
|
||||
expect(itemManager.getTagParent(tag)).toBeUndefined()
|
||||
})
|
||||
|
||||
it('forbids a tag to be its own ancestor', async () => {
|
||||
itemManager = createService()
|
||||
const grandParent = createTag('grandParent')
|
||||
const parent = createTag('parent')
|
||||
const child = createTag('child')
|
||||
|
||||
await itemManager.insertItems([child, parent, grandParent])
|
||||
await itemManager.setTagParent(parent, child)
|
||||
await itemManager.setTagParent(grandParent, parent)
|
||||
await mutator.insertItems([child, parent, grandParent])
|
||||
await mutator.setTagParent(parent, child)
|
||||
await mutator.setTagParent(grandParent, parent)
|
||||
|
||||
expect(() => itemManager.setTagParent(child, grandParent)).toThrow()
|
||||
await expect(mutator.setTagParent(child, grandParent)).rejects.toThrow()
|
||||
expect(itemManager.getTagParent(grandParent)).toBeUndefined()
|
||||
})
|
||||
|
||||
it('getTagParent', async () => {
|
||||
itemManager = createService()
|
||||
const parent = createTag('parent')
|
||||
const child = createTag('child')
|
||||
await itemManager.insertItems([parent, child])
|
||||
await itemManager.setTagParent(parent, child)
|
||||
await mutator.insertItems([parent, child])
|
||||
await mutator.setTagParent(parent, child)
|
||||
|
||||
expect(itemManager.getTagParent(child)?.uuid).toBe(parent.uuid)
|
||||
})
|
||||
|
||||
it('findTagByTitleAndParent', async () => {
|
||||
itemManager = createService()
|
||||
const parent = createTag('name1')
|
||||
const child = createTag('childName')
|
||||
const duplicateNameChild = createTag('name1')
|
||||
|
||||
await itemManager.insertItems([parent, child, duplicateNameChild])
|
||||
await itemManager.setTagParent(parent, child)
|
||||
await itemManager.setTagParent(parent, duplicateNameChild)
|
||||
await mutator.insertItems([parent, child, duplicateNameChild])
|
||||
await mutator.setTagParent(parent, child)
|
||||
await mutator.setTagParent(parent, duplicateNameChild)
|
||||
|
||||
const a = itemManager.findTagByTitleAndParent('name1', undefined)
|
||||
const b = itemManager.findTagByTitleAndParent('name1', parent)
|
||||
@@ -270,16 +248,16 @@ describe('itemManager', () => {
|
||||
|
||||
it('findOrCreateTagByTitle', async () => {
|
||||
setupRandomUuid()
|
||||
itemManager = createService()
|
||||
|
||||
const parent = createTag('parent')
|
||||
const child = createTag('child')
|
||||
await itemManager.insertItems([parent, child])
|
||||
await itemManager.setTagParent(parent, child)
|
||||
await mutator.insertItems([parent, child])
|
||||
await mutator.setTagParent(parent, child)
|
||||
|
||||
const childA = await itemManager.findOrCreateTagByTitle('child')
|
||||
const childB = await itemManager.findOrCreateTagByTitle('child', parent)
|
||||
const childC = await itemManager.findOrCreateTagByTitle('child-bis', parent)
|
||||
const childD = await itemManager.findOrCreateTagByTitle('child-bis', parent)
|
||||
const childA = await mutator.findOrCreateTagByTitle({ title: 'child' })
|
||||
const childB = await mutator.findOrCreateTagByTitle({ title: 'child', parentItemToLookupUuidFor: parent })
|
||||
const childC = await mutator.findOrCreateTagByTitle({ title: 'child-bis', parentItemToLookupUuidFor: parent })
|
||||
const childD = await mutator.findOrCreateTagByTitle({ title: 'child-bis', parentItemToLookupUuidFor: parent })
|
||||
|
||||
expect(childA.uuid).not.toEqual(child.uuid)
|
||||
expect(childB.uuid).toEqual(child.uuid)
|
||||
@@ -292,17 +270,16 @@ describe('itemManager', () => {
|
||||
})
|
||||
|
||||
it('findOrCreateTagParentChain', async () => {
|
||||
itemManager = createService()
|
||||
const parent = createTag('parent')
|
||||
const child = createTag('child')
|
||||
|
||||
await itemManager.insertItems([parent, child])
|
||||
await itemManager.setTagParent(parent, child)
|
||||
await mutator.insertItems([parent, child])
|
||||
await mutator.setTagParent(parent, child)
|
||||
|
||||
const a = await itemManager.findOrCreateTagParentChain(['parent'])
|
||||
const b = await itemManager.findOrCreateTagParentChain(['parent', 'child'])
|
||||
const c = await itemManager.findOrCreateTagParentChain(['parent', 'child2'])
|
||||
const d = await itemManager.findOrCreateTagParentChain(['parent2', 'child1'])
|
||||
const a = await mutator.findOrCreateTagParentChain(['parent'])
|
||||
const b = await mutator.findOrCreateTagParentChain(['parent', 'child'])
|
||||
const c = await mutator.findOrCreateTagParentChain(['parent', 'child2'])
|
||||
const d = await mutator.findOrCreateTagParentChain(['parent2', 'child1'])
|
||||
|
||||
expect(a?.uuid).toEqual(parent.uuid)
|
||||
expect(b?.uuid).toEqual(child.uuid)
|
||||
@@ -317,15 +294,14 @@ describe('itemManager', () => {
|
||||
})
|
||||
|
||||
it('isAncestor', async () => {
|
||||
itemManager = createService()
|
||||
const grandParent = createTag('grandParent')
|
||||
const parent = createTag('parent')
|
||||
const child = createTag('child')
|
||||
const another = createTag('another')
|
||||
|
||||
await itemManager.insertItems([child, parent, grandParent, another])
|
||||
await itemManager.setTagParent(parent, child)
|
||||
await itemManager.setTagParent(grandParent, parent)
|
||||
await mutator.insertItems([child, parent, grandParent, another])
|
||||
await mutator.setTagParent(parent, child)
|
||||
await mutator.setTagParent(grandParent, parent)
|
||||
|
||||
expect(itemManager.isTagAncestor(grandParent, parent)).toEqual(true)
|
||||
expect(itemManager.isTagAncestor(grandParent, child)).toEqual(true)
|
||||
@@ -341,28 +317,26 @@ describe('itemManager', () => {
|
||||
})
|
||||
|
||||
it('unsetTagRelationship', async () => {
|
||||
itemManager = createService()
|
||||
const parent = createTag('parent')
|
||||
const child = createTag('child')
|
||||
await itemManager.insertItems([parent, child])
|
||||
await itemManager.setTagParent(parent, child)
|
||||
await mutator.insertItems([parent, child])
|
||||
await mutator.setTagParent(parent, child)
|
||||
expect(itemManager.getTagParent(child)?.uuid).toBe(parent.uuid)
|
||||
|
||||
await itemManager.unsetTagParent(child)
|
||||
await mutator.unsetTagParent(child)
|
||||
|
||||
expect(itemManager.getTagParent(child)).toBeUndefined()
|
||||
})
|
||||
|
||||
it('getTagParentChain', async () => {
|
||||
itemManager = createService()
|
||||
const greatGrandParent = createTag('greatGrandParent')
|
||||
const grandParent = createTag('grandParent')
|
||||
const parent = createTag('parent')
|
||||
const child = createTag('child')
|
||||
await itemManager.insertItems([greatGrandParent, grandParent, parent, child])
|
||||
await itemManager.setTagParent(parent, child)
|
||||
await itemManager.setTagParent(grandParent, parent)
|
||||
await itemManager.setTagParent(greatGrandParent, grandParent)
|
||||
await mutator.insertItems([greatGrandParent, grandParent, parent, child])
|
||||
await mutator.setTagParent(parent, child)
|
||||
await mutator.setTagParent(grandParent, parent)
|
||||
await mutator.setTagParent(greatGrandParent, grandParent)
|
||||
|
||||
const uuidChain = itemManager.getTagParentChain(child).map((tag) => tag.uuid)
|
||||
|
||||
@@ -371,18 +345,17 @@ describe('itemManager', () => {
|
||||
})
|
||||
|
||||
it('viewing notes for parent tag should not display notes of children', async () => {
|
||||
itemManager = createService()
|
||||
const parentTag = createTag('parent')
|
||||
const childTag = createTag('child')
|
||||
await itemManager.insertItems([parentTag, childTag])
|
||||
await itemManager.setTagParent(parentTag, childTag)
|
||||
await mutator.insertItems([parentTag, childTag])
|
||||
await mutator.setTagParent(parentTag, childTag)
|
||||
|
||||
const parentNote = createNoteWithTitle('parentNote')
|
||||
const childNote = createNoteWithTitle('childNote')
|
||||
await itemManager.insertItems([parentNote, childNote])
|
||||
await mutator.insertItems([parentNote, childNote])
|
||||
|
||||
await itemManager.addTagToNote(parentNote, parentTag, false)
|
||||
await itemManager.addTagToNote(childNote, childTag, false)
|
||||
await mutator.addTagToNote(parentNote, parentTag, false)
|
||||
await mutator.addTagToNote(childNote, childTag, false)
|
||||
|
||||
itemManager.setPrimaryItemDisplayOptions({
|
||||
tags: [parentTag],
|
||||
@@ -397,7 +370,6 @@ describe('itemManager', () => {
|
||||
|
||||
describe('template items', () => {
|
||||
it('create template item', async () => {
|
||||
itemManager = createService()
|
||||
setupRandomUuid()
|
||||
|
||||
const item = await itemManager.createTemplateItem(ContentType.Note, {
|
||||
@@ -412,7 +384,6 @@ describe('itemManager', () => {
|
||||
})
|
||||
|
||||
it('isTemplateItem return the correct value', async () => {
|
||||
itemManager = createService()
|
||||
setupRandomUuid()
|
||||
|
||||
const item = await itemManager.createTemplateItem(ContentType.Note, {
|
||||
@@ -422,13 +393,12 @@ describe('itemManager', () => {
|
||||
|
||||
expect(itemManager.isTemplateItem(item)).toEqual(true)
|
||||
|
||||
await itemManager.insertItem(item)
|
||||
await mutator.insertItem(item)
|
||||
|
||||
expect(itemManager.isTemplateItem(item)).toEqual(false)
|
||||
})
|
||||
|
||||
it('isTemplateItem return the correct value for system smart views', () => {
|
||||
itemManager = createService()
|
||||
setupRandomUuid()
|
||||
|
||||
const [systemTag1, ...restOfSystemViews] = itemManager
|
||||
@@ -445,29 +415,27 @@ describe('itemManager', () => {
|
||||
|
||||
describe('tags', () => {
|
||||
it('lets me create a regular tag with a clear API', async () => {
|
||||
itemManager = createService()
|
||||
setupRandomUuid()
|
||||
|
||||
const tag = await itemManager.createTag('this is my new tag')
|
||||
const tag = await mutator.createTag({ title: 'this is my new tag' })
|
||||
|
||||
expect(tag).toBeTruthy()
|
||||
expect(itemManager.isTemplateItem(tag)).toEqual(false)
|
||||
})
|
||||
|
||||
it('should search tags correctly', async () => {
|
||||
itemManager = createService()
|
||||
setupRandomUuid()
|
||||
|
||||
const foo = await itemManager.createTag('foo[')
|
||||
const foobar = await itemManager.createTag('foo[bar]')
|
||||
const bar = await itemManager.createTag('bar[')
|
||||
const barfoo = await itemManager.createTag('bar[foo]')
|
||||
const fooDelimiter = await itemManager.createTag('bar.foo')
|
||||
const barFooDelimiter = await itemManager.createTag('baz.bar.foo')
|
||||
const fooAttached = await itemManager.createTag('Foo')
|
||||
const foo = await mutator.createTag({ title: 'foo[' })
|
||||
const foobar = await mutator.createTag({ title: 'foo[bar]' })
|
||||
const bar = await mutator.createTag({ title: 'bar[' })
|
||||
const barfoo = await mutator.createTag({ title: 'bar[foo]' })
|
||||
const fooDelimiter = await mutator.createTag({ title: 'bar.foo' })
|
||||
const barFooDelimiter = await mutator.createTag({ title: 'baz.bar.foo' })
|
||||
const fooAttached = await mutator.createTag({ title: 'Foo' })
|
||||
const note = createNoteWithTitle('note')
|
||||
await itemManager.insertItems([foo, foobar, bar, barfoo, fooDelimiter, barFooDelimiter, fooAttached, note])
|
||||
await itemManager.addTagToNote(note, fooAttached, false)
|
||||
await mutator.insertItems([foo, foobar, bar, barfoo, fooDelimiter, barFooDelimiter, fooAttached, note])
|
||||
await mutator.addTagToNote(note, fooAttached, false)
|
||||
|
||||
const fooResults = itemManager.searchTags('foo')
|
||||
expect(fooResults).toContainEqual(foo)
|
||||
@@ -482,19 +450,17 @@ describe('itemManager', () => {
|
||||
|
||||
describe('tags notes index', () => {
|
||||
it('counts countable notes', async () => {
|
||||
itemManager = createService()
|
||||
|
||||
const parentTag = createTag('parent')
|
||||
const childTag = createTag('child')
|
||||
await itemManager.insertItems([parentTag, childTag])
|
||||
await itemManager.setTagParent(parentTag, childTag)
|
||||
await mutator.insertItems([parentTag, childTag])
|
||||
await mutator.setTagParent(parentTag, childTag)
|
||||
|
||||
const parentNote = createNoteWithTitle('parentNote')
|
||||
const childNote = createNoteWithTitle('childNote')
|
||||
await itemManager.insertItems([parentNote, childNote])
|
||||
await mutator.insertItems([parentNote, childNote])
|
||||
|
||||
await itemManager.addTagToNote(parentNote, parentTag, false)
|
||||
await itemManager.addTagToNote(childNote, childTag, false)
|
||||
await mutator.addTagToNote(parentNote, parentTag, false)
|
||||
await mutator.addTagToNote(childNote, childTag, false)
|
||||
|
||||
expect(itemManager.countableNotesForTag(parentTag)).toBe(1)
|
||||
expect(itemManager.countableNotesForTag(childTag)).toBe(1)
|
||||
@@ -502,29 +468,27 @@ describe('itemManager', () => {
|
||||
})
|
||||
|
||||
it('archiving a note should update count index', async () => {
|
||||
itemManager = createService()
|
||||
|
||||
const tag1 = createTag('tag 1')
|
||||
await itemManager.insertItems([tag1])
|
||||
await mutator.insertItems([tag1])
|
||||
|
||||
const note1 = createNoteWithTitle('note 1')
|
||||
const note2 = createNoteWithTitle('note 2')
|
||||
await itemManager.insertItems([note1, note2])
|
||||
await mutator.insertItems([note1, note2])
|
||||
|
||||
await itemManager.addTagToNote(note1, tag1, false)
|
||||
await itemManager.addTagToNote(note2, tag1, false)
|
||||
await mutator.addTagToNote(note1, tag1, false)
|
||||
await mutator.addTagToNote(note2, tag1, false)
|
||||
|
||||
expect(itemManager.countableNotesForTag(tag1)).toBe(2)
|
||||
expect(itemManager.allCountableNotesCount()).toBe(2)
|
||||
|
||||
await itemManager.changeItem<Models.NoteMutator>(note1, (m) => {
|
||||
await mutator.changeItem<Models.NoteMutator>(note1, (m) => {
|
||||
m.archived = true
|
||||
})
|
||||
|
||||
expect(itemManager.allCountableNotesCount()).toBe(1)
|
||||
expect(itemManager.countableNotesForTag(tag1)).toBe(1)
|
||||
|
||||
await itemManager.changeItem<Models.NoteMutator>(note1, (m) => {
|
||||
await mutator.changeItem<Models.NoteMutator>(note1, (m) => {
|
||||
m.archived = false
|
||||
})
|
||||
|
||||
@@ -535,13 +499,12 @@ describe('itemManager', () => {
|
||||
|
||||
describe('smart views', () => {
|
||||
it('lets me create a smart view', async () => {
|
||||
itemManager = createService()
|
||||
setupRandomUuid()
|
||||
|
||||
const [view1, view2, view3] = await Promise.all([
|
||||
itemManager.createSmartView('Not Pinned', NotPinnedPredicate),
|
||||
itemManager.createSmartView('Last Day', LastDayPredicate),
|
||||
itemManager.createSmartView('Long', LongTextPredicate),
|
||||
mutator.createSmartView({ title: 'Not Pinned', predicate: NotPinnedPredicate }),
|
||||
mutator.createSmartView({ title: 'Last Day', predicate: LastDayPredicate }),
|
||||
mutator.createSmartView({ title: 'Long', predicate: LongTextPredicate }),
|
||||
])
|
||||
|
||||
expect(view1).toBeTruthy()
|
||||
@@ -554,10 +517,9 @@ describe('itemManager', () => {
|
||||
})
|
||||
|
||||
it('lets me use a smart view', async () => {
|
||||
itemManager = createService()
|
||||
setupRandomUuid()
|
||||
|
||||
const view = await itemManager.createSmartView('Not Pinned', NotPinnedPredicate)
|
||||
const view = await mutator.createSmartView({ title: 'Not Pinned', predicate: NotPinnedPredicate })
|
||||
|
||||
const notes = itemManager.notesMatchingSmartView(view)
|
||||
|
||||
@@ -565,7 +527,6 @@ describe('itemManager', () => {
|
||||
})
|
||||
|
||||
it('lets me test if a title is a smart view', () => {
|
||||
itemManager = createService()
|
||||
setupRandomUuid()
|
||||
|
||||
expect(itemManager.isSmartViewTitle(VIEW_NOT_PINNED)).toEqual(true)
|
||||
@@ -577,13 +538,12 @@ describe('itemManager', () => {
|
||||
})
|
||||
|
||||
it('lets me create a smart view from the DSL', async () => {
|
||||
itemManager = createService()
|
||||
setupRandomUuid()
|
||||
|
||||
const [tag1, tag2, tag3] = await Promise.all([
|
||||
itemManager.createSmartViewFromDSL(VIEW_NOT_PINNED),
|
||||
itemManager.createSmartViewFromDSL(VIEW_LAST_DAY),
|
||||
itemManager.createSmartViewFromDSL(VIEW_LONG),
|
||||
mutator.createSmartViewFromDSL(VIEW_NOT_PINNED),
|
||||
mutator.createSmartViewFromDSL(VIEW_LAST_DAY),
|
||||
mutator.createSmartViewFromDSL(VIEW_LONG),
|
||||
])
|
||||
|
||||
expect(tag1).toBeTruthy()
|
||||
@@ -596,11 +556,10 @@ describe('itemManager', () => {
|
||||
})
|
||||
|
||||
it('will create smart view or tags from the generic method', async () => {
|
||||
itemManager = createService()
|
||||
setupRandomUuid()
|
||||
|
||||
const someTag = await itemManager.createTagOrSmartView('some-tag')
|
||||
const someView = await itemManager.createTagOrSmartView(VIEW_LONG)
|
||||
const someTag = await mutator.createTagOrSmartView('some-tag')
|
||||
const someView = await mutator.createTagOrSmartView(VIEW_LONG)
|
||||
|
||||
expect(someTag.content_type).toEqual(ContentType.Tag)
|
||||
expect(someView.content_type).toEqual(ContentType.SmartView)
|
||||
@@ -608,12 +567,11 @@ describe('itemManager', () => {
|
||||
})
|
||||
|
||||
it('lets me rename a smart view', async () => {
|
||||
itemManager = createService()
|
||||
setupRandomUuid()
|
||||
|
||||
const tag = await itemManager.createSmartView('Not Pinned', NotPinnedPredicate)
|
||||
const tag = await mutator.createSmartView({ title: 'Not Pinned', predicate: NotPinnedPredicate })
|
||||
|
||||
await itemManager.changeItem<Models.TagMutator>(tag, (m) => {
|
||||
await mutator.changeItem<Models.TagMutator>(tag, (m) => {
|
||||
m.title = 'New Title'
|
||||
})
|
||||
|
||||
@@ -625,10 +583,9 @@ describe('itemManager', () => {
|
||||
})
|
||||
|
||||
it('lets me find a smart view', async () => {
|
||||
itemManager = createService()
|
||||
setupRandomUuid()
|
||||
|
||||
const tag = await itemManager.createSmartView('Not Pinned', NotPinnedPredicate)
|
||||
const tag = await mutator.createSmartView({ title: 'Not Pinned', predicate: NotPinnedPredicate })
|
||||
|
||||
const view = itemManager.findItem(tag.uuid) as Models.SmartView
|
||||
|
||||
@@ -636,7 +593,6 @@ describe('itemManager', () => {
|
||||
})
|
||||
|
||||
it('untagged notes smart view', async () => {
|
||||
itemManager = createService()
|
||||
setupRandomUuid()
|
||||
|
||||
const view = itemManager.untaggedNotesSmartView
|
||||
@@ -644,11 +600,11 @@ describe('itemManager', () => {
|
||||
const tag = createTag('tag')
|
||||
const untaggedNote = createNoteWithTitle('note')
|
||||
const taggedNote = createNoteWithTitle('taggedNote')
|
||||
await itemManager.insertItems([tag, untaggedNote, taggedNote])
|
||||
await mutator.insertItems([tag, untaggedNote, taggedNote])
|
||||
|
||||
expect(itemManager.notesMatchingSmartView(view)).toHaveLength(2)
|
||||
|
||||
await itemManager.addTagToNote(taggedNote, tag, false)
|
||||
await mutator.addTagToNote(taggedNote, tag, false)
|
||||
|
||||
expect(itemManager.notesMatchingSmartView(view)).toHaveLength(1)
|
||||
|
||||
@@ -657,31 +613,28 @@ describe('itemManager', () => {
|
||||
|
||||
describe('files', () => {
|
||||
it('should correctly rename file to filename that has extension', async () => {
|
||||
itemManager = createService()
|
||||
const file = createFile('initialName.ext')
|
||||
await itemManager.insertItems([file])
|
||||
await mutator.insertItems([file])
|
||||
|
||||
const renamedFile = await itemManager.renameFile(file, 'anotherName.anotherExt')
|
||||
const renamedFile = await mutator.renameFile(file, 'anotherName.anotherExt')
|
||||
|
||||
expect(renamedFile.name).toBe('anotherName.anotherExt')
|
||||
})
|
||||
|
||||
it('should correctly rename extensionless file to filename that has extension', async () => {
|
||||
itemManager = createService()
|
||||
const file = createFile('initialName')
|
||||
await itemManager.insertItems([file])
|
||||
await mutator.insertItems([file])
|
||||
|
||||
const renamedFile = await itemManager.renameFile(file, 'anotherName.anotherExt')
|
||||
const renamedFile = await mutator.renameFile(file, 'anotherName.anotherExt')
|
||||
|
||||
expect(renamedFile.name).toBe('anotherName.anotherExt')
|
||||
})
|
||||
|
||||
it('should correctly rename file to filename that does not have extension', async () => {
|
||||
itemManager = createService()
|
||||
const file = createFile('initialName.ext')
|
||||
await itemManager.insertItems([file])
|
||||
await mutator.insertItems([file])
|
||||
|
||||
const renamedFile = await itemManager.renameFile(file, 'anotherName')
|
||||
const renamedFile = await mutator.renameFile(file, 'anotherName')
|
||||
|
||||
expect(renamedFile.name).toBe('anotherName')
|
||||
})
|
||||
@@ -689,15 +642,14 @@ describe('itemManager', () => {
|
||||
|
||||
describe('linking', () => {
|
||||
it('adding a note to a tag hierarchy should add the note to its parent too', async () => {
|
||||
itemManager = createService()
|
||||
const parentTag = createTag('parent')
|
||||
const childTag = createTag('child')
|
||||
const note = createNoteWithTitle('note')
|
||||
|
||||
await itemManager.insertItems([parentTag, childTag, note])
|
||||
await itemManager.setTagParent(parentTag, childTag)
|
||||
await mutator.insertItems([parentTag, childTag, note])
|
||||
await mutator.setTagParent(parentTag, childTag)
|
||||
|
||||
await itemManager.addTagToNote(note, childTag, true)
|
||||
await mutator.addTagToNote(note, childTag, true)
|
||||
|
||||
const tags = itemManager.getSortedTagsForItem(note)
|
||||
|
||||
@@ -707,15 +659,14 @@ describe('itemManager', () => {
|
||||
})
|
||||
|
||||
it('adding a note to a tag hierarchy should not add the note to its parent if hierarchy option is disabled', async () => {
|
||||
itemManager = createService()
|
||||
const parentTag = createTag('parent')
|
||||
const childTag = createTag('child')
|
||||
const note = createNoteWithTitle('note')
|
||||
|
||||
await itemManager.insertItems([parentTag, childTag, note])
|
||||
await itemManager.setTagParent(parentTag, childTag)
|
||||
await mutator.insertItems([parentTag, childTag, note])
|
||||
await mutator.setTagParent(parentTag, childTag)
|
||||
|
||||
await itemManager.addTagToNote(note, childTag, false)
|
||||
await mutator.addTagToNote(note, childTag, false)
|
||||
|
||||
const tags = itemManager.getSortedTagsForItem(note)
|
||||
|
||||
@@ -724,15 +675,14 @@ describe('itemManager', () => {
|
||||
})
|
||||
|
||||
it('adding a file to a tag hierarchy should add the file to its parent too', async () => {
|
||||
itemManager = createService()
|
||||
const parentTag = createTag('parent')
|
||||
const childTag = createTag('child')
|
||||
const file = createFile('file')
|
||||
|
||||
await itemManager.insertItems([parentTag, childTag, file])
|
||||
await itemManager.setTagParent(parentTag, childTag)
|
||||
await mutator.insertItems([parentTag, childTag, file])
|
||||
await mutator.setTagParent(parentTag, childTag)
|
||||
|
||||
await itemManager.addTagToFile(file, childTag, true)
|
||||
await mutator.addTagToFile(file, childTag, true)
|
||||
|
||||
const tags = itemManager.getSortedTagsForItem(file)
|
||||
|
||||
@@ -742,15 +692,14 @@ describe('itemManager', () => {
|
||||
})
|
||||
|
||||
it('adding a file to a tag hierarchy should not add the file to its parent if hierarchy option is disabled', async () => {
|
||||
itemManager = createService()
|
||||
const parentTag = createTag('parent')
|
||||
const childTag = createTag('child')
|
||||
const file = createFile('file')
|
||||
|
||||
await itemManager.insertItems([parentTag, childTag, file])
|
||||
await itemManager.setTagParent(parentTag, childTag)
|
||||
await mutator.insertItems([parentTag, childTag, file])
|
||||
await mutator.setTagParent(parentTag, childTag)
|
||||
|
||||
await itemManager.addTagToFile(file, childTag, false)
|
||||
await mutator.addTagToFile(file, childTag, false)
|
||||
|
||||
const tags = itemManager.getSortedTagsForItem(file)
|
||||
|
||||
@@ -759,12 +708,12 @@ describe('itemManager', () => {
|
||||
})
|
||||
|
||||
it('should link file with note', async () => {
|
||||
itemManager = createService()
|
||||
const note = createNoteWithTitle('invoices')
|
||||
const file = createFile('invoice_1.pdf')
|
||||
await itemManager.insertItems([note, file])
|
||||
await mutator.insertItems([note, file])
|
||||
|
||||
const resultingFile = await itemManager.associateFileWithNote(file, note)
|
||||
const resultingFile = await mutator.associateFileWithNote(file, note)
|
||||
assert(resultingFile)
|
||||
const references = resultingFile.references
|
||||
|
||||
expect(references).toHaveLength(1)
|
||||
@@ -772,25 +721,24 @@ describe('itemManager', () => {
|
||||
})
|
||||
|
||||
it('should unlink file from note', async () => {
|
||||
itemManager = createService()
|
||||
const note = createNoteWithTitle('invoices')
|
||||
const file = createFile('invoice_1.pdf')
|
||||
await itemManager.insertItems([note, file])
|
||||
await mutator.insertItems([note, file])
|
||||
|
||||
const associatedFile = await itemManager.associateFileWithNote(file, note)
|
||||
const disassociatedFile = await itemManager.disassociateFileWithNote(associatedFile, note)
|
||||
const associatedFile = await mutator.associateFileWithNote(file, note)
|
||||
assert(associatedFile)
|
||||
const disassociatedFile = await mutator.disassociateFileWithNote(associatedFile, note)
|
||||
const references = disassociatedFile.references
|
||||
|
||||
expect(references).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('should link note to note', async () => {
|
||||
itemManager = createService()
|
||||
const note = createNoteWithTitle('research')
|
||||
const note2 = createNoteWithTitle('citation')
|
||||
await itemManager.insertItems([note, note2])
|
||||
await mutator.insertItems([note, note2])
|
||||
|
||||
const resultingNote = await itemManager.linkNoteToNote(note, note2)
|
||||
const resultingNote = await mutator.linkNoteToNote(note, note2)
|
||||
const references = resultingNote.references
|
||||
|
||||
expect(references).toHaveLength(1)
|
||||
@@ -798,12 +746,11 @@ describe('itemManager', () => {
|
||||
})
|
||||
|
||||
it('should link file to file', async () => {
|
||||
itemManager = createService()
|
||||
const file = createFile('research')
|
||||
const file2 = createFile('citation')
|
||||
await itemManager.insertItems([file, file2])
|
||||
await mutator.insertItems([file, file2])
|
||||
|
||||
const resultingfile = await itemManager.linkFileToFile(file, file2)
|
||||
const resultingfile = await mutator.linkFileToFile(file, file2)
|
||||
const references = resultingfile.references
|
||||
|
||||
expect(references).toHaveLength(1)
|
||||
@@ -811,13 +758,12 @@ describe('itemManager', () => {
|
||||
})
|
||||
|
||||
it('should get the relationship type for two items', async () => {
|
||||
itemManager = createService()
|
||||
const firstNote = createNoteWithTitle('First note')
|
||||
const secondNote = createNoteWithTitle('Second note')
|
||||
const unlinkedNote = createNoteWithTitle('Unlinked note')
|
||||
await itemManager.insertItems([firstNote, secondNote, unlinkedNote])
|
||||
await mutator.insertItems([firstNote, secondNote, unlinkedNote])
|
||||
|
||||
const firstNoteLinkedToSecond = await itemManager.linkNoteToNote(firstNote, secondNote)
|
||||
const firstNoteLinkedToSecond = await mutator.linkNoteToNote(firstNote, secondNote)
|
||||
|
||||
const relationshipOfFirstNoteToSecond = itemManager.relationshipDirectionBetweenItems(
|
||||
firstNoteLinkedToSecond,
|
||||
@@ -838,13 +784,12 @@ describe('itemManager', () => {
|
||||
})
|
||||
|
||||
it('should unlink itemOne from itemTwo if relation is direct', async () => {
|
||||
itemManager = createService()
|
||||
const note = createNoteWithTitle('Note 1')
|
||||
const note2 = createNoteWithTitle('Note 2')
|
||||
await itemManager.insertItems([note, note2])
|
||||
await mutator.insertItems([note, note2])
|
||||
|
||||
const linkedItem = await itemManager.linkNoteToNote(note, note2)
|
||||
const unlinkedItem = await itemManager.unlinkItems(linkedItem, note2)
|
||||
const linkedItem = await mutator.linkNoteToNote(note, note2)
|
||||
const unlinkedItem = await mutator.unlinkItems(linkedItem, note2)
|
||||
const references = unlinkedItem.references
|
||||
|
||||
expect(unlinkedItem.uuid).toBe(note.uuid)
|
||||
@@ -852,13 +797,12 @@ describe('itemManager', () => {
|
||||
})
|
||||
|
||||
it('should unlink itemTwo from itemOne if relation is indirect', async () => {
|
||||
itemManager = createService()
|
||||
const note = createNoteWithTitle('Note 1')
|
||||
const note2 = createNoteWithTitle('Note 2')
|
||||
await itemManager.insertItems([note, note2])
|
||||
await mutator.insertItems([note, note2])
|
||||
|
||||
const linkedItem = await itemManager.linkNoteToNote(note, note2)
|
||||
const changedItem = await itemManager.unlinkItems(linkedItem, note2)
|
||||
const linkedItem = await mutator.linkNoteToNote(note, note2)
|
||||
const changedItem = await mutator.unlinkItems(linkedItem, note2)
|
||||
|
||||
expect(changedItem.uuid).toBe(note.uuid)
|
||||
expect(changedItem.references).toHaveLength(0)
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import { ContentType } from '@standardnotes/common'
|
||||
import { assert, naturalSort, removeFromArray, UuidGenerator, Uuids } from '@standardnotes/utils'
|
||||
import { ItemsKeyMutator, SNItemsKey } from '@standardnotes/encryption'
|
||||
import { SNItemsKey } from '@standardnotes/encryption'
|
||||
import { PayloadManager } from '../Payloads/PayloadManager'
|
||||
import { TagsToFoldersMigrationApplicator } from '../../Migrations/Applicators/TagsToFolders'
|
||||
import { UuidString } from '../../Types/UuidString'
|
||||
import * as Models from '@standardnotes/models'
|
||||
import * as Services from '@standardnotes/services'
|
||||
import { PayloadManagerChangeData } from '../Payloads'
|
||||
import { DiagnosticInfo, ItemsClientInterface, ItemRelationshipDirection } from '@standardnotes/services'
|
||||
import { CollectionSort, DecryptedItemInterface, ItemContent, SmartViewDefaultIconName } from '@standardnotes/models'
|
||||
import { ItemRelationshipDirection } from '@standardnotes/services'
|
||||
|
||||
type ItemsChangeObserver<I extends Models.DecryptedItemInterface = Models.DecryptedItemInterface> = {
|
||||
contentType: ContentType[]
|
||||
@@ -23,18 +22,18 @@ type ItemsChangeObserver<I extends Models.DecryptedItemInterface = Models.Decryp
|
||||
* will then notify its observers (which is us), we'll convert the payloads to items,
|
||||
* and then we'll propagate them to our listeners.
|
||||
*/
|
||||
export class ItemManager
|
||||
extends Services.AbstractService
|
||||
implements Services.ItemManagerInterface, ItemsClientInterface
|
||||
{
|
||||
export class ItemManager extends Services.AbstractService implements Services.ItemManagerInterface {
|
||||
private unsubChangeObserver: () => void
|
||||
private observers: ItemsChangeObserver[] = []
|
||||
private collection!: Models.ItemCollection
|
||||
private systemSmartViews: Models.SmartView[]
|
||||
private tagItemsIndex!: Models.TagItemsIndex
|
||||
private itemCounter!: Models.ItemCounter
|
||||
|
||||
private navigationDisplayController!: Models.ItemDisplayController<Models.SNNote | Models.FileItem>
|
||||
private tagDisplayController!: Models.ItemDisplayController<Models.SNTag>
|
||||
private navigationDisplayController!: Models.ItemDisplayController<
|
||||
Models.SNNote | Models.FileItem,
|
||||
Models.NotesAndFilesDisplayOptions
|
||||
>
|
||||
private tagDisplayController!: Models.ItemDisplayController<Models.SNTag, Models.TagsDisplayOptions>
|
||||
private itemsKeyDisplayController!: Models.ItemDisplayController<SNItemsKey>
|
||||
private componentDisplayController!: Models.ItemDisplayController<Models.SNComponent>
|
||||
private themeDisplayController!: Models.ItemDisplayController<Models.SNTheme>
|
||||
@@ -52,11 +51,15 @@ export class ItemManager
|
||||
this.unsubChangeObserver = this.payloadManager.addObserver(ContentType.Any, this.setPayloads.bind(this))
|
||||
}
|
||||
|
||||
private rebuildSystemSmartViews(criteria: Models.FilterDisplayOptions): Models.SmartView[] {
|
||||
private rebuildSystemSmartViews(criteria: Models.NotesAndFilesDisplayOptions): Models.SmartView[] {
|
||||
this.systemSmartViews = Models.BuildSmartViews(criteria)
|
||||
return this.systemSmartViews
|
||||
}
|
||||
|
||||
public getCollection(): Models.ItemCollection {
|
||||
return this.collection
|
||||
}
|
||||
|
||||
private createCollection() {
|
||||
this.collection = new Models.ItemCollection()
|
||||
|
||||
@@ -94,7 +97,7 @@ export class ItemManager
|
||||
sortDirection: 'asc',
|
||||
})
|
||||
|
||||
this.tagItemsIndex = new Models.TagItemsIndex(this.collection, this.tagItemsIndex?.observers)
|
||||
this.itemCounter = new Models.ItemCounter(this.collection, this.itemCounter?.observers)
|
||||
}
|
||||
|
||||
private get allDisplayControllers(): Models.ItemDisplayController<Models.DisplayItem>[] {
|
||||
@@ -113,6 +116,10 @@ export class ItemManager
|
||||
return this.collection.invalidElements()
|
||||
}
|
||||
|
||||
public get invalidNonVaultedItems(): Models.EncryptedItemInterface[] {
|
||||
return this.invalidItems.filter((item) => !item.key_system_identifier)
|
||||
}
|
||||
|
||||
public createItemFromPayload(payload: Models.DecryptedPayloadInterface): Models.DecryptedItemInterface {
|
||||
return Models.CreateDecryptedItemFromPayload(payload)
|
||||
}
|
||||
@@ -121,8 +128,8 @@ export class ItemManager
|
||||
return new Models.DecryptedPayload(object)
|
||||
}
|
||||
|
||||
public setPrimaryItemDisplayOptions(options: Models.DisplayOptions): void {
|
||||
const override: Models.FilterDisplayOptions = {}
|
||||
public setPrimaryItemDisplayOptions(options: Models.NotesAndFilesDisplayControllerOptions): void {
|
||||
const override: Models.NotesAndFilesDisplayOptions = {}
|
||||
const additionalFilters: Models.ItemFilter[] = []
|
||||
|
||||
if (options.views && options.views.find((view) => view.uuid === Models.SystemViewId.AllNotes)) {
|
||||
@@ -164,7 +171,7 @@ export class ItemManager
|
||||
})
|
||||
.filter((view) => view != undefined)
|
||||
|
||||
const updatedOptions: Models.DisplayOptions = {
|
||||
const updatedOptions: Models.DisplayControllerDisplayOptions & Models.NotesAndFilesDisplayOptions = {
|
||||
...options,
|
||||
...override,
|
||||
...{
|
||||
@@ -173,7 +180,7 @@ export class ItemManager
|
||||
},
|
||||
}
|
||||
|
||||
if (updatedOptions.sortBy === CollectionSort.Title) {
|
||||
if (updatedOptions.sortBy === Models.CollectionSort.Title) {
|
||||
updatedOptions.sortDirection = updatedOptions.sortDirection === 'asc' ? 'dsc' : 'asc'
|
||||
}
|
||||
|
||||
@@ -181,6 +188,17 @@ export class ItemManager
|
||||
customFilter: Models.computeUnifiedFilterForDisplayOptions(updatedOptions, this.collection, additionalFilters),
|
||||
...updatedOptions,
|
||||
})
|
||||
|
||||
this.itemCounter.setDisplayOptions(updatedOptions)
|
||||
}
|
||||
|
||||
public setVaultDisplayOptions(options: Models.VaultDisplayOptions): void {
|
||||
this.navigationDisplayController.setVaultDisplayOptions(options)
|
||||
this.tagDisplayController.setVaultDisplayOptions(options)
|
||||
this.smartViewDisplayController.setVaultDisplayOptions(options)
|
||||
this.fileDisplayController.setVaultDisplayOptions(options)
|
||||
|
||||
this.itemCounter.setVaultDisplayOptions(options)
|
||||
}
|
||||
|
||||
public getDisplayableNotes(): Models.SNNote[] {
|
||||
@@ -214,7 +232,7 @@ export class ItemManager
|
||||
;(this.unsubChangeObserver as unknown) = undefined
|
||||
;(this.payloadManager as unknown) = undefined
|
||||
;(this.collection as unknown) = undefined
|
||||
;(this.tagItemsIndex as unknown) = undefined
|
||||
;(this.itemCounter as unknown) = undefined
|
||||
;(this.tagDisplayController as unknown) = undefined
|
||||
;(this.navigationDisplayController as unknown) = undefined
|
||||
;(this.itemsKeyDisplayController as unknown) = undefined
|
||||
@@ -252,9 +270,6 @@ export class ItemManager
|
||||
return this.findItem(uuid) as T
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all items matching given ids
|
||||
*/
|
||||
findItems<T extends Models.DecryptedItemInterface>(uuids: UuidString[]): T[] {
|
||||
return this.collection.findAllDecrypted(uuids) as T[]
|
||||
}
|
||||
@@ -271,6 +286,7 @@ export class ItemManager
|
||||
return this.collection.nondeletedElements().filter(Models.isDecryptedItem)
|
||||
}
|
||||
|
||||
/** Unlock .items, this function includes error decrypting items */
|
||||
allTrackedItems(): Models.ItemInterface[] {
|
||||
return this.collection.all()
|
||||
}
|
||||
@@ -280,26 +296,26 @@ export class ItemManager
|
||||
}
|
||||
|
||||
public addNoteCountChangeObserver(observer: Models.TagItemCountChangeObserver): () => void {
|
||||
return this.tagItemsIndex.addCountChangeObserver(observer)
|
||||
return this.itemCounter.addCountChangeObserver(observer)
|
||||
}
|
||||
|
||||
public allCountableNotesCount(): number {
|
||||
return this.tagItemsIndex.allCountableNotesCount()
|
||||
return this.itemCounter.allCountableNotesCount()
|
||||
}
|
||||
|
||||
public allCountableFilesCount(): number {
|
||||
return this.tagItemsIndex.allCountableFilesCount()
|
||||
return this.itemCounter.allCountableFilesCount()
|
||||
}
|
||||
|
||||
public countableNotesForTag(tag: Models.SNTag | Models.SmartView): number {
|
||||
if (tag instanceof Models.SmartView) {
|
||||
if (tag.uuid === Models.SystemViewId.AllNotes) {
|
||||
return this.tagItemsIndex.allCountableNotesCount()
|
||||
return this.itemCounter.allCountableNotesCount()
|
||||
}
|
||||
|
||||
throw Error('countableItemsForTag is not meant to be used for smart views.')
|
||||
}
|
||||
return this.tagItemsIndex.countableItemsForTag(tag)
|
||||
return this.itemCounter.countableItemsForTag(tag)
|
||||
}
|
||||
|
||||
public getNoteCount(): number {
|
||||
@@ -330,12 +346,12 @@ export class ItemManager
|
||||
/**
|
||||
* Returns the items that reference the given item, or an empty array if no results.
|
||||
*/
|
||||
public itemsReferencingItem(
|
||||
itemToLookupUuidFor: Models.DecryptedItemInterface,
|
||||
public itemsReferencingItem<I extends Models.DecryptedItemInterface = Models.DecryptedItemInterface>(
|
||||
itemToLookupUuidFor: { uuid: UuidString },
|
||||
contentType?: ContentType,
|
||||
): Models.DecryptedItemInterface[] {
|
||||
): I[] {
|
||||
const uuids = this.collection.uuidsThatReferenceUuid(itemToLookupUuidFor.uuid)
|
||||
let referencing = this.findItems(uuids)
|
||||
let referencing = this.findItems<I>(uuids)
|
||||
if (contentType) {
|
||||
referencing = referencing.filter((ref) => {
|
||||
return ref?.content_type === contentType
|
||||
@@ -405,7 +421,7 @@ export class ItemManager
|
||||
}
|
||||
|
||||
this.collection.onChange(delta)
|
||||
this.tagItemsIndex.onChange(delta)
|
||||
this.itemCounter.onChange(delta)
|
||||
|
||||
const affectedContentTypesArray = Array.from(affectedContentTypes.values())
|
||||
for (const controller of this.allDisplayControllers) {
|
||||
@@ -509,250 +525,6 @@ export class ItemManager
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Consumers wanting to modify an item should run it through this block,
|
||||
* so that data is properly mapped through our function, and latest state
|
||||
* is properly reconciled.
|
||||
*/
|
||||
public async changeItem<
|
||||
M extends Models.DecryptedItemMutator = Models.DecryptedItemMutator,
|
||||
I extends Models.DecryptedItemInterface = Models.DecryptedItemInterface,
|
||||
>(
|
||||
itemToLookupUuidFor: I,
|
||||
mutate?: (mutator: M) => void,
|
||||
mutationType: Models.MutationType = Models.MutationType.UpdateUserTimestamps,
|
||||
emitSource = Models.PayloadEmitSource.LocalChanged,
|
||||
payloadSourceKey?: string,
|
||||
): Promise<I> {
|
||||
const results = await this.changeItems<M, I>(
|
||||
[itemToLookupUuidFor],
|
||||
mutate,
|
||||
mutationType,
|
||||
emitSource,
|
||||
payloadSourceKey,
|
||||
)
|
||||
return results[0]
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mutate If not supplied, the intention would simply be to mark the item as dirty.
|
||||
*/
|
||||
public async changeItems<
|
||||
M extends Models.DecryptedItemMutator = Models.DecryptedItemMutator,
|
||||
I extends Models.DecryptedItemInterface = Models.DecryptedItemInterface,
|
||||
>(
|
||||
itemsToLookupUuidsFor: I[],
|
||||
mutate?: (mutator: M) => void,
|
||||
mutationType: Models.MutationType = Models.MutationType.UpdateUserTimestamps,
|
||||
emitSource = Models.PayloadEmitSource.LocalChanged,
|
||||
payloadSourceKey?: string,
|
||||
): Promise<I[]> {
|
||||
const items = this.findItemsIncludingBlanks(Uuids(itemsToLookupUuidsFor))
|
||||
const payloads: Models.DecryptedPayloadInterface[] = []
|
||||
|
||||
for (const item of items) {
|
||||
if (!item) {
|
||||
throw Error('Attempting to change non-existant item')
|
||||
}
|
||||
const mutator = Models.CreateDecryptedMutatorForItem(item, mutationType)
|
||||
if (mutate) {
|
||||
mutate(mutator as M)
|
||||
}
|
||||
const payload = mutator.getResult()
|
||||
payloads.push(payload)
|
||||
}
|
||||
|
||||
await this.payloadManager.emitPayloads(payloads, emitSource, payloadSourceKey)
|
||||
|
||||
const results = this.findItems(payloads.map((p) => p.uuid)) as I[]
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
/**
|
||||
* Run unique mutations per each item in the array, then only propagate all changes
|
||||
* once all mutations have been run. This differs from `changeItems` in that changeItems
|
||||
* runs the same mutation on all items.
|
||||
*/
|
||||
public async runTransactionalMutations(
|
||||
transactions: Models.TransactionalMutation[],
|
||||
emitSource = Models.PayloadEmitSource.LocalChanged,
|
||||
payloadSourceKey?: string,
|
||||
): Promise<(Models.DecryptedItemInterface | undefined)[]> {
|
||||
const payloads: Models.DecryptedPayloadInterface[] = []
|
||||
|
||||
for (const transaction of transactions) {
|
||||
const item = this.findItem(transaction.itemUuid)
|
||||
|
||||
if (!item) {
|
||||
continue
|
||||
}
|
||||
|
||||
const mutator = Models.CreateDecryptedMutatorForItem(
|
||||
item,
|
||||
transaction.mutationType || Models.MutationType.UpdateUserTimestamps,
|
||||
)
|
||||
|
||||
transaction.mutate(mutator)
|
||||
const payload = mutator.getResult()
|
||||
payloads.push(payload)
|
||||
}
|
||||
|
||||
await this.payloadManager.emitPayloads(payloads, emitSource, payloadSourceKey)
|
||||
const results = this.findItems(payloads.map((p) => p.uuid))
|
||||
return results
|
||||
}
|
||||
|
||||
public async runTransactionalMutation(
|
||||
transaction: Models.TransactionalMutation,
|
||||
emitSource = Models.PayloadEmitSource.LocalChanged,
|
||||
payloadSourceKey?: string,
|
||||
): Promise<Models.DecryptedItemInterface | undefined> {
|
||||
const item = this.findSureItem(transaction.itemUuid)
|
||||
const mutator = Models.CreateDecryptedMutatorForItem(
|
||||
item,
|
||||
transaction.mutationType || Models.MutationType.UpdateUserTimestamps,
|
||||
)
|
||||
transaction.mutate(mutator)
|
||||
const payload = mutator.getResult()
|
||||
|
||||
await this.payloadManager.emitPayloads([payload], emitSource, payloadSourceKey)
|
||||
const result = this.findItem(payload.uuid)
|
||||
return result
|
||||
}
|
||||
|
||||
async changeNote(
|
||||
itemToLookupUuidFor: Models.SNNote,
|
||||
mutate: (mutator: Models.NoteMutator) => void,
|
||||
mutationType: Models.MutationType = Models.MutationType.UpdateUserTimestamps,
|
||||
emitSource = Models.PayloadEmitSource.LocalChanged,
|
||||
payloadSourceKey?: string,
|
||||
): Promise<Models.DecryptedPayloadInterface[]> {
|
||||
const note = this.findItem<Models.SNNote>(itemToLookupUuidFor.uuid)
|
||||
if (!note) {
|
||||
throw Error('Attempting to change non-existant note')
|
||||
}
|
||||
const mutator = new Models.NoteMutator(note, mutationType)
|
||||
|
||||
return this.applyTransform(mutator, mutate, emitSource, payloadSourceKey)
|
||||
}
|
||||
|
||||
async changeTag(
|
||||
itemToLookupUuidFor: Models.SNTag,
|
||||
mutate: (mutator: Models.TagMutator) => void,
|
||||
mutationType: Models.MutationType = Models.MutationType.UpdateUserTimestamps,
|
||||
emitSource = Models.PayloadEmitSource.LocalChanged,
|
||||
payloadSourceKey?: string,
|
||||
): Promise<Models.SNTag> {
|
||||
const tag = this.findItem<Models.SNTag>(itemToLookupUuidFor.uuid)
|
||||
if (!tag) {
|
||||
throw Error('Attempting to change non-existant tag')
|
||||
}
|
||||
const mutator = new Models.TagMutator(tag, mutationType)
|
||||
await this.applyTransform(mutator, mutate, emitSource, payloadSourceKey)
|
||||
return this.findSureItem<Models.SNTag>(itemToLookupUuidFor.uuid)
|
||||
}
|
||||
|
||||
async changeComponent(
|
||||
itemToLookupUuidFor: Models.SNComponent,
|
||||
mutate: (mutator: Models.ComponentMutator) => void,
|
||||
mutationType: Models.MutationType = Models.MutationType.UpdateUserTimestamps,
|
||||
emitSource = Models.PayloadEmitSource.LocalChanged,
|
||||
payloadSourceKey?: string,
|
||||
): Promise<Models.SNComponent> {
|
||||
const component = this.findItem<Models.SNComponent>(itemToLookupUuidFor.uuid)
|
||||
if (!component) {
|
||||
throw Error('Attempting to change non-existant component')
|
||||
}
|
||||
const mutator = new Models.ComponentMutator(component, mutationType)
|
||||
await this.applyTransform(mutator, mutate, emitSource, payloadSourceKey)
|
||||
return this.findSureItem<Models.SNComponent>(itemToLookupUuidFor.uuid)
|
||||
}
|
||||
|
||||
async changeFeatureRepo(
|
||||
itemToLookupUuidFor: Models.SNFeatureRepo,
|
||||
mutate: (mutator: Models.FeatureRepoMutator) => void,
|
||||
mutationType: Models.MutationType = Models.MutationType.UpdateUserTimestamps,
|
||||
emitSource = Models.PayloadEmitSource.LocalChanged,
|
||||
payloadSourceKey?: string,
|
||||
): Promise<Models.SNFeatureRepo> {
|
||||
const repo = this.findItem(itemToLookupUuidFor.uuid)
|
||||
if (!repo) {
|
||||
throw Error('Attempting to change non-existant repo')
|
||||
}
|
||||
const mutator = new Models.FeatureRepoMutator(repo, mutationType)
|
||||
await this.applyTransform(mutator, mutate, emitSource, payloadSourceKey)
|
||||
return this.findSureItem<Models.SNFeatureRepo>(itemToLookupUuidFor.uuid)
|
||||
}
|
||||
|
||||
async changeActionsExtension(
|
||||
itemToLookupUuidFor: Models.SNActionsExtension,
|
||||
mutate: (mutator: Models.ActionsExtensionMutator) => void,
|
||||
mutationType: Models.MutationType = Models.MutationType.UpdateUserTimestamps,
|
||||
emitSource = Models.PayloadEmitSource.LocalChanged,
|
||||
payloadSourceKey?: string,
|
||||
): Promise<Models.SNActionsExtension> {
|
||||
const extension = this.findItem<Models.SNActionsExtension>(itemToLookupUuidFor.uuid)
|
||||
if (!extension) {
|
||||
throw Error('Attempting to change non-existant extension')
|
||||
}
|
||||
const mutator = new Models.ActionsExtensionMutator(extension, mutationType)
|
||||
await this.applyTransform(mutator, mutate, emitSource, payloadSourceKey)
|
||||
return this.findSureItem<Models.SNActionsExtension>(itemToLookupUuidFor.uuid)
|
||||
}
|
||||
|
||||
async changeItemsKey(
|
||||
itemToLookupUuidFor: Models.ItemsKeyInterface,
|
||||
mutate: (mutator: Models.ItemsKeyMutatorInterface) => void,
|
||||
mutationType: Models.MutationType = Models.MutationType.UpdateUserTimestamps,
|
||||
emitSource = Models.PayloadEmitSource.LocalChanged,
|
||||
payloadSourceKey?: string,
|
||||
): Promise<Models.ItemsKeyInterface> {
|
||||
const itemsKey = this.findItem<SNItemsKey>(itemToLookupUuidFor.uuid)
|
||||
|
||||
if (!itemsKey) {
|
||||
throw Error('Attempting to change non-existant itemsKey')
|
||||
}
|
||||
|
||||
const mutator = new ItemsKeyMutator(itemsKey, mutationType)
|
||||
|
||||
await this.applyTransform(mutator, mutate, emitSource, payloadSourceKey)
|
||||
|
||||
return this.findSureItem<Models.ItemsKeyInterface>(itemToLookupUuidFor.uuid)
|
||||
}
|
||||
|
||||
private async applyTransform<T extends Models.DecryptedItemMutator>(
|
||||
mutator: T,
|
||||
mutate: (mutator: T) => void,
|
||||
emitSource = Models.PayloadEmitSource.LocalChanged,
|
||||
payloadSourceKey?: string,
|
||||
): Promise<Models.DecryptedPayloadInterface[]> {
|
||||
mutate(mutator)
|
||||
const payload = mutator.getResult()
|
||||
return this.payloadManager.emitPayload(payload, emitSource, payloadSourceKey)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the item as needing sync. The item is then run through the mapping function,
|
||||
* and propagated to mapping observers.
|
||||
* @param isUserModified - Whether to update the item's "user modified date"
|
||||
*/
|
||||
public async setItemDirty(itemToLookupUuidFor: Models.DecryptedItemInterface, isUserModified = false) {
|
||||
const result = await this.setItemsDirty([itemToLookupUuidFor], isUserModified)
|
||||
return result[0]
|
||||
}
|
||||
|
||||
public async setItemsDirty(
|
||||
itemsToLookupUuidsFor: Models.DecryptedItemInterface[],
|
||||
isUserModified = false,
|
||||
): Promise<Models.DecryptedItemInterface[]> {
|
||||
return this.changeItems(
|
||||
itemsToLookupUuidsFor,
|
||||
undefined,
|
||||
isUserModified ? Models.MutationType.UpdateUserTimestamps : Models.MutationType.NoUpdateUserTimestamps,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of items that need to be synced.
|
||||
*/
|
||||
@@ -760,47 +532,6 @@ export class ItemManager
|
||||
return this.collection.dirtyElements().filter(Models.isDecryptedOrDeletedItem)
|
||||
}
|
||||
|
||||
/**
|
||||
* Duplicates an item and maps it, thus propagating the item to observers.
|
||||
* @param isConflict - Whether to mark the duplicate as a conflict of the original.
|
||||
*/
|
||||
public async duplicateItem<T extends Models.DecryptedItemInterface>(
|
||||
itemToLookupUuidFor: T,
|
||||
isConflict = false,
|
||||
additionalContent?: Partial<Models.ItemContent>,
|
||||
) {
|
||||
const item = this.findSureItem(itemToLookupUuidFor.uuid)
|
||||
const payload = item.payload.copy()
|
||||
const resultingPayloads = Models.PayloadsByDuplicating({
|
||||
payload,
|
||||
baseCollection: this.payloadManager.getMasterCollection(),
|
||||
isConflict,
|
||||
additionalContent,
|
||||
})
|
||||
|
||||
await this.payloadManager.emitPayloads(resultingPayloads, Models.PayloadEmitSource.LocalChanged)
|
||||
const duplicate = this.findSureItem<T>(resultingPayloads[0].uuid)
|
||||
return duplicate
|
||||
}
|
||||
|
||||
public async createItem<T extends Models.DecryptedItemInterface, C extends Models.ItemContent = Models.ItemContent>(
|
||||
contentType: ContentType,
|
||||
content: C,
|
||||
needsSync = false,
|
||||
): Promise<T> {
|
||||
const payload = new Models.DecryptedPayload<C>({
|
||||
uuid: UuidGenerator.GenerateUuid(),
|
||||
content_type: contentType,
|
||||
content: Models.FillItemContent<C>(content),
|
||||
dirty: needsSync,
|
||||
...Models.PayloadTimestampDefaults(),
|
||||
})
|
||||
|
||||
await this.payloadManager.emitPayload(payload, Models.PayloadEmitSource.LocalInserted)
|
||||
|
||||
return this.findSureItem<T>(payload.uuid)
|
||||
}
|
||||
|
||||
public createTemplateItem<
|
||||
C extends Models.ItemContent = Models.ItemContent,
|
||||
I extends Models.DecryptedItemInterface<C> = Models.DecryptedItemInterface<C>,
|
||||
@@ -824,75 +555,6 @@ export class ItemManager
|
||||
return !this.findItem(item.uuid)
|
||||
}
|
||||
|
||||
public async insertItem(item: Models.DecryptedItemInterface): Promise<Models.DecryptedItemInterface> {
|
||||
return this.emitItemFromPayload(item.payload, Models.PayloadEmitSource.LocalChanged)
|
||||
}
|
||||
|
||||
public async insertItems(
|
||||
items: Models.DecryptedItemInterface[],
|
||||
emitSource: Models.PayloadEmitSource = Models.PayloadEmitSource.LocalInserted,
|
||||
): Promise<Models.DecryptedItemInterface[]> {
|
||||
return this.emitItemsFromPayloads(
|
||||
items.map((item) => item.payload),
|
||||
emitSource,
|
||||
)
|
||||
}
|
||||
|
||||
public async emitItemFromPayload(
|
||||
payload: Models.DecryptedPayloadInterface,
|
||||
emitSource: Models.PayloadEmitSource,
|
||||
): Promise<Models.DecryptedItemInterface> {
|
||||
await this.payloadManager.emitPayload(payload, emitSource)
|
||||
|
||||
return this.findSureItem(payload.uuid)
|
||||
}
|
||||
|
||||
public async emitItemsFromPayloads(
|
||||
payloads: Models.DecryptedPayloadInterface[],
|
||||
emitSource: Models.PayloadEmitSource,
|
||||
): Promise<Models.DecryptedItemInterface[]> {
|
||||
await this.payloadManager.emitPayloads(payloads, emitSource)
|
||||
|
||||
const uuids = Uuids(payloads)
|
||||
|
||||
return this.findItems(uuids)
|
||||
}
|
||||
|
||||
public async setItemToBeDeleted(
|
||||
itemToLookupUuidFor: Models.DecryptedItemInterface | Models.EncryptedItemInterface,
|
||||
source: Models.PayloadEmitSource = Models.PayloadEmitSource.LocalChanged,
|
||||
): Promise<void> {
|
||||
const referencingIdsCapturedBeforeChanges = this.collection.uuidsThatReferenceUuid(itemToLookupUuidFor.uuid)
|
||||
|
||||
const item = this.findAnyItem(itemToLookupUuidFor.uuid)
|
||||
|
||||
if (!item) {
|
||||
return
|
||||
}
|
||||
|
||||
const mutator = new Models.DeleteItemMutator(item, Models.MutationType.UpdateUserTimestamps)
|
||||
|
||||
const deletedPayload = mutator.getDeletedResult()
|
||||
|
||||
await this.payloadManager.emitPayload(deletedPayload, source)
|
||||
|
||||
for (const referencingId of referencingIdsCapturedBeforeChanges) {
|
||||
const referencingItem = this.findItem(referencingId)
|
||||
|
||||
if (referencingItem) {
|
||||
await this.changeItem(referencingItem, (mutator) => {
|
||||
mutator.removeItemAsRelationship(item)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async setItemsToBeDeleted(
|
||||
itemsToLookupUuidsFor: (Models.DecryptedItemInterface | Models.EncryptedItemInterface)[],
|
||||
): Promise<void> {
|
||||
await Promise.all(itemsToLookupUuidsFor.map((item) => this.setItemToBeDeleted(item)))
|
||||
}
|
||||
|
||||
public getItems<T extends Models.DecryptedItemInterface>(contentType: ContentType | ContentType[]): T[] {
|
||||
return this.collection.allDecrypted<T>(contentType)
|
||||
}
|
||||
@@ -1018,20 +680,6 @@ export class ItemManager
|
||||
return chain
|
||||
}
|
||||
|
||||
public async findOrCreateTagParentChain(titlesHierarchy: string[]): Promise<Models.SNTag> {
|
||||
let current: Models.SNTag | undefined = undefined
|
||||
|
||||
for (const title of titlesHierarchy) {
|
||||
current = await this.findOrCreateTagByTitle(title, current)
|
||||
}
|
||||
|
||||
if (!current) {
|
||||
throw new Error('Invalid tag hierarchy')
|
||||
}
|
||||
|
||||
return current
|
||||
}
|
||||
|
||||
public getTagChildren(itemToLookupUuidFor: Models.SNTag): Models.SNTag[] {
|
||||
const tag = this.findItem<Models.SNTag>(itemToLookupUuidFor.uuid)
|
||||
if (!tag) {
|
||||
@@ -1079,117 +727,12 @@ export class ItemManager
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns The changed child tag
|
||||
*/
|
||||
public setTagParent(parentTag: Models.SNTag, childTag: Models.SNTag): Promise<Models.SNTag> {
|
||||
if (parentTag.uuid === childTag.uuid) {
|
||||
throw new Error('Can not set a tag parent of itself')
|
||||
}
|
||||
|
||||
if (this.isTagAncestor(childTag, parentTag)) {
|
||||
throw new Error('Can not set a tag ancestor of itself')
|
||||
}
|
||||
|
||||
return this.changeTag(childTag, (m) => {
|
||||
m.makeChildOf(parentTag)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns The changed child tag
|
||||
*/
|
||||
public unsetTagParent(childTag: Models.SNTag): Promise<Models.SNTag> {
|
||||
const parentTag = this.getTagParent(childTag)
|
||||
|
||||
if (!parentTag) {
|
||||
return Promise.resolve(childTag)
|
||||
}
|
||||
|
||||
return this.changeTag(childTag, (m) => {
|
||||
m.unsetParent()
|
||||
})
|
||||
}
|
||||
|
||||
public async associateFileWithNote(file: Models.FileItem, note: Models.SNNote): Promise<Models.FileItem> {
|
||||
return this.changeItem<Models.FileMutator, Models.FileItem>(file, (mutator) => {
|
||||
mutator.addNote(note)
|
||||
})
|
||||
}
|
||||
|
||||
public async disassociateFileWithNote(file: Models.FileItem, note: Models.SNNote): Promise<Models.FileItem> {
|
||||
return this.changeItem<Models.FileMutator, Models.FileItem>(file, (mutator) => {
|
||||
mutator.removeNote(note)
|
||||
})
|
||||
}
|
||||
|
||||
public async addTagToNote(note: Models.SNNote, tag: Models.SNTag, addHierarchy: boolean): Promise<Models.SNTag[]> {
|
||||
let tagsToAdd = [tag]
|
||||
|
||||
if (addHierarchy) {
|
||||
const parentChainTags = this.getTagParentChain(tag)
|
||||
tagsToAdd = [...parentChainTags, tag]
|
||||
}
|
||||
|
||||
return Promise.all(
|
||||
tagsToAdd.map((tagToAdd) => {
|
||||
return this.changeTag(tagToAdd, (mutator) => {
|
||||
mutator.addNote(note)
|
||||
}) as Promise<Models.SNTag>
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
public async addTagToFile(file: Models.FileItem, tag: Models.SNTag, addHierarchy: boolean): Promise<Models.SNTag[]> {
|
||||
let tagsToAdd = [tag]
|
||||
|
||||
if (addHierarchy) {
|
||||
const parentChainTags = this.getTagParentChain(tag)
|
||||
tagsToAdd = [...parentChainTags, tag]
|
||||
}
|
||||
|
||||
return Promise.all(
|
||||
tagsToAdd.map((tagToAdd) => {
|
||||
return this.changeTag(tagToAdd, (mutator) => {
|
||||
mutator.addFile(file)
|
||||
}) as Promise<Models.SNTag>
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
public async linkNoteToNote(note: Models.SNNote, otherNote: Models.SNNote): Promise<Models.SNNote> {
|
||||
return this.changeItem<Models.NoteMutator, Models.SNNote>(note, (mutator) => {
|
||||
mutator.addNote(otherNote)
|
||||
})
|
||||
}
|
||||
|
||||
public async linkFileToFile(file: Models.FileItem, otherFile: Models.FileItem): Promise<Models.FileItem> {
|
||||
return this.changeItem<Models.FileMutator, Models.FileItem>(file, (mutator) => {
|
||||
mutator.addFile(otherFile)
|
||||
})
|
||||
}
|
||||
|
||||
public async unlinkItems(itemA: DecryptedItemInterface<ItemContent>, itemB: DecryptedItemInterface<ItemContent>) {
|
||||
const relationshipDirection = this.relationshipDirectionBetweenItems(itemA, itemB)
|
||||
|
||||
if (relationshipDirection === ItemRelationshipDirection.NoRelationship) {
|
||||
throw new Error('Trying to unlink already unlinked items')
|
||||
}
|
||||
|
||||
const itemToChange = relationshipDirection === ItemRelationshipDirection.AReferencesB ? itemA : itemB
|
||||
const itemToRemove = itemToChange === itemA ? itemB : itemA
|
||||
|
||||
return this.changeItem(itemToChange, (mutator) => {
|
||||
mutator.removeItemAsRelationship(itemToRemove)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Get tags for a note sorted in natural order
|
||||
* @param item - The item whose tags will be returned
|
||||
* @returns Array containing tags associated with an item
|
||||
*/
|
||||
public getSortedTagsForItem(item: DecryptedItemInterface<ItemContent>): Models.SNTag[] {
|
||||
public getSortedTagsForItem(item: Models.DecryptedItemInterface<Models.ItemContent>): Models.SNTag[] {
|
||||
return naturalSort(
|
||||
this.itemsReferencingItem(item).filter((ref) => {
|
||||
return ref?.content_type === ContentType.Tag
|
||||
@@ -1198,81 +741,16 @@ export class ItemManager
|
||||
)
|
||||
}
|
||||
|
||||
public async createTag(title: string, parentItemToLookupUuidFor?: Models.SNTag): Promise<Models.SNTag> {
|
||||
const newTag = await this.createItem<Models.SNTag>(
|
||||
ContentType.Tag,
|
||||
Models.FillItemContent<Models.TagContent>({ title }),
|
||||
true,
|
||||
)
|
||||
|
||||
if (parentItemToLookupUuidFor) {
|
||||
const parentTag = this.findItem<Models.SNTag>(parentItemToLookupUuidFor.uuid)
|
||||
if (!parentTag) {
|
||||
throw new Error('Invalid parent tag')
|
||||
}
|
||||
return this.changeTag(newTag, (m) => {
|
||||
m.makeChildOf(parentTag)
|
||||
})
|
||||
}
|
||||
|
||||
return newTag
|
||||
}
|
||||
|
||||
public async createSmartView<T extends Models.DecryptedItemInterface>(
|
||||
title: string,
|
||||
predicate: Models.PredicateInterface<T>,
|
||||
iconString?: string,
|
||||
): Promise<Models.SmartView> {
|
||||
return this.createItem(
|
||||
ContentType.SmartView,
|
||||
Models.FillItemContent({
|
||||
title,
|
||||
predicate: predicate.toJson(),
|
||||
iconString: iconString || SmartViewDefaultIconName,
|
||||
} as Models.SmartViewContent),
|
||||
true,
|
||||
) as Promise<Models.SmartView>
|
||||
}
|
||||
|
||||
public async createSmartViewFromDSL<T extends Models.DecryptedItemInterface>(dsl: string): Promise<Models.SmartView> {
|
||||
let components = null
|
||||
try {
|
||||
components = JSON.parse(dsl.substring(1, dsl.length))
|
||||
} catch (e) {
|
||||
throw Error('Invalid smart view syntax')
|
||||
}
|
||||
|
||||
const title = components[0]
|
||||
const predicate = Models.predicateFromDSLString<T>(dsl)
|
||||
return this.createSmartView(title, predicate)
|
||||
}
|
||||
|
||||
public async createTagOrSmartView(title: string): Promise<Models.SNTag | Models.SmartView> {
|
||||
if (this.isSmartViewTitle(title)) {
|
||||
return this.createSmartViewFromDSL(title)
|
||||
} else {
|
||||
return this.createTag(title)
|
||||
}
|
||||
}
|
||||
|
||||
public isSmartViewTitle(title: string): boolean {
|
||||
return title.startsWith(Models.SMART_TAG_DSL_PREFIX)
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds or creates a tag with a given title
|
||||
*/
|
||||
public async findOrCreateTagByTitle(title: string, parentItemToLookupUuidFor?: Models.SNTag): Promise<Models.SNTag> {
|
||||
const tag = this.findTagByTitleAndParent(title, parentItemToLookupUuidFor)
|
||||
return tag || this.createTag(title, parentItemToLookupUuidFor)
|
||||
}
|
||||
|
||||
public notesMatchingSmartView(view: Models.SmartView): Models.SNNote[] {
|
||||
const criteria: Models.FilterDisplayOptions = {
|
||||
const criteria: Models.NotesAndFilesDisplayOptions = {
|
||||
views: [view],
|
||||
}
|
||||
|
||||
return Models.itemsMatchingOptions(
|
||||
return Models.notesAndFilesMatchingOptions(
|
||||
criteria,
|
||||
this.collection.allDecrypted(ContentType.Note),
|
||||
this.collection,
|
||||
@@ -1299,14 +777,6 @@ export class ItemManager
|
||||
return this.notesMatchingSmartView(this.trashSmartView)
|
||||
}
|
||||
|
||||
/**
|
||||
* Permanently deletes any items currently in the trash. Consumer must manually call sync.
|
||||
*/
|
||||
public async emptyTrash(): Promise<void> {
|
||||
const notes = this.trashedItems
|
||||
await this.setItemsToBeDeleted(notes)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all smart views, sorted by title.
|
||||
*/
|
||||
@@ -1346,53 +816,29 @@ export class ItemManager
|
||||
this.payloadManager.resetState()
|
||||
}
|
||||
|
||||
public removeItemLocally(item: Models.DecryptedItemInterface | Models.DeletedItemInterface): void {
|
||||
this.collection.discard([item])
|
||||
this.payloadManager.removePayloadLocally(item.payload)
|
||||
/**
|
||||
* Important: Caller must coordinate with storage service separately to delete item from persistent database.
|
||||
*/
|
||||
public removeItemLocally(item: Models.AnyItemInterface): void {
|
||||
this.removeItemsLocally([item])
|
||||
}
|
||||
|
||||
const delta = Models.CreateItemDelta({ discarded: [item] as Models.DeletedItemInterface[] })
|
||||
/**
|
||||
* Important: Caller must coordinate with storage service separately to delete item from persistent database.
|
||||
*/
|
||||
public removeItemsLocally(items: Models.AnyItemInterface[]): void {
|
||||
this.collection.discard(items)
|
||||
this.payloadManager.removePayloadLocally(items.map((item) => item.payload))
|
||||
|
||||
const delta = Models.CreateItemDelta({ discarded: items as Models.DeletedItemInterface[] })
|
||||
const affectedContentTypes = items.map((item) => item.content_type)
|
||||
for (const controller of this.allDisplayControllers) {
|
||||
if (controller.contentTypes.some((ct) => ct === item.content_type)) {
|
||||
if (controller.contentTypes.some((ct) => affectedContentTypes.includes(ct))) {
|
||||
controller.onCollectionChange(delta)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public renameFile(file: Models.FileItem, name: string): Promise<Models.FileItem> {
|
||||
return this.changeItem<Models.FileMutator, Models.FileItem>(file, (mutator) => {
|
||||
mutator.name = name
|
||||
})
|
||||
}
|
||||
|
||||
public async setLastSyncBeganForItems(
|
||||
itemsToLookupUuidsFor: (Models.DecryptedItemInterface | Models.DeletedItemInterface)[],
|
||||
date: Date,
|
||||
globalDirtyIndex: number,
|
||||
): Promise<(Models.DecryptedItemInterface | Models.DeletedItemInterface)[]> {
|
||||
const uuids = Uuids(itemsToLookupUuidsFor)
|
||||
|
||||
const items = this.collection.findAll(uuids).filter(Models.isDecryptedOrDeletedItem)
|
||||
|
||||
const payloads: (Models.DecryptedPayloadInterface | Models.DeletedPayloadInterface)[] = []
|
||||
|
||||
for (const item of items) {
|
||||
const mutator = new Models.ItemMutator<Models.DecryptedPayloadInterface | Models.DeletedPayloadInterface>(
|
||||
item,
|
||||
Models.MutationType.NonDirtying,
|
||||
)
|
||||
|
||||
mutator.setBeginSync(date, globalDirtyIndex)
|
||||
|
||||
const payload = mutator.getResult()
|
||||
|
||||
payloads.push(payload)
|
||||
}
|
||||
|
||||
await this.payloadManager.emitPayloads(payloads, Models.PayloadEmitSource.PreSyncSave)
|
||||
|
||||
return this.findAnyItems(uuids) as (Models.DecryptedItemInterface | Models.DeletedItemInterface)[]
|
||||
}
|
||||
|
||||
public relationshipDirectionBetweenItems(
|
||||
itemA: Models.DecryptedItemInterface<Models.ItemContent>,
|
||||
itemB: Models.DecryptedItemInterface<Models.ItemContent>,
|
||||
@@ -1407,12 +853,8 @@ export class ItemManager
|
||||
: ItemRelationshipDirection.NoRelationship
|
||||
}
|
||||
|
||||
override getDiagnostics(): Promise<DiagnosticInfo | undefined> {
|
||||
return Promise.resolve({
|
||||
items: {
|
||||
allIds: Uuids(this.collection.all()),
|
||||
},
|
||||
})
|
||||
itemsBelongingToKeySystem(systemIdentifier: Models.KeySystemIdentifier): Models.DecryptedItemInterface[] {
|
||||
return this.items.filter((item) => item.key_system_identifier === systemIdentifier)
|
||||
}
|
||||
|
||||
public conflictsOf(uuid: string) {
|
||||
@@ -1422,4 +864,8 @@ export class ItemManager
|
||||
public numberOfNotesWithConflicts(): number {
|
||||
return this.findItems(this.collection.uuidsOfItemsWithConflicts()).filter(Models.isNote).length
|
||||
}
|
||||
|
||||
getNoteLinkedFiles(note: Models.SNNote): Models.FileItem[] {
|
||||
return this.itemsReferencingItem(note).filter(Models.isFile)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -312,7 +312,7 @@ export class SNKeyRecoveryService extends AbstractService<KeyRecoveryEvent, Decr
|
||||
|
||||
private addKeysToQueue(keys: EncryptedPayloadInterface[]) {
|
||||
for (const key of keys) {
|
||||
const keyParams = this.protocolService.getKeyEmbeddedKeyParams(key)
|
||||
const keyParams = this.protocolService.getKeyEmbeddedKeyParamsFromItemsKey(key)
|
||||
if (!keyParams) {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { SyncClientInterface } from './../Sync/SyncClientInterface'
|
||||
import { isString, lastElement, sleep } from '@standardnotes/utils'
|
||||
import { UuidString } from '@Lib/Types/UuidString'
|
||||
import { ContentType } from '@standardnotes/common'
|
||||
@@ -19,7 +20,8 @@ export class ListedService extends AbstractService implements ListedClientInterf
|
||||
private settingsService: SNSettingsService,
|
||||
private httpSerivce: DeprecatedHttpService,
|
||||
private protectionService: SNProtectionService,
|
||||
private mutatorService: MutatorClientInterface,
|
||||
private mutator: MutatorClientInterface,
|
||||
private sync: SyncClientInterface,
|
||||
protected override internalEventBus: InternalEventBusInterface,
|
||||
) {
|
||||
super(internalEventBus)
|
||||
@@ -31,7 +33,7 @@ export class ListedService extends AbstractService implements ListedClientInterf
|
||||
;(this.apiService as unknown) = undefined
|
||||
;(this.httpSerivce as unknown) = undefined
|
||||
;(this.protectionService as unknown) = undefined
|
||||
;(this.mutatorService as unknown) = undefined
|
||||
;(this.mutator as unknown) = undefined
|
||||
super.deinit()
|
||||
}
|
||||
|
||||
@@ -49,10 +51,12 @@ export class ListedService extends AbstractService implements ListedClientInterf
|
||||
return false
|
||||
}
|
||||
|
||||
await this.mutatorService.changeAndSaveItem<NoteMutator>(note, (mutator) => {
|
||||
await this.mutator.changeItem<NoteMutator>(note, (mutator) => {
|
||||
mutator.authorizedForListed = true
|
||||
})
|
||||
|
||||
void this.sync.sync()
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { SNHistoryManager } from './../History/HistoryManager'
|
||||
import { NoteContent, SNNote, FillItemContent, DecryptedPayload, PayloadTimestampDefaults } from '@standardnotes/models'
|
||||
import { ContentType } from '@standardnotes/common'
|
||||
import { EncryptionService, InternalEventBusInterface } from '@standardnotes/services'
|
||||
import {
|
||||
ChallengeService,
|
||||
MutatorService,
|
||||
PayloadManager,
|
||||
SNComponentManager,
|
||||
SNProtectionService,
|
||||
ItemManager,
|
||||
SNSyncService,
|
||||
} from '../'
|
||||
NoteContent,
|
||||
SNNote,
|
||||
FillItemContent,
|
||||
DecryptedPayload,
|
||||
PayloadTimestampDefaults,
|
||||
MutationType,
|
||||
FileItem,
|
||||
SNTag,
|
||||
} from '@standardnotes/models'
|
||||
import { ContentType } from '@standardnotes/common'
|
||||
import { AlertService, InternalEventBusInterface } from '@standardnotes/services'
|
||||
import { MutatorService, PayloadManager, ItemManager } from '../'
|
||||
import { UuidGenerator } from '@standardnotes/utils'
|
||||
|
||||
const setupRandomUuid = () => {
|
||||
@@ -21,12 +21,6 @@ describe('mutator service', () => {
|
||||
let mutatorService: MutatorService
|
||||
let payloadManager: PayloadManager
|
||||
let itemManager: ItemManager
|
||||
let syncService: SNSyncService
|
||||
let protectionService: SNProtectionService
|
||||
let protocolService: EncryptionService
|
||||
let challengeService: ChallengeService
|
||||
let componentManager: SNComponentManager
|
||||
let historyService: SNHistoryManager
|
||||
|
||||
let internalEventBus: InternalEventBusInterface
|
||||
|
||||
@@ -38,17 +32,10 @@ describe('mutator service', () => {
|
||||
payloadManager = new PayloadManager(internalEventBus)
|
||||
itemManager = new ItemManager(payloadManager, internalEventBus)
|
||||
|
||||
mutatorService = new MutatorService(
|
||||
itemManager,
|
||||
syncService,
|
||||
protectionService,
|
||||
protocolService,
|
||||
payloadManager,
|
||||
challengeService,
|
||||
componentManager,
|
||||
historyService,
|
||||
internalEventBus,
|
||||
)
|
||||
const alerts = {} as jest.Mocked<AlertService>
|
||||
alerts.alert = jest.fn()
|
||||
|
||||
mutatorService = new MutatorService(itemManager, payloadManager, alerts, internalEventBus)
|
||||
})
|
||||
|
||||
const insertNote = (title: string) => {
|
||||
@@ -73,10 +60,76 @@ describe('mutator service', () => {
|
||||
(mutator) => {
|
||||
mutator.pinned = true
|
||||
},
|
||||
false,
|
||||
MutationType.NoUpdateUserTimestamps,
|
||||
)
|
||||
|
||||
expect(note.userModifiedDate).toEqual(pinnedNote?.userModifiedDate)
|
||||
})
|
||||
})
|
||||
|
||||
describe('linking', () => {
|
||||
it('attempting to link file and note should not be allowed if items belong to different vaults', async () => {
|
||||
const note = {
|
||||
uuid: 'note',
|
||||
key_system_identifier: '123',
|
||||
} as jest.Mocked<SNNote>
|
||||
|
||||
const file = {
|
||||
uuid: 'file',
|
||||
key_system_identifier: '456',
|
||||
} as jest.Mocked<FileItem>
|
||||
|
||||
const result = await mutatorService.associateFileWithNote(file, note)
|
||||
|
||||
expect(result).toBeUndefined()
|
||||
})
|
||||
|
||||
it('attempting to link vaulted tag with non vaulted note should not be permissable', async () => {
|
||||
const note = {
|
||||
uuid: 'note',
|
||||
key_system_identifier: undefined,
|
||||
} as jest.Mocked<SNNote>
|
||||
|
||||
const tag = {
|
||||
uuid: 'tag',
|
||||
key_system_identifier: '456',
|
||||
} as jest.Mocked<SNTag>
|
||||
|
||||
const result = await mutatorService.addTagToNote(note, tag, true)
|
||||
|
||||
expect(result).toBeUndefined()
|
||||
})
|
||||
|
||||
it('attempting to link vaulted tag with non vaulted file should not be permissable', async () => {
|
||||
const tag = {
|
||||
uuid: 'tag',
|
||||
key_system_identifier: '456',
|
||||
} as jest.Mocked<SNTag>
|
||||
|
||||
const file = {
|
||||
uuid: 'file',
|
||||
key_system_identifier: undefined,
|
||||
} as jest.Mocked<FileItem>
|
||||
|
||||
const result = await mutatorService.addTagToFile(file, tag, true)
|
||||
|
||||
expect(result).toBeUndefined()
|
||||
})
|
||||
|
||||
it('attempting to link vaulted tag with note belonging to different vault should not be perpermissable', async () => {
|
||||
const note = {
|
||||
uuid: 'note',
|
||||
key_system_identifier: '123',
|
||||
} as jest.Mocked<SNNote>
|
||||
|
||||
const tag = {
|
||||
uuid: 'tag',
|
||||
key_system_identifier: '456',
|
||||
} as jest.Mocked<SNTag>
|
||||
|
||||
const result = await mutatorService.addTagToNote(note, tag, true)
|
||||
|
||||
expect(result).toBeUndefined()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,62 +1,60 @@
|
||||
import { SNHistoryManager } from './../History/HistoryManager'
|
||||
import {
|
||||
AbstractService,
|
||||
InternalEventBusInterface,
|
||||
SyncOptions,
|
||||
ChallengeValidation,
|
||||
ChallengePrompt,
|
||||
ChallengeReason,
|
||||
MutatorClientInterface,
|
||||
Challenge,
|
||||
InfoStrings,
|
||||
ItemRelationshipDirection,
|
||||
AlertService,
|
||||
} from '@standardnotes/services'
|
||||
import { EncryptionProviderInterface } from '@standardnotes/encryption'
|
||||
import { ClientDisplayableError } from '@standardnotes/responses'
|
||||
import { ContentType, ProtocolVersion, compareVersions } from '@standardnotes/common'
|
||||
import { ItemsKeyMutator, SNItemsKey } from '@standardnotes/encryption'
|
||||
import { ContentType } from '@standardnotes/common'
|
||||
import { ItemManager } from '../Items'
|
||||
import { PayloadManager } from '../Payloads/PayloadManager'
|
||||
import { SNComponentManager } from '../ComponentManager/ComponentManager'
|
||||
import { SNProtectionService } from '../Protection/ProtectionService'
|
||||
import { SNSyncService } from '../Sync'
|
||||
import { Strings } from '../../Strings'
|
||||
import { TagsToFoldersMigrationApplicator } from '@Lib/Migrations/Applicators/TagsToFolders'
|
||||
import { ChallengeService } from '../Challenge'
|
||||
import {
|
||||
BackupFile,
|
||||
BackupFileDecryptedContextualPayload,
|
||||
ComponentContent,
|
||||
CopyPayloadWithContentOverride,
|
||||
CreateDecryptedBackupFileContextPayload,
|
||||
ActionsExtensionMutator,
|
||||
ComponentMutator,
|
||||
CreateDecryptedMutatorForItem,
|
||||
CreateEncryptedBackupFileContextPayload,
|
||||
DecryptedItemInterface,
|
||||
DecryptedItemMutator,
|
||||
DecryptedPayload,
|
||||
DecryptedPayloadInterface,
|
||||
DeleteItemMutator,
|
||||
EncryptedItemInterface,
|
||||
FeatureRepoMutator,
|
||||
FileItem,
|
||||
isDecryptedPayload,
|
||||
isEncryptedTransferPayload,
|
||||
FileMutator,
|
||||
FillItemContent,
|
||||
ItemContent,
|
||||
ItemsKeyInterface,
|
||||
ItemsKeyMutatorInterface,
|
||||
MutationType,
|
||||
NoteMutator,
|
||||
PayloadEmitSource,
|
||||
PayloadsByDuplicating,
|
||||
PayloadTimestampDefaults,
|
||||
PayloadVaultOverrides,
|
||||
predicateFromDSLString,
|
||||
PredicateInterface,
|
||||
SmartView,
|
||||
SmartViewContent,
|
||||
SmartViewDefaultIconName,
|
||||
SNActionsExtension,
|
||||
SNComponent,
|
||||
SNFeatureRepo,
|
||||
SNNote,
|
||||
SNTag,
|
||||
TagContent,
|
||||
TagMutator,
|
||||
TransactionalMutation,
|
||||
VaultListingInterface,
|
||||
} from '@standardnotes/models'
|
||||
import { UuidGenerator, Uuids } from '@standardnotes/utils'
|
||||
|
||||
export class MutatorService extends AbstractService implements MutatorClientInterface {
|
||||
constructor(
|
||||
private itemManager: ItemManager,
|
||||
private syncService: SNSyncService,
|
||||
private protectionService: SNProtectionService,
|
||||
private encryption: EncryptionProviderInterface,
|
||||
private payloadManager: PayloadManager,
|
||||
private challengeService: ChallengeService,
|
||||
private componentManager: SNComponentManager,
|
||||
private historyService: SNHistoryManager,
|
||||
private alerts: AlertService,
|
||||
protected override internalEventBus: InternalEventBusInterface,
|
||||
) {
|
||||
super(internalEventBus)
|
||||
@@ -65,86 +63,98 @@ export class MutatorService extends AbstractService implements MutatorClientInte
|
||||
override deinit() {
|
||||
super.deinit()
|
||||
;(this.itemManager as unknown) = undefined
|
||||
;(this.syncService as unknown) = undefined
|
||||
;(this.protectionService as unknown) = undefined
|
||||
;(this.encryption as unknown) = undefined
|
||||
;(this.payloadManager as unknown) = undefined
|
||||
;(this.challengeService as unknown) = undefined
|
||||
;(this.componentManager as unknown) = undefined
|
||||
;(this.historyService as unknown) = undefined
|
||||
}
|
||||
|
||||
public async insertItem(item: DecryptedItemInterface): Promise<DecryptedItemInterface> {
|
||||
const mutator = CreateDecryptedMutatorForItem(item, MutationType.UpdateUserTimestamps)
|
||||
const dirtiedPayload = mutator.getResult()
|
||||
const insertedItem = await this.itemManager.emitItemFromPayload(dirtiedPayload, PayloadEmitSource.LocalInserted)
|
||||
return insertedItem
|
||||
}
|
||||
|
||||
public async changeAndSaveItem<M extends DecryptedItemMutator = DecryptedItemMutator>(
|
||||
itemToLookupUuidFor: DecryptedItemInterface,
|
||||
mutate: (mutator: M) => void,
|
||||
updateTimestamps = true,
|
||||
emitSource?: PayloadEmitSource,
|
||||
syncOptions?: SyncOptions,
|
||||
): Promise<DecryptedItemInterface | undefined> {
|
||||
await this.itemManager.changeItems(
|
||||
/**
|
||||
* Consumers wanting to modify an item should run it through this block,
|
||||
* so that data is properly mapped through our function, and latest state
|
||||
* is properly reconciled.
|
||||
*/
|
||||
public async changeItem<
|
||||
M extends DecryptedItemMutator = DecryptedItemMutator,
|
||||
I extends DecryptedItemInterface = DecryptedItemInterface,
|
||||
>(
|
||||
itemToLookupUuidFor: I,
|
||||
mutate?: (mutator: M) => void,
|
||||
mutationType: MutationType = MutationType.UpdateUserTimestamps,
|
||||
emitSource = PayloadEmitSource.LocalChanged,
|
||||
payloadSourceKey?: string,
|
||||
): Promise<I> {
|
||||
const results = await this.changeItems<M, I>(
|
||||
[itemToLookupUuidFor],
|
||||
mutate,
|
||||
updateTimestamps ? MutationType.UpdateUserTimestamps : MutationType.NoUpdateUserTimestamps,
|
||||
mutationType,
|
||||
emitSource,
|
||||
payloadSourceKey,
|
||||
)
|
||||
await this.syncService.sync(syncOptions)
|
||||
return this.itemManager.findItem(itemToLookupUuidFor.uuid)
|
||||
return results[0]
|
||||
}
|
||||
|
||||
public async changeAndSaveItems<M extends DecryptedItemMutator = DecryptedItemMutator>(
|
||||
itemsToLookupUuidsFor: DecryptedItemInterface[],
|
||||
mutate: (mutator: M) => void,
|
||||
updateTimestamps = true,
|
||||
emitSource?: PayloadEmitSource,
|
||||
syncOptions?: SyncOptions,
|
||||
): Promise<void> {
|
||||
await this.itemManager.changeItems(
|
||||
itemsToLookupUuidsFor,
|
||||
mutate,
|
||||
updateTimestamps ? MutationType.UpdateUserTimestamps : MutationType.NoUpdateUserTimestamps,
|
||||
emitSource,
|
||||
)
|
||||
await this.syncService.sync(syncOptions)
|
||||
}
|
||||
|
||||
public async changeItem<M extends DecryptedItemMutator>(
|
||||
itemToLookupUuidFor: DecryptedItemInterface,
|
||||
mutate: (mutator: M) => void,
|
||||
updateTimestamps = true,
|
||||
): Promise<DecryptedItemInterface | undefined> {
|
||||
await this.itemManager.changeItems(
|
||||
[itemToLookupUuidFor],
|
||||
mutate,
|
||||
updateTimestamps ? MutationType.UpdateUserTimestamps : MutationType.NoUpdateUserTimestamps,
|
||||
)
|
||||
return this.itemManager.findItem(itemToLookupUuidFor.uuid)
|
||||
}
|
||||
|
||||
public async changeItems<M extends DecryptedItemMutator = DecryptedItemMutator>(
|
||||
itemsToLookupUuidsFor: DecryptedItemInterface[],
|
||||
mutate: (mutator: M) => void,
|
||||
updateTimestamps = true,
|
||||
): Promise<(DecryptedItemInterface | undefined)[]> {
|
||||
return this.itemManager.changeItems(
|
||||
itemsToLookupUuidsFor,
|
||||
mutate,
|
||||
updateTimestamps ? MutationType.UpdateUserTimestamps : MutationType.NoUpdateUserTimestamps,
|
||||
)
|
||||
/**
|
||||
* @param mutate If not supplied, the intention would simply be to mark the item as dirty.
|
||||
*/
|
||||
public async changeItems<
|
||||
M extends DecryptedItemMutator = DecryptedItemMutator,
|
||||
I extends DecryptedItemInterface = DecryptedItemInterface,
|
||||
>(
|
||||
itemsToLookupUuidsFor: I[],
|
||||
mutate?: (mutator: M) => void,
|
||||
mutationType: MutationType = MutationType.UpdateUserTimestamps,
|
||||
emitSource = PayloadEmitSource.LocalChanged,
|
||||
payloadSourceKey?: string,
|
||||
): Promise<I[]> {
|
||||
const items = this.itemManager.findItemsIncludingBlanks(Uuids(itemsToLookupUuidsFor))
|
||||
const payloads: DecryptedPayloadInterface[] = []
|
||||
|
||||
for (const item of items) {
|
||||
if (!item) {
|
||||
throw Error('Attempting to change non-existant item')
|
||||
}
|
||||
const mutator = CreateDecryptedMutatorForItem(item, mutationType)
|
||||
if (mutate) {
|
||||
mutate(mutator as M)
|
||||
}
|
||||
const payload = mutator.getResult()
|
||||
payloads.push(payload)
|
||||
}
|
||||
|
||||
await this.payloadManager.emitPayloads(payloads, emitSource, payloadSourceKey)
|
||||
|
||||
const results = this.itemManager.findItems(payloads.map((p) => p.uuid)) as I[]
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
/**
|
||||
* Run unique mutations per each item in the array, then only propagate all changes
|
||||
* once all mutations have been run. This differs from `changeItems` in that changeItems
|
||||
* runs the same mutation on all items.
|
||||
*/
|
||||
public async runTransactionalMutations(
|
||||
transactions: TransactionalMutation[],
|
||||
emitSource = PayloadEmitSource.LocalChanged,
|
||||
payloadSourceKey?: string,
|
||||
): Promise<(DecryptedItemInterface | undefined)[]> {
|
||||
return this.itemManager.runTransactionalMutations(transactions, emitSource, payloadSourceKey)
|
||||
const payloads: DecryptedPayloadInterface[] = []
|
||||
|
||||
for (const transaction of transactions) {
|
||||
const item = this.itemManager.findItem(transaction.itemUuid)
|
||||
|
||||
if (!item) {
|
||||
continue
|
||||
}
|
||||
|
||||
const mutator = CreateDecryptedMutatorForItem(item, transaction.mutationType || MutationType.UpdateUserTimestamps)
|
||||
|
||||
transaction.mutate(mutator)
|
||||
const payload = mutator.getResult()
|
||||
payloads.push(payload)
|
||||
}
|
||||
|
||||
await this.payloadManager.emitPayloads(payloads, emitSource, payloadSourceKey)
|
||||
const results = this.itemManager.findItems(payloads.map((p) => p.uuid))
|
||||
return results
|
||||
}
|
||||
|
||||
public async runTransactionalMutation(
|
||||
@@ -152,97 +162,387 @@ export class MutatorService extends AbstractService implements MutatorClientInte
|
||||
emitSource = PayloadEmitSource.LocalChanged,
|
||||
payloadSourceKey?: string,
|
||||
): Promise<DecryptedItemInterface | undefined> {
|
||||
return this.itemManager.runTransactionalMutation(transaction, emitSource, payloadSourceKey)
|
||||
const item = this.itemManager.findSureItem(transaction.itemUuid)
|
||||
const mutator = CreateDecryptedMutatorForItem(item, transaction.mutationType || MutationType.UpdateUserTimestamps)
|
||||
transaction.mutate(mutator)
|
||||
const payload = mutator.getResult()
|
||||
|
||||
await this.payloadManager.emitPayloads([payload], emitSource, payloadSourceKey)
|
||||
const result = this.itemManager.findItem(payload.uuid)
|
||||
return result
|
||||
}
|
||||
|
||||
async protectItems<M extends DecryptedItemMutator, I extends DecryptedItemInterface>(items: I[]): Promise<I[]> {
|
||||
const protectedItems = await this.itemManager.changeItems<M, I>(
|
||||
items,
|
||||
(mutator) => {
|
||||
mutator.protected = true
|
||||
},
|
||||
MutationType.NoUpdateUserTimestamps,
|
||||
)
|
||||
async changeNote(
|
||||
itemToLookupUuidFor: SNNote,
|
||||
mutate: (mutator: NoteMutator) => void,
|
||||
mutationType: MutationType = MutationType.UpdateUserTimestamps,
|
||||
emitSource = PayloadEmitSource.LocalChanged,
|
||||
payloadSourceKey?: string,
|
||||
): Promise<DecryptedPayloadInterface[]> {
|
||||
const note = this.itemManager.findItem<SNNote>(itemToLookupUuidFor.uuid)
|
||||
if (!note) {
|
||||
throw Error('Attempting to change non-existant note')
|
||||
}
|
||||
const mutator = new NoteMutator(note, mutationType)
|
||||
|
||||
void this.syncService.sync()
|
||||
return protectedItems
|
||||
return this.applyTransform(mutator, mutate, emitSource, payloadSourceKey)
|
||||
}
|
||||
|
||||
async unprotectItems<M extends DecryptedItemMutator, I extends DecryptedItemInterface>(
|
||||
items: I[],
|
||||
reason: ChallengeReason,
|
||||
): Promise<I[] | undefined> {
|
||||
if (
|
||||
!(await this.protectionService.authorizeAction(reason, {
|
||||
fallBackToAccountPassword: true,
|
||||
requireAccountPassword: false,
|
||||
forcePrompt: false,
|
||||
}))
|
||||
) {
|
||||
return undefined
|
||||
async changeTag(
|
||||
itemToLookupUuidFor: SNTag,
|
||||
mutate: (mutator: TagMutator) => void,
|
||||
mutationType: MutationType = MutationType.UpdateUserTimestamps,
|
||||
emitSource = PayloadEmitSource.LocalChanged,
|
||||
payloadSourceKey?: string,
|
||||
): Promise<SNTag> {
|
||||
const tag = this.itemManager.findItem<SNTag>(itemToLookupUuidFor.uuid)
|
||||
if (!tag) {
|
||||
throw Error('Attempting to change non-existant tag')
|
||||
}
|
||||
const mutator = new TagMutator(tag, mutationType)
|
||||
await this.applyTransform(mutator, mutate, emitSource, payloadSourceKey)
|
||||
return this.itemManager.findSureItem<SNTag>(itemToLookupUuidFor.uuid)
|
||||
}
|
||||
|
||||
async changeComponent(
|
||||
itemToLookupUuidFor: SNComponent,
|
||||
mutate: (mutator: ComponentMutator) => void,
|
||||
mutationType: MutationType = MutationType.UpdateUserTimestamps,
|
||||
emitSource = PayloadEmitSource.LocalChanged,
|
||||
payloadSourceKey?: string,
|
||||
): Promise<SNComponent> {
|
||||
const component = this.itemManager.findItem<SNComponent>(itemToLookupUuidFor.uuid)
|
||||
if (!component) {
|
||||
throw Error('Attempting to change non-existant component')
|
||||
}
|
||||
const mutator = new ComponentMutator(component, mutationType)
|
||||
await this.applyTransform(mutator, mutate, emitSource, payloadSourceKey)
|
||||
return this.itemManager.findSureItem<SNComponent>(itemToLookupUuidFor.uuid)
|
||||
}
|
||||
|
||||
async changeFeatureRepo(
|
||||
itemToLookupUuidFor: SNFeatureRepo,
|
||||
mutate: (mutator: FeatureRepoMutator) => void,
|
||||
mutationType: MutationType = MutationType.UpdateUserTimestamps,
|
||||
emitSource = PayloadEmitSource.LocalChanged,
|
||||
payloadSourceKey?: string,
|
||||
): Promise<SNFeatureRepo> {
|
||||
const repo = this.itemManager.findItem(itemToLookupUuidFor.uuid)
|
||||
if (!repo) {
|
||||
throw Error('Attempting to change non-existant repo')
|
||||
}
|
||||
const mutator = new FeatureRepoMutator(repo, mutationType)
|
||||
await this.applyTransform(mutator, mutate, emitSource, payloadSourceKey)
|
||||
return this.itemManager.findSureItem<SNFeatureRepo>(itemToLookupUuidFor.uuid)
|
||||
}
|
||||
|
||||
async changeActionsExtension(
|
||||
itemToLookupUuidFor: SNActionsExtension,
|
||||
mutate: (mutator: ActionsExtensionMutator) => void,
|
||||
mutationType: MutationType = MutationType.UpdateUserTimestamps,
|
||||
emitSource = PayloadEmitSource.LocalChanged,
|
||||
payloadSourceKey?: string,
|
||||
): Promise<SNActionsExtension> {
|
||||
const extension = this.itemManager.findItem<SNActionsExtension>(itemToLookupUuidFor.uuid)
|
||||
if (!extension) {
|
||||
throw Error('Attempting to change non-existant extension')
|
||||
}
|
||||
const mutator = new ActionsExtensionMutator(extension, mutationType)
|
||||
await this.applyTransform(mutator, mutate, emitSource, payloadSourceKey)
|
||||
return this.itemManager.findSureItem<SNActionsExtension>(itemToLookupUuidFor.uuid)
|
||||
}
|
||||
|
||||
async changeItemsKey(
|
||||
itemToLookupUuidFor: ItemsKeyInterface,
|
||||
mutate: (mutator: ItemsKeyMutatorInterface) => void,
|
||||
mutationType: MutationType = MutationType.UpdateUserTimestamps,
|
||||
emitSource = PayloadEmitSource.LocalChanged,
|
||||
payloadSourceKey?: string,
|
||||
): Promise<ItemsKeyInterface> {
|
||||
const itemsKey = this.itemManager.findItem<SNItemsKey>(itemToLookupUuidFor.uuid)
|
||||
|
||||
if (!itemsKey) {
|
||||
throw Error('Attempting to change non-existant itemsKey')
|
||||
}
|
||||
|
||||
const unprotectedItems = await this.itemManager.changeItems<M, I>(
|
||||
items,
|
||||
(mutator) => {
|
||||
mutator.protected = false
|
||||
},
|
||||
MutationType.NoUpdateUserTimestamps,
|
||||
const mutator = new ItemsKeyMutator(itemsKey, mutationType)
|
||||
|
||||
await this.applyTransform(mutator, mutate, emitSource, payloadSourceKey)
|
||||
|
||||
return this.itemManager.findSureItem<ItemsKeyInterface>(itemToLookupUuidFor.uuid)
|
||||
}
|
||||
|
||||
private async applyTransform<T extends DecryptedItemMutator>(
|
||||
mutator: T,
|
||||
mutate: (mutator: T) => void,
|
||||
emitSource = PayloadEmitSource.LocalChanged,
|
||||
payloadSourceKey?: string,
|
||||
): Promise<DecryptedPayloadInterface[]> {
|
||||
mutate(mutator)
|
||||
const payload = mutator.getResult()
|
||||
return this.payloadManager.emitPayload(payload, emitSource, payloadSourceKey)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the item as needing sync. The item is then run through the mapping function,
|
||||
* and propagated to mapping observers.
|
||||
* @param isUserModified - Whether to update the item's "user modified date"
|
||||
*/
|
||||
public async setItemDirty(itemToLookupUuidFor: DecryptedItemInterface, isUserModified = false) {
|
||||
const result = await this.setItemsDirty([itemToLookupUuidFor], isUserModified)
|
||||
return result[0]
|
||||
}
|
||||
|
||||
public async setItemsDirty(
|
||||
itemsToLookupUuidsFor: DecryptedItemInterface[],
|
||||
isUserModified = false,
|
||||
): Promise<DecryptedItemInterface[]> {
|
||||
return this.changeItems(
|
||||
itemsToLookupUuidsFor,
|
||||
undefined,
|
||||
isUserModified ? MutationType.UpdateUserTimestamps : MutationType.NoUpdateUserTimestamps,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Duplicates an item and maps it, thus propagating the item to observers.
|
||||
* @param isConflict - Whether to mark the duplicate as a conflict of the original.
|
||||
*/
|
||||
public async duplicateItem<T extends DecryptedItemInterface>(
|
||||
itemToLookupUuidFor: T,
|
||||
isConflict = false,
|
||||
additionalContent?: Partial<ItemContent>,
|
||||
) {
|
||||
const item = this.itemManager.findSureItem(itemToLookupUuidFor.uuid)
|
||||
const payload = item.payload.copy()
|
||||
const resultingPayloads = PayloadsByDuplicating({
|
||||
payload,
|
||||
baseCollection: this.payloadManager.getMasterCollection(),
|
||||
isConflict,
|
||||
additionalContent,
|
||||
})
|
||||
|
||||
await this.payloadManager.emitPayloads(resultingPayloads, PayloadEmitSource.LocalChanged)
|
||||
const duplicate = this.itemManager.findSureItem<T>(resultingPayloads[0].uuid)
|
||||
|
||||
return duplicate
|
||||
}
|
||||
|
||||
public async createItem<T extends DecryptedItemInterface, C extends ItemContent = ItemContent>(
|
||||
contentType: ContentType,
|
||||
content: C,
|
||||
needsSync = false,
|
||||
vault?: VaultListingInterface,
|
||||
): Promise<T> {
|
||||
const payload = new DecryptedPayload<C>({
|
||||
uuid: UuidGenerator.GenerateUuid(),
|
||||
content_type: contentType,
|
||||
content: FillItemContent<C>(content),
|
||||
dirty: needsSync,
|
||||
...PayloadVaultOverrides(vault),
|
||||
...PayloadTimestampDefaults(),
|
||||
})
|
||||
|
||||
await this.payloadManager.emitPayload(payload, PayloadEmitSource.LocalInserted)
|
||||
|
||||
return this.itemManager.findSureItem<T>(payload.uuid)
|
||||
}
|
||||
|
||||
public async insertItem<T extends DecryptedItemInterface>(item: DecryptedItemInterface, setDirty = true): Promise<T> {
|
||||
if (setDirty) {
|
||||
const mutator = CreateDecryptedMutatorForItem(item, MutationType.UpdateUserTimestamps)
|
||||
const dirtiedPayload = mutator.getResult()
|
||||
const insertedItem = await this.emitItemFromPayload<T>(dirtiedPayload, PayloadEmitSource.LocalInserted)
|
||||
return insertedItem
|
||||
} else {
|
||||
return this.emitItemFromPayload(item.payload, PayloadEmitSource.LocalChanged)
|
||||
}
|
||||
}
|
||||
|
||||
public async insertItems(
|
||||
items: DecryptedItemInterface[],
|
||||
emitSource: PayloadEmitSource = PayloadEmitSource.LocalInserted,
|
||||
): Promise<DecryptedItemInterface[]> {
|
||||
return this.emitItemsFromPayloads(
|
||||
items.map((item) => item.payload),
|
||||
emitSource,
|
||||
)
|
||||
}
|
||||
|
||||
public async emitItemFromPayload<T extends DecryptedItemInterface>(
|
||||
payload: DecryptedPayloadInterface,
|
||||
emitSource: PayloadEmitSource,
|
||||
): Promise<T> {
|
||||
await this.payloadManager.emitPayload(payload, emitSource)
|
||||
|
||||
const result = this.itemManager.findSureItem<T>(payload.uuid)
|
||||
|
||||
if (!result) {
|
||||
throw Error("Emitted item can't be found")
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
public async emitItemsFromPayloads(
|
||||
payloads: DecryptedPayloadInterface[],
|
||||
emitSource: PayloadEmitSource,
|
||||
): Promise<DecryptedItemInterface[]> {
|
||||
await this.payloadManager.emitPayloads(payloads, emitSource)
|
||||
|
||||
const uuids = Uuids(payloads)
|
||||
|
||||
return this.itemManager.findItems(uuids)
|
||||
}
|
||||
|
||||
public async setItemToBeDeleted(
|
||||
itemToLookupUuidFor: DecryptedItemInterface | EncryptedItemInterface,
|
||||
source: PayloadEmitSource = PayloadEmitSource.LocalChanged,
|
||||
): Promise<void> {
|
||||
const referencingIdsCapturedBeforeChanges = this.itemManager
|
||||
.getCollection()
|
||||
.uuidsThatReferenceUuid(itemToLookupUuidFor.uuid)
|
||||
|
||||
const item = this.itemManager.findAnyItem(itemToLookupUuidFor.uuid)
|
||||
if (!item) {
|
||||
return
|
||||
}
|
||||
|
||||
const mutator = new DeleteItemMutator(item, MutationType.UpdateUserTimestamps)
|
||||
|
||||
const deletedPayload = mutator.getDeletedResult()
|
||||
|
||||
await this.payloadManager.emitPayload(deletedPayload, source)
|
||||
|
||||
for (const referencingId of referencingIdsCapturedBeforeChanges) {
|
||||
const referencingItem = this.itemManager.findItem(referencingId)
|
||||
|
||||
if (referencingItem) {
|
||||
await this.changeItem(referencingItem, (mutator) => {
|
||||
mutator.removeItemAsRelationship(item)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async setItemsToBeDeleted(
|
||||
itemsToLookupUuidsFor: (DecryptedItemInterface | EncryptedItemInterface)[],
|
||||
): Promise<void> {
|
||||
await Promise.all(itemsToLookupUuidsFor.map((item) => this.setItemToBeDeleted(item)))
|
||||
}
|
||||
|
||||
public async findOrCreateTagParentChain(titlesHierarchy: string[]): Promise<SNTag> {
|
||||
let current: SNTag | undefined = undefined
|
||||
|
||||
for (const title of titlesHierarchy) {
|
||||
current = await this.findOrCreateTagByTitle({ title, parentItemToLookupUuidFor: current })
|
||||
}
|
||||
|
||||
if (!current) {
|
||||
throw new Error('Invalid tag hierarchy')
|
||||
}
|
||||
|
||||
return current
|
||||
}
|
||||
|
||||
public async createTag(dto: {
|
||||
title: string
|
||||
parentItemToLookupUuidFor?: SNTag
|
||||
createInVault?: VaultListingInterface
|
||||
}): Promise<SNTag> {
|
||||
const newTag = await this.createItem<SNTag>(
|
||||
ContentType.Tag,
|
||||
FillItemContent<TagContent>({ title: dto.title }),
|
||||
true,
|
||||
dto.createInVault,
|
||||
)
|
||||
|
||||
void this.syncService.sync()
|
||||
return unprotectedItems
|
||||
if (dto.parentItemToLookupUuidFor) {
|
||||
const parentTag = this.itemManager.findItem<SNTag>(dto.parentItemToLookupUuidFor.uuid)
|
||||
if (!parentTag) {
|
||||
throw new Error('Invalid parent tag')
|
||||
}
|
||||
return this.changeTag(newTag, (m) => {
|
||||
m.makeChildOf(parentTag)
|
||||
})
|
||||
}
|
||||
|
||||
return newTag
|
||||
}
|
||||
|
||||
public async protectNote(note: SNNote): Promise<SNNote> {
|
||||
const result = await this.protectItems([note])
|
||||
return result[0]
|
||||
public async createSmartView<T extends DecryptedItemInterface>(dto: {
|
||||
title: string
|
||||
predicate: PredicateInterface<T>
|
||||
iconString?: string
|
||||
vault?: VaultListingInterface
|
||||
}): Promise<SmartView> {
|
||||
return this.createItem(
|
||||
ContentType.SmartView,
|
||||
FillItemContent({
|
||||
title: dto.title,
|
||||
predicate: dto.predicate.toJson(),
|
||||
iconString: dto.iconString || SmartViewDefaultIconName,
|
||||
} as SmartViewContent),
|
||||
true,
|
||||
dto.vault,
|
||||
) as Promise<SmartView>
|
||||
}
|
||||
|
||||
public async unprotectNote(note: SNNote): Promise<SNNote | undefined> {
|
||||
const result = await this.unprotectItems([note], ChallengeReason.UnprotectNote)
|
||||
return result ? result[0] : undefined
|
||||
public async createSmartViewFromDSL<T extends DecryptedItemInterface>(
|
||||
dsl: string,
|
||||
vault?: VaultListingInterface,
|
||||
): Promise<SmartView> {
|
||||
let components = null
|
||||
try {
|
||||
components = JSON.parse(dsl.substring(1, dsl.length))
|
||||
} catch (e) {
|
||||
throw Error('Invalid smart view syntax')
|
||||
}
|
||||
|
||||
const title = components[0]
|
||||
const predicate = predicateFromDSLString<T>(dsl)
|
||||
return this.createSmartView({ title, predicate, vault })
|
||||
}
|
||||
|
||||
public async protectNotes(notes: SNNote[]): Promise<SNNote[]> {
|
||||
return this.protectItems(notes)
|
||||
public async createTagOrSmartView<T extends SNTag | SmartView>(
|
||||
title: string,
|
||||
vault?: VaultListingInterface,
|
||||
): Promise<T> {
|
||||
if (this.itemManager.isSmartViewTitle(title)) {
|
||||
return this.createSmartViewFromDSL(title, vault) as Promise<T>
|
||||
} else {
|
||||
return this.createTag({ title, createInVault: vault }) as Promise<T>
|
||||
}
|
||||
}
|
||||
|
||||
public async unprotectNotes(notes: SNNote[]): Promise<SNNote[]> {
|
||||
const results = await this.unprotectItems(notes, ChallengeReason.UnprotectNote)
|
||||
return results || []
|
||||
public async findOrCreateTagByTitle(dto: {
|
||||
title: string
|
||||
parentItemToLookupUuidFor?: SNTag
|
||||
createInVault?: VaultListingInterface
|
||||
}): Promise<SNTag> {
|
||||
const tag = this.itemManager.findTagByTitleAndParent(dto.title, dto.parentItemToLookupUuidFor)
|
||||
return tag || this.createTag(dto)
|
||||
}
|
||||
|
||||
async protectFile(file: FileItem): Promise<FileItem> {
|
||||
const result = await this.protectItems([file])
|
||||
return result[0]
|
||||
}
|
||||
|
||||
async unprotectFile(file: FileItem): Promise<FileItem | undefined> {
|
||||
const result = await this.unprotectItems([file], ChallengeReason.UnprotectFile)
|
||||
return result ? result[0] : undefined
|
||||
public renameFile(file: FileItem, name: string): Promise<FileItem> {
|
||||
return this.changeItem<FileMutator, FileItem>(file, (mutator) => {
|
||||
mutator.name = name
|
||||
})
|
||||
}
|
||||
|
||||
public async mergeItem(item: DecryptedItemInterface, source: PayloadEmitSource): Promise<DecryptedItemInterface> {
|
||||
return this.itemManager.emitItemFromPayload(item.payloadRepresentation(), source)
|
||||
}
|
||||
|
||||
public createTemplateItem<
|
||||
C extends ItemContent = ItemContent,
|
||||
I extends DecryptedItemInterface<C> = DecryptedItemInterface<C>,
|
||||
>(contentType: ContentType, content?: C, override?: Partial<DecryptedPayload<C>>): I {
|
||||
return this.itemManager.createTemplateItem(contentType, content, override)
|
||||
return this.emitItemFromPayload(item.payloadRepresentation(), source)
|
||||
}
|
||||
|
||||
public async setItemNeedsSync(
|
||||
item: DecryptedItemInterface,
|
||||
updateTimestamps = false,
|
||||
): Promise<DecryptedItemInterface | undefined> {
|
||||
return this.itemManager.setItemDirty(item, updateTimestamps)
|
||||
return this.setItemDirty(item, updateTimestamps)
|
||||
}
|
||||
|
||||
public async setItemsNeedsSync(items: DecryptedItemInterface[]): Promise<(DecryptedItemInterface | undefined)[]> {
|
||||
return this.itemManager.setItemsDirty(items)
|
||||
return this.setItemsDirty(items)
|
||||
}
|
||||
|
||||
public async deleteItem(item: DecryptedItemInterface | EncryptedItemInterface): Promise<void> {
|
||||
@@ -250,153 +550,150 @@ export class MutatorService extends AbstractService implements MutatorClientInte
|
||||
}
|
||||
|
||||
public async deleteItems(items: (DecryptedItemInterface | EncryptedItemInterface)[]): Promise<void> {
|
||||
await this.itemManager.setItemsToBeDeleted(items)
|
||||
await this.syncService.sync()
|
||||
await this.setItemsToBeDeleted(items)
|
||||
}
|
||||
|
||||
/**
|
||||
* Permanently deletes any items currently in the trash. Consumer must manually call sync.
|
||||
*/
|
||||
public async emptyTrash(): Promise<void> {
|
||||
await this.itemManager.emptyTrash()
|
||||
await this.syncService.sync()
|
||||
const notes = this.itemManager.trashedItems
|
||||
await this.setItemsToBeDeleted(notes)
|
||||
}
|
||||
|
||||
public duplicateItem<T extends DecryptedItemInterface>(
|
||||
item: T,
|
||||
additionalContent?: Partial<T['content']>,
|
||||
): Promise<T> {
|
||||
const duplicate = this.itemManager.duplicateItem<T>(item, false, additionalContent)
|
||||
void this.syncService.sync()
|
||||
return duplicate
|
||||
public async migrateTagsToFolders(): Promise<void> {
|
||||
await TagsToFoldersMigrationApplicator.run(this.itemManager, this)
|
||||
}
|
||||
|
||||
public async migrateTagsToFolders(): Promise<unknown> {
|
||||
await TagsToFoldersMigrationApplicator.run(this.itemManager)
|
||||
return this.syncService.sync()
|
||||
public async findOrCreateTag(title: string, createInVault?: VaultListingInterface): Promise<SNTag> {
|
||||
return this.findOrCreateTagByTitle({ title, createInVault })
|
||||
}
|
||||
|
||||
public async setTagParent(parentTag: SNTag, childTag: SNTag): Promise<void> {
|
||||
await this.itemManager.setTagParent(parentTag, childTag)
|
||||
}
|
||||
|
||||
public async unsetTagParent(childTag: SNTag): Promise<void> {
|
||||
await this.itemManager.unsetTagParent(childTag)
|
||||
}
|
||||
|
||||
public async findOrCreateTag(title: string): Promise<SNTag> {
|
||||
return this.itemManager.findOrCreateTagByTitle(title)
|
||||
}
|
||||
|
||||
/** Creates and returns the tag but does not run sync. Callers must perform sync. */
|
||||
public async createTagOrSmartView(title: string): Promise<SNTag | SmartView> {
|
||||
return this.itemManager.createTagOrSmartView(title)
|
||||
}
|
||||
|
||||
public async toggleComponent(component: SNComponent): Promise<void> {
|
||||
await this.componentManager.toggleComponent(component.uuid)
|
||||
await this.syncService.sync()
|
||||
}
|
||||
|
||||
public async toggleTheme(theme: SNComponent): Promise<void> {
|
||||
await this.componentManager.toggleTheme(theme.uuid)
|
||||
await this.syncService.sync()
|
||||
}
|
||||
|
||||
public async importData(
|
||||
data: BackupFile,
|
||||
awaitSync = false,
|
||||
): Promise<
|
||||
| {
|
||||
affectedItems: DecryptedItemInterface[]
|
||||
errorCount: number
|
||||
}
|
||||
| {
|
||||
error: ClientDisplayableError
|
||||
}
|
||||
> {
|
||||
if (data.version) {
|
||||
/**
|
||||
* Prior to 003 backup files did not have a version field so we cannot
|
||||
* stop importing if there is no backup file version, only if there is
|
||||
* an unsupported version.
|
||||
*/
|
||||
const version = data.version as ProtocolVersion
|
||||
|
||||
const supportedVersions = this.encryption.supportedVersions()
|
||||
if (!supportedVersions.includes(version)) {
|
||||
return { error: new ClientDisplayableError(InfoStrings.UnsupportedBackupFileVersion) }
|
||||
}
|
||||
|
||||
const userVersion = this.encryption.getUserVersion()
|
||||
if (userVersion && compareVersions(version, userVersion) === 1) {
|
||||
/** File was made with a greater version than the user's account */
|
||||
return { error: new ClientDisplayableError(InfoStrings.BackupFileMoreRecentThanAccount) }
|
||||
}
|
||||
/**
|
||||
* @returns The changed child tag
|
||||
*/
|
||||
public async setTagParent(parentTag: SNTag, childTag: SNTag): Promise<SNTag> {
|
||||
if (parentTag.uuid === childTag.uuid) {
|
||||
throw new Error('Can not set a tag parent of itself')
|
||||
}
|
||||
|
||||
let password: string | undefined
|
||||
|
||||
if (data.auth_params || data.keyParams) {
|
||||
/** Get import file password. */
|
||||
const challenge = new Challenge(
|
||||
[new ChallengePrompt(ChallengeValidation.None, Strings.Input.FileAccountPassword, undefined, true)],
|
||||
ChallengeReason.DecryptEncryptedFile,
|
||||
true,
|
||||
)
|
||||
const passwordResponse = await this.challengeService.promptForChallengeResponse(challenge)
|
||||
if (passwordResponse == undefined) {
|
||||
/** Challenge was canceled */
|
||||
return { error: new ClientDisplayableError('Import aborted') }
|
||||
}
|
||||
this.challengeService.completeChallenge(challenge)
|
||||
password = passwordResponse?.values[0].value as string
|
||||
if (this.itemManager.isTagAncestor(childTag, parentTag)) {
|
||||
throw new Error('Can not set a tag ancestor of itself')
|
||||
}
|
||||
|
||||
if (!(await this.protectionService.authorizeFileImport())) {
|
||||
return { error: new ClientDisplayableError('Import aborted') }
|
||||
}
|
||||
|
||||
data.items = data.items.map((item) => {
|
||||
if (isEncryptedTransferPayload(item)) {
|
||||
return CreateEncryptedBackupFileContextPayload(item)
|
||||
} else {
|
||||
return CreateDecryptedBackupFileContextPayload(item as BackupFileDecryptedContextualPayload)
|
||||
}
|
||||
return this.changeTag(childTag, (m) => {
|
||||
m.makeChildOf(parentTag)
|
||||
})
|
||||
}
|
||||
|
||||
const decryptedPayloadsOrError = await this.encryption.decryptBackupFile(data, password)
|
||||
/**
|
||||
* @returns The changed child tag
|
||||
*/
|
||||
public unsetTagParent(childTag: SNTag): Promise<SNTag> {
|
||||
const parentTag = this.itemManager.getTagParent(childTag)
|
||||
|
||||
if (decryptedPayloadsOrError instanceof ClientDisplayableError) {
|
||||
return { error: decryptedPayloadsOrError }
|
||||
if (!parentTag) {
|
||||
return Promise.resolve(childTag)
|
||||
}
|
||||
|
||||
const validPayloads = decryptedPayloadsOrError.filter(isDecryptedPayload).map((payload) => {
|
||||
/* Don't want to activate any components during import process in
|
||||
* case of exceptions breaking up the import proccess */
|
||||
if (payload.content_type === ContentType.Component && (payload.content as ComponentContent).active) {
|
||||
const typedContent = payload as DecryptedPayloadInterface<ComponentContent>
|
||||
return CopyPayloadWithContentOverride(typedContent, {
|
||||
active: false,
|
||||
})
|
||||
} else {
|
||||
return payload
|
||||
}
|
||||
return this.changeTag(childTag, (m) => {
|
||||
m.unsetParent()
|
||||
})
|
||||
}
|
||||
|
||||
const affectedUuids = await this.payloadManager.importPayloads(
|
||||
validPayloads,
|
||||
this.historyService.getHistoryMapCopy(),
|
||||
public async associateFileWithNote(file: FileItem, note: SNNote): Promise<FileItem | undefined> {
|
||||
const isVaultConflict =
|
||||
file.key_system_identifier &&
|
||||
note.key_system_identifier &&
|
||||
file.key_system_identifier !== note.key_system_identifier
|
||||
|
||||
if (isVaultConflict) {
|
||||
void this.alerts.alert('The items you are trying to link belong to different vaults and cannot be linked')
|
||||
return undefined
|
||||
}
|
||||
|
||||
return this.changeItem<FileMutator, FileItem>(file, (mutator) => {
|
||||
mutator.addNote(note)
|
||||
})
|
||||
}
|
||||
|
||||
public async disassociateFileWithNote(file: FileItem, note: SNNote): Promise<FileItem> {
|
||||
return this.changeItem<FileMutator, FileItem>(file, (mutator) => {
|
||||
mutator.removeNote(note)
|
||||
})
|
||||
}
|
||||
|
||||
public async addTagToNote(note: SNNote, tag: SNTag, addHierarchy: boolean): Promise<SNTag[] | undefined> {
|
||||
if (tag.key_system_identifier !== note.key_system_identifier) {
|
||||
void this.alerts.alert('The items you are trying to link belong to different vaults and cannot be linked')
|
||||
return undefined
|
||||
}
|
||||
|
||||
let tagsToAdd = [tag]
|
||||
|
||||
if (addHierarchy) {
|
||||
const parentChainTags = this.itemManager.getTagParentChain(tag)
|
||||
tagsToAdd = [...parentChainTags, tag]
|
||||
}
|
||||
|
||||
return Promise.all(
|
||||
tagsToAdd.map((tagToAdd) => {
|
||||
return this.changeTag(tagToAdd, (mutator) => {
|
||||
mutator.addNote(note)
|
||||
}) as Promise<SNTag>
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
const promise = this.syncService.sync()
|
||||
|
||||
if (awaitSync) {
|
||||
await promise
|
||||
public async addTagToFile(file: FileItem, tag: SNTag, addHierarchy: boolean): Promise<SNTag[] | undefined> {
|
||||
if (tag.key_system_identifier !== file.key_system_identifier) {
|
||||
void this.alerts.alert('The items you are trying to link belong to different vaults and cannot be linked')
|
||||
return undefined
|
||||
}
|
||||
|
||||
const affectedItems = this.itemManager.findItems(affectedUuids) as DecryptedItemInterface[]
|
||||
let tagsToAdd = [tag]
|
||||
|
||||
return {
|
||||
affectedItems: affectedItems,
|
||||
errorCount: decryptedPayloadsOrError.length - validPayloads.length,
|
||||
if (addHierarchy) {
|
||||
const parentChainTags = this.itemManager.getTagParentChain(tag)
|
||||
tagsToAdd = [...parentChainTags, tag]
|
||||
}
|
||||
|
||||
return Promise.all(
|
||||
tagsToAdd.map((tagToAdd) => {
|
||||
return this.changeTag(tagToAdd, (mutator) => {
|
||||
mutator.addFile(file)
|
||||
}) as Promise<SNTag>
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
public async linkNoteToNote(note: SNNote, otherNote: SNNote): Promise<SNNote> {
|
||||
return this.changeItem<NoteMutator, SNNote>(note, (mutator) => {
|
||||
mutator.addNote(otherNote)
|
||||
})
|
||||
}
|
||||
|
||||
public async linkFileToFile(file: FileItem, otherFile: FileItem): Promise<FileItem> {
|
||||
return this.changeItem<FileMutator, FileItem>(file, (mutator) => {
|
||||
mutator.addFile(otherFile)
|
||||
})
|
||||
}
|
||||
|
||||
public async unlinkItems(
|
||||
itemA: DecryptedItemInterface<ItemContent>,
|
||||
itemB: DecryptedItemInterface<ItemContent>,
|
||||
): Promise<DecryptedItemInterface<ItemContent>> {
|
||||
const relationshipDirection = this.itemManager.relationshipDirectionBetweenItems(itemA, itemB)
|
||||
|
||||
if (relationshipDirection === ItemRelationshipDirection.NoRelationship) {
|
||||
throw new Error('Trying to unlink already unlinked items')
|
||||
}
|
||||
|
||||
const itemToChange = relationshipDirection === ItemRelationshipDirection.AReferencesB ? itemA : itemB
|
||||
const itemToRemove = itemToChange === itemA ? itemB : itemA
|
||||
|
||||
return this.changeItem(itemToChange, (mutator) => {
|
||||
mutator.removeItemAsRelationship(itemToRemove)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -300,7 +300,7 @@ export class PayloadManager extends AbstractService implements PayloadManagerInt
|
||||
return Uuids(payloads)
|
||||
}
|
||||
|
||||
public removePayloadLocally(payload: FullyFormedPayloadInterface) {
|
||||
public removePayloadLocally(payload: FullyFormedPayloadInterface | FullyFormedPayloadInterface[]): void {
|
||||
this.collection.discard(payload)
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
ApplicationStage,
|
||||
PreferenceServiceInterface,
|
||||
PreferencesServiceEvent,
|
||||
MutatorClientInterface,
|
||||
} from '@standardnotes/services'
|
||||
|
||||
export class SNPreferencesService
|
||||
@@ -24,7 +25,8 @@ export class SNPreferencesService
|
||||
|
||||
constructor(
|
||||
private singletonManager: SNSingletonManager,
|
||||
private itemManager: ItemManager,
|
||||
itemManager: ItemManager,
|
||||
private mutator: MutatorClientInterface,
|
||||
private syncService: SNSyncService,
|
||||
protected override internalEventBus: InternalEventBusInterface,
|
||||
) {
|
||||
@@ -45,7 +47,7 @@ export class SNPreferencesService
|
||||
this.removeItemObserver?.()
|
||||
this.removeSyncObserver?.()
|
||||
;(this.singletonManager as unknown) = undefined
|
||||
;(this.itemManager as unknown) = undefined
|
||||
;(this.mutator as unknown) = undefined
|
||||
|
||||
super.deinit()
|
||||
}
|
||||
@@ -77,7 +79,7 @@ export class SNPreferencesService
|
||||
return
|
||||
}
|
||||
|
||||
this.preferences = (await this.itemManager.changeItem<UserPrefsMutator>(this.preferences, (m) => {
|
||||
this.preferences = (await this.mutator.changeItem<UserPrefsMutator>(this.preferences, (m) => {
|
||||
m.setPref(key, value)
|
||||
})) as SNUserPrefs
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
InternalEventBusInterface,
|
||||
ChallengeReason,
|
||||
EncryptionService,
|
||||
MutatorClientInterface,
|
||||
} from '@standardnotes/services'
|
||||
import { UuidGenerator } from '@standardnotes/utils'
|
||||
import {
|
||||
@@ -22,6 +23,7 @@ const setupRandomUuid = () => {
|
||||
}
|
||||
|
||||
describe('protectionService', () => {
|
||||
let mutator: MutatorClientInterface
|
||||
let protocolService: EncryptionService
|
||||
let challengeService: ChallengeService
|
||||
let storageService: DiskStorageService
|
||||
@@ -29,7 +31,7 @@ describe('protectionService', () => {
|
||||
let protectionService: SNProtectionService
|
||||
|
||||
const createService = () => {
|
||||
return new SNProtectionService(protocolService, challengeService, storageService, internalEventBus)
|
||||
return new SNProtectionService(protocolService, mutator, challengeService, storageService, internalEventBus)
|
||||
}
|
||||
|
||||
const createFile = (name: string, isProtected?: boolean) => {
|
||||
@@ -60,6 +62,8 @@ describe('protectionService', () => {
|
||||
protocolService = {} as jest.Mocked<EncryptionService>
|
||||
protocolService.hasAccount = jest.fn().mockReturnValue(true)
|
||||
protocolService.hasPasscode = jest.fn().mockReturnValue(false)
|
||||
|
||||
mutator = {} as jest.Mocked<MutatorClientInterface>
|
||||
})
|
||||
|
||||
describe('files', () => {
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
import { ChallengeService } from './../Challenge/ChallengeService'
|
||||
import { SNLog } from '@Lib/Log'
|
||||
import { DecryptedItem } from '@standardnotes/models'
|
||||
import {
|
||||
DecryptedItem,
|
||||
DecryptedItemInterface,
|
||||
DecryptedItemMutator,
|
||||
FileItem,
|
||||
MutationType,
|
||||
SNNote,
|
||||
} from '@standardnotes/models'
|
||||
import { DiskStorageService } from '@Lib/Services/Storage/DiskStorageService'
|
||||
import { isNullOrUndefined } from '@standardnotes/utils'
|
||||
import {
|
||||
@@ -9,7 +16,6 @@ import {
|
||||
StorageValueModes,
|
||||
ApplicationStage,
|
||||
StorageKey,
|
||||
DiagnosticInfo,
|
||||
Challenge,
|
||||
ChallengeReason,
|
||||
ChallengePrompt,
|
||||
@@ -18,6 +24,7 @@ import {
|
||||
MobileUnlockTiming,
|
||||
TimingDisplayOption,
|
||||
ProtectionsClientInterface,
|
||||
MutatorClientInterface,
|
||||
} from '@standardnotes/services'
|
||||
import { ContentType } from '@standardnotes/common'
|
||||
|
||||
@@ -70,6 +77,7 @@ export class SNProtectionService extends AbstractService<ProtectionEvent> implem
|
||||
|
||||
constructor(
|
||||
private protocolService: EncryptionService,
|
||||
private mutator: MutatorClientInterface,
|
||||
private challengeService: ChallengeService,
|
||||
private storageService: DiskStorageService,
|
||||
protected override internalEventBus: InternalEventBusInterface,
|
||||
@@ -435,15 +443,69 @@ export class SNProtectionService extends AbstractService<ProtectionEvent> implem
|
||||
this.sessionExpiryTimeout = setTimeout(timer, expiryDate.getTime() - Date.now())
|
||||
}
|
||||
|
||||
override getDiagnostics(): Promise<DiagnosticInfo | undefined> {
|
||||
return Promise.resolve({
|
||||
protections: {
|
||||
getSessionExpiryDate: this.getSessionExpiryDate(),
|
||||
getLastSessionLength: this.getLastSessionLength(),
|
||||
hasProtectionSources: this.hasProtectionSources(),
|
||||
hasUnprotectedAccessSession: this.hasUnprotectedAccessSession(),
|
||||
hasBiometricsEnabled: this.hasBiometricsEnabled(),
|
||||
async protectItems<I extends DecryptedItemInterface>(items: I[]): Promise<I[]> {
|
||||
const protectedItems = await this.mutator.changeItems<DecryptedItemMutator, I>(
|
||||
items,
|
||||
(mutator) => {
|
||||
mutator.protected = true
|
||||
},
|
||||
})
|
||||
MutationType.NoUpdateUserTimestamps,
|
||||
)
|
||||
|
||||
return protectedItems
|
||||
}
|
||||
|
||||
async unprotectItems<I extends DecryptedItemInterface>(
|
||||
items: I[],
|
||||
reason: ChallengeReason,
|
||||
): Promise<I[] | undefined> {
|
||||
if (
|
||||
!(await this.authorizeAction(reason, {
|
||||
fallBackToAccountPassword: true,
|
||||
requireAccountPassword: false,
|
||||
forcePrompt: false,
|
||||
}))
|
||||
) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const unprotectedItems = await this.mutator.changeItems<DecryptedItemMutator, I>(
|
||||
items,
|
||||
(mutator) => {
|
||||
mutator.protected = false
|
||||
},
|
||||
MutationType.NoUpdateUserTimestamps,
|
||||
)
|
||||
|
||||
return unprotectedItems
|
||||
}
|
||||
|
||||
public async protectNote(note: SNNote): Promise<SNNote> {
|
||||
const result = await this.protectItems([note])
|
||||
return result[0]
|
||||
}
|
||||
|
||||
public async unprotectNote(note: SNNote): Promise<SNNote | undefined> {
|
||||
const result = await this.unprotectItems([note], ChallengeReason.UnprotectNote)
|
||||
return result ? result[0] : undefined
|
||||
}
|
||||
|
||||
public async protectNotes(notes: SNNote[]): Promise<SNNote[]> {
|
||||
return this.protectItems(notes)
|
||||
}
|
||||
|
||||
public async unprotectNotes(notes: SNNote[]): Promise<SNNote[]> {
|
||||
const results = await this.unprotectItems(notes, ChallengeReason.UnprotectNote)
|
||||
return results || []
|
||||
}
|
||||
|
||||
async protectFile(file: FileItem): Promise<FileItem> {
|
||||
const result = await this.protectItems([file])
|
||||
return result[0]
|
||||
}
|
||||
|
||||
async unprotectFile(file: FileItem): Promise<FileItem | undefined> {
|
||||
const result = await this.unprotectItems([file], ChallengeReason.UnprotectFile)
|
||||
return result ? result[0] : undefined
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ import {
|
||||
AbstractService,
|
||||
InternalEventBusInterface,
|
||||
StorageKey,
|
||||
DiagnosticInfo,
|
||||
ChallengePrompt,
|
||||
ChallengeValidation,
|
||||
ChallengeKeyboardType,
|
||||
@@ -26,8 +25,12 @@ import {
|
||||
InternalEventInterface,
|
||||
ApiServiceEvent,
|
||||
SessionRefreshedData,
|
||||
SessionEvent,
|
||||
UserKeyPairChangedEventData,
|
||||
InternalFeatureService,
|
||||
InternalFeature,
|
||||
} from '@standardnotes/services'
|
||||
import { Base64String } from '@standardnotes/sncrypto-common'
|
||||
import { Base64String, PkcKeyPair } from '@standardnotes/sncrypto-common'
|
||||
import {
|
||||
ClientDisplayableError,
|
||||
SessionBody,
|
||||
@@ -43,7 +46,7 @@ import {
|
||||
SessionListResponse,
|
||||
HttpSuccessResponse,
|
||||
} from '@standardnotes/responses'
|
||||
import { CopyPayloadWithContentOverride } from '@standardnotes/models'
|
||||
import { CopyPayloadWithContentOverride, RootKeyWithKeyPairsInterface } from '@standardnotes/models'
|
||||
import { LegacySession, MapperInterface, Result, Session, SessionToken } from '@standardnotes/domain-core'
|
||||
import { KeyParamsFromApiResponse, SNRootKeyParams, SNRootKey } from '@standardnotes/encryption'
|
||||
import { Subscription } from '@standardnotes/security'
|
||||
@@ -56,7 +59,7 @@ import { DiskStorageService } from '../Storage/DiskStorageService'
|
||||
import { SNWebSocketsService } from '../Api/WebsocketsService'
|
||||
import { Strings } from '@Lib/Strings'
|
||||
import { UuidString } from '@Lib/Types/UuidString'
|
||||
import { ChallengeService } from '../Challenge'
|
||||
import { ChallengeResponse, ChallengeService } from '../Challenge'
|
||||
import {
|
||||
ApiCallError,
|
||||
ErrorMessage,
|
||||
@@ -72,11 +75,6 @@ const cleanedEmailString = (email: string) => {
|
||||
return email.trim().toLowerCase()
|
||||
}
|
||||
|
||||
export enum SessionEvent {
|
||||
Restored = 'SessionRestored',
|
||||
Revoked = 'SessionRevoked',
|
||||
}
|
||||
|
||||
/**
|
||||
* The session manager is responsible for loading initial user state, and any relevant
|
||||
* server credentials, such as the session token. It also exposes methods for registering
|
||||
@@ -139,18 +137,19 @@ export class SNSessionManager
|
||||
}
|
||||
}
|
||||
|
||||
private setUser(user?: User) {
|
||||
private memoizeUser(user?: User) {
|
||||
this.user = user
|
||||
|
||||
this.apiService.setUser(user)
|
||||
}
|
||||
|
||||
async initializeFromDisk() {
|
||||
this.setUser(this.diskStorageService.getValue(StorageKey.User))
|
||||
this.memoizeUser(this.diskStorageService.getValue(StorageKey.User))
|
||||
|
||||
if (!this.user) {
|
||||
const legacyUuidLookup = this.diskStorageService.getValue<string>(StorageKey.LegacyUuid)
|
||||
if (legacyUuidLookup) {
|
||||
this.setUser({ uuid: legacyUuidLookup, email: legacyUuidLookup })
|
||||
this.memoizeUser({ uuid: legacyUuidLookup, email: legacyUuidLookup })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -193,6 +192,36 @@ export class SNSessionManager
|
||||
return this.user
|
||||
}
|
||||
|
||||
public getSureUser(): User {
|
||||
return this.user as User
|
||||
}
|
||||
|
||||
isUserMissingKeyPair(): boolean {
|
||||
try {
|
||||
return this.getPublicKey() == undefined
|
||||
} catch (error) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
public getPublicKey(): string {
|
||||
return this.protocolService.getKeyPair().publicKey
|
||||
}
|
||||
|
||||
public getSigningPublicKey(): string {
|
||||
return this.protocolService.getSigningKeyPair().publicKey
|
||||
}
|
||||
|
||||
public get userUuid(): string {
|
||||
const user = this.getUser()
|
||||
|
||||
if (!user) {
|
||||
throw Error('Attempting to access userUuid when user is undefined')
|
||||
}
|
||||
|
||||
return user.uuid
|
||||
}
|
||||
|
||||
isCurrentSessionReadOnly(): boolean | undefined {
|
||||
if (this.session === undefined) {
|
||||
return undefined
|
||||
@@ -205,16 +234,13 @@ export class SNSessionManager
|
||||
return this.session.isReadOnly()
|
||||
}
|
||||
|
||||
public getSureUser() {
|
||||
return this.user as User
|
||||
}
|
||||
|
||||
public getSession() {
|
||||
return this.apiService.getSession()
|
||||
}
|
||||
|
||||
public async signOut() {
|
||||
this.setUser(undefined)
|
||||
this.memoizeUser(undefined)
|
||||
|
||||
const session = this.apiService.getSession()
|
||||
if (session && session instanceof Session) {
|
||||
await this.apiService.signOut()
|
||||
@@ -268,7 +294,11 @@ export class SNSessionManager
|
||||
currentKeyParams?.version,
|
||||
)
|
||||
if (isErrorResponse(response)) {
|
||||
this.challengeService.setValidationStatusForChallenge(challenge, challengeResponse!.values[1], false)
|
||||
this.challengeService.setValidationStatusForChallenge(
|
||||
challenge,
|
||||
(challengeResponse as ChallengeResponse).values[1],
|
||||
false,
|
||||
)
|
||||
onResponse?.(response)
|
||||
} else {
|
||||
resolve()
|
||||
@@ -373,11 +403,20 @@ export class SNSessionManager
|
||||
|
||||
email = cleanedEmailString(email)
|
||||
|
||||
const rootKey = await this.protocolService.createRootKey(email, password, Common.KeyParamsOrigination.Registration)
|
||||
const rootKey = await this.protocolService.createRootKey<RootKeyWithKeyPairsInterface>(
|
||||
email,
|
||||
password,
|
||||
Common.KeyParamsOrigination.Registration,
|
||||
)
|
||||
const serverPassword = rootKey.serverPassword as string
|
||||
const keyParams = rootKey.keyParams
|
||||
|
||||
const registerResponse = await this.userApiService.register({ email, serverPassword, keyParams, ephemeral })
|
||||
const registerResponse = await this.userApiService.register({
|
||||
email,
|
||||
serverPassword,
|
||||
keyParams,
|
||||
ephemeral,
|
||||
})
|
||||
|
||||
if ('error' in registerResponse.data) {
|
||||
throw new ApiCallError(registerResponse.data.error.message)
|
||||
@@ -485,7 +524,7 @@ export class SNSessionManager
|
||||
response: paramsResult.response,
|
||||
}
|
||||
}
|
||||
const keyParams = paramsResult.keyParams!
|
||||
const keyParams = paramsResult.keyParams as SNRootKeyParams
|
||||
if (!this.protocolService.supportedVersions().includes(keyParams.version)) {
|
||||
if (this.protocolService.isVersionNewerThanLibraryVersion(keyParams.version)) {
|
||||
return {
|
||||
@@ -563,7 +602,7 @@ export class SNSessionManager
|
||||
|
||||
const signInResponse = await this.apiService.signIn({
|
||||
email,
|
||||
serverPassword: rootKey.serverPassword!,
|
||||
serverPassword: rootKey.serverPassword as string,
|
||||
ephemeral,
|
||||
})
|
||||
|
||||
@@ -585,20 +624,49 @@ export class SNSessionManager
|
||||
|
||||
public async changeCredentials(parameters: {
|
||||
currentServerPassword: string
|
||||
newRootKey: SNRootKey
|
||||
newRootKey: RootKeyWithKeyPairsInterface
|
||||
wrappingKey?: SNRootKey
|
||||
newEmail?: string
|
||||
}): Promise<SessionManagerResponse> {
|
||||
const userUuid = this.user!.uuid
|
||||
const response = await this.apiService.changeCredentials({
|
||||
const userUuid = this.getSureUser().uuid
|
||||
const rawResponse = await this.apiService.changeCredentials({
|
||||
userUuid,
|
||||
currentServerPassword: parameters.currentServerPassword,
|
||||
newServerPassword: parameters.newRootKey.serverPassword!,
|
||||
newServerPassword: parameters.newRootKey.serverPassword as string,
|
||||
newKeyParams: parameters.newRootKey.keyParams,
|
||||
newEmail: parameters.newEmail,
|
||||
})
|
||||
|
||||
return this.processChangeCredentialsResponse(response, parameters.newRootKey, parameters.wrappingKey)
|
||||
let oldKeyPair: PkcKeyPair | undefined
|
||||
let oldSigningKeyPair: PkcKeyPair | undefined
|
||||
|
||||
try {
|
||||
oldKeyPair = this.protocolService.getKeyPair()
|
||||
oldSigningKeyPair = this.protocolService.getSigningKeyPair()
|
||||
} catch (error) {
|
||||
void error
|
||||
}
|
||||
|
||||
const processedResponse = await this.processChangeCredentialsResponse(
|
||||
rawResponse,
|
||||
parameters.newRootKey,
|
||||
parameters.wrappingKey,
|
||||
)
|
||||
|
||||
if (!isErrorResponse(rawResponse)) {
|
||||
if (InternalFeatureService.get().isFeatureEnabled(InternalFeature.Vaults)) {
|
||||
const eventData: UserKeyPairChangedEventData = {
|
||||
oldKeyPair,
|
||||
oldSigningKeyPair,
|
||||
newKeyPair: parameters.newRootKey.encryptionKeyPair,
|
||||
newSigningKeyPair: parameters.newRootKey.signingKeyPair,
|
||||
}
|
||||
|
||||
void this.notifyEvent(SessionEvent.UserKeyPairChanged, eventData)
|
||||
}
|
||||
}
|
||||
|
||||
return processedResponse
|
||||
}
|
||||
|
||||
public async getSessionsList(): Promise<HttpResponse<SessionListEntry[]>> {
|
||||
@@ -669,12 +737,10 @@ export class SNSessionManager
|
||||
) {
|
||||
await this.protocolService.setRootKey(rootKey, wrappingKey)
|
||||
|
||||
this.setUser(user)
|
||||
|
||||
this.memoizeUser(user)
|
||||
this.diskStorageService.setValue(StorageKey.User, user)
|
||||
|
||||
void this.apiService.setHost(host)
|
||||
|
||||
this.httpService.setHost(host)
|
||||
|
||||
this.setSession(session)
|
||||
@@ -777,16 +843,4 @@ export class SNSessionManager
|
||||
|
||||
return Result.ok(sessionOrError.getValue())
|
||||
}
|
||||
|
||||
override getDiagnostics(): Promise<DiagnosticInfo | undefined> {
|
||||
return Promise.resolve({
|
||||
session: {
|
||||
isSessionRenewChallengePresented: this.isSessionRenewChallengePresented,
|
||||
online: this.online(),
|
||||
offline: this.offline(),
|
||||
isSignedIn: this.isSignedIn(),
|
||||
isSignedIntoFirstPartyServer: this.isSignedIntoFirstPartyServer(),
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,10 +10,17 @@ import {
|
||||
PayloadEmitSource,
|
||||
PayloadTimestampDefaults,
|
||||
getIncrementedDirtyIndex,
|
||||
Predicate,
|
||||
} from '@standardnotes/models'
|
||||
import { arrayByRemovingFromIndex, extendArray, UuidGenerator } from '@standardnotes/utils'
|
||||
import { SNSyncService } from '../Sync/SyncService'
|
||||
import { AbstractService, InternalEventBusInterface, SyncEvent } from '@standardnotes/services'
|
||||
import {
|
||||
AbstractService,
|
||||
InternalEventBusInterface,
|
||||
MutatorClientInterface,
|
||||
SingletonManagerInterface,
|
||||
SyncEvent,
|
||||
} from '@standardnotes/services'
|
||||
|
||||
/**
|
||||
* The singleton manager allow consumers to ensure that only 1 item exists of a certain
|
||||
@@ -26,7 +33,7 @@ import { AbstractService, InternalEventBusInterface, SyncEvent } from '@standard
|
||||
* 2. Items can override isSingleton, singletonPredicate, and strategyWhenConflictingWithItem (optional)
|
||||
* to automatically gain singleton resolution.
|
||||
*/
|
||||
export class SNSingletonManager extends AbstractService {
|
||||
export class SNSingletonManager extends AbstractService implements SingletonManagerInterface {
|
||||
private resolveQueue: DecryptedItemInterface[] = []
|
||||
|
||||
private removeItemObserver!: () => void
|
||||
@@ -34,6 +41,7 @@ export class SNSingletonManager extends AbstractService {
|
||||
|
||||
constructor(
|
||||
private itemManager: ItemManager,
|
||||
private mutator: MutatorClientInterface,
|
||||
private payloadManager: PayloadManager,
|
||||
private syncService: SNSyncService,
|
||||
protected override internalEventBus: InternalEventBusInterface,
|
||||
@@ -44,6 +52,7 @@ export class SNSingletonManager extends AbstractService {
|
||||
|
||||
public override deinit(): void {
|
||||
;(this.syncService as unknown) = undefined
|
||||
;(this.mutator as unknown) = undefined
|
||||
;(this.itemManager as unknown) = undefined
|
||||
;(this.payloadManager as unknown) = undefined
|
||||
|
||||
@@ -148,7 +157,7 @@ export class SNSingletonManager extends AbstractService {
|
||||
})
|
||||
|
||||
const deleteItems = arrayByRemovingFromIndex(earliestFirst, 0)
|
||||
await this.itemManager.setItemsToBeDeleted(deleteItems)
|
||||
await this.mutator.setItemsToBeDeleted(deleteItems)
|
||||
}
|
||||
|
||||
public findSingleton<T extends DecryptedItemInterface>(
|
||||
@@ -222,7 +231,66 @@ export class SNSingletonManager extends AbstractService {
|
||||
...PayloadTimestampDefaults(),
|
||||
})
|
||||
|
||||
const item = await this.itemManager.emitItemFromPayload(dirtyPayload, PayloadEmitSource.LocalInserted)
|
||||
const item = await this.mutator.emitItemFromPayload(dirtyPayload, PayloadEmitSource.LocalInserted)
|
||||
|
||||
void this.syncService.sync({ sourceDescription: 'After find or create singleton' })
|
||||
|
||||
return item as T
|
||||
}
|
||||
|
||||
public async findOrCreateSingleton<
|
||||
C extends ItemContent = ItemContent,
|
||||
T extends DecryptedItemInterface<C> = DecryptedItemInterface<C>,
|
||||
>(predicate: Predicate<T>, contentType: ContentType, createContent: ItemContent): Promise<T> {
|
||||
const existingItems = this.itemManager.itemsMatchingPredicate<T>(contentType, predicate)
|
||||
if (existingItems.length > 0) {
|
||||
return existingItems[0]
|
||||
}
|
||||
|
||||
/** Item not found, safe to create after full sync has completed */
|
||||
if (!this.syncService.getLastSyncDate()) {
|
||||
/**
|
||||
* Add a temporary observer in case of long-running sync request, where
|
||||
* the item we're looking for ends up resolving early or in the middle.
|
||||
*/
|
||||
let matchingItem: DecryptedItemInterface | undefined
|
||||
|
||||
const removeObserver = this.itemManager.addObserver(contentType, ({ inserted }) => {
|
||||
if (inserted.length > 0) {
|
||||
const matchingItems = inserted.filter((i) => i.satisfiesPredicate(predicate))
|
||||
|
||||
if (matchingItems.length > 0) {
|
||||
matchingItem = matchingItems[0]
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
await this.syncService.sync({ sourceDescription: 'Find or create singleton, before any sync has completed' })
|
||||
|
||||
removeObserver()
|
||||
|
||||
if (matchingItem) {
|
||||
return matchingItem as T
|
||||
}
|
||||
|
||||
/** Check again */
|
||||
const refreshedItems = this.itemManager.itemsMatchingPredicate<T>(contentType, predicate)
|
||||
if (refreshedItems.length > 0) {
|
||||
return refreshedItems[0] as T
|
||||
}
|
||||
}
|
||||
|
||||
/** Safe to create */
|
||||
const dirtyPayload = new DecryptedPayload({
|
||||
uuid: UuidGenerator.GenerateUuid(),
|
||||
content_type: contentType,
|
||||
content: createContent,
|
||||
dirty: true,
|
||||
dirtyIndex: getIncrementedDirtyIndex(),
|
||||
...PayloadTimestampDefaults(),
|
||||
})
|
||||
|
||||
const item = await this.mutator.emitItemFromPayload(dirtyPayload, PayloadEmitSource.LocalInserted)
|
||||
|
||||
void this.syncService.sync({ sourceDescription: 'After find or create singleton' })
|
||||
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { ContentType } from '@standardnotes/common'
|
||||
import { Copy, extendArray, UuidGenerator } from '@standardnotes/utils'
|
||||
import { Copy, extendArray, UuidGenerator, Uuids } from '@standardnotes/utils'
|
||||
import { SNLog } from '../../Log'
|
||||
import { isErrorDecryptingParameters, SNRootKey } from '@standardnotes/encryption'
|
||||
import * as Encryption from '@standardnotes/encryption'
|
||||
import * as Services from '@standardnotes/services'
|
||||
import { DiagnosticInfo } from '@standardnotes/services'
|
||||
import {
|
||||
CreateDecryptedLocalStorageContextPayload,
|
||||
CreateDeletedLocalStorageContextPayload,
|
||||
@@ -252,7 +251,7 @@ export class DiskStorageService extends Services.AbstractService implements Serv
|
||||
return rawContent as Services.StorageValuesObject
|
||||
}
|
||||
|
||||
public setValue(key: string, value: unknown, mode = Services.StorageValueModes.Default): void {
|
||||
public setValue<T>(key: string, value: T, mode = Services.StorageValueModes.Default): void {
|
||||
this.setValueWithNoPersist(key, value, mode)
|
||||
|
||||
void this.persistValuesToDisk()
|
||||
@@ -292,6 +291,14 @@ export class DiskStorageService extends Services.AbstractService implements Serv
|
||||
return value != undefined ? (value as T) : (defaultValue as T)
|
||||
}
|
||||
|
||||
public getAllKeys(mode = Services.StorageValueModes.Default): string[] {
|
||||
if (!this.values) {
|
||||
throw Error('Attempting to get all keys before loading local storage.')
|
||||
}
|
||||
|
||||
return Object.keys(this.values[this.domainKeyForMode(mode)])
|
||||
}
|
||||
|
||||
public async removeValue(key: string, mode = Services.StorageValueModes.Default): Promise<void> {
|
||||
if (!this.values) {
|
||||
throw Error(`Attempting to remove storage key ${key} before loading local storage.`)
|
||||
@@ -370,20 +377,28 @@ export class DiskStorageService extends Services.AbstractService implements Serv
|
||||
const encryptable: DecryptedPayloadInterface[] = []
|
||||
const unencryptable: DecryptedPayloadInterface[] = []
|
||||
|
||||
const split = Encryption.SplitPayloadsByEncryptionType(decrypted)
|
||||
if (split.itemsKeyEncryption) {
|
||||
extendArray(encryptable, split.itemsKeyEncryption)
|
||||
const { rootKeyEncryption, keySystemRootKeyEncryption, itemsKeyEncryption } =
|
||||
Encryption.SplitPayloadsByEncryptionType(decrypted)
|
||||
|
||||
if (itemsKeyEncryption) {
|
||||
extendArray(encryptable, itemsKeyEncryption)
|
||||
}
|
||||
|
||||
if (split.rootKeyEncryption) {
|
||||
if (keySystemRootKeyEncryption) {
|
||||
extendArray(encryptable, keySystemRootKeyEncryption)
|
||||
}
|
||||
|
||||
if (rootKeyEncryption) {
|
||||
if (!rootKeyEncryptionAvailable) {
|
||||
extendArray(unencryptable, split.rootKeyEncryption)
|
||||
extendArray(unencryptable, rootKeyEncryption)
|
||||
} else {
|
||||
extendArray(encryptable, split.rootKeyEncryption)
|
||||
extendArray(encryptable, rootKeyEncryption)
|
||||
}
|
||||
}
|
||||
|
||||
await this.deletePayloads(discardable)
|
||||
if (discardable.length > 0) {
|
||||
await this.deletePayloads(discardable)
|
||||
}
|
||||
|
||||
const encryptableSplit = Encryption.SplitPayloadsByEncryptionType(encryptable)
|
||||
|
||||
@@ -406,16 +421,18 @@ export class DiskStorageService extends Services.AbstractService implements Serv
|
||||
}
|
||||
|
||||
public async deletePayloads(payloads: DeletedPayloadInterface[]) {
|
||||
await Promise.all(payloads.map((payload) => this.deletePayloadWithId(payload.uuid)))
|
||||
await this.deletePayloadsWithUuids(Uuids(payloads))
|
||||
}
|
||||
|
||||
public async forceDeletePayloads(payloads: FullyFormedPayloadInterface[]) {
|
||||
await Promise.all(payloads.map((payload) => this.deletePayloadWithId(payload.uuid)))
|
||||
public async deletePayloadsWithUuids(uuids: string[]): Promise<void> {
|
||||
await this.executeCriticalFunction(async () => {
|
||||
await Promise.all(uuids.map((uuid) => this.deviceInterface.removeDatabaseEntry(uuid, this.identifier)))
|
||||
})
|
||||
}
|
||||
|
||||
public async deletePayloadWithId(uuid: string) {
|
||||
public async deletePayloadWithUuid(uuid: string) {
|
||||
return this.executeCriticalFunction(async () => {
|
||||
return this.deviceInterface.removeDatabaseEntry(uuid, this.identifier)
|
||||
await this.deviceInterface.removeDatabaseEntry(uuid, this.identifier)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -437,17 +454,4 @@ export class DiskStorageService extends Services.AbstractService implements Serv
|
||||
await this.deviceInterface.removeRawStorageValue(this.getPersistenceKey())
|
||||
})
|
||||
}
|
||||
|
||||
override async getDiagnostics(): Promise<DiagnosticInfo | undefined> {
|
||||
return {
|
||||
storage: {
|
||||
storagePersistable: this.storagePersistable,
|
||||
persistencePolicy: Services.StoragePersistencePolicies[this.persistencePolicy],
|
||||
needsPersist: this.needsPersist,
|
||||
currentPersistPromise: this.currentPersistPromise != undefined,
|
||||
isStorageWrapped: this.isStorageWrapped(),
|
||||
allRawPayloadsCount: (await this.getAllRawPayloads()).length,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,17 +21,15 @@ export class AccountSyncOperation {
|
||||
* @param receiver A function that receives callback multiple times during the operation
|
||||
*/
|
||||
constructor(
|
||||
private payloads: ServerSyncPushContextualPayload[],
|
||||
public readonly payloads: ServerSyncPushContextualPayload[],
|
||||
private receiver: ResponseSignalReceiver<ServerSyncResponse>,
|
||||
private lastSyncToken: string,
|
||||
private paginationToken: string,
|
||||
private apiService: SNApiService,
|
||||
public readonly options: {
|
||||
syncToken?: string
|
||||
paginationToken?: string
|
||||
sharedVaultUuids?: string[]
|
||||
},
|
||||
) {
|
||||
this.payloads = payloads
|
||||
this.lastSyncToken = lastSyncToken
|
||||
this.paginationToken = paginationToken
|
||||
this.apiService = apiService
|
||||
this.receiver = receiver
|
||||
this.pendingPayloads = payloads.slice()
|
||||
}
|
||||
|
||||
@@ -55,13 +53,19 @@ export class AccountSyncOperation {
|
||||
})
|
||||
const payloads = this.popPayloads(this.upLimit)
|
||||
|
||||
const rawResponse = await this.apiService.sync(payloads, this.lastSyncToken, this.paginationToken, this.downLimit)
|
||||
const rawResponse = await this.apiService.sync(
|
||||
payloads,
|
||||
this.options.syncToken,
|
||||
this.options.paginationToken,
|
||||
this.downLimit,
|
||||
this.options.sharedVaultUuids,
|
||||
)
|
||||
|
||||
const response = new ServerSyncResponse(rawResponse)
|
||||
this.responses.push(response)
|
||||
|
||||
this.lastSyncToken = response.lastSyncToken as string
|
||||
this.paginationToken = response.paginationToken as string
|
||||
this.options.syncToken = response.lastSyncToken as string
|
||||
this.options.paginationToken = response.paginationToken as string
|
||||
|
||||
try {
|
||||
await this.receiver(SyncSignal.Response, response)
|
||||
@@ -75,7 +79,7 @@ export class AccountSyncOperation {
|
||||
}
|
||||
|
||||
get done() {
|
||||
return this.pendingPayloads.length === 0 && !this.paginationToken
|
||||
return this.pendingPayloads.length === 0 && !this.options.paginationToken
|
||||
}
|
||||
|
||||
private get pendingUploadCount() {
|
||||
|
||||
@@ -1,27 +1,36 @@
|
||||
import {
|
||||
ApiEndpointParam,
|
||||
ConflictParams,
|
||||
ConflictType,
|
||||
SharedVaultInviteServerHash,
|
||||
SharedVaultServerHash,
|
||||
HttpError,
|
||||
HttpResponse,
|
||||
isErrorResponse,
|
||||
RawSyncResponse,
|
||||
ServerItemResponse,
|
||||
UserEventServerHash,
|
||||
AsymmetricMessageServerHash,
|
||||
} from '@standardnotes/responses'
|
||||
import {
|
||||
FilterDisallowedRemotePayloadsAndMap,
|
||||
CreateServerSyncSavedPayload,
|
||||
ServerSyncSavedContextualPayload,
|
||||
FilteredServerItem,
|
||||
TrustedConflictParams,
|
||||
} from '@standardnotes/models'
|
||||
import { deepFreeze } from '@standardnotes/utils'
|
||||
import { TrustedServerConflictMap } from './ServerConflictMap'
|
||||
|
||||
export class ServerSyncResponse {
|
||||
public readonly savedPayloads: ServerSyncSavedContextualPayload[]
|
||||
public readonly retrievedPayloads: FilteredServerItem[]
|
||||
public readonly uuidConflictPayloads: FilteredServerItem[]
|
||||
public readonly dataConflictPayloads: FilteredServerItem[]
|
||||
public readonly rejectedPayloads: FilteredServerItem[]
|
||||
readonly savedPayloads: ServerSyncSavedContextualPayload[]
|
||||
readonly retrievedPayloads: FilteredServerItem[]
|
||||
readonly conflicts: TrustedServerConflictMap
|
||||
|
||||
readonly asymmetricMessages: AsymmetricMessageServerHash[]
|
||||
readonly vaults: SharedVaultServerHash[]
|
||||
readonly vaultInvites: SharedVaultInviteServerHash[]
|
||||
readonly userEvents: UserEventServerHash[]
|
||||
|
||||
private readonly rawConflictObjects: ConflictParams[]
|
||||
|
||||
private successResponseData: RawSyncResponse | undefined
|
||||
|
||||
@@ -32,6 +41,10 @@ export class ServerSyncResponse {
|
||||
this.successResponseData = rawResponse.data
|
||||
}
|
||||
|
||||
const conflicts = this.successResponseData?.conflicts || []
|
||||
const legacyConflicts = this.successResponseData?.unsaved || []
|
||||
this.rawConflictObjects = conflicts.concat(legacyConflicts)
|
||||
|
||||
this.savedPayloads = FilterDisallowedRemotePayloadsAndMap(this.successResponseData?.saved_items || []).map(
|
||||
(rawItem) => {
|
||||
return CreateServerSyncSavedPayload(rawItem)
|
||||
@@ -40,15 +53,53 @@ export class ServerSyncResponse {
|
||||
|
||||
this.retrievedPayloads = FilterDisallowedRemotePayloadsAndMap(this.successResponseData?.retrieved_items || [])
|
||||
|
||||
this.dataConflictPayloads = FilterDisallowedRemotePayloadsAndMap(this.rawDataConflictItems)
|
||||
this.conflicts = this.filterConflicts()
|
||||
|
||||
this.uuidConflictPayloads = FilterDisallowedRemotePayloadsAndMap(this.rawUuidConflictItems)
|
||||
this.vaults = this.successResponseData?.shared_vaults || []
|
||||
|
||||
this.rejectedPayloads = FilterDisallowedRemotePayloadsAndMap(this.rawRejectedPayloads)
|
||||
this.vaultInvites = this.successResponseData?.shared_vault_invites || []
|
||||
|
||||
this.asymmetricMessages = this.successResponseData?.asymmetric_messages || []
|
||||
|
||||
this.userEvents = this.successResponseData?.user_events || []
|
||||
|
||||
deepFreeze(this)
|
||||
}
|
||||
|
||||
private filterConflicts(): TrustedServerConflictMap {
|
||||
const conflicts = this.rawConflictObjects
|
||||
const trustedConflicts: TrustedServerConflictMap = {}
|
||||
|
||||
for (const conflict of conflicts) {
|
||||
let serverItem: FilteredServerItem | undefined
|
||||
let unsavedItem: FilteredServerItem | undefined
|
||||
|
||||
if (conflict.unsaved_item) {
|
||||
unsavedItem = FilterDisallowedRemotePayloadsAndMap([conflict.unsaved_item])[0]
|
||||
}
|
||||
|
||||
if (conflict.server_item) {
|
||||
serverItem = FilterDisallowedRemotePayloadsAndMap([conflict.server_item])[0]
|
||||
}
|
||||
|
||||
if (!trustedConflicts[conflict.type]) {
|
||||
trustedConflicts[conflict.type] = []
|
||||
}
|
||||
|
||||
const conflictArray = trustedConflicts[conflict.type]
|
||||
if (conflictArray) {
|
||||
const entry: TrustedConflictParams = <TrustedConflictParams>{
|
||||
type: conflict.type,
|
||||
server_item: serverItem,
|
||||
unsaved_item: unsavedItem,
|
||||
}
|
||||
conflictArray.push(entry)
|
||||
}
|
||||
}
|
||||
|
||||
return trustedConflicts
|
||||
}
|
||||
|
||||
public get error(): HttpError | undefined {
|
||||
return isErrorResponse(this.rawResponse) ? this.rawResponse.data?.error : undefined
|
||||
}
|
||||
@@ -66,56 +117,9 @@ export class ServerSyncResponse {
|
||||
}
|
||||
|
||||
public get numberOfItemsInvolved(): number {
|
||||
return this.allFullyFormedPayloads.length
|
||||
}
|
||||
const allPayloads = [...this.retrievedPayloads, ...this.rawConflictObjects]
|
||||
|
||||
private get allFullyFormedPayloads(): FilteredServerItem[] {
|
||||
return [
|
||||
...this.retrievedPayloads,
|
||||
...this.dataConflictPayloads,
|
||||
...this.uuidConflictPayloads,
|
||||
...this.rejectedPayloads,
|
||||
]
|
||||
}
|
||||
|
||||
private get rawUuidConflictItems(): ServerItemResponse[] {
|
||||
return this.rawConflictObjects
|
||||
.filter((conflict) => {
|
||||
return conflict.type === ConflictType.UuidConflict
|
||||
})
|
||||
.map((conflict) => {
|
||||
return conflict.unsaved_item || conflict.item!
|
||||
})
|
||||
}
|
||||
|
||||
private get rawDataConflictItems(): ServerItemResponse[] {
|
||||
return this.rawConflictObjects
|
||||
.filter((conflict) => {
|
||||
return conflict.type === ConflictType.ConflictingData
|
||||
})
|
||||
.map((conflict) => {
|
||||
return conflict.server_item || conflict.item!
|
||||
})
|
||||
}
|
||||
|
||||
private get rawRejectedPayloads(): ServerItemResponse[] {
|
||||
return this.rawConflictObjects
|
||||
.filter((conflict) => {
|
||||
return (
|
||||
conflict.type === ConflictType.ContentTypeError ||
|
||||
conflict.type === ConflictType.ContentError ||
|
||||
conflict.type === ConflictType.ReadOnlyError
|
||||
)
|
||||
})
|
||||
.map((conflict) => {
|
||||
return conflict.unsaved_item!
|
||||
})
|
||||
}
|
||||
|
||||
private get rawConflictObjects(): ConflictParams[] {
|
||||
const conflicts = this.successResponseData?.conflicts || []
|
||||
const legacyConflicts = this.successResponseData?.unsaved || []
|
||||
return conflicts.concat(legacyConflicts)
|
||||
return allPayloads.length
|
||||
}
|
||||
|
||||
public get hasError(): boolean {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { ConflictParams, ConflictType } from '@standardnotes/responses'
|
||||
import {
|
||||
ImmutablePayloadCollection,
|
||||
HistoryMap,
|
||||
@@ -11,13 +12,12 @@ import {
|
||||
DeltaRemoteRejected,
|
||||
DeltaEmit,
|
||||
} from '@standardnotes/models'
|
||||
import { DecryptedServerConflictMap } from './ServerConflictMap'
|
||||
|
||||
type PayloadSet = {
|
||||
retrievedPayloads: FullyFormedPayloadInterface[]
|
||||
savedPayloads: ServerSyncSavedContextualPayload[]
|
||||
uuidConflictPayloads: FullyFormedPayloadInterface[]
|
||||
dataConflictPayloads: FullyFormedPayloadInterface[]
|
||||
rejectedPayloads: FullyFormedPayloadInterface[]
|
||||
conflicts: DecryptedServerConflictMap
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -39,8 +39,8 @@ export class ServerSyncResponseResolver {
|
||||
|
||||
emits.push(this.processRetrievedPayloads())
|
||||
emits.push(this.processSavedPayloads())
|
||||
emits.push(this.processUuidConflictPayloads())
|
||||
emits.push(this.processDataConflictPayloads())
|
||||
emits.push(this.processUuidConflictUnsavedPayloads())
|
||||
emits.push(this.processDataConflictServerPayloads())
|
||||
emits.push(this.processRejectedPayloads())
|
||||
|
||||
return emits
|
||||
@@ -60,27 +60,42 @@ export class ServerSyncResponseResolver {
|
||||
return delta.result()
|
||||
}
|
||||
|
||||
private processDataConflictPayloads(): DeltaEmit {
|
||||
const collection = ImmutablePayloadCollection.WithPayloads(this.payloadSet.dataConflictPayloads)
|
||||
private getConflictsForType<T extends ConflictParams<FullyFormedPayloadInterface>>(type: ConflictType): T[] {
|
||||
const results = this.payloadSet.conflicts[type] || []
|
||||
|
||||
const delta = new DeltaRemoteDataConflicts(this.baseCollection, collection, this.historyMap)
|
||||
return results as T[]
|
||||
}
|
||||
|
||||
private processDataConflictServerPayloads(): DeltaEmit {
|
||||
const delta = new DeltaRemoteDataConflicts(
|
||||
this.baseCollection,
|
||||
this.getConflictsForType(ConflictType.ConflictingData),
|
||||
this.historyMap,
|
||||
)
|
||||
|
||||
return delta.result()
|
||||
}
|
||||
|
||||
private processUuidConflictPayloads(): DeltaEmit {
|
||||
const collection = ImmutablePayloadCollection.WithPayloads(this.payloadSet.uuidConflictPayloads)
|
||||
|
||||
const delta = new DeltaRemoteUuidConflicts(this.baseCollection, collection)
|
||||
private processUuidConflictUnsavedPayloads(): DeltaEmit {
|
||||
const delta = new DeltaRemoteUuidConflicts(this.baseCollection, this.getConflictsForType(ConflictType.UuidConflict))
|
||||
|
||||
return delta.result()
|
||||
}
|
||||
|
||||
private processRejectedPayloads(): DeltaEmit {
|
||||
const collection = ImmutablePayloadCollection.WithPayloads(this.payloadSet.rejectedPayloads)
|
||||
const conflicts = [
|
||||
...this.getConflictsForType(ConflictType.ContentTypeError),
|
||||
...this.getConflictsForType(ConflictType.ContentError),
|
||||
...this.getConflictsForType(ConflictType.ReadOnlyError),
|
||||
...this.getConflictsForType(ConflictType.UuidError),
|
||||
...this.getConflictsForType(ConflictType.SharedVaultSnjsVersionError),
|
||||
...this.getConflictsForType(ConflictType.SharedVaultInsufficientPermissionsError),
|
||||
...this.getConflictsForType(ConflictType.SharedVaultNotMemberError),
|
||||
...this.getConflictsForType(ConflictType.SharedVaultInvalidState),
|
||||
]
|
||||
|
||||
const delta = new DeltaRemoteRejected(this.baseCollection, collection)
|
||||
|
||||
return delta.result()
|
||||
const delta = new DeltaRemoteRejected(this.baseCollection, conflicts)
|
||||
const result = delta.result()
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
import { ConflictType, ConflictParams } from '@standardnotes/responses'
|
||||
import { FullyFormedPayloadInterface, TrustedConflictParams } from '@standardnotes/models'
|
||||
|
||||
export type TrustedServerConflictMap = Partial<Record<ConflictType, TrustedConflictParams[]>>
|
||||
export type DecryptedServerConflictMap = Partial<Record<ConflictType, ConflictParams<FullyFormedPayloadInterface>[]>>
|
||||
@@ -1,15 +1,15 @@
|
||||
import { ConflictParams, ConflictType } from '@standardnotes/responses'
|
||||
import { log, LoggingDomain } from './../../Logging'
|
||||
import { AccountSyncOperation } from '@Lib/Services/Sync/Account/Operation'
|
||||
import { ContentType } from '@standardnotes/common'
|
||||
import {
|
||||
Uuids,
|
||||
extendArray,
|
||||
isNotUndefined,
|
||||
isNullOrUndefined,
|
||||
removeFromIndex,
|
||||
sleep,
|
||||
subtractFromArray,
|
||||
useBoolean,
|
||||
Uuids,
|
||||
} from '@standardnotes/utils'
|
||||
import { ItemManager } from '@Lib/Services/Items/ItemManager'
|
||||
import { OfflineSyncOperation } from '@Lib/Services/Sync/Offline/Operation'
|
||||
@@ -56,6 +56,12 @@ import {
|
||||
getIncrementedDirtyIndex,
|
||||
getCurrentDirtyIndex,
|
||||
ItemContent,
|
||||
KeySystemItemsKeyContent,
|
||||
KeySystemItemsKeyInterface,
|
||||
FullyFormedTransferPayload,
|
||||
ItemMutator,
|
||||
isDecryptedOrDeletedItem,
|
||||
MutationType,
|
||||
} from '@standardnotes/models'
|
||||
import {
|
||||
AbstractService,
|
||||
@@ -71,11 +77,14 @@ import {
|
||||
SyncOptions,
|
||||
SyncQueueStrategy,
|
||||
SyncServiceInterface,
|
||||
DiagnosticInfo,
|
||||
EncryptionService,
|
||||
DeviceInterface,
|
||||
isFullEntryLoadChunkResponse,
|
||||
isChunkFullEntry,
|
||||
SyncEventReceivedSharedVaultInvitesData,
|
||||
SyncEventReceivedRemoteSharedVaultsData,
|
||||
SyncEventReceivedUserEventsData,
|
||||
SyncEventReceivedAsymmetricMessagesData,
|
||||
} from '@standardnotes/services'
|
||||
import { OfflineSyncResponse } from './Offline/Response'
|
||||
import {
|
||||
@@ -86,10 +95,23 @@ import {
|
||||
} from '@standardnotes/encryption'
|
||||
import { CreatePayloadFromRawServerItem } from './Account/Utilities'
|
||||
import { ApplicationSyncOptions } from '@Lib/Application/Options/OptionalOptions'
|
||||
import { DecryptedServerConflictMap, TrustedServerConflictMap } from './Account/ServerConflictMap'
|
||||
|
||||
const DEFAULT_MAJOR_CHANGE_THRESHOLD = 15
|
||||
const INVALID_SESSION_RESPONSE_STATUS = 401
|
||||
|
||||
/** Content types appearing first are always mapped first */
|
||||
const ContentTypeLocalLoadPriorty = [
|
||||
ContentType.ItemsKey,
|
||||
ContentType.KeySystemRootKey,
|
||||
ContentType.KeySystemItemsKey,
|
||||
ContentType.VaultListing,
|
||||
ContentType.TrustedContact,
|
||||
ContentType.UserPrefs,
|
||||
ContentType.Component,
|
||||
ContentType.Theme,
|
||||
]
|
||||
|
||||
/**
|
||||
* The sync service orchestrates with the model manager, api service, and storage service
|
||||
* to ensure consistent state between the three. When a change is made to an item, consumers
|
||||
@@ -100,7 +122,7 @@ const INVALID_SESSION_RESPONSE_STATUS = 401
|
||||
* The sync service largely does not perform any task unless it is called upon.
|
||||
*/
|
||||
export class SNSyncService
|
||||
extends AbstractService<SyncEvent, ServerSyncResponse | OfflineSyncResponse | { source: SyncSource }>
|
||||
extends AbstractService<SyncEvent>
|
||||
implements SyncServiceInterface, InternalEventHandlerInterface, SyncClientInterface
|
||||
{
|
||||
private dirtyIndexAtLastPresyncSave?: number
|
||||
@@ -128,14 +150,6 @@ export class SNSyncService
|
||||
public lastSyncInvokationPromise?: Promise<unknown>
|
||||
public currentSyncRequestPromise?: Promise<void>
|
||||
|
||||
/** Content types appearing first are always mapped first */
|
||||
private readonly localLoadPriorty = [
|
||||
ContentType.ItemsKey,
|
||||
ContentType.UserPrefs,
|
||||
ContentType.Component,
|
||||
ContentType.Theme,
|
||||
]
|
||||
|
||||
constructor(
|
||||
private itemManager: ItemManager,
|
||||
private sessionManager: SNSessionManager,
|
||||
@@ -225,29 +239,21 @@ export class SNSyncService
|
||||
return this.databaseLoaded
|
||||
}
|
||||
|
||||
private async processItemsKeysFirstDuringDatabaseLoad(
|
||||
itemsKeysPayloads: FullyFormedPayloadInterface[],
|
||||
): Promise<void> {
|
||||
if (itemsKeysPayloads.length === 0) {
|
||||
private async processPriorityItemsForDatabaseLoad(items: FullyFormedPayloadInterface[]): Promise<void> {
|
||||
if (items.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
const encryptedItemsKeysPayloads = itemsKeysPayloads.filter(isEncryptedPayload)
|
||||
const encryptedPayloads = items.filter(isEncryptedPayload)
|
||||
const alreadyDecryptedPayloads = items.filter(isDecryptedPayload) as DecryptedPayloadInterface<ItemsKeyContent>[]
|
||||
|
||||
const originallyDecryptedItemsKeysPayloads = itemsKeysPayloads.filter(
|
||||
isDecryptedPayload,
|
||||
) as DecryptedPayloadInterface<ItemsKeyContent>[]
|
||||
const encryptionSplit = SplitPayloadsByEncryptionType(encryptedPayloads)
|
||||
const decryptionSplit = CreateDecryptionSplitWithKeyLookup(encryptionSplit)
|
||||
|
||||
const itemsKeysSplit: KeyedDecryptionSplit = {
|
||||
usesRootKeyWithKeyLookup: {
|
||||
items: encryptedItemsKeysPayloads,
|
||||
},
|
||||
}
|
||||
|
||||
const newlyDecryptedItemsKeys = await this.protocolService.decryptSplit(itemsKeysSplit)
|
||||
const newlyDecryptedPayloads = await this.protocolService.decryptSplit(decryptionSplit)
|
||||
|
||||
await this.payloadManager.emitPayloads(
|
||||
[...originallyDecryptedItemsKeysPayloads, ...newlyDecryptedItemsKeys],
|
||||
[...alreadyDecryptedPayloads, ...newlyDecryptedPayloads],
|
||||
PayloadEmitSource.LocalDatabaseLoaded,
|
||||
)
|
||||
}
|
||||
@@ -262,7 +268,7 @@ export class SNSyncService
|
||||
const chunks = await this.device.getDatabaseLoadChunks(
|
||||
{
|
||||
batchSize: this.options.loadBatchSize,
|
||||
contentTypePriority: this.localLoadPriorty,
|
||||
contentTypePriority: ContentTypeLocalLoadPriorty,
|
||||
uuidPriority: this.launchPriorityUuids,
|
||||
},
|
||||
this.identifier,
|
||||
@@ -272,18 +278,30 @@ export class SNSyncService
|
||||
? chunks.fullEntries.itemsKeys.entries
|
||||
: await this.device.getDatabaseEntries(this.identifier, chunks.keys.itemsKeys.keys)
|
||||
|
||||
const itemsKeyPayloads = itemsKeyEntries
|
||||
.map((entry) => {
|
||||
try {
|
||||
return CreatePayload(entry, PayloadSource.Constructor)
|
||||
} catch (e) {
|
||||
console.error('Creating payload failed', e)
|
||||
return undefined
|
||||
}
|
||||
})
|
||||
.filter(isNotUndefined)
|
||||
const keySystemRootKeyEntries = isFullEntryLoadChunkResponse(chunks)
|
||||
? chunks.fullEntries.keySystemRootKeys.entries
|
||||
: await this.device.getDatabaseEntries(this.identifier, chunks.keys.keySystemRootKeys.keys)
|
||||
|
||||
await this.processItemsKeysFirstDuringDatabaseLoad(itemsKeyPayloads)
|
||||
const keySystemItemsKeyEntries = isFullEntryLoadChunkResponse(chunks)
|
||||
? chunks.fullEntries.keySystemItemsKeys.entries
|
||||
: await this.device.getDatabaseEntries(this.identifier, chunks.keys.keySystemItemsKeys.keys)
|
||||
|
||||
const createPayloadFromEntry = (entry: FullyFormedTransferPayload) => {
|
||||
try {
|
||||
return CreatePayload(entry, PayloadSource.LocalDatabaseLoaded)
|
||||
} catch (e) {
|
||||
console.error('Creating payload failed', e)
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
await this.processPriorityItemsForDatabaseLoad(itemsKeyEntries.map(createPayloadFromEntry).filter(isNotUndefined))
|
||||
await this.processPriorityItemsForDatabaseLoad(
|
||||
keySystemRootKeyEntries.map(createPayloadFromEntry).filter(isNotUndefined),
|
||||
)
|
||||
await this.processPriorityItemsForDatabaseLoad(
|
||||
keySystemItemsKeyEntries.map(createPayloadFromEntry).filter(isNotUndefined),
|
||||
)
|
||||
|
||||
/**
|
||||
* Map in batches to give interface a chance to update. Note that total decryption
|
||||
@@ -308,7 +326,7 @@ export class SNSyncService
|
||||
const payloads = dbEntries
|
||||
.map((entry) => {
|
||||
try {
|
||||
return CreatePayload(entry, PayloadSource.Constructor)
|
||||
return CreatePayload(entry, PayloadSource.LocalDatabaseLoaded)
|
||||
} catch (e) {
|
||||
console.error('Creating payload failed', e)
|
||||
return undefined
|
||||
@@ -348,13 +366,10 @@ export class SNSyncService
|
||||
}
|
||||
}
|
||||
|
||||
const split: KeyedDecryptionSplit = {
|
||||
usesItemsKeyWithKeyLookup: {
|
||||
items: encrypted,
|
||||
},
|
||||
}
|
||||
const encryptionSplit = SplitPayloadsByEncryptionType(encrypted)
|
||||
const decryptionSplit = CreateDecryptionSplitWithKeyLookup(encryptionSplit)
|
||||
|
||||
const results = await this.protocolService.decryptSplit(split)
|
||||
const results = await this.protocolService.decryptSplit(decryptionSplit)
|
||||
|
||||
await this.payloadManager.emitPayloads([...nonencrypted, ...results], PayloadEmitSource.LocalDatabaseLoaded)
|
||||
|
||||
@@ -616,11 +631,7 @@ export class SNSyncService
|
||||
if (useStrategy === SyncQueueStrategy.ResolveOnNext) {
|
||||
return this.queueStrategyResolveOnNext()
|
||||
} else if (useStrategy === SyncQueueStrategy.ForceSpawnNew) {
|
||||
return this.queueStrategyForceSpawnNew({
|
||||
mode: options.mode,
|
||||
checkIntegrity: options.checkIntegrity,
|
||||
source: options.source,
|
||||
})
|
||||
return this.queueStrategyForceSpawnNew(options)
|
||||
} else {
|
||||
throw Error(`Unhandled timing strategy ${useStrategy}`)
|
||||
}
|
||||
@@ -634,7 +645,7 @@ export class SNSyncService
|
||||
) {
|
||||
this.opStatus.setDidBegin()
|
||||
|
||||
await this.notifyEvent(SyncEvent.SyncWillBegin)
|
||||
await this.notifyEvent(SyncEvent.SyncDidBeginProcessing)
|
||||
|
||||
/**
|
||||
* Subtract from array as soon as we're sure they'll be called.
|
||||
@@ -647,12 +658,41 @@ export class SNSyncService
|
||||
* Setting this value means the item was 100% sent to the server.
|
||||
*/
|
||||
if (items.length > 0) {
|
||||
return this.itemManager.setLastSyncBeganForItems(items, beginDate, frozenDirtyIndex)
|
||||
return this.setLastSyncBeganForItems(items, beginDate, frozenDirtyIndex)
|
||||
} else {
|
||||
return items
|
||||
}
|
||||
}
|
||||
|
||||
private async setLastSyncBeganForItems(
|
||||
itemsToLookupUuidsFor: (DecryptedItemInterface | DeletedItemInterface)[],
|
||||
date: Date,
|
||||
globalDirtyIndex: number,
|
||||
): Promise<(DecryptedItemInterface | DeletedItemInterface)[]> {
|
||||
const uuids = Uuids(itemsToLookupUuidsFor)
|
||||
|
||||
const items = this.itemManager.getCollection().findAll(uuids).filter(isDecryptedOrDeletedItem)
|
||||
|
||||
const payloads: (DecryptedPayloadInterface | DeletedPayloadInterface)[] = []
|
||||
|
||||
for (const item of items) {
|
||||
const mutator = new ItemMutator<DecryptedPayloadInterface | DeletedPayloadInterface>(
|
||||
item,
|
||||
MutationType.NonDirtying,
|
||||
)
|
||||
|
||||
mutator.setBeginSync(date, globalDirtyIndex)
|
||||
|
||||
const payload = mutator.getResult()
|
||||
|
||||
payloads.push(payload)
|
||||
}
|
||||
|
||||
await this.payloadManager.emitPayloads(payloads, PayloadEmitSource.PreSyncSave)
|
||||
|
||||
return this.itemManager.findAnyItems(uuids) as (DecryptedItemInterface | DeletedItemInterface)[]
|
||||
}
|
||||
|
||||
/**
|
||||
* The InTime resolve queue refers to any sync requests that were made while we still
|
||||
* have not sent out the current request. So, anything in the InTime resolve queue
|
||||
@@ -725,12 +765,15 @@ export class SNSyncService
|
||||
|
||||
private async createServerSyncOperation(
|
||||
payloads: ServerSyncPushContextualPayload[],
|
||||
checkIntegrity: boolean,
|
||||
source: SyncSource,
|
||||
options: SyncOptions,
|
||||
mode: SyncMode = SyncMode.Default,
|
||||
) {
|
||||
const syncToken = await this.getLastSyncToken()
|
||||
const paginationToken = await this.getPaginationToken()
|
||||
const syncToken =
|
||||
options.sharedVaultUuids && options.sharedVaultUuids.length > 0 && options.syncSharedVaultsFromScratch
|
||||
? undefined
|
||||
: await this.getLastSyncToken()
|
||||
const paginationToken =
|
||||
options.sharedVaultUuids && options.syncSharedVaultsFromScratch ? undefined : await this.getPaginationToken()
|
||||
|
||||
const operation = new AccountSyncOperation(
|
||||
payloads,
|
||||
@@ -753,20 +796,23 @@ export class SNSyncService
|
||||
break
|
||||
}
|
||||
},
|
||||
syncToken,
|
||||
paginationToken,
|
||||
this.apiService,
|
||||
{
|
||||
syncToken,
|
||||
paginationToken,
|
||||
sharedVaultUuids: options.sharedVaultUuids,
|
||||
},
|
||||
)
|
||||
|
||||
log(
|
||||
LoggingDomain.Sync,
|
||||
'Syncing online user',
|
||||
'source',
|
||||
SyncSource[source],
|
||||
SyncSource[options.source],
|
||||
'operation id',
|
||||
operation.id,
|
||||
'integrity check',
|
||||
checkIntegrity,
|
||||
options.checkIntegrity,
|
||||
'mode',
|
||||
SyncMode[mode],
|
||||
'syncToken',
|
||||
@@ -789,12 +835,7 @@ export class SNSyncService
|
||||
const { uploadPayloads, syncMode } = await this.getOnlineSyncParameters(payloads, options.mode)
|
||||
|
||||
return {
|
||||
operation: await this.createServerSyncOperation(
|
||||
uploadPayloads,
|
||||
useBoolean(options.checkIntegrity, false),
|
||||
options.source,
|
||||
syncMode,
|
||||
),
|
||||
operation: await this.createServerSyncOperation(uploadPayloads, options, syncMode),
|
||||
mode: syncMode,
|
||||
}
|
||||
} else {
|
||||
@@ -867,6 +908,7 @@ export class SNSyncService
|
||||
|
||||
await this.notifyEventSync(SyncEvent.SyncCompletedWithAllItemsUploadedAndDownloaded, {
|
||||
source: options.source,
|
||||
options,
|
||||
})
|
||||
|
||||
this.resolvePendingSyncRequestsThatMadeItInTimeOfCurrentRequest(inTimeResolveQueue)
|
||||
@@ -889,7 +931,7 @@ export class SNSyncService
|
||||
|
||||
this.opStatus.clearError()
|
||||
|
||||
await this.notifyEvent(SyncEvent.SingleRoundTripSyncCompleted, response)
|
||||
await this.notifyEvent(SyncEvent.PaginatedSyncRequestCompleted, response)
|
||||
}
|
||||
|
||||
private handleErrorServerResponse(response: ServerSyncResponse) {
|
||||
@@ -917,19 +959,36 @@ export class SNSyncService
|
||||
|
||||
const historyMap = this.historyService.getHistoryMapCopy()
|
||||
|
||||
if (response.userEvents) {
|
||||
await this.notifyEventSync(SyncEvent.ReceivedUserEvents, response.userEvents as SyncEventReceivedUserEventsData)
|
||||
}
|
||||
|
||||
if (response.asymmetricMessages) {
|
||||
await this.notifyEventSync(
|
||||
SyncEvent.ReceivedAsymmetricMessages,
|
||||
response.asymmetricMessages as SyncEventReceivedAsymmetricMessagesData,
|
||||
)
|
||||
}
|
||||
|
||||
if (response.vaults) {
|
||||
await this.notifyEventSync(
|
||||
SyncEvent.ReceivedRemoteSharedVaults,
|
||||
response.vaults as SyncEventReceivedRemoteSharedVaultsData,
|
||||
)
|
||||
}
|
||||
|
||||
if (response.vaultInvites) {
|
||||
await this.notifyEventSync(
|
||||
SyncEvent.ReceivedSharedVaultInvites,
|
||||
response.vaultInvites as SyncEventReceivedSharedVaultInvitesData,
|
||||
)
|
||||
}
|
||||
|
||||
const resolver = new ServerSyncResponseResolver(
|
||||
{
|
||||
retrievedPayloads: await this.processServerPayloads(response.retrievedPayloads, PayloadSource.RemoteRetrieved),
|
||||
savedPayloads: response.savedPayloads,
|
||||
uuidConflictPayloads: await this.processServerPayloads(
|
||||
response.uuidConflictPayloads,
|
||||
PayloadSource.RemoteRetrieved,
|
||||
),
|
||||
dataConflictPayloads: await this.processServerPayloads(
|
||||
response.dataConflictPayloads,
|
||||
PayloadSource.RemoteRetrieved,
|
||||
),
|
||||
rejectedPayloads: await this.processServerPayloads(response.rejectedPayloads, PayloadSource.RemoteRetrieved),
|
||||
conflicts: await this.decryptServerConflicts(response.conflicts),
|
||||
},
|
||||
masterCollection,
|
||||
operation.payloadsSavedOrSaving,
|
||||
@@ -954,11 +1013,69 @@ export class SNSyncService
|
||||
await this.persistPayloads(payloadsToPersist)
|
||||
}
|
||||
|
||||
await Promise.all([
|
||||
this.setLastSyncToken(response.lastSyncToken as string),
|
||||
this.setPaginationToken(response.paginationToken as string),
|
||||
this.notifyEvent(SyncEvent.SingleRoundTripSyncCompleted, response),
|
||||
])
|
||||
if (!operation.options.sharedVaultUuids) {
|
||||
await Promise.all([
|
||||
this.setLastSyncToken(response.lastSyncToken as string),
|
||||
this.setPaginationToken(response.paginationToken as string),
|
||||
])
|
||||
}
|
||||
|
||||
await this.notifyEvent(SyncEvent.PaginatedSyncRequestCompleted, {
|
||||
...response,
|
||||
uploadedPayloads: operation.payloads,
|
||||
options: operation.options,
|
||||
})
|
||||
}
|
||||
|
||||
private async decryptServerConflicts(conflictMap: TrustedServerConflictMap): Promise<DecryptedServerConflictMap> {
|
||||
const decrypted: DecryptedServerConflictMap = {}
|
||||
|
||||
for (const conflictType of Object.keys(conflictMap)) {
|
||||
const conflictsForType = conflictMap[conflictType as ConflictType]
|
||||
if (!conflictsForType) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (!decrypted[conflictType as ConflictType]) {
|
||||
decrypted[conflictType as ConflictType] = []
|
||||
}
|
||||
|
||||
const decryptedConflictsForType = decrypted[conflictType as ConflictType]
|
||||
if (!decryptedConflictsForType) {
|
||||
throw Error('Decrypted conflicts for type should exist')
|
||||
}
|
||||
|
||||
for (const conflict of conflictsForType) {
|
||||
const decryptedUnsavedItem = conflict.unsaved_item
|
||||
? await this.processServerPayload(conflict.unsaved_item, PayloadSource.RemoteRetrieved)
|
||||
: undefined
|
||||
|
||||
const decryptedServerItem = conflict.server_item
|
||||
? await this.processServerPayload(conflict.server_item, PayloadSource.RemoteRetrieved)
|
||||
: undefined
|
||||
|
||||
const decryptedEntry: ConflictParams<FullyFormedPayloadInterface> = <
|
||||
ConflictParams<FullyFormedPayloadInterface>
|
||||
>{
|
||||
type: conflict.type,
|
||||
unsaved_item: decryptedUnsavedItem,
|
||||
server_item: decryptedServerItem,
|
||||
}
|
||||
|
||||
decryptedConflictsForType.push(decryptedEntry)
|
||||
}
|
||||
}
|
||||
|
||||
return decrypted
|
||||
}
|
||||
|
||||
private async processServerPayload(
|
||||
item: FilteredServerItem,
|
||||
source: PayloadSource,
|
||||
): Promise<FullyFormedPayloadInterface> {
|
||||
const result = await this.processServerPayloads([item], source)
|
||||
|
||||
return result[0]
|
||||
}
|
||||
|
||||
private async processServerPayloads(
|
||||
@@ -971,7 +1088,8 @@ export class SNSyncService
|
||||
|
||||
const results: FullyFormedPayloadInterface[] = [...deleted]
|
||||
|
||||
const { rootKeyEncryption, itemsKeyEncryption } = SplitPayloadsByEncryptionType(encrypted)
|
||||
const { rootKeyEncryption, itemsKeyEncryption, keySystemRootKeyEncryption } =
|
||||
SplitPayloadsByEncryptionType(encrypted)
|
||||
|
||||
const { results: rootKeyDecryptionResults, map: processedItemsKeys } = await this.decryptServerItemsKeys(
|
||||
rootKeyEncryption || [],
|
||||
@@ -979,8 +1097,16 @@ export class SNSyncService
|
||||
|
||||
extendArray(results, rootKeyDecryptionResults)
|
||||
|
||||
const { results: keySystemRootKeyDecryptionResults, map: processedKeySystemItemsKeys } =
|
||||
await this.decryptServerKeySystemItemsKeys(keySystemRootKeyEncryption || [])
|
||||
|
||||
extendArray(results, keySystemRootKeyDecryptionResults)
|
||||
|
||||
if (itemsKeyEncryption) {
|
||||
const decryptionResults = await this.decryptProcessedServerPayloads(itemsKeyEncryption, processedItemsKeys)
|
||||
const decryptionResults = await this.decryptProcessedServerPayloads(itemsKeyEncryption, {
|
||||
...processedItemsKeys,
|
||||
...processedKeySystemItemsKeys,
|
||||
})
|
||||
extendArray(results, decryptionResults)
|
||||
}
|
||||
|
||||
@@ -1017,17 +1143,53 @@ export class SNSyncService
|
||||
}
|
||||
}
|
||||
|
||||
private async decryptServerKeySystemItemsKeys(payloads: EncryptedPayloadInterface[]) {
|
||||
const map: Record<UuidString, DecryptedPayloadInterface<KeySystemItemsKeyContent>> = {}
|
||||
|
||||
if (payloads.length === 0) {
|
||||
return {
|
||||
results: [],
|
||||
map,
|
||||
}
|
||||
}
|
||||
|
||||
const keySystemRootKeySplit: KeyedDecryptionSplit = {
|
||||
usesKeySystemRootKeyWithKeyLookup: {
|
||||
items: payloads,
|
||||
},
|
||||
}
|
||||
|
||||
const results = await this.protocolService.decryptSplit<KeySystemItemsKeyContent>(keySystemRootKeySplit)
|
||||
|
||||
results.forEach((result) => {
|
||||
if (
|
||||
isDecryptedPayload<KeySystemItemsKeyContent>(result) &&
|
||||
result.content_type === ContentType.KeySystemItemsKey
|
||||
) {
|
||||
map[result.uuid] = result
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
results,
|
||||
map,
|
||||
}
|
||||
}
|
||||
|
||||
private async decryptProcessedServerPayloads(
|
||||
payloads: EncryptedPayloadInterface[],
|
||||
map: Record<UuidString, DecryptedPayloadInterface<ItemsKeyContent>>,
|
||||
map: Record<UuidString, DecryptedPayloadInterface<ItemsKeyContent | KeySystemItemsKeyContent>>,
|
||||
): Promise<(EncryptedPayloadInterface | DecryptedPayloadInterface)[]> {
|
||||
return Promise.all(
|
||||
payloads.map(async (encrypted) => {
|
||||
const previouslyProcessedItemsKey: DecryptedPayloadInterface<ItemsKeyContent> | undefined =
|
||||
map[encrypted.items_key_id as string]
|
||||
const previouslyProcessedItemsKey:
|
||||
| DecryptedPayloadInterface<ItemsKeyContent | KeySystemItemsKeyContent>
|
||||
| undefined = map[encrypted.items_key_id as string]
|
||||
|
||||
const itemsKey = previouslyProcessedItemsKey
|
||||
? (CreateDecryptedItemFromPayload(previouslyProcessedItemsKey) as ItemsKeyInterface)
|
||||
? (CreateDecryptedItemFromPayload(previouslyProcessedItemsKey) as
|
||||
| ItemsKeyInterface
|
||||
| KeySystemItemsKeyInterface)
|
||||
: undefined
|
||||
|
||||
const keyedSplit: KeyedDecryptionSplit = {}
|
||||
@@ -1251,26 +1413,13 @@ export class SNSyncService
|
||||
await this.persistPayloads(emit.emits)
|
||||
}
|
||||
|
||||
override async getDiagnostics(): Promise<DiagnosticInfo | undefined> {
|
||||
const dirtyUuids = Uuids(this.itemsNeedingSync())
|
||||
|
||||
return {
|
||||
sync: {
|
||||
syncToken: await this.getLastSyncToken(),
|
||||
cursorToken: await this.getPaginationToken(),
|
||||
dirtyIndexAtLastPresyncSave: this.dirtyIndexAtLastPresyncSave,
|
||||
lastSyncDate: this.lastSyncDate,
|
||||
outOfSync: this.outOfSync,
|
||||
completedOnlineDownloadFirstSync: this.completedOnlineDownloadFirstSync,
|
||||
clientLocked: this.clientLocked,
|
||||
databaseLoaded: this.databaseLoaded,
|
||||
syncLock: this.syncLock,
|
||||
dealloced: this.dealloced,
|
||||
itemsNeedingSync: dirtyUuids,
|
||||
itemsNeedingSyncCount: dirtyUuids.length,
|
||||
pendingRequestCount: this.resolveQueue.length + this.spawnQueue.length,
|
||||
},
|
||||
}
|
||||
async syncSharedVaultsFromScratch(sharedVaultUuids: string[]): Promise<void> {
|
||||
await this.sync({
|
||||
sharedVaultUuids: sharedVaultUuids,
|
||||
syncSharedVaultsFromScratch: true,
|
||||
queueStrategy: SyncQueueStrategy.ForceSpawnNew,
|
||||
awaitAll: true,
|
||||
})
|
||||
}
|
||||
|
||||
/** @e2e_testing */
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
export const InputStrings = {
|
||||
FileAccountPassword: 'File account password',
|
||||
}
|
||||
@@ -1,9 +1,7 @@
|
||||
import { ConfirmStrings } from './Confirm'
|
||||
import { InputStrings } from './Input'
|
||||
import { NetworkStrings } from './Network'
|
||||
|
||||
export const Strings = {
|
||||
Network: NetworkStrings,
|
||||
Confirm: ConfirmStrings,
|
||||
Input: InputStrings,
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ export * from './Types'
|
||||
export * from './Version'
|
||||
export * from '@standardnotes/common'
|
||||
export * from '@standardnotes/domain-core'
|
||||
export * from '@standardnotes/api'
|
||||
export * from '@standardnotes/encryption'
|
||||
export * from '@standardnotes/features'
|
||||
export * from '@standardnotes/files'
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
"emitDeclarationOnly": true,
|
||||
"esModuleInterop": true,
|
||||
"isolatedModules": true,
|
||||
"lib": ["es6", "dom", "es2016", "es2017"],
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"newLine": "lf",
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
@@ -17,11 +19,11 @@
|
||||
"noImplicitThis": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"skipLibCheck": true,
|
||||
"outDir": "../dist/@types",
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"strictNullChecks": true,
|
||||
"target": "esnext",
|
||||
"target": "es6",
|
||||
"paths": {
|
||||
"@Lib/*": ["*"],
|
||||
"@Services/*": ["Services/*"]
|
||||
|
||||
@@ -22,7 +22,7 @@ describe('000 legacy protocol operations', () => {
|
||||
|
||||
let error
|
||||
try {
|
||||
protocol004.generateDecryptedParametersSync({
|
||||
protocol004.generateDecryptedParameters({
|
||||
uuid: 'foo',
|
||||
content: string,
|
||||
content_type: 'foo',
|
||||
|
||||
@@ -7,16 +7,16 @@ const expect = chai.expect
|
||||
describe('004 protocol operations', function () {
|
||||
const _identifier = 'hello@test.com'
|
||||
const _password = 'password'
|
||||
let _keyParams
|
||||
let _key
|
||||
let rootKeyParams
|
||||
let rootKey
|
||||
|
||||
const application = Factory.createApplicationWithRealCrypto()
|
||||
const protocol004 = new SNProtocolOperator004(new SNWebCrypto())
|
||||
|
||||
before(async function () {
|
||||
await Factory.initializeApplication(application)
|
||||
_key = await protocol004.createRootKey(_identifier, _password, KeyParamsOrigination.Registration)
|
||||
_keyParams = _key.keyParams
|
||||
rootKey = await protocol004.createRootKey(_identifier, _password, KeyParamsOrigination.Registration)
|
||||
rootKeyParams = rootKey.keyParams
|
||||
})
|
||||
|
||||
after(async function () {
|
||||
@@ -69,43 +69,58 @@ describe('004 protocol operations', function () {
|
||||
})
|
||||
|
||||
it('properly encrypts and decrypts', async function () {
|
||||
const text = 'hello world'
|
||||
const rawKey = _key.masterKey
|
||||
const nonce = await application.protocolService.crypto.generateRandomKey(192)
|
||||
const payload = new DecryptedPayload({
|
||||
uuid: '123',
|
||||
content_type: ContentType.ItemsKey,
|
||||
content: FillItemContent({
|
||||
title: 'foo',
|
||||
text: 'bar',
|
||||
}),
|
||||
})
|
||||
|
||||
const operator = application.protocolService.operatorManager.operatorForVersion(ProtocolVersion.V004)
|
||||
const authenticatedData = { foo: 'bar' }
|
||||
const encString = await operator.encryptString004(text, rawKey, nonce, authenticatedData)
|
||||
const decString = await operator.decryptString004(
|
||||
encString,
|
||||
rawKey,
|
||||
nonce,
|
||||
await operator.authenticatedDataToString(authenticatedData),
|
||||
)
|
||||
expect(decString).to.equal(text)
|
||||
|
||||
const encrypted = await operator.generateEncryptedParameters(payload, rootKey)
|
||||
const decrypted = await operator.generateDecryptedParameters(encrypted, rootKey)
|
||||
|
||||
expect(decrypted.content.title).to.equal('foo')
|
||||
expect(decrypted.content.text).to.equal('bar')
|
||||
})
|
||||
|
||||
it('fails to decrypt non-matching aad', async function () {
|
||||
const text = 'hello world'
|
||||
const rawKey = _key.masterKey
|
||||
const nonce = await application.protocolService.crypto.generateRandomKey(192)
|
||||
const payload = new DecryptedPayload({
|
||||
uuid: '123',
|
||||
content_type: ContentType.ItemsKey,
|
||||
content: FillItemContent({
|
||||
title: 'foo',
|
||||
text: 'bar',
|
||||
}),
|
||||
})
|
||||
|
||||
const operator = application.protocolService.operatorManager.operatorForVersion(ProtocolVersion.V004)
|
||||
const aad = { foo: 'bar' }
|
||||
const nonmatchingAad = { foo: 'rab' }
|
||||
const encString = await operator.encryptString004(text, rawKey, nonce, aad)
|
||||
const decString = await operator.decryptString004(encString, rawKey, nonce, nonmatchingAad)
|
||||
expect(decString).to.not.be.ok
|
||||
|
||||
const encrypted = await operator.generateEncryptedParameters(payload, rootKey)
|
||||
const decrypted = await operator.generateDecryptedParameters(
|
||||
{
|
||||
...encrypted,
|
||||
uuid: 'nonmatching',
|
||||
},
|
||||
rootKey,
|
||||
)
|
||||
|
||||
expect(decrypted.errorDecrypting).to.equal(true)
|
||||
})
|
||||
|
||||
it('generates existing keys for key params', async function () {
|
||||
const key = await application.protocolService.computeRootKey(_password, _keyParams)
|
||||
expect(key.compare(_key)).to.be.true
|
||||
const key = await application.protocolService.computeRootKey(_password, rootKeyParams)
|
||||
expect(key.compare(rootKey)).to.be.true
|
||||
})
|
||||
|
||||
it('can decrypt encrypted params', async function () {
|
||||
const payload = Factory.createNotePayload()
|
||||
const key = await protocol004.createItemsKey()
|
||||
const params = await protocol004.generateEncryptedParametersSync(payload, key)
|
||||
const decrypted = await protocol004.generateDecryptedParametersSync(params, key)
|
||||
const params = await protocol004.generateEncryptedParameters(payload, key)
|
||||
const decrypted = await protocol004.generateDecryptedParameters(params, key)
|
||||
expect(decrypted.errorDecrypting).to.not.be.ok
|
||||
expect(decrypted.content).to.eql(payload.content)
|
||||
})
|
||||
@@ -113,9 +128,9 @@ describe('004 protocol operations', function () {
|
||||
it('modifying the uuid of the payload should fail to decrypt', async function () {
|
||||
const payload = Factory.createNotePayload()
|
||||
const key = await protocol004.createItemsKey()
|
||||
const params = await protocol004.generateEncryptedParametersSync(payload, key)
|
||||
const params = await protocol004.generateEncryptedParameters(payload, key)
|
||||
params.uuid = 'foo'
|
||||
const result = await protocol004.generateDecryptedParametersSync(params, key)
|
||||
const result = await protocol004.generateDecryptedParameters(params, key)
|
||||
expect(result.errorDecrypting).to.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
58
packages/snjs/mocha/TestRegistry/BaseTests.js
Normal file
58
packages/snjs/mocha/TestRegistry/BaseTests.js
Normal file
@@ -0,0 +1,58 @@
|
||||
export const BaseTests = [
|
||||
'memory.test.js',
|
||||
'protocol.test.js',
|
||||
'utils.test.js',
|
||||
'000.test.js',
|
||||
'001.test.js',
|
||||
'002.test.js',
|
||||
'003.test.js',
|
||||
'004.test.js',
|
||||
'username.test.js',
|
||||
'app-group.test.js',
|
||||
'application.test.js',
|
||||
'payload.test.js',
|
||||
'payload_encryption.test.js',
|
||||
'item.test.js',
|
||||
'item_manager.test.js',
|
||||
'features.test.js',
|
||||
'settings.test.js',
|
||||
'mfa_service.test.js',
|
||||
'mutator.test.js',
|
||||
'mutator_service.test.js',
|
||||
'payload_manager.test.js',
|
||||
'collections.test.js',
|
||||
'note_display_criteria.test.js',
|
||||
'keys.test.js',
|
||||
'key_params.test.js',
|
||||
'key_recovery_service.test.js',
|
||||
'backups.test.js',
|
||||
'upgrading.test.js',
|
||||
'model_tests/importing.test.js',
|
||||
'model_tests/appmodels.test.js',
|
||||
'model_tests/items.test.js',
|
||||
'model_tests/mapping.test.js',
|
||||
'model_tests/notes_smart_tags.test.js',
|
||||
'model_tests/notes_tags.test.js',
|
||||
'model_tests/notes_tags_folders.test.js',
|
||||
'model_tests/performance.test.js',
|
||||
'sync_tests/offline.test.js',
|
||||
'sync_tests/notes_tags.test.js',
|
||||
'sync_tests/online.test.js',
|
||||
'sync_tests/conflicting.test.js',
|
||||
'sync_tests/integrity.test.js',
|
||||
'auth-fringe-cases.test.js',
|
||||
'auth.test.js',
|
||||
'device_auth.test.js',
|
||||
'storage.test.js',
|
||||
'protection.test.js',
|
||||
'singletons.test.js',
|
||||
'migrations/migration.test.js',
|
||||
'migrations/tags-to-folders.test.js',
|
||||
'history.test.js',
|
||||
'actions.test.js',
|
||||
'preferences.test.js',
|
||||
'files.test.js',
|
||||
'session.test.js',
|
||||
'subscriptions.test.js',
|
||||
'recovery.test.js',
|
||||
];
|
||||
7
packages/snjs/mocha/TestRegistry/MainRegistry.js
Normal file
7
packages/snjs/mocha/TestRegistry/MainRegistry.js
Normal file
@@ -0,0 +1,7 @@
|
||||
import { BaseTests } from './BaseTests.js'
|
||||
import { VaultTests } from './VaultTests.js'
|
||||
|
||||
export default {
|
||||
BaseTests,
|
||||
VaultTests,
|
||||
}
|
||||
16
packages/snjs/mocha/TestRegistry/VaultTests.js
Normal file
16
packages/snjs/mocha/TestRegistry/VaultTests.js
Normal file
@@ -0,0 +1,16 @@
|
||||
|
||||
export const VaultTests = [
|
||||
'vaults/vaults.test.js',
|
||||
'vaults/pkc.test.js',
|
||||
'vaults/contacts.test.js',
|
||||
'vaults/crypto.test.js',
|
||||
'vaults/asymmetric-messages.test.js',
|
||||
'vaults/shared_vaults.test.js',
|
||||
'vaults/invites.test.js',
|
||||
'vaults/items.test.js',
|
||||
'vaults/conflicts.test.js',
|
||||
'vaults/deletion.test.js',
|
||||
'vaults/permissions.test.js',
|
||||
'vaults/key_rotation.test.js',
|
||||
'vaults/files.test.js',
|
||||
];
|
||||
@@ -170,10 +170,7 @@ describe('actions service', () => {
|
||||
})
|
||||
|
||||
// Extension item
|
||||
const extensionItem = await this.application.itemManager.createItem(
|
||||
ContentType.ActionsExtension,
|
||||
this.actionsExtension,
|
||||
)
|
||||
const extensionItem = await this.application.mutator.createItem(ContentType.ActionsExtension, this.actionsExtension)
|
||||
this.extensionItemUuid = extensionItem.uuid
|
||||
})
|
||||
|
||||
@@ -185,7 +182,7 @@ describe('actions service', () => {
|
||||
})
|
||||
|
||||
it('should get extension items', async function () {
|
||||
await this.itemManager.createItem(ContentType.Note, {
|
||||
await this.application.mutator.createItem(ContentType.Note, {
|
||||
title: 'A simple note',
|
||||
text: 'Standard Notes rocks! lml.',
|
||||
})
|
||||
@@ -194,7 +191,7 @@ describe('actions service', () => {
|
||||
})
|
||||
|
||||
it('should get extensions in context of item', async function () {
|
||||
const noteItem = await this.itemManager.createItem(ContentType.Note, {
|
||||
const noteItem = await this.application.mutator.createItem(ContentType.Note, {
|
||||
title: 'Another note',
|
||||
text: 'Whiskey In The Jar',
|
||||
})
|
||||
@@ -205,7 +202,7 @@ describe('actions service', () => {
|
||||
})
|
||||
|
||||
it('should get actions based on item context', async function () {
|
||||
const tagItem = await this.itemManager.createItem(ContentType.Tag, {
|
||||
const tagItem = await this.application.mutator.createItem(ContentType.Tag, {
|
||||
title: 'Music',
|
||||
})
|
||||
|
||||
@@ -217,7 +214,7 @@ describe('actions service', () => {
|
||||
})
|
||||
|
||||
it('should load extension in context of item', async function () {
|
||||
const noteItem = await this.itemManager.createItem(ContentType.Note, {
|
||||
const noteItem = await this.application.mutator.createItem(ContentType.Note, {
|
||||
title: 'Yet another note',
|
||||
text: 'And all things will end ♫',
|
||||
})
|
||||
@@ -249,7 +246,7 @@ describe('actions service', () => {
|
||||
const sandbox = sinon.createSandbox()
|
||||
|
||||
before(async function () {
|
||||
this.noteItem = await this.itemManager.createItem(ContentType.Note, {
|
||||
this.noteItem = await this.application.mutator.createItem(ContentType.Note, {
|
||||
title: 'Hey',
|
||||
text: 'Welcome To Paradise',
|
||||
})
|
||||
@@ -331,7 +328,7 @@ describe('actions service', () => {
|
||||
const sandbox = sinon.createSandbox()
|
||||
|
||||
before(async function () {
|
||||
this.noteItem = await this.itemManager.createItem(ContentType.Note, {
|
||||
this.noteItem = await this.application.mutator.createItem(ContentType.Note, {
|
||||
title: 'Excuse Me',
|
||||
text: 'Time To Be King 8)',
|
||||
})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* eslint-disable no-unused-expressions */
|
||||
/* eslint-disable no-undef */
|
||||
import { BaseItemCounts } from './lib/Applications.js'
|
||||
import { BaseItemCounts } from './lib/BaseItemCounts.js'
|
||||
import * as Factory from './lib/factory.js'
|
||||
chai.use(chaiAsPromised)
|
||||
const expect = chai.expect
|
||||
@@ -75,7 +75,7 @@ describe('application instances', () => {
|
||||
/** Recreate app with different host */
|
||||
const recreatedContext = await Factory.createAppContext({
|
||||
identifier: 'app',
|
||||
host: 'http://nonsense.host'
|
||||
host: 'http://nonsense.host',
|
||||
})
|
||||
await recreatedContext.launch()
|
||||
|
||||
@@ -134,7 +134,7 @@ describe('application instances', () => {
|
||||
})
|
||||
|
||||
it('shows confirmation dialog when there are unsaved changes', async () => {
|
||||
await testSNApp.itemManager.setItemDirty(testNote1)
|
||||
await testSNApp.mutator.setItemDirty(testNote1)
|
||||
await testSNApp.user.signOut()
|
||||
|
||||
const expectedConfirmMessage = signOutConfirmMessage(1)
|
||||
@@ -154,7 +154,7 @@ describe('application instances', () => {
|
||||
})
|
||||
|
||||
it('does not show confirmation dialog when there are unsaved changes and the "force" option is set to true', async () => {
|
||||
await testSNApp.itemManager.setItemDirty(testNote1)
|
||||
await testSNApp.mutator.setItemDirty(testNote1)
|
||||
await testSNApp.user.signOut(true)
|
||||
|
||||
expect(confirmAlert.callCount).to.equal(0)
|
||||
@@ -166,7 +166,7 @@ describe('application instances', () => {
|
||||
confirmAlert.restore()
|
||||
confirmAlert = sinon.stub(testSNApp.alertService, 'confirm').callsFake((_message) => false)
|
||||
|
||||
await testSNApp.itemManager.setItemDirty(testNote1)
|
||||
await testSNApp.mutator.setItemDirty(testNote1)
|
||||
await testSNApp.user.signOut()
|
||||
|
||||
const expectedConfirmMessage = signOutConfirmMessage(1)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* eslint-disable no-unused-expressions */
|
||||
/* eslint-disable no-undef */
|
||||
import { BaseItemCounts } from './lib/Applications.js'
|
||||
import { BaseItemCounts } from './lib/BaseItemCounts.js'
|
||||
import * as Factory from './lib/factory.js'
|
||||
chai.use(chaiAsPromised)
|
||||
const expect = chai.expect
|
||||
@@ -85,14 +85,14 @@ describe('auth fringe cases', () => {
|
||||
|
||||
const serverText = 'server text'
|
||||
|
||||
await context.application.mutator.changeAndSaveItem(firstVersionOfNote, (mutator) => {
|
||||
await context.application.changeAndSaveItem(firstVersionOfNote, (mutator) => {
|
||||
mutator.text = serverText
|
||||
})
|
||||
|
||||
const newApplication = await Factory.signOutApplicationAndReturnNew(context.application)
|
||||
|
||||
/** Create same note but now offline */
|
||||
await newApplication.itemManager.emitItemFromPayload(firstVersionOfNote.payload)
|
||||
await newApplication.mutator.emitItemFromPayload(firstVersionOfNote.payload)
|
||||
|
||||
/** Sign in and merge local data */
|
||||
await newApplication.signIn(context.email, context.password, undefined, undefined, true, true)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* eslint-disable no-unused-expressions */
|
||||
/* eslint-disable no-undef */
|
||||
import { BaseItemCounts } from './lib/Applications.js'
|
||||
import { BaseItemCounts } from './lib/BaseItemCounts.js'
|
||||
import * as Factory from './lib/factory.js'
|
||||
chai.use(chaiAsPromised)
|
||||
const expect = chai.expect
|
||||
@@ -15,7 +15,7 @@ describe('basic auth', function () {
|
||||
|
||||
beforeEach(async function () {
|
||||
localStorage.clear()
|
||||
this.expectedItemCount = BaseItemCounts.DefaultItems
|
||||
this.expectedItemCount = BaseItemCounts.DefaultItemsWithAccount
|
||||
this.context = await Factory.createAppContext()
|
||||
await this.context.launch()
|
||||
this.application = this.context.application
|
||||
@@ -262,7 +262,7 @@ describe('basic auth', function () {
|
||||
if (!didCompleteDownloadFirstSync) {
|
||||
return
|
||||
}
|
||||
if (!didCompletePostDownloadFirstSync && eventName === SyncEvent.SingleRoundTripSyncCompleted) {
|
||||
if (!didCompletePostDownloadFirstSync && eventName === SyncEvent.PaginatedSyncRequestCompleted) {
|
||||
didCompletePostDownloadFirstSync = true
|
||||
/** Should be in sync */
|
||||
outOfSync = this.application.syncService.isOutOfSync()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* eslint-disable no-unused-expressions */
|
||||
/* eslint-disable no-undef */
|
||||
import { BaseItemCounts } from './lib/Applications.js'
|
||||
import { BaseItemCounts } from './lib/BaseItemCounts.js'
|
||||
import * as Factory from './lib/factory.js'
|
||||
chai.use(chaiAsPromised)
|
||||
const expect = chai.expect
|
||||
@@ -25,9 +25,6 @@ describe('backups', function () {
|
||||
this.application = null
|
||||
})
|
||||
|
||||
const BASE_ITEM_COUNT_ENCRYPTED = BaseItemCounts.DefaultItems
|
||||
const BASE_ITEM_COUNT_DECRYPTED = ['UserPreferences', 'DarkTheme'].length
|
||||
|
||||
it('backup file should have a version number', async function () {
|
||||
let data = await this.application.createDecryptedBackupFile()
|
||||
expect(data.version).to.equal(this.application.protocolService.getLatestVersion())
|
||||
@@ -39,7 +36,9 @@ describe('backups', function () {
|
||||
it('no passcode + no account backup file should have correct number of items', async function () {
|
||||
await Promise.all([Factory.createSyncedNote(this.application), Factory.createSyncedNote(this.application)])
|
||||
const data = await this.application.createDecryptedBackupFile()
|
||||
expect(data.items.length).to.equal(BASE_ITEM_COUNT_DECRYPTED + 2)
|
||||
const offsetForNewItems = 2
|
||||
const offsetForNoItemsKey = -1
|
||||
expect(data.items.length).to.equal(BaseItemCounts.DefaultItems + offsetForNewItems + offsetForNoItemsKey)
|
||||
})
|
||||
|
||||
it('passcode + no account backup file should have correct number of items', async function () {
|
||||
@@ -49,12 +48,12 @@ describe('backups', function () {
|
||||
|
||||
// Encrypted backup without authorization
|
||||
const encryptedData = await this.application.createEncryptedBackupFileForAutomatedDesktopBackups()
|
||||
expect(encryptedData.items.length).to.equal(BASE_ITEM_COUNT_ENCRYPTED + 2)
|
||||
expect(encryptedData.items.length).to.equal(BaseItemCounts.DefaultItems + 2)
|
||||
|
||||
// Encrypted backup with authorization
|
||||
Factory.handlePasswordChallenges(this.application, passcode)
|
||||
const authorizedEncryptedData = await this.application.createEncryptedBackupFile()
|
||||
expect(authorizedEncryptedData.items.length).to.equal(BASE_ITEM_COUNT_ENCRYPTED + 2)
|
||||
expect(authorizedEncryptedData.items.length).to.equal(BaseItemCounts.DefaultItems + 2)
|
||||
})
|
||||
|
||||
it('no passcode + account backup file should have correct number of items', async function () {
|
||||
@@ -68,17 +67,17 @@ describe('backups', function () {
|
||||
|
||||
// Encrypted backup without authorization
|
||||
const encryptedData = await this.application.createEncryptedBackupFileForAutomatedDesktopBackups()
|
||||
expect(encryptedData.items.length).to.equal(BASE_ITEM_COUNT_ENCRYPTED + 2)
|
||||
expect(encryptedData.items.length).to.equal(BaseItemCounts.DefaultItemsWithAccount + 2)
|
||||
|
||||
Factory.handlePasswordChallenges(this.application, this.password)
|
||||
|
||||
// Decrypted backup
|
||||
const decryptedData = await this.application.createDecryptedBackupFile()
|
||||
expect(decryptedData.items.length).to.equal(BASE_ITEM_COUNT_DECRYPTED + 2)
|
||||
expect(decryptedData.items.length).to.equal(BaseItemCounts.DefaultItemsWithAccountWithoutItemsKey + 2)
|
||||
|
||||
// Encrypted backup with authorization
|
||||
const authorizedEncryptedData = await this.application.createEncryptedBackupFile()
|
||||
expect(authorizedEncryptedData.items.length).to.equal(BASE_ITEM_COUNT_ENCRYPTED + 2)
|
||||
expect(authorizedEncryptedData.items.length).to.equal(BaseItemCounts.DefaultItemsWithAccount + 2)
|
||||
})
|
||||
|
||||
it('passcode + account backup file should have correct number of items', async function () {
|
||||
@@ -91,17 +90,17 @@ describe('backups', function () {
|
||||
|
||||
// Encrypted backup without authorization
|
||||
const encryptedData = await this.application.createEncryptedBackupFileForAutomatedDesktopBackups()
|
||||
expect(encryptedData.items.length).to.equal(BASE_ITEM_COUNT_ENCRYPTED + 2)
|
||||
expect(encryptedData.items.length).to.equal(BaseItemCounts.DefaultItemsWithAccount + 2)
|
||||
|
||||
Factory.handlePasswordChallenges(this.application, passcode)
|
||||
|
||||
// Decrypted backup
|
||||
const decryptedData = await this.application.createDecryptedBackupFile()
|
||||
expect(decryptedData.items.length).to.equal(BASE_ITEM_COUNT_DECRYPTED + 2)
|
||||
expect(decryptedData.items.length).to.equal(BaseItemCounts.DefaultItemsWithAccountWithoutItemsKey + 2)
|
||||
|
||||
// Encrypted backup with authorization
|
||||
const authorizedEncryptedData = await this.application.createEncryptedBackupFileForAutomatedDesktopBackups()
|
||||
expect(authorizedEncryptedData.items.length).to.equal(BASE_ITEM_COUNT_ENCRYPTED + 2)
|
||||
expect(authorizedEncryptedData.items.length).to.equal(BaseItemCounts.DefaultItemsWithAccount + 2)
|
||||
})
|
||||
|
||||
it('backup file item should have correct fields', async function () {
|
||||
@@ -154,7 +153,7 @@ describe('backups', function () {
|
||||
errorDecrypting: true,
|
||||
})
|
||||
|
||||
await this.application.itemManager.emitItemFromPayload(errored)
|
||||
await this.application.payloadManager.emitPayload(errored)
|
||||
|
||||
const erroredItem = this.application.itemManager.findAnyItem(errored.uuid)
|
||||
|
||||
@@ -162,7 +161,7 @@ describe('backups', function () {
|
||||
|
||||
const backupData = await this.application.createDecryptedBackupFile()
|
||||
|
||||
expect(backupData.items.length).to.equal(BASE_ITEM_COUNT_DECRYPTED + 2)
|
||||
expect(backupData.items.length).to.equal(BaseItemCounts.DefaultItemsNoAccounNoItemsKey + 2)
|
||||
})
|
||||
|
||||
it('decrypted backup file should not have keyParams', async function () {
|
||||
|
||||
@@ -31,9 +31,9 @@ describe('features', () => {
|
||||
expires_at: tomorrow,
|
||||
}
|
||||
|
||||
sinon.spy(application.itemManager, 'createItem')
|
||||
sinon.spy(application.itemManager, 'changeComponent')
|
||||
sinon.spy(application.itemManager, 'setItemsToBeDeleted')
|
||||
sinon.spy(application.mutator, 'createItem')
|
||||
sinon.spy(application.mutator, 'changeComponent')
|
||||
sinon.spy(application.mutator, 'setItemsToBeDeleted')
|
||||
|
||||
getUserFeatures = sinon.stub(application.apiService, 'getUserFeatures').callsFake(() => {
|
||||
return Promise.resolve({
|
||||
@@ -82,7 +82,7 @@ describe('features', () => {
|
||||
|
||||
it('should fetch user features and create items for features with content type', async () => {
|
||||
expect(application.apiService.getUserFeatures.callCount).to.equal(1)
|
||||
expect(application.itemManager.createItem.callCount).to.equal(2)
|
||||
expect(application.mutator.createItem.callCount).to.equal(2)
|
||||
|
||||
const themeItems = application.items.getItems(ContentType.Theme)
|
||||
const systemThemeCount = 1
|
||||
@@ -117,7 +117,7 @@ describe('features', () => {
|
||||
// Wipe roles from initial sync
|
||||
await application.featuresService.setOnlineRoles([])
|
||||
// Create pre-existing item for theme without all the info
|
||||
await application.itemManager.createItem(
|
||||
await application.mutator.createItem(
|
||||
ContentType.Theme,
|
||||
FillItemContent({
|
||||
package_info: {
|
||||
@@ -129,7 +129,7 @@ describe('features', () => {
|
||||
await application.sync.sync()
|
||||
// Timeout since we don't await for features update
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||
expect(application.itemManager.changeComponent.callCount).to.equal(1)
|
||||
expect(application.mutator.changeComponent.callCount).to.equal(1)
|
||||
const themeItems = application.items.getItems(ContentType.Theme)
|
||||
expect(themeItems).to.have.lengthOf(1)
|
||||
expect(themeItems[0].content).to.containSubset(
|
||||
@@ -172,7 +172,7 @@ describe('features', () => {
|
||||
|
||||
// Timeout since we don't await for features update
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||
expect(application.itemManager.setItemsToBeDeleted.calledWith([sinon.match({ uuid: themeItem.uuid })])).to.equal(
|
||||
expect(application.mutator.setItemsToBeDeleted.calledWith([sinon.match({ uuid: themeItem.uuid })])).to.equal(
|
||||
true,
|
||||
)
|
||||
|
||||
@@ -202,7 +202,7 @@ describe('features', () => {
|
||||
sinon.stub(application.featuresService, 'migrateFeatureRepoToUserSetting').callsFake(resolve)
|
||||
})
|
||||
|
||||
await application.itemManager.createItem(
|
||||
await application.mutator.createItem(
|
||||
ContentType.ExtensionRepo,
|
||||
FillItemContent({
|
||||
url: `https://extensions.standardnotes.org/${extensionKey}`,
|
||||
@@ -224,7 +224,7 @@ describe('features', () => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
.callsFake(() => {})
|
||||
const extensionKey = UuidGenerator.GenerateUuid().split('-').join('')
|
||||
await application.itemManager.createItem(
|
||||
await application.mutator.createItem(
|
||||
ContentType.ExtensionRepo,
|
||||
FillItemContent({
|
||||
url: `https://extensions.standardnotes.org/${extensionKey}`,
|
||||
@@ -255,7 +255,7 @@ describe('features', () => {
|
||||
return false
|
||||
})
|
||||
const extensionKey = UuidGenerator.GenerateUuid().split('-').join('')
|
||||
await application.itemManager.createItem(
|
||||
await application.mutator.createItem(
|
||||
ContentType.ExtensionRepo,
|
||||
FillItemContent({
|
||||
url: `https://extensions.standardnotes.org/${extensionKey}`,
|
||||
@@ -290,7 +290,7 @@ describe('features', () => {
|
||||
}
|
||||
})
|
||||
})
|
||||
await application.itemManager.createItem(
|
||||
await application.mutator.createItem(
|
||||
ContentType.ExtensionRepo,
|
||||
FillItemContent({
|
||||
url: `https://extensions.standardnotes.org/${extensionKey}`,
|
||||
@@ -304,7 +304,7 @@ describe('features', () => {
|
||||
it('previous extension repo should be migrated to offline feature repo', async () => {
|
||||
application = await Factory.signOutApplicationAndReturnNew(application)
|
||||
const extensionKey = UuidGenerator.GenerateUuid().split('-').join('')
|
||||
await application.itemManager.createItem(
|
||||
await application.mutator.createItem(
|
||||
ContentType.ExtensionRepo,
|
||||
FillItemContent({
|
||||
url: `https://extensions.standardnotes.org/${extensionKey}`,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import * as Factory from './lib/factory.js'
|
||||
import * as Events from './lib/Events.js'
|
||||
import * as Utils from './lib/Utils.js'
|
||||
import * as Files from './lib/Files.js'
|
||||
|
||||
@@ -38,22 +39,7 @@ describe('files', function () {
|
||||
})
|
||||
|
||||
if (subscription) {
|
||||
await Factory.publishMockedEvent('SUBSCRIPTION_PURCHASED', {
|
||||
userEmail: context.email,
|
||||
subscriptionId: subscriptionId++,
|
||||
subscriptionName: 'PRO_PLAN',
|
||||
subscriptionExpiresAt: (new Date().getTime() + 3_600_000) * 1_000,
|
||||
timestamp: Date.now(),
|
||||
offline: false,
|
||||
discountCode: null,
|
||||
limitedDiscountPurchased: false,
|
||||
newSubscriber: true,
|
||||
totalActiveSubscriptionsCount: 1,
|
||||
userRegisteredAt: 1,
|
||||
billingFrequency: 12,
|
||||
payAmount: 59.00
|
||||
})
|
||||
await Factory.sleep(2)
|
||||
await context.publicMockSubscriptionPurchaseEvent()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,7 +52,7 @@ describe('files', function () {
|
||||
await setup({ fakeCrypto: true, subscription: true })
|
||||
|
||||
const remoteIdentifier = Utils.generateUuid()
|
||||
const token = await application.apiService.createFileValetToken(remoteIdentifier, 'write')
|
||||
const token = await application.apiService.createUserFileValetToken(remoteIdentifier, 'write')
|
||||
|
||||
expect(token.length).to.be.above(0)
|
||||
})
|
||||
@@ -75,15 +61,15 @@ describe('files', function () {
|
||||
await setup({ fakeCrypto: true, subscription: false })
|
||||
|
||||
const remoteIdentifier = Utils.generateUuid()
|
||||
const tokenOrError = await application.apiService.createFileValetToken(remoteIdentifier, 'write')
|
||||
const tokenOrError = await application.apiService.createUserFileValetToken(remoteIdentifier, 'write')
|
||||
|
||||
expect(tokenOrError.tag).to.equal('no-subscription')
|
||||
expect(isClientDisplayableError(tokenOrError)).to.equal(true)
|
||||
})
|
||||
|
||||
it('should not create valet token from server when user has an expired subscription - @paidfeature', async function () {
|
||||
await setup({ fakeCrypto: true, subscription: false })
|
||||
|
||||
await Factory.publishMockedEvent('SUBSCRIPTION_PURCHASED', {
|
||||
await Events.publishMockedEvent('SUBSCRIPTION_PURCHASED', {
|
||||
userEmail: context.email,
|
||||
subscriptionId: subscriptionId++,
|
||||
subscriptionName: 'PLUS_PLAN',
|
||||
@@ -96,27 +82,27 @@ describe('files', function () {
|
||||
totalActiveSubscriptionsCount: 1,
|
||||
userRegisteredAt: 1,
|
||||
billingFrequency: 12,
|
||||
payAmount: 59.00
|
||||
payAmount: 59.0,
|
||||
})
|
||||
|
||||
await Factory.sleep(2)
|
||||
|
||||
const remoteIdentifier = Utils.generateUuid()
|
||||
const tokenOrError = await application.apiService.createFileValetToken(remoteIdentifier, 'write')
|
||||
const tokenOrError = await application.apiService.createUserFileValetToken(remoteIdentifier, 'write')
|
||||
|
||||
expect(tokenOrError.tag).to.equal('expired-subscription')
|
||||
expect(isClientDisplayableError(tokenOrError)).to.equal(true)
|
||||
})
|
||||
|
||||
it('creating two upload sessions successively should succeed - @paidfeature', async function () {
|
||||
await setup({ fakeCrypto: true, subscription: true })
|
||||
|
||||
const firstToken = await application.apiService.createFileValetToken(Utils.generateUuid(), 'write')
|
||||
const firstSession = await application.apiService.startUploadSession(firstToken)
|
||||
const firstToken = await application.apiService.createUserFileValetToken(Utils.generateUuid(), 'write')
|
||||
const firstSession = await application.apiService.startUploadSession(firstToken, 'user')
|
||||
|
||||
expect(firstSession.uploadId).to.be.ok
|
||||
|
||||
const secondToken = await application.apiService.createFileValetToken(Utils.generateUuid(), 'write')
|
||||
const secondSession = await application.apiService.startUploadSession(secondToken)
|
||||
const secondToken = await application.apiService.createUserFileValetToken(Utils.generateUuid(), 'write')
|
||||
const secondSession = await application.apiService.startUploadSession(secondToken, 'user')
|
||||
|
||||
expect(secondSession.uploadId).to.be.ok
|
||||
})
|
||||
@@ -129,7 +115,7 @@ describe('files', function () {
|
||||
|
||||
const file = await Files.uploadFile(fileService, buffer, 'my-file', 'md', 1000)
|
||||
|
||||
const downloadedBytes = await Files.downloadFile(fileService, itemManager, file.remoteIdentifier)
|
||||
const downloadedBytes = await Files.downloadFile(fileService, file)
|
||||
|
||||
expect(downloadedBytes).to.eql(buffer)
|
||||
})
|
||||
@@ -142,7 +128,7 @@ describe('files', function () {
|
||||
|
||||
const file = await Files.uploadFile(fileService, buffer, 'my-file', 'md', 100000)
|
||||
|
||||
const downloadedBytes = await Files.downloadFile(fileService, itemManager, file.remoteIdentifier)
|
||||
const downloadedBytes = await Files.downloadFile(fileService, file)
|
||||
|
||||
expect(downloadedBytes).to.eql(buffer)
|
||||
})
|
||||
|
||||
@@ -35,7 +35,7 @@ describe('history manager', () => {
|
||||
})
|
||||
|
||||
function setTextAndSync(application, item, text) {
|
||||
return application.mutator.changeAndSaveItem(
|
||||
return application.changeAndSaveItem(
|
||||
item,
|
||||
(mutator) => {
|
||||
mutator.text = text
|
||||
@@ -59,7 +59,7 @@ describe('history manager', () => {
|
||||
expect(this.historyManager.sessionHistoryForItem(item).length).to.equal(0)
|
||||
|
||||
/** Sync with different contents, should create new entry */
|
||||
await this.application.mutator.changeAndSaveItem(
|
||||
await this.application.changeAndSaveItem(
|
||||
item,
|
||||
(mutator) => {
|
||||
mutator.title = Math.random()
|
||||
@@ -79,7 +79,7 @@ describe('history manager', () => {
|
||||
const context = await Factory.createAppContext({ identifier })
|
||||
await context.launch()
|
||||
expect(context.application.historyManager.sessionHistoryForItem(item).length).to.equal(0)
|
||||
await context.application.mutator.changeAndSaveItem(
|
||||
await context.application.changeAndSaveItem(
|
||||
item,
|
||||
(mutator) => {
|
||||
mutator.title = Math.random()
|
||||
@@ -97,13 +97,13 @@ describe('history manager', () => {
|
||||
it('creating new item and making 1 change should create 0 revisions', async function () {
|
||||
const context = await Factory.createAppContext()
|
||||
await context.launch()
|
||||
const item = await context.application.mutator.createTemplateItem(ContentType.Note, {
|
||||
const item = await context.application.items.createTemplateItem(ContentType.Note, {
|
||||
references: [],
|
||||
})
|
||||
await context.application.mutator.insertItem(item)
|
||||
expect(context.application.historyManager.sessionHistoryForItem(item).length).to.equal(0)
|
||||
|
||||
await context.application.mutator.changeAndSaveItem(
|
||||
await context.application.changeAndSaveItem(
|
||||
item,
|
||||
(mutator) => {
|
||||
mutator.title = Math.random()
|
||||
@@ -172,8 +172,8 @@ describe('history manager', () => {
|
||||
text: Factory.randomString(100),
|
||||
}),
|
||||
)
|
||||
let item = await this.application.itemManager.emitItemFromPayload(payload, PayloadEmitSource.LocalChanged)
|
||||
await this.application.itemManager.setItemDirty(item)
|
||||
let item = await this.application.mutator.emitItemFromPayload(payload, PayloadEmitSource.LocalChanged)
|
||||
await this.application.mutator.setItemDirty(item)
|
||||
await this.application.syncService.sync(syncOptions)
|
||||
/** It should keep the first and last by default */
|
||||
item = await setTextAndSync(this.application, item, item.content.text)
|
||||
@@ -202,9 +202,9 @@ describe('history manager', () => {
|
||||
}),
|
||||
)
|
||||
|
||||
let item = await this.application.itemManager.emitItemFromPayload(payload, PayloadEmitSource.LocalChanged)
|
||||
let item = await this.application.mutator.emitItemFromPayload(payload, PayloadEmitSource.LocalChanged)
|
||||
|
||||
await this.application.itemManager.setItemDirty(item)
|
||||
await this.application.mutator.setItemDirty(item)
|
||||
await this.application.syncService.sync(syncOptions)
|
||||
|
||||
item = await setTextAndSync(this.application, item, item.content.text + Factory.randomString(1))
|
||||
@@ -241,9 +241,9 @@ describe('history manager', () => {
|
||||
|
||||
it('unsynced entries should use payload created_at for preview titles', async function () {
|
||||
const payload = Factory.createNotePayload()
|
||||
await this.application.itemManager.emitItemFromPayload(payload, PayloadEmitSource.LocalChanged)
|
||||
await this.application.mutator.emitItemFromPayload(payload, PayloadEmitSource.LocalChanged)
|
||||
const item = this.application.items.findItem(payload.uuid)
|
||||
await this.application.mutator.changeAndSaveItem(
|
||||
await this.application.changeAndSaveItem(
|
||||
item,
|
||||
(mutator) => {
|
||||
mutator.title = Math.random()
|
||||
@@ -306,7 +306,7 @@ describe('history manager', () => {
|
||||
expect(itemHistory.length).to.equal(1)
|
||||
|
||||
/** Sync with different contents, should not create a new entry */
|
||||
await this.application.mutator.changeAndSaveItem(
|
||||
await this.application.changeAndSaveItem(
|
||||
item,
|
||||
(mutator) => {
|
||||
mutator.title = Math.random()
|
||||
@@ -327,7 +327,7 @@ describe('history manager', () => {
|
||||
await Factory.sleep(Factory.ServerRevisionFrequency)
|
||||
/** Sync with different contents, should create new entry */
|
||||
const newTitleAfterFirstChange = `The title should be: ${Math.random()}`
|
||||
await this.application.mutator.changeAndSaveItem(
|
||||
await this.application.changeAndSaveItem(
|
||||
item,
|
||||
(mutator) => {
|
||||
mutator.title = newTitleAfterFirstChange
|
||||
@@ -343,7 +343,10 @@ describe('history manager', () => {
|
||||
expect(itemHistory.length).to.equal(2)
|
||||
|
||||
const oldestEntry = lastElement(itemHistory)
|
||||
let revisionFromServerOrError = await this.application.getRevision.execute({ itemUuid: item.uuid, revisionUuid: oldestEntry.uuid })
|
||||
let revisionFromServerOrError = await this.application.getRevision.execute({
|
||||
itemUuid: item.uuid,
|
||||
revisionUuid: oldestEntry.uuid,
|
||||
})
|
||||
const revisionFromServer = revisionFromServerOrError.getValue()
|
||||
expect(revisionFromServer).to.be.ok
|
||||
|
||||
@@ -359,7 +362,7 @@ describe('history manager', () => {
|
||||
it('duplicate revisions should not have the originals uuid', async function () {
|
||||
const note = await Factory.createSyncedNote(this.application)
|
||||
await Factory.markDirtyAndSyncItem(this.application, note)
|
||||
const dupe = await this.application.itemManager.duplicateItem(note, true)
|
||||
const dupe = await this.application.mutator.duplicateItem(note, true)
|
||||
await Factory.markDirtyAndSyncItem(this.application, dupe)
|
||||
|
||||
await Factory.sleep(Factory.ServerRevisionCreationDelay)
|
||||
@@ -367,7 +370,10 @@ describe('history manager', () => {
|
||||
const dupeHistoryOrError = await this.application.listRevisions.execute({ itemUuid: dupe.uuid })
|
||||
const dupeHistory = dupeHistoryOrError.getValue()
|
||||
|
||||
const dupeRevisionOrError = await this.application.getRevision.execute({ itemUuid: dupe.uuid, revisionUuid: dupeHistory[0].uuid })
|
||||
const dupeRevisionOrError = await this.application.getRevision.execute({
|
||||
itemUuid: dupe.uuid,
|
||||
revisionUuid: dupeHistory[0].uuid,
|
||||
})
|
||||
const dupeRevision = dupeRevisionOrError.getValue()
|
||||
expect(dupeRevision.payload.uuid).to.equal(dupe.uuid)
|
||||
})
|
||||
@@ -384,7 +390,7 @@ describe('history manager', () => {
|
||||
await Factory.sleep(Factory.ServerRevisionFrequency)
|
||||
await Factory.markDirtyAndSyncItem(this.application, note)
|
||||
|
||||
const dupe = await this.application.itemManager.duplicateItem(note, true)
|
||||
const dupe = await this.application.mutator.duplicateItem(note, true)
|
||||
await Factory.markDirtyAndSyncItem(this.application, dupe)
|
||||
|
||||
await Factory.sleep(Factory.ServerRevisionCreationDelay)
|
||||
@@ -405,12 +411,12 @@ describe('history manager', () => {
|
||||
await Factory.sleep(Factory.ServerRevisionFrequency)
|
||||
|
||||
const changedText = `${Math.random()}`
|
||||
await this.application.mutator.changeAndSaveItem(note, (mutator) => {
|
||||
await this.application.changeAndSaveItem(note, (mutator) => {
|
||||
mutator.title = changedText
|
||||
})
|
||||
await Factory.markDirtyAndSyncItem(this.application, note)
|
||||
|
||||
const dupe = await this.application.itemManager.duplicateItem(note, true)
|
||||
const dupe = await this.application.mutator.duplicateItem(note, true)
|
||||
await Factory.markDirtyAndSyncItem(this.application, dupe)
|
||||
|
||||
await Factory.sleep(Factory.ServerRevisionCreationDelay)
|
||||
@@ -420,7 +426,10 @@ describe('history manager', () => {
|
||||
expect(itemHistory.length).to.be.above(1)
|
||||
const newestRevision = itemHistory[0]
|
||||
|
||||
const fetchedOrError = await this.application.getRevision.execute({ itemUuid: dupe.uuid, revisionUuid: newestRevision.uuid })
|
||||
const fetchedOrError = await this.application.getRevision.execute({
|
||||
itemUuid: dupe.uuid,
|
||||
revisionUuid: newestRevision.uuid,
|
||||
})
|
||||
const fetched = fetchedOrError.getValue()
|
||||
expect(fetched.payload.errorDecrypting).to.not.be.ok
|
||||
expect(fetched.payload.content.title).to.equal(changedText)
|
||||
|
||||
@@ -1,167 +1,120 @@
|
||||
/* eslint-disable no-unused-expressions */
|
||||
/* eslint-disable no-undef */
|
||||
import * as Factory from './lib/factory.js'
|
||||
import { BaseItemCounts } from './lib/BaseItemCounts.js'
|
||||
|
||||
chai.use(chaiAsPromised)
|
||||
const expect = chai.expect
|
||||
|
||||
describe('item manager', function () {
|
||||
let context
|
||||
let application
|
||||
|
||||
afterEach(async function () {
|
||||
await context.deinit()
|
||||
localStorage.clear()
|
||||
})
|
||||
|
||||
beforeEach(async function () {
|
||||
this.payloadManager = new PayloadManager()
|
||||
this.itemManager = new ItemManager(this.payloadManager)
|
||||
this.createNote = async () => {
|
||||
return this.itemManager.createItem(ContentType.Note, {
|
||||
title: 'hello',
|
||||
text: 'world',
|
||||
})
|
||||
}
|
||||
localStorage.clear()
|
||||
|
||||
this.createTag = async (notes = []) => {
|
||||
const references = notes.map((note) => {
|
||||
return {
|
||||
uuid: note.uuid,
|
||||
content_type: note.content_type,
|
||||
}
|
||||
})
|
||||
return this.itemManager.createItem(ContentType.Tag, {
|
||||
title: 'thoughts',
|
||||
references: references,
|
||||
})
|
||||
}
|
||||
context = await Factory.createAppContextWithFakeCrypto()
|
||||
application = context.application
|
||||
|
||||
await context.launch()
|
||||
})
|
||||
|
||||
it('create item', async function () {
|
||||
const item = await this.createNote()
|
||||
const createNote = async () => {
|
||||
return application.mutator.createItem(ContentType.Note, {
|
||||
title: 'hello',
|
||||
text: 'world',
|
||||
})
|
||||
}
|
||||
|
||||
expect(item).to.be.ok
|
||||
expect(item.title).to.equal('hello')
|
||||
})
|
||||
|
||||
it('emitting item through payload and marking dirty should have userModifiedDate', async function () {
|
||||
const payload = Factory.createNotePayload()
|
||||
const item = await this.itemManager.emitItemFromPayload(payload, PayloadEmitSource.LocalChanged)
|
||||
const result = await this.itemManager.setItemDirty(item)
|
||||
const appData = result.payload.content.appData
|
||||
expect(appData[DecryptedItem.DefaultAppDomain()][AppDataField.UserModifiedDate]).to.be.ok
|
||||
})
|
||||
const createTag = async (notes = []) => {
|
||||
const references = notes.map((note) => {
|
||||
return {
|
||||
uuid: note.uuid,
|
||||
content_type: note.content_type,
|
||||
}
|
||||
})
|
||||
return application.mutator.createItem(ContentType.Tag, {
|
||||
title: 'thoughts',
|
||||
references: references,
|
||||
})
|
||||
}
|
||||
|
||||
it('find items with valid uuid', async function () {
|
||||
const item = await this.createNote()
|
||||
const item = await createNote()
|
||||
|
||||
const results = await this.itemManager.findItems([item.uuid])
|
||||
const results = await application.items.findItems([item.uuid])
|
||||
expect(results.length).to.equal(1)
|
||||
expect(results[0]).to.equal(item)
|
||||
})
|
||||
|
||||
it('find items with invalid uuid no blanks', async function () {
|
||||
const results = await this.itemManager.findItems([Factory.generateUuidish()])
|
||||
const results = await application.items.findItems([Factory.generateUuidish()])
|
||||
expect(results.length).to.equal(0)
|
||||
})
|
||||
|
||||
it('find items with invalid uuid include blanks', async function () {
|
||||
const includeBlanks = true
|
||||
const results = await this.itemManager.findItemsIncludingBlanks([Factory.generateUuidish()])
|
||||
const results = await application.items.findItemsIncludingBlanks([Factory.generateUuidish()])
|
||||
expect(results.length).to.equal(1)
|
||||
expect(results[0]).to.not.be.ok
|
||||
})
|
||||
|
||||
it('item state', async function () {
|
||||
await this.createNote()
|
||||
await createNote()
|
||||
|
||||
expect(this.itemManager.items.length).to.equal(1)
|
||||
expect(this.itemManager.getDisplayableNotes().length).to.equal(1)
|
||||
expect(application.items.items.length).to.equal(1 + BaseItemCounts.DefaultItems)
|
||||
expect(application.items.getDisplayableNotes().length).to.equal(1)
|
||||
})
|
||||
|
||||
it('find item', async function () {
|
||||
const item = await this.createNote()
|
||||
const item = await createNote()
|
||||
|
||||
const foundItem = this.itemManager.findItem(item.uuid)
|
||||
const foundItem = application.items.findItem(item.uuid)
|
||||
expect(foundItem).to.be.ok
|
||||
})
|
||||
|
||||
it('reference map', async function () {
|
||||
const note = await this.createNote()
|
||||
const tag = await this.createTag([note])
|
||||
const note = await createNote()
|
||||
const tag = await createTag([note])
|
||||
|
||||
expect(this.itemManager.collection.referenceMap.directMap.get(tag.uuid)).to.eql([note.uuid])
|
||||
expect(application.items.collection.referenceMap.directMap.get(tag.uuid)).to.eql([note.uuid])
|
||||
})
|
||||
|
||||
it('inverse reference map', async function () {
|
||||
const note = await this.createNote()
|
||||
const tag = await this.createTag([note])
|
||||
const note = await createNote()
|
||||
const tag = await createTag([note])
|
||||
|
||||
expect(this.itemManager.collection.referenceMap.inverseMap.get(note.uuid)).to.eql([tag.uuid])
|
||||
expect(application.items.collection.referenceMap.inverseMap.get(note.uuid)).to.eql([tag.uuid])
|
||||
})
|
||||
|
||||
it('inverse reference map should not have duplicates', async function () {
|
||||
const note = await this.createNote()
|
||||
const tag = await this.createTag([note])
|
||||
await this.itemManager.changeItem(tag)
|
||||
const note = await createNote()
|
||||
const tag = await createTag([note])
|
||||
await application.mutator.changeItem(tag)
|
||||
|
||||
expect(this.itemManager.collection.referenceMap.inverseMap.get(note.uuid)).to.eql([tag.uuid])
|
||||
})
|
||||
|
||||
it('deleting from reference map', async function () {
|
||||
const note = await this.createNote()
|
||||
const tag = await this.createTag([note])
|
||||
await this.itemManager.setItemToBeDeleted(note)
|
||||
|
||||
expect(this.itemManager.collection.referenceMap.directMap.get(tag.uuid)).to.eql([])
|
||||
expect(this.itemManager.collection.referenceMap.inverseMap.get(note.uuid).length).to.equal(0)
|
||||
})
|
||||
|
||||
it('deleting referenced item should update referencing item references', async function () {
|
||||
const note = await this.createNote()
|
||||
let tag = await this.createTag([note])
|
||||
await this.itemManager.setItemToBeDeleted(note)
|
||||
|
||||
tag = this.itemManager.findItem(tag.uuid)
|
||||
expect(tag.content.references.length).to.equal(0)
|
||||
})
|
||||
|
||||
it('removing relationship should update reference map', async function () {
|
||||
const note = await this.createNote()
|
||||
const tag = await this.createTag([note])
|
||||
await this.itemManager.changeItem(tag, (mutator) => {
|
||||
mutator.removeItemAsRelationship(note)
|
||||
})
|
||||
|
||||
expect(this.itemManager.collection.referenceMap.directMap.get(tag.uuid)).to.eql([])
|
||||
expect(this.itemManager.collection.referenceMap.inverseMap.get(note.uuid)).to.eql([])
|
||||
})
|
||||
|
||||
it('emitting discardable payload should remove it from our collection', async function () {
|
||||
const note = await this.createNote()
|
||||
|
||||
const payload = new DeletedPayload({
|
||||
...note.payload.ejected(),
|
||||
content: undefined,
|
||||
deleted: true,
|
||||
dirty: false,
|
||||
})
|
||||
|
||||
expect(payload.discardable).to.equal(true)
|
||||
|
||||
await this.itemManager.emitItemFromPayload(payload)
|
||||
|
||||
expect(this.itemManager.findItem(note.uuid)).to.not.be.ok
|
||||
expect(application.items.collection.referenceMap.inverseMap.get(note.uuid)).to.eql([tag.uuid])
|
||||
})
|
||||
|
||||
it('items that reference item', async function () {
|
||||
const note = await this.createNote()
|
||||
const tag = await this.createTag([note])
|
||||
const note = await createNote()
|
||||
const tag = await createTag([note])
|
||||
|
||||
const itemsThatReference = this.itemManager.itemsReferencingItem(note)
|
||||
const itemsThatReference = application.items.itemsReferencingItem(note)
|
||||
expect(itemsThatReference.length).to.equal(1)
|
||||
expect(itemsThatReference[0]).to.equal(tag)
|
||||
})
|
||||
|
||||
it('observer', async function () {
|
||||
const observed = []
|
||||
this.itemManager.addObserver(ContentType.Any, ({ changed, inserted, removed, source, sourceKey }) => {
|
||||
application.items.addObserver(ContentType.Any, ({ changed, inserted, removed, source, sourceKey }) => {
|
||||
observed.push({ changed, inserted, removed, source, sourceKey })
|
||||
})
|
||||
const note = await this.createNote()
|
||||
const tag = await this.createTag([note])
|
||||
const note = await createNote()
|
||||
const tag = await createTag([note])
|
||||
expect(observed.length).to.equal(2)
|
||||
|
||||
const firstObserved = observed[0]
|
||||
@@ -171,59 +124,23 @@ describe('item manager', function () {
|
||||
expect(secondObserved.inserted).to.eql([tag])
|
||||
})
|
||||
|
||||
it('change existing item', async function () {
|
||||
const note = await this.createNote()
|
||||
const newTitle = String(Math.random())
|
||||
await this.itemManager.changeItem(note, (mutator) => {
|
||||
mutator.title = newTitle
|
||||
})
|
||||
|
||||
const latestVersion = this.itemManager.findItem(note.uuid)
|
||||
expect(latestVersion.title).to.equal(newTitle)
|
||||
})
|
||||
|
||||
it('change non-existant item through uuid should fail', async function () {
|
||||
const note = await this.itemManager.createTemplateItem(ContentType.Note, {
|
||||
title: 'hello',
|
||||
text: 'world',
|
||||
})
|
||||
|
||||
const changeFn = async () => {
|
||||
const newTitle = String(Math.random())
|
||||
return this.itemManager.changeItem(note, (mutator) => {
|
||||
mutator.title = newTitle
|
||||
})
|
||||
}
|
||||
await Factory.expectThrowsAsync(() => changeFn(), 'Attempting to change non-existant item')
|
||||
})
|
||||
|
||||
it('set items dirty', async function () {
|
||||
const note = await this.createNote()
|
||||
await this.itemManager.setItemDirty(note)
|
||||
|
||||
const dirtyItems = this.itemManager.getDirtyItems()
|
||||
expect(dirtyItems.length).to.equal(1)
|
||||
expect(dirtyItems[0].uuid).to.equal(note.uuid)
|
||||
expect(dirtyItems[0].dirty).to.equal(true)
|
||||
})
|
||||
|
||||
it('dirty items should not include errored items', async function () {
|
||||
const note = await this.itemManager.setItemDirty(await this.createNote())
|
||||
const note = await application.mutator.setItemDirty(await createNote())
|
||||
const errorred = new EncryptedPayload({
|
||||
...note.payload,
|
||||
content: '004:...',
|
||||
errorDecrypting: true,
|
||||
})
|
||||
|
||||
await this.itemManager.emitItemsFromPayloads([errorred], PayloadEmitSource.LocalChanged)
|
||||
await application.mutator.emitItemsFromPayloads([errorred], PayloadEmitSource.LocalChanged)
|
||||
|
||||
const dirtyItems = this.itemManager.getDirtyItems()
|
||||
const dirtyItems = application.items.getDirtyItems()
|
||||
|
||||
expect(dirtyItems.length).to.equal(0)
|
||||
})
|
||||
|
||||
it('dirty items should include errored items if they are being deleted', async function () {
|
||||
const note = await this.itemManager.setItemDirty(await this.createNote())
|
||||
const note = await application.mutator.setItemDirty(await createNote())
|
||||
const errorred = new DeletedPayload({
|
||||
...note.payload,
|
||||
content: undefined,
|
||||
@@ -231,181 +148,63 @@ describe('item manager', function () {
|
||||
deleted: true,
|
||||
})
|
||||
|
||||
await this.itemManager.emitItemsFromPayloads([errorred], PayloadEmitSource.LocalChanged)
|
||||
await application.mutator.emitItemsFromPayloads([errorred], PayloadEmitSource.LocalChanged)
|
||||
|
||||
const dirtyItems = this.itemManager.getDirtyItems()
|
||||
const dirtyItems = application.items.getDirtyItems()
|
||||
|
||||
expect(dirtyItems.length).to.equal(1)
|
||||
})
|
||||
|
||||
describe('duplicateItem', async function () {
|
||||
const sandbox = sinon.createSandbox()
|
||||
|
||||
beforeEach(async function () {
|
||||
this.emitPayloads = sandbox.spy(this.itemManager.payloadManager, 'emitPayloads')
|
||||
})
|
||||
|
||||
afterEach(async function () {
|
||||
sandbox.restore()
|
||||
})
|
||||
|
||||
it('should duplicate the item and set the duplicate_of property', async function () {
|
||||
const note = await this.createNote()
|
||||
await this.itemManager.duplicateItem(note)
|
||||
sinon.assert.calledTwice(this.emitPayloads)
|
||||
|
||||
const originalNote = this.itemManager.getDisplayableNotes()[0]
|
||||
const duplicatedNote = this.itemManager.getDisplayableNotes()[1]
|
||||
|
||||
expect(this.itemManager.items.length).to.equal(2)
|
||||
expect(this.itemManager.getDisplayableNotes().length).to.equal(2)
|
||||
expect(originalNote.uuid).to.not.equal(duplicatedNote.uuid)
|
||||
expect(originalNote.uuid).to.equal(duplicatedNote.duplicateOf)
|
||||
expect(originalNote.uuid).to.equal(duplicatedNote.payload.duplicate_of)
|
||||
expect(duplicatedNote.conflictOf).to.be.undefined
|
||||
expect(duplicatedNote.payload.content.conflict_of).to.be.undefined
|
||||
})
|
||||
|
||||
it('should duplicate the item and set the duplicate_of and conflict_of properties', async function () {
|
||||
const note = await this.createNote()
|
||||
await this.itemManager.duplicateItem(note, true)
|
||||
sinon.assert.calledTwice(this.emitPayloads)
|
||||
|
||||
const originalNote = this.itemManager.getDisplayableNotes()[0]
|
||||
const duplicatedNote = this.itemManager.getDisplayableNotes()[1]
|
||||
|
||||
expect(this.itemManager.items.length).to.equal(2)
|
||||
expect(this.itemManager.getDisplayableNotes().length).to.equal(2)
|
||||
expect(originalNote.uuid).to.not.equal(duplicatedNote.uuid)
|
||||
expect(originalNote.uuid).to.equal(duplicatedNote.duplicateOf)
|
||||
expect(originalNote.uuid).to.equal(duplicatedNote.payload.duplicate_of)
|
||||
expect(originalNote.uuid).to.equal(duplicatedNote.conflictOf)
|
||||
expect(originalNote.uuid).to.equal(duplicatedNote.payload.content.conflict_of)
|
||||
})
|
||||
|
||||
it('duplicate item with relationships', async function () {
|
||||
const note = await this.createNote()
|
||||
const tag = await this.createTag([note])
|
||||
const duplicate = await this.itemManager.duplicateItem(tag)
|
||||
|
||||
expect(duplicate.content.references).to.have.length(1)
|
||||
expect(this.itemManager.items).to.have.length(3)
|
||||
expect(this.itemManager.getDisplayableTags()).to.have.length(2)
|
||||
})
|
||||
|
||||
it('adds duplicated item as a relationship to items referencing it', async function () {
|
||||
const note = await this.createNote()
|
||||
let tag = await this.createTag([note])
|
||||
const duplicateNote = await this.itemManager.duplicateItem(note)
|
||||
expect(tag.content.references).to.have.length(1)
|
||||
|
||||
tag = this.itemManager.findItem(tag.uuid)
|
||||
const references = tag.content.references.map((ref) => ref.uuid)
|
||||
expect(references).to.have.length(2)
|
||||
expect(references).to.include(note.uuid, duplicateNote.uuid)
|
||||
})
|
||||
|
||||
it('duplicates item with additional content', async function () {
|
||||
const note = await this.itemManager.createItem(ContentType.Note, {
|
||||
title: 'hello',
|
||||
text: 'world',
|
||||
})
|
||||
const duplicateNote = await this.itemManager.duplicateItem(note, false, {
|
||||
title: 'hello (copy)',
|
||||
})
|
||||
|
||||
expect(duplicateNote.title).to.equal('hello (copy)')
|
||||
expect(duplicateNote.text).to.equal('world')
|
||||
})
|
||||
})
|
||||
|
||||
it('set item deleted', async function () {
|
||||
const note = await this.createNote()
|
||||
await this.itemManager.setItemToBeDeleted(note)
|
||||
|
||||
/** Items should never be mutated directly */
|
||||
expect(note.deleted).to.not.be.ok
|
||||
|
||||
const latestVersion = this.payloadManager.findOne(note.uuid)
|
||||
expect(latestVersion.deleted).to.equal(true)
|
||||
expect(latestVersion.dirty).to.equal(true)
|
||||
expect(latestVersion.content).to.not.be.ok
|
||||
|
||||
/** Deleted items do not show up in item manager's public interface */
|
||||
expect(this.itemManager.items.length).to.equal(0)
|
||||
expect(this.itemManager.getDisplayableNotes().length).to.equal(0)
|
||||
})
|
||||
|
||||
it('system smart views', async function () {
|
||||
expect(this.itemManager.systemSmartViews.length).to.be.above(0)
|
||||
expect(application.items.systemSmartViews.length).to.be.above(0)
|
||||
})
|
||||
|
||||
it('find tag by title', async function () {
|
||||
const tag = await this.createTag()
|
||||
const tag = await createTag()
|
||||
|
||||
expect(this.itemManager.findTagByTitle(tag.title)).to.be.ok
|
||||
expect(application.items.findTagByTitle(tag.title)).to.be.ok
|
||||
})
|
||||
|
||||
it('find tag by title should be case insensitive', async function () {
|
||||
const tag = await this.createTag()
|
||||
const tag = await createTag()
|
||||
|
||||
expect(this.itemManager.findTagByTitle(tag.title.toUpperCase())).to.be.ok
|
||||
expect(application.items.findTagByTitle(tag.title.toUpperCase())).to.be.ok
|
||||
})
|
||||
|
||||
it('find or create tag by title', async function () {
|
||||
const title = 'foo'
|
||||
|
||||
expect(await this.itemManager.findOrCreateTagByTitle(title)).to.be.ok
|
||||
expect(await application.mutator.findOrCreateTagByTitle({ title: title })).to.be.ok
|
||||
})
|
||||
|
||||
it('note count', async function () {
|
||||
await this.createNote()
|
||||
expect(this.itemManager.noteCount).to.equal(1)
|
||||
})
|
||||
|
||||
it('trash', async function () {
|
||||
const note = await this.createNote()
|
||||
const versionTwo = await this.itemManager.changeItem(note, (mutator) => {
|
||||
mutator.trashed = true
|
||||
})
|
||||
|
||||
expect(this.itemManager.trashSmartView).to.be.ok
|
||||
expect(versionTwo.trashed).to.equal(true)
|
||||
expect(versionTwo.dirty).to.equal(true)
|
||||
expect(versionTwo.content).to.be.ok
|
||||
|
||||
expect(this.itemManager.items.length).to.equal(1)
|
||||
expect(this.itemManager.trashedItems.length).to.equal(1)
|
||||
|
||||
await this.itemManager.emptyTrash()
|
||||
const versionThree = this.payloadManager.findOne(note.uuid)
|
||||
expect(versionThree.deleted).to.equal(true)
|
||||
expect(this.itemManager.trashedItems.length).to.equal(0)
|
||||
await createNote()
|
||||
expect(application.items.noteCount).to.equal(1)
|
||||
})
|
||||
|
||||
it('remove all items from memory', async function () {
|
||||
const observed = []
|
||||
this.itemManager.addObserver(ContentType.Any, ({ changed, inserted, removed, ignored }) => {
|
||||
application.items.addObserver(ContentType.Any, ({ changed, inserted, removed, ignored }) => {
|
||||
observed.push({ changed, inserted, removed, ignored })
|
||||
})
|
||||
await this.createNote()
|
||||
await this.itemManager.removeAllItemsFromMemory()
|
||||
await createNote()
|
||||
await application.items.removeAllItemsFromMemory()
|
||||
|
||||
const deletionEvent = observed[1]
|
||||
expect(deletionEvent.removed[0].deleted).to.equal(true)
|
||||
expect(this.itemManager.items.length).to.equal(0)
|
||||
expect(application.items.items.length).to.equal(0)
|
||||
})
|
||||
|
||||
it('remove item locally', async function () {
|
||||
const observed = []
|
||||
this.itemManager.addObserver(ContentType.Any, ({ changed, inserted, removed, ignored }) => {
|
||||
application.items.addObserver(ContentType.Any, ({ changed, inserted, removed, ignored }) => {
|
||||
observed.push({ changed, inserted, removed, ignored })
|
||||
})
|
||||
const note = await this.createNote()
|
||||
await this.itemManager.removeItemLocally(note)
|
||||
const note = await createNote()
|
||||
await application.items.removeItemLocally(note)
|
||||
|
||||
expect(observed.length).to.equal(1)
|
||||
expect(this.itemManager.findItem(note.uuid)).to.not.be.ok
|
||||
expect(application.items.findItem(note.uuid)).to.not.be.ok
|
||||
})
|
||||
|
||||
it('emitting a payload from within observer should queue to end', async function () {
|
||||
@@ -421,7 +220,7 @@ describe('item manager', function () {
|
||||
const changedTitle = 'changed title'
|
||||
let didEmit = false
|
||||
let latestVersion
|
||||
this.itemManager.addObserver(ContentType.Note, ({ changed, inserted }) => {
|
||||
application.items.addObserver(ContentType.Note, ({ changed, inserted }) => {
|
||||
const all = changed.concat(inserted)
|
||||
if (!didEmit) {
|
||||
didEmit = true
|
||||
@@ -431,60 +230,60 @@ describe('item manager', function () {
|
||||
title: changedTitle,
|
||||
},
|
||||
})
|
||||
this.itemManager.emitItemFromPayload(changedPayload)
|
||||
application.mutator.emitItemFromPayload(changedPayload)
|
||||
}
|
||||
latestVersion = all[0]
|
||||
})
|
||||
await this.itemManager.emitItemFromPayload(payload)
|
||||
await application.mutator.emitItemFromPayload(payload)
|
||||
expect(latestVersion.title).to.equal(changedTitle)
|
||||
})
|
||||
|
||||
describe('searchTags', async function () {
|
||||
it('should return tag with query matching title', async function () {
|
||||
const tag = await this.itemManager.findOrCreateTagByTitle('tag')
|
||||
const tag = await application.mutator.findOrCreateTagByTitle({ title: 'tag' })
|
||||
|
||||
const results = this.itemManager.searchTags('tag')
|
||||
const results = application.items.searchTags('tag')
|
||||
expect(results).lengthOf(1)
|
||||
expect(results[0].title).to.equal(tag.title)
|
||||
})
|
||||
it('should return all tags with query partially matching title', async function () {
|
||||
const firstTag = await this.itemManager.findOrCreateTagByTitle('tag one')
|
||||
const secondTag = await this.itemManager.findOrCreateTagByTitle('tag two')
|
||||
const firstTag = await application.mutator.findOrCreateTagByTitle({ title: 'tag one' })
|
||||
const secondTag = await application.mutator.findOrCreateTagByTitle({ title: 'tag two' })
|
||||
|
||||
const results = this.itemManager.searchTags('tag')
|
||||
const results = application.items.searchTags('tag')
|
||||
expect(results).lengthOf(2)
|
||||
expect(results[0].title).to.equal(firstTag.title)
|
||||
expect(results[1].title).to.equal(secondTag.title)
|
||||
})
|
||||
it('should be case insensitive', async function () {
|
||||
const tag = await this.itemManager.findOrCreateTagByTitle('Tag')
|
||||
const tag = await application.mutator.findOrCreateTagByTitle({ title: 'Tag' })
|
||||
|
||||
const results = this.itemManager.searchTags('tag')
|
||||
const results = application.items.searchTags('tag')
|
||||
expect(results).lengthOf(1)
|
||||
expect(results[0].title).to.equal(tag.title)
|
||||
})
|
||||
it('should return tag with query matching delimiter separated component', async function () {
|
||||
const tag = await this.itemManager.findOrCreateTagByTitle('parent.child')
|
||||
const tag = await application.mutator.findOrCreateTagByTitle({ title: 'parent.child' })
|
||||
|
||||
const results = this.itemManager.searchTags('child')
|
||||
const results = application.items.searchTags('child')
|
||||
expect(results).lengthOf(1)
|
||||
expect(results[0].title).to.equal(tag.title)
|
||||
})
|
||||
it('should return tags with matching query including delimiter', async function () {
|
||||
const tag = await this.itemManager.findOrCreateTagByTitle('parent.child')
|
||||
const tag = await application.mutator.findOrCreateTagByTitle({ title: 'parent.child' })
|
||||
|
||||
const results = this.itemManager.searchTags('parent.chi')
|
||||
const results = application.items.searchTags('parent.chi')
|
||||
expect(results).lengthOf(1)
|
||||
expect(results[0].title).to.equal(tag.title)
|
||||
})
|
||||
|
||||
it('should return tags in natural order', async function () {
|
||||
const firstTag = await this.itemManager.findOrCreateTagByTitle('tag 100')
|
||||
const secondTag = await this.itemManager.findOrCreateTagByTitle('tag 2')
|
||||
const thirdTag = await this.itemManager.findOrCreateTagByTitle('tag b')
|
||||
const fourthTag = await this.itemManager.findOrCreateTagByTitle('tag a')
|
||||
const firstTag = await application.mutator.findOrCreateTagByTitle({ title: 'tag 100' })
|
||||
const secondTag = await application.mutator.findOrCreateTagByTitle({ title: 'tag 2' })
|
||||
const thirdTag = await application.mutator.findOrCreateTagByTitle({ title: 'tag b' })
|
||||
const fourthTag = await application.mutator.findOrCreateTagByTitle({ title: 'tag a' })
|
||||
|
||||
const results = this.itemManager.searchTags('tag')
|
||||
const results = application.items.searchTags('tag')
|
||||
expect(results).lengthOf(4)
|
||||
expect(results[0].title).to.equal(secondTag.title)
|
||||
expect(results[1].title).to.equal(firstTag.title)
|
||||
@@ -493,15 +292,15 @@ describe('item manager', function () {
|
||||
})
|
||||
|
||||
it('should not return tags associated with note', async function () {
|
||||
const firstTag = await this.itemManager.findOrCreateTagByTitle('tag one')
|
||||
const secondTag = await this.itemManager.findOrCreateTagByTitle('tag two')
|
||||
const firstTag = await application.mutator.findOrCreateTagByTitle({ title: 'tag one' })
|
||||
const secondTag = await application.mutator.findOrCreateTagByTitle({ title: 'tag two' })
|
||||
|
||||
const note = await this.createNote()
|
||||
await this.itemManager.changeItem(firstTag, (mutator) => {
|
||||
const note = await createNote()
|
||||
await application.mutator.changeItem(firstTag, (mutator) => {
|
||||
mutator.e2ePendingRefactor_addItemAsRelationship(note)
|
||||
})
|
||||
|
||||
const results = this.itemManager.searchTags('tag', note)
|
||||
const results = application.items.searchTags('tag', note)
|
||||
expect(results).lengthOf(1)
|
||||
expect(results[0].title).to.equal(secondTag.title)
|
||||
})
|
||||
@@ -509,68 +308,68 @@ describe('item manager', function () {
|
||||
|
||||
describe('smart views', async function () {
|
||||
it('all view should not include archived notes by default', async function () {
|
||||
const normal = await this.createNote()
|
||||
const normal = await createNote()
|
||||
|
||||
await this.itemManager.changeItem(normal, (mutator) => {
|
||||
await application.mutator.changeItem(normal, (mutator) => {
|
||||
mutator.archived = true
|
||||
})
|
||||
|
||||
this.itemManager.setPrimaryItemDisplayOptions({
|
||||
views: [this.itemManager.allNotesSmartView],
|
||||
application.items.setPrimaryItemDisplayOptions({
|
||||
views: [application.items.allNotesSmartView],
|
||||
})
|
||||
|
||||
expect(this.itemManager.getDisplayableNotes().length).to.equal(0)
|
||||
expect(application.items.getDisplayableNotes().length).to.equal(0)
|
||||
})
|
||||
|
||||
it('archived view should not include trashed notes by default', async function () {
|
||||
const normal = await this.createNote()
|
||||
const normal = await createNote()
|
||||
|
||||
await this.itemManager.changeItem(normal, (mutator) => {
|
||||
await application.mutator.changeItem(normal, (mutator) => {
|
||||
mutator.archived = true
|
||||
mutator.trashed = true
|
||||
})
|
||||
|
||||
this.itemManager.setPrimaryItemDisplayOptions({
|
||||
views: [this.itemManager.archivedSmartView],
|
||||
application.items.setPrimaryItemDisplayOptions({
|
||||
views: [application.items.archivedSmartView],
|
||||
})
|
||||
|
||||
expect(this.itemManager.getDisplayableNotes().length).to.equal(0)
|
||||
expect(application.items.getDisplayableNotes().length).to.equal(0)
|
||||
})
|
||||
|
||||
it('trashed view should include archived notes by default', async function () {
|
||||
const normal = await this.createNote()
|
||||
const normal = await createNote()
|
||||
|
||||
await this.itemManager.changeItem(normal, (mutator) => {
|
||||
await application.mutator.changeItem(normal, (mutator) => {
|
||||
mutator.archived = true
|
||||
mutator.trashed = true
|
||||
})
|
||||
|
||||
this.itemManager.setPrimaryItemDisplayOptions({
|
||||
views: [this.itemManager.trashSmartView],
|
||||
application.items.setPrimaryItemDisplayOptions({
|
||||
views: [application.items.trashSmartView],
|
||||
})
|
||||
|
||||
expect(this.itemManager.getDisplayableNotes().length).to.equal(1)
|
||||
expect(application.items.getDisplayableNotes().length).to.equal(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('getSortedTagsForNote', async function () {
|
||||
it('should return tags associated with a note in natural order', async function () {
|
||||
const tags = [
|
||||
await this.itemManager.findOrCreateTagByTitle('tag 100'),
|
||||
await this.itemManager.findOrCreateTagByTitle('tag 2'),
|
||||
await this.itemManager.findOrCreateTagByTitle('tag b'),
|
||||
await this.itemManager.findOrCreateTagByTitle('tag a'),
|
||||
await application.mutator.findOrCreateTagByTitle({ title: 'tag 100' }),
|
||||
await application.mutator.findOrCreateTagByTitle({ title: 'tag 2' }),
|
||||
await application.mutator.findOrCreateTagByTitle({ title: 'tag b' }),
|
||||
await application.mutator.findOrCreateTagByTitle({ title: 'tag a' }),
|
||||
]
|
||||
|
||||
const note = await this.createNote()
|
||||
const note = await createNote()
|
||||
|
||||
tags.map(async (tag) => {
|
||||
await this.itemManager.changeItem(tag, (mutator) => {
|
||||
await application.mutator.changeItem(tag, (mutator) => {
|
||||
mutator.e2ePendingRefactor_addItemAsRelationship(note)
|
||||
})
|
||||
})
|
||||
|
||||
const results = this.itemManager.getSortedTagsForItem(note)
|
||||
const results = application.items.getSortedTagsForItem(note)
|
||||
|
||||
expect(results).lengthOf(tags.length)
|
||||
expect(results[0].title).to.equal(tags[1].title)
|
||||
@@ -583,16 +382,16 @@ describe('item manager', function () {
|
||||
describe('getTagParentChain', function () {
|
||||
it('should return parent tags for a tag', async function () {
|
||||
const [parent, child, grandchild, _other] = await Promise.all([
|
||||
this.itemManager.findOrCreateTagByTitle('parent'),
|
||||
this.itemManager.findOrCreateTagByTitle('parent.child'),
|
||||
this.itemManager.findOrCreateTagByTitle('parent.child.grandchild'),
|
||||
this.itemManager.findOrCreateTagByTitle('some other tag'),
|
||||
application.mutator.findOrCreateTagByTitle({ title: 'parent' }),
|
||||
application.mutator.findOrCreateTagByTitle({ title: 'parent.child' }),
|
||||
application.mutator.findOrCreateTagByTitle({ title: 'parent.child.grandchild' }),
|
||||
application.mutator.findOrCreateTagByTitle({ title: 'some other tag' }),
|
||||
])
|
||||
|
||||
await this.itemManager.setTagParent(parent, child)
|
||||
await this.itemManager.setTagParent(child, grandchild)
|
||||
await application.mutator.setTagParent(parent, child)
|
||||
await application.mutator.setTagParent(child, grandchild)
|
||||
|
||||
const results = this.itemManager.getTagParentChain(grandchild)
|
||||
const results = application.items.getTagParentChain(grandchild)
|
||||
|
||||
expect(results).lengthOf(2)
|
||||
expect(results[0].uuid).to.equal(parent.uuid)
|
||||
|
||||
@@ -200,7 +200,9 @@ describe('key recovery service', function () {
|
||||
const receiveChallenge = (challenge) => {
|
||||
totalPromptCount++
|
||||
/** Give unassociated password when prompted */
|
||||
application.submitValuesForChallenge(challenge, [CreateChallengeValue(challenge.prompts[0], unassociatedPassword)])
|
||||
application.submitValuesForChallenge(challenge, [
|
||||
CreateChallengeValue(challenge.prompts[0], unassociatedPassword),
|
||||
])
|
||||
}
|
||||
await application.prepareForLaunch({ receiveChallenge })
|
||||
await application.launch(true)
|
||||
@@ -272,7 +274,9 @@ describe('key recovery service', function () {
|
||||
expect(result.error).to.not.be.ok
|
||||
expect(contextB.application.items.getAnyItems(ContentType.ItemsKey).length).to.equal(2)
|
||||
|
||||
const newItemsKey = contextB.application.items.getDisplayableItemsKeys().find((k) => k.uuid !== originalItemsKey.uuid)
|
||||
const newItemsKey = contextB.application.items
|
||||
.getDisplayableItemsKeys()
|
||||
.find((k) => k.uuid !== originalItemsKey.uuid)
|
||||
|
||||
const note = await Factory.createSyncedNote(contextB.application)
|
||||
|
||||
@@ -432,6 +436,7 @@ describe('key recovery service', function () {
|
||||
expect(decryptedKey.content.itemsKey).to.equal(correctItemsKey.content.itemsKey)
|
||||
|
||||
expect(application.syncService.isOutOfSync()).to.equal(false)
|
||||
|
||||
await context.deinit()
|
||||
})
|
||||
|
||||
@@ -457,6 +462,8 @@ describe('key recovery service', function () {
|
||||
updated_at: newUpdated,
|
||||
})
|
||||
|
||||
context.disableKeyRecovery()
|
||||
|
||||
await context.receiveServerResponse({ retrievedItems: [errored.ejected()] })
|
||||
|
||||
/** Our current items key should not be overwritten */
|
||||
@@ -567,7 +574,9 @@ describe('key recovery service', function () {
|
||||
const application = context.application
|
||||
const receiveChallenge = (challenge) => {
|
||||
/** Give unassociated password when prompted */
|
||||
application.submitValuesForChallenge(challenge, [CreateChallengeValue(challenge.prompts[0], unassociatedPassword)])
|
||||
application.submitValuesForChallenge(challenge, [
|
||||
CreateChallengeValue(challenge.prompts[0], unassociatedPassword),
|
||||
])
|
||||
}
|
||||
await application.prepareForLaunch({ receiveChallenge })
|
||||
await application.launch(true)
|
||||
@@ -667,13 +676,15 @@ describe('key recovery service', function () {
|
||||
const stored = (await appA.deviceInterface.getAllDatabaseEntries(appA.identifier)).find(
|
||||
(payload) => payload.uuid === newDefaultKey.uuid,
|
||||
)
|
||||
const storedParams = await appA.protocolService.getKeyEmbeddedKeyParams(new EncryptedPayload(stored))
|
||||
const storedParams = await appA.protocolService.getKeyEmbeddedKeyParamsFromItemsKey(new EncryptedPayload(stored))
|
||||
|
||||
const correctStored = (await appB.deviceInterface.getAllDatabaseEntries(appB.identifier)).find(
|
||||
(payload) => payload.uuid === newDefaultKey.uuid,
|
||||
)
|
||||
|
||||
const correctParams = await appB.protocolService.getKeyEmbeddedKeyParams(new EncryptedPayload(correctStored))
|
||||
const correctParams = await appB.protocolService.getKeyEmbeddedKeyParamsFromItemsKey(
|
||||
new EncryptedPayload(correctStored),
|
||||
)
|
||||
|
||||
expect(storedParams).to.eql(correctParams)
|
||||
|
||||
|
||||
@@ -141,7 +141,8 @@ describe('keys', function () {
|
||||
})
|
||||
|
||||
it('should use items key for encryption of note', async function () {
|
||||
const keyToUse = await this.application.protocolService.itemsEncryption.keyToUseForItemEncryption()
|
||||
const notePayload = Factory.createNotePayload()
|
||||
const keyToUse = await this.application.protocolService.itemsEncryption.keyToUseForItemEncryption(notePayload)
|
||||
expect(keyToUse.content_type).to.equal(ContentType.ItemsKey)
|
||||
})
|
||||
|
||||
@@ -153,7 +154,7 @@ describe('keys', function () {
|
||||
},
|
||||
})
|
||||
|
||||
const itemsKey = this.application.protocolService.itemsKeyForPayload(encryptedPayload)
|
||||
const itemsKey = this.application.protocolService.itemsKeyForEncryptedPayload(encryptedPayload)
|
||||
expect(itemsKey).to.be.ok
|
||||
})
|
||||
|
||||
@@ -166,7 +167,7 @@ describe('keys', function () {
|
||||
},
|
||||
})
|
||||
|
||||
const itemsKey = this.application.protocolService.itemsKeyForPayload(encryptedPayload)
|
||||
const itemsKey = this.application.protocolService.itemsKeyForEncryptedPayload(encryptedPayload)
|
||||
expect(itemsKey).to.be.ok
|
||||
|
||||
const decryptedPayload = await this.application.protocolService.decryptSplitSingle({
|
||||
@@ -187,7 +188,7 @@ describe('keys', function () {
|
||||
},
|
||||
})
|
||||
|
||||
const itemsKey = this.application.protocolService.itemsKeyForPayload(encryptedPayload)
|
||||
const itemsKey = this.application.protocolService.itemsKeyForEncryptedPayload(encryptedPayload)
|
||||
|
||||
await this.application.itemManager.removeItemLocally(itemsKey)
|
||||
|
||||
@@ -197,14 +198,14 @@ describe('keys', function () {
|
||||
},
|
||||
})
|
||||
|
||||
await this.application.itemManager.emitItemsFromPayloads([erroredPayload], PayloadEmitSource.LocalChanged)
|
||||
await this.application.mutator.emitItemsFromPayloads([erroredPayload], PayloadEmitSource.LocalChanged)
|
||||
|
||||
const note = this.application.itemManager.findAnyItem(notePayload.uuid)
|
||||
expect(note.errorDecrypting).to.equal(true)
|
||||
expect(note.waitingForKey).to.equal(true)
|
||||
|
||||
const keyPayload = new DecryptedPayload(itemsKey.payload.ejected())
|
||||
await this.application.itemManager.emitItemsFromPayloads([keyPayload], PayloadEmitSource.LocalChanged)
|
||||
await this.application.mutator.emitItemsFromPayloads([keyPayload], PayloadEmitSource.LocalChanged)
|
||||
|
||||
/**
|
||||
* Sleeping is required to trigger asyncronous protocolService.decryptItemsWaitingForKeys,
|
||||
@@ -238,7 +239,7 @@ describe('keys', function () {
|
||||
},
|
||||
})
|
||||
|
||||
await this.application.syncService.handleSuccessServerResponse({ payloadsSavedOrSaving: [] }, response)
|
||||
await this.application.syncService.handleSuccessServerResponse({ payloadsSavedOrSaving: [], options: {} }, response)
|
||||
|
||||
const refreshedKey = this.application.payloadManager.findOne(itemsKey.uuid)
|
||||
|
||||
@@ -273,10 +274,8 @@ describe('keys', function () {
|
||||
const rawPayloads = await this.application.diskStorageService.getAllRawPayloads()
|
||||
const itemsKeyRawPayload = rawPayloads.find((p) => p.uuid === itemsKey.uuid)
|
||||
const itemsKeyPayload = new EncryptedPayload(itemsKeyRawPayload)
|
||||
const operator = this.application.protocolService.operatorManager.operatorForVersion(ProtocolVersion.V004)
|
||||
const comps = operator.deconstructEncryptedPayloadString(itemsKeyPayload.content)
|
||||
const rawAuthenticatedData = comps.authenticatedData
|
||||
const authenticatedData = await operator.stringToAuthenticatedData(rawAuthenticatedData)
|
||||
|
||||
const authenticatedData = this.context.encryption.getEmbeddedPayloadAuthenticatedData(itemsKeyPayload)
|
||||
const rootKeyParams = await this.application.protocolService.getRootKeyParams()
|
||||
|
||||
expect(authenticatedData.kp).to.be.ok
|
||||
@@ -649,7 +648,7 @@ describe('keys', function () {
|
||||
await contextB.deinit()
|
||||
})
|
||||
|
||||
describe('changing password on 003 client while signed into 004 client should', function () {
|
||||
describe('changing password on 003 client while signed into 004 client', function () {
|
||||
/**
|
||||
* When an 004 client signs into 003 account, it creates a root key based items key.
|
||||
* Then, if the 003 client changes its account password, and the 004 client
|
||||
@@ -658,7 +657,7 @@ describe('keys', function () {
|
||||
* items sync to the 004 client, it can't decrypt them with its existing items key
|
||||
* because its based on the old root key.
|
||||
*/
|
||||
it.skip('add new items key', async function () {
|
||||
it.skip('should add new items key', async function () {
|
||||
this.timeout(Factory.TwentySecondTimeout * 3)
|
||||
let oldClient = this.application
|
||||
|
||||
@@ -718,7 +717,13 @@ describe('keys', function () {
|
||||
await Factory.safeDeinit(oldClient)
|
||||
})
|
||||
|
||||
it('add new items key from migration if pw change already happened', async function () {
|
||||
it('should add new items key from migration if pw change already happened', async function () {
|
||||
this.context.anticipateConsoleError('Shared vault network errors due to not accepting JWT-based token')
|
||||
this.context.anticipateConsoleError(
|
||||
'Cannot find items key to use for encryption',
|
||||
'No items keys being created in this test',
|
||||
)
|
||||
|
||||
/** Register an 003 account */
|
||||
await Factory.registerOldUser({
|
||||
application: this.application,
|
||||
@@ -734,7 +739,15 @@ describe('keys', function () {
|
||||
await this.application.protocolService.getRootKeyParams(),
|
||||
)
|
||||
const operator = this.application.protocolService.operatorManager.operatorForVersion(ProtocolVersion.V003)
|
||||
const newRootKey = await operator.createRootKey(this.email, this.password)
|
||||
const newRootKeyTemplate = await operator.createRootKey(this.email, this.password)
|
||||
const newRootKey = CreateNewRootKey({
|
||||
...newRootKeyTemplate.content,
|
||||
...{
|
||||
encryptionKeyPair: {},
|
||||
signingKeyPair: {},
|
||||
},
|
||||
})
|
||||
|
||||
Object.defineProperty(this.application.apiService, 'apiVersion', {
|
||||
get: function () {
|
||||
return '20190520'
|
||||
@@ -748,7 +761,7 @@ describe('keys', function () {
|
||||
currentServerPassword: currentRootKey.serverPassword,
|
||||
newRootKey,
|
||||
})
|
||||
await this.application.protocolService.reencryptItemsKeys()
|
||||
await this.application.protocolService.reencryptApplicableItemsAfterUserRootKeyChange()
|
||||
/** Note: this may result in a deadlock if features_service syncs and results in an error */
|
||||
await this.application.sync.sync({ awaitAll: true })
|
||||
|
||||
@@ -776,11 +789,16 @@ describe('keys', function () {
|
||||
* The corrective action was to do a final check in protocolService.handleDownloadFirstSyncCompletion
|
||||
* to ensure there exists an items key corresponding to the user's account version.
|
||||
*/
|
||||
const promise = this.context.awaitNextSucessfulSync()
|
||||
await this.context.sync()
|
||||
await promise
|
||||
|
||||
await this.application.itemManager.removeAllItemsFromMemory()
|
||||
expect(this.application.protocolService.getSureDefaultItemsKey()).to.not.be.ok
|
||||
|
||||
const protocol003 = new SNProtocolOperator003(new SNWebCrypto())
|
||||
const key = await protocol003.createItemsKey()
|
||||
await this.application.itemManager.emitItemFromPayload(
|
||||
await this.application.mutator.emitItemFromPayload(
|
||||
key.payload.copy({
|
||||
content: {
|
||||
...key.payload.content,
|
||||
@@ -791,17 +809,21 @@ describe('keys', function () {
|
||||
updated_at: Date.now(),
|
||||
}),
|
||||
)
|
||||
|
||||
const defaultKey = this.application.protocolService.getSureDefaultItemsKey()
|
||||
expect(defaultKey.keyVersion).to.equal(ProtocolVersion.V003)
|
||||
expect(defaultKey.uuid).to.equal(key.uuid)
|
||||
|
||||
await Factory.registerUserToApplication({ application: this.application })
|
||||
expect(await this.application.protocolService.itemsEncryption.keyToUseForItemEncryption()).to.be.ok
|
||||
|
||||
const notePayload = Factory.createNotePayload()
|
||||
expect(await this.application.protocolService.itemsEncryption.keyToUseForItemEncryption(notePayload)).to.be.ok
|
||||
})
|
||||
|
||||
it('having unsynced items keys should resync them upon download first sync completion', async function () {
|
||||
await Factory.registerUserToApplication({ application: this.application })
|
||||
const itemsKey = this.application.itemManager.getDisplayableItemsKeys()[0]
|
||||
await this.application.itemManager.emitItemFromPayload(
|
||||
await this.application.mutator.emitItemFromPayload(
|
||||
itemsKey.payload.copy({
|
||||
dirty: false,
|
||||
updated_at: new Date(0),
|
||||
|
||||
@@ -2,6 +2,7 @@ import FakeWebCrypto from './fake_web_crypto.js'
|
||||
import * as Applications from './Applications.js'
|
||||
import * as Utils from './Utils.js'
|
||||
import * as Defaults from './Defaults.js'
|
||||
import * as Events from './Events.js'
|
||||
import { createNotePayload } from './Items.js'
|
||||
|
||||
UuidGenerator.SetGenerator(new FakeWebCrypto().generateUUID)
|
||||
@@ -11,6 +12,8 @@ const MaximumSyncOptions = {
|
||||
awaitAll: true,
|
||||
}
|
||||
|
||||
let GlobalSubscriptionIdCounter = 1001
|
||||
|
||||
export class AppContext {
|
||||
constructor({ identifier, crypto, email, password, passcode, host } = {}) {
|
||||
this.identifier = identifier || `${Math.random()}`
|
||||
@@ -46,6 +49,62 @@ export class AppContext {
|
||||
)
|
||||
}
|
||||
|
||||
get vaults() {
|
||||
return this.application.vaultService
|
||||
}
|
||||
|
||||
get sessions() {
|
||||
return this.application.sessions
|
||||
}
|
||||
|
||||
get items() {
|
||||
return this.application.items
|
||||
}
|
||||
|
||||
get mutator() {
|
||||
return this.application.mutator
|
||||
}
|
||||
|
||||
get payloads() {
|
||||
return this.application.payloadManager
|
||||
}
|
||||
|
||||
get encryption() {
|
||||
return this.application.protocolService
|
||||
}
|
||||
|
||||
get contacts() {
|
||||
return this.application.contactService
|
||||
}
|
||||
|
||||
get sharedVaults() {
|
||||
return this.application.sharedVaultService
|
||||
}
|
||||
|
||||
get files() {
|
||||
return this.application.fileService
|
||||
}
|
||||
|
||||
get keys() {
|
||||
return this.application.keySystemKeyManager
|
||||
}
|
||||
|
||||
get asymmetric() {
|
||||
return this.application.asymmetricMessageService
|
||||
}
|
||||
|
||||
get publicKey() {
|
||||
return this.sessions.getPublicKey()
|
||||
}
|
||||
|
||||
get signingPublicKey() {
|
||||
return this.sessions.getSigningPublicKey()
|
||||
}
|
||||
|
||||
get privateKey() {
|
||||
return this.encryption.getKeyPair().privateKey
|
||||
}
|
||||
|
||||
ignoreChallenges() {
|
||||
this.ignoringChallenges = true
|
||||
}
|
||||
@@ -118,7 +177,10 @@ export class AppContext {
|
||||
},
|
||||
})
|
||||
|
||||
return this.application.syncService.handleSuccessServerResponse({ payloadsSavedOrSaving: [] }, response)
|
||||
return this.application.syncService.handleSuccessServerResponse(
|
||||
{ payloadsSavedOrSaving: [], options: {} },
|
||||
response,
|
||||
)
|
||||
}
|
||||
|
||||
resolveWhenKeyRecovered(uuid) {
|
||||
@@ -131,6 +193,16 @@ export class AppContext {
|
||||
})
|
||||
}
|
||||
|
||||
resolveWhenSharedVaultUserKeysResolved() {
|
||||
return new Promise((resolve) => {
|
||||
this.application.vaultService.collaboration.addEventObserver((eventName) => {
|
||||
if (eventName === SharedVaultServiceEvent.SharedVaultStatusChanged) {
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async awaitSignInEvent() {
|
||||
return new Promise((resolve) => {
|
||||
this.application.userService.addEventObserver((eventName) => {
|
||||
@@ -182,6 +254,155 @@ export class AppContext {
|
||||
})
|
||||
}
|
||||
|
||||
awaitNextSyncSharedVaultFromScratchEvent() {
|
||||
return new Promise((resolve) => {
|
||||
const removeObserver = this.application.syncService.addEventObserver((event, data) => {
|
||||
if (event === SyncEvent.PaginatedSyncRequestCompleted && data?.options?.sharedVaultUuids) {
|
||||
removeObserver()
|
||||
resolve(data)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
resolveWithUploadedPayloads() {
|
||||
return new Promise((resolve) => {
|
||||
this.application.syncService.addEventObserver((event, data) => {
|
||||
if (event === SyncEvent.PaginatedSyncRequestCompleted) {
|
||||
resolve(data.uploadedPayloads)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
resolveWithConflicts() {
|
||||
return new Promise((resolve) => {
|
||||
this.application.syncService.addEventObserver((event, response) => {
|
||||
if (event === SyncEvent.PaginatedSyncRequestCompleted) {
|
||||
resolve(response.rawConflictObjects)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
resolveWhenSavedSyncPayloadsIncludesItemUuid(uuid) {
|
||||
return new Promise((resolve) => {
|
||||
this.application.syncService.addEventObserver((event, response) => {
|
||||
if (event === SyncEvent.PaginatedSyncRequestCompleted) {
|
||||
const savedPayload = response.savedPayloads.find((payload) => payload.uuid === uuid)
|
||||
if (savedPayload) {
|
||||
resolve()
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
resolveWhenSavedSyncPayloadsIncludesItemThatIsDuplicatedOf(uuid) {
|
||||
return new Promise((resolve) => {
|
||||
this.application.syncService.addEventObserver((event, response) => {
|
||||
if (event === SyncEvent.PaginatedSyncRequestCompleted) {
|
||||
const savedPayload = response.savedPayloads.find((payload) => payload.duplicate_of === uuid)
|
||||
if (savedPayload) {
|
||||
resolve()
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
resolveWhenItemCompletesAddingToVault(targetItem) {
|
||||
return new Promise((resolve) => {
|
||||
const objectToSpy = this.vaults
|
||||
sinon.stub(objectToSpy, 'moveItemToVault').callsFake(async (vault, item) => {
|
||||
objectToSpy.moveItemToVault.restore()
|
||||
const result = await objectToSpy.moveItemToVault(vault, item)
|
||||
if (!targetItem || item.uuid === targetItem.uuid) {
|
||||
resolve()
|
||||
}
|
||||
return result
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
resolveWhenItemCompletesRemovingFromVault(targetItem) {
|
||||
return new Promise((resolve) => {
|
||||
const objectToSpy = this.vaults
|
||||
sinon.stub(objectToSpy, 'removeItemFromVault').callsFake(async (item) => {
|
||||
objectToSpy.removeItemFromVault.restore()
|
||||
const result = await objectToSpy.removeItemFromVault(item)
|
||||
if (item.uuid === targetItem.uuid) {
|
||||
resolve()
|
||||
}
|
||||
return result
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
resolveWhenAsymmetricMessageProcessingCompletes() {
|
||||
return new Promise((resolve) => {
|
||||
const objectToSpy = this.asymmetric
|
||||
sinon.stub(objectToSpy, 'handleRemoteReceivedAsymmetricMessages').callsFake(async (messages) => {
|
||||
objectToSpy.handleRemoteReceivedAsymmetricMessages.restore()
|
||||
const result = await objectToSpy.handleRemoteReceivedAsymmetricMessages(messages)
|
||||
resolve()
|
||||
return result
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
resolveWhenUserMessagesProcessingCompletes() {
|
||||
return new Promise((resolve) => {
|
||||
const objectToSpy = this.application.userEventService
|
||||
sinon.stub(objectToSpy, 'handleReceivedUserEvents').callsFake(async (params) => {
|
||||
objectToSpy.handleReceivedUserEvents.restore()
|
||||
const result = await objectToSpy.handleReceivedUserEvents(params)
|
||||
resolve()
|
||||
return result
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
resolveWhenSharedVaultServiceSendsContactShareMessage() {
|
||||
return new Promise((resolve) => {
|
||||
const objectToSpy = this.sharedVaults
|
||||
sinon.stub(objectToSpy, 'shareContactWithUserAdministeredSharedVaults').callsFake(async (contact) => {
|
||||
objectToSpy.shareContactWithUserAdministeredSharedVaults.restore()
|
||||
const result = await objectToSpy.shareContactWithUserAdministeredSharedVaults(contact)
|
||||
resolve()
|
||||
return result
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
resolveWhenSharedVaultKeyRotationInvitesGetSent(targetVault) {
|
||||
return new Promise((resolve) => {
|
||||
const objectToSpy = this.sharedVaults
|
||||
sinon.stub(objectToSpy, 'handleVaultRootKeyRotatedEvent').callsFake(async (vault) => {
|
||||
objectToSpy.handleVaultRootKeyRotatedEvent.restore()
|
||||
const result = await objectToSpy.handleVaultRootKeyRotatedEvent(vault)
|
||||
if (vault.systemIdentifier === targetVault.systemIdentifier) {
|
||||
resolve()
|
||||
}
|
||||
return result
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
resolveWhenSharedVaultChangeInvitesAreSent(sharedVaultUuid) {
|
||||
return new Promise((resolve) => {
|
||||
const objectToSpy = this.sharedVaults
|
||||
sinon.stub(objectToSpy, 'handleVaultRootKeyRotatedEvent').callsFake(async (vault) => {
|
||||
objectToSpy.handleVaultRootKeyRotatedEvent.restore()
|
||||
const result = await objectToSpy.handleVaultRootKeyRotatedEvent(vault)
|
||||
if (vault.sharing.sharedVaultUuid === sharedVaultUuid) {
|
||||
resolve()
|
||||
}
|
||||
return result
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
awaitUserPrefsSingletonCreation() {
|
||||
const preferences = this.application.preferencesService.preferences
|
||||
if (preferences) {
|
||||
@@ -232,6 +453,10 @@ export class AppContext {
|
||||
await this.application.sync.sync(options || { awaitAll: true })
|
||||
}
|
||||
|
||||
async clearSyncPositionTokens() {
|
||||
await this.application.sync.clearSyncPositionTokens()
|
||||
}
|
||||
|
||||
async maximumSync() {
|
||||
await this.sync(MaximumSyncOptions)
|
||||
}
|
||||
@@ -290,22 +515,31 @@ export class AppContext {
|
||||
})
|
||||
}
|
||||
|
||||
async createSyncedNote(title, text) {
|
||||
async createSyncedNote(title = 'foo', text = 'bar') {
|
||||
const payload = createNotePayload(title, text)
|
||||
const item = await this.application.items.emitItemFromPayload(payload, PayloadEmitSource.LocalChanged)
|
||||
await this.application.items.setItemDirty(item)
|
||||
const item = await this.application.mutator.emitItemFromPayload(payload, PayloadEmitSource.LocalChanged)
|
||||
await this.application.mutator.setItemDirty(item)
|
||||
await this.application.syncService.sync(MaximumSyncOptions)
|
||||
const note = this.application.items.findItem(payload.uuid)
|
||||
|
||||
return note
|
||||
}
|
||||
|
||||
lockSyncing() {
|
||||
this.application.syncService.lockSyncing()
|
||||
}
|
||||
|
||||
unlockSyncing() {
|
||||
this.application.syncService.unlockSyncing()
|
||||
}
|
||||
|
||||
async deleteItemAndSync(item) {
|
||||
await this.application.mutator.deleteItem(item)
|
||||
await this.sync()
|
||||
}
|
||||
|
||||
async changeNoteTitle(note, title) {
|
||||
return this.application.items.changeNote(note, (mutator) => {
|
||||
return this.application.mutator.changeNote(note, (mutator) => {
|
||||
mutator.title = title
|
||||
})
|
||||
}
|
||||
@@ -325,6 +559,10 @@ export class AppContext {
|
||||
return this.application.items.getDisplayableNotes().length
|
||||
}
|
||||
|
||||
get notes() {
|
||||
return this.application.items.getDisplayableNotes()
|
||||
}
|
||||
|
||||
async createConflictedNotes(otherContext) {
|
||||
const note = await this.createSyncedNote()
|
||||
|
||||
@@ -341,4 +579,41 @@ export class AppContext {
|
||||
conflict: this.findNoteByTitle('title-2'),
|
||||
}
|
||||
}
|
||||
|
||||
findDuplicateNote(duplicateOfUuid) {
|
||||
const items = this.items.getDisplayableNotes()
|
||||
return items.find((note) => note.duplicateOf === duplicateOfUuid)
|
||||
}
|
||||
|
||||
get userUuid() {
|
||||
return this.application.sessions.user.uuid
|
||||
}
|
||||
|
||||
sleep(seconds) {
|
||||
return Utils.sleep(seconds)
|
||||
}
|
||||
|
||||
anticipateConsoleError(message, _reason) {
|
||||
console.warn('Anticipating a console error with message:', message)
|
||||
}
|
||||
|
||||
async publicMockSubscriptionPurchaseEvent() {
|
||||
await Events.publishMockedEvent('SUBSCRIPTION_PURCHASED', {
|
||||
userEmail: this.email,
|
||||
subscriptionId: GlobalSubscriptionIdCounter++,
|
||||
subscriptionName: 'PRO_PLAN',
|
||||
subscriptionExpiresAt: (new Date().getTime() + 3_600_000) * 1_000,
|
||||
timestamp: Date.now(),
|
||||
offline: false,
|
||||
discountCode: null,
|
||||
limitedDiscountPurchased: false,
|
||||
newSubscriber: true,
|
||||
totalActiveSubscriptionsCount: 1,
|
||||
userRegisteredAt: 1,
|
||||
billingFrequency: 12,
|
||||
payAmount: 59.0,
|
||||
})
|
||||
|
||||
await Utils.sleep(2)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,10 +2,6 @@ import WebDeviceInterface from './web_device_interface.js'
|
||||
import FakeWebCrypto from './fake_web_crypto.js'
|
||||
import * as Defaults from './Defaults.js'
|
||||
|
||||
export const BaseItemCounts = {
|
||||
DefaultItems: ['ItemsKey', 'UserPreferences', 'DarkTheme'].length,
|
||||
}
|
||||
|
||||
export function createApplicationWithOptions({ identifier, environment, platform, host, crypto, device }) {
|
||||
if (!device) {
|
||||
device = new WebDeviceInterface()
|
||||
|
||||
35
packages/snjs/mocha/lib/BaseItemCounts.js
Normal file
35
packages/snjs/mocha/lib/BaseItemCounts.js
Normal file
@@ -0,0 +1,35 @@
|
||||
const ExpectedItemCountsWithVaultFeatureEnabled = {
|
||||
Items: ['ItemsKey', 'UserPreferences', 'DarkTheme'].length,
|
||||
ItemsWithAccount: ['ItemsKey', 'UserPreferences', 'DarkTheme', 'TrustedSelfContact'].length,
|
||||
ItemsWithAccountWithoutItemsKey: ['UserPreferences', 'DarkTheme', 'TrustedSelfContact'].length,
|
||||
ItemsNoAccounNoItemsKey: ['UserPreferences', 'DarkTheme'].length,
|
||||
BackupFileRootKeyEncryptedItems: ['TrustedSelfContact'].length,
|
||||
}
|
||||
|
||||
const ExpectedItemCountsWithVaultFeatureDisabled = {
|
||||
Items: ['ItemsKey', 'UserPreferences', 'DarkTheme'].length,
|
||||
ItemsWithAccount: ['ItemsKey', 'UserPreferences', 'DarkTheme'].length,
|
||||
ItemsWithAccountWithoutItemsKey: ['UserPreferences', 'DarkTheme'].length,
|
||||
ItemsNoAccounNoItemsKey: ['UserPreferences', 'DarkTheme'].length,
|
||||
BackupFileRootKeyEncryptedItems: [].length,
|
||||
}
|
||||
|
||||
const isVaultsEnabled = InternalFeatureService.get().isFeatureEnabled(InternalFeature.Vaults)
|
||||
|
||||
export const BaseItemCounts = {
|
||||
DefaultItems: isVaultsEnabled
|
||||
? ExpectedItemCountsWithVaultFeatureEnabled.Items
|
||||
: ExpectedItemCountsWithVaultFeatureDisabled.Items,
|
||||
DefaultItemsWithAccount: isVaultsEnabled
|
||||
? ExpectedItemCountsWithVaultFeatureEnabled.ItemsWithAccount
|
||||
: ExpectedItemCountsWithVaultFeatureDisabled.ItemsWithAccount,
|
||||
DefaultItemsWithAccountWithoutItemsKey: isVaultsEnabled
|
||||
? ExpectedItemCountsWithVaultFeatureEnabled.ItemsWithAccountWithoutItemsKey
|
||||
: ExpectedItemCountsWithVaultFeatureDisabled.ItemsWithAccountWithoutItemsKey,
|
||||
DefaultItemsNoAccounNoItemsKey: isVaultsEnabled
|
||||
? ExpectedItemCountsWithVaultFeatureEnabled.ItemsNoAccounNoItemsKey
|
||||
: ExpectedItemCountsWithVaultFeatureDisabled.ItemsNoAccounNoItemsKey,
|
||||
BackupFileRootKeyEncryptedItems: isVaultsEnabled
|
||||
? ExpectedItemCountsWithVaultFeatureEnabled.BackupFileRootKeyEncryptedItems
|
||||
: ExpectedItemCountsWithVaultFeatureDisabled.BackupFileRootKeyEncryptedItems,
|
||||
}
|
||||
140
packages/snjs/mocha/lib/Collaboration.js
Normal file
140
packages/snjs/mocha/lib/Collaboration.js
Normal file
@@ -0,0 +1,140 @@
|
||||
import * as Factory from './factory.js'
|
||||
|
||||
export const createContactContext = async () => {
|
||||
const contactContext = await Factory.createAppContextWithRealCrypto()
|
||||
await contactContext.launch()
|
||||
await contactContext.register()
|
||||
|
||||
return {
|
||||
contactContext,
|
||||
deinitContactContext: contactContext.deinit.bind(contactContext),
|
||||
}
|
||||
}
|
||||
|
||||
export const createTrustedContactForUserOfContext = async (
|
||||
contextAddingNewContact,
|
||||
contextImportingContactInfoFrom,
|
||||
) => {
|
||||
const contact = await contextAddingNewContact.application.contactService.createOrEditTrustedContact({
|
||||
name: 'John Doe',
|
||||
publicKey: contextImportingContactInfoFrom.publicKey,
|
||||
signingPublicKey: contextImportingContactInfoFrom.signingPublicKey,
|
||||
contactUuid: contextImportingContactInfoFrom.userUuid,
|
||||
})
|
||||
|
||||
return contact
|
||||
}
|
||||
|
||||
export const acceptAllInvites = async (context) => {
|
||||
const inviteRecords = context.sharedVaults.getCachedPendingInviteRecords()
|
||||
for (const record of inviteRecords) {
|
||||
await context.sharedVaults.acceptPendingSharedVaultInvite(record)
|
||||
}
|
||||
}
|
||||
|
||||
export const createSharedVaultWithAcceptedInvite = async (context, permissions = SharedVaultPermission.Write) => {
|
||||
const { sharedVault, contact, contactContext, deinitContactContext } =
|
||||
await createSharedVaultWithUnacceptedButTrustedInvite(context, permissions)
|
||||
|
||||
const promise = contactContext.awaitNextSyncSharedVaultFromScratchEvent()
|
||||
|
||||
await acceptAllInvites(contactContext)
|
||||
|
||||
await promise
|
||||
|
||||
const contactVault = contactContext.vaults.getVault({ keySystemIdentifier: sharedVault.systemIdentifier })
|
||||
|
||||
return { sharedVault, contact, contactVault, contactContext, deinitContactContext }
|
||||
}
|
||||
|
||||
export const createSharedVaultWithAcceptedInviteAndNote = async (
|
||||
context,
|
||||
permissions = SharedVaultPermission.Write,
|
||||
) => {
|
||||
const { sharedVault, contactContext, contact, deinitContactContext } = await createSharedVaultWithAcceptedInvite(
|
||||
context,
|
||||
permissions,
|
||||
)
|
||||
const note = await context.createSyncedNote('foo', 'bar')
|
||||
const updatedNote = await moveItemToVault(context, sharedVault, note)
|
||||
await contactContext.sync()
|
||||
|
||||
return { sharedVault, note: updatedNote, contact, contactContext, deinitContactContext }
|
||||
}
|
||||
|
||||
export const createSharedVaultWithUnacceptedButTrustedInvite = async (
|
||||
context,
|
||||
permissions = SharedVaultPermission.Write,
|
||||
) => {
|
||||
const sharedVault = await createSharedVault(context)
|
||||
|
||||
const { contactContext, deinitContactContext } = await createContactContext()
|
||||
const contact = await createTrustedContactForUserOfContext(context, contactContext)
|
||||
await createTrustedContactForUserOfContext(contactContext, context)
|
||||
|
||||
const invite = await context.sharedVaults.inviteContactToSharedVault(sharedVault, contact, permissions)
|
||||
await contactContext.sync()
|
||||
|
||||
return { sharedVault, contact, contactContext, deinitContactContext, invite }
|
||||
}
|
||||
|
||||
export const createSharedVaultWithUnacceptedAndUntrustedInvite = async (
|
||||
context,
|
||||
permissions = SharedVaultPermission.Write,
|
||||
) => {
|
||||
const sharedVault = await createSharedVault(context)
|
||||
|
||||
const { contactContext, deinitContactContext } = await createContactContext()
|
||||
const contact = await createTrustedContactForUserOfContext(context, contactContext)
|
||||
|
||||
const invite = await context.sharedVaults.inviteContactToSharedVault(sharedVault, contact, permissions)
|
||||
await contactContext.sync()
|
||||
|
||||
return { sharedVault, contact, contactContext, deinitContactContext, invite }
|
||||
}
|
||||
|
||||
export const inviteNewPartyToSharedVault = async (context, sharedVault, permissions = SharedVaultPermission.Write) => {
|
||||
const { contactContext: thirdPartyContext, deinitContactContext: deinitThirdPartyContext } =
|
||||
await createContactContext()
|
||||
|
||||
const thirdPartyContact = await createTrustedContactForUserOfContext(context, thirdPartyContext)
|
||||
await createTrustedContactForUserOfContext(thirdPartyContext, context)
|
||||
await context.sharedVaults.inviteContactToSharedVault(sharedVault, thirdPartyContact, permissions)
|
||||
|
||||
await thirdPartyContext.sync()
|
||||
|
||||
return { thirdPartyContext, thirdPartyContact, deinitThirdPartyContext }
|
||||
}
|
||||
|
||||
export const createPrivateVault = async (context) => {
|
||||
const privateVault = await context.vaults.createRandomizedVault({
|
||||
name: 'My Private Vault',
|
||||
storagePreference: KeySystemRootKeyStorageMode.Synced,
|
||||
})
|
||||
|
||||
return privateVault
|
||||
}
|
||||
|
||||
export const createSharedVault = async (context) => {
|
||||
const sharedVault = await context.sharedVaults.createSharedVault({ name: 'My Shared Vault' })
|
||||
|
||||
if (isClientDisplayableError(sharedVault)) {
|
||||
throw new Error(sharedVault.text)
|
||||
}
|
||||
|
||||
return sharedVault
|
||||
}
|
||||
|
||||
export const createSharedVaultWithNote = async (context) => {
|
||||
const sharedVault = await createSharedVault(context)
|
||||
const note = await context.createSyncedNote()
|
||||
const updatedNote = await moveItemToVault(context, sharedVault, note)
|
||||
return { sharedVault, note: updatedNote }
|
||||
}
|
||||
|
||||
export const moveItemToVault = async (context, sharedVault, item) => {
|
||||
const promise = context.resolveWhenItemCompletesAddingToVault(item)
|
||||
const updatedItem = await context.vaults.moveItemToVault(sharedVault, item)
|
||||
await promise
|
||||
return updatedItem
|
||||
}
|
||||
19
packages/snjs/mocha/lib/Events.js
Normal file
19
packages/snjs/mocha/lib/Events.js
Normal file
@@ -0,0 +1,19 @@
|
||||
import * as Defaults from './Defaults.js'
|
||||
|
||||
export async function publishMockedEvent(eventType, eventPayload) {
|
||||
const response = await fetch(`${Defaults.getDefaultMockedEventServiceUrl()}/events`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
eventType,
|
||||
eventPayload,
|
||||
}),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
console.error(`Failed to publish mocked event: ${response.status} ${response.statusText}`)
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
export async function uploadFile(fileService, buffer, name, ext, chunkSize) {
|
||||
const operation = await fileService.beginNewFileUpload(buffer.byteLength)
|
||||
export async function uploadFile(fileService, buffer, name, ext, chunkSize, vault) {
|
||||
const operation = await fileService.beginNewFileUpload(buffer.byteLength, vault)
|
||||
|
||||
let chunkId = 1
|
||||
for (let i = 0; i < buffer.length; i += chunkSize) {
|
||||
@@ -18,14 +18,16 @@ export async function uploadFile(fileService, buffer, name, ext, chunkSize) {
|
||||
return file
|
||||
}
|
||||
|
||||
export async function downloadFile(fileService, itemManager, remoteIdentifier) {
|
||||
const file = itemManager.getItems(ContentType.File).find((file) => file.remoteIdentifier === remoteIdentifier)
|
||||
|
||||
export async function downloadFile(fileService, file) {
|
||||
let receivedBytes = new Uint8Array()
|
||||
|
||||
await fileService.downloadFile(file, (decryptedBytes) => {
|
||||
const error = await fileService.downloadFile(file, (decryptedBytes) => {
|
||||
receivedBytes = new Uint8Array([...receivedBytes, ...decryptedBytes])
|
||||
})
|
||||
|
||||
if (error) {
|
||||
throw new Error('Could not download file', error.text)
|
||||
}
|
||||
|
||||
return receivedBytes
|
||||
}
|
||||
|
||||
@@ -63,7 +63,7 @@ export function createRelatedNoteTagPairPayload({ noteTitle, noteText, tagTitle,
|
||||
|
||||
export async function createSyncedNoteWithTag(application) {
|
||||
const payloads = createRelatedNoteTagPairPayload()
|
||||
await application.itemManager.emitItemsFromPayloads(payloads)
|
||||
await application.mutator.emitItemsFromPayloads(payloads)
|
||||
return application.sync.sync(MaximumSyncOptions)
|
||||
}
|
||||
|
||||
|
||||
@@ -71,24 +71,6 @@ export function getDefaultHost() {
|
||||
return Defaults.getDefaultHost()
|
||||
}
|
||||
|
||||
export async function publishMockedEvent(eventType, eventPayload) {
|
||||
const response = await fetch(`${Defaults.getDefaultMockedEventServiceUrl()}/events`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
eventType,
|
||||
eventPayload,
|
||||
}),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
console.error(`Failed to publish mocked event: ${response.status} ${response.statusText}`)
|
||||
}
|
||||
}
|
||||
|
||||
export function createApplicationWithFakeCrypto(identifier, environment, platform, host) {
|
||||
return Applications.createApplicationWithFakeCrypto(identifier, environment, platform, host)
|
||||
}
|
||||
@@ -154,7 +136,7 @@ export async function registerOldUser({ application, email, password, version })
|
||||
keyParams: accountKey.keyParams,
|
||||
})
|
||||
/** Mark all existing items as dirty. */
|
||||
await application.itemManager.changeItems(application.itemManager.items, (m) => {
|
||||
await application.mutator.changeItems(application.itemManager.items, (m) => {
|
||||
m.dirty = true
|
||||
})
|
||||
await application.sessionManager.handleSuccessAuthResponse(response, accountKey)
|
||||
@@ -188,18 +170,18 @@ export function itemToStoragePayload(item) {
|
||||
|
||||
export function createMappedNote(application, title, text, dirty = true) {
|
||||
const payload = createNotePayload(title, text, dirty)
|
||||
return application.itemManager.emitItemFromPayload(payload, PayloadEmitSource.LocalChanged)
|
||||
return application.mutator.emitItemFromPayload(payload, PayloadEmitSource.LocalChanged)
|
||||
}
|
||||
|
||||
export async function createMappedTag(application, tagParams = {}) {
|
||||
const payload = createStorageItemTagPayload(tagParams)
|
||||
return application.itemManager.emitItemFromPayload(payload, PayloadEmitSource.LocalChanged)
|
||||
return application.mutator.emitItemFromPayload(payload, PayloadEmitSource.LocalChanged)
|
||||
}
|
||||
|
||||
export async function createSyncedNote(application, title, text) {
|
||||
const payload = createNotePayload(title, text)
|
||||
const item = await application.itemManager.emitItemFromPayload(payload, PayloadEmitSource.LocalChanged)
|
||||
await application.itemManager.setItemDirty(item)
|
||||
const item = await application.mutator.emitItemFromPayload(payload, PayloadEmitSource.LocalChanged)
|
||||
await application.mutator.setItemDirty(item)
|
||||
await application.syncService.sync(syncOptions)
|
||||
const note = application.items.findItem(payload.uuid)
|
||||
return note
|
||||
@@ -218,7 +200,7 @@ export async function createManyMappedNotes(application, count) {
|
||||
const createdNotes = []
|
||||
for (let i = 0; i < count; i++) {
|
||||
const note = await createMappedNote(application)
|
||||
await application.itemManager.setItemDirty(note)
|
||||
await application.mutator.setItemDirty(note)
|
||||
createdNotes.push(note)
|
||||
}
|
||||
return createdNotes
|
||||
@@ -406,7 +388,7 @@ export function pinNote(application, note) {
|
||||
}
|
||||
|
||||
export async function insertItemWithOverride(application, contentType, content, needsSync = false, errorDecrypting) {
|
||||
const item = await application.itemManager.createItem(contentType, content, needsSync)
|
||||
const item = await application.mutator.createItem(contentType, content, needsSync)
|
||||
|
||||
if (errorDecrypting) {
|
||||
const encrypted = new EncryptedPayload({
|
||||
@@ -415,12 +397,12 @@ export async function insertItemWithOverride(application, contentType, content,
|
||||
errorDecrypting,
|
||||
})
|
||||
|
||||
await application.itemManager.emitItemFromPayload(encrypted)
|
||||
await application.payloadManager.emitPayload(encrypted)
|
||||
} else {
|
||||
const decrypted = new DecryptedPayload({
|
||||
...item.payload.ejected(),
|
||||
})
|
||||
await application.itemManager.emitItemFromPayload(decrypted)
|
||||
await application.payloadManager.emitPayload(decrypted)
|
||||
}
|
||||
|
||||
return application.itemManager.findAnyItem(item.uuid)
|
||||
@@ -441,7 +423,7 @@ export async function markDirtyAndSyncItem(application, itemToLookupUuidFor) {
|
||||
throw Error('Attempting to save non-inserted item')
|
||||
}
|
||||
if (!item.dirty) {
|
||||
await application.itemManager.changeItem(item, undefined, MutationType.NoUpdateUserTimestamps)
|
||||
await application.mutator.changeItem(item, undefined, MutationType.NoUpdateUserTimestamps)
|
||||
}
|
||||
await application.sync.sync()
|
||||
}
|
||||
@@ -467,23 +449,22 @@ export async function changePayloadTimeStamp(application, payload, timestamp, co
|
||||
updated_at_timestamp: timestamp,
|
||||
})
|
||||
|
||||
await application.itemManager.emitItemFromPayload(changedPayload)
|
||||
await application.mutator.emitItemFromPayload(changedPayload)
|
||||
|
||||
return application.itemManager.findAnyItem(payload.uuid)
|
||||
}
|
||||
|
||||
export async function changePayloadUpdatedAt(application, payload, updatedAt) {
|
||||
const latestPayload = application.payloadManager.collection.find(payload.uuid)
|
||||
|
||||
const changedPayload = new DecryptedPayload({
|
||||
...latestPayload,
|
||||
...latestPayload.ejected(),
|
||||
dirty: true,
|
||||
dirtyIndex: getIncrementedDirtyIndex(),
|
||||
updated_at: updatedAt,
|
||||
})
|
||||
|
||||
await application.itemManager.emitItemFromPayload(changedPayload)
|
||||
|
||||
return application.itemManager.findAnyItem(payload.uuid)
|
||||
return application.mutator.emitItemFromPayload(changedPayload)
|
||||
}
|
||||
|
||||
export async function changePayloadTimeStampDeleteAndSync(application, payload, timestamp, syncOptions) {
|
||||
@@ -497,6 +478,6 @@ export async function changePayloadTimeStampDeleteAndSync(application, payload,
|
||||
updated_at_timestamp: timestamp,
|
||||
})
|
||||
|
||||
await application.itemManager.emitItemFromPayload(changedPayload)
|
||||
await application.payloadManager.emitPayload(changedPayload)
|
||||
await application.sync.sync(syncOptions)
|
||||
}
|
||||
|
||||
@@ -158,8 +158,39 @@ export default class FakeWebCrypto {
|
||||
return data.message
|
||||
}
|
||||
|
||||
sodiumCryptoBoxGenerateKeypair() {
|
||||
return { publicKey: this.randomString(64), privateKey: this.randomString(64), keyType: 'x25519' }
|
||||
sodiumCryptoSign(message, secretKey) {
|
||||
const data = {
|
||||
message,
|
||||
secretKey,
|
||||
}
|
||||
return btoa(JSON.stringify(data))
|
||||
}
|
||||
|
||||
sodiumCryptoKdfDeriveFromKey(key, subkeyNumber, subkeyLength, context) {
|
||||
return btoa(key + subkeyNumber + subkeyLength + context)
|
||||
}
|
||||
|
||||
sodiumCryptoGenericHash(message, key) {
|
||||
return btoa(message + key)
|
||||
}
|
||||
|
||||
sodiumCryptoSignVerify(message, signature, publicKey) {
|
||||
return true
|
||||
}
|
||||
|
||||
sodiumCryptoBoxSeedKeypair(seed) {
|
||||
return {
|
||||
privateKey: seed,
|
||||
publicKey: seed,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
sodiumCryptoSignSeedKeypair(seed) {
|
||||
return {
|
||||
privateKey: seed,
|
||||
publicKey: seed,
|
||||
}
|
||||
}
|
||||
|
||||
generateOtpSecret() {
|
||||
|
||||
@@ -37,6 +37,10 @@ export default class WebDeviceInterface {
|
||||
return {}
|
||||
}
|
||||
|
||||
clearAllDataFromDevice() {
|
||||
localStorage.clear()
|
||||
}
|
||||
|
||||
_getDatabaseKeyPrefix(identifier) {
|
||||
if (identifier) {
|
||||
return `${identifier}-item-`
|
||||
@@ -61,29 +65,45 @@ export default class WebDeviceInterface {
|
||||
|
||||
async getDatabaseLoadChunks(options, identifier) {
|
||||
const entries = await this.getAllDatabaseEntries(identifier)
|
||||
const sorted = GetSortedPayloadsByPriority(entries, options)
|
||||
const {
|
||||
itemsKeyPayloads,
|
||||
keySystemRootKeyPayloads,
|
||||
keySystemItemsKeyPayloads,
|
||||
contentTypePriorityPayloads,
|
||||
remainingPayloads,
|
||||
} = GetSortedPayloadsByPriority(entries, options)
|
||||
|
||||
const itemsKeysChunk = {
|
||||
entries: sorted.itemsKeyPayloads,
|
||||
entries: itemsKeyPayloads,
|
||||
}
|
||||
|
||||
const keySystemRootKeysChunk = {
|
||||
entries: keySystemRootKeyPayloads,
|
||||
}
|
||||
|
||||
const keySystemItemsKeysChunk = {
|
||||
entries: keySystemItemsKeyPayloads,
|
||||
}
|
||||
|
||||
const contentTypePriorityChunk = {
|
||||
entries: sorted.contentTypePriorityPayloads,
|
||||
entries: contentTypePriorityPayloads,
|
||||
}
|
||||
|
||||
const remainingPayloadsChunks = []
|
||||
for (let i = 0; i < sorted.remainingPayloads.length; i += options.batchSize) {
|
||||
for (let i = 0; i < remainingPayloads.length; i += options.batchSize) {
|
||||
remainingPayloadsChunks.push({
|
||||
entries: sorted.remainingPayloads.slice(i, i + options.batchSize),
|
||||
entries: remainingPayloads.slice(i, i + options.batchSize),
|
||||
})
|
||||
}
|
||||
|
||||
const result = {
|
||||
fullEntries: {
|
||||
itemsKeys: itemsKeysChunk,
|
||||
keySystemRootKeys: keySystemRootKeysChunk,
|
||||
keySystemItemsKeys: keySystemItemsKeysChunk,
|
||||
remainingChunks: [contentTypePriorityChunk, ...remainingPayloadsChunks],
|
||||
},
|
||||
remainingChunksItemCount: sorted.contentTypePriorityPayloads.length + sorted.remainingPayloads.length,
|
||||
remainingChunksItemCount: contentTypePriorityPayloads.length + remainingPayloads.length,
|
||||
}
|
||||
|
||||
return result
|
||||
|
||||
@@ -68,7 +68,7 @@ describe('migrations', () => {
|
||||
}),
|
||||
}),
|
||||
)
|
||||
await application.mutator.insertItem(mfaItem)
|
||||
await application.mutator.insertItem(mfaItem, true)
|
||||
await application.sync.sync()
|
||||
|
||||
expect(application.items.getItems('SF|MFA').length).to.equal(1)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/* eslint-disable camelcase */
|
||||
/* eslint-disable no-unused-expressions */
|
||||
/* eslint-disable no-undef */
|
||||
import { BaseItemCounts } from '../lib/Applications.js'
|
||||
import { BaseItemCounts } from '../lib/BaseItemCounts.js'
|
||||
import * as Factory from '../lib/factory.js'
|
||||
chai.use(chaiAsPromised)
|
||||
const expect = chai.expect
|
||||
@@ -70,8 +70,8 @@ describe('app models', () => {
|
||||
},
|
||||
})
|
||||
|
||||
await this.application.itemManager.emitItemsFromPayloads([mutated], PayloadEmitSource.LocalChanged)
|
||||
await this.application.itemManager.emitItemsFromPayloads([params2], PayloadEmitSource.LocalChanged)
|
||||
await this.application.mutator.emitItemsFromPayloads([mutated], PayloadEmitSource.LocalChanged)
|
||||
await this.application.mutator.emitItemsFromPayloads([params2], PayloadEmitSource.LocalChanged)
|
||||
|
||||
const item1 = this.application.itemManager.findItem(params1.uuid)
|
||||
const item2 = this.application.itemManager.findItem(params2.uuid)
|
||||
@@ -93,11 +93,11 @@ describe('app models', () => {
|
||||
},
|
||||
})
|
||||
|
||||
let items = await this.application.itemManager.emitItemsFromPayloads([mutated], PayloadEmitSource.LocalChanged)
|
||||
let items = await this.application.mutator.emitItemsFromPayloads([mutated], PayloadEmitSource.LocalChanged)
|
||||
let item = items[0]
|
||||
expect(item).to.be.ok
|
||||
|
||||
items = await this.application.itemManager.emitItemsFromPayloads([mutated], PayloadEmitSource.LocalChanged)
|
||||
items = await this.application.mutator.emitItemsFromPayloads([mutated], PayloadEmitSource.LocalChanged)
|
||||
item = items[0]
|
||||
|
||||
expect(item.content.foo).to.equal('bar')
|
||||
@@ -108,10 +108,10 @@ describe('app models', () => {
|
||||
const item1 = await Factory.createMappedNote(this.application)
|
||||
const item2 = await Factory.createMappedNote(this.application)
|
||||
|
||||
await this.application.itemManager.changeItem(item1, (mutator) => {
|
||||
await this.application.mutator.changeItem(item1, (mutator) => {
|
||||
mutator.e2ePendingRefactor_addItemAsRelationship(item2)
|
||||
})
|
||||
await this.application.itemManager.changeItem(item2, (mutator) => {
|
||||
await this.application.mutator.changeItem(item2, (mutator) => {
|
||||
mutator.e2ePendingRefactor_addItemAsRelationship(item1)
|
||||
})
|
||||
|
||||
@@ -123,10 +123,10 @@ describe('app models', () => {
|
||||
var item1 = await Factory.createMappedNote(this.application)
|
||||
var item2 = await Factory.createMappedNote(this.application)
|
||||
|
||||
await this.application.itemManager.changeItem(item1, (mutator) => {
|
||||
await this.application.mutator.changeItem(item1, (mutator) => {
|
||||
mutator.e2ePendingRefactor_addItemAsRelationship(item2)
|
||||
})
|
||||
await this.application.itemManager.changeItem(item2, (mutator) => {
|
||||
await this.application.mutator.changeItem(item2, (mutator) => {
|
||||
mutator.e2ePendingRefactor_addItemAsRelationship(item1)
|
||||
})
|
||||
|
||||
@@ -143,7 +143,7 @@ describe('app models', () => {
|
||||
references: [],
|
||||
},
|
||||
})
|
||||
await this.application.itemManager.emitItemsFromPayloads([damagedPayload], PayloadEmitSource.LocalChanged)
|
||||
await this.application.mutator.emitItemsFromPayloads([damagedPayload], PayloadEmitSource.LocalChanged)
|
||||
|
||||
const refreshedItem1_2 = this.application.itemManager.findItem(item1.uuid)
|
||||
const refreshedItem2_2 = this.application.itemManager.findItem(item2.uuid)
|
||||
@@ -155,10 +155,10 @@ describe('app models', () => {
|
||||
it('creating and removing relationships between two items should have valid references', async function () {
|
||||
var item1 = await Factory.createMappedNote(this.application)
|
||||
var item2 = await Factory.createMappedNote(this.application)
|
||||
await this.application.itemManager.changeItem(item1, (mutator) => {
|
||||
await this.application.mutator.changeItem(item1, (mutator) => {
|
||||
mutator.e2ePendingRefactor_addItemAsRelationship(item2)
|
||||
})
|
||||
await this.application.itemManager.changeItem(item2, (mutator) => {
|
||||
await this.application.mutator.changeItem(item2, (mutator) => {
|
||||
mutator.e2ePendingRefactor_addItemAsRelationship(item1)
|
||||
})
|
||||
|
||||
@@ -171,10 +171,10 @@ describe('app models', () => {
|
||||
expect(this.application.itemManager.itemsReferencingItem(item1)).to.include(refreshedItem2)
|
||||
expect(this.application.itemManager.itemsReferencingItem(item2)).to.include(refreshedItem1)
|
||||
|
||||
await this.application.itemManager.changeItem(item1, (mutator) => {
|
||||
await this.application.mutator.changeItem(item1, (mutator) => {
|
||||
mutator.removeItemAsRelationship(item2)
|
||||
})
|
||||
await this.application.itemManager.changeItem(item2, (mutator) => {
|
||||
await this.application.mutator.changeItem(item2, (mutator) => {
|
||||
mutator.removeItemAsRelationship(item1)
|
||||
})
|
||||
|
||||
@@ -190,7 +190,7 @@ describe('app models', () => {
|
||||
|
||||
it('properly duplicates item with no relationships', async function () {
|
||||
const item = await Factory.createMappedNote(this.application)
|
||||
const duplicate = await this.application.itemManager.duplicateItem(item)
|
||||
const duplicate = await this.application.mutator.duplicateItem(item)
|
||||
expect(duplicate.uuid).to.not.equal(item.uuid)
|
||||
expect(item.isItemContentEqualWith(duplicate)).to.equal(true)
|
||||
expect(item.created_at.toISOString()).to.equal(duplicate.created_at.toISOString())
|
||||
@@ -201,13 +201,13 @@ describe('app models', () => {
|
||||
const item1 = await Factory.createMappedNote(this.application)
|
||||
const item2 = await Factory.createMappedNote(this.application)
|
||||
|
||||
const refreshedItem1 = await this.application.itemManager.changeItem(item1, (mutator) => {
|
||||
const refreshedItem1 = await this.application.mutator.changeItem(item1, (mutator) => {
|
||||
mutator.e2ePendingRefactor_addItemAsRelationship(item2)
|
||||
})
|
||||
|
||||
expect(refreshedItem1.content.references.length).to.equal(1)
|
||||
|
||||
const duplicate = await this.application.itemManager.duplicateItem(item1)
|
||||
const duplicate = await this.application.mutator.duplicateItem(item1)
|
||||
expect(duplicate.uuid).to.not.equal(item1.uuid)
|
||||
expect(duplicate.content.references.length).to.equal(1)
|
||||
|
||||
@@ -223,11 +223,11 @@ describe('app models', () => {
|
||||
it('removing references should update cross-refs', async function () {
|
||||
const item1 = await Factory.createMappedNote(this.application)
|
||||
const item2 = await Factory.createMappedNote(this.application)
|
||||
const refreshedItem1 = await this.application.itemManager.changeItem(item1, (mutator) => {
|
||||
const refreshedItem1 = await this.application.mutator.changeItem(item1, (mutator) => {
|
||||
mutator.e2ePendingRefactor_addItemAsRelationship(item2)
|
||||
})
|
||||
|
||||
const refreshedItem1_2 = await this.application.itemManager.emitItemFromPayload(
|
||||
const refreshedItem1_2 = await this.application.mutator.emitItemFromPayload(
|
||||
refreshedItem1.payloadRepresentation({
|
||||
deleted: true,
|
||||
content: {
|
||||
@@ -247,7 +247,7 @@ describe('app models', () => {
|
||||
const item1 = await Factory.createMappedNote(this.application)
|
||||
const item2 = await Factory.createMappedNote(this.application)
|
||||
|
||||
const refreshedItem1 = await this.application.itemManager.changeItem(item1, (mutator) => {
|
||||
const refreshedItem1 = await this.application.mutator.changeItem(item1, (mutator) => {
|
||||
mutator.e2ePendingRefactor_addItemAsRelationship(item2)
|
||||
})
|
||||
|
||||
@@ -290,12 +290,12 @@ describe('app models', () => {
|
||||
waitingForKey: true,
|
||||
})
|
||||
|
||||
await this.application.itemManager.emitItemFromPayload(errored)
|
||||
await this.application.payloadManager.emitPayload(errored)
|
||||
|
||||
expect(this.application.payloadManager.findOne(item1.uuid).errorDecrypting).to.equal(true)
|
||||
expect(this.application.payloadManager.findOne(item1.uuid).items_key_id).to.equal(itemsKey.uuid)
|
||||
|
||||
sinon.stub(this.application.protocolService.itemsEncryption, 'decryptErroredPayloads').callsFake(() => {
|
||||
sinon.stub(this.application.protocolService.itemsEncryption, 'decryptErroredItemPayloads').callsFake(() => {
|
||||
// prevent auto decryption
|
||||
})
|
||||
|
||||
@@ -310,7 +310,7 @@ describe('app models', () => {
|
||||
const item2 = await Factory.createMappedNote(this.application)
|
||||
this.expectedItemCount += 2
|
||||
|
||||
await this.application.itemManager.changeItem(item1, (mutator) => {
|
||||
await this.application.mutator.changeItem(item1, (mutator) => {
|
||||
mutator.e2ePendingRefactor_addItemAsRelationship(item2)
|
||||
})
|
||||
|
||||
@@ -339,13 +339,13 @@ describe('app models', () => {
|
||||
it('maintains referencing relationships when duplicating', async function () {
|
||||
const tag = await Factory.createMappedTag(this.application)
|
||||
const note = await Factory.createMappedNote(this.application)
|
||||
const refreshedTag = await this.application.itemManager.changeItem(tag, (mutator) => {
|
||||
const refreshedTag = await this.application.mutator.changeItem(tag, (mutator) => {
|
||||
mutator.e2ePendingRefactor_addItemAsRelationship(note)
|
||||
})
|
||||
|
||||
expect(refreshedTag.content.references.length).to.equal(1)
|
||||
|
||||
const noteCopy = await this.application.itemManager.duplicateItem(note)
|
||||
const noteCopy = await this.application.mutator.duplicateItem(note)
|
||||
expect(note.uuid).to.not.equal(noteCopy.uuid)
|
||||
|
||||
expect(this.application.itemManager.getDisplayableNotes().length).to.equal(2)
|
||||
@@ -358,7 +358,7 @@ describe('app models', () => {
|
||||
})
|
||||
|
||||
it('maintains editor reference when duplicating note', async function () {
|
||||
const editor = await this.application.itemManager.createItem(
|
||||
const editor = await this.application.mutator.createItem(
|
||||
ContentType.Component,
|
||||
{ area: ComponentArea.Editor, package_info: { identifier: 'foo-editor' } },
|
||||
true,
|
||||
@@ -369,7 +369,7 @@ describe('app models', () => {
|
||||
|
||||
expect(this.application.componentManager.editorForNote(note).uuid).to.equal(editor.uuid)
|
||||
|
||||
const duplicate = await this.application.itemManager.duplicateItem(note, true)
|
||||
const duplicate = await this.application.mutator.duplicateItem(note, true)
|
||||
expect(this.application.componentManager.editorForNote(duplicate).uuid).to.equal(editor.uuid)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* eslint-disable no-unused-expressions */
|
||||
/* eslint-disable no-undef */
|
||||
import { BaseItemCounts } from '../lib/Applications.js'
|
||||
import { BaseItemCounts } from '../lib/BaseItemCounts.js'
|
||||
import * as Factory from '../lib/factory.js'
|
||||
import { createRelatedNoteTagPairPayload } from '../lib/Items.js'
|
||||
chai.use(chaiAsPromised)
|
||||
@@ -43,7 +43,7 @@ describe('importing', function () {
|
||||
|
||||
it('should not import backups made from unsupported versions', async function () {
|
||||
await setup({ fakeCrypto: true })
|
||||
const result = await application.mutator.importData({
|
||||
const result = await application.importData({
|
||||
version: '-1',
|
||||
items: [],
|
||||
})
|
||||
@@ -58,7 +58,7 @@ describe('importing', function () {
|
||||
password,
|
||||
version: ProtocolVersion.V003,
|
||||
})
|
||||
const result = await application.mutator.importData({
|
||||
const result = await application.importData({
|
||||
version: ProtocolVersion.V004,
|
||||
items: [],
|
||||
})
|
||||
@@ -71,7 +71,7 @@ describe('importing', function () {
|
||||
const notePayload = pair[0]
|
||||
const tagPayload = pair[1]
|
||||
|
||||
await application.itemManager.emitItemsFromPayloads([notePayload, tagPayload], PayloadEmitSource.LocalChanged)
|
||||
await application.mutator.emitItemsFromPayloads([notePayload, tagPayload], PayloadEmitSource.LocalChanged)
|
||||
expectedItemCount += 2
|
||||
const note = application.itemManager.getItems([ContentType.Note])[0]
|
||||
const tag = application.itemManager.getItems([ContentType.Tag])[0]
|
||||
@@ -82,7 +82,7 @@ describe('importing', function () {
|
||||
expect(note.content.references.length).to.equal(0)
|
||||
expect(application.itemManager.itemsReferencingItem(note).length).to.equal(1)
|
||||
|
||||
await application.mutator.importData(
|
||||
await application.importData(
|
||||
{
|
||||
items: [notePayload, tagPayload],
|
||||
},
|
||||
@@ -105,7 +105,7 @@ describe('importing', function () {
|
||||
*/
|
||||
await setup({ fakeCrypto: true })
|
||||
const notePayload = Factory.createNotePayload()
|
||||
await application.itemManager.emitItemFromPayload(notePayload, PayloadEmitSource.LocalChanged)
|
||||
await application.mutator.emitItemFromPayload(notePayload, PayloadEmitSource.LocalChanged)
|
||||
expectedItemCount++
|
||||
const mutatedNote = new DecryptedPayload({
|
||||
...notePayload,
|
||||
@@ -114,7 +114,7 @@ describe('importing', function () {
|
||||
title: `${Math.random()}`,
|
||||
},
|
||||
})
|
||||
await application.mutator.importData(
|
||||
await application.importData(
|
||||
{
|
||||
items: [mutatedNote, mutatedNote, mutatedNote],
|
||||
},
|
||||
@@ -130,7 +130,7 @@ describe('importing', function () {
|
||||
await setup({ fakeCrypto: true })
|
||||
const pair = createRelatedNoteTagPairPayload()
|
||||
const tagPayload = pair[1]
|
||||
await application.itemManager.emitItemsFromPayloads(pair, PayloadEmitSource.LocalChanged)
|
||||
await application.mutator.emitItemsFromPayloads(pair, PayloadEmitSource.LocalChanged)
|
||||
const mutatedTag = new DecryptedPayload({
|
||||
...tagPayload,
|
||||
content: {
|
||||
@@ -138,7 +138,7 @@ describe('importing', function () {
|
||||
references: [],
|
||||
},
|
||||
})
|
||||
await application.mutator.importData(
|
||||
await application.importData(
|
||||
{
|
||||
items: [mutatedTag],
|
||||
},
|
||||
@@ -153,7 +153,7 @@ describe('importing', function () {
|
||||
const pair = createRelatedNoteTagPairPayload()
|
||||
const notePayload = pair[0]
|
||||
const tagPayload = pair[1]
|
||||
await application.itemManager.emitItemsFromPayloads(pair, PayloadEmitSource.LocalChanged)
|
||||
await application.mutator.emitItemsFromPayloads(pair, PayloadEmitSource.LocalChanged)
|
||||
expectedItemCount += 2
|
||||
const note = application.itemManager.getDisplayableNotes()[0]
|
||||
const tag = application.itemManager.getDisplayableTags()[0]
|
||||
@@ -171,7 +171,7 @@ describe('importing', function () {
|
||||
title: `${Math.random()}`,
|
||||
},
|
||||
})
|
||||
await application.mutator.importData(
|
||||
await application.importData(
|
||||
{
|
||||
items: [mutatedNote, mutatedTag],
|
||||
},
|
||||
@@ -217,7 +217,7 @@ describe('importing', function () {
|
||||
const tag = await Factory.createMappedTag(application)
|
||||
expectedItemCount += 2
|
||||
|
||||
await application.itemManager.changeItem(tag, (mutator) => {
|
||||
await application.mutator.changeItem(tag, (mutator) => {
|
||||
mutator.e2ePendingRefactor_addItemAsRelationship(note)
|
||||
})
|
||||
|
||||
@@ -240,7 +240,7 @@ describe('importing', function () {
|
||||
},
|
||||
)
|
||||
|
||||
await application.mutator.importData(
|
||||
await application.importData(
|
||||
{
|
||||
items: [externalNote, externalTag],
|
||||
},
|
||||
@@ -272,12 +272,14 @@ describe('importing', function () {
|
||||
await application.sync.sync({ awaitAll: true })
|
||||
|
||||
await application.mutator.deleteItem(note)
|
||||
await application.sync.sync()
|
||||
expect(application.items.findItem(note.uuid)).to.not.exist
|
||||
|
||||
await application.mutator.deleteItem(tag)
|
||||
await application.sync.sync()
|
||||
expect(application.items.findItem(tag.uuid)).to.not.exist
|
||||
|
||||
await application.mutator.importData(
|
||||
await application.importData(
|
||||
{
|
||||
items: [note, tag],
|
||||
},
|
||||
@@ -311,7 +313,7 @@ describe('importing', function () {
|
||||
password: password,
|
||||
})
|
||||
|
||||
await application.mutator.importData(
|
||||
await application.importData(
|
||||
{
|
||||
items: [note.payload],
|
||||
},
|
||||
@@ -341,7 +343,7 @@ describe('importing', function () {
|
||||
password: password,
|
||||
})
|
||||
|
||||
await application.mutator.importData(
|
||||
await application.importData(
|
||||
{
|
||||
items: [note],
|
||||
},
|
||||
@@ -372,12 +374,14 @@ describe('importing', function () {
|
||||
await application.sync.sync({ awaitAll: true })
|
||||
|
||||
await application.mutator.deleteItem(note)
|
||||
await application.sync.sync()
|
||||
expect(application.items.findItem(note.uuid)).to.not.exist
|
||||
|
||||
await application.mutator.deleteItem(tag)
|
||||
await application.sync.sync()
|
||||
expect(application.items.findItem(tag.uuid)).to.not.exist
|
||||
|
||||
await application.mutator.importData(backupData, true)
|
||||
await application.importData(backupData, true)
|
||||
|
||||
expect(application.itemManager.getDisplayableNotes().length).to.equal(1)
|
||||
expect(application.items.findItem(note.uuid).deleted).to.not.be.ok
|
||||
@@ -402,7 +406,7 @@ describe('importing', function () {
|
||||
application = await Factory.createInitAppWithFakeCrypto()
|
||||
Factory.handlePasswordChallenges(application, password)
|
||||
|
||||
await application.mutator.importData(backupData, true)
|
||||
await application.importData(backupData, true)
|
||||
|
||||
const importedNote = application.items.findItem(note.uuid)
|
||||
const importedTag = application.items.findItem(tag.uuid)
|
||||
@@ -427,7 +431,7 @@ describe('importing', function () {
|
||||
application = await Factory.createInitAppWithFakeCrypto()
|
||||
Factory.handlePasswordChallenges(application, password)
|
||||
|
||||
await application.mutator.importData(backupData, true)
|
||||
await application.importData(backupData, true)
|
||||
|
||||
const importedNote = application.items.findItem(note.uuid)
|
||||
const importedTag = application.items.findItem(tag.uuid)
|
||||
@@ -445,7 +449,7 @@ describe('importing', function () {
|
||||
version: oldVersion,
|
||||
})
|
||||
|
||||
const noteItem = await application.itemManager.createItem(ContentType.Note, {
|
||||
const noteItem = await application.mutator.createItem(ContentType.Note, {
|
||||
title: 'Encrypted note',
|
||||
text: 'On protocol version 003.',
|
||||
})
|
||||
@@ -456,7 +460,7 @@ describe('importing', function () {
|
||||
application = await Factory.createInitAppWithFakeCrypto()
|
||||
Factory.handlePasswordChallenges(application, password)
|
||||
|
||||
const result = await application.mutator.importData(backupData, true)
|
||||
const result = await application.importData(backupData, true)
|
||||
expect(result).to.not.be.undefined
|
||||
expect(result.affectedItems.length).to.be.eq(backupData.items.length)
|
||||
expect(result.errorCount).to.be.eq(0)
|
||||
@@ -512,7 +516,7 @@ describe('importing', function () {
|
||||
application = await Factory.createInitAppWithRealCrypto()
|
||||
Factory.handlePasswordChallenges(application, password)
|
||||
|
||||
const result = await application.mutator.importData(backupData, true)
|
||||
const result = await application.importData(backupData, true)
|
||||
expect(result).to.not.be.undefined
|
||||
expect(result.affectedItems.length).to.be.eq(backupData.items.length)
|
||||
expect(result.errorCount).to.be.eq(0)
|
||||
@@ -526,7 +530,7 @@ describe('importing', function () {
|
||||
password: password,
|
||||
})
|
||||
|
||||
const noteItem = await application.itemManager.createItem(ContentType.Note, {
|
||||
const noteItem = await application.mutator.createItem(ContentType.Note, {
|
||||
title: 'Encrypted note',
|
||||
text: 'On protocol version 004.',
|
||||
})
|
||||
@@ -537,7 +541,7 @@ describe('importing', function () {
|
||||
application = await Factory.createInitAppWithFakeCrypto()
|
||||
Factory.handlePasswordChallenges(application, password)
|
||||
|
||||
const result = await application.mutator.importData(backupData, true)
|
||||
const result = await application.importData(backupData, true)
|
||||
expect(result).to.not.be.undefined
|
||||
expect(result.affectedItems.length).to.be.eq(backupData.items.length)
|
||||
expect(result.errorCount).to.be.eq(0)
|
||||
@@ -556,7 +560,7 @@ describe('importing', function () {
|
||||
password: password,
|
||||
})
|
||||
|
||||
const noteItem = await application.itemManager.createItem(ContentType.Note, {
|
||||
const noteItem = await application.mutator.createItem(ContentType.Note, {
|
||||
title: 'This is a valid, encrypted note',
|
||||
text: 'On protocol version 004.',
|
||||
})
|
||||
@@ -577,7 +581,7 @@ describe('importing', function () {
|
||||
|
||||
backupData.items = [...backupData.items, madeUpPayload]
|
||||
|
||||
const result = await application.mutator.importData(backupData, true)
|
||||
const result = await application.importData(backupData, true)
|
||||
expect(result).to.not.be.undefined
|
||||
expect(result.affectedItems.length).to.be.eq(backupData.items.length - 1)
|
||||
expect(result.errorCount).to.be.eq(1)
|
||||
@@ -594,7 +598,7 @@ describe('importing', function () {
|
||||
version: oldVersion,
|
||||
})
|
||||
|
||||
await application.itemManager.createItem(ContentType.Note, {
|
||||
await application.mutator.createItem(ContentType.Note, {
|
||||
title: 'Encrypted note',
|
||||
text: 'On protocol version 003.',
|
||||
})
|
||||
@@ -615,7 +619,7 @@ describe('importing', function () {
|
||||
},
|
||||
})
|
||||
|
||||
const result = await application.mutator.importData(backupData, true)
|
||||
const result = await application.importData(backupData, true)
|
||||
expect(result).to.not.be.undefined
|
||||
|
||||
expect(result.affectedItems.length).to.be.eq(0)
|
||||
@@ -631,7 +635,7 @@ describe('importing', function () {
|
||||
password: password,
|
||||
})
|
||||
|
||||
await application.itemManager.createItem(ContentType.Note, {
|
||||
await application.mutator.createItem(ContentType.Note, {
|
||||
title: 'This is a valid, encrypted note',
|
||||
text: 'On protocol version 004.',
|
||||
})
|
||||
@@ -647,7 +651,7 @@ describe('importing', function () {
|
||||
},
|
||||
})
|
||||
|
||||
const result = await application.mutator.importData(backupData, true)
|
||||
const result = await application.importData(backupData, true)
|
||||
expect(result).to.not.be.undefined
|
||||
expect(result.affectedItems.length).to.be.eq(0)
|
||||
expect(result.errorCount).to.be.eq(backupData.items.length)
|
||||
@@ -662,7 +666,7 @@ describe('importing', function () {
|
||||
password: password,
|
||||
})
|
||||
|
||||
await application.itemManager.createItem(ContentType.Note, {
|
||||
await application.mutator.createItem(ContentType.Note, {
|
||||
title: 'Encrypted note',
|
||||
text: 'On protocol version 004.',
|
||||
})
|
||||
@@ -673,7 +677,7 @@ describe('importing', function () {
|
||||
await Factory.safeDeinit(application)
|
||||
application = await Factory.createInitAppWithFakeCrypto()
|
||||
|
||||
const result = await application.mutator.importData(backupData)
|
||||
const result = await application.importData(backupData)
|
||||
|
||||
expect(result.error).to.be.ok
|
||||
})
|
||||
@@ -687,7 +691,7 @@ describe('importing', function () {
|
||||
})
|
||||
Factory.handlePasswordChallenges(application, password)
|
||||
|
||||
await application.itemManager.createItem(ContentType.Note, {
|
||||
await application.mutator.createItem(ContentType.Note, {
|
||||
title: 'Encrypted note',
|
||||
text: 'On protocol version 004.',
|
||||
})
|
||||
@@ -699,11 +703,13 @@ describe('importing', function () {
|
||||
application = await Factory.createInitAppWithFakeCrypto()
|
||||
Factory.handlePasswordChallenges(application, password)
|
||||
|
||||
const result = await application.mutator.importData(backupData, true)
|
||||
const result = await application.importData(backupData, true)
|
||||
|
||||
expect(result).to.not.be.undefined
|
||||
expect(result.affectedItems.length).to.be.eq(0)
|
||||
expect(result.errorCount).to.be.eq(backupData.items.length)
|
||||
|
||||
expect(result.affectedItems.length).to.equal(BaseItemCounts.BackupFileRootKeyEncryptedItems)
|
||||
|
||||
expect(result.errorCount).to.be.eq(backupData.items.length - BaseItemCounts.BackupFileRootKeyEncryptedItems)
|
||||
expect(application.itemManager.getDisplayableNotes().length).to.equal(0)
|
||||
})
|
||||
|
||||
@@ -784,7 +790,14 @@ describe('importing', function () {
|
||||
|
||||
await application.prepareForLaunch({
|
||||
receiveChallenge: (challenge) => {
|
||||
if (challenge.prompts.length === 2) {
|
||||
if (challenge.reason === ChallengeReason.Custom) {
|
||||
return
|
||||
}
|
||||
|
||||
if (
|
||||
challenge.reason === ChallengeReason.DecryptEncryptedFile ||
|
||||
challenge.reason === ChallengeReason.ImportFile
|
||||
) {
|
||||
application.submitValuesForChallenge(
|
||||
challenge,
|
||||
challenge.prompts.map((prompt) =>
|
||||
@@ -796,9 +809,6 @@ describe('importing', function () {
|
||||
),
|
||||
),
|
||||
)
|
||||
} else {
|
||||
const prompt = challenge.prompts[0]
|
||||
application.submitValuesForChallenge(challenge, [CreateChallengeValue(prompt, password)])
|
||||
}
|
||||
},
|
||||
})
|
||||
@@ -827,7 +837,7 @@ describe('importing', function () {
|
||||
},
|
||||
}
|
||||
|
||||
const result = await application.mutator.importData(backupFile, false)
|
||||
const result = await application.importData(backupFile, false)
|
||||
expect(result.errorCount).to.equal(0)
|
||||
await Factory.safeDeinit(application)
|
||||
})
|
||||
@@ -846,7 +856,7 @@ describe('importing', function () {
|
||||
Factory.handlePasswordChallenges(application, password)
|
||||
|
||||
const pair = createRelatedNoteTagPairPayload()
|
||||
await application.itemManager.emitItemsFromPayloads(pair, PayloadEmitSource.LocalChanged)
|
||||
await application.mutator.emitItemsFromPayloads(pair, PayloadEmitSource.LocalChanged)
|
||||
|
||||
await application.sync.sync()
|
||||
|
||||
@@ -862,7 +872,7 @@ describe('importing', function () {
|
||||
password: password,
|
||||
})
|
||||
|
||||
await application.mutator.importData(backupData, true)
|
||||
await application.importData(backupData, true)
|
||||
|
||||
expect(application.itemManager.getDisplayableNotes().length).to.equal(1)
|
||||
expect(application.itemManager.getDisplayableTags().length).to.equal(1)
|
||||
@@ -872,4 +882,8 @@ describe('importing', function () {
|
||||
expect(application.itemManager.referencesForItem(importedTag).length).to.equal(1)
|
||||
expect(application.itemManager.itemsReferencingItem(importedNote).length).to.equal(1)
|
||||
})
|
||||
|
||||
it('should decrypt backup file which contains a vaulted note without a synced key system root key', async () => {
|
||||
console.error('TODO: Implement this test')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* eslint-disable no-unused-expressions */
|
||||
/* eslint-disable no-undef */
|
||||
import { BaseItemCounts } from '../lib/Applications.js'
|
||||
import { BaseItemCounts } from '../lib/BaseItemCounts.js'
|
||||
import * as Factory from '../lib/factory.js'
|
||||
chai.use(chaiAsPromised)
|
||||
const expect = chai.expect
|
||||
@@ -22,11 +22,11 @@ describe('items', () => {
|
||||
|
||||
it('setting an item as dirty should update its client updated at', async function () {
|
||||
const params = Factory.createNotePayload()
|
||||
await this.application.itemManager.emitItemsFromPayloads([params], PayloadEmitSource.LocalChanged)
|
||||
await this.application.mutator.emitItemsFromPayloads([params], PayloadEmitSource.LocalChanged)
|
||||
const item = this.application.itemManager.items[0]
|
||||
const prevDate = item.userModifiedDate.getTime()
|
||||
await Factory.sleep(0.1)
|
||||
await this.application.itemManager.setItemDirty(item, true)
|
||||
await this.application.mutator.setItemDirty(item, true)
|
||||
const refreshedItem = this.application.itemManager.findItem(item.uuid)
|
||||
const newDate = refreshedItem.userModifiedDate.getTime()
|
||||
expect(prevDate).to.not.equal(newDate)
|
||||
@@ -34,23 +34,23 @@ describe('items', () => {
|
||||
|
||||
it('setting an item as dirty with option to skip client updated at', async function () {
|
||||
const params = Factory.createNotePayload()
|
||||
await this.application.itemManager.emitItemsFromPayloads([params], PayloadEmitSource.LocalChanged)
|
||||
await this.application.mutator.emitItemsFromPayloads([params], PayloadEmitSource.LocalChanged)
|
||||
const item = this.application.itemManager.items[0]
|
||||
const prevDate = item.userModifiedDate.getTime()
|
||||
await Factory.sleep(0.1)
|
||||
await this.application.itemManager.setItemDirty(item)
|
||||
await this.application.mutator.setItemDirty(item)
|
||||
const newDate = item.userModifiedDate.getTime()
|
||||
expect(prevDate).to.equal(newDate)
|
||||
})
|
||||
|
||||
it('properly pins, archives, and locks', async function () {
|
||||
const params = Factory.createNotePayload()
|
||||
await this.application.itemManager.emitItemsFromPayloads([params], PayloadEmitSource.LocalChanged)
|
||||
await this.application.mutator.emitItemsFromPayloads([params], PayloadEmitSource.LocalChanged)
|
||||
|
||||
const item = this.application.itemManager.items[0]
|
||||
expect(item.pinned).to.not.be.ok
|
||||
|
||||
const refreshedItem = await this.application.mutator.changeAndSaveItem(
|
||||
const refreshedItem = await this.application.changeAndSaveItem(
|
||||
item,
|
||||
(mutator) => {
|
||||
mutator.pinned = true
|
||||
@@ -69,7 +69,7 @@ describe('items', () => {
|
||||
it('properly compares item equality', async function () {
|
||||
const params1 = Factory.createNotePayload()
|
||||
const params2 = Factory.createNotePayload()
|
||||
await this.application.itemManager.emitItemsFromPayloads([params1, params2], PayloadEmitSource.LocalChanged)
|
||||
await this.application.mutator.emitItemsFromPayloads([params1, params2], PayloadEmitSource.LocalChanged)
|
||||
|
||||
let item1 = this.application.itemManager.getDisplayableNotes()[0]
|
||||
let item2 = this.application.itemManager.getDisplayableNotes()[1]
|
||||
@@ -77,7 +77,7 @@ describe('items', () => {
|
||||
expect(item1.isItemContentEqualWith(item2)).to.equal(true)
|
||||
|
||||
// items should ignore this field when checking for equality
|
||||
item1 = await this.application.mutator.changeAndSaveItem(
|
||||
item1 = await this.application.changeAndSaveItem(
|
||||
item1,
|
||||
(mutator) => {
|
||||
mutator.userModifiedDate = new Date()
|
||||
@@ -86,7 +86,7 @@ describe('items', () => {
|
||||
undefined,
|
||||
syncOptions,
|
||||
)
|
||||
item2 = await this.application.mutator.changeAndSaveItem(
|
||||
item2 = await this.application.changeAndSaveItem(
|
||||
item2,
|
||||
(mutator) => {
|
||||
mutator.userModifiedDate = undefined
|
||||
@@ -98,7 +98,7 @@ describe('items', () => {
|
||||
|
||||
expect(item1.isItemContentEqualWith(item2)).to.equal(true)
|
||||
|
||||
item1 = await this.application.mutator.changeAndSaveItem(
|
||||
item1 = await this.application.changeAndSaveItem(
|
||||
item1,
|
||||
(mutator) => {
|
||||
mutator.mutableContent.foo = 'bar'
|
||||
@@ -110,7 +110,7 @@ describe('items', () => {
|
||||
|
||||
expect(item1.isItemContentEqualWith(item2)).to.equal(false)
|
||||
|
||||
item2 = await this.application.mutator.changeAndSaveItem(
|
||||
item2 = await this.application.changeAndSaveItem(
|
||||
item2,
|
||||
(mutator) => {
|
||||
mutator.mutableContent.foo = 'bar'
|
||||
@@ -123,7 +123,7 @@ describe('items', () => {
|
||||
expect(item1.isItemContentEqualWith(item2)).to.equal(true)
|
||||
expect(item2.isItemContentEqualWith(item1)).to.equal(true)
|
||||
|
||||
item1 = await this.application.mutator.changeAndSaveItem(
|
||||
item1 = await this.application.changeAndSaveItem(
|
||||
item1,
|
||||
(mutator) => {
|
||||
mutator.e2ePendingRefactor_addItemAsRelationship(item2)
|
||||
@@ -132,7 +132,7 @@ describe('items', () => {
|
||||
undefined,
|
||||
syncOptions,
|
||||
)
|
||||
item2 = await this.application.mutator.changeAndSaveItem(
|
||||
item2 = await this.application.changeAndSaveItem(
|
||||
item2,
|
||||
(mutator) => {
|
||||
mutator.e2ePendingRefactor_addItemAsRelationship(item1)
|
||||
@@ -147,7 +147,7 @@ describe('items', () => {
|
||||
|
||||
expect(item1.isItemContentEqualWith(item2)).to.equal(false)
|
||||
|
||||
item1 = await this.application.mutator.changeAndSaveItem(
|
||||
item1 = await this.application.changeAndSaveItem(
|
||||
item1,
|
||||
(mutator) => {
|
||||
mutator.removeItemAsRelationship(item2)
|
||||
@@ -156,7 +156,7 @@ describe('items', () => {
|
||||
undefined,
|
||||
syncOptions,
|
||||
)
|
||||
item2 = await this.application.mutator.changeAndSaveItem(
|
||||
item2 = await this.application.changeAndSaveItem(
|
||||
item2,
|
||||
(mutator) => {
|
||||
mutator.removeItemAsRelationship(item1)
|
||||
@@ -174,12 +174,12 @@ describe('items', () => {
|
||||
it('content equality should not have side effects', async function () {
|
||||
const params1 = Factory.createNotePayload()
|
||||
const params2 = Factory.createNotePayload()
|
||||
await this.application.itemManager.emitItemsFromPayloads([params1, params2], PayloadEmitSource.LocalChanged)
|
||||
await this.application.mutator.emitItemsFromPayloads([params1, params2], PayloadEmitSource.LocalChanged)
|
||||
|
||||
let item1 = this.application.itemManager.getDisplayableNotes()[0]
|
||||
const item2 = this.application.itemManager.getDisplayableNotes()[1]
|
||||
|
||||
item1 = await this.application.mutator.changeAndSaveItem(
|
||||
item1 = await this.application.changeAndSaveItem(
|
||||
item1,
|
||||
(mutator) => {
|
||||
mutator.mutableContent.foo = 'bar'
|
||||
@@ -203,7 +203,7 @@ describe('items', () => {
|
||||
// There was an issue where calling that function would modify values directly to omit keys
|
||||
// in contentKeysToIgnoreWhenCheckingEquality.
|
||||
|
||||
await this.application.itemManager.setItemsDirty([item1, item2])
|
||||
await this.application.mutator.setItemsDirty([item1, item2])
|
||||
|
||||
expect(item1.userModifiedDate).to.be.ok
|
||||
expect(item2.userModifiedDate).to.be.ok
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* eslint-disable no-unused-expressions */
|
||||
/* eslint-disable no-undef */
|
||||
import { BaseItemCounts } from '../lib/Applications.js'
|
||||
import { BaseItemCounts } from '../lib/BaseItemCounts.js'
|
||||
import * as Factory from '../lib/factory.js'
|
||||
import { createNoteParams } from '../lib/Items.js'
|
||||
chai.use(chaiAsPromised)
|
||||
@@ -20,7 +20,7 @@ describe('model manager mapping', () => {
|
||||
|
||||
it('mapping nonexistent item creates it', async function () {
|
||||
const payload = Factory.createNotePayload()
|
||||
await this.application.itemManager.emitItemsFromPayloads([payload], PayloadEmitSource.LocalChanged)
|
||||
await this.application.mutator.emitItemsFromPayloads([payload], PayloadEmitSource.LocalChanged)
|
||||
this.expectedItemCount++
|
||||
expect(this.application.itemManager.items.length).to.equal(this.expectedItemCount)
|
||||
})
|
||||
@@ -31,13 +31,13 @@ describe('model manager mapping', () => {
|
||||
dirty: false,
|
||||
deleted: true,
|
||||
})
|
||||
await this.application.itemManager.emitItemFromPayload(payload, PayloadEmitSource.LocalChanged)
|
||||
await this.application.payloadManager.emitPayload(payload, PayloadEmitSource.LocalChanged)
|
||||
expect(this.application.itemManager.items.length).to.equal(this.expectedItemCount)
|
||||
})
|
||||
|
||||
it('mapping and deleting nonexistent item creates and deletes it', async function () {
|
||||
const payload = Factory.createNotePayload()
|
||||
await this.application.itemManager.emitItemsFromPayloads([payload], PayloadEmitSource.LocalChanged)
|
||||
await this.application.mutator.emitItemsFromPayloads([payload], PayloadEmitSource.LocalChanged)
|
||||
|
||||
this.expectedItemCount++
|
||||
|
||||
@@ -51,7 +51,7 @@ describe('model manager mapping', () => {
|
||||
|
||||
this.expectedItemCount--
|
||||
|
||||
await this.application.itemManager.emitItemsFromPayloads([changedParams], PayloadEmitSource.LocalChanged)
|
||||
await this.application.mutator.emitItemsFromPayloads([changedParams], PayloadEmitSource.LocalChanged)
|
||||
|
||||
expect(this.application.itemManager.items.length).to.equal(this.expectedItemCount)
|
||||
})
|
||||
@@ -59,22 +59,22 @@ describe('model manager mapping', () => {
|
||||
it('mapping deleted but dirty item should not delete it', async function () {
|
||||
const payload = Factory.createNotePayload()
|
||||
|
||||
const [item] = await this.application.itemManager.emitItemsFromPayloads([payload], PayloadEmitSource.LocalChanged)
|
||||
const [item] = await this.application.mutator.emitItemsFromPayloads([payload], PayloadEmitSource.LocalChanged)
|
||||
|
||||
this.expectedItemCount++
|
||||
|
||||
await this.application.itemManager.emitItemFromPayload(new DeleteItemMutator(item).getDeletedResult())
|
||||
await this.application.payloadManager.emitPayload(new DeleteItemMutator(item).getDeletedResult())
|
||||
|
||||
const payload2 = new DeletedPayload(this.application.payloadManager.findOne(payload.uuid).ejected())
|
||||
|
||||
await this.application.itemManager.emitItemsFromPayloads([payload2], PayloadEmitSource.LocalChanged)
|
||||
await this.application.payloadManager.emitPayloads([payload2], PayloadEmitSource.LocalChanged)
|
||||
|
||||
expect(this.application.payloadManager.collection.all().length).to.equal(this.expectedItemCount)
|
||||
})
|
||||
|
||||
it('mapping existing item updates its properties', async function () {
|
||||
const payload = Factory.createNotePayload()
|
||||
await this.application.itemManager.emitItemsFromPayloads([payload], PayloadEmitSource.LocalChanged)
|
||||
await this.application.mutator.emitItemsFromPayloads([payload], PayloadEmitSource.LocalChanged)
|
||||
|
||||
const newTitle = 'updated title'
|
||||
const mutated = new DecryptedPayload({
|
||||
@@ -84,7 +84,7 @@ describe('model manager mapping', () => {
|
||||
title: newTitle,
|
||||
},
|
||||
})
|
||||
await this.application.itemManager.emitItemsFromPayloads([mutated], PayloadEmitSource.LocalChanged)
|
||||
await this.application.mutator.emitItemsFromPayloads([mutated], PayloadEmitSource.LocalChanged)
|
||||
const item = this.application.itemManager.getDisplayableNotes()[0]
|
||||
|
||||
expect(item.content.title).to.equal(newTitle)
|
||||
@@ -92,9 +92,9 @@ describe('model manager mapping', () => {
|
||||
|
||||
it('setting an item dirty should retrieve it in dirty items', async function () {
|
||||
const payload = Factory.createNotePayload()
|
||||
await this.application.itemManager.emitItemsFromPayloads([payload], PayloadEmitSource.LocalChanged)
|
||||
await this.application.mutator.emitItemsFromPayloads([payload], PayloadEmitSource.LocalChanged)
|
||||
const note = this.application.itemManager.getDisplayableNotes()[0]
|
||||
await this.application.itemManager.setItemDirty(note)
|
||||
await this.application.mutator.setItemDirty(note)
|
||||
const dirtyItems = this.application.itemManager.getDirtyItems()
|
||||
expect(Uuids(dirtyItems).includes(note.uuid))
|
||||
})
|
||||
@@ -106,7 +106,7 @@ describe('model manager mapping', () => {
|
||||
for (let i = 0; i < count; i++) {
|
||||
payloads.push(Factory.createNotePayload())
|
||||
}
|
||||
await this.application.itemManager.emitItemsFromPayloads(payloads, PayloadEmitSource.LocalChanged)
|
||||
await this.application.mutator.emitItemsFromPayloads(payloads, PayloadEmitSource.LocalChanged)
|
||||
await this.application.syncService.markAllItemsAsNeedingSyncAndPersist()
|
||||
|
||||
const dirtyItems = this.application.itemManager.getDirtyItems()
|
||||
@@ -115,14 +115,14 @@ describe('model manager mapping', () => {
|
||||
|
||||
it('sync observers should be notified of changes', async function () {
|
||||
const payload = Factory.createNotePayload()
|
||||
await this.application.itemManager.emitItemsFromPayloads([payload], PayloadEmitSource.LocalChanged)
|
||||
await this.application.mutator.emitItemsFromPayloads([payload], PayloadEmitSource.LocalChanged)
|
||||
const item = this.application.itemManager.items[0]
|
||||
return new Promise((resolve) => {
|
||||
this.application.itemManager.addObserver(ContentType.Any, ({ changed }) => {
|
||||
expect(changed[0].uuid === item.uuid)
|
||||
resolve()
|
||||
})
|
||||
this.application.itemManager.emitItemsFromPayloads([payload], PayloadEmitSource.LocalChanged)
|
||||
this.application.mutator.emitItemsFromPayloads([payload], PayloadEmitSource.LocalChanged)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import * as Factory from '../lib/factory.js'
|
||||
import * as Utils from '../lib/Utils.js'
|
||||
import { createRelatedNoteTagPairPayload } from '../lib/Items.js'
|
||||
import { BaseItemCounts } from '../lib/Applications.js'
|
||||
import { BaseItemCounts } from '../lib/BaseItemCounts.js'
|
||||
chai.use(chaiAsPromised)
|
||||
const expect = chai.expect
|
||||
|
||||
@@ -25,7 +25,7 @@ describe('notes and tags', () => {
|
||||
|
||||
it('uses proper class for note', async function () {
|
||||
const payload = Factory.createNotePayload()
|
||||
await this.application.itemManager.emitItemFromPayload(payload, PayloadEmitSource.LocalChanged)
|
||||
await this.application.mutator.emitItemFromPayload(payload, PayloadEmitSource.LocalChanged)
|
||||
const note = this.application.itemManager.getItems([ContentType.Note])[0]
|
||||
expect(note.constructor === SNNote).to.equal(true)
|
||||
})
|
||||
@@ -33,7 +33,7 @@ describe('notes and tags', () => {
|
||||
it('properly constructs syncing params', async function () {
|
||||
const title = 'Foo'
|
||||
const text = 'Bar'
|
||||
const note = await this.application.mutator.createTemplateItem(ContentType.Note, {
|
||||
const note = await this.application.items.createTemplateItem(ContentType.Note, {
|
||||
title,
|
||||
text,
|
||||
})
|
||||
@@ -41,7 +41,7 @@ describe('notes and tags', () => {
|
||||
expect(note.content.title).to.equal(title)
|
||||
expect(note.content.text).to.equal(text)
|
||||
|
||||
const tag = await this.application.mutator.createTemplateItem(ContentType.Tag, {
|
||||
const tag = await this.application.items.createTemplateItem(ContentType.Tag, {
|
||||
title,
|
||||
})
|
||||
|
||||
@@ -73,7 +73,7 @@ describe('notes and tags', () => {
|
||||
},
|
||||
})
|
||||
|
||||
await this.application.itemManager.emitItemsFromPayloads([mutatedNote, mutatedTag], PayloadEmitSource.LocalChanged)
|
||||
await this.application.mutator.emitItemsFromPayloads([mutatedNote, mutatedTag], PayloadEmitSource.LocalChanged)
|
||||
const note = this.application.itemManager.getItems([ContentType.Note])[0]
|
||||
const tag = this.application.itemManager.getItems([ContentType.Tag])[0]
|
||||
|
||||
@@ -89,7 +89,7 @@ describe('notes and tags', () => {
|
||||
expect(notePayload.content.references.length).to.equal(0)
|
||||
expect(tagPayload.content.references.length).to.equal(1)
|
||||
|
||||
await this.application.itemManager.emitItemsFromPayloads([notePayload, tagPayload], PayloadEmitSource.LocalChanged)
|
||||
await this.application.mutator.emitItemsFromPayloads([notePayload, tagPayload], PayloadEmitSource.LocalChanged)
|
||||
let note = this.application.itemManager.getDisplayableNotes()[0]
|
||||
let tag = this.application.itemManager.getDisplayableTags()[0]
|
||||
|
||||
@@ -106,7 +106,7 @@ describe('notes and tags', () => {
|
||||
expect(note.payload.references.length).to.equal(0)
|
||||
expect(tag.noteCount).to.equal(1)
|
||||
|
||||
await this.application.itemManager.setItemToBeDeleted(note)
|
||||
await this.application.mutator.setItemToBeDeleted(note)
|
||||
|
||||
tag = this.application.itemManager.getDisplayableTags()[0]
|
||||
|
||||
@@ -130,7 +130,7 @@ describe('notes and tags', () => {
|
||||
const notePayload = pair[0]
|
||||
const tagPayload = pair[1]
|
||||
|
||||
await this.application.itemManager.emitItemsFromPayloads(pair, PayloadEmitSource.LocalChanged)
|
||||
await this.application.mutator.emitItemsFromPayloads(pair, PayloadEmitSource.LocalChanged)
|
||||
let note = this.application.itemManager.getItems([ContentType.Note])[0]
|
||||
let tag = this.application.itemManager.getItems([ContentType.Tag])[0]
|
||||
|
||||
@@ -147,7 +147,7 @@ describe('notes and tags', () => {
|
||||
references: [],
|
||||
},
|
||||
})
|
||||
await this.application.itemManager.emitItemsFromPayloads([mutatedTag], PayloadEmitSource.LocalChanged)
|
||||
await this.application.mutator.emitItemsFromPayloads([mutatedTag], PayloadEmitSource.LocalChanged)
|
||||
|
||||
note = this.application.itemManager.findItem(note.uuid)
|
||||
tag = this.application.itemManager.findItem(tag.uuid)
|
||||
@@ -177,14 +177,14 @@ describe('notes and tags', () => {
|
||||
const notePayload = pair[0]
|
||||
const tagPayload = pair[1]
|
||||
|
||||
await this.application.itemManager.emitItemsFromPayloads([notePayload, tagPayload], PayloadEmitSource.LocalChanged)
|
||||
await this.application.mutator.emitItemsFromPayloads([notePayload, tagPayload], PayloadEmitSource.LocalChanged)
|
||||
const note = this.application.itemManager.getItems([ContentType.Note])[0]
|
||||
let tag = this.application.itemManager.getItems([ContentType.Tag])[0]
|
||||
|
||||
expect(note.content.references.length).to.equal(0)
|
||||
expect(tag.content.references.length).to.equal(1)
|
||||
|
||||
tag = await this.application.mutator.changeAndSaveItem(
|
||||
tag = await this.application.changeAndSaveItem(
|
||||
tag,
|
||||
(mutator) => {
|
||||
mutator.removeItemAsRelationship(note)
|
||||
@@ -200,11 +200,11 @@ describe('notes and tags', () => {
|
||||
|
||||
it('properly handles tag duplication', async function () {
|
||||
const pair = createRelatedNoteTagPairPayload()
|
||||
await this.application.itemManager.emitItemsFromPayloads(pair, PayloadEmitSource.LocalChanged)
|
||||
await this.application.mutator.emitItemsFromPayloads(pair, PayloadEmitSource.LocalChanged)
|
||||
let note = this.application.itemManager.getDisplayableNotes()[0]
|
||||
let tag = this.application.itemManager.getDisplayableTags()[0]
|
||||
|
||||
const duplicateTag = await this.application.itemManager.duplicateItem(tag, true)
|
||||
const duplicateTag = await this.application.mutator.duplicateItem(tag, true)
|
||||
await this.application.syncService.sync(syncOptions)
|
||||
|
||||
note = this.application.itemManager.findItem(note.uuid)
|
||||
@@ -232,9 +232,9 @@ describe('notes and tags', () => {
|
||||
const pair = createRelatedNoteTagPairPayload()
|
||||
const notePayload = pair[0]
|
||||
const tagPayload = pair[1]
|
||||
await this.application.itemManager.emitItemsFromPayloads([notePayload, tagPayload], PayloadEmitSource.LocalChanged)
|
||||
await this.application.mutator.emitItemsFromPayloads([notePayload, tagPayload], PayloadEmitSource.LocalChanged)
|
||||
const note = this.application.itemManager.getItems([ContentType.Note])[0]
|
||||
const duplicateNote = await this.application.itemManager.duplicateItem(note, true)
|
||||
const duplicateNote = await this.application.mutator.duplicateItem(note, true)
|
||||
expect(note.uuid).to.not.equal(duplicateNote.uuid)
|
||||
|
||||
expect(this.application.itemManager.itemsReferencingItem(duplicateNote).length).to.equal(
|
||||
@@ -246,7 +246,7 @@ describe('notes and tags', () => {
|
||||
const pair = createRelatedNoteTagPairPayload()
|
||||
const notePayload = pair[0]
|
||||
const tagPayload = pair[1]
|
||||
await this.application.itemManager.emitItemsFromPayloads([notePayload, tagPayload], PayloadEmitSource.LocalChanged)
|
||||
await this.application.mutator.emitItemsFromPayloads([notePayload, tagPayload], PayloadEmitSource.LocalChanged)
|
||||
const note = this.application.itemManager.getItems([ContentType.Note])[0]
|
||||
let tag = this.application.itemManager.getItems([ContentType.Tag])[0]
|
||||
|
||||
@@ -256,16 +256,16 @@ describe('notes and tags', () => {
|
||||
expect(note.content.references.length).to.equal(0)
|
||||
expect(this.application.itemManager.itemsReferencingItem(note).length).to.equal(1)
|
||||
|
||||
await this.application.itemManager.setItemToBeDeleted(tag)
|
||||
await this.application.mutator.setItemToBeDeleted(tag)
|
||||
tag = this.application.itemManager.findItem(tag.uuid)
|
||||
expect(tag).to.not.be.ok
|
||||
})
|
||||
|
||||
it('modifying item content should not modify payload content', async function () {
|
||||
const notePayload = Factory.createNotePayload()
|
||||
await this.application.itemManager.emitItemsFromPayloads([notePayload], PayloadEmitSource.LocalChanged)
|
||||
await this.application.mutator.emitItemsFromPayloads([notePayload], PayloadEmitSource.LocalChanged)
|
||||
let note = this.application.itemManager.getItems([ContentType.Note])[0]
|
||||
note = await this.application.mutator.changeAndSaveItem(
|
||||
note = await this.application.changeAndSaveItem(
|
||||
note,
|
||||
(mutator) => {
|
||||
mutator.mutableContent.title = Math.random()
|
||||
@@ -285,12 +285,12 @@ describe('notes and tags', () => {
|
||||
const notePayload = pair[0]
|
||||
const tagPayload = pair[1]
|
||||
|
||||
await this.application.itemManager.emitItemsFromPayloads([notePayload, tagPayload], PayloadEmitSource.LocalChanged)
|
||||
await this.application.mutator.emitItemsFromPayloads([notePayload, tagPayload], PayloadEmitSource.LocalChanged)
|
||||
let note = this.application.itemManager.getItems([ContentType.Note])[0]
|
||||
let tag = this.application.itemManager.getItems([ContentType.Tag])[0]
|
||||
|
||||
await this.application.syncService.sync(syncOptions)
|
||||
await this.application.itemManager.setItemToBeDeleted(tag)
|
||||
await this.application.mutator.setItemToBeDeleted(tag)
|
||||
|
||||
note = this.application.itemManager.findItem(note.uuid)
|
||||
this.application.itemManager.findItem(tag.uuid)
|
||||
@@ -302,7 +302,7 @@ describe('notes and tags', () => {
|
||||
await Promise.all(
|
||||
['Y', 'Z', 'A', 'B'].map(async (title) => {
|
||||
return this.application.mutator.insertItem(
|
||||
await this.application.mutator.createTemplateItem(ContentType.Note, { title }),
|
||||
await this.application.items.createTemplateItem(ContentType.Note, { title }),
|
||||
)
|
||||
}),
|
||||
)
|
||||
@@ -316,7 +316,7 @@ describe('notes and tags', () => {
|
||||
})
|
||||
|
||||
it('setting a note dirty should collapse its properties into content', async function () {
|
||||
let note = await this.application.mutator.createTemplateItem(ContentType.Note, {
|
||||
let note = await this.application.items.createTemplateItem(ContentType.Note, {
|
||||
title: 'Foo',
|
||||
})
|
||||
await this.application.mutator.insertItem(note)
|
||||
@@ -339,7 +339,7 @@ describe('notes and tags', () => {
|
||||
mutator.e2ePendingRefactor_addItemAsRelationship(taggedNote)
|
||||
})
|
||||
await this.application.mutator.insertItem(
|
||||
await this.application.mutator.createTemplateItem(ContentType.Note, {
|
||||
await this.application.items.createTemplateItem(ContentType.Note, {
|
||||
title: 'A',
|
||||
}),
|
||||
)
|
||||
@@ -379,7 +379,7 @@ describe('notes and tags', () => {
|
||||
await Promise.all(
|
||||
['Y', 'Z', 'A', 'B'].map(async (title) => {
|
||||
return this.application.mutator.insertItem(
|
||||
await this.application.mutator.createTemplateItem(ContentType.Note, {
|
||||
await this.application.items.createTemplateItem(ContentType.Note, {
|
||||
title,
|
||||
}),
|
||||
)
|
||||
@@ -413,17 +413,17 @@ describe('notes and tags', () => {
|
||||
describe('Smart views', function () {
|
||||
it('"title", "startsWith", "Foo"', async function () {
|
||||
const note = await this.application.mutator.insertItem(
|
||||
await this.application.mutator.createTemplateItem(ContentType.Note, {
|
||||
await this.application.items.createTemplateItem(ContentType.Note, {
|
||||
title: 'Foo 🎲',
|
||||
}),
|
||||
)
|
||||
await this.application.mutator.insertItem(
|
||||
await this.application.mutator.createTemplateItem(ContentType.Note, {
|
||||
await this.application.items.createTemplateItem(ContentType.Note, {
|
||||
title: 'Not Foo 🎲',
|
||||
}),
|
||||
)
|
||||
const view = await this.application.mutator.insertItem(
|
||||
await this.application.mutator.createTemplateItem(ContentType.SmartView, {
|
||||
await this.application.items.createTemplateItem(ContentType.SmartView, {
|
||||
title: 'Foo Notes',
|
||||
predicate: {
|
||||
keypath: 'title',
|
||||
@@ -447,7 +447,7 @@ describe('notes and tags', () => {
|
||||
|
||||
it('"pinned", "=", true', async function () {
|
||||
const note = await this.application.mutator.insertItem(
|
||||
await this.application.mutator.createTemplateItem(ContentType.Note, {
|
||||
await this.application.items.createTemplateItem(ContentType.Note, {
|
||||
title: 'A',
|
||||
}),
|
||||
)
|
||||
@@ -455,13 +455,13 @@ describe('notes and tags', () => {
|
||||
mutator.pinned = true
|
||||
})
|
||||
await this.application.mutator.insertItem(
|
||||
await this.application.mutator.createTemplateItem(ContentType.Note, {
|
||||
await this.application.items.createTemplateItem(ContentType.Note, {
|
||||
title: 'B',
|
||||
pinned: false,
|
||||
}),
|
||||
)
|
||||
const view = await this.application.mutator.insertItem(
|
||||
await this.application.mutator.createTemplateItem(ContentType.SmartView, {
|
||||
await this.application.items.createTemplateItem(ContentType.SmartView, {
|
||||
title: 'Pinned',
|
||||
predicate: {
|
||||
keypath: 'pinned',
|
||||
@@ -485,7 +485,7 @@ describe('notes and tags', () => {
|
||||
|
||||
it('"pinned", "=", false', async function () {
|
||||
const pinnedNote = await this.application.mutator.insertItem(
|
||||
await this.application.mutator.createTemplateItem(ContentType.Note, {
|
||||
await this.application.items.createTemplateItem(ContentType.Note, {
|
||||
title: 'A',
|
||||
}),
|
||||
)
|
||||
@@ -493,12 +493,12 @@ describe('notes and tags', () => {
|
||||
mutator.pinned = true
|
||||
})
|
||||
const unpinnedNote = await this.application.mutator.insertItem(
|
||||
await this.application.mutator.createTemplateItem(ContentType.Note, {
|
||||
await this.application.items.createTemplateItem(ContentType.Note, {
|
||||
title: 'B',
|
||||
}),
|
||||
)
|
||||
const view = await this.application.mutator.insertItem(
|
||||
await this.application.mutator.createTemplateItem(ContentType.SmartView, {
|
||||
await this.application.items.createTemplateItem(ContentType.SmartView, {
|
||||
title: 'Not pinned',
|
||||
predicate: {
|
||||
keypath: 'pinned',
|
||||
@@ -522,19 +522,19 @@ describe('notes and tags', () => {
|
||||
|
||||
it('"text.length", ">", 500', async function () {
|
||||
const longNote = await this.application.mutator.insertItem(
|
||||
await this.application.mutator.createTemplateItem(ContentType.Note, {
|
||||
await this.application.items.createTemplateItem(ContentType.Note, {
|
||||
title: 'A',
|
||||
text: Array(501).fill(0).join(''),
|
||||
}),
|
||||
)
|
||||
await this.application.mutator.insertItem(
|
||||
await this.application.mutator.createTemplateItem(ContentType.Note, {
|
||||
await this.application.items.createTemplateItem(ContentType.Note, {
|
||||
title: 'B',
|
||||
text: 'b',
|
||||
}),
|
||||
)
|
||||
const view = await this.application.mutator.insertItem(
|
||||
await this.application.mutator.createTemplateItem(ContentType.SmartView, {
|
||||
await this.application.items.createTemplateItem(ContentType.SmartView, {
|
||||
title: 'Long',
|
||||
predicate: {
|
||||
keypath: 'text.length',
|
||||
@@ -563,18 +563,20 @@ describe('notes and tags', () => {
|
||||
})
|
||||
|
||||
const recentNote = await this.application.mutator.insertItem(
|
||||
await this.application.mutator.createTemplateItem(ContentType.Note, {
|
||||
await this.application.items.createTemplateItem(ContentType.Note, {
|
||||
title: 'A',
|
||||
}),
|
||||
true,
|
||||
)
|
||||
|
||||
await this.application.sync.sync()
|
||||
|
||||
const olderNote = await this.application.mutator.insertItem(
|
||||
await this.application.mutator.createTemplateItem(ContentType.Note, {
|
||||
await this.application.items.createTemplateItem(ContentType.Note, {
|
||||
title: 'B',
|
||||
text: 'b',
|
||||
}),
|
||||
true,
|
||||
)
|
||||
|
||||
const threeDays = 3 * 24 * 60 * 60 * 1000
|
||||
@@ -582,13 +584,13 @@ describe('notes and tags', () => {
|
||||
|
||||
/** Create an unsynced note which shouldn't get an updated_at */
|
||||
await this.application.mutator.insertItem(
|
||||
await this.application.mutator.createTemplateItem(ContentType.Note, {
|
||||
await this.application.items.createTemplateItem(ContentType.Note, {
|
||||
title: 'B',
|
||||
text: 'b',
|
||||
}),
|
||||
)
|
||||
const view = await this.application.mutator.insertItem(
|
||||
await this.application.mutator.createTemplateItem(ContentType.SmartView, {
|
||||
await this.application.items.createTemplateItem(ContentType.SmartView, {
|
||||
title: 'One day ago',
|
||||
predicate: {
|
||||
keypath: 'serverUpdatedAt',
|
||||
@@ -598,6 +600,9 @@ describe('notes and tags', () => {
|
||||
}),
|
||||
)
|
||||
const matches = this.application.items.notesMatchingSmartView(view)
|
||||
expect(matches.length).to.equal(1)
|
||||
expect(matches[0].uuid).to.equal(recentNote.uuid)
|
||||
|
||||
this.application.items.setPrimaryItemDisplayOptions({
|
||||
sortBy: 'title',
|
||||
sortDirection: 'asc',
|
||||
@@ -605,13 +610,11 @@ describe('notes and tags', () => {
|
||||
})
|
||||
const displayedNotes = this.application.items.getDisplayableNotes()
|
||||
expect(displayedNotes).to.deep.equal(matches)
|
||||
expect(matches.length).to.equal(1)
|
||||
expect(matches[0].uuid).to.equal(recentNote.uuid)
|
||||
})
|
||||
|
||||
it('"tags.length", "=", 0', async function () {
|
||||
const untaggedNote = await this.application.mutator.insertItem(
|
||||
await this.application.mutator.createTemplateItem(ContentType.Note, {
|
||||
await this.application.items.createTemplateItem(ContentType.Note, {
|
||||
title: 'A',
|
||||
}),
|
||||
)
|
||||
@@ -622,7 +625,7 @@ describe('notes and tags', () => {
|
||||
})
|
||||
|
||||
const view = await this.application.mutator.insertItem(
|
||||
await this.application.mutator.createTemplateItem(ContentType.SmartView, {
|
||||
await this.application.items.createTemplateItem(ContentType.SmartView, {
|
||||
title: 'Untagged',
|
||||
predicate: {
|
||||
keypath: 'tags.length',
|
||||
@@ -650,13 +653,13 @@ describe('notes and tags', () => {
|
||||
mutator.e2ePendingRefactor_addItemAsRelationship(taggedNote)
|
||||
})
|
||||
await this.application.mutator.insertItem(
|
||||
await this.application.mutator.createTemplateItem(ContentType.Note, {
|
||||
await this.application.items.createTemplateItem(ContentType.Note, {
|
||||
title: 'A',
|
||||
}),
|
||||
)
|
||||
|
||||
const view = await this.application.mutator.insertItem(
|
||||
await this.application.mutator.createTemplateItem(ContentType.SmartView, {
|
||||
await this.application.items.createTemplateItem(ContentType.SmartView, {
|
||||
title: 'B-tags',
|
||||
predicate: {
|
||||
keypath: 'tags',
|
||||
@@ -685,7 +688,7 @@ describe('notes and tags', () => {
|
||||
})
|
||||
|
||||
const pinnedNote = await this.application.mutator.insertItem(
|
||||
await this.application.mutator.createTemplateItem(ContentType.Note, {
|
||||
await this.application.items.createTemplateItem(ContentType.Note, {
|
||||
title: 'A',
|
||||
}),
|
||||
)
|
||||
@@ -694,7 +697,7 @@ describe('notes and tags', () => {
|
||||
})
|
||||
|
||||
const lockedNote = await this.application.mutator.insertItem(
|
||||
await this.application.mutator.createTemplateItem(ContentType.Note, {
|
||||
await this.application.items.createTemplateItem(ContentType.Note, {
|
||||
title: 'A',
|
||||
}),
|
||||
)
|
||||
@@ -703,7 +706,7 @@ describe('notes and tags', () => {
|
||||
})
|
||||
|
||||
const view = await this.application.mutator.insertItem(
|
||||
await this.application.mutator.createTemplateItem(ContentType.SmartView, {
|
||||
await this.application.items.createTemplateItem(ContentType.SmartView, {
|
||||
title: 'Pinned & Locked',
|
||||
predicate: {
|
||||
operator: 'and',
|
||||
@@ -733,7 +736,7 @@ describe('notes and tags', () => {
|
||||
})
|
||||
|
||||
const pinnedNote = await this.application.mutator.insertItem(
|
||||
await this.application.mutator.createTemplateItem(ContentType.Note, {
|
||||
await this.application.items.createTemplateItem(ContentType.Note, {
|
||||
title: 'A',
|
||||
}),
|
||||
)
|
||||
@@ -742,7 +745,7 @@ describe('notes and tags', () => {
|
||||
})
|
||||
|
||||
const pinnedAndProtectedNote = await this.application.mutator.insertItem(
|
||||
await this.application.mutator.createTemplateItem(ContentType.Note, {
|
||||
await this.application.items.createTemplateItem(ContentType.Note, {
|
||||
title: 'A',
|
||||
}),
|
||||
)
|
||||
@@ -752,13 +755,13 @@ describe('notes and tags', () => {
|
||||
})
|
||||
|
||||
await this.application.mutator.insertItem(
|
||||
await this.application.mutator.createTemplateItem(ContentType.Note, {
|
||||
await this.application.items.createTemplateItem(ContentType.Note, {
|
||||
title: 'A',
|
||||
}),
|
||||
)
|
||||
|
||||
const view = await this.application.mutator.insertItem(
|
||||
await this.application.mutator.createTemplateItem(ContentType.SmartView, {
|
||||
await this.application.items.createTemplateItem(ContentType.SmartView, {
|
||||
title: 'Protected or Pinned',
|
||||
predicate: {
|
||||
operator: 'or',
|
||||
@@ -794,7 +797,7 @@ describe('notes and tags', () => {
|
||||
const notePayload3 = Factory.createNotePayload('Bar')
|
||||
const notePayload4 = Factory.createNotePayload('Testing')
|
||||
|
||||
await this.application.itemManager.emitItemsFromPayloads(
|
||||
await this.application.mutator.emitItemsFromPayloads(
|
||||
[notePayload1, notePayload2, notePayload3, notePayload4, tagPayload1],
|
||||
PayloadEmitSource.LocalChanged,
|
||||
)
|
||||
@@ -824,7 +827,7 @@ describe('notes and tags', () => {
|
||||
const notePayload3 = Factory.createNotePayload('Testing FOO (Bar)')
|
||||
const notePayload4 = Factory.createNotePayload('This should not match')
|
||||
|
||||
await this.application.itemManager.emitItemsFromPayloads(
|
||||
await this.application.mutator.emitItemsFromPayloads(
|
||||
[notePayload1, notePayload2, notePayload3, notePayload4, tagPayload1],
|
||||
PayloadEmitSource.LocalChanged,
|
||||
)
|
||||
|
||||
@@ -75,8 +75,8 @@ describe('tags as folders', () => {
|
||||
const note2 = await Factory.createMappedNote(this.application, 'my second note')
|
||||
|
||||
// ## The user add a note to the child tag
|
||||
await this.application.items.addTagToNote(note1, tags.child, true)
|
||||
await this.application.items.addTagToNote(note2, tags.another, true)
|
||||
await this.application.mutator.addTagToNote(note1, tags.child, true)
|
||||
await this.application.mutator.addTagToNote(note2, tags.another, true)
|
||||
|
||||
// ## The note has been added to other tags
|
||||
const note1Tags = await this.application.items.getSortedTagsForItem(note1)
|
||||
|
||||
@@ -56,7 +56,7 @@ describe('mapping performance', () => {
|
||||
const batchSize = 100
|
||||
for (let i = 0; i < payloads.length; i += batchSize) {
|
||||
const subArray = payloads.slice(currentIndex, currentIndex + batchSize)
|
||||
await application.itemManager.emitItemsFromPayloads(subArray, PayloadEmitSource.LocalChanged)
|
||||
await application.mutator.emitItemsFromPayloads(subArray, PayloadEmitSource.LocalChanged)
|
||||
currentIndex += batchSize
|
||||
}
|
||||
|
||||
@@ -117,7 +117,7 @@ describe('mapping performance', () => {
|
||||
const batchSize = 100
|
||||
for (let i = 0; i < payloads.length; i += batchSize) {
|
||||
var subArray = payloads.slice(currentIndex, currentIndex + batchSize)
|
||||
await application.itemManager.emitItemsFromPayloads(subArray, PayloadEmitSource.LocalChanged)
|
||||
await application.mutator.emitItemsFromPayloads(subArray, PayloadEmitSource.LocalChanged)
|
||||
currentIndex += batchSize
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import * as Factory from './lib/factory.js'
|
||||
chai.use(chaiAsPromised)
|
||||
const expect = chai.expect
|
||||
|
||||
describe('mutator', () => {
|
||||
describe('item mutator', () => {
|
||||
beforeEach(async function () {
|
||||
this.createBarePayload = () => {
|
||||
return new DecryptedPayload({
|
||||
|
||||
271
packages/snjs/mocha/mutator_service.test.js
Normal file
271
packages/snjs/mocha/mutator_service.test.js
Normal file
@@ -0,0 +1,271 @@
|
||||
/* eslint-disable no-unused-expressions */
|
||||
/* eslint-disable no-undef */
|
||||
import * as Factory from './lib/factory.js'
|
||||
import { BaseItemCounts } from './lib/BaseItemCounts.js'
|
||||
chai.use(chaiAsPromised)
|
||||
const expect = chai.expect
|
||||
|
||||
describe('mutator service', function () {
|
||||
this.timeout(Factory.TwentySecondTimeout)
|
||||
|
||||
let context
|
||||
let application
|
||||
let mutator
|
||||
|
||||
afterEach(async function () {
|
||||
await context.deinit()
|
||||
localStorage.clear()
|
||||
})
|
||||
|
||||
beforeEach(async function () {
|
||||
localStorage.clear()
|
||||
|
||||
context = await Factory.createAppContextWithFakeCrypto()
|
||||
application = context.application
|
||||
mutator = application.mutator
|
||||
|
||||
await context.launch()
|
||||
})
|
||||
|
||||
const createNote = async () => {
|
||||
return mutator.createItem(ContentType.Note, {
|
||||
title: 'hello',
|
||||
text: 'world',
|
||||
})
|
||||
}
|
||||
|
||||
const createTag = async (notes = []) => {
|
||||
const references = notes.map((note) => {
|
||||
return {
|
||||
uuid: note.uuid,
|
||||
content_type: note.content_type,
|
||||
}
|
||||
})
|
||||
return mutator.createItem(ContentType.Tag, {
|
||||
title: 'thoughts',
|
||||
references: references,
|
||||
})
|
||||
}
|
||||
|
||||
it('create item', async function () {
|
||||
const item = await createNote()
|
||||
|
||||
expect(item).to.be.ok
|
||||
expect(item.title).to.equal('hello')
|
||||
})
|
||||
|
||||
it('emitting item through payload and marking dirty should have userModifiedDate', async function () {
|
||||
const payload = Factory.createNotePayload()
|
||||
const item = await mutator.emitItemFromPayload(payload, PayloadEmitSource.LocalChanged)
|
||||
const result = await mutator.setItemDirty(item)
|
||||
const appData = result.payload.content.appData
|
||||
expect(appData[DecryptedItem.DefaultAppDomain()][AppDataField.UserModifiedDate]).to.be.ok
|
||||
})
|
||||
|
||||
it('deleting an item should make it immediately unfindable', async () => {
|
||||
const note = await context.createSyncedNote()
|
||||
await mutator.setItemToBeDeleted(note)
|
||||
const foundNote = application.items.findItem(note.uuid)
|
||||
expect(foundNote).to.not.be.ok
|
||||
})
|
||||
|
||||
it('deleting from reference map', async function () {
|
||||
const note = await createNote()
|
||||
const tag = await createTag([note])
|
||||
await mutator.setItemToBeDeleted(note)
|
||||
|
||||
expect(application.items.collection.referenceMap.directMap.get(tag.uuid)).to.eql([])
|
||||
expect(application.items.collection.referenceMap.inverseMap.get(note.uuid).length).to.equal(0)
|
||||
})
|
||||
|
||||
it('deleting referenced item should update referencing item references', async function () {
|
||||
const note = await createNote()
|
||||
let tag = await createTag([note])
|
||||
await mutator.setItemToBeDeleted(note)
|
||||
|
||||
tag = application.items.findItem(tag.uuid)
|
||||
expect(tag.content.references.length).to.equal(0)
|
||||
})
|
||||
|
||||
it('removing relationship should update reference map', async function () {
|
||||
const note = await createNote()
|
||||
const tag = await createTag([note])
|
||||
await mutator.changeItem(tag, (mutator) => {
|
||||
mutator.removeItemAsRelationship(note)
|
||||
})
|
||||
|
||||
expect(application.items.collection.referenceMap.directMap.get(tag.uuid)).to.eql([])
|
||||
expect(application.items.collection.referenceMap.inverseMap.get(note.uuid)).to.eql([])
|
||||
})
|
||||
|
||||
it('emitting discardable payload should remove it from our collection', async function () {
|
||||
const note = await createNote()
|
||||
|
||||
const payload = new DeletedPayload({
|
||||
...note.payload.ejected(),
|
||||
content: undefined,
|
||||
deleted: true,
|
||||
dirty: false,
|
||||
})
|
||||
|
||||
expect(payload.discardable).to.equal(true)
|
||||
|
||||
await context.payloads.emitPayload(payload)
|
||||
|
||||
expect(application.items.findItem(note.uuid)).to.not.be.ok
|
||||
})
|
||||
|
||||
it('change existing item', async function () {
|
||||
const note = await createNote()
|
||||
const newTitle = String(Math.random())
|
||||
await mutator.changeItem(note, (mutator) => {
|
||||
mutator.title = newTitle
|
||||
})
|
||||
|
||||
const latestVersion = application.items.findItem(note.uuid)
|
||||
expect(latestVersion.title).to.equal(newTitle)
|
||||
})
|
||||
|
||||
it('change non-existant item through uuid should fail', async function () {
|
||||
const note = await application.items.createTemplateItem(ContentType.Note, {
|
||||
title: 'hello',
|
||||
text: 'world',
|
||||
})
|
||||
|
||||
const changeFn = async () => {
|
||||
const newTitle = String(Math.random())
|
||||
return mutator.changeItem(note, (mutator) => {
|
||||
mutator.title = newTitle
|
||||
})
|
||||
}
|
||||
await Factory.expectThrowsAsync(() => changeFn(), 'Attempting to change non-existant item')
|
||||
})
|
||||
|
||||
it('set items dirty', async function () {
|
||||
const note = await createNote()
|
||||
await mutator.setItemDirty(note)
|
||||
|
||||
const dirtyItems = application.items.getDirtyItems()
|
||||
expect(dirtyItems.length).to.equal(1)
|
||||
expect(dirtyItems[0].uuid).to.equal(note.uuid)
|
||||
expect(dirtyItems[0].dirty).to.equal(true)
|
||||
})
|
||||
|
||||
describe('duplicateItem', async function () {
|
||||
const sandbox = sinon.createSandbox()
|
||||
|
||||
beforeEach(async function () {
|
||||
this.emitPayloads = sandbox.spy(application.items.payloadManager, 'emitPayloads')
|
||||
})
|
||||
|
||||
afterEach(async function () {
|
||||
sandbox.restore()
|
||||
})
|
||||
|
||||
it('should duplicate the item and set the duplicate_of property', async function () {
|
||||
const note = await createNote()
|
||||
await mutator.duplicateItem(note)
|
||||
sinon.assert.calledTwice(this.emitPayloads)
|
||||
|
||||
const originalNote = application.items.getDisplayableNotes()[0]
|
||||
const duplicatedNote = application.items.getDisplayableNotes()[1]
|
||||
|
||||
expect(application.items.items.length).to.equal(2 + BaseItemCounts.DefaultItems)
|
||||
expect(application.items.getDisplayableNotes().length).to.equal(2)
|
||||
expect(originalNote.uuid).to.not.equal(duplicatedNote.uuid)
|
||||
expect(originalNote.uuid).to.equal(duplicatedNote.duplicateOf)
|
||||
expect(originalNote.uuid).to.equal(duplicatedNote.payload.duplicate_of)
|
||||
expect(duplicatedNote.conflictOf).to.be.undefined
|
||||
expect(duplicatedNote.payload.content.conflict_of).to.be.undefined
|
||||
})
|
||||
|
||||
it('should duplicate the item and set the duplicate_of and conflict_of properties', async function () {
|
||||
const note = await createNote()
|
||||
await mutator.duplicateItem(note, true)
|
||||
sinon.assert.calledTwice(this.emitPayloads)
|
||||
|
||||
const originalNote = application.items.getDisplayableNotes()[0]
|
||||
const duplicatedNote = application.items.getDisplayableNotes()[1]
|
||||
|
||||
expect(application.items.items.length).to.equal(2 + BaseItemCounts.DefaultItems)
|
||||
expect(application.items.getDisplayableNotes().length).to.equal(2)
|
||||
expect(originalNote.uuid).to.not.equal(duplicatedNote.uuid)
|
||||
expect(originalNote.uuid).to.equal(duplicatedNote.duplicateOf)
|
||||
expect(originalNote.uuid).to.equal(duplicatedNote.payload.duplicate_of)
|
||||
expect(originalNote.uuid).to.equal(duplicatedNote.conflictOf)
|
||||
expect(originalNote.uuid).to.equal(duplicatedNote.payload.content.conflict_of)
|
||||
})
|
||||
|
||||
it('duplicate item with relationships', async function () {
|
||||
const note = await createNote()
|
||||
const tag = await createTag([note])
|
||||
const duplicate = await mutator.duplicateItem(tag)
|
||||
|
||||
expect(duplicate.content.references).to.have.length(1)
|
||||
expect(application.items.items).to.have.length(3 + BaseItemCounts.DefaultItems)
|
||||
expect(application.items.getDisplayableTags()).to.have.length(2)
|
||||
})
|
||||
|
||||
it('adds duplicated item as a relationship to items referencing it', async function () {
|
||||
const note = await createNote()
|
||||
let tag = await createTag([note])
|
||||
const duplicateNote = await mutator.duplicateItem(note)
|
||||
expect(tag.content.references).to.have.length(1)
|
||||
|
||||
tag = application.items.findItem(tag.uuid)
|
||||
const references = tag.content.references.map((ref) => ref.uuid)
|
||||
expect(references).to.have.length(2)
|
||||
expect(references).to.include(note.uuid, duplicateNote.uuid)
|
||||
})
|
||||
|
||||
it('duplicates item with additional content', async function () {
|
||||
const note = await mutator.createItem(ContentType.Note, {
|
||||
title: 'hello',
|
||||
text: 'world',
|
||||
})
|
||||
const duplicateNote = await mutator.duplicateItem(note, false, {
|
||||
title: 'hello (copy)',
|
||||
})
|
||||
|
||||
expect(duplicateNote.title).to.equal('hello (copy)')
|
||||
expect(duplicateNote.text).to.equal('world')
|
||||
})
|
||||
})
|
||||
|
||||
it('set item deleted', async function () {
|
||||
const note = await createNote()
|
||||
await mutator.setItemToBeDeleted(note)
|
||||
|
||||
/** Items should never be mutated directly */
|
||||
expect(note.deleted).to.not.be.ok
|
||||
|
||||
const latestVersion = context.payloads.findOne(note.uuid)
|
||||
expect(latestVersion.deleted).to.equal(true)
|
||||
expect(latestVersion.dirty).to.equal(true)
|
||||
expect(latestVersion.content).to.not.be.ok
|
||||
|
||||
/** Deleted items do not show up in item manager's public interface */
|
||||
expect(application.items.items.length).to.equal(BaseItemCounts.DefaultItems)
|
||||
expect(application.items.getDisplayableNotes().length).to.equal(0)
|
||||
})
|
||||
|
||||
it('should empty trash', async function () {
|
||||
const note = await createNote()
|
||||
const versionTwo = await mutator.changeItem(note, (mutator) => {
|
||||
mutator.trashed = true
|
||||
})
|
||||
|
||||
expect(application.items.trashSmartView).to.be.ok
|
||||
expect(versionTwo.trashed).to.equal(true)
|
||||
expect(versionTwo.dirty).to.equal(true)
|
||||
expect(versionTwo.content).to.be.ok
|
||||
|
||||
expect(application.items.items.length).to.equal(1 + BaseItemCounts.DefaultItems)
|
||||
expect(application.items.trashedItems.length).to.equal(1)
|
||||
|
||||
await application.mutator.emptyTrash()
|
||||
const versionThree = context.payloads.findOne(note.uuid)
|
||||
expect(versionThree.deleted).to.equal(true)
|
||||
expect(application.items.trashedItems.length).to.equal(0)
|
||||
})
|
||||
})
|
||||
@@ -6,9 +6,10 @@ describe('note display criteria', function () {
|
||||
beforeEach(async function () {
|
||||
this.payloadManager = new PayloadManager()
|
||||
this.itemManager = new ItemManager(this.payloadManager)
|
||||
this.mutator = new MutatorService(this.itemManager, this.payloadManager)
|
||||
|
||||
this.createNote = async (title = 'hello', text = 'world') => {
|
||||
return this.itemManager.createItem(ContentType.Note, {
|
||||
return this.mutator.createItem(ContentType.Note, {
|
||||
title: title,
|
||||
text: text,
|
||||
})
|
||||
@@ -21,7 +22,7 @@ describe('note display criteria', function () {
|
||||
content_type: note.content_type,
|
||||
}
|
||||
})
|
||||
return this.itemManager.createItem(ContentType.Tag, {
|
||||
return this.mutator.createItem(ContentType.Tag, {
|
||||
title: title,
|
||||
references: references,
|
||||
})
|
||||
@@ -31,138 +32,168 @@ describe('note display criteria', function () {
|
||||
it('includePinned off', async function () {
|
||||
await this.createNote()
|
||||
const pendingPin = await this.createNote()
|
||||
await this.itemManager.changeItem(pendingPin, (mutator) => {
|
||||
await this.mutator.changeItem(pendingPin, (mutator) => {
|
||||
mutator.pinned = true
|
||||
})
|
||||
const criteria = {
|
||||
includePinned: false,
|
||||
}
|
||||
expect(
|
||||
itemsMatchingOptions(criteria, this.itemManager.collection.all(ContentType.Note), this.itemManager.collection)
|
||||
.length,
|
||||
notesAndFilesMatchingOptions(
|
||||
criteria,
|
||||
this.itemManager.collection.all(ContentType.Note),
|
||||
this.itemManager.collection,
|
||||
).length,
|
||||
).to.equal(1)
|
||||
})
|
||||
|
||||
it('includePinned on', async function () {
|
||||
await this.createNote()
|
||||
const pendingPin = await this.createNote()
|
||||
await this.itemManager.changeItem(pendingPin, (mutator) => {
|
||||
await this.mutator.changeItem(pendingPin, (mutator) => {
|
||||
mutator.pinned = true
|
||||
})
|
||||
const criteria = { includePinned: true }
|
||||
expect(
|
||||
itemsMatchingOptions(criteria, this.itemManager.collection.all(ContentType.Note), this.itemManager.collection)
|
||||
.length,
|
||||
notesAndFilesMatchingOptions(
|
||||
criteria,
|
||||
this.itemManager.collection.all(ContentType.Note),
|
||||
this.itemManager.collection,
|
||||
).length,
|
||||
).to.equal(2)
|
||||
})
|
||||
|
||||
it('includeTrashed off', async function () {
|
||||
await this.createNote()
|
||||
const pendingTrash = await this.createNote()
|
||||
await this.itemManager.changeItem(pendingTrash, (mutator) => {
|
||||
await this.mutator.changeItem(pendingTrash, (mutator) => {
|
||||
mutator.trashed = true
|
||||
})
|
||||
const criteria = { includeTrashed: false }
|
||||
expect(
|
||||
itemsMatchingOptions(criteria, this.itemManager.collection.all(ContentType.Note), this.itemManager.collection)
|
||||
.length,
|
||||
notesAndFilesMatchingOptions(
|
||||
criteria,
|
||||
this.itemManager.collection.all(ContentType.Note),
|
||||
this.itemManager.collection,
|
||||
).length,
|
||||
).to.equal(1)
|
||||
})
|
||||
|
||||
it('includeTrashed on', async function () {
|
||||
await this.createNote()
|
||||
const pendingTrash = await this.createNote()
|
||||
await this.itemManager.changeItem(pendingTrash, (mutator) => {
|
||||
await this.mutator.changeItem(pendingTrash, (mutator) => {
|
||||
mutator.trashed = true
|
||||
})
|
||||
const criteria = { includeTrashed: true }
|
||||
expect(
|
||||
itemsMatchingOptions(criteria, this.itemManager.collection.all(ContentType.Note), this.itemManager.collection)
|
||||
.length,
|
||||
notesAndFilesMatchingOptions(
|
||||
criteria,
|
||||
this.itemManager.collection.all(ContentType.Note),
|
||||
this.itemManager.collection,
|
||||
).length,
|
||||
).to.equal(2)
|
||||
})
|
||||
|
||||
it('includeArchived off', async function () {
|
||||
await this.createNote()
|
||||
const pendingArchive = await this.createNote()
|
||||
await this.itemManager.changeItem(pendingArchive, (mutator) => {
|
||||
await this.mutator.changeItem(pendingArchive, (mutator) => {
|
||||
mutator.archived = true
|
||||
})
|
||||
const criteria = { includeArchived: false }
|
||||
expect(
|
||||
itemsMatchingOptions(criteria, this.itemManager.collection.all(ContentType.Note), this.itemManager.collection)
|
||||
.length,
|
||||
notesAndFilesMatchingOptions(
|
||||
criteria,
|
||||
this.itemManager.collection.all(ContentType.Note),
|
||||
this.itemManager.collection,
|
||||
).length,
|
||||
).to.equal(1)
|
||||
})
|
||||
|
||||
it('includeArchived on', async function () {
|
||||
await this.createNote()
|
||||
const pendingArchive = await this.createNote()
|
||||
await this.itemManager.changeItem(pendingArchive, (mutator) => {
|
||||
await this.mutator.changeItem(pendingArchive, (mutator) => {
|
||||
mutator.archived = true
|
||||
})
|
||||
const criteria = {
|
||||
includeArchived: true,
|
||||
}
|
||||
expect(
|
||||
itemsMatchingOptions(criteria, this.itemManager.collection.all(ContentType.Note), this.itemManager.collection)
|
||||
.length,
|
||||
notesAndFilesMatchingOptions(
|
||||
criteria,
|
||||
this.itemManager.collection.all(ContentType.Note),
|
||||
this.itemManager.collection,
|
||||
).length,
|
||||
).to.equal(2)
|
||||
})
|
||||
|
||||
it('includeProtected off', async function () {
|
||||
await this.createNote()
|
||||
const pendingProtected = await this.createNote()
|
||||
await this.itemManager.changeItem(pendingProtected, (mutator) => {
|
||||
await this.mutator.changeItem(pendingProtected, (mutator) => {
|
||||
mutator.protected = true
|
||||
})
|
||||
const criteria = { includeProtected: false }
|
||||
expect(
|
||||
itemsMatchingOptions(criteria, this.itemManager.collection.all(ContentType.Note), this.itemManager.collection)
|
||||
.length,
|
||||
notesAndFilesMatchingOptions(
|
||||
criteria,
|
||||
this.itemManager.collection.all(ContentType.Note),
|
||||
this.itemManager.collection,
|
||||
).length,
|
||||
).to.equal(1)
|
||||
})
|
||||
|
||||
it('includeProtected on', async function () {
|
||||
await this.createNote()
|
||||
const pendingProtected = await this.createNote()
|
||||
await this.itemManager.changeItem(pendingProtected, (mutator) => {
|
||||
await this.mutator.changeItem(pendingProtected, (mutator) => {
|
||||
mutator.protected = true
|
||||
})
|
||||
const criteria = {
|
||||
includeProtected: true,
|
||||
}
|
||||
expect(
|
||||
itemsMatchingOptions(criteria, this.itemManager.collection.all(ContentType.Note), this.itemManager.collection)
|
||||
.length,
|
||||
notesAndFilesMatchingOptions(
|
||||
criteria,
|
||||
this.itemManager.collection.all(ContentType.Note),
|
||||
this.itemManager.collection,
|
||||
).length,
|
||||
).to.equal(2)
|
||||
})
|
||||
|
||||
it('protectedSearchEnabled false', async function () {
|
||||
const normal = await this.createNote('hello', 'world')
|
||||
await this.itemManager.changeItem(normal, (mutator) => {
|
||||
await this.mutator.changeItem(normal, (mutator) => {
|
||||
mutator.protected = true
|
||||
})
|
||||
const criteria = {
|
||||
searchQuery: { query: 'world', includeProtectedNoteText: false },
|
||||
}
|
||||
expect(
|
||||
itemsMatchingOptions(criteria, this.itemManager.collection.all(ContentType.Note), this.itemManager.collection)
|
||||
.length,
|
||||
notesAndFilesMatchingOptions(
|
||||
criteria,
|
||||
this.itemManager.collection.all(ContentType.Note),
|
||||
this.itemManager.collection,
|
||||
).length,
|
||||
).to.equal(0)
|
||||
})
|
||||
|
||||
it('protectedSearchEnabled true', async function () {
|
||||
const normal = await this.createNote()
|
||||
await this.itemManager.changeItem(normal, (mutator) => {
|
||||
await this.mutator.changeItem(normal, (mutator) => {
|
||||
mutator.protected = true
|
||||
})
|
||||
const criteria = {
|
||||
searchQuery: { query: 'world', includeProtectedNoteText: true },
|
||||
}
|
||||
expect(
|
||||
itemsMatchingOptions(criteria, this.itemManager.collection.all(ContentType.Note), this.itemManager.collection)
|
||||
.length,
|
||||
notesAndFilesMatchingOptions(
|
||||
criteria,
|
||||
this.itemManager.collection.all(ContentType.Note),
|
||||
this.itemManager.collection,
|
||||
).length,
|
||||
).to.equal(1)
|
||||
})
|
||||
|
||||
@@ -175,7 +206,7 @@ describe('note display criteria', function () {
|
||||
tags: [tag],
|
||||
}
|
||||
expect(
|
||||
itemsMatchingOptions(
|
||||
notesAndFilesMatchingOptions(
|
||||
matchingCriteria,
|
||||
this.itemManager.collection.all(ContentType.Note),
|
||||
this.itemManager.collection,
|
||||
@@ -186,7 +217,7 @@ describe('note display criteria', function () {
|
||||
tags: [looseTag],
|
||||
}
|
||||
expect(
|
||||
itemsMatchingOptions(
|
||||
notesAndFilesMatchingOptions(
|
||||
nonmatchingCriteria,
|
||||
this.itemManager.collection.all(ContentType.Note),
|
||||
this.itemManager.collection,
|
||||
@@ -198,7 +229,7 @@ describe('note display criteria', function () {
|
||||
it('normal note', async function () {
|
||||
await this.createNote()
|
||||
expect(
|
||||
itemsMatchingOptions(
|
||||
notesAndFilesMatchingOptions(
|
||||
{
|
||||
views: [this.itemManager.allNotesSmartView],
|
||||
},
|
||||
@@ -208,7 +239,7 @@ describe('note display criteria', function () {
|
||||
).to.equal(1)
|
||||
|
||||
expect(
|
||||
itemsMatchingOptions(
|
||||
notesAndFilesMatchingOptions(
|
||||
{
|
||||
views: [this.itemManager.trashSmartView],
|
||||
},
|
||||
@@ -218,7 +249,7 @@ describe('note display criteria', function () {
|
||||
).to.equal(0)
|
||||
|
||||
expect(
|
||||
itemsMatchingOptions(
|
||||
notesAndFilesMatchingOptions(
|
||||
{
|
||||
views: [this.itemManager.archivedSmartView],
|
||||
},
|
||||
@@ -230,12 +261,12 @@ describe('note display criteria', function () {
|
||||
|
||||
it('trashed note', async function () {
|
||||
const normal = await this.createNote()
|
||||
await this.itemManager.changeItem(normal, (mutator) => {
|
||||
await this.mutator.changeItem(normal, (mutator) => {
|
||||
mutator.trashed = true
|
||||
})
|
||||
|
||||
expect(
|
||||
itemsMatchingOptions(
|
||||
notesAndFilesMatchingOptions(
|
||||
{
|
||||
views: [this.itemManager.allNotesSmartView],
|
||||
includeTrashed: false,
|
||||
@@ -246,7 +277,7 @@ describe('note display criteria', function () {
|
||||
).to.equal(0)
|
||||
|
||||
expect(
|
||||
itemsMatchingOptions(
|
||||
notesAndFilesMatchingOptions(
|
||||
{
|
||||
views: [this.itemManager.trashSmartView],
|
||||
},
|
||||
@@ -256,7 +287,7 @@ describe('note display criteria', function () {
|
||||
).to.equal(1)
|
||||
|
||||
expect(
|
||||
itemsMatchingOptions(
|
||||
notesAndFilesMatchingOptions(
|
||||
{
|
||||
views: [this.itemManager.archivedSmartView],
|
||||
},
|
||||
@@ -268,12 +299,12 @@ describe('note display criteria', function () {
|
||||
|
||||
it('archived note', async function () {
|
||||
const normal = await this.createNote()
|
||||
await this.itemManager.changeItem(normal, (mutator) => {
|
||||
await this.mutator.changeItem(normal, (mutator) => {
|
||||
mutator.trashed = false
|
||||
mutator.archived = true
|
||||
})
|
||||
expect(
|
||||
itemsMatchingOptions(
|
||||
notesAndFilesMatchingOptions(
|
||||
{
|
||||
views: [this.itemManager.allNotesSmartView],
|
||||
includeArchived: false,
|
||||
@@ -284,7 +315,7 @@ describe('note display criteria', function () {
|
||||
).to.equal(0)
|
||||
|
||||
expect(
|
||||
itemsMatchingOptions(
|
||||
notesAndFilesMatchingOptions(
|
||||
{
|
||||
views: [this.itemManager.trashSmartView],
|
||||
},
|
||||
@@ -294,7 +325,7 @@ describe('note display criteria', function () {
|
||||
).to.equal(0)
|
||||
|
||||
expect(
|
||||
itemsMatchingOptions(
|
||||
notesAndFilesMatchingOptions(
|
||||
{
|
||||
views: [this.itemManager.archivedSmartView],
|
||||
},
|
||||
@@ -306,13 +337,13 @@ describe('note display criteria', function () {
|
||||
|
||||
it('archived + trashed note', async function () {
|
||||
const normal = await this.createNote()
|
||||
await this.itemManager.changeItem(normal, (mutator) => {
|
||||
await this.mutator.changeItem(normal, (mutator) => {
|
||||
mutator.trashed = true
|
||||
mutator.archived = true
|
||||
})
|
||||
|
||||
expect(
|
||||
itemsMatchingOptions(
|
||||
notesAndFilesMatchingOptions(
|
||||
{
|
||||
views: [this.itemManager.allNotesSmartView],
|
||||
},
|
||||
@@ -322,7 +353,7 @@ describe('note display criteria', function () {
|
||||
).to.equal(1)
|
||||
|
||||
expect(
|
||||
itemsMatchingOptions(
|
||||
notesAndFilesMatchingOptions(
|
||||
{
|
||||
views: [this.itemManager.trashSmartView],
|
||||
},
|
||||
@@ -332,7 +363,7 @@ describe('note display criteria', function () {
|
||||
).to.equal(1)
|
||||
|
||||
expect(
|
||||
itemsMatchingOptions(
|
||||
notesAndFilesMatchingOptions(
|
||||
{
|
||||
views: [this.itemManager.archivedSmartView],
|
||||
},
|
||||
@@ -348,7 +379,7 @@ describe('note display criteria', function () {
|
||||
await this.createNote()
|
||||
|
||||
expect(
|
||||
itemsMatchingOptions(
|
||||
notesAndFilesMatchingOptions(
|
||||
{
|
||||
views: [this.itemManager.allNotesSmartView],
|
||||
includeTrashed: true,
|
||||
@@ -359,7 +390,7 @@ describe('note display criteria', function () {
|
||||
).to.equal(1)
|
||||
|
||||
expect(
|
||||
itemsMatchingOptions(
|
||||
notesAndFilesMatchingOptions(
|
||||
{
|
||||
views: [this.itemManager.trashSmartView],
|
||||
includeTrashed: true,
|
||||
@@ -373,12 +404,12 @@ describe('note display criteria', function () {
|
||||
it('trashed note', async function () {
|
||||
const normal = await this.createNote()
|
||||
|
||||
await this.itemManager.changeItem(normal, (mutator) => {
|
||||
await this.mutator.changeItem(normal, (mutator) => {
|
||||
mutator.trashed = true
|
||||
})
|
||||
|
||||
expect(
|
||||
itemsMatchingOptions(
|
||||
notesAndFilesMatchingOptions(
|
||||
{
|
||||
views: [this.itemManager.allNotesSmartView],
|
||||
includeTrashed: false,
|
||||
@@ -389,7 +420,7 @@ describe('note display criteria', function () {
|
||||
).to.equal(0)
|
||||
|
||||
expect(
|
||||
itemsMatchingOptions(
|
||||
notesAndFilesMatchingOptions(
|
||||
{
|
||||
views: [this.itemManager.allNotesSmartView],
|
||||
includeTrashed: true,
|
||||
@@ -400,7 +431,7 @@ describe('note display criteria', function () {
|
||||
).to.equal(1)
|
||||
|
||||
expect(
|
||||
itemsMatchingOptions(
|
||||
notesAndFilesMatchingOptions(
|
||||
{
|
||||
views: [this.itemManager.trashSmartView],
|
||||
includeTrashed: true,
|
||||
@@ -411,7 +442,7 @@ describe('note display criteria', function () {
|
||||
).to.equal(1)
|
||||
|
||||
expect(
|
||||
itemsMatchingOptions(
|
||||
notesAndFilesMatchingOptions(
|
||||
{
|
||||
views: [this.itemManager.archivedSmartView],
|
||||
includeTrashed: true,
|
||||
@@ -425,13 +456,13 @@ describe('note display criteria', function () {
|
||||
it('archived + trashed note', async function () {
|
||||
const normal = await this.createNote()
|
||||
|
||||
await this.itemManager.changeItem(normal, (mutator) => {
|
||||
await this.mutator.changeItem(normal, (mutator) => {
|
||||
mutator.trashed = true
|
||||
mutator.archived = true
|
||||
})
|
||||
|
||||
expect(
|
||||
itemsMatchingOptions(
|
||||
notesAndFilesMatchingOptions(
|
||||
{
|
||||
views: [this.itemManager.allNotesSmartView],
|
||||
},
|
||||
@@ -441,7 +472,7 @@ describe('note display criteria', function () {
|
||||
).to.equal(1)
|
||||
|
||||
expect(
|
||||
itemsMatchingOptions(
|
||||
notesAndFilesMatchingOptions(
|
||||
{
|
||||
views: [this.itemManager.trashSmartView],
|
||||
},
|
||||
@@ -451,7 +482,7 @@ describe('note display criteria', function () {
|
||||
).to.equal(1)
|
||||
|
||||
expect(
|
||||
itemsMatchingOptions(
|
||||
notesAndFilesMatchingOptions(
|
||||
{
|
||||
views: [this.itemManager.archivedSmartView],
|
||||
},
|
||||
@@ -467,7 +498,7 @@ describe('note display criteria', function () {
|
||||
await this.createNote()
|
||||
|
||||
expect(
|
||||
itemsMatchingOptions(
|
||||
notesAndFilesMatchingOptions(
|
||||
{
|
||||
views: [this.itemManager.allNotesSmartView],
|
||||
includeArchived: true,
|
||||
@@ -478,7 +509,7 @@ describe('note display criteria', function () {
|
||||
).to.equal(1)
|
||||
|
||||
expect(
|
||||
itemsMatchingOptions(
|
||||
notesAndFilesMatchingOptions(
|
||||
{
|
||||
views: [this.itemManager.trashSmartView],
|
||||
includeArchived: true,
|
||||
@@ -491,12 +522,12 @@ describe('note display criteria', function () {
|
||||
|
||||
it('archived note', async function () {
|
||||
const normal = await this.createNote()
|
||||
await this.itemManager.changeItem(normal, (mutator) => {
|
||||
await this.mutator.changeItem(normal, (mutator) => {
|
||||
mutator.archived = true
|
||||
})
|
||||
|
||||
expect(
|
||||
itemsMatchingOptions(
|
||||
notesAndFilesMatchingOptions(
|
||||
{
|
||||
views: [this.itemManager.allNotesSmartView],
|
||||
includeArchived: false,
|
||||
@@ -507,7 +538,7 @@ describe('note display criteria', function () {
|
||||
).to.equal(0)
|
||||
|
||||
expect(
|
||||
itemsMatchingOptions(
|
||||
notesAndFilesMatchingOptions(
|
||||
{
|
||||
views: [this.itemManager.allNotesSmartView],
|
||||
includeArchived: true,
|
||||
@@ -518,7 +549,7 @@ describe('note display criteria', function () {
|
||||
).to.equal(1)
|
||||
|
||||
expect(
|
||||
itemsMatchingOptions(
|
||||
notesAndFilesMatchingOptions(
|
||||
{
|
||||
views: [this.itemManager.trashSmartView],
|
||||
includeArchived: true,
|
||||
@@ -529,7 +560,7 @@ describe('note display criteria', function () {
|
||||
).to.equal(0)
|
||||
|
||||
expect(
|
||||
itemsMatchingOptions(
|
||||
notesAndFilesMatchingOptions(
|
||||
{
|
||||
views: [this.itemManager.archivedSmartView],
|
||||
includeArchived: false,
|
||||
@@ -542,13 +573,13 @@ describe('note display criteria', function () {
|
||||
|
||||
it('archived + trashed note', async function () {
|
||||
const normal = await this.createNote()
|
||||
await this.itemManager.changeItem(normal, (mutator) => {
|
||||
await this.mutator.changeItem(normal, (mutator) => {
|
||||
mutator.trashed = true
|
||||
mutator.archived = true
|
||||
})
|
||||
|
||||
expect(
|
||||
itemsMatchingOptions(
|
||||
notesAndFilesMatchingOptions(
|
||||
{
|
||||
views: [this.itemManager.allNotesSmartView],
|
||||
includeArchived: true,
|
||||
@@ -559,7 +590,7 @@ describe('note display criteria', function () {
|
||||
).to.equal(1)
|
||||
|
||||
expect(
|
||||
itemsMatchingOptions(
|
||||
notesAndFilesMatchingOptions(
|
||||
{
|
||||
views: [this.itemManager.trashSmartView],
|
||||
includeArchived: true,
|
||||
@@ -570,7 +601,7 @@ describe('note display criteria', function () {
|
||||
).to.equal(1)
|
||||
|
||||
expect(
|
||||
itemsMatchingOptions(
|
||||
notesAndFilesMatchingOptions(
|
||||
{
|
||||
views: [this.itemManager.archivedSmartView],
|
||||
includeArchived: true,
|
||||
@@ -587,7 +618,7 @@ describe('note display criteria', function () {
|
||||
await this.createNote()
|
||||
|
||||
expect(
|
||||
itemsMatchingOptions(
|
||||
notesAndFilesMatchingOptions(
|
||||
{
|
||||
views: [
|
||||
this.itemManager.allNotesSmartView,
|
||||
@@ -601,7 +632,7 @@ describe('note display criteria', function () {
|
||||
).to.equal(1)
|
||||
|
||||
expect(
|
||||
itemsMatchingOptions(
|
||||
notesAndFilesMatchingOptions(
|
||||
{
|
||||
views: [this.itemManager.trashSmartView],
|
||||
},
|
||||
@@ -613,12 +644,12 @@ describe('note display criteria', function () {
|
||||
|
||||
it('archived note', async function () {
|
||||
const normal = await this.createNote()
|
||||
await this.itemManager.changeItem(normal, (mutator) => {
|
||||
await this.mutator.changeItem(normal, (mutator) => {
|
||||
mutator.archived = true
|
||||
})
|
||||
|
||||
expect(
|
||||
itemsMatchingOptions(
|
||||
notesAndFilesMatchingOptions(
|
||||
{
|
||||
views: [this.itemManager.allNotesSmartView],
|
||||
includeArchived: false,
|
||||
@@ -629,7 +660,7 @@ describe('note display criteria', function () {
|
||||
).to.equal(0)
|
||||
|
||||
expect(
|
||||
itemsMatchingOptions(
|
||||
notesAndFilesMatchingOptions(
|
||||
{
|
||||
views: [this.itemManager.allNotesSmartView],
|
||||
includeArchived: true,
|
||||
@@ -640,7 +671,7 @@ describe('note display criteria', function () {
|
||||
).to.equal(1)
|
||||
|
||||
expect(
|
||||
itemsMatchingOptions(
|
||||
notesAndFilesMatchingOptions(
|
||||
{
|
||||
views: [this.itemManager.trashSmartView],
|
||||
includeArchived: true,
|
||||
@@ -651,7 +682,7 @@ describe('note display criteria', function () {
|
||||
).to.equal(0)
|
||||
|
||||
expect(
|
||||
itemsMatchingOptions(
|
||||
notesAndFilesMatchingOptions(
|
||||
{
|
||||
views: [this.itemManager.archivedSmartView],
|
||||
includeArchived: false,
|
||||
@@ -664,13 +695,13 @@ describe('note display criteria', function () {
|
||||
|
||||
it('archived + trashed note', async function () {
|
||||
const normal = await this.createNote()
|
||||
await this.itemManager.changeItem(normal, (mutator) => {
|
||||
await this.mutator.changeItem(normal, (mutator) => {
|
||||
mutator.trashed = true
|
||||
mutator.archived = true
|
||||
})
|
||||
|
||||
expect(
|
||||
itemsMatchingOptions(
|
||||
notesAndFilesMatchingOptions(
|
||||
{
|
||||
views: [this.itemManager.allNotesSmartView],
|
||||
includeArchived: true,
|
||||
@@ -681,7 +712,7 @@ describe('note display criteria', function () {
|
||||
).to.equal(0)
|
||||
|
||||
expect(
|
||||
itemsMatchingOptions(
|
||||
notesAndFilesMatchingOptions(
|
||||
{
|
||||
views: [this.itemManager.trashSmartView],
|
||||
includeArchived: true,
|
||||
@@ -692,7 +723,7 @@ describe('note display criteria', function () {
|
||||
).to.equal(1)
|
||||
|
||||
expect(
|
||||
itemsMatchingOptions(
|
||||
notesAndFilesMatchingOptions(
|
||||
{
|
||||
views: [this.itemManager.archivedSmartView],
|
||||
includeArchived: true,
|
||||
|
||||
@@ -48,7 +48,7 @@ describe('protections', function () {
|
||||
})
|
||||
|
||||
let note = await Factory.createMappedNote(application)
|
||||
note = await application.mutator.protectNote(note)
|
||||
note = await application.protections.protectNote(note)
|
||||
|
||||
expect(await application.authorizeNoteAccess(note)).to.be.true
|
||||
expect(challengePrompts).to.equal(1)
|
||||
@@ -57,7 +57,7 @@ describe('protections', function () {
|
||||
it('sets `note.protected` to true', async function () {
|
||||
application = await Factory.createInitAppWithFakeCrypto()
|
||||
let note = await Factory.createMappedNote(application)
|
||||
note = await application.mutator.protectNote(note)
|
||||
note = await application.protections.protectNote(note)
|
||||
expect(note.protected).to.be.true
|
||||
})
|
||||
|
||||
@@ -87,7 +87,7 @@ describe('protections', function () {
|
||||
|
||||
await application.addPasscode(passcode)
|
||||
let note = await Factory.createMappedNote(application)
|
||||
note = await application.mutator.protectNote(note)
|
||||
note = await application.protections.protectNote(note)
|
||||
|
||||
expect(await application.authorizeNoteAccess(note)).to.be.true
|
||||
expect(challengePrompts).to.equal(1)
|
||||
@@ -120,8 +120,8 @@ describe('protections', function () {
|
||||
await application.addPasscode(passcode)
|
||||
let note = await Factory.createMappedNote(application)
|
||||
const uuid = note.uuid
|
||||
note = await application.mutator.protectNote(note)
|
||||
note = await application.mutator.unprotectNote(note)
|
||||
note = await application.protections.protectNote(note)
|
||||
note = await application.protections.unprotectNote(note)
|
||||
expect(note.uuid).to.equal(uuid)
|
||||
expect(note.protected).to.equal(false)
|
||||
expect(challengePrompts).to.equal(1)
|
||||
@@ -142,8 +142,8 @@ describe('protections', function () {
|
||||
|
||||
await application.addPasscode(passcode)
|
||||
let note = await Factory.createMappedNote(application)
|
||||
note = await application.mutator.protectNote(note)
|
||||
const result = await application.mutator.unprotectNote(note)
|
||||
note = await application.protections.protectNote(note)
|
||||
const result = await application.protections.unprotectNote(note)
|
||||
expect(result).to.be.undefined
|
||||
expect(challengePrompts).to.equal(1)
|
||||
})
|
||||
@@ -174,7 +174,7 @@ describe('protections', function () {
|
||||
|
||||
await application.addPasscode(passcode)
|
||||
let note = await Factory.createMappedNote(application)
|
||||
note = await application.mutator.protectNote(note)
|
||||
note = await application.protections.protectNote(note)
|
||||
|
||||
expect(await application.authorizeNoteAccess(note)).to.be.true
|
||||
expect(await application.authorizeNoteAccess(note)).to.be.true
|
||||
@@ -226,7 +226,7 @@ describe('protections', function () {
|
||||
application = await Factory.createInitAppWithFakeCrypto()
|
||||
|
||||
let note = await Factory.createMappedNote(application)
|
||||
note = await application.mutator.protectNote(note)
|
||||
note = await application.protections.protectNote(note)
|
||||
|
||||
expect(await application.authorizeNoteAccess(note)).to.be.true
|
||||
})
|
||||
@@ -431,8 +431,8 @@ describe('protections', function () {
|
||||
const NOTE_COUNT = 3
|
||||
let notes = await Factory.createManyMappedNotes(application, NOTE_COUNT)
|
||||
|
||||
notes[0] = await application.mutator.protectNote(notes[0])
|
||||
notes[1] = await application.mutator.protectNote(notes[1])
|
||||
notes[0] = await application.protections.protectNote(notes[0])
|
||||
notes[1] = await application.protections.protectNote(notes[1])
|
||||
|
||||
expect(await application.authorizeProtectedActionForNotes(notes, ChallengeReason.SelectProtectedNote)).lengthOf(
|
||||
NOTE_COUNT,
|
||||
@@ -468,8 +468,8 @@ describe('protections', function () {
|
||||
|
||||
const NOTE_COUNT = 3
|
||||
let notes = await Factory.createManyMappedNotes(application, NOTE_COUNT)
|
||||
notes[0] = await application.mutator.protectNote(notes[0])
|
||||
notes[1] = await application.mutator.protectNote(notes[1])
|
||||
notes[0] = await application.protections.protectNote(notes[0])
|
||||
notes[1] = await application.protections.protectNote(notes[1])
|
||||
|
||||
expect(await application.authorizeProtectedActionForNotes(notes, ChallengeReason.SelectProtectedNote)).lengthOf(
|
||||
NOTE_COUNT,
|
||||
@@ -493,8 +493,8 @@ describe('protections', function () {
|
||||
|
||||
const NOTE_COUNT = 3
|
||||
let notes = await Factory.createManyMappedNotes(application, NOTE_COUNT)
|
||||
notes[0] = await application.mutator.protectNote(notes[0])
|
||||
notes[1] = await application.mutator.protectNote(notes[1])
|
||||
notes[0] = await application.protections.protectNote(notes[0])
|
||||
notes[1] = await application.protections.protectNote(notes[1])
|
||||
|
||||
expect(await application.authorizeProtectedActionForNotes(notes, ChallengeReason.SelectProtectedNote)).lengthOf(1)
|
||||
expect(challengePrompts).to.equal(1)
|
||||
@@ -513,7 +513,7 @@ describe('protections', function () {
|
||||
|
||||
const NOTE_COUNT = 3
|
||||
let notes = await Factory.createManyMappedNotes(application, NOTE_COUNT)
|
||||
notes = await application.mutator.protectNotes(notes)
|
||||
notes = await application.protections.protectNotes(notes)
|
||||
|
||||
for (const note of notes) {
|
||||
expect(note.protected).to.be.true
|
||||
@@ -550,8 +550,8 @@ describe('protections', function () {
|
||||
|
||||
const NOTE_COUNT = 3
|
||||
let notes = await Factory.createManyMappedNotes(application, NOTE_COUNT)
|
||||
notes = await application.mutator.protectNotes(notes)
|
||||
notes = await application.mutator.unprotectNotes(notes)
|
||||
notes = await application.protections.protectNotes(notes)
|
||||
notes = await application.protections.unprotectNotes(notes)
|
||||
|
||||
for (const note of notes) {
|
||||
expect(note.protected).to.be.false
|
||||
@@ -587,8 +587,8 @@ describe('protections', function () {
|
||||
|
||||
const NOTE_COUNT = 3
|
||||
let notes = await Factory.createManyMappedNotes(application, NOTE_COUNT)
|
||||
notes = await application.mutator.protectNotes(notes)
|
||||
notes = await application.mutator.unprotectNotes(notes)
|
||||
notes = await application.protections.protectNotes(notes)
|
||||
notes = await application.protections.unprotectNotes(notes)
|
||||
|
||||
for (const note of notes) {
|
||||
expect(note.protected).to.be.false
|
||||
@@ -612,8 +612,8 @@ describe('protections', function () {
|
||||
|
||||
const NOTE_COUNT = 3
|
||||
let notes = await Factory.createManyMappedNotes(application, NOTE_COUNT)
|
||||
notes = await application.mutator.protectNotes(notes)
|
||||
notes = await application.mutator.unprotectNotes(notes)
|
||||
notes = await application.protections.protectNotes(notes)
|
||||
notes = await application.protections.unprotectNotes(notes)
|
||||
|
||||
for (const note of notes) {
|
||||
expect(note.protected).to.be(true)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* eslint-disable no-unused-expressions */
|
||||
/* eslint-disable no-undef */
|
||||
import { BaseItemCounts } from './lib/Applications.js'
|
||||
import { BaseItemCounts } from './lib/BaseItemCounts.js'
|
||||
import * as Factory from './lib/factory.js'
|
||||
import WebDeviceInterface from './lib/web_device_interface.js'
|
||||
chai.use(chaiAsPromised)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import * as Factory from './lib/factory.js'
|
||||
import * as Files from './lib/Files.js'
|
||||
import * as Events from './lib/Events.js'
|
||||
|
||||
chai.use(chaiAsPromised)
|
||||
const expect = chai.expect
|
||||
@@ -98,26 +99,43 @@ describe('settings service', function () {
|
||||
})
|
||||
|
||||
it('reads a nonexistent sensitive setting', async () => {
|
||||
const setting = await application.settings.getDoesSensitiveSettingExist(SettingName.create(SettingName.NAMES.MfaSecret).getValue())
|
||||
const setting = await application.settings.getDoesSensitiveSettingExist(
|
||||
SettingName.create(SettingName.NAMES.MfaSecret).getValue(),
|
||||
)
|
||||
expect(setting).to.equal(false)
|
||||
})
|
||||
|
||||
it('creates and reads a sensitive setting', async () => {
|
||||
await application.settings.updateSetting(SettingName.create(SettingName.NAMES.MfaSecret).getValue(), 'fake_secret', true)
|
||||
const setting = await application.settings.getDoesSensitiveSettingExist(SettingName.create(SettingName.NAMES.MfaSecret).getValue())
|
||||
await application.settings.updateSetting(
|
||||
SettingName.create(SettingName.NAMES.MfaSecret).getValue(),
|
||||
'fake_secret',
|
||||
true,
|
||||
)
|
||||
const setting = await application.settings.getDoesSensitiveSettingExist(
|
||||
SettingName.create(SettingName.NAMES.MfaSecret).getValue(),
|
||||
)
|
||||
expect(setting).to.equal(true)
|
||||
})
|
||||
|
||||
it('creates and lists a sensitive setting', async () => {
|
||||
await application.settings.updateSetting(SettingName.create(SettingName.NAMES.MfaSecret).getValue(), 'fake_secret', true)
|
||||
await application.settings.updateSetting(SettingName.create(SettingName.NAMES.MuteFailedBackupsEmails).getValue(), MuteFailedBackupsEmailsOption.Muted)
|
||||
await application.settings.updateSetting(
|
||||
SettingName.create(SettingName.NAMES.MfaSecret).getValue(),
|
||||
'fake_secret',
|
||||
true,
|
||||
)
|
||||
await application.settings.updateSetting(
|
||||
SettingName.create(SettingName.NAMES.MuteFailedBackupsEmails).getValue(),
|
||||
MuteFailedBackupsEmailsOption.Muted,
|
||||
)
|
||||
const settings = await application.settings.listSettings()
|
||||
expect(settings.getSettingValue(SettingName.create(SettingName.NAMES.MuteFailedBackupsEmails).getValue())).to.eql(MuteFailedBackupsEmailsOption.Muted)
|
||||
expect(settings.getSettingValue(SettingName.create(SettingName.NAMES.MuteFailedBackupsEmails).getValue())).to.eql(
|
||||
MuteFailedBackupsEmailsOption.Muted,
|
||||
)
|
||||
expect(settings.getSettingValue(SettingName.create(SettingName.NAMES.MfaSecret).getValue())).to.not.be.ok
|
||||
})
|
||||
|
||||
it('reads a subscription setting - @paidfeature', async () => {
|
||||
await Factory.publishMockedEvent('SUBSCRIPTION_PURCHASED', {
|
||||
await Events.publishMockedEvent('SUBSCRIPTION_PURCHASED', {
|
||||
userEmail: context.email,
|
||||
subscriptionId: subscriptionId++,
|
||||
subscriptionName: 'PRO_PLAN',
|
||||
@@ -130,19 +148,21 @@ describe('settings service', function () {
|
||||
totalActiveSubscriptionsCount: 1,
|
||||
userRegisteredAt: 1,
|
||||
billingFrequency: 12,
|
||||
payAmount: 59.00
|
||||
payAmount: 59.0,
|
||||
})
|
||||
|
||||
await Factory.sleep(2)
|
||||
|
||||
const setting = await application.settings.getSubscriptionSetting(SettingName.create(SettingName.NAMES.FileUploadBytesLimit).getValue())
|
||||
const setting = await application.settings.getSubscriptionSetting(
|
||||
SettingName.create(SettingName.NAMES.FileUploadBytesLimit).getValue(),
|
||||
)
|
||||
expect(setting).to.be.a('string')
|
||||
})
|
||||
|
||||
it('persist irreplaceable subscription settings between subsequent subscriptions - @paidfeature', async () => {
|
||||
await reInitializeApplicationWithRealCrypto()
|
||||
|
||||
await Factory.publishMockedEvent('SUBSCRIPTION_PURCHASED', {
|
||||
await Events.publishMockedEvent('SUBSCRIPTION_PURCHASED', {
|
||||
userEmail: context.email,
|
||||
subscriptionId: subscriptionId,
|
||||
subscriptionName: 'PRO_PLAN',
|
||||
@@ -155,7 +175,7 @@ describe('settings service', function () {
|
||||
totalActiveSubscriptionsCount: 1,
|
||||
userRegisteredAt: 1,
|
||||
billingFrequency: 12,
|
||||
payAmount: 59.00
|
||||
payAmount: 59.0,
|
||||
})
|
||||
await Factory.sleep(1)
|
||||
|
||||
@@ -166,13 +186,17 @@ describe('settings service', function () {
|
||||
|
||||
await Factory.sleep(1)
|
||||
|
||||
const limitSettingBefore = await application.settings.getSubscriptionSetting(SettingName.create(SettingName.NAMES.FileUploadBytesLimit).getValue())
|
||||
const limitSettingBefore = await application.settings.getSubscriptionSetting(
|
||||
SettingName.create(SettingName.NAMES.FileUploadBytesLimit).getValue(),
|
||||
)
|
||||
expect(limitSettingBefore).to.equal('107374182400')
|
||||
|
||||
const usedSettingBefore = await application.settings.getSubscriptionSetting(SettingName.create(SettingName.NAMES.FileUploadBytesUsed).getValue())
|
||||
const usedSettingBefore = await application.settings.getSubscriptionSetting(
|
||||
SettingName.create(SettingName.NAMES.FileUploadBytesUsed).getValue(),
|
||||
)
|
||||
expect(usedSettingBefore).to.equal('196')
|
||||
|
||||
await Factory.publishMockedEvent('SUBSCRIPTION_EXPIRED', {
|
||||
await Events.publishMockedEvent('SUBSCRIPTION_EXPIRED', {
|
||||
userEmail: context.email,
|
||||
subscriptionId: subscriptionId++,
|
||||
subscriptionName: 'PRO_PLAN',
|
||||
@@ -181,11 +205,11 @@ describe('settings service', function () {
|
||||
totalActiveSubscriptionsCount: 1,
|
||||
userExistingSubscriptionsCount: 1,
|
||||
billingFrequency: 12,
|
||||
payAmount: 59.00
|
||||
payAmount: 59.0,
|
||||
})
|
||||
await Factory.sleep(1)
|
||||
|
||||
await Factory.publishMockedEvent('SUBSCRIPTION_PURCHASED', {
|
||||
await Events.publishMockedEvent('SUBSCRIPTION_PURCHASED', {
|
||||
userEmail: context.email,
|
||||
subscriptionId: subscriptionId++,
|
||||
subscriptionName: 'PRO_PLAN',
|
||||
@@ -198,14 +222,18 @@ describe('settings service', function () {
|
||||
totalActiveSubscriptionsCount: 2,
|
||||
userRegisteredAt: 1,
|
||||
billingFrequency: 12,
|
||||
payAmount: 59.00
|
||||
payAmount: 59.0,
|
||||
})
|
||||
await Factory.sleep(1)
|
||||
|
||||
const limitSettingAfter = await application.settings.getSubscriptionSetting(SettingName.create(SettingName.NAMES.FileUploadBytesLimit).getValue())
|
||||
const limitSettingAfter = await application.settings.getSubscriptionSetting(
|
||||
SettingName.create(SettingName.NAMES.FileUploadBytesLimit).getValue(),
|
||||
)
|
||||
expect(limitSettingAfter).to.equal(limitSettingBefore)
|
||||
|
||||
const usedSettingAfter = await application.settings.getSubscriptionSetting(SettingName.create(SettingName.NAMES.FileUploadBytesUsed).getValue())
|
||||
const usedSettingAfter = await application.settings.getSubscriptionSetting(
|
||||
SettingName.create(SettingName.NAMES.FileUploadBytesUsed).getValue(),
|
||||
)
|
||||
expect(usedSettingAfter).to.equal(usedSettingBefore)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* eslint-disable no-unused-expressions */
|
||||
/* eslint-disable no-undef */
|
||||
import { BaseItemCounts } from './lib/Applications.js'
|
||||
import { BaseItemCounts } from './lib/BaseItemCounts.js'
|
||||
import * as Factory from './lib/factory.js'
|
||||
import WebDeviceInterface from './lib/web_device_interface.js'
|
||||
chai.use(chaiAsPromised)
|
||||
@@ -38,7 +38,9 @@ describe('singletons', function () {
|
||||
|
||||
this.email = UuidGenerator.GenerateUuid()
|
||||
this.password = UuidGenerator.GenerateUuid()
|
||||
|
||||
this.registerUser = async () => {
|
||||
this.expectedItemCount = BaseItemCounts.DefaultItemsWithAccount
|
||||
await Factory.registerUserToApplication({
|
||||
application: this.application,
|
||||
email: this.email,
|
||||
@@ -62,7 +64,7 @@ describe('singletons', function () {
|
||||
])
|
||||
|
||||
this.createExtMgr = () => {
|
||||
return this.application.itemManager.createItem(
|
||||
return this.application.mutator.createItem(
|
||||
ContentType.Component,
|
||||
{
|
||||
package_info: {
|
||||
@@ -93,11 +95,11 @@ describe('singletons', function () {
|
||||
const prefs2 = createPrefsPayload()
|
||||
const prefs3 = createPrefsPayload()
|
||||
|
||||
const items = await this.application.itemManager.emitItemsFromPayloads(
|
||||
const items = await this.application.mutator.emitItemsFromPayloads(
|
||||
[prefs1, prefs2, prefs3],
|
||||
PayloadEmitSource.LocalChanged,
|
||||
)
|
||||
await this.application.itemManager.setItemsDirty(items)
|
||||
await this.application.mutator.setItemsDirty(items)
|
||||
await this.application.syncService.sync(syncOptions)
|
||||
expect(this.application.itemManager.items.length).to.equal(this.expectedItemCount)
|
||||
})
|
||||
@@ -192,7 +194,7 @@ describe('singletons', function () {
|
||||
if (!beginCheckingResponse) {
|
||||
return
|
||||
}
|
||||
if (!didCompleteRelevantSync && eventName === SyncEvent.SingleRoundTripSyncCompleted) {
|
||||
if (!didCompleteRelevantSync && eventName === SyncEvent.PaginatedSyncRequestCompleted) {
|
||||
didCompleteRelevantSync = true
|
||||
const saved = data.savedPayloads
|
||||
expect(saved.length).to.equal(1)
|
||||
@@ -327,7 +329,7 @@ describe('singletons', function () {
|
||||
|
||||
it('alternating the uuid of a singleton should return correct result', async function () {
|
||||
const payload = createPrefsPayload()
|
||||
const item = await this.application.itemManager.emitItemFromPayload(payload, PayloadEmitSource.LocalChanged)
|
||||
const item = await this.application.mutator.emitItemFromPayload(payload, PayloadEmitSource.LocalChanged)
|
||||
await this.application.syncService.sync(syncOptions)
|
||||
const predicate = new Predicate('content_type', '=', item.content_type)
|
||||
let resolvedItem = await this.application.singletonManager.findOrCreateContentTypeSingleton(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* eslint-disable no-unused-expressions */
|
||||
/* eslint-disable no-undef */
|
||||
import { BaseItemCounts } from './lib/Applications.js'
|
||||
import { BaseItemCounts } from './lib/BaseItemCounts.js'
|
||||
import * as Factory from './lib/factory.js'
|
||||
chai.use(chaiAsPromised)
|
||||
const expect = chai.expect
|
||||
@@ -279,7 +279,7 @@ describe('storage manager', function () {
|
||||
})
|
||||
|
||||
await Factory.createSyncedNote(this.application)
|
||||
expect(await Factory.storagePayloadCount(this.application)).to.equal(BaseItemCounts.DefaultItems + 1)
|
||||
expect(await Factory.storagePayloadCount(this.application)).to.equal(BaseItemCounts.DefaultItemsWithAccount + 1)
|
||||
this.application = await Factory.signOutApplicationAndReturnNew(this.application)
|
||||
await Factory.sleep(0.1, 'Allow all untrackable singleton syncs to complete')
|
||||
expect(await Factory.storagePayloadCount(this.application)).to.equal(BaseItemCounts.DefaultItems)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import * as Factory from './lib/factory.js'
|
||||
import * as Events from './lib/Events.js'
|
||||
chai.use(chaiAsPromised)
|
||||
const expect = chai.expect
|
||||
|
||||
@@ -31,7 +32,7 @@ describe('subscriptions', function () {
|
||||
password: context.password,
|
||||
})
|
||||
|
||||
await Factory.publishMockedEvent('SUBSCRIPTION_PURCHASED', {
|
||||
await Events.publishMockedEvent('SUBSCRIPTION_PURCHASED', {
|
||||
userEmail: context.email,
|
||||
subscriptionId: subscriptionId++,
|
||||
subscriptionName: 'PRO_PLAN',
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/* eslint-disable no-undef */
|
||||
import { BaseItemCounts } from '../lib/Applications.js'
|
||||
import { BaseItemCounts } from '../lib/BaseItemCounts.js'
|
||||
import * as Factory from '../lib/factory.js'
|
||||
import { createSyncedNoteWithTag } from '../lib/Items.js'
|
||||
import * as Utils from '../lib/Utils.js'
|
||||
@@ -16,7 +16,7 @@ describe('online conflict handling', function () {
|
||||
|
||||
beforeEach(async function () {
|
||||
localStorage.clear()
|
||||
this.expectedItemCount = BaseItemCounts.DefaultItems
|
||||
this.expectedItemCount = BaseItemCounts.DefaultItemsWithAccount
|
||||
|
||||
this.context = await Factory.createAppContextWithFakeCrypto('AppA')
|
||||
await this.context.launch()
|
||||
@@ -64,7 +64,7 @@ describe('online conflict handling', function () {
|
||||
it('components should not be duplicated under any circumstances', async function () {
|
||||
const payload = createDirtyPayload(ContentType.Component)
|
||||
|
||||
const item = await this.application.itemManager.emitItemFromPayload(payload, PayloadEmitSource.LocalChanged)
|
||||
const item = await this.application.mutator.emitItemFromPayload(payload, PayloadEmitSource.LocalChanged)
|
||||
|
||||
this.expectedItemCount++
|
||||
|
||||
@@ -91,7 +91,7 @@ describe('online conflict handling', function () {
|
||||
|
||||
it('items keys should not be duplicated under any circumstances', async function () {
|
||||
const payload = createDirtyPayload(ContentType.ItemsKey)
|
||||
const item = await this.application.itemManager.emitItemFromPayload(payload, PayloadEmitSource.LocalChanged)
|
||||
const item = await this.application.mutator.emitItemFromPayload(payload, PayloadEmitSource.LocalChanged)
|
||||
this.expectedItemCount++
|
||||
await this.application.syncService.sync(syncOptions)
|
||||
/** First modify the item without saving so that
|
||||
@@ -118,7 +118,7 @@ describe('online conflict handling', function () {
|
||||
// create an item and sync it
|
||||
const note = await Factory.createMappedNote(this.application)
|
||||
this.expectedItemCount++
|
||||
await this.application.itemManager.setItemDirty(note)
|
||||
await this.application.mutator.setItemDirty(note)
|
||||
await this.application.syncService.sync(syncOptions)
|
||||
|
||||
const rawPayloads = await this.application.diskStorageService.getAllRawPayloads()
|
||||
@@ -128,11 +128,11 @@ describe('online conflict handling', function () {
|
||||
const dirtyValue = `${Math.random()}`
|
||||
|
||||
/** Modify nonsense first to get around strategyWhenConflictingWithItem with previousRevision check */
|
||||
await this.application.itemManager.changeNote(note, (mutator) => {
|
||||
await this.application.mutator.changeNote(note, (mutator) => {
|
||||
mutator.title = 'any'
|
||||
})
|
||||
|
||||
await this.application.itemManager.changeNote(note, (mutator) => {
|
||||
await this.application.mutator.changeNote(note, (mutator) => {
|
||||
// modify this item locally to have differing contents from server
|
||||
mutator.title = dirtyValue
|
||||
// Intentionally don't change updated_at. We want to simulate a chaotic case where
|
||||
@@ -238,7 +238,7 @@ describe('online conflict handling', function () {
|
||||
it('should duplicate item if saving a modified item and clearing our sync token', async function () {
|
||||
let note = await Factory.createMappedNote(this.application)
|
||||
|
||||
await this.application.itemManager.setItemDirty(note)
|
||||
await this.application.mutator.setItemDirty(note)
|
||||
await this.application.syncService.sync(syncOptions)
|
||||
|
||||
this.expectedItemCount++
|
||||
@@ -279,11 +279,11 @@ describe('online conflict handling', function () {
|
||||
it('should handle sync conflicts by not duplicating same data', async function () {
|
||||
const note = await Factory.createMappedNote(this.application)
|
||||
this.expectedItemCount++
|
||||
await this.application.itemManager.setItemDirty(note)
|
||||
await this.application.mutator.setItemDirty(note)
|
||||
await this.application.syncService.sync(syncOptions)
|
||||
|
||||
// keep item as is and set dirty
|
||||
await this.application.itemManager.setItemDirty(note)
|
||||
await this.application.mutator.setItemDirty(note)
|
||||
|
||||
// clear sync token so that all items are retrieved on next sync
|
||||
this.application.syncService.clearSyncPositionTokens()
|
||||
@@ -295,10 +295,10 @@ describe('online conflict handling', function () {
|
||||
|
||||
it('clearing conflict_of on two clients simultaneously should keep us in sync', async function () {
|
||||
const note = await Factory.createMappedNote(this.application)
|
||||
await this.application.itemManager.setItemDirty(note)
|
||||
await this.application.mutator.setItemDirty(note)
|
||||
this.expectedItemCount++
|
||||
|
||||
await this.application.mutator.changeAndSaveItem(
|
||||
await this.application.changeAndSaveItem(
|
||||
note,
|
||||
(mutator) => {
|
||||
// client A
|
||||
@@ -311,7 +311,7 @@ describe('online conflict handling', function () {
|
||||
|
||||
// client B
|
||||
await this.application.syncService.clearSyncPositionTokens()
|
||||
await this.application.itemManager.changeItem(
|
||||
await this.application.mutator.changeItem(
|
||||
note,
|
||||
(mutator) => {
|
||||
mutator.mutableContent.conflict_of = 'bar'
|
||||
@@ -329,10 +329,10 @@ describe('online conflict handling', function () {
|
||||
|
||||
it('setting property on two clients simultaneously should create conflict', async function () {
|
||||
const note = await Factory.createMappedNote(this.application)
|
||||
await this.application.itemManager.setItemDirty(note)
|
||||
await this.application.mutator.setItemDirty(note)
|
||||
this.expectedItemCount++
|
||||
|
||||
await this.application.mutator.changeAndSaveItem(
|
||||
await this.application.changeAndSaveItem(
|
||||
note,
|
||||
(mutator) => {
|
||||
// client A
|
||||
@@ -369,12 +369,12 @@ describe('online conflict handling', function () {
|
||||
const note = await Factory.createMappedNote(this.application)
|
||||
const originalPayload = note.payloadRepresentation()
|
||||
this.expectedItemCount++
|
||||
await this.application.itemManager.setItemDirty(note)
|
||||
await this.application.mutator.setItemDirty(note)
|
||||
await this.application.syncService.sync(syncOptions)
|
||||
expect(this.application.itemManager.items.length).to.equal(this.expectedItemCount)
|
||||
|
||||
// client A
|
||||
await this.application.itemManager.setItemToBeDeleted(note)
|
||||
await this.application.mutator.setItemToBeDeleted(note)
|
||||
await this.application.syncService.sync(syncOptions)
|
||||
this.expectedItemCount--
|
||||
expect(this.application.itemManager.items.length).to.equal(this.expectedItemCount)
|
||||
@@ -387,10 +387,10 @@ describe('online conflict handling', function () {
|
||||
deleted: false,
|
||||
updated_at: Factory.yesterday(),
|
||||
})
|
||||
await this.application.itemManager.emitItemsFromPayloads([mutatedPayload], PayloadEmitSource.LocalChanged)
|
||||
await this.application.mutator.emitItemsFromPayloads([mutatedPayload], PayloadEmitSource.LocalChanged)
|
||||
const resultNote = this.application.itemManager.findItem(note.uuid)
|
||||
expect(resultNote.uuid).to.equal(note.uuid)
|
||||
await this.application.itemManager.setItemDirty(resultNote)
|
||||
await this.application.mutator.setItemDirty(resultNote)
|
||||
await this.application.syncService.sync(syncOptions)
|
||||
|
||||
// We expect that this item is now gone for good, and a duplicate has not been created.
|
||||
@@ -400,7 +400,7 @@ describe('online conflict handling', function () {
|
||||
|
||||
it('if server says not deleted but client says deleted, keep server state', async function () {
|
||||
const note = await Factory.createMappedNote(this.application)
|
||||
await this.application.itemManager.setItemDirty(note)
|
||||
await this.application.mutator.setItemDirty(note)
|
||||
this.expectedItemCount++
|
||||
|
||||
// client A
|
||||
@@ -426,7 +426,7 @@ describe('online conflict handling', function () {
|
||||
|
||||
it('should create conflict if syncing an item that is stale', async function () {
|
||||
let note = await Factory.createMappedNote(this.application)
|
||||
await this.application.itemManager.setItemDirty(note)
|
||||
await this.application.mutator.setItemDirty(note)
|
||||
await this.application.syncService.sync(syncOptions)
|
||||
note = this.application.items.findItem(note.uuid)
|
||||
expect(note.dirty).to.equal(false)
|
||||
@@ -462,7 +462,7 @@ describe('online conflict handling', function () {
|
||||
|
||||
it('creating conflict with exactly equal content should keep us in sync', async function () {
|
||||
const note = await Factory.createMappedNote(this.application)
|
||||
await this.application.itemManager.setItemDirty(note)
|
||||
await this.application.mutator.setItemDirty(note)
|
||||
this.expectedItemCount++
|
||||
|
||||
await this.application.syncService.sync(syncOptions)
|
||||
@@ -505,7 +505,7 @@ describe('online conflict handling', function () {
|
||||
for (const note of this.application.itemManager.getDisplayableNotes()) {
|
||||
/** First modify the item without saving so that
|
||||
* our local contents digress from the server's */
|
||||
await this.application.itemManager.changeItem(note, (mutator) => {
|
||||
await this.application.mutator.changeItem(note, (mutator) => {
|
||||
mutator.text = '1'
|
||||
})
|
||||
|
||||
@@ -530,18 +530,18 @@ describe('online conflict handling', function () {
|
||||
const payload1 = Factory.createStorageItemPayload(ContentType.Tag)
|
||||
const payload2 = Factory.createStorageItemPayload(ContentType.UserPrefs)
|
||||
this.expectedItemCount -= 1 /** auto-created user preferences */
|
||||
await this.application.itemManager.emitItemsFromPayloads([payload1, payload2], PayloadEmitSource.LocalChanged)
|
||||
await this.application.mutator.emitItemsFromPayloads([payload1, payload2], PayloadEmitSource.LocalChanged)
|
||||
this.expectedItemCount += 2
|
||||
let tag = this.application.itemManager.getItems(ContentType.Tag)[0]
|
||||
let userPrefs = this.application.itemManager.getItems(ContentType.UserPrefs)[0]
|
||||
expect(tag).to.be.ok
|
||||
expect(userPrefs).to.be.ok
|
||||
|
||||
tag = await this.application.itemManager.changeItem(tag, (mutator) => {
|
||||
tag = await this.application.mutator.changeItem(tag, (mutator) => {
|
||||
mutator.e2ePendingRefactor_addItemAsRelationship(userPrefs)
|
||||
})
|
||||
|
||||
await this.application.itemManager.setItemDirty(userPrefs)
|
||||
await this.application.mutator.setItemDirty(userPrefs)
|
||||
userPrefs = this.application.items.findItem(userPrefs.uuid)
|
||||
|
||||
expect(this.application.itemManager.itemsReferencingItem(userPrefs).length).to.equal(1)
|
||||
@@ -599,7 +599,7 @@ describe('online conflict handling', function () {
|
||||
*/
|
||||
let tag = await Factory.createMappedTag(this.application)
|
||||
let note = await Factory.createMappedNote(this.application)
|
||||
tag = await this.application.mutator.changeAndSaveItem(
|
||||
tag = await this.application.changeAndSaveItem(
|
||||
tag,
|
||||
(mutator) => {
|
||||
mutator.e2ePendingRefactor_addItemAsRelationship(note)
|
||||
@@ -608,7 +608,7 @@ describe('online conflict handling', function () {
|
||||
undefined,
|
||||
syncOptions,
|
||||
)
|
||||
await this.application.itemManager.setItemDirty(note)
|
||||
await this.application.mutator.setItemDirty(note)
|
||||
this.expectedItemCount += 2
|
||||
|
||||
await this.application.syncService.sync(syncOptions)
|
||||
@@ -663,18 +663,18 @@ describe('online conflict handling', function () {
|
||||
|
||||
const baseTitle = 'base title'
|
||||
/** Change the note */
|
||||
const noteAfterChange = await this.application.itemManager.changeItem(note, (mutator) => {
|
||||
const noteAfterChange = await this.application.mutator.changeItem(note, (mutator) => {
|
||||
mutator.title = baseTitle
|
||||
})
|
||||
await this.application.sync.sync()
|
||||
|
||||
/** Simulate a dropped response by reverting the note back its post-change, pre-sync state */
|
||||
const retroNote = await this.application.itemManager.emitItemFromPayload(noteAfterChange.payload)
|
||||
const retroNote = await this.application.mutator.emitItemFromPayload(noteAfterChange.payload)
|
||||
expect(retroNote.serverUpdatedAt.getTime()).to.equal(noteAfterChange.serverUpdatedAt.getTime())
|
||||
|
||||
/** Change the item to its final title and sync */
|
||||
const finalTitle = 'final title'
|
||||
await this.application.itemManager.changeItem(note, (mutator) => {
|
||||
await this.application.mutator.changeItem(note, (mutator) => {
|
||||
mutator.title = finalTitle
|
||||
})
|
||||
await this.application.sync.sync()
|
||||
@@ -708,7 +708,7 @@ describe('online conflict handling', function () {
|
||||
errorDecrypting: true,
|
||||
dirty: true,
|
||||
})
|
||||
await this.application.itemManager.emitItemsFromPayloads([errorred], PayloadEmitSource.LocalChanged)
|
||||
await this.application.mutator.emitItemsFromPayloads([errorred], PayloadEmitSource.LocalChanged)
|
||||
|
||||
/**
|
||||
* Retrieve this note from the server by clearing sync token
|
||||
@@ -758,7 +758,7 @@ describe('online conflict handling', function () {
|
||||
email: Utils.generateUuid(),
|
||||
password: Utils.generateUuid(),
|
||||
})
|
||||
await newApp.itemManager.emitItemsFromPayloads(priorData.map((i) => i.payload))
|
||||
await newApp.mutator.emitItemsFromPayloads(priorData.map((i) => i.payload))
|
||||
await newApp.syncService.markAllItemsAsNeedingSyncAndPersist()
|
||||
await newApp.syncService.sync(syncOptions)
|
||||
expect(newApp.payloadManager.invalidPayloads.length).to.equal(0)
|
||||
@@ -786,7 +786,7 @@ describe('online conflict handling', function () {
|
||||
password: password,
|
||||
})
|
||||
Factory.handlePasswordChallenges(newApp, password)
|
||||
await newApp.mutator.importData(backupFile, true)
|
||||
await newApp.importData(backupFile, true)
|
||||
expect(newApp.itemManager.getDisplayableTags().length).to.equal(1)
|
||||
expect(newApp.itemManager.getDisplayableNotes().length).to.equal(1)
|
||||
await Factory.safeDeinit(newApp)
|
||||
@@ -801,7 +801,7 @@ describe('online conflict handling', function () {
|
||||
await createSyncedNoteWithTag(this.application)
|
||||
const tag = this.application.itemManager.getDisplayableTags()[0]
|
||||
const note2 = await Factory.createMappedNote(this.application)
|
||||
await this.application.mutator.changeAndSaveItem(tag, (mutator) => {
|
||||
await this.application.changeAndSaveItem(tag, (mutator) => {
|
||||
mutator.e2ePendingRefactor_addItemAsRelationship(note2)
|
||||
})
|
||||
let backupFile = await this.application.createEncryptedBackupFileForAutomatedDesktopBackups()
|
||||
@@ -821,7 +821,7 @@ describe('online conflict handling', function () {
|
||||
password: password,
|
||||
})
|
||||
Factory.handlePasswordChallenges(newApp, password)
|
||||
await newApp.mutator.importData(backupFile, true)
|
||||
await newApp.importData(backupFile, true)
|
||||
const newTag = newApp.itemManager.getDisplayableTags()[0]
|
||||
const notes = newApp.items.referencesForItem(newTag)
|
||||
expect(notes.length).to.equal(2)
|
||||
@@ -855,7 +855,7 @@ describe('online conflict handling', function () {
|
||||
},
|
||||
dirty: true,
|
||||
})
|
||||
await this.application.itemManager.emitItemFromPayload(modified)
|
||||
await this.application.mutator.emitItemFromPayload(modified)
|
||||
await this.application.sync.sync()
|
||||
expect(this.application.itemManager.getDisplayableNotes().length).to.equal(1)
|
||||
await this.sharedFinalAssertions()
|
||||
@@ -879,7 +879,7 @@ describe('online conflict handling', function () {
|
||||
dirty: true,
|
||||
})
|
||||
this.expectedItemCount++
|
||||
await this.application.itemManager.emitItemFromPayload(modified)
|
||||
await this.application.mutator.emitItemFromPayload(modified)
|
||||
await this.application.sync.sync()
|
||||
expect(this.application.itemManager.getDisplayableNotes().length).to.equal(2)
|
||||
await this.sharedFinalAssertions()
|
||||
@@ -911,7 +911,7 @@ describe('online conflict handling', function () {
|
||||
dirty: true,
|
||||
})
|
||||
this.expectedItemCount++
|
||||
await this.application.itemManager.emitItemFromPayload(modified)
|
||||
await this.application.mutator.emitItemFromPayload(modified)
|
||||
await this.application.sync.sync()
|
||||
expect(this.application.itemManager.getDisplayableNotes().length).to.equal(2)
|
||||
await this.sharedFinalAssertions()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* eslint-disable no-unused-expressions */
|
||||
/* eslint-disable no-undef */
|
||||
import { BaseItemCounts } from '../lib/Applications.js'
|
||||
import { BaseItemCounts } from '../lib/BaseItemCounts.js'
|
||||
import * as Factory from '../lib/factory.js'
|
||||
chai.use(chaiAsPromised)
|
||||
const expect = chai.expect
|
||||
@@ -15,7 +15,7 @@ describe('sync integrity', () => {
|
||||
})
|
||||
|
||||
beforeEach(async function () {
|
||||
this.expectedItemCount = BaseItemCounts.DefaultItems
|
||||
this.expectedItemCount = BaseItemCounts.DefaultItemsWithAccount
|
||||
this.application = await Factory.createInitAppWithFakeCrypto()
|
||||
this.email = UuidGenerator.GenerateUuid()
|
||||
this.password = UuidGenerator.GenerateUuid()
|
||||
@@ -44,7 +44,7 @@ describe('sync integrity', () => {
|
||||
})
|
||||
|
||||
it('should detect when out of sync', async function () {
|
||||
const item = await this.application.itemManager.emitItemFromPayload(
|
||||
const item = await this.application.mutator.emitItemFromPayload(
|
||||
Factory.createNotePayload(),
|
||||
PayloadEmitSource.LocalChanged,
|
||||
)
|
||||
@@ -60,7 +60,7 @@ describe('sync integrity', () => {
|
||||
})
|
||||
|
||||
it('should self heal after out of sync', async function () {
|
||||
const item = await this.application.itemManager.emitItemFromPayload(
|
||||
const item = await this.application.mutator.emitItemFromPayload(
|
||||
Factory.createNotePayload(),
|
||||
PayloadEmitSource.LocalChanged,
|
||||
)
|
||||
|
||||
@@ -33,7 +33,7 @@ describe('notes + tags syncing', function () {
|
||||
|
||||
it('syncing an item then downloading it should include items_key_id', async function () {
|
||||
const note = await Factory.createMappedNote(this.application)
|
||||
await this.application.itemManager.setItemDirty(note)
|
||||
await this.application.mutator.setItemDirty(note)
|
||||
await this.application.syncService.sync(syncOptions)
|
||||
await this.application.payloadManager.resetState()
|
||||
await this.application.itemManager.resetState()
|
||||
@@ -52,14 +52,14 @@ describe('notes + tags syncing', function () {
|
||||
const notePayload = pair[0]
|
||||
const tagPayload = pair[1]
|
||||
|
||||
await this.application.itemManager.emitItemsFromPayloads([notePayload, tagPayload], PayloadEmitSource.LocalChanged)
|
||||
await this.application.mutator.emitItemsFromPayloads([notePayload, tagPayload], PayloadEmitSource.LocalChanged)
|
||||
const note = this.application.itemManager.getItems([ContentType.Note])[0]
|
||||
const tag = this.application.itemManager.getItems([ContentType.Tag])[0]
|
||||
expect(this.application.itemManager.getDisplayableNotes().length).to.equal(1)
|
||||
expect(this.application.itemManager.getDisplayableTags().length).to.equal(1)
|
||||
|
||||
for (let i = 0; i < 9; i++) {
|
||||
await this.application.itemManager.setItemsDirty([note, tag])
|
||||
await this.application.mutator.setItemsDirty([note, tag])
|
||||
await this.application.syncService.sync(syncOptions)
|
||||
this.application.syncService.clearSyncPositionTokens()
|
||||
expect(tag.content.references.length).to.equal(1)
|
||||
@@ -76,10 +76,10 @@ describe('notes + tags syncing', function () {
|
||||
const pair = createRelatedNoteTagPairPayload()
|
||||
const notePayload = pair[0]
|
||||
const tagPayload = pair[1]
|
||||
await this.application.itemManager.emitItemsFromPayloads([notePayload, tagPayload], PayloadEmitSource.LocalChanged)
|
||||
await this.application.mutator.emitItemsFromPayloads([notePayload, tagPayload], PayloadEmitSource.LocalChanged)
|
||||
const originalNote = this.application.itemManager.getDisplayableNotes()[0]
|
||||
const originalTag = this.application.itemManager.getDisplayableTags()[0]
|
||||
await this.application.itemManager.setItemsDirty([originalNote, originalTag])
|
||||
await this.application.mutator.setItemsDirty([originalNote, originalTag])
|
||||
|
||||
await this.application.syncService.sync(syncOptions)
|
||||
|
||||
@@ -109,12 +109,12 @@ describe('notes + tags syncing', function () {
|
||||
const pair = createRelatedNoteTagPairPayload()
|
||||
const notePayload = pair[0]
|
||||
const tagPayload = pair[1]
|
||||
await this.application.itemManager.emitItemsFromPayloads([notePayload, tagPayload], PayloadEmitSource.LocalChanged)
|
||||
await this.application.mutator.emitItemsFromPayloads([notePayload, tagPayload], PayloadEmitSource.LocalChanged)
|
||||
let note = this.application.itemManager.getDisplayableNotes()[0]
|
||||
let tag = this.application.itemManager.getDisplayableTags()[0]
|
||||
expect(this.application.itemManager.itemsReferencingItem(note).length).to.equal(1)
|
||||
|
||||
await this.application.itemManager.setItemsDirty([note, tag])
|
||||
await this.application.mutator.setItemsDirty([note, tag])
|
||||
await this.application.syncService.sync(syncOptions)
|
||||
await this.application.syncService.clearSyncPositionTokens()
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* eslint-disable no-unused-expressions */
|
||||
/* eslint-disable no-undef */
|
||||
import { BaseItemCounts } from '../lib/Applications.js'
|
||||
import { BaseItemCounts } from '../lib/BaseItemCounts.js'
|
||||
import * as Factory from '../lib/factory.js'
|
||||
chai.use(chaiAsPromised)
|
||||
const expect = chai.expect
|
||||
@@ -31,6 +31,21 @@ describe('offline syncing', () => {
|
||||
localStorage.clear()
|
||||
})
|
||||
|
||||
it('uuid alternation should delete original payload', async function () {
|
||||
const note = await Factory.createMappedNote(this.application)
|
||||
this.expectedItemCount++
|
||||
|
||||
await Factory.alternateUuidForItem(this.application, note.uuid)
|
||||
await this.application.sync.sync(syncOptions)
|
||||
|
||||
const notes = this.application.itemManager.getDisplayableNotes()
|
||||
expect(notes.length).to.equal(1)
|
||||
expect(notes[0].uuid).to.not.equal(note.uuid)
|
||||
|
||||
const items = this.application.itemManager.allTrackedItems()
|
||||
expect(items.length).to.equal(this.expectedItemCount)
|
||||
})
|
||||
|
||||
it('should sync item with no passcode', async function () {
|
||||
let note = await Factory.createMappedNote(this.application)
|
||||
expect(Uuids(this.application.itemManager.getDirtyItems()).includes(note.uuid))
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/* eslint-disable no-undef */
|
||||
import { BaseItemCounts } from '../lib/Applications.js'
|
||||
import { BaseItemCounts } from '../lib/BaseItemCounts.js'
|
||||
import * as Factory from '../lib/factory.js'
|
||||
import * as Utils from '../lib/Utils.js'
|
||||
chai.use(chaiAsPromised)
|
||||
@@ -15,7 +15,7 @@ describe('online syncing', function () {
|
||||
|
||||
beforeEach(async function () {
|
||||
localStorage.clear()
|
||||
this.expectedItemCount = BaseItemCounts.DefaultItems
|
||||
this.expectedItemCount = BaseItemCounts.DefaultItemsWithAccount
|
||||
|
||||
this.context = await Factory.createAppContext()
|
||||
await this.context.launch()
|
||||
@@ -43,8 +43,10 @@ describe('online syncing', function () {
|
||||
|
||||
afterEach(async function () {
|
||||
expect(this.application.syncService.isOutOfSync()).to.equal(false)
|
||||
|
||||
const items = this.application.itemManager.allTrackedItems()
|
||||
expect(items.length).to.equal(this.expectedItemCount)
|
||||
|
||||
const rawPayloads = await this.application.diskStorageService.getAllRawPayloads()
|
||||
expect(rawPayloads.length).to.equal(this.expectedItemCount)
|
||||
await Factory.safeDeinit(this.application)
|
||||
@@ -119,18 +121,6 @@ describe('online syncing', function () {
|
||||
await Factory.sleep(0.5)
|
||||
}).timeout(20000)
|
||||
|
||||
it('uuid alternation should delete original payload', async function () {
|
||||
this.application = await Factory.signOutApplicationAndReturnNew(this.application)
|
||||
const note = await Factory.createMappedNote(this.application)
|
||||
this.expectedItemCount++
|
||||
await Factory.alternateUuidForItem(this.application, note.uuid)
|
||||
await this.application.sync.sync(syncOptions)
|
||||
|
||||
const notes = this.application.itemManager.getDisplayableNotes()
|
||||
expect(notes.length).to.equal(1)
|
||||
expect(notes[0].uuid).to.not.equal(note.uuid)
|
||||
})
|
||||
|
||||
it('having offline data then signing in should not alternate uuid and merge with account', async function () {
|
||||
this.application = await Factory.signOutApplicationAndReturnNew(this.application)
|
||||
const note = await Factory.createMappedNote(this.application)
|
||||
@@ -222,7 +212,7 @@ describe('online syncing', function () {
|
||||
this.application = await Factory.signOutApplicationAndReturnNew(this.application)
|
||||
const promise = new Promise((resolve) => {
|
||||
this.application.syncService.addEventObserver(async (event) => {
|
||||
if (event === SyncEvent.SingleRoundTripSyncCompleted) {
|
||||
if (event === SyncEvent.PaginatedSyncRequestCompleted) {
|
||||
const note = this.application.items.findItem(originalNote.uuid)
|
||||
if (note) {
|
||||
expect(note.dirty).to.not.be.ok
|
||||
@@ -241,7 +231,7 @@ describe('online syncing', function () {
|
||||
expect(this.application.itemManager.getDisplayableItemsKeys().length).to.equal(1)
|
||||
const note = await Factory.createMappedNote(this.application)
|
||||
this.expectedItemCount++
|
||||
await this.application.itemManager.setItemDirty(note)
|
||||
await this.application.mutator.setItemDirty(note)
|
||||
await this.application.syncService.sync(syncOptions)
|
||||
const rawPayloads = await this.application.diskStorageService.getAllRawPayloads()
|
||||
const notePayload = noteObjectsFromObjects(rawPayloads)
|
||||
@@ -283,7 +273,7 @@ describe('online syncing', function () {
|
||||
|
||||
const originalTitle = note.content.title
|
||||
|
||||
await this.application.itemManager.setItemDirty(note)
|
||||
await this.application.mutator.setItemDirty(note)
|
||||
await this.application.syncService.sync(syncOptions)
|
||||
|
||||
const encrypted = CreateEncryptedServerSyncPushPayload(
|
||||
@@ -299,7 +289,7 @@ describe('online syncing', function () {
|
||||
errorDecrypting: true,
|
||||
})
|
||||
|
||||
const items = await this.application.itemManager.emitItemsFromPayloads([errorred], PayloadEmitSource.LocalChanged)
|
||||
const items = await this.application.mutator.emitItemsFromPayloads([errorred], PayloadEmitSource.LocalChanged)
|
||||
|
||||
const mappedItem = this.application.itemManager.findAnyItem(errorred.uuid)
|
||||
|
||||
@@ -311,7 +301,7 @@ describe('online syncing', function () {
|
||||
},
|
||||
})
|
||||
|
||||
const mappedItems2 = await this.application.itemManager.emitItemsFromPayloads(
|
||||
const mappedItems2 = await this.application.mutator.emitItemsFromPayloads(
|
||||
[decryptedPayload],
|
||||
PayloadEmitSource.LocalChanged,
|
||||
)
|
||||
@@ -336,14 +326,14 @@ describe('online syncing', function () {
|
||||
let note = await Factory.createMappedNote(this.application)
|
||||
this.expectedItemCount++
|
||||
|
||||
await this.application.itemManager.setItemDirty(note)
|
||||
await this.application.mutator.setItemDirty(note)
|
||||
await this.application.syncService.sync(syncOptions)
|
||||
|
||||
note = this.application.items.findItem(note.uuid)
|
||||
expect(note.dirty).to.equal(false)
|
||||
expect(this.application.itemManager.items.length).to.equal(this.expectedItemCount)
|
||||
|
||||
await this.application.itemManager.setItemToBeDeleted(note)
|
||||
await this.application.mutator.setItemToBeDeleted(note)
|
||||
note = this.application.items.findAnyItem(note.uuid)
|
||||
expect(note.dirty).to.equal(true)
|
||||
this.expectedItemCount--
|
||||
@@ -361,7 +351,7 @@ describe('online syncing', function () {
|
||||
|
||||
it('retrieving item with no content should correctly map local state', async function () {
|
||||
const note = await Factory.createMappedNote(this.application)
|
||||
await this.application.itemManager.setItemDirty(note)
|
||||
await this.application.mutator.setItemDirty(note)
|
||||
await this.application.syncService.sync(syncOptions)
|
||||
|
||||
const syncToken = await this.application.syncService.getLastSyncToken()
|
||||
@@ -370,7 +360,7 @@ describe('online syncing', function () {
|
||||
expect(this.application.itemManager.items.length).to.equal(this.expectedItemCount)
|
||||
|
||||
// client A
|
||||
await this.application.itemManager.setItemToBeDeleted(note)
|
||||
await this.application.mutator.setItemToBeDeleted(note)
|
||||
await this.application.syncService.sync(syncOptions)
|
||||
|
||||
// Subtract 1
|
||||
@@ -399,7 +389,7 @@ describe('online syncing', function () {
|
||||
|
||||
await Factory.sleep(0.1)
|
||||
|
||||
await this.application.itemManager.changeItem(note, (mutator) => {
|
||||
await this.application.mutator.changeItem(note, (mutator) => {
|
||||
mutator.title = 'latest title'
|
||||
})
|
||||
|
||||
@@ -427,7 +417,7 @@ describe('online syncing', function () {
|
||||
|
||||
await Factory.sleep(0.1)
|
||||
|
||||
await this.application.itemManager.setItemToBeDeleted(note)
|
||||
await this.application.mutator.setItemToBeDeleted(note)
|
||||
|
||||
this.expectedItemCount--
|
||||
|
||||
@@ -444,8 +434,8 @@ describe('online syncing', function () {
|
||||
|
||||
it('items that are never synced and deleted should not be uploaded to server', async function () {
|
||||
const note = await Factory.createMappedNote(this.application)
|
||||
await this.application.itemManager.setItemDirty(note)
|
||||
await this.application.itemManager.setItemToBeDeleted(note)
|
||||
await this.application.mutator.setItemDirty(note)
|
||||
await this.application.mutator.setItemToBeDeleted(note)
|
||||
|
||||
let success = true
|
||||
let didCompleteRelevantSync = false
|
||||
@@ -457,7 +447,7 @@ describe('online syncing', function () {
|
||||
if (!beginCheckingResponse) {
|
||||
return
|
||||
}
|
||||
if (!didCompleteRelevantSync && eventName === SyncEvent.SingleRoundTripSyncCompleted) {
|
||||
if (!didCompleteRelevantSync && eventName === SyncEvent.PaginatedSyncRequestCompleted) {
|
||||
didCompleteRelevantSync = true
|
||||
const response = data
|
||||
const matching = response.savedPayloads.find((p) => p.uuid === note.uuid)
|
||||
@@ -474,20 +464,20 @@ describe('online syncing', function () {
|
||||
it('items that are deleted after download first sync complete should not be uploaded to server', async function () {
|
||||
/** The singleton manager may delete items are download first. We dont want those uploaded to server. */
|
||||
const note = await Factory.createMappedNote(this.application)
|
||||
await this.application.itemManager.setItemDirty(note)
|
||||
await this.application.mutator.setItemDirty(note)
|
||||
|
||||
let success = true
|
||||
let didCompleteRelevantSync = false
|
||||
let beginCheckingResponse = false
|
||||
this.application.syncService.addEventObserver(async (eventName, data) => {
|
||||
if (eventName === SyncEvent.DownloadFirstSyncCompleted) {
|
||||
await this.application.itemManager.setItemToBeDeleted(note)
|
||||
await this.application.mutator.setItemToBeDeleted(note)
|
||||
beginCheckingResponse = true
|
||||
}
|
||||
if (!beginCheckingResponse) {
|
||||
return
|
||||
}
|
||||
if (!didCompleteRelevantSync && eventName === SyncEvent.SingleRoundTripSyncCompleted) {
|
||||
if (!didCompleteRelevantSync && eventName === SyncEvent.PaginatedSyncRequestCompleted) {
|
||||
didCompleteRelevantSync = true
|
||||
const response = data
|
||||
const matching = response.savedPayloads.find((p) => p.uuid === note.uuid)
|
||||
@@ -527,7 +517,7 @@ describe('online syncing', function () {
|
||||
|
||||
const decryptionResults = await this.application.protocolService.decryptSplit(keyedSplit)
|
||||
|
||||
await this.application.itemManager.emitItemsFromPayloads(decryptionResults, PayloadEmitSource.LocalChanged)
|
||||
await this.application.mutator.emitItemsFromPayloads(decryptionResults, PayloadEmitSource.LocalChanged)
|
||||
|
||||
expect(this.application.itemManager.allTrackedItems().length).to.equal(this.expectedItemCount)
|
||||
|
||||
@@ -543,7 +533,7 @@ describe('online syncing', function () {
|
||||
const largeItemCount = SyncUpDownLimit + 10
|
||||
for (let i = 0; i < largeItemCount; i++) {
|
||||
const note = await Factory.createMappedNote(this.application)
|
||||
await this.application.itemManager.setItemDirty(note)
|
||||
await this.application.mutator.setItemDirty(note)
|
||||
}
|
||||
|
||||
this.expectedItemCount += largeItemCount
|
||||
@@ -558,7 +548,7 @@ describe('online syncing', function () {
|
||||
const largeItemCount = SyncUpDownLimit + 10
|
||||
for (let i = 0; i < largeItemCount; i++) {
|
||||
const note = await Factory.createMappedNote(this.application)
|
||||
await this.application.itemManager.setItemDirty(note)
|
||||
await this.application.mutator.setItemDirty(note)
|
||||
}
|
||||
/** Upload */
|
||||
this.application.syncService.sync({ awaitAll: true, checkIntegrity: false })
|
||||
@@ -583,7 +573,7 @@ describe('online syncing', function () {
|
||||
|
||||
it('syncing an item should storage it encrypted', async function () {
|
||||
const note = await Factory.createMappedNote(this.application)
|
||||
await this.application.itemManager.setItemDirty(note)
|
||||
await this.application.mutator.setItemDirty(note)
|
||||
await this.application.syncService.sync(syncOptions)
|
||||
this.expectedItemCount++
|
||||
const rawPayloads = await this.application.diskStorageService.getAllRawPayloads()
|
||||
@@ -593,7 +583,7 @@ describe('online syncing', function () {
|
||||
|
||||
it('syncing an item before data load should storage it encrypted', async function () {
|
||||
const note = await Factory.createMappedNote(this.application)
|
||||
await this.application.itemManager.setItemDirty(note)
|
||||
await this.application.mutator.setItemDirty(note)
|
||||
this.expectedItemCount++
|
||||
|
||||
/** Simulate database not loaded */
|
||||
@@ -610,7 +600,7 @@ describe('online syncing', function () {
|
||||
it('saving an item after sync should persist it with content property', async function () {
|
||||
const note = await Factory.createMappedNote(this.application)
|
||||
const text = Factory.randomString(10000)
|
||||
await this.application.mutator.changeAndSaveItem(
|
||||
await this.application.changeAndSaveItem(
|
||||
note,
|
||||
(mutator) => {
|
||||
mutator.text = text
|
||||
@@ -634,7 +624,7 @@ describe('online syncing', function () {
|
||||
expect(this.application.itemManager.getDirtyItems().length).to.equal(0)
|
||||
|
||||
let note = await Factory.createMappedNote(this.application)
|
||||
note = await this.application.itemManager.changeItem(note, (mutator) => {
|
||||
note = await this.application.mutator.changeItem(note, (mutator) => {
|
||||
mutator.text = `${Math.random()}`
|
||||
})
|
||||
/** This sync request should exit prematurely as we called ut_setDatabaseNotLoaded */
|
||||
@@ -705,13 +695,13 @@ describe('online syncing', function () {
|
||||
|
||||
it('valid sync date tracking', async function () {
|
||||
let note = await Factory.createMappedNote(this.application)
|
||||
note = await this.application.itemManager.setItemDirty(note)
|
||||
note = await this.application.mutator.setItemDirty(note)
|
||||
this.expectedItemCount++
|
||||
|
||||
expect(note.dirty).to.equal(true)
|
||||
expect(note.payload.dirtyIndex).to.be.at.most(getCurrentDirtyIndex())
|
||||
|
||||
note = await this.application.itemManager.changeItem(note, (mutator) => {
|
||||
note = await this.application.mutator.changeItem(note, (mutator) => {
|
||||
mutator.text = `${Math.random()}`
|
||||
})
|
||||
const sync = this.application.sync.sync(syncOptions)
|
||||
@@ -748,7 +738,7 @@ describe('online syncing', function () {
|
||||
* It will do based on comparing whether item.dirtyIndex > item.globalDirtyIndexAtLastSync
|
||||
*/
|
||||
let note = await Factory.createMappedNote(this.application)
|
||||
await this.application.itemManager.setItemDirty(note)
|
||||
await this.application.mutator.setItemDirty(note)
|
||||
this.expectedItemCount++
|
||||
|
||||
// client A. Don't await, we want to do other stuff.
|
||||
@@ -759,12 +749,12 @@ describe('online syncing', function () {
|
||||
|
||||
// While that sync is going on, we want to modify this item many times.
|
||||
const text = `${Math.random()}`
|
||||
note = await this.application.itemManager.changeItem(note, (mutator) => {
|
||||
note = await this.application.mutator.changeItem(note, (mutator) => {
|
||||
mutator.text = text
|
||||
})
|
||||
await this.application.itemManager.setItemDirty(note)
|
||||
await this.application.itemManager.setItemDirty(note)
|
||||
await this.application.itemManager.setItemDirty(note)
|
||||
await this.application.mutator.setItemDirty(note)
|
||||
await this.application.mutator.setItemDirty(note)
|
||||
await this.application.mutator.setItemDirty(note)
|
||||
expect(note.payload.dirtyIndex).to.be.above(note.payload.globalDirtyIndexAtLastSync)
|
||||
|
||||
// Now do a regular sync with no latency.
|
||||
@@ -817,7 +807,7 @@ describe('online syncing', function () {
|
||||
|
||||
setTimeout(
|
||||
async function () {
|
||||
await this.application.itemManager.changeItem(note, (mutator) => {
|
||||
await this.application.mutator.changeItem(note, (mutator) => {
|
||||
mutator.text = newText
|
||||
})
|
||||
}.bind(this),
|
||||
@@ -862,9 +852,9 @@ describe('online syncing', function () {
|
||||
const newText = `${Math.random()}`
|
||||
|
||||
this.application.syncService.addEventObserver(async (eventName) => {
|
||||
if (eventName === SyncEvent.SyncWillBegin && !didPerformMutatation) {
|
||||
if (eventName === SyncEvent.SyncDidBeginProcessing && !didPerformMutatation) {
|
||||
didPerformMutatation = true
|
||||
await this.application.itemManager.changeItem(note, (mutator) => {
|
||||
await this.application.mutator.changeItem(note, (mutator) => {
|
||||
mutator.text = newText
|
||||
})
|
||||
}
|
||||
@@ -898,7 +888,7 @@ describe('online syncing', function () {
|
||||
dirtyIndex: changed[0].payload.globalDirtyIndexAtLastSync + 1,
|
||||
})
|
||||
|
||||
await this.application.itemManager.emitItemFromPayload(mutated)
|
||||
await this.application.mutator.emitItemFromPayload(mutated)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -916,6 +906,7 @@ describe('online syncing', function () {
|
||||
const note = await Factory.createSyncedNote(this.application)
|
||||
const preDeleteSyncToken = await this.application.syncService.getLastSyncToken()
|
||||
await this.application.mutator.deleteItem(note)
|
||||
await this.application.sync.sync()
|
||||
await this.application.syncService.setLastSyncToken(preDeleteSyncToken)
|
||||
await this.application.sync.sync(syncOptions)
|
||||
expect(this.application.itemManager.items.length).to.equal(this.expectedItemCount)
|
||||
@@ -938,7 +929,7 @@ describe('online syncing', function () {
|
||||
dirty: true,
|
||||
})
|
||||
|
||||
await this.application.itemManager.emitItemFromPayload(errored)
|
||||
await this.application.payloadManager.emitPayload(errored)
|
||||
await this.application.sync.sync(syncOptions)
|
||||
|
||||
const updatedNote = this.application.items.findAnyItem(note.uuid)
|
||||
@@ -966,7 +957,7 @@ describe('online syncing', function () {
|
||||
},
|
||||
})
|
||||
|
||||
await this.application.syncService.handleSuccessServerResponse({ payloadsSavedOrSaving: [] }, response)
|
||||
await this.application.syncService.handleSuccessServerResponse({ payloadsSavedOrSaving: [], options: {} }, response)
|
||||
|
||||
expect(this.application.payloadManager.findOne(invalidPayload.uuid)).to.not.be.ok
|
||||
expect(this.application.payloadManager.findOne(validPayload.uuid)).to.be.ok
|
||||
@@ -995,7 +986,7 @@ describe('online syncing', function () {
|
||||
content: {},
|
||||
})
|
||||
this.expectedItemCount++
|
||||
await this.application.itemManager.emitItemsFromPayloads([payload])
|
||||
await this.application.mutator.emitItemsFromPayloads([payload])
|
||||
await this.application.sync.sync(syncOptions)
|
||||
|
||||
/** Item should no longer be dirty, otherwise it would keep syncing */
|
||||
@@ -1006,7 +997,7 @@ describe('online syncing', function () {
|
||||
it('should call onPresyncSave before sync begins', async function () {
|
||||
const events = []
|
||||
this.application.syncService.addEventObserver((event) => {
|
||||
if (event === SyncEvent.SyncWillBegin) {
|
||||
if (event === SyncEvent.SyncDidBeginProcessing) {
|
||||
events.push('sync-will-begin')
|
||||
}
|
||||
})
|
||||
@@ -1032,6 +1023,7 @@ describe('online syncing', function () {
|
||||
|
||||
const note = await Factory.createSyncedNote(this.application)
|
||||
await this.application.mutator.deleteItem(note)
|
||||
await this.application.sync.sync()
|
||||
|
||||
expect(conditionMet).to.equal(true)
|
||||
})
|
||||
|
||||
@@ -12,14 +12,9 @@
|
||||
<script src="https://unpkg.com/sinon@13.0.2/pkg/sinon.js"></script>
|
||||
<script src="./vendor/sncrypto-web.js"></script>
|
||||
<script src="../dist/snjs.js"></script>
|
||||
<script>
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const syncServerHostName = urlParams.get('sync_server_host_name') ?? 'syncing-server-proxy';
|
||||
const bail = urlParams.get('bail') === 'false' ? false : true;
|
||||
const skipPaidFeatures = urlParams.get('skip_paid_features') === 'true' ? true : false;
|
||||
|
||||
<script type="module">
|
||||
Object.assign(window, SNCrypto);
|
||||
|
||||
Object.assign(window, SNLibrary);
|
||||
|
||||
SNLog.onLog = (message) => {
|
||||
@@ -30,6 +25,10 @@
|
||||
console.error(error);
|
||||
};
|
||||
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const bail = urlParams.get('bail') === 'false' ? false : true;
|
||||
const skipPaidFeatures = urlParams.get('skip_paid_features') === 'true' ? true : false;
|
||||
|
||||
mocha.setup({
|
||||
ui: 'bdd',
|
||||
timeout: 5000,
|
||||
@@ -39,63 +38,42 @@
|
||||
mocha.grep('@paidfeature').invert();
|
||||
}
|
||||
</script>
|
||||
<script type="module" src="memory.test.js"></script>
|
||||
<script type="module" src="protocol.test.js"></script>
|
||||
<script type="module" src="utils.test.js"></script>
|
||||
<script type="module" src="000.test.js"></script>
|
||||
<script type="module" src="001.test.js"></script>
|
||||
<script type="module" src="002.test.js"></script>
|
||||
<script type="module" src="003.test.js"></script>
|
||||
<script type="module" src="004.test.js"></script>
|
||||
<script type="module" src="username.test.js"></script>
|
||||
<script type="module" src="app-group.test.js"></script>
|
||||
<script type="module" src="application.test.js"></script>
|
||||
<script type="module" src="payload.test.js"></script>
|
||||
<script type="module" src="payload_encryption.test.js"></script>
|
||||
<script type="module" src="item.test.js"></script>
|
||||
<script type="module" src="item_manager.test.js"></script>
|
||||
<script type="module" src="features.test.js"></script>
|
||||
<script type="module" src="settings.test.js"></script>
|
||||
<script type="module" src="mfa_service.test.js"></script>
|
||||
<script type="module" src="mutator.test.js"></script>
|
||||
<script type="module" src="payload_manager.test.js"></script>
|
||||
<script type="module" src="collections.test.js"></script>
|
||||
<script type="module" src="note_display_criteria.test.js"></script>
|
||||
<script type="module" src="keys.test.js"></script>
|
||||
<script type="module" src="key_params.test.js"></script>
|
||||
<script type="module" src="key_recovery_service.test.js"></script>
|
||||
<script type="module" src="backups.test.js"></script>
|
||||
<script type="module" src="upgrading.test.js"></script>
|
||||
<script type="module" src="model_tests/importing.test.js"></script>
|
||||
<script type="module" src="model_tests/appmodels.test.js"></script>
|
||||
<script type="module" src="model_tests/items.test.js"></script>
|
||||
<script type="module" src="model_tests/mapping.test.js"></script>
|
||||
<script type="module" src="model_tests/notes_smart_tags.test.js"></script>
|
||||
<script type="module" src="model_tests/notes_tags.test.js"></script>
|
||||
<script type="module" src="model_tests/notes_tags_folders.test.js"></script>
|
||||
<script type="module" src="model_tests/performance.test.js"></script>
|
||||
<script type="module" src="sync_tests/offline.test.js"></script>
|
||||
<script type="module" src="sync_tests/notes_tags.test.js"></script>
|
||||
<script type="module" src="sync_tests/online.test.js"></script>
|
||||
<script type="module" src="sync_tests/conflicting.test.js"></script>
|
||||
<script type="module" src="sync_tests/integrity.test.js"></script>
|
||||
<script type="module" src="auth-fringe-cases.test.js"></script>
|
||||
<script type="module" src="auth.test.js"></script>
|
||||
<script type="module" src="device_auth.test.js"></script>
|
||||
<script type="module" src="storage.test.js"></script>
|
||||
<script type="module" src="protection.test.js"></script>
|
||||
<script type="module" src="singletons.test.js"></script>
|
||||
<script type="module" src="migrations/migration.test.js"></script>
|
||||
<script type="module" src="migrations/tags-to-folders.test.js"></script>
|
||||
<script type="module" src="history.test.js"></script>
|
||||
<script type="module" src="actions.test.js"></script>
|
||||
<script type="module" src="preferences.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" src="recovery.test.js"></script>
|
||||
|
||||
<script type="module">
|
||||
mocha.run();
|
||||
import MainRegistry from './TestRegistry/MainRegistry.js'
|
||||
|
||||
const InternalFeatureStatus = {
|
||||
[InternalFeature.Vaults]: { enabled: false, exclusive: false },
|
||||
}
|
||||
|
||||
const loadTest = (fileName) => {
|
||||
return new Promise((resolve) => {
|
||||
const script = document.createElement('script');
|
||||
script.type = 'module';
|
||||
script.src = fileName;
|
||||
script.async = false;
|
||||
script.defer = false;
|
||||
script.addEventListener('load', resolve);
|
||||
document.head.append(script);
|
||||
})
|
||||
}
|
||||
|
||||
const loadTests = async (fileNames) => {
|
||||
for (const fileName of fileNames) {
|
||||
await loadTest(fileName);
|
||||
}
|
||||
}
|
||||
|
||||
if (InternalFeatureStatus[InternalFeature.Vaults].enabled) {
|
||||
InternalFeatureService.get().enableFeature(InternalFeature.Vaults);
|
||||
await loadTests(MainRegistry.VaultTests);
|
||||
}
|
||||
|
||||
if (!InternalFeatureStatus[InternalFeature.Vaults].exclusive) {
|
||||
await loadTests(MainRegistry.BaseTests);
|
||||
}
|
||||
|
||||
mocha.run()
|
||||
</script>
|
||||
</head>
|
||||
|
||||
@@ -103,4 +81,4 @@
|
||||
<div id="mocha"></div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
</html>
|
||||
@@ -173,7 +173,7 @@ describe('upgrading', () => {
|
||||
it('protocol version should be upgraded on password change', async function () {
|
||||
/** Delete default items key that is created on launch */
|
||||
const itemsKey = await this.application.protocolService.getSureDefaultItemsKey()
|
||||
await this.application.itemManager.setItemToBeDeleted(itemsKey)
|
||||
await this.application.mutator.setItemToBeDeleted(itemsKey)
|
||||
expect(Uuids(this.application.itemManager.getDisplayableItemsKeys()).includes(itemsKey.uuid)).to.equal(false)
|
||||
|
||||
Factory.createMappedNote(this.application)
|
||||
|
||||
277
packages/snjs/mocha/vaults/asymmetric-messages.test.js
Normal file
277
packages/snjs/mocha/vaults/asymmetric-messages.test.js
Normal file
@@ -0,0 +1,277 @@
|
||||
import * as Factory from '../lib/factory.js'
|
||||
import * as Collaboration from '../lib/Collaboration.js'
|
||||
|
||||
chai.use(chaiAsPromised)
|
||||
const expect = chai.expect
|
||||
|
||||
describe('asymmetric messages', function () {
|
||||
this.timeout(Factory.TwentySecondTimeout)
|
||||
|
||||
let context
|
||||
let service
|
||||
|
||||
afterEach(async function () {
|
||||
await context.deinit()
|
||||
localStorage.clear()
|
||||
})
|
||||
|
||||
beforeEach(async function () {
|
||||
localStorage.clear()
|
||||
|
||||
context = await Factory.createAppContextWithRealCrypto()
|
||||
|
||||
await context.launch()
|
||||
await context.register()
|
||||
|
||||
service = context.asymmetric
|
||||
})
|
||||
|
||||
it('should not trust message if the trusted payload data recipientUuid does not match the message user uuid', async () => {
|
||||
console.error('TODO: implement')
|
||||
})
|
||||
|
||||
it('should delete message after processing it', async () => {
|
||||
const { contactContext, deinitContactContext } = await Collaboration.createSharedVaultWithAcceptedInvite(context)
|
||||
|
||||
const eventData = {
|
||||
oldKeyPair: context.encryption.getKeyPair(),
|
||||
oldSigningKeyPair: context.encryption.getSigningKeyPair(),
|
||||
newKeyPair: context.encryption.getKeyPair(),
|
||||
newSigningKeyPair: context.encryption.getSigningKeyPair(),
|
||||
}
|
||||
|
||||
await service.sendOwnContactChangeEventToAllContacts(eventData)
|
||||
|
||||
const deleteFunction = sinon.spy(contactContext.asymmetric, 'deleteMessageAfterProcessing')
|
||||
|
||||
const promise = contactContext.resolveWhenAsymmetricMessageProcessingCompletes()
|
||||
|
||||
await contactContext.sync()
|
||||
|
||||
await promise
|
||||
|
||||
expect(deleteFunction.callCount).to.equal(1)
|
||||
|
||||
const messages = await contactContext.asymmetric.getInboundMessages()
|
||||
expect(messages.length).to.equal(0)
|
||||
|
||||
await deinitContactContext()
|
||||
})
|
||||
|
||||
it('should send contact share message after trusted contact belonging to group changes', async () => {
|
||||
const { sharedVault, contactContext, deinitContactContext } =
|
||||
await Collaboration.createSharedVaultWithAcceptedInvite(context)
|
||||
|
||||
const { thirdPartyContext, deinitThirdPartyContext } = await Collaboration.inviteNewPartyToSharedVault(
|
||||
context,
|
||||
sharedVault,
|
||||
)
|
||||
|
||||
await Collaboration.acceptAllInvites(thirdPartyContext)
|
||||
|
||||
const sendContactSharePromise = context.resolveWhenSharedVaultServiceSendsContactShareMessage()
|
||||
|
||||
await context.contacts.createOrEditTrustedContact({
|
||||
contactUuid: thirdPartyContext.userUuid,
|
||||
publicKey: thirdPartyContext.publicKey,
|
||||
signingPublicKey: thirdPartyContext.signingPublicKey,
|
||||
name: 'Changed 3rd Party Name',
|
||||
})
|
||||
|
||||
await sendContactSharePromise
|
||||
|
||||
const completedProcessingMessagesPromise = contactContext.resolveWhenAsymmetricMessageProcessingCompletes()
|
||||
|
||||
await contactContext.sync()
|
||||
await completedProcessingMessagesPromise
|
||||
|
||||
const updatedContact = contactContext.contacts.findTrustedContact(thirdPartyContext.userUuid)
|
||||
expect(updatedContact.name).to.equal('Changed 3rd Party Name')
|
||||
|
||||
await deinitContactContext()
|
||||
await deinitThirdPartyContext()
|
||||
})
|
||||
|
||||
it('should not send contact share message to self or to contact who is changed', async () => {
|
||||
const { sharedVault, contactContext, deinitContactContext } =
|
||||
await Collaboration.createSharedVaultWithAcceptedInvite(context)
|
||||
|
||||
const { thirdPartyContext, deinitThirdPartyContext } = await Collaboration.inviteNewPartyToSharedVault(
|
||||
context,
|
||||
sharedVault,
|
||||
)
|
||||
const handleInitialContactShareMessage = contactContext.resolveWhenAsymmetricMessageProcessingCompletes()
|
||||
|
||||
await Collaboration.acceptAllInvites(thirdPartyContext)
|
||||
|
||||
await contactContext.sync()
|
||||
await handleInitialContactShareMessage
|
||||
|
||||
const sendContactSharePromise = context.resolveWhenSharedVaultServiceSendsContactShareMessage()
|
||||
|
||||
await context.contacts.createOrEditTrustedContact({
|
||||
contactUuid: thirdPartyContext.userUuid,
|
||||
publicKey: thirdPartyContext.publicKey,
|
||||
signingPublicKey: thirdPartyContext.signingPublicKey,
|
||||
name: 'Changed 3rd Party Name',
|
||||
})
|
||||
|
||||
await sendContactSharePromise
|
||||
|
||||
const firstPartySpy = sinon.spy(context.asymmetric, 'handleTrustedContactShareMessage')
|
||||
const secondPartySpy = sinon.spy(contactContext.asymmetric, 'handleTrustedContactShareMessage')
|
||||
const thirdPartySpy = sinon.spy(thirdPartyContext.asymmetric, 'handleTrustedContactShareMessage')
|
||||
|
||||
await context.sync()
|
||||
await contactContext.sync()
|
||||
await thirdPartyContext.sync()
|
||||
|
||||
expect(firstPartySpy.callCount).to.equal(0)
|
||||
expect(secondPartySpy.callCount).to.equal(1)
|
||||
expect(thirdPartySpy.callCount).to.equal(0)
|
||||
|
||||
await deinitThirdPartyContext()
|
||||
await deinitContactContext()
|
||||
})
|
||||
|
||||
it('should send shared vault root key change message after root key change', async () => {
|
||||
const { sharedVault, contactContext, deinitContactContext } =
|
||||
await Collaboration.createSharedVaultWithAcceptedInvite(context)
|
||||
|
||||
await context.vaults.rotateVaultRootKey(sharedVault)
|
||||
|
||||
const firstPartySpy = sinon.spy(context.asymmetric, 'handleTrustedSharedVaultRootKeyChangedMessage')
|
||||
const secondPartySpy = sinon.spy(contactContext.asymmetric, 'handleTrustedSharedVaultRootKeyChangedMessage')
|
||||
|
||||
await context.sync()
|
||||
await contactContext.sync()
|
||||
|
||||
expect(firstPartySpy.callCount).to.equal(0)
|
||||
expect(secondPartySpy.callCount).to.equal(1)
|
||||
|
||||
await deinitContactContext()
|
||||
})
|
||||
|
||||
it('should send shared vault metadata change message after shared vault name change', async () => {
|
||||
const { sharedVault, contactContext, deinitContactContext } =
|
||||
await Collaboration.createSharedVaultWithAcceptedInvite(context)
|
||||
|
||||
await context.vaults.changeVaultNameAndDescription(sharedVault, {
|
||||
name: 'New Name',
|
||||
description: 'New Description',
|
||||
})
|
||||
|
||||
const firstPartySpy = sinon.spy(context.asymmetric, 'handleVaultMetadataChangedMessage')
|
||||
const secondPartySpy = sinon.spy(contactContext.asymmetric, 'handleVaultMetadataChangedMessage')
|
||||
|
||||
await context.sync()
|
||||
await contactContext.sync()
|
||||
|
||||
expect(firstPartySpy.callCount).to.equal(0)
|
||||
expect(secondPartySpy.callCount).to.equal(1)
|
||||
|
||||
const updatedVault = contactContext.vaults.getVault({ keySystemIdentifier: sharedVault.systemIdentifier })
|
||||
expect(updatedVault.name).to.equal('New Name')
|
||||
expect(updatedVault.description).to.equal('New Description')
|
||||
|
||||
await deinitContactContext()
|
||||
})
|
||||
|
||||
it('should send sender keypair changed message to trusted contacts', async () => {
|
||||
const { contactContext, deinitContactContext } = await Collaboration.createSharedVaultWithAcceptedInvite(context)
|
||||
|
||||
await context.changePassword('new password')
|
||||
|
||||
const firstPartySpy = sinon.spy(context.asymmetric, 'handleTrustedSenderKeypairChangedMessage')
|
||||
const secondPartySpy = sinon.spy(contactContext.asymmetric, 'handleTrustedSenderKeypairChangedMessage')
|
||||
|
||||
await context.sync()
|
||||
|
||||
const completedProcessingMessagesPromise = contactContext.resolveWhenAsymmetricMessageProcessingCompletes()
|
||||
await contactContext.sync()
|
||||
await completedProcessingMessagesPromise
|
||||
|
||||
expect(firstPartySpy.callCount).to.equal(0)
|
||||
expect(secondPartySpy.callCount).to.equal(1)
|
||||
|
||||
const contact = contactContext.contacts.findTrustedContact(context.userUuid)
|
||||
expect(contact.publicKeySet.encryption).to.equal(context.publicKey)
|
||||
expect(contact.publicKeySet.signing).to.equal(context.signingPublicKey)
|
||||
|
||||
await deinitContactContext()
|
||||
})
|
||||
|
||||
it('should process sender keypair changed message', async () => {
|
||||
const { contactContext, deinitContactContext } = await Collaboration.createContactContext()
|
||||
await Collaboration.createTrustedContactForUserOfContext(context, contactContext)
|
||||
await Collaboration.createTrustedContactForUserOfContext(contactContext, context)
|
||||
const originalContact = contactContext.contacts.findTrustedContact(context.userUuid)
|
||||
|
||||
await context.changePassword('new_password')
|
||||
|
||||
const completedProcessingMessagesPromise = contactContext.resolveWhenAsymmetricMessageProcessingCompletes()
|
||||
await contactContext.sync()
|
||||
await completedProcessingMessagesPromise
|
||||
|
||||
const updatedContact = contactContext.contacts.findTrustedContact(context.userUuid)
|
||||
|
||||
expect(updatedContact.publicKeySet.encryption).to.not.equal(originalContact.publicKeySet.encryption)
|
||||
expect(updatedContact.publicKeySet.signing).to.not.equal(originalContact.publicKeySet.signing)
|
||||
|
||||
expect(updatedContact.publicKeySet.encryption).to.equal(context.publicKey)
|
||||
expect(updatedContact.publicKeySet.signing).to.equal(context.signingPublicKey)
|
||||
|
||||
await deinitContactContext()
|
||||
})
|
||||
|
||||
it('sender keypair changed message should be signed using old key pair', async () => {
|
||||
const { contactContext, deinitContactContext } = await Collaboration.createSharedVaultWithAcceptedInvite(context)
|
||||
|
||||
const oldKeyPair = context.encryption.getKeyPair()
|
||||
const oldSigningKeyPair = context.encryption.getSigningKeyPair()
|
||||
|
||||
await context.changePassword('new password')
|
||||
|
||||
const secondPartySpy = sinon.spy(contactContext.asymmetric, 'handleTrustedSenderKeypairChangedMessage')
|
||||
|
||||
await context.sync()
|
||||
const completedProcessingMessagesPromise = contactContext.resolveWhenAsymmetricMessageProcessingCompletes()
|
||||
await contactContext.sync()
|
||||
await completedProcessingMessagesPromise
|
||||
|
||||
const message = secondPartySpy.args[0][0]
|
||||
const encryptedMessage = message.encrypted_message
|
||||
|
||||
const publicKeySet =
|
||||
contactContext.encryption.getSenderPublicKeySetFromAsymmetricallyEncryptedString(encryptedMessage)
|
||||
|
||||
expect(publicKeySet.encryption).to.equal(oldKeyPair.publicKey)
|
||||
expect(publicKeySet.signing).to.equal(oldSigningKeyPair.publicKey)
|
||||
|
||||
await deinitContactContext()
|
||||
})
|
||||
|
||||
it('sender keypair changed message should contain new keypair and be trusted', async () => {
|
||||
const { contactContext, deinitContactContext } = await Collaboration.createSharedVaultWithAcceptedInvite(context)
|
||||
|
||||
await context.changePassword('new password')
|
||||
|
||||
const newKeyPair = context.encryption.getKeyPair()
|
||||
const newSigningKeyPair = context.encryption.getSigningKeyPair()
|
||||
|
||||
const completedProcessingMessagesPromise = contactContext.resolveWhenAsymmetricMessageProcessingCompletes()
|
||||
await contactContext.sync()
|
||||
await completedProcessingMessagesPromise
|
||||
|
||||
const updatedContact = contactContext.contacts.findTrustedContact(context.userUuid)
|
||||
expect(updatedContact.publicKeySet.encryption).to.equal(newKeyPair.publicKey)
|
||||
expect(updatedContact.publicKeySet.signing).to.equal(newSigningKeyPair.publicKey)
|
||||
|
||||
await deinitContactContext()
|
||||
})
|
||||
|
||||
it('should delete all inbound messages after changing user password', async () => {
|
||||
/** Messages to user are encrypted with old keypair and are no longer decryptable */
|
||||
console.error('TODO: implement test')
|
||||
})
|
||||
})
|
||||
186
packages/snjs/mocha/vaults/conflicts.test.js
Normal file
186
packages/snjs/mocha/vaults/conflicts.test.js
Normal file
@@ -0,0 +1,186 @@
|
||||
import * as Factory from '../lib/factory.js'
|
||||
import * as Collaboration from '../lib/Collaboration.js'
|
||||
|
||||
chai.use(chaiAsPromised)
|
||||
const expect = chai.expect
|
||||
|
||||
describe('shared vault conflicts', function () {
|
||||
this.timeout(Factory.TwentySecondTimeout)
|
||||
|
||||
let context
|
||||
|
||||
afterEach(async function () {
|
||||
await context.deinit()
|
||||
localStorage.clear()
|
||||
})
|
||||
|
||||
beforeEach(async function () {
|
||||
localStorage.clear()
|
||||
|
||||
context = await Factory.createAppContextWithRealCrypto()
|
||||
|
||||
await context.launch()
|
||||
await context.register()
|
||||
})
|
||||
|
||||
it('after being removed from shared vault, attempting to sync previous vault item should result in SharedVaultNotMemberError. The item should be duplicated then removed.', async () => {
|
||||
const { sharedVault, note, contactContext, deinitContactContext } =
|
||||
await Collaboration.createSharedVaultWithAcceptedInviteAndNote(context)
|
||||
|
||||
contactContext.lockSyncing()
|
||||
await context.sharedVaults.removeUserFromSharedVault(sharedVault, contactContext.userUuid)
|
||||
const promise = contactContext.resolveWithConflicts()
|
||||
contactContext.unlockSyncing()
|
||||
await contactContext.changeNoteTitleAndSync(note, 'new title')
|
||||
const conflicts = await promise
|
||||
await contactContext.sync()
|
||||
|
||||
expect(conflicts.length).to.equal(1)
|
||||
expect(conflicts[0].type).to.equal(ConflictType.SharedVaultNotMemberError)
|
||||
expect(conflicts[0].unsaved_item.content_type).to.equal(ContentType.Note)
|
||||
|
||||
const collaboratorNotes = contactContext.items.getDisplayableNotes()
|
||||
expect(collaboratorNotes.length).to.equal(1)
|
||||
expect(collaboratorNotes[0].duplicate_of).to.not.be.undefined
|
||||
expect(collaboratorNotes[0].title).to.equal('new title')
|
||||
|
||||
await deinitContactContext()
|
||||
})
|
||||
|
||||
it('conflicts created should be associated with the vault', async () => {
|
||||
const { note, contactContext, deinitContactContext } =
|
||||
await Collaboration.createSharedVaultWithAcceptedInviteAndNote(context)
|
||||
|
||||
await context.changeNoteTitle(note, 'new title first client')
|
||||
await contactContext.changeNoteTitle(note, 'new title second client')
|
||||
|
||||
const doneAddingConflictToSharedVault = contactContext.resolveWhenSavedSyncPayloadsIncludesItemThatIsDuplicatedOf(
|
||||
note.uuid,
|
||||
)
|
||||
|
||||
await context.sync({ desc: 'First client sync' })
|
||||
await contactContext.sync({
|
||||
desc: 'Second client sync with conflicts to be created',
|
||||
})
|
||||
await doneAddingConflictToSharedVault
|
||||
await context.sync({ desc: 'First client sync with conflicts to be pulled in' })
|
||||
|
||||
expect(context.items.invalidItems.length).to.equal(0)
|
||||
expect(contactContext.items.invalidItems.length).to.equal(0)
|
||||
|
||||
const collaboratorNotes = contactContext.items.getDisplayableNotes()
|
||||
expect(collaboratorNotes.length).to.equal(2)
|
||||
expect(collaboratorNotes.find((note) => !!note.duplicate_of)).to.not.be.undefined
|
||||
|
||||
const originatorNotes = context.items.getDisplayableNotes()
|
||||
expect(originatorNotes.length).to.equal(2)
|
||||
expect(originatorNotes.find((note) => !!note.duplicate_of)).to.not.be.undefined
|
||||
|
||||
await deinitContactContext()
|
||||
})
|
||||
|
||||
it('attempting to modify note as read user should result in SharedVaultInsufficientPermissionsError', async () => {
|
||||
const { note, contactContext, deinitContactContext } =
|
||||
await Collaboration.createSharedVaultWithAcceptedInviteAndNote(context, SharedVaultPermission.Read)
|
||||
|
||||
const promise = contactContext.resolveWithConflicts()
|
||||
await contactContext.changeNoteTitleAndSync(note, 'new title')
|
||||
const conflicts = await promise
|
||||
|
||||
expect(conflicts.length).to.equal(1)
|
||||
expect(conflicts[0].type).to.equal(ConflictType.SharedVaultInsufficientPermissionsError)
|
||||
expect(conflicts[0].unsaved_item.content_type).to.equal(ContentType.Note)
|
||||
|
||||
await deinitContactContext()
|
||||
})
|
||||
|
||||
it('should handle SharedVaultNotMemberError by duplicating item to user non-vault', async () => {
|
||||
const { sharedVault, note, contactContext, deinitContactContext } =
|
||||
await Collaboration.createSharedVaultWithAcceptedInviteAndNote(context)
|
||||
|
||||
await context.sharedVaults.removeUserFromSharedVault(sharedVault, contactContext.userUuid)
|
||||
await contactContext.changeNoteTitleAndSync(note, 'new title')
|
||||
const notes = contactContext.notes
|
||||
|
||||
expect(notes.length).to.equal(1)
|
||||
expect(notes[0].title).to.equal('new title')
|
||||
expect(notes[0].key_system_identifier).to.not.be.ok
|
||||
expect(notes[0].duplicate_of).to.equal(note.uuid)
|
||||
|
||||
await deinitContactContext()
|
||||
})
|
||||
|
||||
it('attempting to save note to non-existent vault should result in SharedVaultNotMemberError conflict', async () => {
|
||||
context.anticipateConsoleError(
|
||||
'Error decrypting contentKey from parameters',
|
||||
'An invalid shared vault uuid is being assigned to an item',
|
||||
)
|
||||
const { note } = await Collaboration.createSharedVaultWithNote(context)
|
||||
|
||||
const promise = context.resolveWithConflicts()
|
||||
|
||||
const objectToSpy = context.application.sync
|
||||
sinon.stub(objectToSpy, 'payloadsByPreparingForServer').callsFake(async (params) => {
|
||||
objectToSpy.payloadsByPreparingForServer.restore()
|
||||
const payloads = await objectToSpy.payloadsByPreparingForServer(params)
|
||||
for (const payload of payloads) {
|
||||
payload.shared_vault_uuid = 'non-existent-vault-uuid-123'
|
||||
}
|
||||
|
||||
return payloads
|
||||
})
|
||||
|
||||
await context.changeNoteTitleAndSync(note, 'new-title')
|
||||
const conflicts = await promise
|
||||
|
||||
expect(conflicts.length).to.equal(1)
|
||||
expect(conflicts[0].type).to.equal(ConflictType.SharedVaultNotMemberError)
|
||||
expect(conflicts[0].unsaved_item.content_type).to.equal(ContentType.Note)
|
||||
})
|
||||
|
||||
it('should create a non-vaulted copy if attempting to move item from vault to user and item belongs to someone else', async () => {
|
||||
const { note, sharedVault, contactContext, deinitContactContext } =
|
||||
await Collaboration.createSharedVaultWithAcceptedInviteAndNote(context)
|
||||
|
||||
const promise = contactContext.resolveWithConflicts()
|
||||
await contactContext.vaults.removeItemFromVault(note)
|
||||
const conflicts = await promise
|
||||
|
||||
expect(conflicts.length).to.equal(1)
|
||||
expect(conflicts[0].unsaved_item.content_type).to.equal(ContentType.Note)
|
||||
|
||||
const duplicateNote = contactContext.findDuplicateNote(note.uuid)
|
||||
expect(duplicateNote).to.not.be.undefined
|
||||
expect(duplicateNote.key_system_identifier).to.not.be.ok
|
||||
|
||||
const existingNote = contactContext.items.findItem(note.uuid)
|
||||
expect(existingNote.key_system_identifier).to.equal(sharedVault.systemIdentifier)
|
||||
|
||||
await deinitContactContext()
|
||||
})
|
||||
|
||||
it('should created a non-vaulted copy if admin attempts to move item from vault to user if the item belongs to someone else', async () => {
|
||||
const { sharedVault, contactContext, deinitContactContext } =
|
||||
await Collaboration.createSharedVaultWithAcceptedInvite(context)
|
||||
|
||||
const note = await contactContext.createSyncedNote('foo', 'bar')
|
||||
await Collaboration.moveItemToVault(contactContext, sharedVault, note)
|
||||
await context.sync()
|
||||
|
||||
const promise = context.resolveWithConflicts()
|
||||
await context.vaults.removeItemFromVault(note)
|
||||
const conflicts = await promise
|
||||
|
||||
expect(conflicts.length).to.equal(1)
|
||||
expect(conflicts[0].unsaved_item.content_type).to.equal(ContentType.Note)
|
||||
|
||||
const duplicateNote = context.findDuplicateNote(note.uuid)
|
||||
expect(duplicateNote).to.not.be.undefined
|
||||
expect(duplicateNote.key_system_identifier).to.not.be.ok
|
||||
|
||||
const existingNote = context.items.findItem(note.uuid)
|
||||
expect(existingNote.key_system_identifier).to.equal(sharedVault.systemIdentifier)
|
||||
|
||||
await deinitContactContext()
|
||||
})
|
||||
})
|
||||
83
packages/snjs/mocha/vaults/contacts.test.js
Normal file
83
packages/snjs/mocha/vaults/contacts.test.js
Normal file
@@ -0,0 +1,83 @@
|
||||
import * as Factory from '../lib/factory.js'
|
||||
|
||||
chai.use(chaiAsPromised)
|
||||
const expect = chai.expect
|
||||
|
||||
describe('contacts', function () {
|
||||
this.timeout(Factory.TwentySecondTimeout)
|
||||
|
||||
let context
|
||||
|
||||
afterEach(async function () {
|
||||
await context.deinit()
|
||||
localStorage.clear()
|
||||
})
|
||||
|
||||
beforeEach(async function () {
|
||||
localStorage.clear()
|
||||
|
||||
context = await Factory.createAppContextWithRealCrypto()
|
||||
|
||||
await context.launch()
|
||||
await context.register()
|
||||
})
|
||||
|
||||
it('should create contact', async () => {
|
||||
const contact = await context.contacts.createOrEditTrustedContact({
|
||||
name: 'John Doe',
|
||||
publicKey: 'my_public_key',
|
||||
signingPublicKey: 'my_signing_public_key',
|
||||
contactUuid: '123',
|
||||
})
|
||||
|
||||
expect(contact).to.not.be.undefined
|
||||
expect(contact.name).to.equal('John Doe')
|
||||
expect(contact.publicKeySet.encryption).to.equal('my_public_key')
|
||||
expect(contact.publicKeySet.signing).to.equal('my_signing_public_key')
|
||||
expect(contact.contactUuid).to.equal('123')
|
||||
})
|
||||
|
||||
it('should create self contact on registration', async () => {
|
||||
const selfContact = context.contacts.getSelfContact()
|
||||
|
||||
expect(selfContact).to.not.be.undefined
|
||||
|
||||
expect(selfContact.publicKeySet.encryption).to.equal(context.publicKey)
|
||||
expect(selfContact.publicKeySet.signing).to.equal(context.signingPublicKey)
|
||||
})
|
||||
|
||||
it('should create self contact on sign in if it does not exist', async () => {
|
||||
let selfContact = context.contacts.getSelfContact()
|
||||
await context.mutator.setItemToBeDeleted(selfContact)
|
||||
await context.sync()
|
||||
await context.signout()
|
||||
|
||||
await context.signIn()
|
||||
selfContact = context.contacts.getSelfContact()
|
||||
expect(selfContact).to.not.be.undefined
|
||||
})
|
||||
|
||||
it('should update self contact on password change', async () => {
|
||||
const selfContact = context.contacts.getSelfContact()
|
||||
|
||||
await context.changePassword('new_password')
|
||||
|
||||
const updatedSelfContact = context.contacts.getSelfContact()
|
||||
|
||||
expect(updatedSelfContact.publicKeySet.encryption).to.not.equal(selfContact.publicKeySet.encryption)
|
||||
expect(updatedSelfContact.publicKeySet.signing).to.not.equal(selfContact.publicKeySet.signing)
|
||||
|
||||
expect(updatedSelfContact.publicKeySet.encryption).to.equal(context.publicKey)
|
||||
expect(updatedSelfContact.publicKeySet.signing).to.equal(context.signingPublicKey)
|
||||
})
|
||||
|
||||
it('should not be able to delete self contact', async () => {
|
||||
const selfContact = context.contacts.getSelfContact()
|
||||
|
||||
await Factory.expectThrowsAsync(() => context.contacts.deleteContact(selfContact), 'Cannot delete self')
|
||||
})
|
||||
|
||||
it('should not be able to delete a trusted contact if it belongs to a vault I administer', async () => {
|
||||
console.error('TODO: implement test')
|
||||
})
|
||||
})
|
||||
204
packages/snjs/mocha/vaults/crypto.test.js
Normal file
204
packages/snjs/mocha/vaults/crypto.test.js
Normal file
@@ -0,0 +1,204 @@
|
||||
import * as Factory from '../lib/factory.js'
|
||||
import * as Collaboration from '../lib/Collaboration.js'
|
||||
|
||||
chai.use(chaiAsPromised)
|
||||
const expect = chai.expect
|
||||
|
||||
describe('shared vault crypto', function () {
|
||||
this.timeout(Factory.TwentySecondTimeout)
|
||||
|
||||
let context
|
||||
|
||||
afterEach(async function () {
|
||||
await context.deinit()
|
||||
localStorage.clear()
|
||||
})
|
||||
|
||||
beforeEach(async function () {
|
||||
localStorage.clear()
|
||||
|
||||
context = await Factory.createAppContextWithRealCrypto()
|
||||
|
||||
await context.launch()
|
||||
await context.register()
|
||||
})
|
||||
|
||||
describe('root key', () => {
|
||||
it('root key loaded from disk should have keypairs', async () => {
|
||||
const appIdentifier = context.identifier
|
||||
await context.deinit()
|
||||
|
||||
let recreatedContext = await Factory.createAppContextWithRealCrypto(appIdentifier)
|
||||
await recreatedContext.launch()
|
||||
|
||||
expect(recreatedContext.encryption.getKeyPair()).to.not.be.undefined
|
||||
expect(recreatedContext.encryption.getSigningKeyPair()).to.not.be.undefined
|
||||
})
|
||||
|
||||
it('changing user password should re-encrypt all key system root keys', async () => {
|
||||
console.error('TODO: implement')
|
||||
})
|
||||
|
||||
it('changing user password should re-encrypt all trusted contacts', async () => {
|
||||
console.error('TODO: implement')
|
||||
})
|
||||
})
|
||||
|
||||
describe('persistent content signature', () => {
|
||||
it('storage payloads should include signatureData', async () => {
|
||||
const { note, contactContext, deinitContactContext } =
|
||||
await Collaboration.createSharedVaultWithAcceptedInviteAndNote(context)
|
||||
|
||||
await contactContext.changeNoteTitleAndSync(note, 'new title')
|
||||
await context.sync()
|
||||
|
||||
const rawPayloads = await context.application.diskStorageService.getAllRawPayloads()
|
||||
const noteRawPayload = rawPayloads.find((payload) => payload.uuid === note.uuid)
|
||||
|
||||
expect(noteRawPayload.signatureData).to.not.be.undefined
|
||||
|
||||
await deinitContactContext()
|
||||
})
|
||||
|
||||
it('changing item content should erase existing signatureData', async () => {
|
||||
const { note, contactContext, deinitContactContext } =
|
||||
await Collaboration.createSharedVaultWithAcceptedInviteAndNote(context)
|
||||
|
||||
await contactContext.changeNoteTitleAndSync(note, 'new title')
|
||||
await context.sync()
|
||||
|
||||
let updatedNote = context.items.findItem(note.uuid)
|
||||
await context.changeNoteTitleAndSync(updatedNote, 'new title 2')
|
||||
|
||||
updatedNote = context.items.findItem(note.uuid)
|
||||
expect(updatedNote.signatureData).to.be.undefined
|
||||
|
||||
await deinitContactContext()
|
||||
})
|
||||
|
||||
it('encrypting an item into storage then loading it should verify authenticity of original content rather than most recent symmetric signature', async () => {
|
||||
const { note, contactContext, deinitContactContext } =
|
||||
await Collaboration.createSharedVaultWithAcceptedInviteAndNote(context)
|
||||
|
||||
await contactContext.changeNoteTitleAndSync(note, 'new title')
|
||||
|
||||
/** Override decrypt result to return failing signature */
|
||||
const objectToSpy = context.encryption
|
||||
sinon.stub(objectToSpy, 'decryptSplit').callsFake(async (split) => {
|
||||
objectToSpy.decryptSplit.restore()
|
||||
|
||||
const decryptedPayloads = await objectToSpy.decryptSplit(split)
|
||||
expect(decryptedPayloads.length).to.equal(1)
|
||||
|
||||
const payload = decryptedPayloads[0]
|
||||
const mutatedPayload = new DecryptedPayload({
|
||||
...payload.ejected(),
|
||||
signatureData: {
|
||||
...payload.signatureData,
|
||||
result: {
|
||||
...payload.signatureData.result,
|
||||
passes: false,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
return [mutatedPayload]
|
||||
})
|
||||
await context.sync()
|
||||
|
||||
let updatedNote = context.items.findItem(note.uuid)
|
||||
expect(updatedNote.content.title).to.equal('new title')
|
||||
expect(updatedNote.signatureData.result.passes).to.equal(false)
|
||||
|
||||
const appIdentifier = context.identifier
|
||||
await context.deinit()
|
||||
|
||||
let recreatedContext = await Factory.createAppContextWithRealCrypto(appIdentifier)
|
||||
await recreatedContext.launch()
|
||||
|
||||
updatedNote = recreatedContext.items.findItem(note.uuid)
|
||||
expect(updatedNote.signatureData.result.passes).to.equal(false)
|
||||
|
||||
/** Changing the content now should clear failing signature */
|
||||
await recreatedContext.changeNoteTitleAndSync(updatedNote, 'new title 2')
|
||||
updatedNote = recreatedContext.items.findItem(note.uuid)
|
||||
expect(updatedNote.signatureData).to.be.undefined
|
||||
|
||||
await recreatedContext.deinit()
|
||||
|
||||
recreatedContext = await Factory.createAppContextWithRealCrypto(appIdentifier)
|
||||
await recreatedContext.launch()
|
||||
|
||||
/** Decrypting from storage will now verify current user symmetric signature only */
|
||||
updatedNote = recreatedContext.items.findItem(note.uuid)
|
||||
expect(updatedNote.signatureData.result.passes).to.equal(true)
|
||||
|
||||
await recreatedContext.deinit()
|
||||
await deinitContactContext()
|
||||
})
|
||||
})
|
||||
|
||||
describe('symmetrically encrypted items', () => {
|
||||
it('created items with a payload source of remote saved should not have signature data', async () => {
|
||||
const note = await context.createSyncedNote()
|
||||
|
||||
expect(note.payload.source).to.equal(PayloadSource.RemoteSaved)
|
||||
|
||||
expect(note.signatureData).to.be.undefined
|
||||
})
|
||||
|
||||
it('retrieved items that are then remote saved should have their signature data cleared', async () => {
|
||||
const { note, contactContext, deinitContactContext } =
|
||||
await Collaboration.createSharedVaultWithAcceptedInviteAndNote(context)
|
||||
|
||||
await contactContext.changeNoteTitleAndSync(contactContext.items.findItem(note.uuid), 'new title')
|
||||
|
||||
await context.sync()
|
||||
expect(context.items.findItem(note.uuid).signatureData).to.not.be.undefined
|
||||
|
||||
await context.changeNoteTitleAndSync(context.items.findItem(note.uuid), 'new title')
|
||||
expect(context.items.findItem(note.uuid).signatureData).to.be.undefined
|
||||
|
||||
await deinitContactContext()
|
||||
})
|
||||
|
||||
it('should allow client verification of authenticity of shared item changes', async () => {
|
||||
const { note, contactContext, deinitContactContext } =
|
||||
await Collaboration.createSharedVaultWithAcceptedInviteAndNote(context)
|
||||
|
||||
expect(context.contacts.isItemAuthenticallySigned(note)).to.equal('not-applicable')
|
||||
|
||||
const contactNote = contactContext.items.findItem(note.uuid)
|
||||
|
||||
expect(contactContext.contacts.isItemAuthenticallySigned(contactNote)).to.equal('yes')
|
||||
|
||||
await contactContext.changeNoteTitleAndSync(contactNote, 'new title')
|
||||
|
||||
await context.sync()
|
||||
|
||||
let updatedNote = context.items.findItem(note.uuid)
|
||||
|
||||
expect(context.contacts.isItemAuthenticallySigned(updatedNote)).to.equal('yes')
|
||||
|
||||
await deinitContactContext()
|
||||
})
|
||||
})
|
||||
|
||||
describe('keypair revocation', () => {
|
||||
it('should be able to revoke non-current keypair', async () => {
|
||||
console.error('TODO')
|
||||
})
|
||||
|
||||
it('revoking a keypair should send a keypair revocation event to trusted contacts', async () => {
|
||||
console.error('TODO')
|
||||
})
|
||||
|
||||
it('should not be able to revoke current key pair', async () => {
|
||||
console.error('TODO')
|
||||
})
|
||||
|
||||
it('should distrust revoked keypair as contact', async () => {
|
||||
console.error('TODO')
|
||||
})
|
||||
})
|
||||
})
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user