From 247daddf5a9939aa885efc4da0ce86bb206ac43e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Karol=20S=C3=B3jko?= Date: Wed, 2 Aug 2023 00:23:56 +0200 Subject: [PATCH] chore: vault tests refactors and lint (#2374) --- .../Domain/Http/FetchRequestHandler.spec.ts | 4 +- .../src/Domain/Http/FetchRequestHandler.ts | 4 +- packages/api/src/Domain/Http/HttpService.ts | 10 +- packages/mobile/.mocharc.yml | 5 - .../responses/src/Domain/Item/RawSyncData.ts | 4 +- .../NotificationServerHash.ts} | 2 +- packages/responses/src/Domain/index.ts | 2 +- .../AsymmetricMessageService.spec.ts | 55 ++++- .../AsymmetricMessageService.ts | 51 +++-- .../AsymmetricMessageServiceInterface.ts | 7 +- .../UseCase/GetInboundMessages.ts | 11 +- .../UseCase/GetOutboundMessages.ts | 11 +- .../UseCase/ResendAllMessages.spec.ts | 124 ++++++++++++ .../UseCase/ResendAllMessages.ts | 30 ++- .../UseCase/ResendMessage.ts | 21 +- .../src/Domain/Contacts/ContactService.ts | 82 ++------ .../src/Domain/Contacts/SelfContactManager.ts | 21 -- .../Contacts/UseCase/CreateOrEditContact.ts | 4 - .../Domain/Contacts/UseCase/EditContact.ts | 8 +- .../Domain/Contacts/UseCase/FindContact.ts | 16 +- .../UseCase/HandleKeyPairChange.spec.ts | 161 +++++++++++++++ .../Contacts/UseCase/HandleKeyPairChange.ts | 125 ++++++++++-- .../Encryption/UseCase/DecryptBackupFile.ts | 9 +- .../services/src/Domain/Event/SyncEvent.ts | 6 +- .../src/Domain/Files/FileService.spec.ts | 7 + .../services/src/Domain/Files/FileService.ts | 12 +- .../Domain/Integrity/IntegrityService.spec.ts | 14 +- .../src/Domain/Integrity/IntegrityService.ts | 6 +- packages/services/src/Domain/Logging.ts | 33 --- .../src/Domain/Service/AbstractService.ts | 9 +- .../Service/ApplicationServiceInterface.ts | 1 - .../Domain/SharedVaults/SharedVaultService.ts | 8 +- .../UseCase/ConvertToSharedVault.ts | 2 +- .../Domain/UserEvent/NotificationService.ts | 52 +++++ .../UserEvent/NotificationServiceEvent.ts | 9 + .../src/Domain/UserEvent/UserEventService.ts | 43 ---- .../Domain/UserEvent/UserEventServiceEvent.ts | 9 - .../Domain/VaultInvite/VaultInviteService.ts | 6 +- packages/services/src/Domain/index.ts | 4 +- packages/snjs/lib/Application/Application.ts | 14 +- .../Application/Dependencies/Dependencies.ts | 48 +++-- .../lib/Application/Dependencies/Types.ts | 3 +- packages/snjs/lib/Logging.ts | 24 --- packages/snjs/lib/Services/Api/Paths.ts | 5 - .../ComponentManager/ComponentManager.spec.ts | 5 + .../ComponentManager/ComponentManager.ts | 10 +- .../ComponentManager/ComponentViewer.ts | 34 ++-- .../Services/Features/FeaturesService.spec.ts | 43 ++-- .../lib/Services/Features/FeaturesService.ts | 5 +- .../lib/Services/Items/ItemManager.spec.ts | 8 +- .../Services/Mutator/MutatorService.spec.ts | 8 +- .../Services/Payloads/PayloadManager.spec.ts | 7 +- .../lib/Services/Payloads/PayloadManager.ts | 9 +- .../lib/Services/Sync/Account/Response.ts | 4 +- .../snjs/lib/Services/Sync/SyncService.ts | 38 ++-- packages/snjs/mocha/application.test.js | 6 +- packages/snjs/mocha/auth.test.js | 10 +- packages/snjs/mocha/backups.test.js | 3 +- packages/snjs/mocha/features.test.js | 5 +- packages/snjs/mocha/history.test.js | 2 +- packages/snjs/mocha/keys.test.js | 4 +- packages/snjs/mocha/lib/AppContext.js | 97 ++++++--- packages/snjs/mocha/lib/Collaboration.js | 89 ++++++--- packages/snjs/mocha/lib/Utils.js | 25 ++- packages/snjs/mocha/lib/VaultsContext.js | 54 +++++ packages/snjs/mocha/lib/factory.js | 13 +- .../snjs/mocha/model_tests/importing.test.js | 4 +- .../snjs/mocha/note_display_criteria.test.js | 9 +- packages/snjs/mocha/payload_manager.test.js | 3 +- packages/snjs/mocha/preferences.test.js | 17 +- packages/snjs/mocha/session.test.js | 61 ++---- packages/snjs/mocha/settings.test.js | 1 - packages/snjs/mocha/storage.test.js | 13 +- .../snjs/mocha/sync_tests/conflicting.test.js | 69 +++---- packages/snjs/mocha/sync_tests/online.test.js | 8 +- .../mocha/vaults/asymmetric-messages.test.js | 188 ++++++++++++------ packages/snjs/mocha/vaults/conflicts.test.js | 2 +- packages/snjs/mocha/vaults/contacts.test.js | 5 +- packages/snjs/mocha/vaults/crypto.test.js | 18 +- packages/snjs/mocha/vaults/deletion.test.js | 2 +- packages/snjs/mocha/vaults/files.test.js | 8 +- packages/snjs/mocha/vaults/importing.test.js | 4 +- packages/snjs/mocha/vaults/invites.test.js | 2 +- packages/snjs/mocha/vaults/items.test.js | 15 +- .../snjs/mocha/vaults/key-management.test.js | 2 +- .../snjs/mocha/vaults/key-rotation.test.js | 12 +- .../snjs/mocha/vaults/keypair-change.test.js | 33 ++- .../snjs/mocha/vaults/permissions.test.js | 2 +- packages/snjs/mocha/vaults/pkc.test.js | 10 +- .../snjs/mocha/vaults/shared_vaults.test.js | 73 +++++-- packages/snjs/mocha/vaults/signatures.test.js | 2 +- packages/snjs/mocha/vaults/vaults.test.js | 10 +- packages/utils/src/Domain/Logger/LogLevel.ts | 1 + packages/utils/src/Domain/Logger/Logger.ts | 64 ++++++ .../src/Domain/Logger/LoggerInterface.ts | 10 + packages/utils/src/Domain/index.ts | 5 +- 96 files changed, 1463 insertions(+), 751 deletions(-) delete mode 100644 packages/mobile/.mocharc.yml rename packages/responses/src/Domain/{UserEvent/UserEventServerHash.ts => Notification/NotificationServerHash.ts} (77%) create mode 100644 packages/services/src/Domain/AsymmetricMessage/UseCase/ResendAllMessages.spec.ts create mode 100644 packages/services/src/Domain/Contacts/UseCase/HandleKeyPairChange.spec.ts delete mode 100644 packages/services/src/Domain/Logging.ts create mode 100644 packages/services/src/Domain/UserEvent/NotificationService.ts create mode 100644 packages/services/src/Domain/UserEvent/NotificationServiceEvent.ts delete mode 100644 packages/services/src/Domain/UserEvent/UserEventService.ts delete mode 100644 packages/services/src/Domain/UserEvent/UserEventServiceEvent.ts delete mode 100644 packages/snjs/lib/Logging.ts create mode 100644 packages/snjs/mocha/lib/VaultsContext.js create mode 100644 packages/utils/src/Domain/Logger/LogLevel.ts create mode 100644 packages/utils/src/Domain/Logger/Logger.ts create mode 100644 packages/utils/src/Domain/Logger/LoggerInterface.ts diff --git a/packages/api/src/Domain/Http/FetchRequestHandler.spec.ts b/packages/api/src/Domain/Http/FetchRequestHandler.spec.ts index 4da7c198c..699d52da7 100644 --- a/packages/api/src/Domain/Http/FetchRequestHandler.spec.ts +++ b/packages/api/src/Domain/Http/FetchRequestHandler.spec.ts @@ -4,12 +4,14 @@ import { FetchRequestHandler } from './FetchRequestHandler' import { HttpErrorResponseBody, HttpRequest } from '@standardnotes/responses' import { ErrorMessage } from '../Error' +import { LoggerInterface } from '@standardnotes/utils' describe('FetchRequestHandler', () => { const snjsVersion = 'snjsVersion' const appVersion = 'appVersion' const environment = Environment.Web - const requestHandler = new FetchRequestHandler(snjsVersion, appVersion, environment) + const logger: LoggerInterface = {} as jest.Mocked + const requestHandler = new FetchRequestHandler(snjsVersion, appVersion, environment, logger) it('should create a request', () => { const httpRequest: HttpRequest = { diff --git a/packages/api/src/Domain/Http/FetchRequestHandler.ts b/packages/api/src/Domain/Http/FetchRequestHandler.ts index 71fcbcc2a..558ffa3fc 100644 --- a/packages/api/src/Domain/Http/FetchRequestHandler.ts +++ b/packages/api/src/Domain/Http/FetchRequestHandler.ts @@ -11,12 +11,14 @@ import { RequestHandlerInterface } from './RequestHandlerInterface' import { Environment } from '@standardnotes/models' import { isString } from 'lodash' import { ErrorMessage } from '../Error' +import { LoggerInterface } from '@standardnotes/utils' export class FetchRequestHandler implements RequestHandlerInterface { constructor( protected readonly snjsVersion: string, protected readonly appVersion: string, protected readonly environment: Environment, + private logger: LoggerInterface, ) {} async handleRequest(httpRequest: HttpRequest): Promise> { @@ -122,7 +124,7 @@ export class FetchRequestHandler implements RequestHandlerInterface { } } } catch (error) { - console.error(error) + this.logger.error(JSON.stringify(error)) } if (httpStatus >= HttpStatusCode.Success && httpStatus < HttpStatusCode.InternalServerError) { diff --git a/packages/api/src/Domain/Http/HttpService.ts b/packages/api/src/Domain/Http/HttpService.ts index d3350b309..7b9163543 100644 --- a/packages/api/src/Domain/Http/HttpService.ts +++ b/packages/api/src/Domain/Http/HttpService.ts @@ -1,4 +1,4 @@ -import { joinPaths, sleep } from '@standardnotes/utils' +import { LoggerInterface, joinPaths, sleep } from '@standardnotes/utils' import { Environment } from '@standardnotes/models' import { LegacySession, Session, SessionToken } from '@standardnotes/domain-core' import { @@ -21,6 +21,7 @@ export class HttpService implements HttpServiceInterface { private session?: Session | LegacySession private __latencySimulatorMs?: number private declare host: string + loggingEnabled = false private inProgressRefreshSessionPromise?: Promise private updateMetaCallback!: (meta: HttpResponseMeta) => void @@ -32,8 +33,9 @@ export class HttpService implements HttpServiceInterface { private environment: Environment, private appVersion: string, private snjsVersion: string, + private logger: LoggerInterface, ) { - this.requestHandler = new FetchRequestHandler(this.snjsVersion, this.appVersion, this.environment) + this.requestHandler = new FetchRequestHandler(this.snjsVersion, this.appVersion, this.environment, this.logger) } setCallbacks( @@ -150,6 +152,10 @@ export class HttpService implements HttpServiceInterface { const response = await this.requestHandler.handleRequest(httpRequest) + if (this.loggingEnabled && isErrorResponse(response)) { + this.logger.error('Request failed', httpRequest, response) + } + if (response.meta && !httpRequest.external) { this.updateMetaCallback?.(response.meta) } diff --git a/packages/mobile/.mocharc.yml b/packages/mobile/.mocharc.yml deleted file mode 100644 index 72aa73adb..000000000 --- a/packages/mobile/.mocharc.yml +++ /dev/null @@ -1,5 +0,0 @@ -recursive: true -timeout: 120000 -bail: true -file: - - e2e/init.js \ No newline at end of file diff --git a/packages/responses/src/Domain/Item/RawSyncData.ts b/packages/responses/src/Domain/Item/RawSyncData.ts index c5f2fd845..ee338d8a3 100644 --- a/packages/responses/src/Domain/Item/RawSyncData.ts +++ b/packages/responses/src/Domain/Item/RawSyncData.ts @@ -3,7 +3,7 @@ import { ApiEndpointParam } from './ApiEndpointParam' import { ConflictParams } from './ConflictParams' import { ServerItemResponse } from './ServerItemResponse' import { SharedVaultServerHash } from '../SharedVaults/SharedVaultServerHash' -import { UserEventServerHash } from '../UserEvent/UserEventServerHash' +import { NotificationServerHash } from '../Notification/NotificationServerHash' import { AsymmetricMessageServerHash } from '../AsymmetricMessage/AsymmetricMessageServerHash' export type RawSyncData = { @@ -16,7 +16,7 @@ export type RawSyncData = { unsaved?: ConflictParams[] shared_vaults?: SharedVaultServerHash[] shared_vault_invites?: SharedVaultInviteServerHash[] - notifications?: UserEventServerHash[] + notifications?: NotificationServerHash[] messages?: AsymmetricMessageServerHash[] status?: number } diff --git a/packages/responses/src/Domain/UserEvent/UserEventServerHash.ts b/packages/responses/src/Domain/Notification/NotificationServerHash.ts similarity index 77% rename from packages/responses/src/Domain/UserEvent/UserEventServerHash.ts rename to packages/responses/src/Domain/Notification/NotificationServerHash.ts index a57eaa4b3..3b9e36798 100644 --- a/packages/responses/src/Domain/UserEvent/UserEventServerHash.ts +++ b/packages/responses/src/Domain/Notification/NotificationServerHash.ts @@ -1,4 +1,4 @@ -export type UserEventServerHash = { +export type NotificationServerHash = { uuid: string user_uuid: string type: string diff --git a/packages/responses/src/Domain/index.ts b/packages/responses/src/Domain/index.ts index 5e5968e9a..fd5f9d193 100644 --- a/packages/responses/src/Domain/index.ts +++ b/packages/responses/src/Domain/index.ts @@ -66,4 +66,4 @@ export * from './User/PostSubscriptionTokensResponse' export * from './User/SettingData' export * from './User/UpdateSettingResponse' -export * from './UserEvent/UserEventServerHash' +export * from './Notification/NotificationServerHash' diff --git a/packages/services/src/Domain/AsymmetricMessage/AsymmetricMessageService.spec.ts b/packages/services/src/Domain/AsymmetricMessage/AsymmetricMessageService.spec.ts index 6d0145b82..398c57eca 100644 --- a/packages/services/src/Domain/AsymmetricMessage/AsymmetricMessageService.spec.ts +++ b/packages/services/src/Domain/AsymmetricMessage/AsymmetricMessageService.spec.ts @@ -25,6 +25,7 @@ import { KeySystemRootKeyContentSpecialized, TrustedContactInterface, } from '@standardnotes/models' +import { Result } from '@standardnotes/domain-core' describe('AsymmetricMessageService', () => { let sync: jest.Mocked @@ -61,6 +62,7 @@ describe('AsymmetricMessageService', () => { encryption, mutator, sessions, + sync, messageServer, createOrEditContact, findContact, @@ -115,6 +117,45 @@ describe('AsymmetricMessageService', () => { }) }) + describe('handleTrustedMessageResult', () => { + it('should not double handle the same message', async () => { + /** + * Because message retrieval is based on a syncToken, and the server aligns syncTokens to items sent back + * rather than messages, we may receive the same message twice. We want to keep track of processed messages + * and avoid double processing. + */ + + const message: AsymmetricMessageServerHash = { + uuid: 'message', + recipient_uuid: '1', + sender_uuid: '2', + encrypted_message: 'encrypted_message', + created_at_timestamp: 2, + updated_at_timestamp: 2, + } + + const decryptedMessagePayload: AsymmetricMessageTrustedContactShare = { + type: AsymmetricMessagePayloadType.ContactShare, + data: { + recipientUuid: '1', + trustedContact: {} as TrustedContactInterface, + }, + } + + service.getTrustedMessagePayload = service.getUntrustedMessagePayload = jest + .fn() + .mockReturnValue(Result.ok(decryptedMessagePayload)) + + service.handleTrustedContactShareMessage = jest.fn() + await service.handleTrustedMessageResult(message, decryptedMessagePayload) + expect(service.handleTrustedContactShareMessage).toHaveBeenCalledTimes(1) + + service.handleTrustedContactShareMessage = jest.fn() + await service.handleTrustedMessageResult(message, decryptedMessagePayload) + expect(service.handleTrustedContactShareMessage).toHaveBeenCalledTimes(0) + }) + }) + it('should process incoming messages oldest first', async () => { const messages: AsymmetricMessageServerHash[] = [ { @@ -139,7 +180,7 @@ describe('AsymmetricMessageService', () => { service.getTrustedMessagePayload = service.getUntrustedMessagePayload = jest .fn() - .mockReturnValue(trustedPayloadMock) + .mockReturnValue(Result.ok(trustedPayloadMock)) const handleTrustedContactShareMessageMock = jest.fn() service.handleTrustedContactShareMessage = handleTrustedContactShareMessageMock @@ -171,7 +212,7 @@ describe('AsymmetricMessageService', () => { service.handleTrustedContactShareMessage = jest.fn() service.getTrustedMessagePayload = service.getUntrustedMessagePayload = jest .fn() - .mockReturnValue(decryptedMessagePayload) + .mockReturnValue(Result.ok(decryptedMessagePayload)) await service.handleRemoteReceivedAsymmetricMessages([message]) @@ -200,7 +241,7 @@ describe('AsymmetricMessageService', () => { service.handleTrustedSenderKeypairChangedMessage = jest.fn() service.getTrustedMessagePayload = service.getUntrustedMessagePayload = jest .fn() - .mockReturnValue(decryptedMessagePayload) + .mockReturnValue(Result.ok(decryptedMessagePayload)) await service.handleRemoteReceivedAsymmetricMessages([message]) @@ -228,7 +269,7 @@ describe('AsymmetricMessageService', () => { service.handleTrustedSharedVaultRootKeyChangedMessage = jest.fn() service.getTrustedMessagePayload = service.getUntrustedMessagePayload = jest .fn() - .mockReturnValue(decryptedMessagePayload) + .mockReturnValue(Result.ok(decryptedMessagePayload)) await service.handleRemoteReceivedAsymmetricMessages([message]) @@ -258,7 +299,7 @@ describe('AsymmetricMessageService', () => { service.handleTrustedVaultMetadataChangedMessage = jest.fn() service.getTrustedMessagePayload = service.getUntrustedMessagePayload = jest .fn() - .mockReturnValue(decryptedMessagePayload) + .mockReturnValue(Result.ok(decryptedMessagePayload)) await service.handleRemoteReceivedAsymmetricMessages([message]) @@ -284,7 +325,7 @@ describe('AsymmetricMessageService', () => { service.getTrustedMessagePayload = service.getUntrustedMessagePayload = jest .fn() - .mockReturnValue(decryptedMessagePayload) + .mockReturnValue(Result.ok(decryptedMessagePayload)) await expect(service.handleRemoteReceivedAsymmetricMessages([message])).rejects.toThrow( 'Shared vault invites payloads are not handled as part of asymmetric messages', @@ -313,7 +354,7 @@ describe('AsymmetricMessageService', () => { service.handleTrustedContactShareMessage = jest.fn() service.getTrustedMessagePayload = service.getUntrustedMessagePayload = jest .fn() - .mockReturnValue(decryptedMessagePayload) + .mockReturnValue(Result.ok(decryptedMessagePayload)) await service.handleRemoteReceivedAsymmetricMessages([message]) diff --git a/packages/services/src/Domain/AsymmetricMessage/AsymmetricMessageService.ts b/packages/services/src/Domain/AsymmetricMessage/AsymmetricMessageService.ts index 820b6034b..ede56a369 100644 --- a/packages/services/src/Domain/AsymmetricMessage/AsymmetricMessageService.ts +++ b/packages/services/src/Domain/AsymmetricMessage/AsymmetricMessageService.ts @@ -1,6 +1,7 @@ +import { SyncServiceInterface } from './../Sync/SyncServiceInterface' import { SessionsClientInterface } from './../Session/SessionsClientInterface' import { MutatorClientInterface } from './../Mutator/MutatorClientInterface' -import { AsymmetricMessageServerHash, ClientDisplayableError, isClientDisplayableError } from '@standardnotes/responses' +import { AsymmetricMessageServerHash } from '@standardnotes/responses' import { SyncEvent, SyncEventReceivedAsymmetricMessagesData } from '../Event/SyncEvent' import { InternalEventBusInterface } from '../Internal/InternalEventBusInterface' import { InternalEventHandlerInterface } from '../Internal/InternalEventHandlerInterface' @@ -20,7 +21,6 @@ import { VaultListingInterface, } from '@standardnotes/models' import { HandleRootKeyChangedMessage } from './UseCase/HandleRootKeyChangedMessage' -import { SessionEvent } from '../Session/SessionEvent' import { AsymmetricMessageServer } from '@standardnotes/api' import { GetOutboundMessages } from './UseCase/GetOutboundMessages' import { GetInboundMessages } from './UseCase/GetInboundMessages' @@ -31,15 +31,19 @@ import { FindContact } from '../Contacts/UseCase/FindContact' import { CreateOrEditContact } from '../Contacts/UseCase/CreateOrEditContact' import { ReplaceContactData } from '../Contacts/UseCase/ReplaceContactData' import { EncryptionProviderInterface } from '../Encryption/EncryptionProviderInterface' +import { Result } from '@standardnotes/domain-core' export class AsymmetricMessageService extends AbstractService implements AsymmetricMessageServiceInterface, InternalEventHandlerInterface { + private handledMessages = new Set() + constructor( private encryption: EncryptionProviderInterface, private mutator: MutatorClientInterface, private sessions: SessionsClientInterface, + private sync: SyncServiceInterface, private messageServer: AsymmetricMessageServer, private _createOrEditContact: CreateOrEditContact, private _findContact: FindContact, @@ -73,30 +77,27 @@ export class AsymmetricMessageService async handleEvent(event: InternalEventInterface): Promise { switch (event.type) { - case SessionEvent.UserKeyPairChanged: - void this.messageServer.deleteAllInboundMessages() - break case SyncEvent.ReceivedAsymmetricMessages: void this.handleRemoteReceivedAsymmetricMessages(event.payload as SyncEventReceivedAsymmetricMessagesData) break } } - public async getOutboundMessages(): Promise { + public async getOutboundMessages(): Promise> { return this._getOutboundMessagesUseCase.execute() } - public async getInboundMessages(): Promise { + public async getInboundMessages(): Promise> { return this._getInboundMessagesUseCase.execute() } public async downloadAndProcessInboundMessages(): Promise { const messages = await this.getInboundMessages() - if (isClientDisplayableError(messages)) { + if (messages.isFailed()) { return } - await this.handleRemoteReceivedAsymmetricMessages(messages) + await this.handleRemoteReceivedAsymmetricMessages(messages.getValue()) } sortServerMessages(messages: AsymmetricMessageServerHash[]): AsymmetricMessageServerHash[] { @@ -143,11 +144,11 @@ export class AsymmetricMessageService getServerMessageType(message: AsymmetricMessageServerHash): AsymmetricMessagePayloadType | undefined { const result = this.getUntrustedMessagePayload(message) - if (!result) { + if (result.isFailed()) { return undefined } - return result.type + return result.getValue().type } async handleRemoteReceivedAsymmetricMessages(messages: AsymmetricMessageServerHash[]): Promise { @@ -159,18 +160,26 @@ export class AsymmetricMessageService for (const message of sortedMessages) { const trustedPayload = this.getTrustedMessagePayload(message) - if (!trustedPayload) { + if (trustedPayload.isFailed()) { continue } - await this.handleTrustedMessageResult(message, trustedPayload) + await this.handleTrustedMessageResult(message, trustedPayload.getValue()) } + + void this.sync.sync() } - private async handleTrustedMessageResult( + async handleTrustedMessageResult( message: AsymmetricMessageServerHash, payload: AsymmetricMessagePayload, ): Promise { + if (this.handledMessages.has(message.uuid)) { + return + } + + this.handledMessages.add(message.uuid) + if (payload.type === AsymmetricMessagePayloadType.ContactShare) { await this.handleTrustedContactShareMessage(message, payload) } else if (payload.type === AsymmetricMessagePayloadType.SenderKeypairChanged) { @@ -186,23 +195,23 @@ export class AsymmetricMessageService await this.deleteMessageAfterProcessing(message) } - getUntrustedMessagePayload(message: AsymmetricMessageServerHash): AsymmetricMessagePayload | undefined { + getUntrustedMessagePayload(message: AsymmetricMessageServerHash): Result { const result = this._getUntrustedPayload.execute({ privateKey: this.encryption.getKeyPair().privateKey, message, }) if (result.isFailed()) { - return undefined + return Result.fail(result.getError()) } - return result.getValue() + return result } - getTrustedMessagePayload(message: AsymmetricMessageServerHash): AsymmetricMessagePayload | undefined { + getTrustedMessagePayload(message: AsymmetricMessageServerHash): Result { const contact = this._findContact.execute({ userUuid: message.sender_uuid }) if (contact.isFailed()) { - return undefined + return Result.fail(contact.getError()) } const result = this._getTrustedPayload.execute({ @@ -213,10 +222,10 @@ export class AsymmetricMessageService }) if (result.isFailed()) { - return undefined + return Result.fail(result.getError()) } - return result.getValue() + return result } async deleteMessageAfterProcessing(message: AsymmetricMessageServerHash): Promise { diff --git a/packages/services/src/Domain/AsymmetricMessage/AsymmetricMessageServiceInterface.ts b/packages/services/src/Domain/AsymmetricMessage/AsymmetricMessageServiceInterface.ts index aef707438..a60df45ab 100644 --- a/packages/services/src/Domain/AsymmetricMessage/AsymmetricMessageServiceInterface.ts +++ b/packages/services/src/Domain/AsymmetricMessage/AsymmetricMessageServiceInterface.ts @@ -1,7 +1,8 @@ -import { AsymmetricMessageServerHash, ClientDisplayableError } from '@standardnotes/responses' +import { Result } from '@standardnotes/domain-core' +import { AsymmetricMessageServerHash } from '@standardnotes/responses' export interface AsymmetricMessageServiceInterface { - getOutboundMessages(): Promise - getInboundMessages(): Promise + getOutboundMessages(): Promise> + getInboundMessages(): Promise> downloadAndProcessInboundMessages(): Promise } diff --git a/packages/services/src/Domain/AsymmetricMessage/UseCase/GetInboundMessages.ts b/packages/services/src/Domain/AsymmetricMessage/UseCase/GetInboundMessages.ts index 9b4aefcd8..0655adae4 100644 --- a/packages/services/src/Domain/AsymmetricMessage/UseCase/GetInboundMessages.ts +++ b/packages/services/src/Domain/AsymmetricMessage/UseCase/GetInboundMessages.ts @@ -1,16 +1,17 @@ -import { ClientDisplayableError, isErrorResponse, AsymmetricMessageServerHash } from '@standardnotes/responses' +import { isErrorResponse, AsymmetricMessageServerHash, getErrorFromErrorResponse } from '@standardnotes/responses' import { AsymmetricMessageServerInterface } from '@standardnotes/api' +import { Result, UseCaseInterface } from '@standardnotes/domain-core' -export class GetInboundMessages { +export class GetInboundMessages implements UseCaseInterface { constructor(private messageServer: AsymmetricMessageServerInterface) {} - async execute(): Promise { + async execute(): Promise> { const response = await this.messageServer.getMessages() if (isErrorResponse(response)) { - return ClientDisplayableError.FromNetworkError(response) + return Result.fail(getErrorFromErrorResponse(response).message) } - return response.data.messages + return Result.ok(response.data.messages) } } diff --git a/packages/services/src/Domain/AsymmetricMessage/UseCase/GetOutboundMessages.ts b/packages/services/src/Domain/AsymmetricMessage/UseCase/GetOutboundMessages.ts index 99c58874a..dd3f524ce 100644 --- a/packages/services/src/Domain/AsymmetricMessage/UseCase/GetOutboundMessages.ts +++ b/packages/services/src/Domain/AsymmetricMessage/UseCase/GetOutboundMessages.ts @@ -1,16 +1,17 @@ -import { ClientDisplayableError, isErrorResponse, AsymmetricMessageServerHash } from '@standardnotes/responses' +import { isErrorResponse, AsymmetricMessageServerHash, getErrorFromErrorResponse } from '@standardnotes/responses' import { AsymmetricMessageServerInterface } from '@standardnotes/api' +import { Result, UseCaseInterface } from '@standardnotes/domain-core' -export class GetOutboundMessages { +export class GetOutboundMessages implements UseCaseInterface { constructor(private messageServer: AsymmetricMessageServerInterface) {} - async execute(): Promise { + async execute(): Promise> { const response = await this.messageServer.getOutboundUserMessages() if (isErrorResponse(response)) { - return ClientDisplayableError.FromNetworkError(response) + return Result.fail(getErrorFromErrorResponse(response).message) } - return response.data.messages + return Result.ok(response.data.messages) } } diff --git a/packages/services/src/Domain/AsymmetricMessage/UseCase/ResendAllMessages.spec.ts b/packages/services/src/Domain/AsymmetricMessage/UseCase/ResendAllMessages.spec.ts new file mode 100644 index 000000000..36b766b5b --- /dev/null +++ b/packages/services/src/Domain/AsymmetricMessage/UseCase/ResendAllMessages.spec.ts @@ -0,0 +1,124 @@ +import { ResendAllMessages } from './ResendAllMessages' +import { Result } from '@standardnotes/domain-core' +import { PkcKeyPair } from '@standardnotes/sncrypto-common' +import { AsymmetricMessagePayloadType } from '@standardnotes/models' + +describe('ResendAllMessages', () => { + let mockDecryptOwnMessage: any + let mockMessageServer: any + let mockResendMessage: any + let mockFindContact: any + + let useCase: ResendAllMessages + let params: { + keys: { encryption: PkcKeyPair; signing: PkcKeyPair } + previousKeys?: { encryption: PkcKeyPair; signing: PkcKeyPair } + } + + beforeEach(() => { + jest.clearAllMocks() + + mockDecryptOwnMessage = { + execute: jest.fn(), + } + + mockMessageServer = { + getOutboundUserMessages: jest.fn(), + deleteMessage: jest.fn(), + } + + mockResendMessage = { + execute: jest.fn(), + } + + mockFindContact = { + execute: jest.fn(), + } + + useCase = new ResendAllMessages(mockResendMessage, mockDecryptOwnMessage, mockMessageServer, mockFindContact) + params = { + keys: { + encryption: { publicKey: 'new_public_key', privateKey: 'new_private_key' }, + signing: { publicKey: 'new_public_key', privateKey: 'new_private_key' }, + }, + } + }) + + it('should successfully resend all messages', async () => { + const messages = { + data: { messages: [{ recipient_uuid: 'uuid', uuid: 'uuid', encrypted_message: 'encrypted_message' }] }, + } + const recipient = { publicKeySet: { encryption: 'public_key' } } + const decryptedMessage = { type: AsymmetricMessagePayloadType.ContactShare } + + mockMessageServer.getOutboundUserMessages.mockReturnValue(messages) + mockFindContact.execute.mockReturnValue(Result.ok(recipient)) + mockDecryptOwnMessage.execute.mockReturnValue(Result.ok(decryptedMessage)) + + const result = await useCase.execute(params) + + expect(result).toEqual(Result.ok()) + expect(mockMessageServer.getOutboundUserMessages).toHaveBeenCalled() + expect(mockFindContact.execute).toHaveBeenCalled() + expect(mockDecryptOwnMessage.execute).toHaveBeenCalled() + expect(mockResendMessage.execute).toHaveBeenCalled() + expect(mockMessageServer.deleteMessage).toHaveBeenCalled() + }) + + it('should handle errors while getting outbound user messages', async () => { + mockMessageServer.getOutboundUserMessages.mockReturnValue({ data: { error: 'Error' } }) + + const result = await useCase.execute(params) + + expect(result.isFailed()).toBeTruthy() + expect(result.getError()).toBe('Failed to get outbound user messages') + }) + + it('should handle errors while finding contact', async () => { + const messages = { + data: { messages: [{ recipient_uuid: 'uuid', uuid: 'uuid', encrypted_message: 'encrypted_message' }] }, + } + + mockMessageServer.getOutboundUserMessages.mockReturnValue(messages) + mockFindContact.execute.mockReturnValue(Result.fail('Contact not found')) + + const result = await useCase.execute(params) + + expect(result.isFailed()).toBeTruthy() + expect(result.getError()).toContain('Contact not found') + }) + + it('should skip messages of excluded types', async () => { + const messages = { + data: { + messages: [ + { recipient_uuid: 'uuid', uuid: 'uuid', encrypted_message: 'encrypted_message' }, + { recipient_uuid: 'uuid2', uuid: 'uuid2', encrypted_message: 'encrypted_message2' }, + ], + }, + } + const recipient = { publicKeySet: { encryption: 'public_key' } } + const decryptedMessage1 = { type: AsymmetricMessagePayloadType.SenderKeypairChanged } + const decryptedMessage2 = { type: AsymmetricMessagePayloadType.ContactShare } + + mockMessageServer.getOutboundUserMessages.mockReturnValue(messages) + mockFindContact.execute.mockReturnValue(Result.ok(recipient)) + + mockDecryptOwnMessage.execute + .mockReturnValueOnce(Result.ok(decryptedMessage1)) + .mockReturnValueOnce(Result.ok(decryptedMessage2)) + + const result = await useCase.execute(params) + + expect(result).toEqual(Result.ok()) + expect(mockMessageServer.getOutboundUserMessages).toHaveBeenCalled() + expect(mockFindContact.execute).toHaveBeenCalledTimes(2) + expect(mockDecryptOwnMessage.execute).toHaveBeenCalledTimes(2) + expect(mockResendMessage.execute).toHaveBeenCalledTimes(1) + expect(mockMessageServer.deleteMessage).toHaveBeenCalledTimes(1) + expect(mockResendMessage.execute).toHaveBeenCalledWith( + expect.objectContaining({ rawMessage: messages.data.messages[1] }), + ) + expect(mockMessageServer.deleteMessage).toHaveBeenCalledWith({ messageUuid: messages.data.messages[1].uuid }) + }) +}) diff --git a/packages/services/src/Domain/AsymmetricMessage/UseCase/ResendAllMessages.ts b/packages/services/src/Domain/AsymmetricMessage/UseCase/ResendAllMessages.ts index c5826070e..e405164e2 100644 --- a/packages/services/src/Domain/AsymmetricMessage/UseCase/ResendAllMessages.ts +++ b/packages/services/src/Domain/AsymmetricMessage/UseCase/ResendAllMessages.ts @@ -1,17 +1,28 @@ +import { DecryptOwnMessage } from './../../Encryption/UseCase/Asymmetric/DecryptOwnMessage' import { AsymmetricMessageServerHash, isErrorResponse } from '@standardnotes/responses' import { PkcKeyPair } from '@standardnotes/sncrypto-common' import { Result, UseCaseInterface } from '@standardnotes/domain-core' import { AsymmetricMessageServerInterface } from '@standardnotes/api' import { ResendMessage } from './ResendMessage' import { FindContact } from '../../Contacts/UseCase/FindContact' +import { AsymmetricMessagePayload, AsymmetricMessagePayloadType } from '@standardnotes/models' export class ResendAllMessages implements UseCaseInterface { constructor( private resendMessage: ResendMessage, + private decryptOwnMessage: DecryptOwnMessage, private messageServer: AsymmetricMessageServerInterface, private findContact: FindContact, ) {} + messagesToExcludeFromResending(): AsymmetricMessagePayloadType[] { + /** + * Sender key pair changed messages should never be re-encrypted with new keys as they must use the + * previous keys used by the sender before their keypair changed. + */ + return [AsymmetricMessagePayloadType.SenderKeypairChanged] + } + async execute(params: { keys: { encryption: PkcKeyPair @@ -37,10 +48,27 @@ export class ResendAllMessages implements UseCaseInterface { continue } + const decryptionResult = this.decryptOwnMessage.execute({ + message: message.encrypted_message, + privateKey: params.previousKeys?.encryption.privateKey ?? params.keys.encryption.privateKey, + recipientPublicKey: recipient.getValue().publicKeySet.encryption, + }) + + if (decryptionResult.isFailed()) { + errors.push(`Failed to decrypt message ${message.uuid}`) + continue + } + + const decryptedMessage = decryptionResult.getValue() + if (this.messagesToExcludeFromResending().includes(decryptedMessage.type)) { + continue + } + await this.resendMessage.execute({ keys: params.keys, previousKeys: params.previousKeys, - message: message, + decryptedMessage: decryptedMessage, + rawMessage: message, recipient: recipient.getValue(), }) diff --git a/packages/services/src/Domain/AsymmetricMessage/UseCase/ResendMessage.ts b/packages/services/src/Domain/AsymmetricMessage/UseCase/ResendMessage.ts index acc5ac3f6..556322681 100644 --- a/packages/services/src/Domain/AsymmetricMessage/UseCase/ResendMessage.ts +++ b/packages/services/src/Domain/AsymmetricMessage/UseCase/ResendMessage.ts @@ -1,4 +1,3 @@ -import { DecryptOwnMessage } from '../../Encryption/UseCase/Asymmetric/DecryptOwnMessage' import { AsymmetricMessagePayload, TrustedContactInterface } from '@standardnotes/models' import { AsymmetricMessageServerHash } from '@standardnotes/responses' import { PkcKeyPair } from '@standardnotes/sncrypto-common' @@ -8,7 +7,6 @@ import { SendMessage } from './SendMessage' export class ResendMessage implements UseCaseInterface { constructor( - private decryptOwnMessage: DecryptOwnMessage, private sendMessage: SendMessage, private encryptMessage: EncryptMessage, ) {} @@ -23,22 +21,11 @@ export class ResendMessage implements UseCaseInterface { signing: PkcKeyPair } recipient: TrustedContactInterface - message: AsymmetricMessageServerHash + rawMessage: AsymmetricMessageServerHash + decryptedMessage: AsymmetricMessagePayload }): Promise> { - const decryptionResult = this.decryptOwnMessage.execute({ - message: params.message.encrypted_message, - privateKey: params.previousKeys?.encryption.privateKey ?? params.keys.encryption.privateKey, - recipientPublicKey: params.recipient.publicKeySet.encryption, - }) - - if (decryptionResult.isFailed()) { - return Result.fail(decryptionResult.getError()) - } - - const decryptedMessage = decryptionResult.getValue() - const encryptedMessage = this.encryptMessage.execute({ - message: decryptedMessage, + message: params.decryptedMessage, keys: params.keys, recipientPublicKey: params.recipient.publicKeySet.encryption, }) @@ -50,7 +37,7 @@ export class ResendMessage implements UseCaseInterface { const sendMessageResult = await this.sendMessage.execute({ recipientUuid: params.recipient.contactUuid, encryptedMessage: encryptedMessage.getValue(), - replaceabilityIdentifier: params.message.replaceabilityIdentifier, + replaceabilityIdentifier: params.rawMessage.replaceabilityIdentifier, }) return sendMessageResult diff --git a/packages/services/src/Domain/Contacts/ContactService.ts b/packages/services/src/Domain/Contacts/ContactService.ts index ef7334f95..bf21e14f5 100644 --- a/packages/services/src/Domain/Contacts/ContactService.ts +++ b/packages/services/src/Domain/Contacts/ContactService.ts @@ -1,10 +1,5 @@ -import { SendOwnContactChangeMessage } from './UseCase/SendOwnContactChangeMessage' import { DeleteContact } from './UseCase/DeleteContact' import { MutatorClientInterface } from './../Mutator/MutatorClientInterface' -import { UserKeyPairChangedEventData } from './../Session/UserKeyPairChangedEventData' -import { SessionEvent } from './../Session/SessionEvent' -import { InternalEventInterface } from './../Internal/InternalEventInterface' -import { InternalEventHandlerInterface } from './../Internal/InternalEventHandlerInterface' import { PureCryptoInterface } from '@standardnotes/sncrypto-common' import { SharedVaultInviteServerHash, SharedVaultUserServerHash } from '@standardnotes/responses' import { TrustedContactInterface, TrustedContactMutator, DecryptedItemInterface } from '@standardnotes/models' @@ -25,10 +20,7 @@ import { GetAllContacts } from './UseCase/GetAllContacts' import { EncryptionProviderInterface } from '../Encryption/EncryptionProviderInterface' import { Result } from '@standardnotes/domain-core' -export class ContactService - extends AbstractService - implements ContactServiceInterface, InternalEventHandlerInterface -{ +export class ContactService extends AbstractService implements ContactServiceInterface { constructor( private sync: SyncServiceInterface, private mutator: MutatorClientInterface, @@ -43,48 +35,25 @@ export class ContactService private _createOrEditContact: CreateOrEditContact, private _editContact: EditContact, private _validateItemSigner: ValidateItemSigner, - private _sendOwnContactChangedMessage: SendOwnContactChangeMessage, eventBus: InternalEventBusInterface, ) { super(eventBus) - - eventBus.addEventHandler(this, SessionEvent.UserKeyPairChanged) } - async handleEvent(event: InternalEventInterface): Promise { - if (event.type === SessionEvent.UserKeyPairChanged) { - const data = event.payload as UserKeyPairChangedEventData - await this.selfContactManager.updateWithNewPublicKeySet({ - encryption: data.current.encryption.publicKey, - signing: data.current.signing.publicKey, - }) - void this.sendOwnContactChangeEventToAllContacts(event.payload as UserKeyPairChangedEventData) - } - } - - private async sendOwnContactChangeEventToAllContacts(data: UserKeyPairChangedEventData): Promise { - if (!data.previous) { - return - } - - const contacts = this._getAllContacts.execute() - if (contacts.isFailed()) { - return - } - - for (const contact of contacts.getValue()) { - if (contact.isMe) { - continue - } - - await this._sendOwnContactChangedMessage.execute({ - senderOldKeyPair: data.previous.encryption, - senderOldSigningKeyPair: data.previous.signing, - senderNewKeyPair: data.current.encryption, - senderNewSigningKeyPair: data.current.signing, - contact, - }) - } + override deinit(): void { + super.deinit() + ;(this.sync as unknown) = undefined + ;(this.mutator as unknown) = undefined + ;(this.session as unknown) = undefined + ;(this.crypto as unknown) = undefined + ;(this.user as unknown) = undefined + ;(this.selfContactManager as unknown) = undefined + ;(this.encryption as unknown) = undefined + ;(this._findContact as unknown) = undefined + ;(this._getAllContacts as unknown) = undefined + ;(this._createOrEditContact as unknown) = undefined + ;(this._editContact as unknown) = undefined + ;(this._validateItemSigner as unknown) = undefined } getSelfContact(): TrustedContactInterface | undefined { @@ -183,6 +152,8 @@ export class ContactService ): Promise { const updatedContact = await this._editContact.execute(contact, params) + void this.sync.sync() + return updatedContact } @@ -194,6 +165,9 @@ export class ContactService isMe?: boolean }): Promise { const contact = await this._createOrEditContact.execute(params) + + void this.sync.sync() + return contact } @@ -233,20 +207,4 @@ export class ContactService getItemSignatureStatus(item: DecryptedItemInterface): ItemSignatureValidationResult { return this._validateItemSigner.execute(item) } - - override deinit(): void { - super.deinit() - ;(this.sync as unknown) = undefined - ;(this.mutator as unknown) = undefined - ;(this.session as unknown) = undefined - ;(this.crypto as unknown) = undefined - ;(this.user as unknown) = undefined - ;(this.selfContactManager as unknown) = undefined - ;(this.encryption as unknown) = undefined - ;(this._findContact as unknown) = undefined - ;(this._getAllContacts as unknown) = undefined - ;(this._createOrEditContact as unknown) = undefined - ;(this._editContact as unknown) = undefined - ;(this._validateItemSigner as unknown) = undefined - } } diff --git a/packages/services/src/Domain/Contacts/SelfContactManager.ts b/packages/services/src/Domain/Contacts/SelfContactManager.ts index b0964b7bb..e97ecc554 100644 --- a/packages/services/src/Domain/Contacts/SelfContactManager.ts +++ b/packages/services/src/Domain/Contacts/SelfContactManager.ts @@ -17,9 +17,7 @@ import { TrustedContactContent, TrustedContactContentSpecialized, TrustedContactInterface, - PortablePublicKeySet, } from '@standardnotes/models' -import { CreateOrEditContact } from './UseCase/CreateOrEditContact' import { ContentType } from '@standardnotes/domain-core' const SelfContactName = 'Me' @@ -35,7 +33,6 @@ export class SelfContactManager implements InternalEventHandlerInterface { items: ItemManagerInterface, private session: SessionsClientInterface, private singletons: SingletonManagerInterface, - private createOrEditContact: CreateOrEditContact, ) { this.eventDisposers.push( sync.addEventObserver((event) => { @@ -82,23 +79,6 @@ export class SelfContactManager implements InternalEventHandlerInterface { ) } - public async updateWithNewPublicKeySet(publicKeySet: PortablePublicKeySet) { - if (!InternalFeatureService.get().isFeatureEnabled(InternalFeature.Vaults)) { - return - } - - if (!this.selfContact) { - return - } - - await this.createOrEditContact.execute({ - name: SelfContactName, - contactUuid: this.selfContact.contactUuid, - publicKey: publicKeySet.encryption, - signingPublicKey: publicKeySet.signing, - }) - } - private async reloadSelfContactAndCreateIfNecessary() { if (!InternalFeatureService.get().isFeatureEnabled(InternalFeature.Vaults)) { return @@ -146,6 +126,5 @@ export class SelfContactManager implements InternalEventHandlerInterface { this.eventDisposers.forEach((disposer) => disposer()) ;(this.session as unknown) = undefined ;(this.singletons as unknown) = undefined - ;(this.createOrEditContact as unknown) = undefined } } diff --git a/packages/services/src/Domain/Contacts/UseCase/CreateOrEditContact.ts b/packages/services/src/Domain/Contacts/UseCase/CreateOrEditContact.ts index b289e49ab..31a116eaf 100644 --- a/packages/services/src/Domain/Contacts/UseCase/CreateOrEditContact.ts +++ b/packages/services/src/Domain/Contacts/UseCase/CreateOrEditContact.ts @@ -1,4 +1,3 @@ -import { SyncServiceInterface } from '../../Sync/SyncServiceInterface' import { MutatorClientInterface } from '../../Mutator/MutatorClientInterface' import { ContactPublicKeySet, @@ -15,7 +14,6 @@ import { ContentType } from '@standardnotes/domain-core' export class CreateOrEditContact { constructor( private mutator: MutatorClientInterface, - private sync: SyncServiceInterface, private findContact: FindContact, private editContact: EditContact, ) {} @@ -54,8 +52,6 @@ export class CreateOrEditContact { true, ) - await this.sync.sync() - return contact } } diff --git a/packages/services/src/Domain/Contacts/UseCase/EditContact.ts b/packages/services/src/Domain/Contacts/UseCase/EditContact.ts index daaf441df..9200a5af3 100644 --- a/packages/services/src/Domain/Contacts/UseCase/EditContact.ts +++ b/packages/services/src/Domain/Contacts/UseCase/EditContact.ts @@ -1,12 +1,8 @@ -import { SyncServiceInterface } from '../../Sync/SyncServiceInterface' import { MutatorClientInterface } from '../../Mutator/MutatorClientInterface' import { TrustedContactInterface, TrustedContactMutator } from '@standardnotes/models' export class EditContact { - constructor( - private mutator: MutatorClientInterface, - private sync: SyncServiceInterface, - ) {} + constructor(private mutator: MutatorClientInterface) {} async execute( contact: TrustedContactInterface, @@ -28,8 +24,6 @@ export class EditContact { }, ) - await this.sync.sync() - return updatedContact } } diff --git a/packages/services/src/Domain/Contacts/UseCase/FindContact.ts b/packages/services/src/Domain/Contacts/UseCase/FindContact.ts index f53af789b..d5b58a9b0 100644 --- a/packages/services/src/Domain/Contacts/UseCase/FindContact.ts +++ b/packages/services/src/Domain/Contacts/UseCase/FindContact.ts @@ -8,16 +8,20 @@ export class FindContact implements SyncUseCaseInterface { if ('userUuid' in query && query.userUuid) { - const contact = this.items.itemsMatchingPredicate( + const contacts = this.items.itemsMatchingPredicate( ContentType.TYPES.TrustedContact, new Predicate('contactUuid', '=', query.userUuid), - )[0] + ) - if (contact) { - return Result.ok(contact) - } else { - return Result.fail('Contact not found') + if (contacts.length === 0) { + return Result.fail(`Contact not found for user ${query.userUuid}`) } + + if (contacts.length > 1) { + return Result.fail(`Multiple contacts found for user ${query.userUuid}`) + } + + return Result.ok(contacts[0]) } if ('signingPublicKey' in query && query.signingPublicKey) { diff --git a/packages/services/src/Domain/Contacts/UseCase/HandleKeyPairChange.spec.ts b/packages/services/src/Domain/Contacts/UseCase/HandleKeyPairChange.spec.ts new file mode 100644 index 000000000..d94dda89d --- /dev/null +++ b/packages/services/src/Domain/Contacts/UseCase/HandleKeyPairChange.spec.ts @@ -0,0 +1,161 @@ +import { HandleKeyPairChange } from './HandleKeyPairChange' +import { Result } from '@standardnotes/domain-core' +import { PkcKeyPair } from '@standardnotes/sncrypto-common' +import { LoggerInterface } from '@standardnotes/utils' + +describe('HandleKeyPairChange', () => { + let useCase: HandleKeyPairChange + let mockSelfContactManager: any + let mockInvitesServer: any + let mockMessageServer: any + let mockReuploadAllInvites: any + let mockResendAllMessages: any + let mockGetAllContacts: any + let mockCreateOrEditContact: any + let mockSendOwnContactChangedMessage: any + let logger: LoggerInterface + + const dto = { + newKeys: { + encryption: { + publicKey: 'new-encryption-public-key', + privateKey: 'new-encryption-private-key', + }, + signing: { + publicKey: 'new-signing-public-key', + privateKey: 'new-signing-private-key', + }, + }, + previousKeys: { + encryption: { + publicKey: 'previous-encryption-public-key', + privateKey: 'previous-encryption-private-key', + }, + signing: { + publicKey: 'previous-signing-public-key', + privateKey: 'previous-signing-private-key', + }, + }, + } + + beforeEach(() => { + mockSelfContactManager = { + updateWithNewPublicKeySet: jest.fn().mockReturnValue({}), + } + + mockInvitesServer = { + deleteAllInboundInvites: jest.fn().mockReturnValue({}), + } + + mockMessageServer = { + deleteAllInboundMessages: jest.fn().mockReturnValue({}), + } + + mockReuploadAllInvites = { + execute: jest.fn().mockReturnValue(Result.ok()), + } + + mockResendAllMessages = { + execute: jest.fn().mockReturnValue(Result.ok()), + } + + mockGetAllContacts = { + execute: jest.fn().mockReturnValue(Result.ok()), + } + + mockSendOwnContactChangedMessage = { + execute: jest.fn().mockReturnValue(Result.ok()), + } + + mockCreateOrEditContact = { + execute: jest.fn().mockReturnValue(Result.ok()), + } + + logger = {} as jest.Mocked + logger.error = jest.fn() + + useCase = new HandleKeyPairChange( + mockSelfContactManager, + mockInvitesServer, + mockMessageServer, + mockReuploadAllInvites, + mockResendAllMessages, + mockGetAllContacts, + mockSendOwnContactChangedMessage, + mockCreateOrEditContact, + logger, + ) + }) + + it('should handle key pair change correctly', async () => { + mockGetAllContacts.execute.mockReturnValue(Result.ok([])) + + const result = await useCase.execute(dto) + + expect(mockReuploadAllInvites.execute).toBeCalledWith({ keys: dto.newKeys, previousKeys: dto.previousKeys }) + expect(mockResendAllMessages.execute).toBeCalledWith({ keys: dto.newKeys, previousKeys: dto.previousKeys }) + expect(mockSendOwnContactChangedMessage.execute).not.toBeCalled() + expect(mockMessageServer.deleteAllInboundMessages).toBeCalled() + expect(mockInvitesServer.deleteAllInboundInvites).toBeCalled() + + expect(result.isFailed()).toBe(false) + }) + + it('should handle sending contact change event to all contacts', async () => { + const contact = { isMe: false } + mockGetAllContacts.execute.mockReturnValue(Result.ok([contact])) + + await useCase.execute(dto) + + expect(mockSendOwnContactChangedMessage.execute).toBeCalledWith({ + senderOldKeyPair: dto.previousKeys.encryption, + senderOldSigningKeyPair: dto.previousKeys.signing, + senderNewKeyPair: dto.newKeys.encryption, + senderNewSigningKeyPair: dto.newKeys.signing, + contact, + }) + }) + + it('should not send contact change event if previous keys are missing', async () => { + const contact = { isMe: false } + mockGetAllContacts.execute.mockReturnValue(Result.ok([contact])) + + await useCase.execute({ newKeys: dto.newKeys }) + + expect(mockSendOwnContactChangedMessage.execute).not.toBeCalled() + }) + + it('should not send contact change event if getAllContacts fails', async () => { + mockGetAllContacts.execute.mockReturnValue(Result.fail('Some error')) + + await useCase.execute(dto) + + expect(mockSendOwnContactChangedMessage.execute).not.toBeCalled() + }) + + it('should not send contact change event for self contact', async () => { + const contact = { isMe: true } + mockGetAllContacts.execute.mockReturnValue(Result.ok([contact])) + + await useCase.execute(dto) + + expect(mockSendOwnContactChangedMessage.execute).not.toBeCalled() + }) + + it('should reupload invites and resend messages before sending contact change message', async () => { + const contact = { isMe: false } + mockGetAllContacts.execute.mockReturnValue(Result.ok([contact])) + + await useCase.execute(dto) + + const callOrder = [ + mockReuploadAllInvites.execute, + mockResendAllMessages.execute, + mockSendOwnContactChangedMessage.execute, + ].map((fn) => fn.mock.invocationCallOrder[0]) + + for (let i = 0; i < callOrder.length - 1; i++) { + expect(callOrder[i]).toBeLessThan(callOrder[i + 1]) + } + }) +}) diff --git a/packages/services/src/Domain/Contacts/UseCase/HandleKeyPairChange.ts b/packages/services/src/Domain/Contacts/UseCase/HandleKeyPairChange.ts index 6e00938b0..1756b8ff8 100644 --- a/packages/services/src/Domain/Contacts/UseCase/HandleKeyPairChange.ts +++ b/packages/services/src/Domain/Contacts/UseCase/HandleKeyPairChange.ts @@ -1,34 +1,121 @@ +import { InternalFeatureService } from './../../InternalFeatures/InternalFeatureService' import { Result, UseCaseInterface } from '@standardnotes/domain-core' import { PkcKeyPair } from '@standardnotes/sncrypto-common' import { ReuploadAllInvites } from '../../VaultInvite/UseCase/ReuploadAllInvites' import { ResendAllMessages } from '../../AsymmetricMessage/UseCase/ResendAllMessages' +import { SelfContactManager } from '../SelfContactManager' +import { GetAllContacts } from './GetAllContacts' +import { SendOwnContactChangeMessage } from './SendOwnContactChangeMessage' +import { AsymmetricMessageServer, SharedVaultInvitesServer } from '@standardnotes/api' +import { PortablePublicKeySet } from '@standardnotes/models' +import { InternalFeature } from '../../InternalFeatures/InternalFeature' +import { CreateOrEditContact } from './CreateOrEditContact' +import { isErrorResponse } from '@standardnotes/responses' +import { LoggerInterface } from '@standardnotes/utils' + +type Dto = { + newKeys: { + encryption: PkcKeyPair + signing: PkcKeyPair + } + previousKeys?: { + encryption: PkcKeyPair + signing: PkcKeyPair + } +} export class HandleKeyPairChange implements UseCaseInterface { constructor( - private reuploadAllInvites: ReuploadAllInvites, - private resendAllMessages: ResendAllMessages, + private selfContactManager: SelfContactManager, + private invitesServer: SharedVaultInvitesServer, + private messageServer: AsymmetricMessageServer, + private _reuploadAllInvites: ReuploadAllInvites, + private _resendAllMessages: ResendAllMessages, + private _getAllContacts: GetAllContacts, + private _sendOwnContactChangedMessage: SendOwnContactChangeMessage, + private _createOrEditContact: CreateOrEditContact, + private logger: LoggerInterface, ) {} - async execute(dto: { - newKeys: { - encryption: PkcKeyPair - signing: PkcKeyPair - } - previousKeys?: { - encryption: PkcKeyPair - signing: PkcKeyPair - } - }): Promise> { - await this.reuploadAllInvites.execute({ - keys: dto.newKeys, - previousKeys: dto.previousKeys, + async execute(dto: Dto): Promise> { + await this.updateSelfContact({ + encryption: dto.newKeys.encryption.publicKey, + signing: dto.newKeys.signing.publicKey, }) - await this.resendAllMessages.execute({ - keys: dto.newKeys, - previousKeys: dto.previousKeys, - }) + const results = await Promise.all([ + this._reuploadAllInvites.execute({ + keys: dto.newKeys, + previousKeys: dto.previousKeys, + }), + + this._resendAllMessages.execute({ + keys: dto.newKeys, + previousKeys: dto.previousKeys, + }), + ]) + + for (const result of results) { + if (result.isFailed()) { + this.logger.error(result.getError()) + } + } + + await this.sendOwnContactChangeEventToAllContacts(dto) + + const deleteResponses = await Promise.all([ + this.messageServer.deleteAllInboundMessages(), + this.invitesServer.deleteAllInboundInvites(), + ]) + + for (const response of deleteResponses) { + if (isErrorResponse(response)) { + this.logger.error(JSON.stringify(response)) + } + } return Result.ok() } + + private async updateSelfContact(publicKeySet: PortablePublicKeySet) { + if (!InternalFeatureService.get().isFeatureEnabled(InternalFeature.Vaults)) { + return + } + + const selfContact = this.selfContactManager.selfContact + if (!selfContact) { + return + } + + await this._createOrEditContact.execute({ + contactUuid: selfContact.contactUuid, + publicKey: publicKeySet.encryption, + signingPublicKey: publicKeySet.signing, + }) + } + + private async sendOwnContactChangeEventToAllContacts(data: Dto): Promise { + if (!data.previousKeys) { + return + } + + const contacts = this._getAllContacts.execute() + if (contacts.isFailed()) { + return + } + + for (const contact of contacts.getValue()) { + if (contact.isMe) { + continue + } + + await this._sendOwnContactChangedMessage.execute({ + senderOldKeyPair: data.previousKeys.encryption, + senderOldSigningKeyPair: data.previousKeys.signing, + senderNewKeyPair: data.newKeys.encryption, + senderNewSigningKeyPair: data.newKeys.signing, + contact, + }) + } + } } diff --git a/packages/services/src/Domain/Encryption/UseCase/DecryptBackupFile.ts b/packages/services/src/Domain/Encryption/UseCase/DecryptBackupFile.ts index 9848173a8..a93288a0f 100644 --- a/packages/services/src/Domain/Encryption/UseCase/DecryptBackupFile.ts +++ b/packages/services/src/Domain/Encryption/UseCase/DecryptBackupFile.ts @@ -36,12 +36,15 @@ import { RootKeyParamsInterface, } from '@standardnotes/models' import { ClientDisplayableError } from '@standardnotes/responses' -import { extendArray } from '@standardnotes/utils' +import { extendArray, LoggerInterface } from '@standardnotes/utils' import { EncryptionService } from '../EncryptionService' import { ContentType } from '@standardnotes/domain-core' export class DecryptBackupFile { - constructor(private encryption: EncryptionService) {} + constructor( + private encryption: EncryptionService, + private logger: LoggerInterface, + ) {} async execute( file: BackupFile, @@ -273,7 +276,7 @@ export class DecryptBackupFile { errorDecrypting: true, }), ) - console.error('Error decrypting payload', encryptedPayload, e) + this.logger.error('Error decrypting payload', encryptedPayload, e) } } diff --git a/packages/services/src/Domain/Event/SyncEvent.ts b/packages/services/src/Domain/Event/SyncEvent.ts index 3fddfa3a7..19a65f139 100644 --- a/packages/services/src/Domain/Event/SyncEvent.ts +++ b/packages/services/src/Domain/Event/SyncEvent.ts @@ -2,7 +2,7 @@ import { AsymmetricMessageServerHash, SharedVaultInviteServerHash, SharedVaultServerHash, - UserEventServerHash, + NotificationServerHash, } from '@standardnotes/responses' /* istanbul ignore file */ @@ -31,11 +31,11 @@ export enum SyncEvent { SyncRequestsIntegrityCheck = 'sync:requests-integrity-check', ReceivedRemoteSharedVaults = 'received-shared-vaults', ReceivedSharedVaultInvites = 'received-shared-vault-invites', - ReceivedUserEvents = 'received-user-events', + ReceivedNotifications = 'received-user-events', ReceivedAsymmetricMessages = 'received-asymmetric-messages', } export type SyncEventReceivedRemoteSharedVaultsData = SharedVaultServerHash[] export type SyncEventReceivedSharedVaultInvitesData = SharedVaultInviteServerHash[] export type SyncEventReceivedAsymmetricMessagesData = AsymmetricMessageServerHash[] -export type SyncEventReceivedUserEventsData = UserEventServerHash[] +export type SyncEventReceivedNotificationsData = NotificationServerHash[] diff --git a/packages/services/src/Domain/Files/FileService.spec.ts b/packages/services/src/Domain/Files/FileService.spec.ts index 0d861884a..35d8c86bb 100644 --- a/packages/services/src/Domain/Files/FileService.spec.ts +++ b/packages/services/src/Domain/Files/FileService.spec.ts @@ -11,6 +11,7 @@ import { SyncServiceInterface } from '../Sync/SyncServiceInterface' import { FileService } from './FileService' import { BackupServiceInterface } from '@standardnotes/files' import { HttpServiceInterface } from '@standardnotes/api' +import { LoggerInterface } from '@standardnotes/utils' describe('fileService', () => { let apiService: LegacyApiServiceInterface @@ -26,6 +27,8 @@ describe('fileService', () => { let backupService: BackupServiceInterface let http: HttpServiceInterface + let logger: LoggerInterface + beforeEach(() => { apiService = {} as jest.Mocked apiService.addEventObserver = jest.fn() @@ -82,6 +85,9 @@ describe('fileService', () => { backupService.readEncryptedFileFromBackup = jest.fn() backupService.getFileBackupInfo = jest.fn() + logger = {} as jest.Mocked + logger.info = jest.fn() + http = {} as jest.Mocked fileService = new FileService( @@ -94,6 +100,7 @@ describe('fileService', () => { alertService, crypto, internalEventBus, + logger, backupService, ) diff --git a/packages/services/src/Domain/Files/FileService.ts b/packages/services/src/Domain/Files/FileService.ts index 4ed66a0b4..8a85cd490 100644 --- a/packages/services/src/Domain/Files/FileService.ts +++ b/packages/services/src/Domain/Files/FileService.ts @@ -19,7 +19,7 @@ import { SharedVaultListingInterface, } from '@standardnotes/models' import { PureCryptoInterface } from '@standardnotes/sncrypto-common' -import { spaceSeparatedStrings, UuidGenerator } from '@standardnotes/utils' +import { LoggerInterface, spaceSeparatedStrings, UuidGenerator } from '@standardnotes/utils' import { SNItemsKey } from '@standardnotes/encryption' import { DownloadAndDecryptFileOperation, @@ -47,7 +47,6 @@ import { InternalEventBusInterface } from '../Internal/InternalEventBusInterface import { AbstractService } from '../Service/AbstractService' import { SyncServiceInterface } from '../Sync/SyncServiceInterface' import { DecryptItemsKeyWithUserFallback } from '../Encryption/Functions' -import { log, LoggingDomain } from '../Logging' import { SharedVaultServer, SharedVaultServerInterface, HttpServiceInterface } from '@standardnotes/api' import { ContentType } from '@standardnotes/domain-core' import { EncryptionProviderInterface } from '../Encryption/EncryptionProviderInterface' @@ -68,6 +67,7 @@ export class FileService extends AbstractService implements FilesClientInterface private alertService: AlertService, private crypto: PureCryptoInterface, protected override internalEventBus: InternalEventBusInterface, + private logger: LoggerInterface, private backupsService?: BackupServiceInterface, ) { super(internalEventBus) @@ -317,19 +317,19 @@ export class FileService extends AbstractService implements FilesClientInterface const fileBackup = await this.backupsService?.getFileBackupInfo(file) if (this.backupsService && fileBackup) { - log(LoggingDomain.FilesService, 'Downloading file from backup', fileBackup) + this.logger.info('Downloading file from backup', fileBackup) await readAndDecryptBackupFileUsingBackupService(file, this.backupsService, this.crypto, async (chunk) => { - log(LoggingDomain.FilesService, 'Got local file chunk', chunk.progress) + this.logger.info('Got local file chunk', chunk.progress) return onDecryptedBytes(chunk.data, chunk.progress) }) - log(LoggingDomain.FilesService, 'Finished downloading file from backup') + this.logger.info('Finished downloading file from backup') return undefined } else { - log(LoggingDomain.FilesService, 'Downloading file from network') + this.logger.info('Downloading file from network') const addToCache = file.encryptedSize < this.encryptedCache.maxSize diff --git a/packages/services/src/Domain/Integrity/IntegrityService.spec.ts b/packages/services/src/Domain/Integrity/IntegrityService.spec.ts index 00cab5680..d1028882c 100644 --- a/packages/services/src/Domain/Integrity/IntegrityService.spec.ts +++ b/packages/services/src/Domain/Integrity/IntegrityService.spec.ts @@ -8,14 +8,16 @@ import { IntegrityApiInterface } from './IntegrityApiInterface' import { IntegrityService } from './IntegrityService' import { PayloadManagerInterface } from '../Payloads/PayloadManagerInterface' import { IntegrityPayload } from '@standardnotes/responses' +import { LoggerInterface } from '@standardnotes/utils' describe('IntegrityService', () => { let integrityApi: IntegrityApiInterface let itemApi: ItemsServerInterface let payloadManager: PayloadManagerInterface + let logger: LoggerInterface let internalEventBus: InternalEventBusInterface - const createService = () => new IntegrityService(integrityApi, itemApi, payloadManager, internalEventBus) + const createService = () => new IntegrityService(integrityApi, itemApi, payloadManager, logger, internalEventBus) beforeEach(() => { integrityApi = {} as jest.Mocked @@ -29,6 +31,10 @@ describe('IntegrityService', () => { internalEventBus = {} as jest.Mocked internalEventBus.publishSync = jest.fn() + + logger = {} as jest.Mocked + logger.info = jest.fn() + logger.error = jest.fn() }) it('should check integrity of payloads and publish mismatches', async () => { @@ -63,7 +69,7 @@ describe('IntegrityService', () => { uuid: '1-2-3', }, ], - source: "AfterDownloadFirst", + source: 'AfterDownloadFirst', }, type: 'IntegrityCheckCompleted', }, @@ -90,7 +96,7 @@ describe('IntegrityService', () => { { payload: { rawPayloads: [], - source: "AfterDownloadFirst", + source: 'AfterDownloadFirst', }, type: 'IntegrityCheckCompleted', }, @@ -140,7 +146,7 @@ describe('IntegrityService', () => { { payload: { rawPayloads: [], - source: "AfterDownloadFirst", + source: 'AfterDownloadFirst', }, type: 'IntegrityCheckCompleted', }, diff --git a/packages/services/src/Domain/Integrity/IntegrityService.ts b/packages/services/src/Domain/Integrity/IntegrityService.ts index b71e740f8..c73f044e9 100644 --- a/packages/services/src/Domain/Integrity/IntegrityService.ts +++ b/packages/services/src/Domain/Integrity/IntegrityService.ts @@ -10,6 +10,7 @@ import { SyncEvent } from '../Event/SyncEvent' import { IntegrityEventPayload } from './IntegrityEventPayload' import { SyncSource } from '../Sync/SyncSource' import { PayloadManagerInterface } from '../Payloads/PayloadManagerInterface' +import { LoggerInterface } from '@standardnotes/utils' export class IntegrityService extends AbstractService @@ -19,6 +20,7 @@ export class IntegrityService private integrityApi: IntegrityApiInterface, private itemApi: ItemsServerInterface, private payloadManager: PayloadManagerInterface, + private logger: LoggerInterface, protected override internalEventBus: InternalEventBusInterface, ) { super(internalEventBus) @@ -31,7 +33,7 @@ export class IntegrityService const integrityCheckResponse = await this.integrityApi.checkIntegrity(this.payloadManager.integrityPayloads) if (isErrorResponse(integrityCheckResponse)) { - this.log(`Could not obtain integrity check: ${integrityCheckResponse.data.error?.message}`) + this.logger.error(`Could not obtain integrity check: ${integrityCheckResponse.data.error?.message}`) return } @@ -50,7 +52,7 @@ export class IntegrityService isErrorResponse(serverItemResponse) || !('item' in serverItemResponse.data) ) { - this.log( + this.logger.error( `Could not obtain item for integrity adjustments: ${ isErrorResponse(serverItemResponse) ? serverItemResponse.data.error?.message : '' }`, diff --git a/packages/services/src/Domain/Logging.ts b/packages/services/src/Domain/Logging.ts deleted file mode 100644 index bafcc7c58..000000000 --- a/packages/services/src/Domain/Logging.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { logWithColor } from '@standardnotes/utils' - -declare const process: { - env: { - NODE_ENV: string | null | undefined - } -} - -export const isDev = process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'test' - -export enum LoggingDomain { - FilesService, - FilesBackups, -} - -const LoggingStatus: Record = { - [LoggingDomain.FilesService]: false, - [LoggingDomain.FilesBackups]: false, -} - -const LoggingColor: Record = { - [LoggingDomain.FilesService]: 'blue', - [LoggingDomain.FilesBackups]: 'yellow', -} - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export function log(domain: LoggingDomain, ...args: any[]): void { - if (!isDev || !LoggingStatus[domain]) { - return - } - - logWithColor(LoggingDomain[domain], LoggingColor[domain], ...args) -} diff --git a/packages/services/src/Domain/Service/AbstractService.ts b/packages/services/src/Domain/Service/AbstractService.ts index fb5e4326d..3a3b01f50 100644 --- a/packages/services/src/Domain/Service/AbstractService.ts +++ b/packages/services/src/Domain/Service/AbstractService.ts @@ -1,6 +1,6 @@ /* istanbul ignore file */ -import { log, removeFromArray } from '@standardnotes/utils' +import { removeFromArray } from '@standardnotes/utils' import { EventObserver } from '../Event/EventObserver' import { ApplicationServiceInterface } from './ApplicationServiceInterface' import { InternalEventBusInterface } from '../Internal/InternalEventBusInterface' @@ -99,11 +99,4 @@ export abstract class AbstractService isApplicationService(): true { return true } - - log(..._args: unknown[]): void { - if (this.loggingEnabled) { - // eslint-disable-next-line prefer-rest-params - log(this.getServiceName(), ...arguments) - } - } } diff --git a/packages/services/src/Domain/Service/ApplicationServiceInterface.ts b/packages/services/src/Domain/Service/ApplicationServiceInterface.ts index 7bd254a20..eb4d78d73 100644 --- a/packages/services/src/Domain/Service/ApplicationServiceInterface.ts +++ b/packages/services/src/Domain/Service/ApplicationServiceInterface.ts @@ -6,5 +6,4 @@ export interface ApplicationServiceInterface extends ServiceDiagnostics { addEventObserver(observer: EventObserver): () => void blockDeinit(): Promise deinit(): void - log(message: string, ...args: unknown[]): void } diff --git a/packages/services/src/Domain/SharedVaults/SharedVaultService.ts b/packages/services/src/Domain/SharedVaults/SharedVaultService.ts index a9b676dfd..035f53026 100644 --- a/packages/services/src/Domain/SharedVaults/SharedVaultService.ts +++ b/packages/services/src/Domain/SharedVaults/SharedVaultService.ts @@ -19,7 +19,7 @@ import { InternalEventBusInterface } from '../Internal/InternalEventBusInterface import { SyncEvent } from '../Event/SyncEvent' import { SessionEvent } from '../Session/SessionEvent' import { InternalEventInterface } from '../Internal/InternalEventInterface' -import { UserEventServiceEvent, UserEventServiceEventPayload } from '../UserEvent/UserEventServiceEvent' +import { NotificationServiceEvent, NotificationServiceEventPayload } from '../UserEvent/NotificationServiceEvent' import { DeleteThirdPartyVault } from './UseCase/DeleteExternalSharedVault' import { DeleteSharedVault } from './UseCase/DeleteSharedVault' import { VaultServiceEvent, VaultServiceEventPayload } from '../Vault/VaultServiceEvent' @@ -106,8 +106,8 @@ export class SharedVaultService }) break } - case UserEventServiceEvent.UserEventReceived: - await this.handleUserEvent(event.payload as UserEventServiceEventPayload) + case NotificationServiceEvent.NotificationReceived: + await this.handleUserEvent(event.payload as NotificationServiceEventPayload) break case VaultServiceEvent.VaultRootKeyRotated: { const payload = event.payload as VaultServiceEventPayload[VaultServiceEvent.VaultRootKeyRotated] @@ -120,7 +120,7 @@ export class SharedVaultService } } - private async handleUserEvent(event: UserEventServiceEventPayload): Promise { + private async handleUserEvent(event: NotificationServiceEventPayload): Promise { switch (event.eventPayload.props.type.value) { case NotificationType.TYPES.RemovedFromSharedVault: { const vault = this._getVault.execute({ diff --git a/packages/services/src/Domain/SharedVaults/UseCase/ConvertToSharedVault.ts b/packages/services/src/Domain/SharedVaults/UseCase/ConvertToSharedVault.ts index 52f92fd6f..5f89de17b 100644 --- a/packages/services/src/Domain/SharedVaults/UseCase/ConvertToSharedVault.ts +++ b/packages/services/src/Domain/SharedVaults/UseCase/ConvertToSharedVault.ts @@ -20,7 +20,7 @@ export class ConvertToSharedVault { const serverResult = await this.sharedVaultServer.createSharedVault() if (isErrorResponse(serverResult)) { - return ClientDisplayableError.FromString(`Failed to create shared vault ${JSON.stringify(serverResult)}`) + return ClientDisplayableError.FromString(`Failed to convert to shared vault ${JSON.stringify(serverResult)}`) } const serverVaultHash = serverResult.data.sharedVault diff --git a/packages/services/src/Domain/UserEvent/NotificationService.ts b/packages/services/src/Domain/UserEvent/NotificationService.ts new file mode 100644 index 000000000..0409f9c91 --- /dev/null +++ b/packages/services/src/Domain/UserEvent/NotificationService.ts @@ -0,0 +1,52 @@ +import { NotificationServerHash } from '@standardnotes/responses' +import { SyncEvent, SyncEventReceivedNotificationsData } from '../Event/SyncEvent' +import { InternalEventBusInterface } from '../Internal/InternalEventBusInterface' +import { InternalEventHandlerInterface } from '../Internal/InternalEventHandlerInterface' +import { InternalEventInterface } from '../Internal/InternalEventInterface' +import { AbstractService } from '../Service/AbstractService' +import { NotificationServiceEventPayload, NotificationServiceEvent } from './NotificationServiceEvent' +import { NotificationPayload } from '@standardnotes/domain-core' + +export class NotificationService + extends AbstractService + implements InternalEventHandlerInterface +{ + private handledNotifications = new Set() + + constructor(internalEventBus: InternalEventBusInterface) { + super(internalEventBus) + + internalEventBus.addEventHandler(this, SyncEvent.ReceivedNotifications) + } + + async handleEvent(event: InternalEventInterface): Promise { + if (event.type === SyncEvent.ReceivedNotifications) { + return this.handleReceivedNotifications(event.payload as SyncEventReceivedNotificationsData) + } + } + + private async handleReceivedNotifications(notifications: NotificationServerHash[]): Promise { + if (notifications.length === 0) { + return + } + + for (const notification of notifications) { + if (this.handledNotifications.has(notification.uuid)) { + continue + } + + this.handledNotifications.add(notification.uuid) + + const eventPayloadOrError = NotificationPayload.createFromString(notification.payload) + if (eventPayloadOrError.isFailed()) { + continue + } + + const payload: NotificationPayload = eventPayloadOrError.getValue() + + const serviceEvent: NotificationServiceEventPayload = { eventPayload: payload } + + await this.notifyEventSync(NotificationServiceEvent.NotificationReceived, serviceEvent) + } + } +} diff --git a/packages/services/src/Domain/UserEvent/NotificationServiceEvent.ts b/packages/services/src/Domain/UserEvent/NotificationServiceEvent.ts new file mode 100644 index 000000000..11b24addb --- /dev/null +++ b/packages/services/src/Domain/UserEvent/NotificationServiceEvent.ts @@ -0,0 +1,9 @@ +import { NotificationPayload } from '@standardnotes/domain-core' + +export enum NotificationServiceEvent { + NotificationReceived = 'NotificationReceived', +} + +export type NotificationServiceEventPayload = { + eventPayload: NotificationPayload +} diff --git a/packages/services/src/Domain/UserEvent/UserEventService.ts b/packages/services/src/Domain/UserEvent/UserEventService.ts deleted file mode 100644 index 0acf06709..000000000 --- a/packages/services/src/Domain/UserEvent/UserEventService.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { UserEventServerHash } from '@standardnotes/responses' -import { SyncEvent, SyncEventReceivedUserEventsData } from '../Event/SyncEvent' -import { InternalEventBusInterface } from '../Internal/InternalEventBusInterface' -import { InternalEventHandlerInterface } from '../Internal/InternalEventHandlerInterface' -import { InternalEventInterface } from '../Internal/InternalEventInterface' -import { AbstractService } from '../Service/AbstractService' -import { UserEventServiceEventPayload, UserEventServiceEvent } from './UserEventServiceEvent' -import { NotificationPayload } from '@standardnotes/domain-core' - -export class UserEventService - extends AbstractService - implements InternalEventHandlerInterface -{ - constructor(internalEventBus: InternalEventBusInterface) { - super(internalEventBus) - - internalEventBus.addEventHandler(this, SyncEvent.ReceivedUserEvents) - } - - async handleEvent(event: InternalEventInterface): Promise { - if (event.type === SyncEvent.ReceivedUserEvents) { - return this.handleReceivedUserEvents(event.payload as SyncEventReceivedUserEventsData) - } - } - - private async handleReceivedUserEvents(userEvents: UserEventServerHash[]): Promise { - if (userEvents.length === 0) { - return - } - - for (const serverEvent of userEvents) { - const eventPayloadOrError = NotificationPayload.createFromString(serverEvent.payload) - if (eventPayloadOrError.isFailed()) { - continue - } - const eventPayload = eventPayloadOrError.getValue() - - const serviceEvent: UserEventServiceEventPayload = { eventPayload } - - await this.notifyEventSync(UserEventServiceEvent.UserEventReceived, serviceEvent) - } - } -} diff --git a/packages/services/src/Domain/UserEvent/UserEventServiceEvent.ts b/packages/services/src/Domain/UserEvent/UserEventServiceEvent.ts deleted file mode 100644 index 991133caf..000000000 --- a/packages/services/src/Domain/UserEvent/UserEventServiceEvent.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { NotificationPayload } from '@standardnotes/domain-core' - -export enum UserEventServiceEvent { - UserEventReceived = 'UserEventReceived', -} - -export type UserEventServiceEventPayload = { - eventPayload: NotificationPayload -} diff --git a/packages/services/src/Domain/VaultInvite/VaultInviteService.ts b/packages/services/src/Domain/VaultInvite/VaultInviteService.ts index cd8a4ea8e..187c00499 100644 --- a/packages/services/src/Domain/VaultInvite/VaultInviteService.ts +++ b/packages/services/src/Domain/VaultInvite/VaultInviteService.ts @@ -1,6 +1,5 @@ import { AcceptVaultInvite } from './UseCase/AcceptVaultInvite' import { SyncEvent, SyncEventReceivedSharedVaultInvitesData } from './../Event/SyncEvent' -import { SessionEvent } from './../Session/SessionEvent' import { InternalEventInterface } from './../Internal/InternalEventInterface' import { InternalEventHandlerInterface } from './../Internal/InternalEventHandlerInterface' import { ItemManagerInterface } from './../Item/ItemManagerInterface' @@ -92,9 +91,6 @@ export class VaultInviteService async handleEvent(event: InternalEventInterface): Promise { switch (event.type) { - case SessionEvent.UserKeyPairChanged: - void this.invitesServer.deleteAllInboundInvites() - break case SyncEvent.ReceivedSharedVaultInvites: await this.processInboundInvites(event.payload as SyncEventReceivedSharedVaultInvitesData) break @@ -238,6 +234,8 @@ export class VaultInviteService } for (const invite of invites) { + delete this.pendingInvites[invite.uuid] + const sender = this._findContact.execute({ userUuid: invite.sender_uuid }) if (!sender.isFailed()) { const trustedMessage = this._getTrustedPayload.execute({ diff --git a/packages/services/src/Domain/index.ts b/packages/services/src/Domain/index.ts index 8fd8a2056..eae24607a 100644 --- a/packages/services/src/Domain/index.ts +++ b/packages/services/src/Domain/index.ts @@ -177,8 +177,8 @@ export * from './User/SignedOutEventPayload' export * from './User/UserClientInterface' export * from './User/UserClientInterface' export * from './User/UserService' -export * from './UserEvent/UserEventService' -export * from './UserEvent/UserEventServiceEvent' +export * from './UserEvent/NotificationService' +export * from './UserEvent/NotificationServiceEvent' export * from './VaultInvite/InviteRecord' export * from './VaultInvite/UseCase/AcceptVaultInvite' export * from './VaultInvite/UseCase/InviteToVault' diff --git a/packages/snjs/lib/Application/Application.ts b/packages/snjs/lib/Application/Application.ts index a94c352ca..1fd93868e 100644 --- a/packages/snjs/lib/Application/Application.ts +++ b/packages/snjs/lib/Application/Application.ts @@ -73,7 +73,7 @@ import { EncryptionProviderInterface, VaultUserServiceInterface, VaultInviteServiceInterface, - UserEventServiceEvent, + NotificationServiceEvent, VaultServiceEvent, VaultLockServiceInterface, } from '@standardnotes/services' @@ -116,6 +116,7 @@ import { sleep, UuidGenerator, useBoolean, + LoggerInterface, } from '@standardnotes/utils' import { UuidString, ApplicationEventPayload } from '../Types' import { applicationEventForSyncEvent } from '@Lib/Application/Event' @@ -504,7 +505,8 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli private beginAutoSyncTimer() { this.autoSyncInterval = setInterval(() => { - this.sync.log('Syncing from autosync') + const logger = this.dependencies.get(TYPES.Logger) + logger.info('Syncing from autosync') void this.sync.sync({ sourceDescription: 'Auto Sync' }) }, DEFAULT_AUTO_SYNC_INTERVAL) } @@ -807,7 +809,7 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli await promise } else { /** Await up to maxWait. If not resolved by then, return. */ - await Promise.race([promise, sleep(maxWait)]) + await Promise.race([promise, sleep(maxWait, false, 'Preparing for deinit...')]) } } @@ -1120,7 +1122,7 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli } private createBackgroundDependencies() { - this.dependencies.get(TYPES.UserEventService) + this.dependencies.get(TYPES.NotificationService) this.dependencies.get(TYPES.KeyRecoveryService) } @@ -1133,12 +1135,11 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli this.events.addEventHandler(this.dependencies.get(TYPES.SubscriptionManager), SessionEvent.Restored) this.events.addEventHandler(this.dependencies.get(TYPES.VaultInviteService), SyncEvent.ReceivedSharedVaultInvites) - this.events.addEventHandler(this.dependencies.get(TYPES.VaultInviteService), SessionEvent.UserKeyPairChanged) this.events.addEventHandler(this.dependencies.get(TYPES.SharedVaultService), SessionEvent.UserKeyPairChanged) this.events.addEventHandler( this.dependencies.get(TYPES.SharedVaultService), - UserEventServiceEvent.UserEventReceived, + NotificationServiceEvent.NotificationReceived, ) this.events.addEventHandler(this.dependencies.get(TYPES.SharedVaultService), VaultServiceEvent.VaultRootKeyRotated) this.events.addEventHandler(this.dependencies.get(TYPES.SharedVaultService), SyncEvent.ReceivedRemoteSharedVaults) @@ -1147,7 +1148,6 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli this.dependencies.get(TYPES.AsymmetricMessageService), SyncEvent.ReceivedAsymmetricMessages, ) - this.events.addEventHandler(this.dependencies.get(TYPES.AsymmetricMessageService), SessionEvent.UserKeyPairChanged) if (this.dependencies.get(TYPES.FilesBackupService)) { this.events.addEventHandler( diff --git a/packages/snjs/lib/Application/Dependencies/Dependencies.ts b/packages/snjs/lib/Application/Dependencies/Dependencies.ts index 97dd97448..d310afab6 100644 --- a/packages/snjs/lib/Application/Dependencies/Dependencies.ts +++ b/packages/snjs/lib/Application/Dependencies/Dependencies.ts @@ -52,7 +52,7 @@ import { SelfContactManager, StatusService, SubscriptionManager, - UserEventService, + NotificationService, UserService, ValidateItemSigner, isDesktopDevice, @@ -147,7 +147,7 @@ import { import { FullyResolvedApplicationOptions } from '../Options/ApplicationOptions' import { TYPES } from './Types' import { isDeinitable } from './isDeinitable' -import { isNotUndefined } from '@standardnotes/utils' +import { Logger, isNotUndefined } from '@standardnotes/utils' import { EncryptionOperators } from '@standardnotes/encryption' export class Dependencies { @@ -225,7 +225,7 @@ export class Dependencies { }) this.factory.set(TYPES.DecryptBackupFile, () => { - return new DecryptBackupFile(this.get(TYPES.EncryptionService)) + return new DecryptBackupFile(this.get(TYPES.EncryptionService), this.get(TYPES.Logger)) }) this.factory.set(TYPES.DiscardItemsLocally, () => { @@ -254,7 +254,7 @@ export class Dependencies { }) this.factory.set(TYPES.EditContact, () => { - return new EditContact(this.get(TYPES.MutatorService), this.get(TYPES.SyncService)) + return new EditContact(this.get(TYPES.MutatorService)) }) this.factory.set(TYPES.GetAllContacts, () => { @@ -268,7 +268,6 @@ export class Dependencies { this.factory.set(TYPES.CreateOrEditContact, () => { return new CreateOrEditContact( this.get(TYPES.MutatorService), - this.get(TYPES.SyncService), this.get(TYPES.FindContact), this.get(TYPES.EditContact), ) @@ -364,6 +363,7 @@ export class Dependencies { this.factory.set(TYPES.ResendAllMessages, () => { return new ResendAllMessages( this.get(TYPES.ResendMessage), + this.get(TYPES.DecryptOwnMessage), this.get(TYPES.AsymmetricMessageServer), this.get(TYPES.FindContact), ) @@ -380,7 +380,17 @@ export class Dependencies { }) this.factory.set(TYPES.HandleKeyPairChange, () => { - return new HandleKeyPairChange(this.get(TYPES.ReuploadAllInvites), this.get(TYPES.ResendAllMessages)) + return new HandleKeyPairChange( + this.get(TYPES.SelfContactManager), + this.get(TYPES.SharedVaultInvitesServer), + this.get(TYPES.AsymmetricMessageServer), + this.get(TYPES.ReuploadAllInvites), + this.get(TYPES.ResendAllMessages), + this.get(TYPES.GetAllContacts), + this.get(TYPES.SendOwnContactChangeMessage), + this.get(TYPES.CreateOrEditContact), + this.get(TYPES.Logger), + ) }) this.factory.set(TYPES.NotifyVaultUsersOfKeyRotation, () => { @@ -515,11 +525,7 @@ export class Dependencies { }) this.factory.set(TYPES.ResendMessage, () => { - return new ResendMessage( - this.get(TYPES.DecryptOwnMessage), - this.get(TYPES.SendMessage), - this.get(TYPES.EncryptMessage), - ) + return new ResendMessage(this.get(TYPES.SendMessage), this.get(TYPES.EncryptMessage)) }) this.factory.set(TYPES.SendMessage, () => { @@ -613,6 +619,10 @@ export class Dependencies { } private registerServiceMakers() { + this.factory.set(TYPES.Logger, () => { + return new Logger(this.options.identifier) + }) + this.factory.set(TYPES.UserServer, () => { return new UserServer(this.get(TYPES.HttpService)) }) @@ -703,6 +713,7 @@ export class Dependencies { this.get(TYPES.EncryptionService), this.get(TYPES.MutatorService), this.get(TYPES.SessionManager), + this.get(TYPES.SyncService), this.get(TYPES.AsymmetricMessageServer), this.get(TYPES.CreateOrEditContact), this.get(TYPES.FindContact), @@ -774,7 +785,6 @@ export class Dependencies { this.get(TYPES.ItemManager), this.get(TYPES.SessionManager), this.get(TYPES.SingletonManager), - this.get(TYPES.CreateOrEditContact), ) }) @@ -793,7 +803,6 @@ export class Dependencies { this.get(TYPES.CreateOrEditContact), this.get(TYPES.EditContact), this.get(TYPES.ValidateItemSigner), - this.get(TYPES.SendOwnContactChangeMessage), this.get(TYPES.InternalEventBus), ) }) @@ -921,6 +930,7 @@ export class Dependencies { this.get(TYPES.LegacyApiService), this.get(TYPES.LegacyApiService), this.get(TYPES.PayloadManager), + this.get(TYPES.Logger), this.get(TYPES.InternalEventBus), ) }) @@ -936,6 +946,7 @@ export class Dependencies { this.get(TYPES.AlertService), this.get(TYPES.Crypto), this.get(TYPES.InternalEventBus), + this.get(TYPES.Logger), this.get(TYPES.FilesBackupService), ) }) @@ -1014,6 +1025,7 @@ export class Dependencies { this.options.environment, this.options.platform, this.get(TYPES.DeviceInterface), + this.get(TYPES.Logger), this.get(TYPES.InternalEventBus), ) }) @@ -1032,6 +1044,7 @@ export class Dependencies { this.get(TYPES.AlertService), this.get(TYPES.SessionManager), this.get(TYPES.Crypto), + this.get(TYPES.Logger), this.get(TYPES.InternalEventBus), ) }) @@ -1120,6 +1133,7 @@ export class Dependencies { loadBatchSize: this.options.loadBatchSize, sleepBetweenBatches: this.options.sleepBetweenBatches, }, + this.get(TYPES.Logger), this.get(TYPES.InternalEventBus), ) }) @@ -1198,7 +1212,7 @@ export class Dependencies { }) this.factory.set(TYPES.PayloadManager, () => { - return new PayloadManager(this.get(TYPES.InternalEventBus)) + return new PayloadManager(this.get(TYPES.Logger), this.get(TYPES.InternalEventBus)) }) this.factory.set(TYPES.ItemManager, () => { @@ -1222,8 +1236,8 @@ export class Dependencies { ) }) - this.factory.set(TYPES.UserEventService, () => { - return new UserEventService(this.get(TYPES.InternalEventBus)) + this.factory.set(TYPES.NotificationService, () => { + return new NotificationService(this.get(TYPES.InternalEventBus)) }) this.factory.set(TYPES.InMemoryStore, () => { @@ -1278,7 +1292,7 @@ export class Dependencies { }) this.factory.set(TYPES.HttpService, () => { - return new HttpService(this.options.environment, this.options.appVersion, SnjsVersion) + return new HttpService(this.options.environment, this.options.appVersion, SnjsVersion, this.get(TYPES.Logger)) }) this.factory.set(TYPES.LegacyApiService, () => { diff --git a/packages/snjs/lib/Application/Dependencies/Types.ts b/packages/snjs/lib/Application/Dependencies/Types.ts index 49c192763..fd8b8e950 100644 --- a/packages/snjs/lib/Application/Dependencies/Types.ts +++ b/packages/snjs/lib/Application/Dependencies/Types.ts @@ -10,7 +10,7 @@ export const TYPES = { ItemManager: Symbol.for('ItemManager'), MutatorService: Symbol.for('MutatorService'), DiskStorageService: Symbol.for('DiskStorageService'), - UserEventService: Symbol.for('UserEventService'), + NotificationService: Symbol.for('NotificationService'), InMemoryStore: Symbol.for('InMemoryStore'), KeySystemKeyManager: Symbol.for('KeySystemKeyManager'), EncryptionService: Symbol.for('EncryptionService'), @@ -63,6 +63,7 @@ export const TYPES = { VaultInviteService: Symbol.for('VaultInviteService'), VaultUserCache: Symbol.for('VaultUserCache'), VaultLockService: Symbol.for('VaultLockService'), + Logger: Symbol.for('Logger'), // Servers RevisionServer: Symbol.for('RevisionServer'), diff --git a/packages/snjs/lib/Logging.ts b/packages/snjs/lib/Logging.ts deleted file mode 100644 index 0d01fd914..000000000 --- a/packages/snjs/lib/Logging.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { log as utilsLog } from '@standardnotes/utils' - -export const isDev = true - -export enum LoggingDomain { - DatabaseLoad, - Sync, - AccountMigration, -} - -const LoggingStatus: Record = { - [LoggingDomain.DatabaseLoad]: false, - [LoggingDomain.Sync]: false, - [LoggingDomain.AccountMigration]: true, -} - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export function log(domain: LoggingDomain, ...args: any[]): void { - if (!isDev || !LoggingStatus[domain]) { - return - } - - utilsLog(LoggingDomain[domain], ...args) -} diff --git a/packages/snjs/lib/Services/Api/Paths.ts b/packages/snjs/lib/Services/Api/Paths.ts index 80c00a4db..7a78f916b 100644 --- a/packages/snjs/lib/Services/Api/Paths.ts +++ b/packages/snjs/lib/Services/Api/Paths.ts @@ -51,10 +51,6 @@ const SubscriptionPaths = { subscriptionTokens: '/v1/subscription-tokens', } -const SubscriptionPathsV2 = { - subscriptions: '/v2/subscriptions', -} - const UserPathsV2 = { keyParams: '/v2/login-params', signIn: '/v2/login', @@ -75,7 +71,6 @@ export const Paths = { ...UserPaths, }, v2: { - ...SubscriptionPathsV2, ...UserPathsV2, }, } diff --git a/packages/snjs/lib/Services/ComponentManager/ComponentManager.spec.ts b/packages/snjs/lib/Services/ComponentManager/ComponentManager.spec.ts index 5f71b07b1..22eba0b60 100644 --- a/packages/snjs/lib/Services/ComponentManager/ComponentManager.spec.ts +++ b/packages/snjs/lib/Services/ComponentManager/ComponentManager.spec.ts @@ -13,6 +13,7 @@ import { ItemManager } from '@Lib/Services/Items/ItemManager' import { FeaturesService } from '@Lib/Services/Features/FeaturesService' import { SNComponentManager } from './ComponentManager' import { SyncService } from '../Sync/SyncService' +import { LoggerInterface } from '@standardnotes/utils' describe('featuresService', () => { let items: ItemManagerInterface @@ -23,6 +24,7 @@ describe('featuresService', () => { let prefs: PreferenceServiceInterface let eventBus: InternalEventBusInterface let device: DeviceInterface + let logger: LoggerInterface const createManager = (environment: Environment, platform: Platform) => { const manager = new SNComponentManager( @@ -35,6 +37,7 @@ describe('featuresService', () => { environment, platform, device, + logger, eventBus, ) @@ -46,6 +49,8 @@ describe('featuresService', () => { addEventListener: jest.fn(), attachEvent: jest.fn(), } as unknown as Window & typeof globalThis + logger = {} as jest.Mocked + logger.info = jest.fn() sync = {} as jest.Mocked sync.sync = jest.fn() diff --git a/packages/snjs/lib/Services/ComponentManager/ComponentManager.ts b/packages/snjs/lib/Services/ComponentManager/ComponentManager.ts index a9583932a..88e762af4 100644 --- a/packages/snjs/lib/Services/ComponentManager/ComponentManager.ts +++ b/packages/snjs/lib/Services/ComponentManager/ComponentManager.ts @@ -29,7 +29,7 @@ import { GetNativeThemes, NativeFeatureIdentifier, } from '@standardnotes/features' -import { Copy, removeFromArray, sleep, isNotUndefined } from '@standardnotes/utils' +import { Copy, removeFromArray, sleep, isNotUndefined, LoggerInterface } from '@standardnotes/utils' import { ComponentViewer } from '@Lib/Services/ComponentManager/ComponentViewer' import { AbstractService, @@ -98,6 +98,7 @@ export class SNComponentManager private environment: Environment, private platform: Platform, private device: DeviceInterface, + private logger: LoggerInterface, protected override internalEventBus: InternalEventBusInterface, ) { super(internalEventBus) @@ -177,6 +178,7 @@ export class SNComponentManager alerts: this.alerts, preferences: this.preferences, features: this.features, + logger: this.logger, }, { url: this.urlForFeature(component) ?? '', @@ -312,7 +314,7 @@ export class SNComponentManager onWindowMessage = (event: MessageEvent): void => { const data = event.data as ComponentMessage if (data.sessionKey) { - this.log('Component manager received message', data) + this.logger.info('Component manager received message', data) this.componentViewerForSessionKey(data.sessionKey)?.handleMessage(data) } } @@ -369,7 +371,7 @@ export class SNComponentManager } public async toggleTheme(uiFeature: UIFeature): Promise { - this.log('Toggling theme', uiFeature.uniqueIdentifier) + this.logger.info('Toggling theme', uiFeature.uniqueIdentifier) if (this.isThemeActive(uiFeature)) { await this.removeActiveTheme(uiFeature) @@ -449,7 +451,7 @@ export class SNComponentManager } public async toggleComponent(component: ComponentInterface): Promise { - this.log('Toggling component', component.uuid) + this.logger.info('Toggling component', component.uuid) if (this.isComponentActive(component)) { await this.removeActiveComponent(component) diff --git a/packages/snjs/lib/Services/ComponentManager/ComponentViewer.ts b/packages/snjs/lib/Services/ComponentManager/ComponentViewer.ts index 63a6ce7f4..4e6fa5db5 100644 --- a/packages/snjs/lib/Services/ComponentManager/ComponentViewer.ts +++ b/packages/snjs/lib/Services/ComponentManager/ComponentViewer.ts @@ -65,13 +65,13 @@ import { extendArray, Copy, removeFromArray, - log, nonSecureRandomIdentifier, UuidGenerator, Uuids, sureSearchArray, isNotUndefined, uniqueArray, + LoggerInterface, } from '@standardnotes/utils' import { ContentType, Uuid } from '@standardnotes/domain-core' @@ -80,7 +80,6 @@ export class ComponentViewer implements ComponentViewerInterface { private streamContextItemOriginalMessage?: ComponentMessage private streamItemsOriginalMessage?: ComponentMessage private removeItemObserver: () => void - private loggingEnabled = false public identifier = nonSecureRandomIdentifier() private actionObservers: ActionObserver[] = [] @@ -102,6 +101,7 @@ export class ComponentViewer implements ComponentViewerInterface { alerts: AlertService preferences: PreferenceServiceInterface features: FeaturesService + logger: LoggerInterface }, private options: { item: ComponentViewerItem @@ -143,7 +143,7 @@ export class ComponentViewer implements ComponentViewerInterface { } }) - this.log('Constructor', this) + this.services.logger.info('Constructor', this) } public getComponentOrFeatureItem(): UIFeature { @@ -163,7 +163,7 @@ export class ComponentViewer implements ComponentViewerInterface { } public destroy(): void { - this.log('Destroying', this) + this.services.logger.info('Destroying', this) this.deinit() } @@ -347,7 +347,7 @@ export class ComponentViewer implements ComponentViewerInterface { this.componentUniqueIdentifier.value, requiredContextPermissions, () => { - this.log( + this.services.logger.info( 'Send context item in reply', 'component:', this.componentOrFeature, @@ -364,18 +364,12 @@ export class ComponentViewer implements ComponentViewerInterface { ) } - private log(message: string, ...args: unknown[]): void { - if (this.loggingEnabled) { - log('ComponentViewer', message, args) - } - } - private sendItemsInReply( items: (DecryptedItemInterface | DeletedItemInterface)[], message: ComponentMessage, source?: PayloadEmitSource, ): void { - this.log('Send items in reply', this.componentOrFeature, items, message) + this.services.logger.info('Send items in reply', this.componentOrFeature, items, message) const responseData: MessageReplyData = {} @@ -453,10 +447,14 @@ export class ComponentViewer implements ComponentViewerInterface { */ private sendMessage(message: ComponentMessage | MessageReply, essential = true): void { if (!this.window && message.action === ComponentAction.Reply) { - this.log('Component has been deallocated in between message send and reply', this.componentOrFeature, message) + this.services.logger.info( + 'Component has been deallocated in between message send and reply', + this.componentOrFeature, + message, + ) return } - this.log('Send message to component', this.componentOrFeature, 'message: ', message) + this.services.logger.info('Send message to component', this.componentOrFeature, 'message: ', message) if (!this.window) { if (essential) { @@ -518,7 +516,7 @@ export class ComponentViewer implements ComponentViewerInterface { throw Error('Attempting to override component viewer window. Create a new component viewer instead.') } - this.log('setWindow', 'component: ', this.componentOrFeature, 'window: ', window) + this.services.logger.info('setWindow', 'component: ', this.componentOrFeature, 'window: ', window) this.window = window this.sessionKey = UuidGenerator.GenerateUuid() @@ -537,7 +535,7 @@ export class ComponentViewer implements ComponentViewerInterface { }, }) - this.log('setWindow got new sessionKey', this.sessionKey) + this.services.logger.info('setWindow got new sessionKey', this.sessionKey) this.postActiveThemes() } @@ -557,9 +555,9 @@ export class ComponentViewer implements ComponentViewerInterface { } handleMessage(message: ComponentMessage): void { - this.log('Handle message', message, this) + this.services.logger.info('Handle message', message, this) if (!this.componentOrFeature) { - this.log('Component not defined for message, returning', message) + this.services.logger.info('Component not defined for message, returning', message) void this.services.alerts.alert( 'A component is trying to communicate with Standard Notes, ' + 'but there is an error establishing a bridge. Please restart the app and try again.', diff --git a/packages/snjs/lib/Services/Features/FeaturesService.spec.ts b/packages/snjs/lib/Services/Features/FeaturesService.spec.ts index 85ff6655e..e597e0d17 100644 --- a/packages/snjs/lib/Services/Features/FeaturesService.spec.ts +++ b/packages/snjs/lib/Services/Features/FeaturesService.spec.ts @@ -27,6 +27,7 @@ import { LegacyApiService, SessionManager } from '../Api' import { ItemManager } from '../Items' import { DiskStorageService } from '../Storage/DiskStorageService' import { SettingsClientInterface } from '../Settings/SettingsClientInterface' +import { LoggerInterface } from '@standardnotes/utils' describe('FeaturesService', () => { let storageService: StorageServiceInterface @@ -45,26 +46,12 @@ describe('FeaturesService', () => { let items: ItemInterface[] let internalEventBus: InternalEventBusInterface let featureService: FeaturesService - - const createService = () => { - return new FeaturesService( - storageService, - itemManager, - mutator, - subscriptions, - apiService, - webSocketsService, - settingsService, - userService, - syncService, - alertService, - sessionManager, - crypto, - internalEventBus, - ) - } + let logger: LoggerInterface beforeEach(() => { + logger = {} as jest.Mocked + logger.info = jest.fn() + roles = [RoleName.NAMES.CoreUser, RoleName.NAMES.PlusUser] items = [] as jest.Mocked @@ -133,6 +120,7 @@ describe('FeaturesService', () => { alertService, sessionManager, crypto, + logger, internalEventBus, ) }) @@ -199,6 +187,25 @@ describe('FeaturesService', () => { describe('loadUserRoles()', () => { it('retrieves user roles and features from storage', async () => { + const createService = () => { + return new FeaturesService( + storageService, + itemManager, + mutator, + subscriptions, + apiService, + webSocketsService, + settingsService, + userService, + syncService, + alertService, + sessionManager, + crypto, + logger, + internalEventBus, + ) + } + createService().initializeFromDisk() expect(storageService.getValue).toHaveBeenCalledWith(StorageKey.UserRoles, undefined, []) }) diff --git a/packages/snjs/lib/Services/Features/FeaturesService.ts b/packages/snjs/lib/Services/Features/FeaturesService.ts index 35f847c20..2cd1d1781 100644 --- a/packages/snjs/lib/Services/Features/FeaturesService.ts +++ b/packages/snjs/lib/Services/Features/FeaturesService.ts @@ -1,5 +1,5 @@ import { MigrateFeatureRepoToUserSettingUseCase } from './UseCase/MigrateFeatureRepoToUserSetting' -import { arraysEqual, removeFromArray, lastElement } from '@standardnotes/utils' +import { arraysEqual, removeFromArray, lastElement, LoggerInterface } from '@standardnotes/utils' import { ClientDisplayableError } from '@standardnotes/responses' import { RoleName, ContentType, Uuid } from '@standardnotes/domain-core' import { PROD_OFFLINE_FEATURES_URL } from '../../Hosts' @@ -81,6 +81,7 @@ export class FeaturesService private alerts: AlertService, private sessions: SessionsClientInterface, private crypto: PureCryptoInterface, + private logger: LoggerInterface, protected override internalEventBus: InternalEventBusInterface, ) { super(internalEventBus) @@ -146,7 +147,7 @@ export class FeaturesService switch (event.type) { case ApiServiceEvent.MetaReceived: { if (!this.sync) { - this.log('Handling events interrupted. Sync service is not yet initialized.', event) + this.logger.warn('Handling events interrupted. Sync service is not yet initialized.', event) return } diff --git a/packages/snjs/lib/Services/Items/ItemManager.spec.ts b/packages/snjs/lib/Services/Items/ItemManager.spec.ts index 4d15943a5..7e31da505 100644 --- a/packages/snjs/lib/Services/Items/ItemManager.spec.ts +++ b/packages/snjs/lib/Services/Items/ItemManager.spec.ts @@ -2,7 +2,7 @@ import { ContentType } from '@standardnotes/domain-core' import { AlertService, InternalEventBusInterface, ItemRelationshipDirection } from '@standardnotes/services' import { ItemManager } from './ItemManager' import { PayloadManager } from '../Payloads/PayloadManager' -import { UuidGenerator, assert } from '@standardnotes/utils' +import { LoggerInterface, UuidGenerator, assert } from '@standardnotes/utils' import * as Models from '@standardnotes/models' import { DecryptedPayload, @@ -48,14 +48,18 @@ describe('itemManager', () => { let payloadManager: PayloadManager let itemManager: ItemManager let internalEventBus: InternalEventBusInterface + let logger: LoggerInterface beforeEach(() => { setupRandomUuid() + logger = {} as jest.Mocked + logger.debug = jest.fn() + internalEventBus = {} as jest.Mocked internalEventBus.publish = jest.fn() - payloadManager = new PayloadManager(internalEventBus) + payloadManager = new PayloadManager(logger, internalEventBus) itemManager = new ItemManager(payloadManager, internalEventBus) mutator = new MutatorService(itemManager, payloadManager, {} as jest.Mocked, internalEventBus) diff --git a/packages/snjs/lib/Services/Mutator/MutatorService.spec.ts b/packages/snjs/lib/Services/Mutator/MutatorService.spec.ts index a93a51728..79f7b3950 100644 --- a/packages/snjs/lib/Services/Mutator/MutatorService.spec.ts +++ b/packages/snjs/lib/Services/Mutator/MutatorService.spec.ts @@ -11,7 +11,7 @@ import { import { ContentType } from '@standardnotes/domain-core' import { AlertService, InternalEventBusInterface } from '@standardnotes/services' import { MutatorService, PayloadManager, ItemManager } from '../' -import { UuidGenerator, sleep } from '@standardnotes/utils' +import { UuidGenerator, sleep, LoggerInterface } from '@standardnotes/utils' const setupRandomUuid = () => { UuidGenerator.SetGenerator(() => String(Math.random())) @@ -23,13 +23,17 @@ describe('mutator service', () => { let itemManager: ItemManager let internalEventBus: InternalEventBusInterface + let logger: LoggerInterface beforeEach(() => { setupRandomUuid() internalEventBus = {} as jest.Mocked internalEventBus.publish = jest.fn() - payloadManager = new PayloadManager(internalEventBus) + logger = {} as jest.Mocked + logger.debug = jest.fn() + + payloadManager = new PayloadManager(logger, internalEventBus) itemManager = new ItemManager(payloadManager, internalEventBus) const alerts = {} as jest.Mocked diff --git a/packages/snjs/lib/Services/Payloads/PayloadManager.spec.ts b/packages/snjs/lib/Services/Payloads/PayloadManager.spec.ts index f425c8436..ec3b5f4c2 100644 --- a/packages/snjs/lib/Services/Payloads/PayloadManager.spec.ts +++ b/packages/snjs/lib/Services/Payloads/PayloadManager.spec.ts @@ -8,16 +8,21 @@ import { import { PayloadManager } from './PayloadManager' import { InternalEventBusInterface } from '@standardnotes/services' import { ContentType } from '@standardnotes/domain-core' +import { LoggerInterface } from '@standardnotes/utils' describe('payload manager', () => { let payloadManager: PayloadManager let internalEventBus: InternalEventBusInterface + let logger: LoggerInterface beforeEach(() => { internalEventBus = {} as jest.Mocked internalEventBus.publish = jest.fn() - payloadManager = new PayloadManager(internalEventBus) + logger = {} as jest.Mocked + logger.debug = jest.fn() + + payloadManager = new PayloadManager(logger, internalEventBus) }) it('emitting a payload should emit as-is and not merge on top of existing payload', async () => { diff --git a/packages/snjs/lib/Services/Payloads/PayloadManager.ts b/packages/snjs/lib/Services/Payloads/PayloadManager.ts index 42cbbc1fe..6b69da3d4 100644 --- a/packages/snjs/lib/Services/Payloads/PayloadManager.ts +++ b/packages/snjs/lib/Services/Payloads/PayloadManager.ts @@ -1,6 +1,6 @@ import { ContentType } from '@standardnotes/domain-core' import { PayloadsChangeObserver, QueueElement, PayloadsChangeObserverCallback, EmitQueue } from './Types' -import { removeFromArray, Uuids } from '@standardnotes/utils' +import { LoggerInterface, removeFromArray, Uuids } from '@standardnotes/utils' import { DeltaFileImport, isDeletedPayload, @@ -42,7 +42,10 @@ export class PayloadManager extends AbstractService implements PayloadManagerInt public collection: PayloadCollection private emitQueue: EmitQueue = [] - constructor(protected override internalEventBus: InternalEventBusInterface) { + constructor( + private logger: LoggerInterface, + protected override internalEventBus: InternalEventBusInterface, + ) { super(internalEventBus) this.collection = new PayloadCollection() } @@ -183,7 +186,7 @@ export class PayloadManager extends AbstractService implements PayloadManagerInt continue } - this.log( + this.logger.debug( 'applying payload', apply.uuid, 'globalDirtyIndexAtLastSync', diff --git a/packages/snjs/lib/Services/Sync/Account/Response.ts b/packages/snjs/lib/Services/Sync/Account/Response.ts index 5a06d4663..dc9720cfd 100644 --- a/packages/snjs/lib/Services/Sync/Account/Response.ts +++ b/packages/snjs/lib/Services/Sync/Account/Response.ts @@ -7,7 +7,7 @@ import { HttpResponse, isErrorResponse, RawSyncResponse, - UserEventServerHash, + NotificationServerHash, AsymmetricMessageServerHash, getErrorFromErrorResponse, } from '@standardnotes/responses' @@ -29,7 +29,7 @@ export class ServerSyncResponse { readonly asymmetricMessages: AsymmetricMessageServerHash[] readonly vaults: SharedVaultServerHash[] readonly vaultInvites: SharedVaultInviteServerHash[] - readonly userEvents: UserEventServerHash[] + readonly userEvents: NotificationServerHash[] private readonly rawConflictObjects: ConflictParams[] diff --git a/packages/snjs/lib/Services/Sync/SyncService.ts b/packages/snjs/lib/Services/Sync/SyncService.ts index 9d8282c1b..55d433af7 100644 --- a/packages/snjs/lib/Services/Sync/SyncService.ts +++ b/packages/snjs/lib/Services/Sync/SyncService.ts @@ -1,7 +1,7 @@ import { ConflictParams, ConflictType } from '@standardnotes/responses' -import { log, LoggingDomain } from './../../Logging' import { AccountSyncOperation } from '@Lib/Services/Sync/Account/Operation' import { + LoggerInterface, Uuids, extendArray, isNotUndefined, @@ -80,7 +80,7 @@ import { isChunkFullEntry, SyncEventReceivedSharedVaultInvitesData, SyncEventReceivedRemoteSharedVaultsData, - SyncEventReceivedUserEventsData, + SyncEventReceivedNotificationsData, SyncEventReceivedAsymmetricMessagesData, SyncOpStatus, } from '@standardnotes/services' @@ -160,6 +160,7 @@ export class SyncService private device: DeviceInterface, private identifier: string, private readonly options: ApplicationSyncOptions, + private logger: LoggerInterface, protected override internalEventBus: InternalEventBusInterface, ) { super(internalEventBus) @@ -258,7 +259,7 @@ export class SyncService } public async loadDatabasePayloads(): Promise { - log(LoggingDomain.DatabaseLoad, 'Loading database payloads') + this.logger.debug('Loading database payloads') if (this.databaseLoaded) { throw 'Attempting to initialize already initialized local database.' @@ -353,7 +354,7 @@ export class SyncService currentPosition?: number, payloadCount?: number, ) { - log(LoggingDomain.DatabaseLoad, 'Processing batch at index', currentPosition, 'length', batch.length) + this.logger.debug('Processing batch at index', currentPosition, 'length', batch.length) const encrypted: EncryptedPayloadInterface[] = [] const nonencrypted: (DecryptedPayloadInterface | DeletedPayloadInterface)[] = [] @@ -419,7 +420,7 @@ export class SyncService } public async markAllItemsAsNeedingSyncAndPersist(): Promise { - log(LoggingDomain.Sync, 'Marking all items as needing sync') + this.logger.debug('Marking all items as needing sync') const items = this.itemManager.items const payloads = items.map((item) => { @@ -485,7 +486,7 @@ export class SyncService const promise = this.spawnQueue[0] removeFromIndex(this.spawnQueue, 0) - log(LoggingDomain.Sync, 'Syncing again from spawn queue') + this.logger.debug('Syncing again from spawn queue') return this.sync({ queueStrategy: SyncQueueStrategy.ForceSpawnNew, @@ -547,7 +548,7 @@ export class SyncService public async sync(options: Partial = {}): Promise { if (this.clientLocked) { - log(LoggingDomain.Sync, 'Sync locked by client') + this.logger.debug('Sync locked by client') return } @@ -613,8 +614,7 @@ export class SyncService if (shouldExecuteSync) { this.syncLock = true } else { - log( - LoggingDomain.Sync, + this.logger.debug( !canExecuteSync ? 'Another function call has begun preparing for sync.' : syncInProgress @@ -727,8 +727,7 @@ export class SyncService payloads: (DeletedPayloadInterface | DecryptedPayloadInterface)[], options: SyncOptions, ) { - log( - LoggingDomain.Sync, + this.logger.debug( 'Syncing offline user', 'source:', SyncSource[options.source], @@ -812,8 +811,7 @@ export class SyncService }, ) - log( - LoggingDomain.Sync, + this.logger.debug( 'Syncing online user', 'source', SyncSource[options.source], @@ -925,7 +923,7 @@ export class SyncService } private async handleOfflineResponse(response: OfflineSyncResponse) { - log(LoggingDomain.Sync, 'Offline Sync Response', response) + this.logger.debug('Offline Sync Response', response) const masterCollection = this.payloadManager.getMasterCollection() @@ -943,7 +941,7 @@ export class SyncService } private handleErrorServerResponse(response: ServerSyncResponse) { - log(LoggingDomain.Sync, 'Sync Error', response) + this.logger.debug('Sync Error', response) if (response.status === INVALID_SESSION_RESPONSE_STATUS) { void this.notifyEvent(SyncEvent.InvalidSession) @@ -968,7 +966,10 @@ export class SyncService const historyMap = this.historyService.getHistoryMapCopy() if (response.userEvents && response.userEvents.length > 0) { - await this.notifyEventSync(SyncEvent.ReceivedUserEvents, response.userEvents as SyncEventReceivedUserEventsData) + await this.notifyEventSync( + SyncEvent.ReceivedNotifications, + response.userEvents as SyncEventReceivedNotificationsData, + ) } if (response.asymmetricMessages && response.asymmetricMessages.length > 0) { @@ -1003,8 +1004,7 @@ export class SyncService historyMap, ) - log( - LoggingDomain.Sync, + this.logger.debug( 'Online Sync Response', 'Operator ID', operation.id, @@ -1263,7 +1263,7 @@ export class SyncService } private async syncAgainByHandlingRequestsWaitingInResolveQueue(options: SyncOptions) { - log(LoggingDomain.Sync, 'Syncing again from resolve queue') + this.logger.debug('Syncing again from resolve queue') const promise = this.sync({ source: SyncSource.ResolveQueue, checkIntegrity: options.checkIntegrity, diff --git a/packages/snjs/mocha/application.test.js b/packages/snjs/mocha/application.test.js index 88e3e18f3..a41a8837e 100644 --- a/packages/snjs/mocha/application.test.js +++ b/packages/snjs/mocha/application.test.js @@ -112,7 +112,7 @@ describe('application instances', () => { await app.lock() }) - describe.skip('signOut()', () => { + describe('signOut()', () => { let testNote1 let confirmAlert let deinit @@ -129,7 +129,7 @@ describe('application instances', () => { beforeEach(async () => { testSNApp = await Factory.createAndInitializeApplication('test-application') testNote1 = await Factory.createMappedNote(testSNApp, 'Note 1', 'This is a test note!', false) - confirmAlert = sinon.spy(testSNApp.alertService, 'confirm') + confirmAlert = sinon.spy(testSNApp.alerts, 'confirm') deinit = sinon.spy(testSNApp, 'deinit') }) @@ -164,7 +164,7 @@ describe('application instances', () => { it('cancels sign out if confirmation dialog is rejected', async () => { confirmAlert.restore() - confirmAlert = sinon.stub(testSNApp.alertService, 'confirm').callsFake((_message) => false) + confirmAlert = sinon.stub(testSNApp.alerts, 'confirm').callsFake((_message) => false) await testSNApp.mutator.setItemDirty(testNote1) await testSNApp.user.signOut() diff --git a/packages/snjs/mocha/auth.test.js b/packages/snjs/mocha/auth.test.js index f0c7c334a..c75bb0716 100644 --- a/packages/snjs/mocha/auth.test.js +++ b/packages/snjs/mocha/auth.test.js @@ -207,7 +207,6 @@ describe('basic auth', function () { await specContext.launch() await specContext.register() await specContext.signout() - await specContext.deinit() specContext = await Factory.createAppContextWithFakeCrypto(Math.random(), uppercase, password) @@ -217,6 +216,7 @@ describe('basic auth', function () { expect(response).to.be.ok expect(response.data.error).to.not.be.ok expect(await specContext.application.encryption.getRootKey()).to.be.ok + await specContext.deinit() }).timeout(20000) it('can sign into account regardless of whitespace', async function () { @@ -232,7 +232,6 @@ describe('basic auth', function () { await specContext.launch() await specContext.register() await specContext.signout() - await specContext.deinit() specContext = await Factory.createAppContextWithFakeCrypto(Math.random(), withspace, password) await specContext.launch() @@ -241,6 +240,7 @@ describe('basic auth', function () { expect(response).to.be.ok expect(response.data.error).to.not.be.ok expect(await specContext.application.encryption.getRootKey()).to.be.ok + await specContext.deinit() }).timeout(20000) it('fails login with wrong password', async function () { @@ -367,7 +367,7 @@ describe('basic auth', function () { it('successfully changes password', changePassword).timeout(40000) - it.skip('successfully changes password when passcode is set', async function () { + it('successfully changes password when passcode is set', async function () { const passcode = 'passcode' const promptValueReply = (prompts) => { const values = [] @@ -393,7 +393,7 @@ describe('basic auth', function () { context.application.submitValuesForChallenge(challenge, initialValues) }, }) - await context.application.setPasscode(passcode) + await context.application.addPasscode(passcode) await changePassword.bind(this)() }).timeout(20000) @@ -550,6 +550,8 @@ describe('basic auth', function () { expect(response.status).to.equal(401) expect(response.data.error.message).to.equal('Operation not allowed.') + + await secondContext.deinit() }) }) }) diff --git a/packages/snjs/mocha/backups.test.js b/packages/snjs/mocha/backups.test.js index 7b11bca52..ab9a5db87 100644 --- a/packages/snjs/mocha/backups.test.js +++ b/packages/snjs/mocha/backups.test.js @@ -81,7 +81,6 @@ describe('backups', function () { }) it('passcode + account backup file should have correct number of items', async function () { - this.timeout(10000) const passcode = 'passcode' await this.application.register(this.email, this.password) Factory.handlePasswordChallenges(this.application, this.password) @@ -101,7 +100,7 @@ describe('backups', function () { // Encrypted backup with authorization const authorizedEncryptedData = await this.application.createEncryptedBackupFileForAutomatedDesktopBackups() expect(authorizedEncryptedData.items.length).to.equal(BaseItemCounts.DefaultItemsWithAccount + 2) - }) + }).timeout(10000) it('backup file item should have correct fields', async function () { await Factory.createSyncedNote(this.application) diff --git a/packages/snjs/mocha/features.test.js b/packages/snjs/mocha/features.test.js index 3954d5868..fba97f477 100644 --- a/packages/snjs/mocha/features.test.js +++ b/packages/snjs/mocha/features.test.js @@ -133,11 +133,12 @@ describe('features', () => { await promise }) - it.skip('migrated ext repo should have property indicating it was migrated', async () => { + it('migrated ext repo should have property indicating it was migrated', async () => { sinon.stub(application.legacyApi, 'isThirdPartyHostUsed').callsFake(() => { return false }) - expect(await application.settings.getDoesSensitiveSettingExist(SettingName.ExtensionKey)).to.equal(false) + const setting = SettingName.create(SettingName.NAMES.ExtensionKey).getValue() + expect(await application.settings.getDoesSensitiveSettingExist(setting)).to.equal(false) const extensionKey = UuidGenerator.GenerateUuid().split('-').join('') const promise = new Promise((resolve) => { application.streamItems(ContentType.TYPES.ExtensionRepo, ({ changed }) => { diff --git a/packages/snjs/mocha/history.test.js b/packages/snjs/mocha/history.test.js index 715796074..08b515c74 100644 --- a/packages/snjs/mocha/history.test.js +++ b/packages/snjs/mocha/history.test.js @@ -404,7 +404,7 @@ describe('history manager', () => { expect(noteHistory.length).to.equal(expectedRevisions) expect(dupeHistory.length).to.equal(expectedRevisions + 1) - }) + }).timeout(Factory.ThirtySecondTimeout) it('can decrypt revisions for duplicate_of items', async function () { const note = await Factory.createSyncedNote(this.application) diff --git a/packages/snjs/mocha/keys.test.js b/packages/snjs/mocha/keys.test.js index 40a6d73bf..484e8b212 100644 --- a/packages/snjs/mocha/keys.test.js +++ b/packages/snjs/mocha/keys.test.js @@ -449,6 +449,7 @@ describe('keys', function () { }, }) expect(payload.items_key_id).to.equal(newDefaultItemsKey.uuid) + await Factory.safeDeinit(application) }) it('compares root keys', async function () { @@ -642,7 +643,7 @@ describe('keys', function () { await contextB.deinit() }) - describe('changing password on 003 client while signed into 004 client', function () { + describe('changing password on 003 account 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 @@ -859,5 +860,6 @@ describe('keys', function () { const notePayload = rawPayloads.find((p) => p.content_type === ContentType.TYPES.Note) expect(notePayload.items_key_id).to.equal(itemsKey.uuid) + await otherClient.deinit() }) }) diff --git a/packages/snjs/mocha/lib/AppContext.js b/packages/snjs/mocha/lib/AppContext.js index 14b64755c..f41f2bbe8 100644 --- a/packages/snjs/mocha/lib/AppContext.js +++ b/packages/snjs/mocha/lib/AppContext.js @@ -47,6 +47,30 @@ export class AppContext { this.host, this.crypto || new FakeWebCrypto(), ) + + this.application.dependencies.get(TYPES.Logger).setLevel('error') + + this.disableSubscriptionFetching() + } + + disableSubscriptionFetching() { + this.application.subscriptions.fetchAvailableSubscriptions = () => {} + } + + async launch({ awaitDatabaseLoad = true, receiveChallenge } = { awaitDatabaseLoad: true }) { + await this.application.prepareForLaunch({ + receiveChallenge: receiveChallenge || this.handleChallenge, + }) + + await this.application.launch(awaitDatabaseLoad) + + await this.awaitUserPrefsSingletonCreation() + + this.application.http.loggingEnabled = true + } + + async deinit() { + await Utils.safeDeinit(this.application) } get sessions() { @@ -253,10 +277,10 @@ export class AppContext { awaitNextSucessfulSync() { return new Promise((resolve) => { - const removeObserver = this.application.sync.addEventObserver((event) => { + const removeObserver = this.application.sync.addEventObserver((event, data) => { if (event === SyncEvent.SyncCompletedWithAllItemsUploadedAndDownloaded) { removeObserver() - resolve() + resolve(data) } }) }) @@ -294,6 +318,16 @@ export class AppContext { }) } + resolveWithSyncRetrievedPayloads() { + return new Promise((resolve) => { + this.application.sync.addEventObserver((event, data) => { + if (event === SyncEvent.PaginatedSyncRequestCompleted) { + resolve(data.retrievedPayloads) + } + }) + }) + } + resolveWithConflicts() { return new Promise((resolve) => { this.application.sync.addEventObserver((event, response) => { @@ -359,10 +393,16 @@ export class AppContext { } resolveWhenAsyncFunctionCompletes(object, functionName) { + if (!object[functionName]) { + throw new Error(`Object does not have function ${functionName}`) + } + + const originalFunction = object[functionName].bind(object) + return new Promise((resolve) => { sinon.stub(object, functionName).callsFake(async (params) => { object[functionName].restore() - const result = await object[functionName](params) + const result = await originalFunction(params) resolve() return result }) @@ -370,23 +410,26 @@ export class AppContext { } spyOnFunctionResult(object, functionName) { - return new Promise((resolve) => { - sinon.stub(object, functionName).callsFake(async (params) => { - object[functionName].restore() - const result = await object[functionName](params) - resolve(result) - return result - }) + const originalFunction = object[functionName].bind(object) + return new Promise((resolve, reject) => { + try { + sinon.stub(object, functionName).callsFake(async (params) => { + const result = await originalFunction(params) + object[functionName].restore() + setTimeout(() => { + resolve(result) + }, 0) + return result + }) + } catch (err) { + reject(err) + } }) } - resolveWhenAsymmetricMessageProcessingCompletes() { - return this.resolveWhenAsyncFunctionCompletes(this.asymmetric, 'handleRemoteReceivedAsymmetricMessages') - } - resolveWhenUserMessagesProcessingCompletes() { - const objectToSpy = this.application.dependencies.get(TYPES.UserEventService) - return this.resolveWhenAsyncFunctionCompletes(objectToSpy, 'handleReceivedUserEvents') + const objectToSpy = this.application.dependencies.get(TYPES.NotificationService) + return this.resolveWhenAsyncFunctionCompletes(objectToSpy, 'handleReceivedNotifications') } resolveWhenAllInboundAsymmetricMessagesAreDeleted() { @@ -466,18 +509,6 @@ export class AppContext { }) } - async launch({ awaitDatabaseLoad = true, receiveChallenge } = { awaitDatabaseLoad: true }) { - await this.application.prepareForLaunch({ - receiveChallenge: receiveChallenge || this.handleChallenge, - }) - await this.application.launch(awaitDatabaseLoad) - await this.awaitUserPrefsSingletonCreation() - } - - async deinit() { - await Utils.safeDeinit(this.application) - } - async sync(options) { await this.application.sync.sync(options || { awaitAll: true }) } @@ -628,6 +659,10 @@ export class AppContext { console.warn('Anticipating a console error with message:', message) } + awaitPromiseOrThrow(promise, maxWait = 2.0, reason = 'Awaiting promise timed out; No description provided') { + return Utils.awaitPromiseOrThrow(promise, maxWait, reason) + } + async activatePaidSubscriptionForUser(options = {}) { const dateInAnHour = new Date() dateInAnHour.setHours(dateInAnHour.getHours() + 1) @@ -655,17 +690,17 @@ export class AppContext { await Utils.sleep(2) } catch (error) { console.warn( - `Mock events service not available. You are probalby running a test suite for home server: ${error.message}`, + `Mock events service not available. You are probably running a test suite for home server: ${error.message}`, ) } try { await HomeServer.activatePremiumFeatures(this.email, options.subscriptionPlanName, options.expiresAt) - await Utils.sleep(1) + await Utils.sleep(1, 'Waiting for premium features to be activated') } catch (error) { console.warn( - `Home server not available. You are probalby running a test suite for self hosted setup: ${error.message}`, + `Home server not available. You are probably running a test suite for self hosted setup: ${error.message}`, ) } } diff --git a/packages/snjs/mocha/lib/Collaboration.js b/packages/snjs/mocha/lib/Collaboration.js index e96f853b6..0af922613 100644 --- a/packages/snjs/mocha/lib/Collaboration.js +++ b/packages/snjs/mocha/lib/Collaboration.js @@ -1,7 +1,8 @@ import * as Factory from './factory.js' +import * as Utils from './Utils.js' export const createContactContext = async () => { - const contactContext = await Factory.createAppContextWithRealCrypto() + const contactContext = await Factory.createVaultsContextWithRealCrypto() await contactContext.launch() await contactContext.register() @@ -15,6 +16,8 @@ export const createTrustedContactForUserOfContext = async ( contextAddingNewContact, contextImportingContactInfoFrom, ) => { + const syncPromisme = contextAddingNewContact.awaitNextSucessfulSync() + const contact = await contextAddingNewContact.contacts.createOrEditTrustedContact({ name: 'John Doe', publicKey: contextImportingContactInfoFrom.publicKey, @@ -22,6 +25,8 @@ export const createTrustedContactForUserOfContext = async ( contactUuid: contextImportingContactInfoFrom.userUuid, }) + await syncPromisme + return contact } @@ -36,7 +41,35 @@ export const acceptAllInvites = async (context) => { } } -export const createSharedVaultWithAcceptedInvite = async (context, permission = SharedVaultUserPermission.PERMISSIONS.Write) => { +const inviteContext = async (context, contactContext, sharedVault, contact, permission) => { + contactContext.lockSyncing() + + const inviteOrError = await context.vaultInvites.inviteContactToSharedVault(sharedVault, contact, permission) + if (inviteOrError.isFailed()) { + throw new Error(inviteOrError.getError()) + } + + const invite = inviteOrError.getValue() + + const promise = contactContext.resolveWhenAsyncFunctionCompletes(contactContext.vaultInvites, 'processInboundInvites') + + contactContext.unlockSyncing() + await contactContext.sync() + + await Utils.awaitPromiseOrThrow(promise, 2.0, '[inviteContext] processInboundInvites was not called in time') + + const inviteRecords = contactContext.vaultInvites.getCachedPendingInviteRecords() + if (inviteRecords.length === 0) { + throw new Error('Invite was not properly received') + } + + return invite +} + +export const createSharedVaultWithAcceptedInvite = async ( + context, + permission = SharedVaultUserPermission.PERMISSIONS.Write, +) => { const { sharedVault, contact, contactContext, deinitContactContext } = await createSharedVaultWithUnacceptedButTrustedInvite(context, permission) @@ -44,7 +77,7 @@ export const createSharedVaultWithAcceptedInvite = async (context, permission = await acceptAllInvites(contactContext) - await promise + await Utils.awaitPromiseOrThrow(promise, 2.0, 'Waiting for vault to sync') const contactVault = contactContext.vaults.getVault({ keySystemIdentifier: sharedVault.systemIdentifier }) @@ -61,7 +94,10 @@ export const createSharedVaultWithAcceptedInviteAndNote = async ( ) const note = await context.createSyncedNote('foo', 'bar') const updatedNote = await moveItemToVault(context, sharedVault, note) + + const promise = contactContext.awaitNextSucessfulSync() await contactContext.sync() + await Utils.awaitPromiseOrThrow(promise, 2.0, 'Waiting for contactContext to sync added note') return { sharedVault, note: updatedNote, contact, contactContext, deinitContactContext } } @@ -76,34 +112,20 @@ export const createSharedVaultWithUnacceptedButTrustedInvite = async ( const contact = await createTrustedContactForUserOfContext(context, contactContext) await createTrustedContactForUserOfContext(contactContext, context) - const inviteOrError = await context.vaultInvites.inviteContactToSharedVault(sharedVault, contact, permission) - if (inviteOrError.isFailed()) { - throw new Error(inviteOrError.getError()) - } - const invite = inviteOrError.getValue() - - await contactContext.sync() + const invite = await inviteContext(context, contactContext, sharedVault, contact, permission) return { sharedVault, contact, contactContext, deinitContactContext, invite } } export const createSharedVaultAndInviteContact = async ( - createInContext, - inviteContext, - inviteContact, + context, + contactContext, + contact, permission = SharedVaultUserPermission.PERMISSIONS.Write, ) => { - const sharedVault = await createSharedVault(createInContext) + const sharedVault = await createSharedVault(context) - await createInContext.vaultInvites.inviteContactToSharedVault(sharedVault, inviteContact, permission) - - const promise = inviteContext.awaitNextSyncSharedVaultFromScratchEvent() - - await inviteContext.sync() - - await acceptAllInvites(inviteContext) - - await promise + await inviteContext(context, contactContext, sharedVault, contact, permission) return { sharedVault } } @@ -118,20 +140,33 @@ export const createSharedVaultWithUnacceptedAndUntrustedInvite = async ( const contact = await createTrustedContactForUserOfContext(context, contactContext) const invite = (await context.vaultInvites.inviteContactToSharedVault(sharedVault, contact, permission)).getValue() + + const promise = contactContext.resolveWhenAsyncFunctionCompletes(contactContext.vaultInvites, 'processInboundInvites') + await contactContext.sync() + await Utils.awaitPromiseOrThrow( + promise, + 2.0, + '[createSharedVaultWithUnacceptedAndUntrustedInvite] Waiting to process invites', + ) + return { sharedVault, contact, contactContext, deinitContactContext, invite } } -export const inviteNewPartyToSharedVault = async (context, sharedVault, permission = SharedVaultUserPermission.PERMISSIONS.Write) => { +export const inviteNewPartyToSharedVault = async ( + context, + sharedVault, + permission = SharedVaultUserPermission.PERMISSIONS.Write, +) => { const { contactContext: thirdPartyContext, deinitContactContext: deinitThirdPartyContext } = await createContactContext() const thirdPartyContact = await createTrustedContactForUserOfContext(context, thirdPartyContext) - await createTrustedContactForUserOfContext(thirdPartyContext, context) - await context.vaultInvites.inviteContactToSharedVault(sharedVault, thirdPartyContact, permission) - await thirdPartyContext.sync() + await createTrustedContactForUserOfContext(thirdPartyContext, context) + + await inviteContext(context, thirdPartyContext, sharedVault, thirdPartyContact, permission) return { thirdPartyContext, thirdPartyContact, deinitThirdPartyContext } } diff --git a/packages/snjs/mocha/lib/Utils.js b/packages/snjs/mocha/lib/Utils.js index a39b01225..dbb35019b 100644 --- a/packages/snjs/mocha/lib/Utils.js +++ b/packages/snjs/mocha/lib/Utils.js @@ -11,15 +11,15 @@ export async function safeDeinit(application) { await application.storage.awaitPersist() /** Limit waiting to 1s */ - await Promise.race([sleep(1), application.sync?.awaitCurrentSyncs()]) + await Promise.race([sleep(1, 'Deinit'), application.sync?.awaitCurrentSyncs()]) await application.prepareForDeinit() application.deinit(DeinitMode.Soft, DeinitSource.SignOut) } -export async function sleep(seconds) { - console.warn(`Test sleeping for ${seconds}s`) +export async function sleep(seconds, reason) { + console.warn(`Test sleeping for ${seconds}s. Reason: ${reason}`) return new Promise((resolve, reject) => { setTimeout(function () { @@ -32,3 +32,22 @@ export function generateUuid() { const crypto = new FakeWebCrypto() return crypto.generateUUID() } + +export async function awaitPromiseOrThrow(promise, maxWait, reason) { + let timer = undefined + + // Create a promise that rejects in milliseconds + const timeout = new Promise((resolve, reject) => { + timer = setTimeout(() => { + clearTimeout(timer) + console.error(reason) + reject(new Error(reason || `Promise timed out after ${maxWait} milliseconds: ${reason}`)) + }, maxWait * 1000) + }) + + // Returns a race between our timeout and the passed in promise + return Promise.race([promise, timeout]).then((result) => { + clearTimeout(timer) + return result + }) +} diff --git a/packages/snjs/mocha/lib/VaultsContext.js b/packages/snjs/mocha/lib/VaultsContext.js new file mode 100644 index 000000000..244074e20 --- /dev/null +++ b/packages/snjs/mocha/lib/VaultsContext.js @@ -0,0 +1,54 @@ +import { AppContext } from './AppContext.js' + +export class VaultsContext extends AppContext { + constructor(params) { + super(params) + } + + async changeVaultName(vault, nameAndDesc) { + const sendDataChangePromise = this.resolveWhenAsyncFunctionCompletes( + this.sharedVaults._sendVaultDataChangeMessage, + 'execute', + ) + + await this.vaults.changeVaultNameAndDescription(vault, { + name: nameAndDesc.name, + description: nameAndDesc.description, + }) + + await this.awaitPromiseOrThrow(sendDataChangePromise, undefined, 'Waiting for vault data change message to process') + } + + async changePassword(password) { + const promise = this.resolveWhenAsyncFunctionCompletes(this.sharedVaults._handleKeyPairChange, 'execute') + + await super.changePassword(password) + + await this.awaitPromiseOrThrow(promise, undefined, 'Waiting for keypair change message to process') + } + + async syncAndAwaitMessageProcessing() { + const promise = this.resolveWhenAsyncFunctionCompletes(this.asymmetric, 'handleRemoteReceivedAsymmetricMessages') + + await this.sync() + + await this.awaitPromiseOrThrow(promise, undefined, 'Waiting for messages to process') + } + + async syncAndAwaitInviteProcessing() { + const promise = this.resolveWhenAsyncFunctionCompletes(this.vaultInvites, 'processInboundInvites') + + await this.sync() + + await this.awaitPromiseOrThrow(promise, undefined, 'Waiting for invites to process') + } + + /** + * Run a request to keep refresh token from expiring due to long bouts of inactivity for contact context + * while main context changes password. Tests have a refresh token age of 10s typically, and changing password + * on CI environment may be time consuming. + */ + async runAnyRequestToPreventRefreshTokenFromExpiring() { + await this.asymmetric.getInboundMessages() + } +} diff --git a/packages/snjs/mocha/lib/factory.js b/packages/snjs/mocha/lib/factory.js index 7f3fa9bdc..b4a0a0582 100644 --- a/packages/snjs/mocha/lib/factory.js +++ b/packages/snjs/mocha/lib/factory.js @@ -2,6 +2,7 @@ /* eslint-disable no-undef */ import FakeWebCrypto from './fake_web_crypto.js' import { AppContext } from './AppContext.js' +import { VaultsContext } from './VaultsContext.js' import * as Applications from './Applications.js' import * as Defaults from './Defaults.js' import * as Utils from './Utils.js' @@ -57,6 +58,16 @@ export async function createAppContext({ identifier, crypto, email, password, ho return context } +export async function createVaultsContextWithRealCrypto(identifier) { + return createVaultsContext({ identifier, crypto: new SNWebCrypto() }) +} + +export async function createVaultsContext({ identifier, crypto, email, password, host } = {}) { + const context = new VaultsContext({ identifier, crypto, email, password, host }) + await context.initialize() + return context +} + export function disableIntegrityAutoHeal(application) { application.sync.emitOutOfSyncRemotePayloads = () => { console.warn('Integrity self-healing is disabled for this test') @@ -288,7 +299,7 @@ export function tomorrow() { } export async function sleep(seconds, reason) { - console.log('Sleeping for reason', reason) + console.log('[Factory] Sleeping for reason', reason) return Utils.sleep(seconds) } diff --git a/packages/snjs/mocha/model_tests/importing.test.js b/packages/snjs/mocha/model_tests/importing.test.js index 5c4f33acf..b6398ff54 100644 --- a/packages/snjs/mocha/model_tests/importing.test.js +++ b/packages/snjs/mocha/model_tests/importing.test.js @@ -843,8 +843,6 @@ describe('importing', function () { }) it('importing another accounts notes/tags should correctly keep relationships', async function () { - this.timeout(Factory.TwentySecondTimeout) - await setup({ fakeCrypto: true }) await Factory.registerUserToApplication({ @@ -881,5 +879,5 @@ describe('importing', function () { const importedTag = application.items.getDisplayableTags()[0] expect(application.items.referencesForItem(importedTag).length).to.equal(1) expect(application.items.itemsReferencingItem(importedNote).length).to.equal(1) - }) + }).timeout(Factory.TwentySecondTimeout) }) diff --git a/packages/snjs/mocha/note_display_criteria.test.js b/packages/snjs/mocha/note_display_criteria.test.js index 6b79f9489..d8df0fae5 100644 --- a/packages/snjs/mocha/note_display_criteria.test.js +++ b/packages/snjs/mocha/note_display_criteria.test.js @@ -4,7 +4,9 @@ const expect = chai.expect describe('note display criteria', function () { beforeEach(async function () { - this.payloadManager = new PayloadManager() + const logger = new Logger('test') + + this.payloadManager = new PayloadManager(logger) this.itemManager = new ItemManager(this.payloadManager) this.mutator = new MutatorService(this.itemManager, this.payloadManager) @@ -616,7 +618,10 @@ describe('note display criteria', function () { describe.skip('multiple tags', function () { it('normal note', async function () { await this.createNote() - + /** + * This test presently fails because the compound predicate created + * when using multiple views is an AND predicate instead of OR + */ expect( notesAndFilesMatchingOptions( { diff --git a/packages/snjs/mocha/payload_manager.test.js b/packages/snjs/mocha/payload_manager.test.js index f1943c8ce..95a03db49 100644 --- a/packages/snjs/mocha/payload_manager.test.js +++ b/packages/snjs/mocha/payload_manager.test.js @@ -6,7 +6,8 @@ const expect = chai.expect describe('payload manager', () => { beforeEach(async function () { - this.payloadManager = new PayloadManager() + const logger = new Logger('test') + this.payloadManager = new PayloadManager(logger) this.createNotePayload = async () => { return new DecryptedPayload({ uuid: Factory.generateUuidish(), diff --git a/packages/snjs/mocha/preferences.test.js b/packages/snjs/mocha/preferences.test.js index b298ee7a0..ac82ccce3 100644 --- a/packages/snjs/mocha/preferences.test.js +++ b/packages/snjs/mocha/preferences.test.js @@ -58,16 +58,15 @@ describe('preferences', function () { }) it('emits an event when preferences change', async function () { - let callTimes = 0 - this.application.addEventObserver(() => { - callTimes++ - }, ApplicationEvent.PreferencesChanged) - callTimes += 1 - await Factory.sleep(0) /** Await next tick */ - expect(callTimes).to.equal(1) /** App start */ - await register.call(this) + const promise = new Promise((resolve) => { + this.application.addEventObserver(() => { + resolve() + }, ApplicationEvent.PreferencesChanged) + }) + await this.application.setPreference('editorLeft', 300) - expect(callTimes).to.equal(2) + await promise + expect(promise).to.be.fulfilled }) it('discards existing preferences when signing in', async function () { diff --git a/packages/snjs/mocha/session.test.js b/packages/snjs/mocha/session.test.js index 4c61a965a..7a20d23d3 100644 --- a/packages/snjs/mocha/session.test.js +++ b/packages/snjs/mocha/session.test.js @@ -46,8 +46,6 @@ describe('server session', function () { } it('should succeed when a sync request is perfomed with an expired access token', async function () { - this.timeout(Factory.TwentySecondTimeout) - await Factory.registerUserToApplication({ application: this.application, email: this.email, @@ -59,7 +57,7 @@ describe('server session', function () { const response = await this.application.legacyApi.sync([]) expect(response.status).to.equal(200) - }) + }).timeout(Factory.TwentySecondTimeout) it('should return the new session in the response when refreshed', async function () { await Factory.registerUserToApplication({ @@ -78,8 +76,6 @@ describe('server session', function () { }) it('should be refreshed on any api call if access token is expired', async function () { - this.timeout(Factory.TwentySecondTimeout) - await Factory.registerUserToApplication({ application: this.application, email: this.email, @@ -103,11 +99,9 @@ describe('server session', function () { expect(sessionBeforeSync.accessToken.expiresAt).to.be.lessThan(sessionAfterSync.accessToken.expiresAt) // New token should expire in the future. expect(sessionAfterSync.accessToken.expiresAt).to.be.greaterThan(Date.now()) - }) + }).timeout(Factory.TwentySecondTimeout) it('should not deadlock while renewing session', async function () { - this.timeout(Factory.TwentySecondTimeout) - await Factory.registerUserToApplication({ application: this.application, email: this.email, @@ -125,7 +119,7 @@ describe('server session', function () { const sessionAfterSync = this.application.legacyApi.getSession() expect(sessionAfterSync.accessToken.expiresAt).to.be.greaterThan(Date.now()) - }) + }).timeout(Factory.TwentySecondTimeout) it('should succeed when a sync request is perfomed after signing into an ephemeral session', async function () { await Factory.registerUserToApplication({ @@ -199,8 +193,6 @@ describe('server session', function () { }) it('sign out request should be performed successfully and terminate session with expired access token', async function () { - this.timeout(Factory.TwentySecondTimeout) - await Factory.registerUserToApplication({ application: this.application, email: this.email, @@ -218,11 +210,9 @@ describe('server session', function () { expect(syncResponse.status).to.equal(401) expect(syncResponse.data.error.tag).to.equal('invalid-auth') expect(syncResponse.data.error.message).to.equal('Invalid login credentials.') - }) + }).timeout(Factory.TwentySecondTimeout) it('change email request should be successful with a valid access token', async function () { - this.timeout(Factory.TwentySecondTimeout) - let { application, password } = await Factory.createAndInitSimpleAppContext({ registerUser: true, }) @@ -241,11 +231,9 @@ describe('server session', function () { expect(loginResponse).to.be.ok expect(loginResponse.status).to.equal(200) await Factory.safeDeinit(application) - }) + }).timeout(Factory.TwentySecondTimeout) it('change email request should fail with an invalid access token', async function () { - this.timeout(Factory.TwentySecondTimeout) - let { application, password } = await Factory.createAndInitSimpleAppContext({ registerUser: true, }) @@ -266,14 +254,13 @@ describe('server session', function () { expect(changeEmailResponse.error.message).to.equal('Invalid login credentials.') await Factory.safeDeinit(application) - }) + }).timeout(Factory.TwentySecondTimeout) it('change email request should fail with an expired refresh token', async function () { - this.timeout(Factory.ThirtySecondTimeout) - - let { application, email, password } = await Factory.createAndInitSimpleAppContext({ + let { application, password } = await Factory.createAndInitSimpleAppContext({ registerUser: true, }) + application.sync.lockSyncing() /** Waiting for the refresh token to expire. */ await sleepUntilSessionExpires(application, false) @@ -285,11 +272,9 @@ describe('server session', function () { expect(changeEmailResponse.error.message).to.equal('Invalid login credentials.') await Factory.safeDeinit(application) - }) + }).timeout(Factory.ThirtySecondTimeout) it('change password request should be successful with a valid access token', async function () { - this.timeout(Factory.TwentySecondTimeout) - await Factory.registerUserToApplication({ application: this.application, email: this.email, @@ -309,11 +294,9 @@ describe('server session', function () { expect(loginResponse).to.be.ok expect(loginResponse.status).to.be.equal(200) - }) + }).timeout(Factory.TwentySecondTimeout) it('change password request should be successful after the expired access token is refreshed', async function () { - this.timeout(Factory.TwentySecondTimeout) - await Factory.registerUserToApplication({ application: this.application, email: this.email, @@ -336,7 +319,7 @@ describe('server session', function () { expect(loginResponse).to.be.ok expect(loginResponse.status).to.be.equal(200) - }) + }).timeout(Factory.TwentySecondTimeout) it('change password request should fail with an invalid access token', async function () { await Factory.registerUserToApplication({ @@ -360,14 +343,14 @@ describe('server session', function () { }) it('change password request should fail with an expired refresh token', async function () { - this.timeout(Factory.TwentySecondTimeout) - await Factory.registerUserToApplication({ application: this.application, email: this.email, password: this.password, }) + this.application.sync.lockSyncing() + /** Waiting for the refresh token to expire. */ await sleepUntilSessionExpires(this.application, false) @@ -399,14 +382,14 @@ describe('server session', function () { }) it('should fail when renewing a session with an expired refresh token', async function () { - this.timeout(Factory.TwentySecondTimeout) - await Factory.registerUserToApplication({ application: this.application, email: this.email, password: this.password, }) + this.application.sync.lockSyncing() + await sleepUntilSessionExpires(this.application, false) const refreshSessionResponse = await this.application.legacyApi.refreshSession() @@ -424,7 +407,7 @@ describe('server session', function () { expect(syncResponse.status).to.equal(401) expect(syncResponse.data.error.tag).to.equal('invalid-auth') expect(syncResponse.data.error.message).to.equal('Invalid login credentials.') - }) + }).timeout(Factory.TwentySecondTimeout) it('should fail when renewing a session with an invalid refresh token', async function () { await Factory.registerUserToApplication({ @@ -474,8 +457,6 @@ describe('server session', function () { }) it('notes should be synced as expected after refreshing a session', async function () { - this.timeout(Factory.TwentySecondTimeout) - await Factory.registerUserToApplication({ application: this.application, email: this.email, @@ -500,7 +481,7 @@ describe('server session', function () { const noteResult = await this.application.items.findItem(aNoteBeforeSync.uuid) expect(aNoteBeforeSync.isItemContentEqualWith(noteResult)).to.equal(true) } - }) + }).timeout(Factory.TwentySecondTimeout) it('changing password on one client should not invalidate other sessions', async function () { await Factory.registerUserToApplication({ @@ -635,8 +616,6 @@ describe('server session', function () { }) it('revoking a session should destroy local data', async function () { - this.timeout(Factory.TwentySecondTimeout) - Factory.handlePasswordChallenges(this.application, this.password) await Factory.registerUserToApplication({ application: this.application, @@ -662,11 +641,9 @@ describe('server session', function () { const deviceInterface = new WebDeviceInterface() const payloads = await deviceInterface.getAllDatabaseEntries(app2identifier) expect(payloads).to.be.empty - }) + }).timeout(Factory.TwentySecondTimeout) it('revoking other sessions should destroy their local data', async function () { - this.timeout(Factory.TwentySecondTimeout) - Factory.handlePasswordChallenges(this.application, this.password) await Factory.registerUserToApplication({ application: this.application, @@ -690,7 +667,7 @@ describe('server session', function () { const deviceInterface = new WebDeviceInterface() const payloads = await deviceInterface.getAllDatabaseEntries(app2identifier) expect(payloads).to.be.empty - }) + }).timeout(Factory.TwentySecondTimeout) it('signing out with invalid session token should still delete local data', async function () { await Factory.registerUserToApplication({ diff --git a/packages/snjs/mocha/settings.test.js b/packages/snjs/mocha/settings.test.js index ef2d27339..587295294 100644 --- a/packages/snjs/mocha/settings.test.js +++ b/packages/snjs/mocha/settings.test.js @@ -14,7 +14,6 @@ describe('settings service', function () { let application let context - let subscriptionId = 2001 beforeEach(async function () { localStorage.clear() diff --git a/packages/snjs/mocha/storage.test.js b/packages/snjs/mocha/storage.test.js index a15e6ad98..72edf1bbc 100644 --- a/packages/snjs/mocha/storage.test.js +++ b/packages/snjs/mocha/storage.test.js @@ -7,17 +7,19 @@ const expect = chai.expect describe('storage manager', function () { this.timeout(Factory.TenSecondTimeout) + /** * Items are saved in localStorage in tests. - * Base keys are `storage`, `snjs_version`, and `keychain` */ - const BASE_KEY_COUNT = 3 + const BASE_KEY_COUNT = ['storage', 'snjs_version', 'keychain'].length beforeEach(async function () { localStorage.clear() this.expectedKeyCount = BASE_KEY_COUNT + this.context = await Factory.createAppContext() await this.context.launch() + this.application = this.context.application this.email = UuidGenerator.GenerateUuid() this.password = UuidGenerator.GenerateUuid() @@ -62,18 +64,19 @@ describe('storage manager', function () { expect(keychainValue.serverPassword).to.not.be.ok }) - it.skip('regular session should persist data', async function () { + it('regular session should persist data', async function () { await Factory.registerUserToApplication({ application: this.application, email: this.email, password: this.password, ephemeral: false, }) + const key = 'foo' const value = 'bar' await this.application.storage.setValue(key, value) - /** Items are stored in local storage */ - expect(Object.keys(localStorage).length).to.equal(this.expectedKeyCount + BaseItemCounts.DefaultItems) + + expect(Object.keys(localStorage).length).to.equal(this.expectedKeyCount + BaseItemCounts.DefaultItemsWithAccount) const retrievedValue = await this.application.storage.getValue(key) expect(retrievedValue).to.equal(value) }) diff --git a/packages/snjs/mocha/sync_tests/conflicting.test.js b/packages/snjs/mocha/sync_tests/conflicting.test.js index a08451958..4e2e9de90 100644 --- a/packages/snjs/mocha/sync_tests/conflicting.test.js +++ b/packages/snjs/mocha/sync_tests/conflicting.test.js @@ -479,8 +479,7 @@ describe('online conflict handling', function () { await this.sharedFinalAssertions() }) - /** Temporarily skipping due to long run time */ - it.skip('handles stale data in bulk', async function () { + it('handles stale data in bulk', async function () { /** This number must be greater than the pagination limit per sync request. * For example if the limit per request is 150 items sent/received, this number should * be something like 160. */ @@ -728,43 +727,39 @@ describe('online conflict handling', function () { await this.sharedFinalAssertions() }) - /** Temporarily skipping due to long run time */ - it.skip( - 'registering for account with bulk offline data belonging to another account should be error-free', - async function () { - /** - * When performing a multi-page sync request where we are uploading data imported from a backup, - * if the first page of the sync request returns conflicted items keys, we rotate their UUID. - * The second page of sync waiting to be sent up is still encrypted with the old items key UUID. - * This causes a problem because when that second page is returned as conflicts, we will be looking - * for an items_key_id that no longer exists (has been rotated). Rather than modifying the entire - * sync paradigm to allow multi-page requests to consider side-effects of each page, we will instead - * take the approach of making sure the decryption function is liberal with regards to searching - * for the right items key. It will now consider (as a result of this test) an items key as being - * the correct key to decrypt an item if the itemskey.uuid == item.items_key_id OR if the itemsKey.duplicateOf - * value is equal to item.items_key_id. - */ + it('registering for account with bulk offline data belonging to another account should be error-free', async function () { + /** + * When performing a multi-page sync request where we are uploading data imported from a backup, + * if the first page of the sync request returns conflicted items keys, we rotate their UUID. + * The second page of sync waiting to be sent up is still encrypted with the old items key UUID. + * This causes a problem because when that second page is returned as conflicts, we will be looking + * for an items_key_id that no longer exists (has been rotated). Rather than modifying the entire + * sync paradigm to allow multi-page requests to consider side-effects of each page, we will instead + * take the approach of making sure the decryption function is liberal with regards to searching + * for the right items key. It will now consider (as a result of this test) an items key as being + * the correct key to decrypt an item if the itemskey.uuid == item.items_key_id OR if the itemsKey.duplicateOf + * value is equal to item.items_key_id. + */ - /** Create bulk data belonging to another account and sync */ - const largeItemCount = SyncUpDownLimit + 10 - await Factory.createManyMappedNotes(this.application, largeItemCount) - await this.application.sync.sync(syncOptions) - const priorData = this.application.items.items + /** Create bulk data belonging to another account and sync */ + const largeItemCount = SyncUpDownLimit + 10 + await Factory.createManyMappedNotes(this.application, largeItemCount) + await this.application.sync.sync(syncOptions) + const priorData = this.application.items.items - /** Register new account and import this same data */ - const newApp = await Factory.signOutApplicationAndReturnNew(this.application) - await Factory.registerUserToApplication({ - application: newApp, - email: Utils.generateUuid(), - password: Utils.generateUuid(), - }) - await newApp.mutator.emitItemsFromPayloads(priorData.map((i) => i.payload)) - await newApp.sync.markAllItemsAsNeedingSyncAndPersist() - await newApp.sync.sync(syncOptions) - expect(newApp.payloads.invalidPayloads.length).to.equal(0) - await Factory.safeDeinit(newApp) - }, - ).timeout(80000) + /** Register new account and import this same data */ + const newApp = await Factory.signOutApplicationAndReturnNew(this.application) + await Factory.registerUserToApplication({ + application: newApp, + email: Utils.generateUuid(), + password: Utils.generateUuid(), + }) + await newApp.mutator.emitItemsFromPayloads(priorData.map((i) => i.payload)) + await newApp.sync.markAllItemsAsNeedingSyncAndPersist() + await newApp.sync.sync(syncOptions) + expect(newApp.payloads.invalidPayloads.length).to.equal(0) + await Factory.safeDeinit(newApp) + }).timeout(80000) it('importing data belonging to another account should not result in duplication', async function () { /** Create primary account and export data */ diff --git a/packages/snjs/mocha/sync_tests/online.test.js b/packages/snjs/mocha/sync_tests/online.test.js index 16d44a51d..1c8731067 100644 --- a/packages/snjs/mocha/sync_tests/online.test.js +++ b/packages/snjs/mocha/sync_tests/online.test.js @@ -528,8 +528,7 @@ describe('online syncing', function () { await this.application.sync.sync(syncOptions) }) - /** Temporarily skipping due to long run time */ - it.skip('should handle uploading with sync pagination', async function () { + it('should handle uploading with sync pagination', async function () { const largeItemCount = SyncUpDownLimit + 10 for (let i = 0; i < largeItemCount; i++) { const note = await Factory.createMappedNote(this.application) @@ -541,10 +540,9 @@ describe('online syncing', function () { await this.application.sync.sync(syncOptions) const rawPayloads = await this.application.storage.getAllRawPayloads() expect(rawPayloads.length).to.equal(this.expectedItemCount) - }).timeout(15000) + }).timeout(Factory.TwentySecondTimeout) - /** Temporarily skipping due to long run time */ - it.skip('should handle downloading with sync pagination', async function () { + it('should handle downloading with sync pagination', async function () { const largeItemCount = SyncUpDownLimit + 10 for (let i = 0; i < largeItemCount; i++) { const note = await Factory.createMappedNote(this.application) diff --git a/packages/snjs/mocha/vaults/asymmetric-messages.test.js b/packages/snjs/mocha/vaults/asymmetric-messages.test.js index 125af668f..e8cebe33f 100644 --- a/packages/snjs/mocha/vaults/asymmetric-messages.test.js +++ b/packages/snjs/mocha/vaults/asymmetric-messages.test.js @@ -17,7 +17,7 @@ describe('asymmetric messages', function () { beforeEach(async function () { localStorage.clear() - context = await Factory.createAppContextWithRealCrypto() + context = await Factory.createVaultsContextWithRealCrypto() await context.launch() await context.register() @@ -29,7 +29,7 @@ describe('asymmetric messages', function () { contactContext.lockSyncing() - await context.vaults.changeVaultNameAndDescription(sharedVault, { + await context.changeVaultName(sharedVault, { name: 'new vault name', description: 'new vault description', }) @@ -38,11 +38,8 @@ describe('asymmetric messages', function () { get: () => 'invalid user uuid', }) - const completedProcessingMessagesPromise = contactContext.resolveWhenAsymmetricMessageProcessingCompletes() - contactContext.unlockSyncing() - await contactContext.sync() - await completedProcessingMessagesPromise + await contactContext.syncAndAwaitMessageProcessing() const updatedVault = contactContext.vaults.getVault({ keySystemIdentifier: sharedVault.systemIdentifier }) @@ -53,33 +50,22 @@ describe('asymmetric messages', function () { }) it('should delete message after processing it', async () => { - const { contactContext, deinitContactContext } = await Collaboration.createSharedVaultWithAcceptedInvite(context) + const { sharedVault, contactContext, deinitContactContext } = + await Collaboration.createSharedVaultWithAcceptedInvite(context) - const eventData = { - current: { - encryption: context.encryption.getKeyPair(), - signing: context.encryption.getSigningKeyPair(), - }, - previous: { - encryption: context.encryption.getKeyPair(), - signing: context.encryption.getSigningKeyPair(), - }, - } - - await context.contacts.sendOwnContactChangeEventToAllContacts(eventData) + await context.changeVaultName(sharedVault, { + name: 'New Name', + description: 'New Description', + }) const deleteFunction = sinon.spy(contactContext.asymmetric, 'deleteMessageAfterProcessing') - const promise = contactContext.resolveWhenAsymmetricMessageProcessingCompletes() - - await contactContext.sync() - - await promise + await contactContext.syncAndAwaitMessageProcessing() expect(deleteFunction.callCount).to.equal(1) const messages = await contactContext.asymmetric.getInboundMessages() - expect(messages.length).to.equal(0) + expect(messages.getValue().length).to.equal(0) await deinitContactContext() }) @@ -106,10 +92,7 @@ describe('asymmetric messages', function () { await sendContactSharePromise - const completedProcessingMessagesPromise = contactContext.resolveWhenAsymmetricMessageProcessingCompletes() - - await contactContext.sync() - await completedProcessingMessagesPromise + await contactContext.syncAndAwaitMessageProcessing() const updatedContact = contactContext.contacts.findContact(thirdPartyContext.userUuid) expect(updatedContact.name).to.equal('Changed 3rd Party Name') @@ -122,7 +105,10 @@ describe('asymmetric messages', function () { const { sharedVault, contactContext, deinitContactContext } = await Collaboration.createSharedVaultWithAcceptedInvite(context) - const handleInitialContactShareMessage = contactContext.resolveWhenAsymmetricMessageProcessingCompletes() + const handleInitialContactShareMessage = contactContext.resolveWhenAsyncFunctionCompletes( + contactContext.asymmetric, + 'handleRemoteReceivedAsymmetricMessages', + ) const { thirdPartyContext, deinitThirdPartyContext } = await Collaboration.inviteNewPartyToSharedVault( context, @@ -162,6 +148,7 @@ describe('asymmetric messages', function () { await Collaboration.acceptAllInvites(thirdPartyContext) await contactContext.sync() + contactContext.lockSyncing() const sendContactSharePromise = context.resolveWhenSharedVaultServiceSendsContactShareMessage() @@ -179,6 +166,7 @@ describe('asymmetric messages', function () { const thirdPartySpy = sinon.spy(thirdPartyContext.asymmetric, 'handleTrustedContactShareMessage') await context.sync() + contactContext.unlockSyncing() await contactContext.sync() await thirdPartyContext.sync() @@ -194,12 +182,26 @@ describe('asymmetric messages', function () { const { sharedVault, contactContext, deinitContactContext } = await Collaboration.createSharedVaultWithAcceptedInvite(context) + contactContext.anticipateConsoleError( + '(2x) Error decrypting contentKey from parameters', + 'Items keys are encrypted with new root key and are later decrypted in the test', + ) + + contactContext.lockSyncing() + + const promise = context.resolveWhenAsyncFunctionCompletes( + context.sharedVaults._notifyVaultUsersOfKeyRotation, + 'execute', + ) await context.vaults.rotateVaultRootKey(sharedVault) + await promise const firstPartySpy = sinon.spy(context.asymmetric, 'handleTrustedSharedVaultRootKeyChangedMessage') const secondPartySpy = sinon.spy(contactContext.asymmetric, 'handleTrustedSharedVaultRootKeyChangedMessage') await context.sync() + + contactContext.unlockSyncing() await contactContext.sync() expect(firstPartySpy.callCount).to.equal(0) @@ -212,7 +214,7 @@ describe('asymmetric messages', function () { const { sharedVault, contactContext, deinitContactContext } = await Collaboration.createSharedVaultWithAcceptedInvite(context) - await context.vaults.changeVaultNameAndDescription(sharedVault, { + await context.changeVaultName(sharedVault, { name: 'New Name', description: 'New Description', }) @@ -238,20 +240,13 @@ describe('asymmetric messages', function () { contactContext.lockSyncing() - const sendPromise = context.resolveWhenAsyncFunctionCompletes( - context.contacts, - 'sendOwnContactChangeEventToAllContacts', - ) await context.changePassword('new password') - await sendPromise const firstPartySpy = sinon.spy(context.asymmetric, 'handleTrustedSenderKeypairChangedMessage') const secondPartySpy = sinon.spy(contactContext.asymmetric, 'handleTrustedSenderKeypairChangedMessage') - const completedProcessingMessagesPromise = contactContext.resolveWhenAsymmetricMessageProcessingCompletes() contactContext.unlockSyncing() - await contactContext.sync() - await completedProcessingMessagesPromise + await contactContext.syncAndAwaitMessageProcessing() expect(firstPartySpy.callCount).to.equal(0) expect(secondPartySpy.callCount).to.equal(1) @@ -269,14 +264,12 @@ describe('asymmetric messages', function () { await context.changePassword('new password') - await context.vaults.changeVaultNameAndDescription(sharedVault, { + await context.changeVaultName(sharedVault, { name: 'New Name', description: 'New Description', }) - const completedProcessingMessagesPromise = contactContext.resolveWhenAsymmetricMessageProcessingCompletes() - await contactContext.sync() - await completedProcessingMessagesPromise + await contactContext.syncAndAwaitMessageProcessing() const updatedVault = contactContext.vaults.getVault({ keySystemIdentifier: sharedVault.systemIdentifier }) expect(updatedVault.name).to.equal('New Name') @@ -295,16 +288,14 @@ describe('asymmetric messages', function () { await context.changePassword('new password') - await context.vaults.changeVaultNameAndDescription(sharedVault, { + await context.changeVaultName(sharedVault, { name: 'New Name', description: 'New Description', }) context.lockSyncing() - const completedProcessingMessagesPromise = contactContext.resolveWhenAsymmetricMessageProcessingCompletes() - await contactContext.sync() - await completedProcessingMessagesPromise + await contactContext.syncAndAwaitMessageProcessing() /** * There's really no good way to await the exact call since @@ -313,12 +304,12 @@ describe('asymmetric messages', function () { await context.sleep(0.25) const messages = await context.asymmetric.getInboundMessages() - expect(messages.length).to.equal(0) + expect(messages.getValue().length).to.equal(0) await deinitContactContext() }) - it.skip('should process sender keypair changed message', async () => { + it('should process sender keypair changed message', async () => { const { contactContext, deinitContactContext } = await Collaboration.createContactContext() await Collaboration.createTrustedContactForUserOfContext(context, contactContext) await Collaboration.createTrustedContactForUserOfContext(contactContext, context) @@ -326,9 +317,7 @@ describe('asymmetric messages', function () { await context.changePassword('new_password') - const completedProcessingMessagesPromise = contactContext.resolveWhenAsymmetricMessageProcessingCompletes() - await contactContext.sync() - await completedProcessingMessagesPromise + await contactContext.syncAndAwaitMessageProcessing() const updatedContact = contactContext.contacts.findContact(context.userUuid) @@ -341,7 +330,7 @@ describe('asymmetric messages', function () { await deinitContactContext() }) - it.skip('sender keypair changed message should be signed using old key pair', async () => { + 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() @@ -352,9 +341,7 @@ describe('asymmetric messages', function () { const secondPartySpy = sinon.spy(contactContext.asymmetric, 'handleTrustedSenderKeypairChangedMessage') await context.sync() - const completedProcessingMessagesPromise = contactContext.resolveWhenAsymmetricMessageProcessingCompletes() - await contactContext.sync() - await completedProcessingMessagesPromise + await contactContext.syncAndAwaitMessageProcessing() const message = secondPartySpy.args[0][0] const encryptedMessage = message.encrypted_message @@ -376,9 +363,7 @@ describe('asymmetric messages', function () { const newKeyPair = context.encryption.getKeyPair() const newSigningKeyPair = context.encryption.getSigningKeyPair() - const completedProcessingMessagesPromise = contactContext.resolveWhenAsymmetricMessageProcessingCompletes() - await contactContext.sync() - await completedProcessingMessagesPromise + await contactContext.syncAndAwaitMessageProcessing() const updatedContact = contactContext.contacts.findContact(context.userUuid) expect(updatedContact.publicKeySet.encryption).to.equal(newKeyPair.publicKey) @@ -395,7 +380,7 @@ describe('asymmetric messages', function () { contactContext.lockSyncing() - await context.vaults.changeVaultNameAndDescription(sharedVault, { + await context.changeVaultName(sharedVault, { name: 'New Name', description: 'New Description', }) @@ -405,7 +390,7 @@ describe('asymmetric messages', function () { await promise const messages = await contactContext.asymmetric.getInboundMessages() - expect(messages.length).to.equal(0) + expect(messages.getValue().length).to.equal(0) const updatedVault = contactContext.vaults.getVault({ keySystemIdentifier: sharedVault.systemIdentifier }) expect(updatedVault.name).to.not.equal('New Name') @@ -413,4 +398,85 @@ describe('asymmetric messages', function () { await deinitContactContext() }) + + it('should be able to decrypt previously sent own messages', async () => { + const { sharedVault, contactContext, deinitContactContext } = + await Collaboration.createSharedVaultWithAcceptedInvite(context) + + contactContext.lockSyncing() + + await context.changeVaultName(sharedVault, { + name: 'New Name', + description: 'New Description', + }) + + const usecase = context.application.dependencies.get(TYPES.ResendAllMessages) + const result = await usecase.execute({ + keys: { + encryption: context.encryption.getKeyPair(), + signing: context.encryption.getSigningKeyPair(), + }, + previousKeys: { + encryption: context.encryption.getKeyPair(), + signing: context.encryption.getSigningKeyPair(), + }, + }) + + expect(result.isFailed()).to.be.false + + await deinitContactContext() + }) + + it('sending a new vault invite to a trusted contact then changing account password should still allow contact to trust invite', async () => { + const { contactContext, contact, deinitContactContext } = await Collaboration.createSharedVaultWithAcceptedInvite( + context, + ) + + contactContext.lockSyncing() + + const newVault = await Collaboration.createSharedVault(context) + + await context.vaultInvites.inviteContactToSharedVault( + newVault, + contact, + SharedVaultUserPermission.PERMISSIONS.Write, + ) + + await contactContext.runAnyRequestToPreventRefreshTokenFromExpiring() + + await context.changePassword('new password') + + await contactContext.runAnyRequestToPreventRefreshTokenFromExpiring() + + /** + * When resending keypair changed messages here, we expect that one of their previous messages will fail to decrypt. + * This is because the first contact keypair change message was encrypted using their keypair N (original), then after + * the second password change, the reference to "previous" key will be N + 1 instead of N, so there is no longer a reference + * to the original keypair. This is not a problem, and in fact even if the message were decryptable, it would be skipped + * because we do not want to re-send keypair changed messages. + */ + + await context.changePassword('new password 2') + + const messages = await contactContext.asymmetric.getInboundMessages() + if (messages.isFailed()) { + console.error(messages.getError()) + } + + expect(messages.isFailed()).to.be.false + expect(messages.getValue().length).to.equal(2) + + contactContext.unlockSyncing() + await contactContext.syncAndAwaitInviteProcessing() + + const invites = contactContext.vaultInvites.getCachedPendingInviteRecords() + expect(invites.length).to.equal(1) + + const invite = invites[0] + expect(invite.trusted).to.equal(true) + + await contactContext.vaultInvites.acceptInvite(invite) + + await deinitContactContext() + }).timeout(Factory.ThirtySecondTimeout) }) diff --git a/packages/snjs/mocha/vaults/conflicts.test.js b/packages/snjs/mocha/vaults/conflicts.test.js index 1fbe88113..ff45780cf 100644 --- a/packages/snjs/mocha/vaults/conflicts.test.js +++ b/packages/snjs/mocha/vaults/conflicts.test.js @@ -17,7 +17,7 @@ describe('shared vault conflicts', function () { beforeEach(async function () { localStorage.clear() - context = await Factory.createAppContextWithRealCrypto() + context = await Factory.createVaultsContextWithRealCrypto() await context.launch() await context.register() diff --git a/packages/snjs/mocha/vaults/contacts.test.js b/packages/snjs/mocha/vaults/contacts.test.js index 37e77e978..4361aef4d 100644 --- a/packages/snjs/mocha/vaults/contacts.test.js +++ b/packages/snjs/mocha/vaults/contacts.test.js @@ -17,7 +17,7 @@ describe('contacts', function () { beforeEach(async function () { localStorage.clear() - context = await Factory.createAppContextWithRealCrypto() + context = await Factory.createVaultsContextWithRealCrypto() await context.launch() await context.register() @@ -101,6 +101,5 @@ describe('contacts', function () { await deinitContactContext() }) - it.skip('should be able to refresh a contact using a collaborationID that includes full chain of previous public keys', async () => { - }) + it.skip('should be able to refresh a contact using a collaborationID that includes full chain of previous public keys', async () => {}) }) diff --git a/packages/snjs/mocha/vaults/crypto.test.js b/packages/snjs/mocha/vaults/crypto.test.js index 307714289..911222e86 100644 --- a/packages/snjs/mocha/vaults/crypto.test.js +++ b/packages/snjs/mocha/vaults/crypto.test.js @@ -17,7 +17,7 @@ describe('shared vault crypto', function () { beforeEach(async function () { localStorage.clear() - context = await Factory.createAppContextWithRealCrypto() + context = await Factory.createVaultsContextWithRealCrypto() await context.launch() await context.register() @@ -28,11 +28,13 @@ describe('shared vault crypto', function () { const appIdentifier = context.identifier await context.deinit() - let recreatedContext = await Factory.createAppContextWithRealCrypto(appIdentifier) + let recreatedContext = await Factory.createVaultsContextWithRealCrypto(appIdentifier) await recreatedContext.launch() expect(recreatedContext.encryption.getKeyPair()).to.not.be.undefined expect(recreatedContext.encryption.getSigningKeyPair()).to.not.be.undefined + + await recreatedContext.deinit() }) it('changing user password should re-encrypt all key system root keys and contacts with new user root key', async () => { @@ -90,11 +92,13 @@ describe('shared vault crypto', function () { await deinitContactContext() }) - it.skip('encrypting an item into storage then loading it should verify authenticity of original content rather than most recent symmetric signature', async () => { + 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') + const contactNote = contactContext.items.findItem(note.uuid) + + await contactContext.changeNoteTitleAndSync(contactNote, 'new title') /** Override decrypt result to return failing signature */ const objectToSpy = context.encryption @@ -103,6 +107,7 @@ describe('shared vault crypto', function () { const decryptedPayloads = await objectToSpy.decryptSplit(split) expect(decryptedPayloads.length).to.equal(1) + expect(decryptedPayloads[0].content_type).to.equal(ContentType.TYPES.Note) const payload = decryptedPayloads[0] const mutatedPayload = new DecryptedPayload({ @@ -118,6 +123,7 @@ describe('shared vault crypto', function () { return [mutatedPayload] }) + await context.sync() let updatedNote = context.items.findItem(note.uuid) @@ -127,7 +133,7 @@ describe('shared vault crypto', function () { const appIdentifier = context.identifier await context.deinit() - let recreatedContext = await Factory.createAppContextWithRealCrypto(appIdentifier) + let recreatedContext = await Factory.createVaultsContextWithRealCrypto(appIdentifier) await recreatedContext.launch() updatedNote = recreatedContext.items.findItem(note.uuid) @@ -140,7 +146,7 @@ describe('shared vault crypto', function () { await recreatedContext.deinit() - recreatedContext = await Factory.createAppContextWithRealCrypto(appIdentifier) + recreatedContext = await Factory.createVaultsContextWithRealCrypto(appIdentifier) await recreatedContext.launch() /** Decrypting from storage will now verify current user symmetric signature only */ diff --git a/packages/snjs/mocha/vaults/deletion.test.js b/packages/snjs/mocha/vaults/deletion.test.js index 556e83fb2..896f8abeb 100644 --- a/packages/snjs/mocha/vaults/deletion.test.js +++ b/packages/snjs/mocha/vaults/deletion.test.js @@ -18,7 +18,7 @@ describe('shared vault deletion', function () { beforeEach(async function () { localStorage.clear() - context = await Factory.createAppContextWithRealCrypto() + context = await Factory.createVaultsContextWithRealCrypto() await context.launch() await context.register() diff --git a/packages/snjs/mocha/vaults/files.test.js b/packages/snjs/mocha/vaults/files.test.js index cc28be737..ab5264859 100644 --- a/packages/snjs/mocha/vaults/files.test.js +++ b/packages/snjs/mocha/vaults/files.test.js @@ -5,7 +5,7 @@ import * as Collaboration from '../lib/Collaboration.js' chai.use(chaiAsPromised) const expect = chai.expect -describe.skip('shared vault files', function () { +describe('shared vault files', function () { this.timeout(Factory.TwentySecondTimeout) let context @@ -19,7 +19,7 @@ describe.skip('shared vault files', function () { beforeEach(async function () { localStorage.clear() - context = await Factory.createAppContextWithRealCrypto() + context = await Factory.createVaultsContextWithRealCrypto() await context.launch() await context.register() @@ -29,7 +29,7 @@ describe.skip('shared vault files', function () { }) describe('private vaults', () => { - it('should be able to upload and download file to vault as owner', async () => { + it('should be able to upload and download file to private vault as owner', async () => { const vault = await Collaboration.createPrivateVault(context) const response = await fetch('/mocha/assets/small_file.md') const buffer = new Uint8Array(await response.arrayBuffer()) @@ -45,7 +45,7 @@ describe.skip('shared vault files', function () { }) }) - it('should be able to upload and download file to vault as owner', async () => { + it('should be able to upload and download file to shared vault as owner', async () => { const sharedVault = await Collaboration.createSharedVault(context) const response = await fetch('/mocha/assets/small_file.md') const buffer = new Uint8Array(await response.arrayBuffer()) diff --git a/packages/snjs/mocha/vaults/importing.test.js b/packages/snjs/mocha/vaults/importing.test.js index 5b391aa63..45a1c7d43 100644 --- a/packages/snjs/mocha/vaults/importing.test.js +++ b/packages/snjs/mocha/vaults/importing.test.js @@ -17,7 +17,7 @@ describe.skip('vault importing', function () { beforeEach(async function () { localStorage.clear() - context = await Factory.createAppContextWithRealCrypto() + context = await Factory.createVaultsContextWithRealCrypto() await context.launch() await context.register() @@ -39,7 +39,7 @@ describe.skip('vault importing', function () { const backupData = await context.application.createEncryptedBackupFileForAutomatedDesktopBackups() - const otherContext = await Factory.createAppContextWithRealCrypto() + const otherContext = await Factory.createVaultsContextWithRealCrypto() await otherContext.launch() await otherContext.application.importData(backupData) diff --git a/packages/snjs/mocha/vaults/invites.test.js b/packages/snjs/mocha/vaults/invites.test.js index f8849d0a6..b59962ff7 100644 --- a/packages/snjs/mocha/vaults/invites.test.js +++ b/packages/snjs/mocha/vaults/invites.test.js @@ -17,7 +17,7 @@ describe('shared vault invites', function () { beforeEach(async function () { localStorage.clear() - context = await Factory.createAppContextWithRealCrypto() + context = await Factory.createVaultsContextWithRealCrypto() await context.launch() await context.register() }) diff --git a/packages/snjs/mocha/vaults/items.test.js b/packages/snjs/mocha/vaults/items.test.js index 225d1762c..c23e39bdf 100644 --- a/packages/snjs/mocha/vaults/items.test.js +++ b/packages/snjs/mocha/vaults/items.test.js @@ -17,7 +17,7 @@ describe('shared vault items', function () { beforeEach(async function () { localStorage.clear() - context = await Factory.createAppContextWithRealCrypto() + context = await Factory.createVaultsContextWithRealCrypto() await context.launch() await context.register() @@ -38,13 +38,19 @@ describe('shared vault items', function () { it('should add item to shared vault with contact', async () => { const note = await context.createSyncedNote('foo', 'bar') - const { sharedVault, deinitContactContext } = await Collaboration.createSharedVaultWithAcceptedInvite(context) + const { sharedVault, contactContext, deinitContactContext } = + await Collaboration.createSharedVaultWithAcceptedInvite(context) await Collaboration.moveItemToVault(context, sharedVault, note) + await contactContext.sync() + const updatedNote = context.items.findItem(note.uuid) expect(updatedNote.key_system_identifier).to.equal(sharedVault.systemIdentifier) + const contactNote = contactContext.items.findItem(note.uuid) + expect(contactNote.key_system_identifier).to.equal(sharedVault.systemIdentifier) + await deinitContactContext() }) @@ -117,7 +123,7 @@ describe('shared vault items', function () { }) it('adding item to vault while belonging to other vault should move the item to new vault', async () => { - const { note, sharedVault, contactContext, contact, deinitContactContext } = + const { note, contactContext, contact, deinitContactContext } = await Collaboration.createSharedVaultWithAcceptedInviteAndNote(context) const { sharedVault: otherSharedVault } = await Collaboration.createSharedVaultAndInviteContact( @@ -126,6 +132,8 @@ describe('shared vault items', function () { contact, ) + await Collaboration.acceptAllInvites(contactContext) + const updatedNote = await Collaboration.moveItemToVault(context, otherSharedVault, note) expect(updatedNote.key_system_identifier).to.equal(otherSharedVault.systemIdentifier) @@ -134,7 +142,6 @@ describe('shared vault items', function () { await contactContext.sync() const receivedNote = contactContext.items.findItem(note.uuid) - expect(receivedNote).to.not.be.undefined expect(receivedNote.title).to.equal(note.title) expect(receivedNote.key_system_identifier).to.equal(otherSharedVault.systemIdentifier) expect(receivedNote.shared_vault_uuid).to.equal(otherSharedVault.sharing.sharedVaultUuid) diff --git a/packages/snjs/mocha/vaults/key-management.test.js b/packages/snjs/mocha/vaults/key-management.test.js index 049aab4c1..ead6cfebc 100644 --- a/packages/snjs/mocha/vaults/key-management.test.js +++ b/packages/snjs/mocha/vaults/key-management.test.js @@ -16,7 +16,7 @@ describe('vault key management', function () { beforeEach(async function () { localStorage.clear() - context = await Factory.createAppContextWithRealCrypto() + context = await Factory.createVaultsContextWithRealCrypto() await context.launch() }) diff --git a/packages/snjs/mocha/vaults/key-rotation.test.js b/packages/snjs/mocha/vaults/key-rotation.test.js index b8734be2f..48c540d89 100644 --- a/packages/snjs/mocha/vaults/key-rotation.test.js +++ b/packages/snjs/mocha/vaults/key-rotation.test.js @@ -17,7 +17,7 @@ describe('vault key rotation', function () { beforeEach(async function () { localStorage.clear() - context = await Factory.createAppContextWithRealCrypto() + context = await Factory.createVaultsContextWithRealCrypto() await context.launch() await context.register() @@ -99,7 +99,7 @@ describe('vault key rotation', function () { await context.vaults.rotateVaultRootKey(sharedVault) await promise - const outboundMessages = await context.asymmetric.getOutboundMessages() + const outboundMessages = (await context.asymmetric.getOutboundMessages()).getValue() const expectedMessages = ['root key change', 'vault metadata change'] expect(outboundMessages.length).to.equal(expectedMessages.length) @@ -154,10 +154,8 @@ describe('vault key rotation', function () { await context.vaults.rotateVaultRootKey(sharedVault) await promise - const contactPromise = contactContext.resolveWhenAsymmetricMessageProcessingCompletes() contactContext.unlockSyncing() - await contactContext.sync() - await contactPromise + await contactContext.syncAndAwaitMessageProcessing() const newPrimaryItemsKey = contactContext.keys.getPrimaryKeySystemItemsKey(sharedVault.systemIdentifier) expect(newPrimaryItemsKey).to.not.be.undefined @@ -217,7 +215,7 @@ describe('vault key rotation', function () { await context.vaults.rotateVaultRootKey(sharedVault) await firstPromise - const asymmetricMessageAfterFirstChange = await context.asymmetric.getOutboundMessages() + const asymmetricMessageAfterFirstChange = (await context.asymmetric.getOutboundMessages()).getValue() const expectedMessages = ['root key change', 'vault metadata change'] expect(asymmetricMessageAfterFirstChange.length).to.equal(expectedMessages.length) @@ -227,7 +225,7 @@ describe('vault key rotation', function () { await context.vaults.rotateVaultRootKey(sharedVault) await secondPromise - const asymmetricMessageAfterSecondChange = await context.asymmetric.getOutboundMessages() + const asymmetricMessageAfterSecondChange = (await context.asymmetric.getOutboundMessages()).getValue() expect(asymmetricMessageAfterSecondChange.length).to.equal(expectedMessages.length) const messageAfterSecondChange = asymmetricMessageAfterSecondChange[0] diff --git a/packages/snjs/mocha/vaults/keypair-change.test.js b/packages/snjs/mocha/vaults/keypair-change.test.js index 3968c0a51..af979bfdf 100644 --- a/packages/snjs/mocha/vaults/keypair-change.test.js +++ b/packages/snjs/mocha/vaults/keypair-change.test.js @@ -17,13 +17,13 @@ describe('keypair change', function () { beforeEach(async function () { localStorage.clear() - context = await Factory.createAppContextWithRealCrypto() + context = await Factory.createVaultsContextWithRealCrypto() await context.launch() await context.register() }) - it.skip('contacts should be able to handle receiving multiple keypair changed messages and trust them in order', async () => { + it('contacts should be able to handle receiving multiple keypair changed messages and trust them in order', async () => { const { note, contactContext, deinitContactContext } = await Collaboration.createSharedVaultWithAcceptedInviteAndNote(context) @@ -39,20 +39,24 @@ describe('keypair change', function () { publicKeyChain.push(context.publicKey) signingPublicKeyChain.push(context.signingPublicKey) + await contactContext.runAnyRequestToPreventRefreshTokenFromExpiring() + await context.changePassword('new_password-2') publicKeyChain.push(context.publicKey) signingPublicKeyChain.push(context.signingPublicKey) + await contactContext.runAnyRequestToPreventRefreshTokenFromExpiring() + await context.changePassword('new_password-3') publicKeyChain.push(context.publicKey) signingPublicKeyChain.push(context.signingPublicKey) + await contactContext.runAnyRequestToPreventRefreshTokenFromExpiring() + await context.changeNoteTitleAndSync(note, 'new title') contactContext.unlockSyncing() - const promise = contactContext.resolveWhenAsymmetricMessageProcessingCompletes() - await contactContext.sync() - await promise + await contactContext.syncAndAwaitMessageProcessing() const originatorContact = contactContext.contacts.findContact(context.userUuid) let currentKeySet = originatorContact.publicKeySet @@ -70,7 +74,7 @@ describe('keypair change', function () { expect(receivedNote.signatureData.result.passes).to.be.true await deinitContactContext() - }) + }).timeout(Factory.ThirtySecondTimeout) it('should not trust messages sent with previous key pair', async () => { const { sharedVault, contactContext, deinitContactContext } = @@ -86,15 +90,13 @@ describe('keypair change', function () { sinon.stub(context.encryption, 'getKeyPair').returns(previousKeyPair) sinon.stub(context.encryption, 'getSigningKeyPair').returns(previousSigningKeyPair) - await context.vaults.changeVaultNameAndDescription(sharedVault, { + await context.changeVaultName(sharedVault, { name: 'New Name', description: 'New Description', }) contactContext.unlockSyncing() - const promise = contactContext.resolveWhenAsymmetricMessageProcessingCompletes() - await contactContext.sync() - await promise + await contactContext.syncAndAwaitMessageProcessing() const updatedVault = contactContext.vaults.getVault({ keySystemIdentifier: sharedVault.systemIdentifier }) expect(updatedVault.name).to.equal(sharedVault.name) @@ -133,23 +135,18 @@ describe('keypair change', function () { contactContext.lockSyncing() - await context.vaults.changeVaultNameAndDescription(sharedVault, { + await context.changeVaultName(sharedVault, { name: 'New Name', description: 'New Description', }) - const originalMessages = await contactContext.asymmetric.getInboundMessages() + const originalMessages = (await contactContext.asymmetric.getInboundMessages()).getValue() expect(originalMessages.length).to.equal(1) const originalMessage = originalMessages[0] - const promise = context.resolveWhenAsyncFunctionCompletes( - context.application.dependencies.get(TYPES.HandleKeyPairChange), - 'execute', - ) await context.changePassword('new_password') - await promise - const updatedMessages = await contactContext.asymmetric.getInboundMessages() + const updatedMessages = (await contactContext.asymmetric.getInboundMessages()).getValue() const expectedMessages = ['keypair-change', 'vault-change'] expect(updatedMessages.length).to.equal(expectedMessages.length) diff --git a/packages/snjs/mocha/vaults/permissions.test.js b/packages/snjs/mocha/vaults/permissions.test.js index 4efa0474b..bfa031275 100644 --- a/packages/snjs/mocha/vaults/permissions.test.js +++ b/packages/snjs/mocha/vaults/permissions.test.js @@ -17,7 +17,7 @@ describe('shared vault permissions', function () { beforeEach(async function () { localStorage.clear() - context = await Factory.createAppContextWithRealCrypto() + context = await Factory.createVaultsContextWithRealCrypto() await context.launch() await context.register() diff --git a/packages/snjs/mocha/vaults/pkc.test.js b/packages/snjs/mocha/vaults/pkc.test.js index 4a431d22d..c1d76f8f0 100644 --- a/packages/snjs/mocha/vaults/pkc.test.js +++ b/packages/snjs/mocha/vaults/pkc.test.js @@ -18,7 +18,7 @@ describe('public key cryptography', function () { beforeEach(async function () { localStorage.clear() - context = await Factory.createAppContextWithRealCrypto() + context = await Factory.createVaultsContextWithRealCrypto() await context.launch() await context.register() @@ -40,7 +40,7 @@ describe('public key cryptography', function () { const password = context.password await context.signout() - const recreatedContext = await Factory.createAppContextWithRealCrypto() + const recreatedContext = await Factory.createVaultsContextWithRealCrypto() await recreatedContext.launch() recreatedContext.email = email recreatedContext.password = password @@ -51,6 +51,8 @@ describe('public key cryptography', function () { expect(recreatedContext.sessions.getSigningPublicKey()).to.not.be.undefined expect(recreatedContext.encryption.getSigningKeyPair().privateKey).to.not.be.undefined + + await recreatedContext.deinit() }) it('should rotate keypair during password change', async () => { @@ -74,7 +76,7 @@ describe('public key cryptography', function () { }) it('should allow option to enable collaboration for previously signed in accounts', async () => { - const newContext = await Factory.createAppContextWithRealCrypto() + const newContext = await Factory.createVaultsContextWithRealCrypto() await newContext.launch() await newContext.register() @@ -94,5 +96,7 @@ describe('public key cryptography', function () { expect(result.error).to.be.undefined expect(newContext.application.sessions.isUserMissingKeyPair()).to.be.false + + await newContext.deinit() }) }) diff --git a/packages/snjs/mocha/vaults/shared_vaults.test.js b/packages/snjs/mocha/vaults/shared_vaults.test.js index 8f06bb4f0..8335eca8b 100644 --- a/packages/snjs/mocha/vaults/shared_vaults.test.js +++ b/packages/snjs/mocha/vaults/shared_vaults.test.js @@ -10,15 +10,10 @@ describe('shared vaults', function () { let context let vaults - afterEach(async function () { - await context.deinit() - localStorage.clear() - }) - beforeEach(async function () { localStorage.clear() - context = await Factory.createAppContextWithRealCrypto() + context = await Factory.createVaultsContextWithRealCrypto() await context.launch() await context.register() @@ -26,11 +21,18 @@ describe('shared vaults', function () { vaults = context.vaults }) + afterEach(async function () { + await context.deinit() + localStorage.clear() + }) + it('should update vault name and description', async () => { const { sharedVault, contactContext, deinitContactContext } = await Collaboration.createSharedVaultWithAcceptedInvite(context) - await vaults.changeVaultNameAndDescription(sharedVault, { + contactContext.lockSyncing() + + await context.changeVaultName(sharedVault, { name: 'new vault name', description: 'new vault description', }) @@ -39,9 +41,8 @@ describe('shared vaults', function () { expect(updatedVault.name).to.equal('new vault name') expect(updatedVault.description).to.equal('new vault description') - const promise = contactContext.resolveWhenAsymmetricMessageProcessingCompletes() - await contactContext.sync() - await promise + contactContext.unlockSyncing() + await contactContext.syncAndAwaitMessageProcessing() const contactVault = contactContext.vaults.getVault({ keySystemIdentifier: sharedVault.systemIdentifier }) expect(contactVault.name).to.equal('new vault name') @@ -65,7 +66,7 @@ describe('shared vaults', function () { expect(contactContext.keys.getPrimaryKeySystemRootKey(sharedVault.systemIdentifier)).to.be.undefined expect(contactContext.keys.getKeySystemItemsKeys(sharedVault.systemIdentifier)).to.be.empty - const recreatedContext = await Factory.createAppContextWithRealCrypto(contactContext.identifier) + const recreatedContext = await Factory.createVaultsContextWithRealCrypto(contactContext.identifier) await recreatedContext.launch() expect(recreatedContext.vaults.getVault({ keySystemIdentifier: sharedVault.systemIdentifier })).to.be.undefined @@ -92,7 +93,7 @@ describe('shared vaults', function () { expect(contactContext.keys.getPrimaryKeySystemRootKey(sharedVault.systemIdentifier)).to.be.undefined expect(contactContext.keys.getKeySystemItemsKeys(sharedVault.systemIdentifier)).to.be.empty - const recreatedContext = await Factory.createAppContextWithRealCrypto(contactContext.identifier) + const recreatedContext = await Factory.createVaultsContextWithRealCrypto(contactContext.identifier) await recreatedContext.launch() expect(recreatedContext.vaults.getVault({ keySystemIdentifier: sharedVault.systemIdentifier })).to.be.undefined @@ -127,4 +128,52 @@ describe('shared vaults', function () { await deinitThirdPartyContext() }) + + it('syncing a shared vault exclusively should not retrieve non vault items', async () => { + const { sharedVault, contactContext, deinitContactContext } = + await Collaboration.createSharedVaultWithAcceptedInvite(context) + + await contactContext.createSyncedNote('foo', 'bar') + + const syncPromise = contactContext.awaitNextSyncSharedVaultFromScratchEvent() + + await contactContext.application.sync.syncSharedVaultsFromScratch([sharedVault.sharing.sharedVaultUuid]) + + const syncResponse = await syncPromise + + const expectedItems = ['key system items key'] + + expect(syncResponse.retrievedPayloads.length).to.equal(expectedItems.length) + + await deinitContactContext() + }) + + it('syncing a shared vault with note exclusively should retrieve note and items key', async () => { + const { sharedVault, contactContext, deinitContactContext } = + await Collaboration.createSharedVaultWithAcceptedInviteAndNote(context) + + const syncPromise = contactContext.awaitNextSyncSharedVaultFromScratchEvent() + + await contactContext.application.sync.syncSharedVaultsFromScratch([sharedVault.sharing.sharedVaultUuid]) + + const syncResponse = await syncPromise + + const expectedItems = ['key system items key', 'note'] + + expect(syncResponse.retrievedPayloads.length).to.equal(expectedItems.length) + + await deinitContactContext() + }) + + it('regular sync should not needlessly return vault items', async () => { + await Collaboration.createSharedVault(context) + + const promise = context.resolveWithSyncRetrievedPayloads() + + await context.sync() + + const retrievedPayloads = await promise + + expect(retrievedPayloads.length).to.equal(0) + }) }) diff --git a/packages/snjs/mocha/vaults/signatures.test.js b/packages/snjs/mocha/vaults/signatures.test.js index 1a3da4420..433578b3c 100644 --- a/packages/snjs/mocha/vaults/signatures.test.js +++ b/packages/snjs/mocha/vaults/signatures.test.js @@ -17,7 +17,7 @@ describe('signatures', function () { beforeEach(async function () { localStorage.clear() - context = await Factory.createAppContextWithRealCrypto() + context = await Factory.createVaultsContextWithRealCrypto() await context.launch() await context.register() diff --git a/packages/snjs/mocha/vaults/vaults.test.js b/packages/snjs/mocha/vaults/vaults.test.js index d9c8eba41..de7c80399 100644 --- a/packages/snjs/mocha/vaults/vaults.test.js +++ b/packages/snjs/mocha/vaults/vaults.test.js @@ -17,7 +17,7 @@ describe('vaults', function () { beforeEach(async function () { localStorage.clear() - context = await Factory.createAppContextWithRealCrypto() + context = await Factory.createVaultsContextWithRealCrypto() await context.launch() @@ -77,7 +77,7 @@ describe('vaults', function () { await vaults.moveItemToVault(vault, note) await context.deinit() - const recreatedContext = await Factory.createAppContextWithRealCrypto(appIdentifier) + const recreatedContext = await Factory.createVaultsContextWithRealCrypto(appIdentifier) await recreatedContext.launch() const updatedNote = recreatedContext.items.findItem(note.uuid) @@ -101,7 +101,7 @@ describe('vaults', function () { await context.deinit() - const recreatedContext = await Factory.createAppContextWithRealCrypto(appIdentifier) + const recreatedContext = await Factory.createVaultsContextWithRealCrypto(appIdentifier) await recreatedContext.launch() const notes = recreatedContext.notes @@ -128,7 +128,7 @@ describe('vaults', function () { await context.deinit() - const recreatedContext = await Factory.createAppContextWithRealCrypto(appIdentifier) + const recreatedContext = await Factory.createVaultsContextWithRealCrypto(appIdentifier) await recreatedContext.launch() const updatedNote = recreatedContext.items.findItem(note.uuid) @@ -181,7 +181,7 @@ describe('vaults', function () { await vaults.moveItemToVault(vault, note) await context.deinit() - const recreatedContext = await Factory.createAppContextWithRealCrypto(appIdentifier) + const recreatedContext = await Factory.createVaultsContextWithRealCrypto(appIdentifier) await recreatedContext.launch() const updatedNote = recreatedContext.items.findItem(note.uuid) diff --git a/packages/utils/src/Domain/Logger/LogLevel.ts b/packages/utils/src/Domain/Logger/LogLevel.ts new file mode 100644 index 000000000..652d62634 --- /dev/null +++ b/packages/utils/src/Domain/Logger/LogLevel.ts @@ -0,0 +1 @@ +export type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'none' diff --git a/packages/utils/src/Domain/Logger/Logger.ts b/packages/utils/src/Domain/Logger/Logger.ts new file mode 100644 index 000000000..43c5e137c --- /dev/null +++ b/packages/utils/src/Domain/Logger/Logger.ts @@ -0,0 +1,64 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import { LogLevel } from './LogLevel' + +export class Logger { + private level: LogLevel = 'none' + + constructor(private appIdentifier: string) {} + + private canLog(level: LogLevel): boolean { + if (this.level === 'none') { + return false + } + + const levels: LogLevel[] = ['debug', 'info', 'warn', 'error'] + return levels.indexOf(level) >= levels.indexOf(this.level) + } + + public setLevel(level: LogLevel): void { + this.level = level + } + + public debug(message: string, ...optionalParams: any[]): void { + if (this.canLog('debug')) { + this.logWithColor(message, ...optionalParams) + } + } + + public info(message: string, ...optionalParams: any[]): void { + if (this.canLog('info')) { + this.logWithColor(message, ...optionalParams) + } + } + + public warn(message: string, ...optionalParams: any[]): void { + if (this.canLog('warn')) { + console.warn(message, ...optionalParams) + } + } + + public error(message: string, ...optionalParams: any[]): void { + if (this.canLog('error')) { + console.error(message, ...optionalParams) + } + } + + private logWithColor(...args: any[]): void { + const date = new Date() + const timeString = `${date.toLocaleTimeString().replace(' PM', '').replace(' AM', '')}.${date.getMilliseconds()}` + this.customLog( + `%c${this.appIdentifier}%c${timeString}`, + 'color: font-weight: bold; margin-right: 4px', + 'color: gray', + ...args, + ) + } + + private customLog(..._args: any[]) { + // eslint-disable-next-line no-console, prefer-rest-params + Function.prototype.apply.call(console.log, console, arguments) + } +} + +export default Logger diff --git a/packages/utils/src/Domain/Logger/LoggerInterface.ts b/packages/utils/src/Domain/Logger/LoggerInterface.ts new file mode 100644 index 000000000..802b0f380 --- /dev/null +++ b/packages/utils/src/Domain/Logger/LoggerInterface.ts @@ -0,0 +1,10 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { LogLevel } from './LogLevel' + +export interface LoggerInterface { + setLevel(level: LogLevel): void + debug(message: string, ...optionalParams: any[]): void + info(message: string, ...optionalParams: any[]): void + warn(message: string, ...optionalParams: any[]): void + error(message: string, ...optionalParams: any[]): void +} diff --git a/packages/utils/src/Domain/index.ts b/packages/utils/src/Domain/index.ts index 800cbd5a0..626591680 100644 --- a/packages/utils/src/Domain/index.ts +++ b/packages/utils/src/Domain/index.ts @@ -1,8 +1,11 @@ export * from './Date/DateUtils' export * from './Deferred/Deferred' +export * from './Logger/Logger' +export * from './Logger/LoggerInterface' +export * from './Logger/LogLevel' export * from './Utils/ClassNames' -export * from './Utils/Utils' export * from './Utils/Debounce' +export * from './Utils/Utils' export * from './Uuid/Utils' export * from './Uuid/UuidGenerator' export * from './Uuid/UuidMap'