chore: vault tests refactors and lint (#2374)

This commit is contained in:
Karol Sójko
2023-08-02 00:23:56 +02:00
committed by GitHub
parent a0bc1d2488
commit 247daddf5a
96 changed files with 1463 additions and 751 deletions

View File

@@ -4,12 +4,14 @@ import { FetchRequestHandler } from './FetchRequestHandler'
import { HttpErrorResponseBody, HttpRequest } from '@standardnotes/responses' import { HttpErrorResponseBody, HttpRequest } from '@standardnotes/responses'
import { ErrorMessage } from '../Error' import { ErrorMessage } from '../Error'
import { LoggerInterface } from '@standardnotes/utils'
describe('FetchRequestHandler', () => { describe('FetchRequestHandler', () => {
const snjsVersion = 'snjsVersion' const snjsVersion = 'snjsVersion'
const appVersion = 'appVersion' const appVersion = 'appVersion'
const environment = Environment.Web const environment = Environment.Web
const requestHandler = new FetchRequestHandler(snjsVersion, appVersion, environment) const logger: LoggerInterface = {} as jest.Mocked<LoggerInterface>
const requestHandler = new FetchRequestHandler(snjsVersion, appVersion, environment, logger)
it('should create a request', () => { it('should create a request', () => {
const httpRequest: HttpRequest = { const httpRequest: HttpRequest = {

View File

@@ -11,12 +11,14 @@ import { RequestHandlerInterface } from './RequestHandlerInterface'
import { Environment } from '@standardnotes/models' import { Environment } from '@standardnotes/models'
import { isString } from 'lodash' import { isString } from 'lodash'
import { ErrorMessage } from '../Error' import { ErrorMessage } from '../Error'
import { LoggerInterface } from '@standardnotes/utils'
export class FetchRequestHandler implements RequestHandlerInterface { export class FetchRequestHandler implements RequestHandlerInterface {
constructor( constructor(
protected readonly snjsVersion: string, protected readonly snjsVersion: string,
protected readonly appVersion: string, protected readonly appVersion: string,
protected readonly environment: Environment, protected readonly environment: Environment,
private logger: LoggerInterface,
) {} ) {}
async handleRequest<T>(httpRequest: HttpRequest): Promise<HttpResponse<T>> { async handleRequest<T>(httpRequest: HttpRequest): Promise<HttpResponse<T>> {
@@ -122,7 +124,7 @@ export class FetchRequestHandler implements RequestHandlerInterface {
} }
} }
} catch (error) { } catch (error) {
console.error(error) this.logger.error(JSON.stringify(error))
} }
if (httpStatus >= HttpStatusCode.Success && httpStatus < HttpStatusCode.InternalServerError) { if (httpStatus >= HttpStatusCode.Success && httpStatus < HttpStatusCode.InternalServerError) {

View File

@@ -1,4 +1,4 @@
import { joinPaths, sleep } from '@standardnotes/utils' import { LoggerInterface, joinPaths, sleep } from '@standardnotes/utils'
import { Environment } from '@standardnotes/models' import { Environment } from '@standardnotes/models'
import { LegacySession, Session, SessionToken } from '@standardnotes/domain-core' import { LegacySession, Session, SessionToken } from '@standardnotes/domain-core'
import { import {
@@ -21,6 +21,7 @@ export class HttpService implements HttpServiceInterface {
private session?: Session | LegacySession private session?: Session | LegacySession
private __latencySimulatorMs?: number private __latencySimulatorMs?: number
private declare host: string private declare host: string
loggingEnabled = false
private inProgressRefreshSessionPromise?: Promise<boolean> private inProgressRefreshSessionPromise?: Promise<boolean>
private updateMetaCallback!: (meta: HttpResponseMeta) => void private updateMetaCallback!: (meta: HttpResponseMeta) => void
@@ -32,8 +33,9 @@ export class HttpService implements HttpServiceInterface {
private environment: Environment, private environment: Environment,
private appVersion: string, private appVersion: string,
private snjsVersion: 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( setCallbacks(
@@ -150,6 +152,10 @@ export class HttpService implements HttpServiceInterface {
const response = await this.requestHandler.handleRequest<T>(httpRequest) const response = await this.requestHandler.handleRequest<T>(httpRequest)
if (this.loggingEnabled && isErrorResponse(response)) {
this.logger.error('Request failed', httpRequest, response)
}
if (response.meta && !httpRequest.external) { if (response.meta && !httpRequest.external) {
this.updateMetaCallback?.(response.meta) this.updateMetaCallback?.(response.meta)
} }

View File

@@ -1,5 +0,0 @@
recursive: true
timeout: 120000
bail: true
file:
- e2e/init.js

View File

@@ -3,7 +3,7 @@ import { ApiEndpointParam } from './ApiEndpointParam'
import { ConflictParams } from './ConflictParams' import { ConflictParams } from './ConflictParams'
import { ServerItemResponse } from './ServerItemResponse' import { ServerItemResponse } from './ServerItemResponse'
import { SharedVaultServerHash } from '../SharedVaults/SharedVaultServerHash' import { SharedVaultServerHash } from '../SharedVaults/SharedVaultServerHash'
import { UserEventServerHash } from '../UserEvent/UserEventServerHash' import { NotificationServerHash } from '../Notification/NotificationServerHash'
import { AsymmetricMessageServerHash } from '../AsymmetricMessage/AsymmetricMessageServerHash' import { AsymmetricMessageServerHash } from '../AsymmetricMessage/AsymmetricMessageServerHash'
export type RawSyncData = { export type RawSyncData = {
@@ -16,7 +16,7 @@ export type RawSyncData = {
unsaved?: ConflictParams[] unsaved?: ConflictParams[]
shared_vaults?: SharedVaultServerHash[] shared_vaults?: SharedVaultServerHash[]
shared_vault_invites?: SharedVaultInviteServerHash[] shared_vault_invites?: SharedVaultInviteServerHash[]
notifications?: UserEventServerHash[] notifications?: NotificationServerHash[]
messages?: AsymmetricMessageServerHash[] messages?: AsymmetricMessageServerHash[]
status?: number status?: number
} }

View File

@@ -1,4 +1,4 @@
export type UserEventServerHash = { export type NotificationServerHash = {
uuid: string uuid: string
user_uuid: string user_uuid: string
type: string type: string

View File

@@ -66,4 +66,4 @@ export * from './User/PostSubscriptionTokensResponse'
export * from './User/SettingData' export * from './User/SettingData'
export * from './User/UpdateSettingResponse' export * from './User/UpdateSettingResponse'
export * from './UserEvent/UserEventServerHash' export * from './Notification/NotificationServerHash'

View File

@@ -25,6 +25,7 @@ import {
KeySystemRootKeyContentSpecialized, KeySystemRootKeyContentSpecialized,
TrustedContactInterface, TrustedContactInterface,
} from '@standardnotes/models' } from '@standardnotes/models'
import { Result } from '@standardnotes/domain-core'
describe('AsymmetricMessageService', () => { describe('AsymmetricMessageService', () => {
let sync: jest.Mocked<SyncServiceInterface> let sync: jest.Mocked<SyncServiceInterface>
@@ -61,6 +62,7 @@ describe('AsymmetricMessageService', () => {
encryption, encryption,
mutator, mutator,
sessions, sessions,
sync,
messageServer, messageServer,
createOrEditContact, createOrEditContact,
findContact, 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 () => { it('should process incoming messages oldest first', async () => {
const messages: AsymmetricMessageServerHash[] = [ const messages: AsymmetricMessageServerHash[] = [
{ {
@@ -139,7 +180,7 @@ describe('AsymmetricMessageService', () => {
service.getTrustedMessagePayload = service.getUntrustedMessagePayload = jest service.getTrustedMessagePayload = service.getUntrustedMessagePayload = jest
.fn() .fn()
.mockReturnValue(trustedPayloadMock) .mockReturnValue(Result.ok(trustedPayloadMock))
const handleTrustedContactShareMessageMock = jest.fn() const handleTrustedContactShareMessageMock = jest.fn()
service.handleTrustedContactShareMessage = handleTrustedContactShareMessageMock service.handleTrustedContactShareMessage = handleTrustedContactShareMessageMock
@@ -171,7 +212,7 @@ describe('AsymmetricMessageService', () => {
service.handleTrustedContactShareMessage = jest.fn() service.handleTrustedContactShareMessage = jest.fn()
service.getTrustedMessagePayload = service.getUntrustedMessagePayload = jest service.getTrustedMessagePayload = service.getUntrustedMessagePayload = jest
.fn() .fn()
.mockReturnValue(decryptedMessagePayload) .mockReturnValue(Result.ok(decryptedMessagePayload))
await service.handleRemoteReceivedAsymmetricMessages([message]) await service.handleRemoteReceivedAsymmetricMessages([message])
@@ -200,7 +241,7 @@ describe('AsymmetricMessageService', () => {
service.handleTrustedSenderKeypairChangedMessage = jest.fn() service.handleTrustedSenderKeypairChangedMessage = jest.fn()
service.getTrustedMessagePayload = service.getUntrustedMessagePayload = jest service.getTrustedMessagePayload = service.getUntrustedMessagePayload = jest
.fn() .fn()
.mockReturnValue(decryptedMessagePayload) .mockReturnValue(Result.ok(decryptedMessagePayload))
await service.handleRemoteReceivedAsymmetricMessages([message]) await service.handleRemoteReceivedAsymmetricMessages([message])
@@ -228,7 +269,7 @@ describe('AsymmetricMessageService', () => {
service.handleTrustedSharedVaultRootKeyChangedMessage = jest.fn() service.handleTrustedSharedVaultRootKeyChangedMessage = jest.fn()
service.getTrustedMessagePayload = service.getUntrustedMessagePayload = jest service.getTrustedMessagePayload = service.getUntrustedMessagePayload = jest
.fn() .fn()
.mockReturnValue(decryptedMessagePayload) .mockReturnValue(Result.ok(decryptedMessagePayload))
await service.handleRemoteReceivedAsymmetricMessages([message]) await service.handleRemoteReceivedAsymmetricMessages([message])
@@ -258,7 +299,7 @@ describe('AsymmetricMessageService', () => {
service.handleTrustedVaultMetadataChangedMessage = jest.fn() service.handleTrustedVaultMetadataChangedMessage = jest.fn()
service.getTrustedMessagePayload = service.getUntrustedMessagePayload = jest service.getTrustedMessagePayload = service.getUntrustedMessagePayload = jest
.fn() .fn()
.mockReturnValue(decryptedMessagePayload) .mockReturnValue(Result.ok(decryptedMessagePayload))
await service.handleRemoteReceivedAsymmetricMessages([message]) await service.handleRemoteReceivedAsymmetricMessages([message])
@@ -284,7 +325,7 @@ describe('AsymmetricMessageService', () => {
service.getTrustedMessagePayload = service.getUntrustedMessagePayload = jest service.getTrustedMessagePayload = service.getUntrustedMessagePayload = jest
.fn() .fn()
.mockReturnValue(decryptedMessagePayload) .mockReturnValue(Result.ok(decryptedMessagePayload))
await expect(service.handleRemoteReceivedAsymmetricMessages([message])).rejects.toThrow( await expect(service.handleRemoteReceivedAsymmetricMessages([message])).rejects.toThrow(
'Shared vault invites payloads are not handled as part of asymmetric messages', 'Shared vault invites payloads are not handled as part of asymmetric messages',
@@ -313,7 +354,7 @@ describe('AsymmetricMessageService', () => {
service.handleTrustedContactShareMessage = jest.fn() service.handleTrustedContactShareMessage = jest.fn()
service.getTrustedMessagePayload = service.getUntrustedMessagePayload = jest service.getTrustedMessagePayload = service.getUntrustedMessagePayload = jest
.fn() .fn()
.mockReturnValue(decryptedMessagePayload) .mockReturnValue(Result.ok(decryptedMessagePayload))
await service.handleRemoteReceivedAsymmetricMessages([message]) await service.handleRemoteReceivedAsymmetricMessages([message])

View File

@@ -1,6 +1,7 @@
import { SyncServiceInterface } from './../Sync/SyncServiceInterface'
import { SessionsClientInterface } from './../Session/SessionsClientInterface' import { SessionsClientInterface } from './../Session/SessionsClientInterface'
import { MutatorClientInterface } from './../Mutator/MutatorClientInterface' import { MutatorClientInterface } from './../Mutator/MutatorClientInterface'
import { AsymmetricMessageServerHash, ClientDisplayableError, isClientDisplayableError } from '@standardnotes/responses' import { AsymmetricMessageServerHash } from '@standardnotes/responses'
import { SyncEvent, SyncEventReceivedAsymmetricMessagesData } from '../Event/SyncEvent' import { SyncEvent, SyncEventReceivedAsymmetricMessagesData } from '../Event/SyncEvent'
import { InternalEventBusInterface } from '../Internal/InternalEventBusInterface' import { InternalEventBusInterface } from '../Internal/InternalEventBusInterface'
import { InternalEventHandlerInterface } from '../Internal/InternalEventHandlerInterface' import { InternalEventHandlerInterface } from '../Internal/InternalEventHandlerInterface'
@@ -20,7 +21,6 @@ import {
VaultListingInterface, VaultListingInterface,
} from '@standardnotes/models' } from '@standardnotes/models'
import { HandleRootKeyChangedMessage } from './UseCase/HandleRootKeyChangedMessage' import { HandleRootKeyChangedMessage } from './UseCase/HandleRootKeyChangedMessage'
import { SessionEvent } from '../Session/SessionEvent'
import { AsymmetricMessageServer } from '@standardnotes/api' import { AsymmetricMessageServer } from '@standardnotes/api'
import { GetOutboundMessages } from './UseCase/GetOutboundMessages' import { GetOutboundMessages } from './UseCase/GetOutboundMessages'
import { GetInboundMessages } from './UseCase/GetInboundMessages' import { GetInboundMessages } from './UseCase/GetInboundMessages'
@@ -31,15 +31,19 @@ import { FindContact } from '../Contacts/UseCase/FindContact'
import { CreateOrEditContact } from '../Contacts/UseCase/CreateOrEditContact' import { CreateOrEditContact } from '../Contacts/UseCase/CreateOrEditContact'
import { ReplaceContactData } from '../Contacts/UseCase/ReplaceContactData' import { ReplaceContactData } from '../Contacts/UseCase/ReplaceContactData'
import { EncryptionProviderInterface } from '../Encryption/EncryptionProviderInterface' import { EncryptionProviderInterface } from '../Encryption/EncryptionProviderInterface'
import { Result } from '@standardnotes/domain-core'
export class AsymmetricMessageService export class AsymmetricMessageService
extends AbstractService extends AbstractService
implements AsymmetricMessageServiceInterface, InternalEventHandlerInterface implements AsymmetricMessageServiceInterface, InternalEventHandlerInterface
{ {
private handledMessages = new Set<string>()
constructor( constructor(
private encryption: EncryptionProviderInterface, private encryption: EncryptionProviderInterface,
private mutator: MutatorClientInterface, private mutator: MutatorClientInterface,
private sessions: SessionsClientInterface, private sessions: SessionsClientInterface,
private sync: SyncServiceInterface,
private messageServer: AsymmetricMessageServer, private messageServer: AsymmetricMessageServer,
private _createOrEditContact: CreateOrEditContact, private _createOrEditContact: CreateOrEditContact,
private _findContact: FindContact, private _findContact: FindContact,
@@ -73,30 +77,27 @@ export class AsymmetricMessageService
async handleEvent(event: InternalEventInterface): Promise<void> { async handleEvent(event: InternalEventInterface): Promise<void> {
switch (event.type) { switch (event.type) {
case SessionEvent.UserKeyPairChanged:
void this.messageServer.deleteAllInboundMessages()
break
case SyncEvent.ReceivedAsymmetricMessages: case SyncEvent.ReceivedAsymmetricMessages:
void this.handleRemoteReceivedAsymmetricMessages(event.payload as SyncEventReceivedAsymmetricMessagesData) void this.handleRemoteReceivedAsymmetricMessages(event.payload as SyncEventReceivedAsymmetricMessagesData)
break break
} }
} }
public async getOutboundMessages(): Promise<AsymmetricMessageServerHash[] | ClientDisplayableError> { public async getOutboundMessages(): Promise<Result<AsymmetricMessageServerHash[]>> {
return this._getOutboundMessagesUseCase.execute() return this._getOutboundMessagesUseCase.execute()
} }
public async getInboundMessages(): Promise<AsymmetricMessageServerHash[] | ClientDisplayableError> { public async getInboundMessages(): Promise<Result<AsymmetricMessageServerHash[]>> {
return this._getInboundMessagesUseCase.execute() return this._getInboundMessagesUseCase.execute()
} }
public async downloadAndProcessInboundMessages(): Promise<void> { public async downloadAndProcessInboundMessages(): Promise<void> {
const messages = await this.getInboundMessages() const messages = await this.getInboundMessages()
if (isClientDisplayableError(messages)) { if (messages.isFailed()) {
return return
} }
await this.handleRemoteReceivedAsymmetricMessages(messages) await this.handleRemoteReceivedAsymmetricMessages(messages.getValue())
} }
sortServerMessages(messages: AsymmetricMessageServerHash[]): AsymmetricMessageServerHash[] { sortServerMessages(messages: AsymmetricMessageServerHash[]): AsymmetricMessageServerHash[] {
@@ -143,11 +144,11 @@ export class AsymmetricMessageService
getServerMessageType(message: AsymmetricMessageServerHash): AsymmetricMessagePayloadType | undefined { getServerMessageType(message: AsymmetricMessageServerHash): AsymmetricMessagePayloadType | undefined {
const result = this.getUntrustedMessagePayload(message) const result = this.getUntrustedMessagePayload(message)
if (!result) { if (result.isFailed()) {
return undefined return undefined
} }
return result.type return result.getValue().type
} }
async handleRemoteReceivedAsymmetricMessages(messages: AsymmetricMessageServerHash[]): Promise<void> { async handleRemoteReceivedAsymmetricMessages(messages: AsymmetricMessageServerHash[]): Promise<void> {
@@ -159,18 +160,26 @@ export class AsymmetricMessageService
for (const message of sortedMessages) { for (const message of sortedMessages) {
const trustedPayload = this.getTrustedMessagePayload(message) const trustedPayload = this.getTrustedMessagePayload(message)
if (!trustedPayload) { if (trustedPayload.isFailed()) {
continue continue
} }
await this.handleTrustedMessageResult(message, trustedPayload) await this.handleTrustedMessageResult(message, trustedPayload.getValue())
} }
void this.sync.sync()
} }
private async handleTrustedMessageResult( async handleTrustedMessageResult(
message: AsymmetricMessageServerHash, message: AsymmetricMessageServerHash,
payload: AsymmetricMessagePayload, payload: AsymmetricMessagePayload,
): Promise<void> { ): Promise<void> {
if (this.handledMessages.has(message.uuid)) {
return
}
this.handledMessages.add(message.uuid)
if (payload.type === AsymmetricMessagePayloadType.ContactShare) { if (payload.type === AsymmetricMessagePayloadType.ContactShare) {
await this.handleTrustedContactShareMessage(message, payload) await this.handleTrustedContactShareMessage(message, payload)
} else if (payload.type === AsymmetricMessagePayloadType.SenderKeypairChanged) { } else if (payload.type === AsymmetricMessagePayloadType.SenderKeypairChanged) {
@@ -186,23 +195,23 @@ export class AsymmetricMessageService
await this.deleteMessageAfterProcessing(message) await this.deleteMessageAfterProcessing(message)
} }
getUntrustedMessagePayload(message: AsymmetricMessageServerHash): AsymmetricMessagePayload | undefined { getUntrustedMessagePayload(message: AsymmetricMessageServerHash): Result<AsymmetricMessagePayload> {
const result = this._getUntrustedPayload.execute({ const result = this._getUntrustedPayload.execute({
privateKey: this.encryption.getKeyPair().privateKey, privateKey: this.encryption.getKeyPair().privateKey,
message, message,
}) })
if (result.isFailed()) { if (result.isFailed()) {
return undefined return Result.fail(result.getError())
} }
return result.getValue() return result
} }
getTrustedMessagePayload(message: AsymmetricMessageServerHash): AsymmetricMessagePayload | undefined { getTrustedMessagePayload(message: AsymmetricMessageServerHash): Result<AsymmetricMessagePayload> {
const contact = this._findContact.execute({ userUuid: message.sender_uuid }) const contact = this._findContact.execute({ userUuid: message.sender_uuid })
if (contact.isFailed()) { if (contact.isFailed()) {
return undefined return Result.fail(contact.getError())
} }
const result = this._getTrustedPayload.execute({ const result = this._getTrustedPayload.execute({
@@ -213,10 +222,10 @@ export class AsymmetricMessageService
}) })
if (result.isFailed()) { if (result.isFailed()) {
return undefined return Result.fail(result.getError())
} }
return result.getValue() return result
} }
async deleteMessageAfterProcessing(message: AsymmetricMessageServerHash): Promise<void> { async deleteMessageAfterProcessing(message: AsymmetricMessageServerHash): Promise<void> {

View File

@@ -1,7 +1,8 @@
import { AsymmetricMessageServerHash, ClientDisplayableError } from '@standardnotes/responses' import { Result } from '@standardnotes/domain-core'
import { AsymmetricMessageServerHash } from '@standardnotes/responses'
export interface AsymmetricMessageServiceInterface { export interface AsymmetricMessageServiceInterface {
getOutboundMessages(): Promise<AsymmetricMessageServerHash[] | ClientDisplayableError> getOutboundMessages(): Promise<Result<AsymmetricMessageServerHash[]>>
getInboundMessages(): Promise<AsymmetricMessageServerHash[] | ClientDisplayableError> getInboundMessages(): Promise<Result<AsymmetricMessageServerHash[]>>
downloadAndProcessInboundMessages(): Promise<void> downloadAndProcessInboundMessages(): Promise<void>
} }

View File

@@ -1,16 +1,17 @@
import { ClientDisplayableError, isErrorResponse, AsymmetricMessageServerHash } from '@standardnotes/responses' import { isErrorResponse, AsymmetricMessageServerHash, getErrorFromErrorResponse } from '@standardnotes/responses'
import { AsymmetricMessageServerInterface } from '@standardnotes/api' import { AsymmetricMessageServerInterface } from '@standardnotes/api'
import { Result, UseCaseInterface } from '@standardnotes/domain-core'
export class GetInboundMessages { export class GetInboundMessages implements UseCaseInterface<AsymmetricMessageServerHash[]> {
constructor(private messageServer: AsymmetricMessageServerInterface) {} constructor(private messageServer: AsymmetricMessageServerInterface) {}
async execute(): Promise<AsymmetricMessageServerHash[] | ClientDisplayableError> { async execute(): Promise<Result<AsymmetricMessageServerHash[]>> {
const response = await this.messageServer.getMessages() const response = await this.messageServer.getMessages()
if (isErrorResponse(response)) { if (isErrorResponse(response)) {
return ClientDisplayableError.FromNetworkError(response) return Result.fail(getErrorFromErrorResponse(response).message)
} }
return response.data.messages return Result.ok(response.data.messages)
} }
} }

View File

@@ -1,16 +1,17 @@
import { ClientDisplayableError, isErrorResponse, AsymmetricMessageServerHash } from '@standardnotes/responses' import { isErrorResponse, AsymmetricMessageServerHash, getErrorFromErrorResponse } from '@standardnotes/responses'
import { AsymmetricMessageServerInterface } from '@standardnotes/api' import { AsymmetricMessageServerInterface } from '@standardnotes/api'
import { Result, UseCaseInterface } from '@standardnotes/domain-core'
export class GetOutboundMessages { export class GetOutboundMessages implements UseCaseInterface<AsymmetricMessageServerHash[]> {
constructor(private messageServer: AsymmetricMessageServerInterface) {} constructor(private messageServer: AsymmetricMessageServerInterface) {}
async execute(): Promise<AsymmetricMessageServerHash[] | ClientDisplayableError> { async execute(): Promise<Result<AsymmetricMessageServerHash[]>> {
const response = await this.messageServer.getOutboundUserMessages() const response = await this.messageServer.getOutboundUserMessages()
if (isErrorResponse(response)) { if (isErrorResponse(response)) {
return ClientDisplayableError.FromNetworkError(response) return Result.fail(getErrorFromErrorResponse(response).message)
} }
return response.data.messages return Result.ok(response.data.messages)
} }
} }

View File

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

View File

@@ -1,17 +1,28 @@
import { DecryptOwnMessage } from './../../Encryption/UseCase/Asymmetric/DecryptOwnMessage'
import { AsymmetricMessageServerHash, isErrorResponse } from '@standardnotes/responses' import { AsymmetricMessageServerHash, isErrorResponse } from '@standardnotes/responses'
import { PkcKeyPair } from '@standardnotes/sncrypto-common' import { PkcKeyPair } from '@standardnotes/sncrypto-common'
import { Result, UseCaseInterface } from '@standardnotes/domain-core' import { Result, UseCaseInterface } from '@standardnotes/domain-core'
import { AsymmetricMessageServerInterface } from '@standardnotes/api' import { AsymmetricMessageServerInterface } from '@standardnotes/api'
import { ResendMessage } from './ResendMessage' import { ResendMessage } from './ResendMessage'
import { FindContact } from '../../Contacts/UseCase/FindContact' import { FindContact } from '../../Contacts/UseCase/FindContact'
import { AsymmetricMessagePayload, AsymmetricMessagePayloadType } from '@standardnotes/models'
export class ResendAllMessages implements UseCaseInterface<void> { export class ResendAllMessages implements UseCaseInterface<void> {
constructor( constructor(
private resendMessage: ResendMessage, private resendMessage: ResendMessage,
private decryptOwnMessage: DecryptOwnMessage<AsymmetricMessagePayload>,
private messageServer: AsymmetricMessageServerInterface, private messageServer: AsymmetricMessageServerInterface,
private findContact: FindContact, 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: { async execute(params: {
keys: { keys: {
encryption: PkcKeyPair encryption: PkcKeyPair
@@ -37,10 +48,27 @@ export class ResendAllMessages implements UseCaseInterface<void> {
continue 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({ await this.resendMessage.execute({
keys: params.keys, keys: params.keys,
previousKeys: params.previousKeys, previousKeys: params.previousKeys,
message: message, decryptedMessage: decryptedMessage,
rawMessage: message,
recipient: recipient.getValue(), recipient: recipient.getValue(),
}) })

View File

@@ -1,4 +1,3 @@
import { DecryptOwnMessage } from '../../Encryption/UseCase/Asymmetric/DecryptOwnMessage'
import { AsymmetricMessagePayload, TrustedContactInterface } from '@standardnotes/models' import { AsymmetricMessagePayload, TrustedContactInterface } from '@standardnotes/models'
import { AsymmetricMessageServerHash } from '@standardnotes/responses' import { AsymmetricMessageServerHash } from '@standardnotes/responses'
import { PkcKeyPair } from '@standardnotes/sncrypto-common' import { PkcKeyPair } from '@standardnotes/sncrypto-common'
@@ -8,7 +7,6 @@ import { SendMessage } from './SendMessage'
export class ResendMessage implements UseCaseInterface<void> { export class ResendMessage implements UseCaseInterface<void> {
constructor( constructor(
private decryptOwnMessage: DecryptOwnMessage<AsymmetricMessagePayload>,
private sendMessage: SendMessage, private sendMessage: SendMessage,
private encryptMessage: EncryptMessage, private encryptMessage: EncryptMessage,
) {} ) {}
@@ -23,22 +21,11 @@ export class ResendMessage implements UseCaseInterface<void> {
signing: PkcKeyPair signing: PkcKeyPair
} }
recipient: TrustedContactInterface recipient: TrustedContactInterface
message: AsymmetricMessageServerHash rawMessage: AsymmetricMessageServerHash
decryptedMessage: AsymmetricMessagePayload
}): Promise<Result<AsymmetricMessageServerHash>> { }): Promise<Result<AsymmetricMessageServerHash>> {
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({ const encryptedMessage = this.encryptMessage.execute({
message: decryptedMessage, message: params.decryptedMessage,
keys: params.keys, keys: params.keys,
recipientPublicKey: params.recipient.publicKeySet.encryption, recipientPublicKey: params.recipient.publicKeySet.encryption,
}) })
@@ -50,7 +37,7 @@ export class ResendMessage implements UseCaseInterface<void> {
const sendMessageResult = await this.sendMessage.execute({ const sendMessageResult = await this.sendMessage.execute({
recipientUuid: params.recipient.contactUuid, recipientUuid: params.recipient.contactUuid,
encryptedMessage: encryptedMessage.getValue(), encryptedMessage: encryptedMessage.getValue(),
replaceabilityIdentifier: params.message.replaceabilityIdentifier, replaceabilityIdentifier: params.rawMessage.replaceabilityIdentifier,
}) })
return sendMessageResult return sendMessageResult

View File

@@ -1,10 +1,5 @@
import { SendOwnContactChangeMessage } from './UseCase/SendOwnContactChangeMessage'
import { DeleteContact } from './UseCase/DeleteContact' import { DeleteContact } from './UseCase/DeleteContact'
import { MutatorClientInterface } from './../Mutator/MutatorClientInterface' 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 { PureCryptoInterface } from '@standardnotes/sncrypto-common'
import { SharedVaultInviteServerHash, SharedVaultUserServerHash } from '@standardnotes/responses' import { SharedVaultInviteServerHash, SharedVaultUserServerHash } from '@standardnotes/responses'
import { TrustedContactInterface, TrustedContactMutator, DecryptedItemInterface } from '@standardnotes/models' import { TrustedContactInterface, TrustedContactMutator, DecryptedItemInterface } from '@standardnotes/models'
@@ -25,10 +20,7 @@ import { GetAllContacts } from './UseCase/GetAllContacts'
import { EncryptionProviderInterface } from '../Encryption/EncryptionProviderInterface' import { EncryptionProviderInterface } from '../Encryption/EncryptionProviderInterface'
import { Result } from '@standardnotes/domain-core' import { Result } from '@standardnotes/domain-core'
export class ContactService export class ContactService extends AbstractService<ContactServiceEvent> implements ContactServiceInterface {
extends AbstractService<ContactServiceEvent>
implements ContactServiceInterface, InternalEventHandlerInterface
{
constructor( constructor(
private sync: SyncServiceInterface, private sync: SyncServiceInterface,
private mutator: MutatorClientInterface, private mutator: MutatorClientInterface,
@@ -43,48 +35,25 @@ export class ContactService
private _createOrEditContact: CreateOrEditContact, private _createOrEditContact: CreateOrEditContact,
private _editContact: EditContact, private _editContact: EditContact,
private _validateItemSigner: ValidateItemSigner, private _validateItemSigner: ValidateItemSigner,
private _sendOwnContactChangedMessage: SendOwnContactChangeMessage,
eventBus: InternalEventBusInterface, eventBus: InternalEventBusInterface,
) { ) {
super(eventBus) super(eventBus)
eventBus.addEventHandler(this, SessionEvent.UserKeyPairChanged)
} }
async handleEvent(event: InternalEventInterface): Promise<void> { override deinit(): void {
if (event.type === SessionEvent.UserKeyPairChanged) { super.deinit()
const data = event.payload as UserKeyPairChangedEventData ;(this.sync as unknown) = undefined
await this.selfContactManager.updateWithNewPublicKeySet({ ;(this.mutator as unknown) = undefined
encryption: data.current.encryption.publicKey, ;(this.session as unknown) = undefined
signing: data.current.signing.publicKey, ;(this.crypto as unknown) = undefined
}) ;(this.user as unknown) = undefined
void this.sendOwnContactChangeEventToAllContacts(event.payload as UserKeyPairChangedEventData) ;(this.selfContactManager as unknown) = undefined
} ;(this.encryption as unknown) = undefined
} ;(this._findContact as unknown) = undefined
;(this._getAllContacts as unknown) = undefined
private async sendOwnContactChangeEventToAllContacts(data: UserKeyPairChangedEventData): Promise<void> { ;(this._createOrEditContact as unknown) = undefined
if (!data.previous) { ;(this._editContact as unknown) = undefined
return ;(this._validateItemSigner as unknown) = undefined
}
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,
})
}
} }
getSelfContact(): TrustedContactInterface | undefined { getSelfContact(): TrustedContactInterface | undefined {
@@ -183,6 +152,8 @@ export class ContactService
): Promise<TrustedContactInterface> { ): Promise<TrustedContactInterface> {
const updatedContact = await this._editContact.execute(contact, params) const updatedContact = await this._editContact.execute(contact, params)
void this.sync.sync()
return updatedContact return updatedContact
} }
@@ -194,6 +165,9 @@ export class ContactService
isMe?: boolean isMe?: boolean
}): Promise<TrustedContactInterface | undefined> { }): Promise<TrustedContactInterface | undefined> {
const contact = await this._createOrEditContact.execute(params) const contact = await this._createOrEditContact.execute(params)
void this.sync.sync()
return contact return contact
} }
@@ -233,20 +207,4 @@ export class ContactService
getItemSignatureStatus(item: DecryptedItemInterface): ItemSignatureValidationResult { getItemSignatureStatus(item: DecryptedItemInterface): ItemSignatureValidationResult {
return this._validateItemSigner.execute(item) 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
}
} }

View File

@@ -17,9 +17,7 @@ import {
TrustedContactContent, TrustedContactContent,
TrustedContactContentSpecialized, TrustedContactContentSpecialized,
TrustedContactInterface, TrustedContactInterface,
PortablePublicKeySet,
} from '@standardnotes/models' } from '@standardnotes/models'
import { CreateOrEditContact } from './UseCase/CreateOrEditContact'
import { ContentType } from '@standardnotes/domain-core' import { ContentType } from '@standardnotes/domain-core'
const SelfContactName = 'Me' const SelfContactName = 'Me'
@@ -35,7 +33,6 @@ export class SelfContactManager implements InternalEventHandlerInterface {
items: ItemManagerInterface, items: ItemManagerInterface,
private session: SessionsClientInterface, private session: SessionsClientInterface,
private singletons: SingletonManagerInterface, private singletons: SingletonManagerInterface,
private createOrEditContact: CreateOrEditContact,
) { ) {
this.eventDisposers.push( this.eventDisposers.push(
sync.addEventObserver((event) => { 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() { private async reloadSelfContactAndCreateIfNecessary() {
if (!InternalFeatureService.get().isFeatureEnabled(InternalFeature.Vaults)) { if (!InternalFeatureService.get().isFeatureEnabled(InternalFeature.Vaults)) {
return return
@@ -146,6 +126,5 @@ export class SelfContactManager implements InternalEventHandlerInterface {
this.eventDisposers.forEach((disposer) => disposer()) this.eventDisposers.forEach((disposer) => disposer())
;(this.session as unknown) = undefined ;(this.session as unknown) = undefined
;(this.singletons as unknown) = undefined ;(this.singletons as unknown) = undefined
;(this.createOrEditContact as unknown) = undefined
} }
} }

View File

@@ -1,4 +1,3 @@
import { SyncServiceInterface } from '../../Sync/SyncServiceInterface'
import { MutatorClientInterface } from '../../Mutator/MutatorClientInterface' import { MutatorClientInterface } from '../../Mutator/MutatorClientInterface'
import { import {
ContactPublicKeySet, ContactPublicKeySet,
@@ -15,7 +14,6 @@ import { ContentType } from '@standardnotes/domain-core'
export class CreateOrEditContact { export class CreateOrEditContact {
constructor( constructor(
private mutator: MutatorClientInterface, private mutator: MutatorClientInterface,
private sync: SyncServiceInterface,
private findContact: FindContact, private findContact: FindContact,
private editContact: EditContact, private editContact: EditContact,
) {} ) {}
@@ -54,8 +52,6 @@ export class CreateOrEditContact {
true, true,
) )
await this.sync.sync()
return contact return contact
} }
} }

View File

@@ -1,12 +1,8 @@
import { SyncServiceInterface } from '../../Sync/SyncServiceInterface'
import { MutatorClientInterface } from '../../Mutator/MutatorClientInterface' import { MutatorClientInterface } from '../../Mutator/MutatorClientInterface'
import { TrustedContactInterface, TrustedContactMutator } from '@standardnotes/models' import { TrustedContactInterface, TrustedContactMutator } from '@standardnotes/models'
export class EditContact { export class EditContact {
constructor( constructor(private mutator: MutatorClientInterface) {}
private mutator: MutatorClientInterface,
private sync: SyncServiceInterface,
) {}
async execute( async execute(
contact: TrustedContactInterface, contact: TrustedContactInterface,
@@ -28,8 +24,6 @@ export class EditContact {
}, },
) )
await this.sync.sync()
return updatedContact return updatedContact
} }
} }

View File

@@ -8,16 +8,20 @@ export class FindContact implements SyncUseCaseInterface<TrustedContactInterface
execute(query: FindContactQuery): Result<TrustedContactInterface> { execute(query: FindContactQuery): Result<TrustedContactInterface> {
if ('userUuid' in query && query.userUuid) { if ('userUuid' in query && query.userUuid) {
const contact = this.items.itemsMatchingPredicate<TrustedContactInterface>( const contacts = this.items.itemsMatchingPredicate<TrustedContactInterface>(
ContentType.TYPES.TrustedContact, ContentType.TYPES.TrustedContact,
new Predicate<TrustedContactInterface>('contactUuid', '=', query.userUuid), new Predicate<TrustedContactInterface>('contactUuid', '=', query.userUuid),
)[0] )
if (contact) { if (contacts.length === 0) {
return Result.ok(contact) return Result.fail(`Contact not found for user ${query.userUuid}`)
} else {
return Result.fail('Contact not found')
} }
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) { if ('signingPublicKey' in query && query.signingPublicKey) {

View File

@@ -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: <PkcKeyPair>{
publicKey: 'new-encryption-public-key',
privateKey: 'new-encryption-private-key',
},
signing: <PkcKeyPair>{
publicKey: 'new-signing-public-key',
privateKey: 'new-signing-private-key',
},
},
previousKeys: {
encryption: <PkcKeyPair>{
publicKey: 'previous-encryption-public-key',
privateKey: 'previous-encryption-private-key',
},
signing: <PkcKeyPair>{
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<LoggerInterface>
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])
}
})
})

View File

@@ -1,34 +1,121 @@
import { InternalFeatureService } from './../../InternalFeatures/InternalFeatureService'
import { Result, UseCaseInterface } from '@standardnotes/domain-core' import { Result, UseCaseInterface } from '@standardnotes/domain-core'
import { PkcKeyPair } from '@standardnotes/sncrypto-common' import { PkcKeyPair } from '@standardnotes/sncrypto-common'
import { ReuploadAllInvites } from '../../VaultInvite/UseCase/ReuploadAllInvites' import { ReuploadAllInvites } from '../../VaultInvite/UseCase/ReuploadAllInvites'
import { ResendAllMessages } from '../../AsymmetricMessage/UseCase/ResendAllMessages' 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<void> { export class HandleKeyPairChange implements UseCaseInterface<void> {
constructor( constructor(
private reuploadAllInvites: ReuploadAllInvites, private selfContactManager: SelfContactManager,
private resendAllMessages: ResendAllMessages, 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: { async execute(dto: Dto): Promise<Result<void>> {
newKeys: { await this.updateSelfContact({
encryption: PkcKeyPair encryption: dto.newKeys.encryption.publicKey,
signing: PkcKeyPair signing: dto.newKeys.signing.publicKey,
}
previousKeys?: {
encryption: PkcKeyPair
signing: PkcKeyPair
}
}): Promise<Result<void>> {
await this.reuploadAllInvites.execute({
keys: dto.newKeys,
previousKeys: dto.previousKeys,
}) })
await this.resendAllMessages.execute({ const results = await Promise.all([
keys: dto.newKeys, this._reuploadAllInvites.execute({
previousKeys: dto.previousKeys, 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() 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<void> {
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,
})
}
}
} }

View File

@@ -36,12 +36,15 @@ import {
RootKeyParamsInterface, RootKeyParamsInterface,
} from '@standardnotes/models' } from '@standardnotes/models'
import { ClientDisplayableError } from '@standardnotes/responses' import { ClientDisplayableError } from '@standardnotes/responses'
import { extendArray } from '@standardnotes/utils' import { extendArray, LoggerInterface } from '@standardnotes/utils'
import { EncryptionService } from '../EncryptionService' import { EncryptionService } from '../EncryptionService'
import { ContentType } from '@standardnotes/domain-core' import { ContentType } from '@standardnotes/domain-core'
export class DecryptBackupFile { export class DecryptBackupFile {
constructor(private encryption: EncryptionService) {} constructor(
private encryption: EncryptionService,
private logger: LoggerInterface,
) {}
async execute( async execute(
file: BackupFile, file: BackupFile,
@@ -273,7 +276,7 @@ export class DecryptBackupFile {
errorDecrypting: true, errorDecrypting: true,
}), }),
) )
console.error('Error decrypting payload', encryptedPayload, e) this.logger.error('Error decrypting payload', encryptedPayload, e)
} }
} }

View File

@@ -2,7 +2,7 @@ import {
AsymmetricMessageServerHash, AsymmetricMessageServerHash,
SharedVaultInviteServerHash, SharedVaultInviteServerHash,
SharedVaultServerHash, SharedVaultServerHash,
UserEventServerHash, NotificationServerHash,
} from '@standardnotes/responses' } from '@standardnotes/responses'
/* istanbul ignore file */ /* istanbul ignore file */
@@ -31,11 +31,11 @@ export enum SyncEvent {
SyncRequestsIntegrityCheck = 'sync:requests-integrity-check', SyncRequestsIntegrityCheck = 'sync:requests-integrity-check',
ReceivedRemoteSharedVaults = 'received-shared-vaults', ReceivedRemoteSharedVaults = 'received-shared-vaults',
ReceivedSharedVaultInvites = 'received-shared-vault-invites', ReceivedSharedVaultInvites = 'received-shared-vault-invites',
ReceivedUserEvents = 'received-user-events', ReceivedNotifications = 'received-user-events',
ReceivedAsymmetricMessages = 'received-asymmetric-messages', ReceivedAsymmetricMessages = 'received-asymmetric-messages',
} }
export type SyncEventReceivedRemoteSharedVaultsData = SharedVaultServerHash[] export type SyncEventReceivedRemoteSharedVaultsData = SharedVaultServerHash[]
export type SyncEventReceivedSharedVaultInvitesData = SharedVaultInviteServerHash[] export type SyncEventReceivedSharedVaultInvitesData = SharedVaultInviteServerHash[]
export type SyncEventReceivedAsymmetricMessagesData = AsymmetricMessageServerHash[] export type SyncEventReceivedAsymmetricMessagesData = AsymmetricMessageServerHash[]
export type SyncEventReceivedUserEventsData = UserEventServerHash[] export type SyncEventReceivedNotificationsData = NotificationServerHash[]

View File

@@ -11,6 +11,7 @@ import { SyncServiceInterface } from '../Sync/SyncServiceInterface'
import { FileService } from './FileService' import { FileService } from './FileService'
import { BackupServiceInterface } from '@standardnotes/files' import { BackupServiceInterface } from '@standardnotes/files'
import { HttpServiceInterface } from '@standardnotes/api' import { HttpServiceInterface } from '@standardnotes/api'
import { LoggerInterface } from '@standardnotes/utils'
describe('fileService', () => { describe('fileService', () => {
let apiService: LegacyApiServiceInterface let apiService: LegacyApiServiceInterface
@@ -26,6 +27,8 @@ describe('fileService', () => {
let backupService: BackupServiceInterface let backupService: BackupServiceInterface
let http: HttpServiceInterface let http: HttpServiceInterface
let logger: LoggerInterface
beforeEach(() => { beforeEach(() => {
apiService = {} as jest.Mocked<LegacyApiServiceInterface> apiService = {} as jest.Mocked<LegacyApiServiceInterface>
apiService.addEventObserver = jest.fn() apiService.addEventObserver = jest.fn()
@@ -82,6 +85,9 @@ describe('fileService', () => {
backupService.readEncryptedFileFromBackup = jest.fn() backupService.readEncryptedFileFromBackup = jest.fn()
backupService.getFileBackupInfo = jest.fn() backupService.getFileBackupInfo = jest.fn()
logger = {} as jest.Mocked<LoggerInterface>
logger.info = jest.fn()
http = {} as jest.Mocked<HttpServiceInterface> http = {} as jest.Mocked<HttpServiceInterface>
fileService = new FileService( fileService = new FileService(
@@ -94,6 +100,7 @@ describe('fileService', () => {
alertService, alertService,
crypto, crypto,
internalEventBus, internalEventBus,
logger,
backupService, backupService,
) )

View File

@@ -19,7 +19,7 @@ import {
SharedVaultListingInterface, SharedVaultListingInterface,
} from '@standardnotes/models' } from '@standardnotes/models'
import { PureCryptoInterface } from '@standardnotes/sncrypto-common' 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 { SNItemsKey } from '@standardnotes/encryption'
import { import {
DownloadAndDecryptFileOperation, DownloadAndDecryptFileOperation,
@@ -47,7 +47,6 @@ import { InternalEventBusInterface } from '../Internal/InternalEventBusInterface
import { AbstractService } from '../Service/AbstractService' import { AbstractService } from '../Service/AbstractService'
import { SyncServiceInterface } from '../Sync/SyncServiceInterface' import { SyncServiceInterface } from '../Sync/SyncServiceInterface'
import { DecryptItemsKeyWithUserFallback } from '../Encryption/Functions' import { DecryptItemsKeyWithUserFallback } from '../Encryption/Functions'
import { log, LoggingDomain } from '../Logging'
import { SharedVaultServer, SharedVaultServerInterface, HttpServiceInterface } from '@standardnotes/api' import { SharedVaultServer, SharedVaultServerInterface, HttpServiceInterface } from '@standardnotes/api'
import { ContentType } from '@standardnotes/domain-core' import { ContentType } from '@standardnotes/domain-core'
import { EncryptionProviderInterface } from '../Encryption/EncryptionProviderInterface' import { EncryptionProviderInterface } from '../Encryption/EncryptionProviderInterface'
@@ -68,6 +67,7 @@ export class FileService extends AbstractService implements FilesClientInterface
private alertService: AlertService, private alertService: AlertService,
private crypto: PureCryptoInterface, private crypto: PureCryptoInterface,
protected override internalEventBus: InternalEventBusInterface, protected override internalEventBus: InternalEventBusInterface,
private logger: LoggerInterface,
private backupsService?: BackupServiceInterface, private backupsService?: BackupServiceInterface,
) { ) {
super(internalEventBus) super(internalEventBus)
@@ -317,19 +317,19 @@ export class FileService extends AbstractService implements FilesClientInterface
const fileBackup = await this.backupsService?.getFileBackupInfo(file) const fileBackup = await this.backupsService?.getFileBackupInfo(file)
if (this.backupsService && fileBackup) { 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) => { 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) return onDecryptedBytes(chunk.data, chunk.progress)
}) })
log(LoggingDomain.FilesService, 'Finished downloading file from backup') this.logger.info('Finished downloading file from backup')
return undefined return undefined
} else { } else {
log(LoggingDomain.FilesService, 'Downloading file from network') this.logger.info('Downloading file from network')
const addToCache = file.encryptedSize < this.encryptedCache.maxSize const addToCache = file.encryptedSize < this.encryptedCache.maxSize

View File

@@ -8,14 +8,16 @@ import { IntegrityApiInterface } from './IntegrityApiInterface'
import { IntegrityService } from './IntegrityService' import { IntegrityService } from './IntegrityService'
import { PayloadManagerInterface } from '../Payloads/PayloadManagerInterface' import { PayloadManagerInterface } from '../Payloads/PayloadManagerInterface'
import { IntegrityPayload } from '@standardnotes/responses' import { IntegrityPayload } from '@standardnotes/responses'
import { LoggerInterface } from '@standardnotes/utils'
describe('IntegrityService', () => { describe('IntegrityService', () => {
let integrityApi: IntegrityApiInterface let integrityApi: IntegrityApiInterface
let itemApi: ItemsServerInterface let itemApi: ItemsServerInterface
let payloadManager: PayloadManagerInterface let payloadManager: PayloadManagerInterface
let logger: LoggerInterface
let internalEventBus: InternalEventBusInterface let internalEventBus: InternalEventBusInterface
const createService = () => new IntegrityService(integrityApi, itemApi, payloadManager, internalEventBus) const createService = () => new IntegrityService(integrityApi, itemApi, payloadManager, logger, internalEventBus)
beforeEach(() => { beforeEach(() => {
integrityApi = {} as jest.Mocked<IntegrityApiInterface> integrityApi = {} as jest.Mocked<IntegrityApiInterface>
@@ -29,6 +31,10 @@ describe('IntegrityService', () => {
internalEventBus = {} as jest.Mocked<InternalEventBusInterface> internalEventBus = {} as jest.Mocked<InternalEventBusInterface>
internalEventBus.publishSync = jest.fn() internalEventBus.publishSync = jest.fn()
logger = {} as jest.Mocked<LoggerInterface>
logger.info = jest.fn()
logger.error = jest.fn()
}) })
it('should check integrity of payloads and publish mismatches', async () => { it('should check integrity of payloads and publish mismatches', async () => {
@@ -63,7 +69,7 @@ describe('IntegrityService', () => {
uuid: '1-2-3', uuid: '1-2-3',
}, },
], ],
source: "AfterDownloadFirst", source: 'AfterDownloadFirst',
}, },
type: 'IntegrityCheckCompleted', type: 'IntegrityCheckCompleted',
}, },
@@ -90,7 +96,7 @@ describe('IntegrityService', () => {
{ {
payload: { payload: {
rawPayloads: [], rawPayloads: [],
source: "AfterDownloadFirst", source: 'AfterDownloadFirst',
}, },
type: 'IntegrityCheckCompleted', type: 'IntegrityCheckCompleted',
}, },
@@ -140,7 +146,7 @@ describe('IntegrityService', () => {
{ {
payload: { payload: {
rawPayloads: [], rawPayloads: [],
source: "AfterDownloadFirst", source: 'AfterDownloadFirst',
}, },
type: 'IntegrityCheckCompleted', type: 'IntegrityCheckCompleted',
}, },

View File

@@ -10,6 +10,7 @@ import { SyncEvent } from '../Event/SyncEvent'
import { IntegrityEventPayload } from './IntegrityEventPayload' import { IntegrityEventPayload } from './IntegrityEventPayload'
import { SyncSource } from '../Sync/SyncSource' import { SyncSource } from '../Sync/SyncSource'
import { PayloadManagerInterface } from '../Payloads/PayloadManagerInterface' import { PayloadManagerInterface } from '../Payloads/PayloadManagerInterface'
import { LoggerInterface } from '@standardnotes/utils'
export class IntegrityService export class IntegrityService
extends AbstractService<IntegrityEvent, IntegrityEventPayload> extends AbstractService<IntegrityEvent, IntegrityEventPayload>
@@ -19,6 +20,7 @@ export class IntegrityService
private integrityApi: IntegrityApiInterface, private integrityApi: IntegrityApiInterface,
private itemApi: ItemsServerInterface, private itemApi: ItemsServerInterface,
private payloadManager: PayloadManagerInterface, private payloadManager: PayloadManagerInterface,
private logger: LoggerInterface,
protected override internalEventBus: InternalEventBusInterface, protected override internalEventBus: InternalEventBusInterface,
) { ) {
super(internalEventBus) super(internalEventBus)
@@ -31,7 +33,7 @@ export class IntegrityService
const integrityCheckResponse = await this.integrityApi.checkIntegrity(this.payloadManager.integrityPayloads) const integrityCheckResponse = await this.integrityApi.checkIntegrity(this.payloadManager.integrityPayloads)
if (isErrorResponse(integrityCheckResponse)) { 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 return
} }
@@ -50,7 +52,7 @@ export class IntegrityService
isErrorResponse(serverItemResponse) || isErrorResponse(serverItemResponse) ||
!('item' in serverItemResponse.data) !('item' in serverItemResponse.data)
) { ) {
this.log( this.logger.error(
`Could not obtain item for integrity adjustments: ${ `Could not obtain item for integrity adjustments: ${
isErrorResponse(serverItemResponse) ? serverItemResponse.data.error?.message : '' isErrorResponse(serverItemResponse) ? serverItemResponse.data.error?.message : ''
}`, }`,

View File

@@ -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, boolean> = {
[LoggingDomain.FilesService]: false,
[LoggingDomain.FilesBackups]: false,
}
const LoggingColor: Record<LoggingDomain, string> = {
[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)
}

View File

@@ -1,6 +1,6 @@
/* istanbul ignore file */ /* istanbul ignore file */
import { log, removeFromArray } from '@standardnotes/utils' import { removeFromArray } from '@standardnotes/utils'
import { EventObserver } from '../Event/EventObserver' import { EventObserver } from '../Event/EventObserver'
import { ApplicationServiceInterface } from './ApplicationServiceInterface' import { ApplicationServiceInterface } from './ApplicationServiceInterface'
import { InternalEventBusInterface } from '../Internal/InternalEventBusInterface' import { InternalEventBusInterface } from '../Internal/InternalEventBusInterface'
@@ -99,11 +99,4 @@ export abstract class AbstractService<EventName = string, EventData = unknown>
isApplicationService(): true { isApplicationService(): true {
return true return true
} }
log(..._args: unknown[]): void {
if (this.loggingEnabled) {
// eslint-disable-next-line prefer-rest-params
log(this.getServiceName(), ...arguments)
}
}
} }

View File

@@ -6,5 +6,4 @@ export interface ApplicationServiceInterface<E, D> extends ServiceDiagnostics {
addEventObserver(observer: EventObserver<E, D>): () => void addEventObserver(observer: EventObserver<E, D>): () => void
blockDeinit(): Promise<void> blockDeinit(): Promise<void>
deinit(): void deinit(): void
log(message: string, ...args: unknown[]): void
} }

View File

@@ -19,7 +19,7 @@ import { InternalEventBusInterface } from '../Internal/InternalEventBusInterface
import { SyncEvent } from '../Event/SyncEvent' import { SyncEvent } from '../Event/SyncEvent'
import { SessionEvent } from '../Session/SessionEvent' import { SessionEvent } from '../Session/SessionEvent'
import { InternalEventInterface } from '../Internal/InternalEventInterface' import { InternalEventInterface } from '../Internal/InternalEventInterface'
import { UserEventServiceEvent, UserEventServiceEventPayload } from '../UserEvent/UserEventServiceEvent' import { NotificationServiceEvent, NotificationServiceEventPayload } from '../UserEvent/NotificationServiceEvent'
import { DeleteThirdPartyVault } from './UseCase/DeleteExternalSharedVault' import { DeleteThirdPartyVault } from './UseCase/DeleteExternalSharedVault'
import { DeleteSharedVault } from './UseCase/DeleteSharedVault' import { DeleteSharedVault } from './UseCase/DeleteSharedVault'
import { VaultServiceEvent, VaultServiceEventPayload } from '../Vault/VaultServiceEvent' import { VaultServiceEvent, VaultServiceEventPayload } from '../Vault/VaultServiceEvent'
@@ -106,8 +106,8 @@ export class SharedVaultService
}) })
break break
} }
case UserEventServiceEvent.UserEventReceived: case NotificationServiceEvent.NotificationReceived:
await this.handleUserEvent(event.payload as UserEventServiceEventPayload) await this.handleUserEvent(event.payload as NotificationServiceEventPayload)
break break
case VaultServiceEvent.VaultRootKeyRotated: { case VaultServiceEvent.VaultRootKeyRotated: {
const payload = event.payload as VaultServiceEventPayload[VaultServiceEvent.VaultRootKeyRotated] const payload = event.payload as VaultServiceEventPayload[VaultServiceEvent.VaultRootKeyRotated]
@@ -120,7 +120,7 @@ export class SharedVaultService
} }
} }
private async handleUserEvent(event: UserEventServiceEventPayload): Promise<void> { private async handleUserEvent(event: NotificationServiceEventPayload): Promise<void> {
switch (event.eventPayload.props.type.value) { switch (event.eventPayload.props.type.value) {
case NotificationType.TYPES.RemovedFromSharedVault: { case NotificationType.TYPES.RemovedFromSharedVault: {
const vault = this._getVault.execute<SharedVaultListingInterface>({ const vault = this._getVault.execute<SharedVaultListingInterface>({

View File

@@ -20,7 +20,7 @@ export class ConvertToSharedVault {
const serverResult = await this.sharedVaultServer.createSharedVault() const serverResult = await this.sharedVaultServer.createSharedVault()
if (isErrorResponse(serverResult)) { 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 const serverVaultHash = serverResult.data.sharedVault

View File

@@ -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<NotificationServiceEvent, NotificationServiceEventPayload>
implements InternalEventHandlerInterface
{
private handledNotifications = new Set<string>()
constructor(internalEventBus: InternalEventBusInterface) {
super(internalEventBus)
internalEventBus.addEventHandler(this, SyncEvent.ReceivedNotifications)
}
async handleEvent(event: InternalEventInterface): Promise<void> {
if (event.type === SyncEvent.ReceivedNotifications) {
return this.handleReceivedNotifications(event.payload as SyncEventReceivedNotificationsData)
}
}
private async handleReceivedNotifications(notifications: NotificationServerHash[]): Promise<void> {
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)
}
}
}

View File

@@ -0,0 +1,9 @@
import { NotificationPayload } from '@standardnotes/domain-core'
export enum NotificationServiceEvent {
NotificationReceived = 'NotificationReceived',
}
export type NotificationServiceEventPayload = {
eventPayload: NotificationPayload
}

View File

@@ -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<UserEventServiceEvent, UserEventServiceEventPayload>
implements InternalEventHandlerInterface
{
constructor(internalEventBus: InternalEventBusInterface) {
super(internalEventBus)
internalEventBus.addEventHandler(this, SyncEvent.ReceivedUserEvents)
}
async handleEvent(event: InternalEventInterface): Promise<void> {
if (event.type === SyncEvent.ReceivedUserEvents) {
return this.handleReceivedUserEvents(event.payload as SyncEventReceivedUserEventsData)
}
}
private async handleReceivedUserEvents(userEvents: UserEventServerHash[]): Promise<void> {
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)
}
}
}

View File

@@ -1,9 +0,0 @@
import { NotificationPayload } from '@standardnotes/domain-core'
export enum UserEventServiceEvent {
UserEventReceived = 'UserEventReceived',
}
export type UserEventServiceEventPayload = {
eventPayload: NotificationPayload
}

View File

@@ -1,6 +1,5 @@
import { AcceptVaultInvite } from './UseCase/AcceptVaultInvite' import { AcceptVaultInvite } from './UseCase/AcceptVaultInvite'
import { SyncEvent, SyncEventReceivedSharedVaultInvitesData } from './../Event/SyncEvent' import { SyncEvent, SyncEventReceivedSharedVaultInvitesData } from './../Event/SyncEvent'
import { SessionEvent } from './../Session/SessionEvent'
import { InternalEventInterface } from './../Internal/InternalEventInterface' import { InternalEventInterface } from './../Internal/InternalEventInterface'
import { InternalEventHandlerInterface } from './../Internal/InternalEventHandlerInterface' import { InternalEventHandlerInterface } from './../Internal/InternalEventHandlerInterface'
import { ItemManagerInterface } from './../Item/ItemManagerInterface' import { ItemManagerInterface } from './../Item/ItemManagerInterface'
@@ -92,9 +91,6 @@ export class VaultInviteService
async handleEvent(event: InternalEventInterface): Promise<void> { async handleEvent(event: InternalEventInterface): Promise<void> {
switch (event.type) { switch (event.type) {
case SessionEvent.UserKeyPairChanged:
void this.invitesServer.deleteAllInboundInvites()
break
case SyncEvent.ReceivedSharedVaultInvites: case SyncEvent.ReceivedSharedVaultInvites:
await this.processInboundInvites(event.payload as SyncEventReceivedSharedVaultInvitesData) await this.processInboundInvites(event.payload as SyncEventReceivedSharedVaultInvitesData)
break break
@@ -238,6 +234,8 @@ export class VaultInviteService
} }
for (const invite of invites) { for (const invite of invites) {
delete this.pendingInvites[invite.uuid]
const sender = this._findContact.execute({ userUuid: invite.sender_uuid }) const sender = this._findContact.execute({ userUuid: invite.sender_uuid })
if (!sender.isFailed()) { if (!sender.isFailed()) {
const trustedMessage = this._getTrustedPayload.execute<AsymmetricMessageSharedVaultInvite>({ const trustedMessage = this._getTrustedPayload.execute<AsymmetricMessageSharedVaultInvite>({

View File

@@ -177,8 +177,8 @@ export * from './User/SignedOutEventPayload'
export * from './User/UserClientInterface' export * from './User/UserClientInterface'
export * from './User/UserClientInterface' export * from './User/UserClientInterface'
export * from './User/UserService' export * from './User/UserService'
export * from './UserEvent/UserEventService' export * from './UserEvent/NotificationService'
export * from './UserEvent/UserEventServiceEvent' export * from './UserEvent/NotificationServiceEvent'
export * from './VaultInvite/InviteRecord' export * from './VaultInvite/InviteRecord'
export * from './VaultInvite/UseCase/AcceptVaultInvite' export * from './VaultInvite/UseCase/AcceptVaultInvite'
export * from './VaultInvite/UseCase/InviteToVault' export * from './VaultInvite/UseCase/InviteToVault'

View File

@@ -73,7 +73,7 @@ import {
EncryptionProviderInterface, EncryptionProviderInterface,
VaultUserServiceInterface, VaultUserServiceInterface,
VaultInviteServiceInterface, VaultInviteServiceInterface,
UserEventServiceEvent, NotificationServiceEvent,
VaultServiceEvent, VaultServiceEvent,
VaultLockServiceInterface, VaultLockServiceInterface,
} from '@standardnotes/services' } from '@standardnotes/services'
@@ -116,6 +116,7 @@ import {
sleep, sleep,
UuidGenerator, UuidGenerator,
useBoolean, useBoolean,
LoggerInterface,
} from '@standardnotes/utils' } from '@standardnotes/utils'
import { UuidString, ApplicationEventPayload } from '../Types' import { UuidString, ApplicationEventPayload } from '../Types'
import { applicationEventForSyncEvent } from '@Lib/Application/Event' import { applicationEventForSyncEvent } from '@Lib/Application/Event'
@@ -504,7 +505,8 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
private beginAutoSyncTimer() { private beginAutoSyncTimer() {
this.autoSyncInterval = setInterval(() => { this.autoSyncInterval = setInterval(() => {
this.sync.log('Syncing from autosync') const logger = this.dependencies.get<LoggerInterface>(TYPES.Logger)
logger.info('Syncing from autosync')
void this.sync.sync({ sourceDescription: 'Auto Sync' }) void this.sync.sync({ sourceDescription: 'Auto Sync' })
}, DEFAULT_AUTO_SYNC_INTERVAL) }, DEFAULT_AUTO_SYNC_INTERVAL)
} }
@@ -807,7 +809,7 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
await promise await promise
} else { } else {
/** Await up to maxWait. If not resolved by then, return. */ /** 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() { private createBackgroundDependencies() {
this.dependencies.get(TYPES.UserEventService) this.dependencies.get(TYPES.NotificationService)
this.dependencies.get(TYPES.KeyRecoveryService) 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.SubscriptionManager), SessionEvent.Restored)
this.events.addEventHandler(this.dependencies.get(TYPES.VaultInviteService), SyncEvent.ReceivedSharedVaultInvites) 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), SessionEvent.UserKeyPairChanged)
this.events.addEventHandler( this.events.addEventHandler(
this.dependencies.get(TYPES.SharedVaultService), 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), VaultServiceEvent.VaultRootKeyRotated)
this.events.addEventHandler(this.dependencies.get(TYPES.SharedVaultService), SyncEvent.ReceivedRemoteSharedVaults) 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), this.dependencies.get(TYPES.AsymmetricMessageService),
SyncEvent.ReceivedAsymmetricMessages, SyncEvent.ReceivedAsymmetricMessages,
) )
this.events.addEventHandler(this.dependencies.get(TYPES.AsymmetricMessageService), SessionEvent.UserKeyPairChanged)
if (this.dependencies.get(TYPES.FilesBackupService)) { if (this.dependencies.get(TYPES.FilesBackupService)) {
this.events.addEventHandler( this.events.addEventHandler(

View File

@@ -52,7 +52,7 @@ import {
SelfContactManager, SelfContactManager,
StatusService, StatusService,
SubscriptionManager, SubscriptionManager,
UserEventService, NotificationService,
UserService, UserService,
ValidateItemSigner, ValidateItemSigner,
isDesktopDevice, isDesktopDevice,
@@ -147,7 +147,7 @@ import {
import { FullyResolvedApplicationOptions } from '../Options/ApplicationOptions' import { FullyResolvedApplicationOptions } from '../Options/ApplicationOptions'
import { TYPES } from './Types' import { TYPES } from './Types'
import { isDeinitable } from './isDeinitable' import { isDeinitable } from './isDeinitable'
import { isNotUndefined } from '@standardnotes/utils' import { Logger, isNotUndefined } from '@standardnotes/utils'
import { EncryptionOperators } from '@standardnotes/encryption' import { EncryptionOperators } from '@standardnotes/encryption'
export class Dependencies { export class Dependencies {
@@ -225,7 +225,7 @@ export class Dependencies {
}) })
this.factory.set(TYPES.DecryptBackupFile, () => { 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, () => { this.factory.set(TYPES.DiscardItemsLocally, () => {
@@ -254,7 +254,7 @@ export class Dependencies {
}) })
this.factory.set(TYPES.EditContact, () => { 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, () => { this.factory.set(TYPES.GetAllContacts, () => {
@@ -268,7 +268,6 @@ export class Dependencies {
this.factory.set(TYPES.CreateOrEditContact, () => { this.factory.set(TYPES.CreateOrEditContact, () => {
return new CreateOrEditContact( return new CreateOrEditContact(
this.get(TYPES.MutatorService), this.get(TYPES.MutatorService),
this.get(TYPES.SyncService),
this.get(TYPES.FindContact), this.get(TYPES.FindContact),
this.get(TYPES.EditContact), this.get(TYPES.EditContact),
) )
@@ -364,6 +363,7 @@ export class Dependencies {
this.factory.set(TYPES.ResendAllMessages, () => { this.factory.set(TYPES.ResendAllMessages, () => {
return new ResendAllMessages( return new ResendAllMessages(
this.get(TYPES.ResendMessage), this.get(TYPES.ResendMessage),
this.get(TYPES.DecryptOwnMessage),
this.get(TYPES.AsymmetricMessageServer), this.get(TYPES.AsymmetricMessageServer),
this.get(TYPES.FindContact), this.get(TYPES.FindContact),
) )
@@ -380,7 +380,17 @@ export class Dependencies {
}) })
this.factory.set(TYPES.HandleKeyPairChange, () => { 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, () => { this.factory.set(TYPES.NotifyVaultUsersOfKeyRotation, () => {
@@ -515,11 +525,7 @@ export class Dependencies {
}) })
this.factory.set(TYPES.ResendMessage, () => { this.factory.set(TYPES.ResendMessage, () => {
return new ResendMessage( return new ResendMessage(this.get(TYPES.SendMessage), this.get(TYPES.EncryptMessage))
this.get(TYPES.DecryptOwnMessage),
this.get(TYPES.SendMessage),
this.get(TYPES.EncryptMessage),
)
}) })
this.factory.set(TYPES.SendMessage, () => { this.factory.set(TYPES.SendMessage, () => {
@@ -613,6 +619,10 @@ export class Dependencies {
} }
private registerServiceMakers() { private registerServiceMakers() {
this.factory.set(TYPES.Logger, () => {
return new Logger(this.options.identifier)
})
this.factory.set(TYPES.UserServer, () => { this.factory.set(TYPES.UserServer, () => {
return new UserServer(this.get(TYPES.HttpService)) return new UserServer(this.get(TYPES.HttpService))
}) })
@@ -703,6 +713,7 @@ export class Dependencies {
this.get(TYPES.EncryptionService), this.get(TYPES.EncryptionService),
this.get(TYPES.MutatorService), this.get(TYPES.MutatorService),
this.get(TYPES.SessionManager), this.get(TYPES.SessionManager),
this.get(TYPES.SyncService),
this.get(TYPES.AsymmetricMessageServer), this.get(TYPES.AsymmetricMessageServer),
this.get(TYPES.CreateOrEditContact), this.get(TYPES.CreateOrEditContact),
this.get(TYPES.FindContact), this.get(TYPES.FindContact),
@@ -774,7 +785,6 @@ export class Dependencies {
this.get(TYPES.ItemManager), this.get(TYPES.ItemManager),
this.get(TYPES.SessionManager), this.get(TYPES.SessionManager),
this.get(TYPES.SingletonManager), this.get(TYPES.SingletonManager),
this.get(TYPES.CreateOrEditContact),
) )
}) })
@@ -793,7 +803,6 @@ export class Dependencies {
this.get(TYPES.CreateOrEditContact), this.get(TYPES.CreateOrEditContact),
this.get(TYPES.EditContact), this.get(TYPES.EditContact),
this.get(TYPES.ValidateItemSigner), this.get(TYPES.ValidateItemSigner),
this.get(TYPES.SendOwnContactChangeMessage),
this.get(TYPES.InternalEventBus), this.get(TYPES.InternalEventBus),
) )
}) })
@@ -921,6 +930,7 @@ export class Dependencies {
this.get(TYPES.LegacyApiService), this.get(TYPES.LegacyApiService),
this.get(TYPES.LegacyApiService), this.get(TYPES.LegacyApiService),
this.get(TYPES.PayloadManager), this.get(TYPES.PayloadManager),
this.get(TYPES.Logger),
this.get(TYPES.InternalEventBus), this.get(TYPES.InternalEventBus),
) )
}) })
@@ -936,6 +946,7 @@ export class Dependencies {
this.get(TYPES.AlertService), this.get(TYPES.AlertService),
this.get(TYPES.Crypto), this.get(TYPES.Crypto),
this.get(TYPES.InternalEventBus), this.get(TYPES.InternalEventBus),
this.get(TYPES.Logger),
this.get(TYPES.FilesBackupService), this.get(TYPES.FilesBackupService),
) )
}) })
@@ -1014,6 +1025,7 @@ export class Dependencies {
this.options.environment, this.options.environment,
this.options.platform, this.options.platform,
this.get(TYPES.DeviceInterface), this.get(TYPES.DeviceInterface),
this.get(TYPES.Logger),
this.get(TYPES.InternalEventBus), this.get(TYPES.InternalEventBus),
) )
}) })
@@ -1032,6 +1044,7 @@ export class Dependencies {
this.get(TYPES.AlertService), this.get(TYPES.AlertService),
this.get(TYPES.SessionManager), this.get(TYPES.SessionManager),
this.get(TYPES.Crypto), this.get(TYPES.Crypto),
this.get(TYPES.Logger),
this.get(TYPES.InternalEventBus), this.get(TYPES.InternalEventBus),
) )
}) })
@@ -1120,6 +1133,7 @@ export class Dependencies {
loadBatchSize: this.options.loadBatchSize, loadBatchSize: this.options.loadBatchSize,
sleepBetweenBatches: this.options.sleepBetweenBatches, sleepBetweenBatches: this.options.sleepBetweenBatches,
}, },
this.get(TYPES.Logger),
this.get(TYPES.InternalEventBus), this.get(TYPES.InternalEventBus),
) )
}) })
@@ -1198,7 +1212,7 @@ export class Dependencies {
}) })
this.factory.set(TYPES.PayloadManager, () => { 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, () => { this.factory.set(TYPES.ItemManager, () => {
@@ -1222,8 +1236,8 @@ export class Dependencies {
) )
}) })
this.factory.set(TYPES.UserEventService, () => { this.factory.set(TYPES.NotificationService, () => {
return new UserEventService(this.get(TYPES.InternalEventBus)) return new NotificationService(this.get(TYPES.InternalEventBus))
}) })
this.factory.set(TYPES.InMemoryStore, () => { this.factory.set(TYPES.InMemoryStore, () => {
@@ -1278,7 +1292,7 @@ export class Dependencies {
}) })
this.factory.set(TYPES.HttpService, () => { 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, () => { this.factory.set(TYPES.LegacyApiService, () => {

View File

@@ -10,7 +10,7 @@ export const TYPES = {
ItemManager: Symbol.for('ItemManager'), ItemManager: Symbol.for('ItemManager'),
MutatorService: Symbol.for('MutatorService'), MutatorService: Symbol.for('MutatorService'),
DiskStorageService: Symbol.for('DiskStorageService'), DiskStorageService: Symbol.for('DiskStorageService'),
UserEventService: Symbol.for('UserEventService'), NotificationService: Symbol.for('NotificationService'),
InMemoryStore: Symbol.for('InMemoryStore'), InMemoryStore: Symbol.for('InMemoryStore'),
KeySystemKeyManager: Symbol.for('KeySystemKeyManager'), KeySystemKeyManager: Symbol.for('KeySystemKeyManager'),
EncryptionService: Symbol.for('EncryptionService'), EncryptionService: Symbol.for('EncryptionService'),
@@ -63,6 +63,7 @@ export const TYPES = {
VaultInviteService: Symbol.for('VaultInviteService'), VaultInviteService: Symbol.for('VaultInviteService'),
VaultUserCache: Symbol.for('VaultUserCache'), VaultUserCache: Symbol.for('VaultUserCache'),
VaultLockService: Symbol.for('VaultLockService'), VaultLockService: Symbol.for('VaultLockService'),
Logger: Symbol.for('Logger'),
// Servers // Servers
RevisionServer: Symbol.for('RevisionServer'), RevisionServer: Symbol.for('RevisionServer'),

View File

@@ -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, boolean> = {
[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)
}

View File

@@ -51,10 +51,6 @@ const SubscriptionPaths = {
subscriptionTokens: '/v1/subscription-tokens', subscriptionTokens: '/v1/subscription-tokens',
} }
const SubscriptionPathsV2 = {
subscriptions: '/v2/subscriptions',
}
const UserPathsV2 = { const UserPathsV2 = {
keyParams: '/v2/login-params', keyParams: '/v2/login-params',
signIn: '/v2/login', signIn: '/v2/login',
@@ -75,7 +71,6 @@ export const Paths = {
...UserPaths, ...UserPaths,
}, },
v2: { v2: {
...SubscriptionPathsV2,
...UserPathsV2, ...UserPathsV2,
}, },
} }

View File

@@ -13,6 +13,7 @@ import { ItemManager } from '@Lib/Services/Items/ItemManager'
import { FeaturesService } from '@Lib/Services/Features/FeaturesService' import { FeaturesService } from '@Lib/Services/Features/FeaturesService'
import { SNComponentManager } from './ComponentManager' import { SNComponentManager } from './ComponentManager'
import { SyncService } from '../Sync/SyncService' import { SyncService } from '../Sync/SyncService'
import { LoggerInterface } from '@standardnotes/utils'
describe('featuresService', () => { describe('featuresService', () => {
let items: ItemManagerInterface let items: ItemManagerInterface
@@ -23,6 +24,7 @@ describe('featuresService', () => {
let prefs: PreferenceServiceInterface let prefs: PreferenceServiceInterface
let eventBus: InternalEventBusInterface let eventBus: InternalEventBusInterface
let device: DeviceInterface let device: DeviceInterface
let logger: LoggerInterface
const createManager = (environment: Environment, platform: Platform) => { const createManager = (environment: Environment, platform: Platform) => {
const manager = new SNComponentManager( const manager = new SNComponentManager(
@@ -35,6 +37,7 @@ describe('featuresService', () => {
environment, environment,
platform, platform,
device, device,
logger,
eventBus, eventBus,
) )
@@ -46,6 +49,8 @@ describe('featuresService', () => {
addEventListener: jest.fn(), addEventListener: jest.fn(),
attachEvent: jest.fn(), attachEvent: jest.fn(),
} as unknown as Window & typeof globalThis } as unknown as Window & typeof globalThis
logger = {} as jest.Mocked<LoggerInterface>
logger.info = jest.fn()
sync = {} as jest.Mocked<SyncService> sync = {} as jest.Mocked<SyncService>
sync.sync = jest.fn() sync.sync = jest.fn()

View File

@@ -29,7 +29,7 @@ import {
GetNativeThemes, GetNativeThemes,
NativeFeatureIdentifier, NativeFeatureIdentifier,
} from '@standardnotes/features' } 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 { ComponentViewer } from '@Lib/Services/ComponentManager/ComponentViewer'
import { import {
AbstractService, AbstractService,
@@ -98,6 +98,7 @@ export class SNComponentManager
private environment: Environment, private environment: Environment,
private platform: Platform, private platform: Platform,
private device: DeviceInterface, private device: DeviceInterface,
private logger: LoggerInterface,
protected override internalEventBus: InternalEventBusInterface, protected override internalEventBus: InternalEventBusInterface,
) { ) {
super(internalEventBus) super(internalEventBus)
@@ -177,6 +178,7 @@ export class SNComponentManager
alerts: this.alerts, alerts: this.alerts,
preferences: this.preferences, preferences: this.preferences,
features: this.features, features: this.features,
logger: this.logger,
}, },
{ {
url: this.urlForFeature(component) ?? '', url: this.urlForFeature(component) ?? '',
@@ -312,7 +314,7 @@ export class SNComponentManager
onWindowMessage = (event: MessageEvent): void => { onWindowMessage = (event: MessageEvent): void => {
const data = event.data as ComponentMessage const data = event.data as ComponentMessage
if (data.sessionKey) { if (data.sessionKey) {
this.log('Component manager received message', data) this.logger.info('Component manager received message', data)
this.componentViewerForSessionKey(data.sessionKey)?.handleMessage(data) this.componentViewerForSessionKey(data.sessionKey)?.handleMessage(data)
} }
} }
@@ -369,7 +371,7 @@ export class SNComponentManager
} }
public async toggleTheme(uiFeature: UIFeature<ThemeFeatureDescription>): Promise<void> { public async toggleTheme(uiFeature: UIFeature<ThemeFeatureDescription>): Promise<void> {
this.log('Toggling theme', uiFeature.uniqueIdentifier) this.logger.info('Toggling theme', uiFeature.uniqueIdentifier)
if (this.isThemeActive(uiFeature)) { if (this.isThemeActive(uiFeature)) {
await this.removeActiveTheme(uiFeature) await this.removeActiveTheme(uiFeature)
@@ -449,7 +451,7 @@ export class SNComponentManager
} }
public async toggleComponent(component: ComponentInterface): Promise<void> { public async toggleComponent(component: ComponentInterface): Promise<void> {
this.log('Toggling component', component.uuid) this.logger.info('Toggling component', component.uuid)
if (this.isComponentActive(component)) { if (this.isComponentActive(component)) {
await this.removeActiveComponent(component) await this.removeActiveComponent(component)

View File

@@ -65,13 +65,13 @@ import {
extendArray, extendArray,
Copy, Copy,
removeFromArray, removeFromArray,
log,
nonSecureRandomIdentifier, nonSecureRandomIdentifier,
UuidGenerator, UuidGenerator,
Uuids, Uuids,
sureSearchArray, sureSearchArray,
isNotUndefined, isNotUndefined,
uniqueArray, uniqueArray,
LoggerInterface,
} from '@standardnotes/utils' } from '@standardnotes/utils'
import { ContentType, Uuid } from '@standardnotes/domain-core' import { ContentType, Uuid } from '@standardnotes/domain-core'
@@ -80,7 +80,6 @@ export class ComponentViewer implements ComponentViewerInterface {
private streamContextItemOriginalMessage?: ComponentMessage private streamContextItemOriginalMessage?: ComponentMessage
private streamItemsOriginalMessage?: ComponentMessage private streamItemsOriginalMessage?: ComponentMessage
private removeItemObserver: () => void private removeItemObserver: () => void
private loggingEnabled = false
public identifier = nonSecureRandomIdentifier() public identifier = nonSecureRandomIdentifier()
private actionObservers: ActionObserver[] = [] private actionObservers: ActionObserver[] = []
@@ -102,6 +101,7 @@ export class ComponentViewer implements ComponentViewerInterface {
alerts: AlertService alerts: AlertService
preferences: PreferenceServiceInterface preferences: PreferenceServiceInterface
features: FeaturesService features: FeaturesService
logger: LoggerInterface
}, },
private options: { private options: {
item: ComponentViewerItem item: ComponentViewerItem
@@ -143,7 +143,7 @@ export class ComponentViewer implements ComponentViewerInterface {
} }
}) })
this.log('Constructor', this) this.services.logger.info('Constructor', this)
} }
public getComponentOrFeatureItem(): UIFeature<IframeComponentFeatureDescription> { public getComponentOrFeatureItem(): UIFeature<IframeComponentFeatureDescription> {
@@ -163,7 +163,7 @@ export class ComponentViewer implements ComponentViewerInterface {
} }
public destroy(): void { public destroy(): void {
this.log('Destroying', this) this.services.logger.info('Destroying', this)
this.deinit() this.deinit()
} }
@@ -347,7 +347,7 @@ export class ComponentViewer implements ComponentViewerInterface {
this.componentUniqueIdentifier.value, this.componentUniqueIdentifier.value,
requiredContextPermissions, requiredContextPermissions,
() => { () => {
this.log( this.services.logger.info(
'Send context item in reply', 'Send context item in reply',
'component:', 'component:',
this.componentOrFeature, 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( private sendItemsInReply(
items: (DecryptedItemInterface | DeletedItemInterface)[], items: (DecryptedItemInterface | DeletedItemInterface)[],
message: ComponentMessage, message: ComponentMessage,
source?: PayloadEmitSource, source?: PayloadEmitSource,
): void { ): 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 = {} const responseData: MessageReplyData = {}
@@ -453,10 +447,14 @@ export class ComponentViewer implements ComponentViewerInterface {
*/ */
private sendMessage(message: ComponentMessage | MessageReply, essential = true): void { private sendMessage(message: ComponentMessage | MessageReply, essential = true): void {
if (!this.window && message.action === ComponentAction.Reply) { 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 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 (!this.window) {
if (essential) { 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.') 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.window = window
this.sessionKey = UuidGenerator.GenerateUuid() 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() this.postActiveThemes()
} }
@@ -557,9 +555,9 @@ export class ComponentViewer implements ComponentViewerInterface {
} }
handleMessage(message: ComponentMessage): void { handleMessage(message: ComponentMessage): void {
this.log('Handle message', message, this) this.services.logger.info('Handle message', message, this)
if (!this.componentOrFeature) { 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( void this.services.alerts.alert(
'A component is trying to communicate with Standard Notes, ' + 'A component is trying to communicate with Standard Notes, ' +
'but there is an error establishing a bridge. Please restart the app and try again.', 'but there is an error establishing a bridge. Please restart the app and try again.',

View File

@@ -27,6 +27,7 @@ import { LegacyApiService, SessionManager } from '../Api'
import { ItemManager } from '../Items' import { ItemManager } from '../Items'
import { DiskStorageService } from '../Storage/DiskStorageService' import { DiskStorageService } from '../Storage/DiskStorageService'
import { SettingsClientInterface } from '../Settings/SettingsClientInterface' import { SettingsClientInterface } from '../Settings/SettingsClientInterface'
import { LoggerInterface } from '@standardnotes/utils'
describe('FeaturesService', () => { describe('FeaturesService', () => {
let storageService: StorageServiceInterface let storageService: StorageServiceInterface
@@ -45,26 +46,12 @@ describe('FeaturesService', () => {
let items: ItemInterface[] let items: ItemInterface[]
let internalEventBus: InternalEventBusInterface let internalEventBus: InternalEventBusInterface
let featureService: FeaturesService let featureService: FeaturesService
let logger: LoggerInterface
const createService = () => {
return new FeaturesService(
storageService,
itemManager,
mutator,
subscriptions,
apiService,
webSocketsService,
settingsService,
userService,
syncService,
alertService,
sessionManager,
crypto,
internalEventBus,
)
}
beforeEach(() => { beforeEach(() => {
logger = {} as jest.Mocked<LoggerInterface>
logger.info = jest.fn()
roles = [RoleName.NAMES.CoreUser, RoleName.NAMES.PlusUser] roles = [RoleName.NAMES.CoreUser, RoleName.NAMES.PlusUser]
items = [] as jest.Mocked<ItemInterface[]> items = [] as jest.Mocked<ItemInterface[]>
@@ -133,6 +120,7 @@ describe('FeaturesService', () => {
alertService, alertService,
sessionManager, sessionManager,
crypto, crypto,
logger,
internalEventBus, internalEventBus,
) )
}) })
@@ -199,6 +187,25 @@ describe('FeaturesService', () => {
describe('loadUserRoles()', () => { describe('loadUserRoles()', () => {
it('retrieves user roles and features from storage', async () => { 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() createService().initializeFromDisk()
expect(storageService.getValue).toHaveBeenCalledWith(StorageKey.UserRoles, undefined, []) expect(storageService.getValue).toHaveBeenCalledWith(StorageKey.UserRoles, undefined, [])
}) })

View File

@@ -1,5 +1,5 @@
import { MigrateFeatureRepoToUserSettingUseCase } from './UseCase/MigrateFeatureRepoToUserSetting' 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 { ClientDisplayableError } from '@standardnotes/responses'
import { RoleName, ContentType, Uuid } from '@standardnotes/domain-core' import { RoleName, ContentType, Uuid } from '@standardnotes/domain-core'
import { PROD_OFFLINE_FEATURES_URL } from '../../Hosts' import { PROD_OFFLINE_FEATURES_URL } from '../../Hosts'
@@ -81,6 +81,7 @@ export class FeaturesService
private alerts: AlertService, private alerts: AlertService,
private sessions: SessionsClientInterface, private sessions: SessionsClientInterface,
private crypto: PureCryptoInterface, private crypto: PureCryptoInterface,
private logger: LoggerInterface,
protected override internalEventBus: InternalEventBusInterface, protected override internalEventBus: InternalEventBusInterface,
) { ) {
super(internalEventBus) super(internalEventBus)
@@ -146,7 +147,7 @@ export class FeaturesService
switch (event.type) { switch (event.type) {
case ApiServiceEvent.MetaReceived: { case ApiServiceEvent.MetaReceived: {
if (!this.sync) { 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 return
} }

View File

@@ -2,7 +2,7 @@ import { ContentType } from '@standardnotes/domain-core'
import { AlertService, InternalEventBusInterface, ItemRelationshipDirection } from '@standardnotes/services' import { AlertService, InternalEventBusInterface, ItemRelationshipDirection } from '@standardnotes/services'
import { ItemManager } from './ItemManager' import { ItemManager } from './ItemManager'
import { PayloadManager } from '../Payloads/PayloadManager' 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 * as Models from '@standardnotes/models'
import { import {
DecryptedPayload, DecryptedPayload,
@@ -48,14 +48,18 @@ describe('itemManager', () => {
let payloadManager: PayloadManager let payloadManager: PayloadManager
let itemManager: ItemManager let itemManager: ItemManager
let internalEventBus: InternalEventBusInterface let internalEventBus: InternalEventBusInterface
let logger: LoggerInterface
beforeEach(() => { beforeEach(() => {
setupRandomUuid() setupRandomUuid()
logger = {} as jest.Mocked<LoggerInterface>
logger.debug = jest.fn()
internalEventBus = {} as jest.Mocked<InternalEventBusInterface> internalEventBus = {} as jest.Mocked<InternalEventBusInterface>
internalEventBus.publish = jest.fn() internalEventBus.publish = jest.fn()
payloadManager = new PayloadManager(internalEventBus) payloadManager = new PayloadManager(logger, internalEventBus)
itemManager = new ItemManager(payloadManager, internalEventBus) itemManager = new ItemManager(payloadManager, internalEventBus)
mutator = new MutatorService(itemManager, payloadManager, {} as jest.Mocked<AlertService>, internalEventBus) mutator = new MutatorService(itemManager, payloadManager, {} as jest.Mocked<AlertService>, internalEventBus)

View File

@@ -11,7 +11,7 @@ import {
import { ContentType } from '@standardnotes/domain-core' import { ContentType } from '@standardnotes/domain-core'
import { AlertService, InternalEventBusInterface } from '@standardnotes/services' import { AlertService, InternalEventBusInterface } from '@standardnotes/services'
import { MutatorService, PayloadManager, ItemManager } from '../' import { MutatorService, PayloadManager, ItemManager } from '../'
import { UuidGenerator, sleep } from '@standardnotes/utils' import { UuidGenerator, sleep, LoggerInterface } from '@standardnotes/utils'
const setupRandomUuid = () => { const setupRandomUuid = () => {
UuidGenerator.SetGenerator(() => String(Math.random())) UuidGenerator.SetGenerator(() => String(Math.random()))
@@ -23,13 +23,17 @@ describe('mutator service', () => {
let itemManager: ItemManager let itemManager: ItemManager
let internalEventBus: InternalEventBusInterface let internalEventBus: InternalEventBusInterface
let logger: LoggerInterface
beforeEach(() => { beforeEach(() => {
setupRandomUuid() setupRandomUuid()
internalEventBus = {} as jest.Mocked<InternalEventBusInterface> internalEventBus = {} as jest.Mocked<InternalEventBusInterface>
internalEventBus.publish = jest.fn() internalEventBus.publish = jest.fn()
payloadManager = new PayloadManager(internalEventBus) logger = {} as jest.Mocked<LoggerInterface>
logger.debug = jest.fn()
payloadManager = new PayloadManager(logger, internalEventBus)
itemManager = new ItemManager(payloadManager, internalEventBus) itemManager = new ItemManager(payloadManager, internalEventBus)
const alerts = {} as jest.Mocked<AlertService> const alerts = {} as jest.Mocked<AlertService>

View File

@@ -8,16 +8,21 @@ import {
import { PayloadManager } from './PayloadManager' import { PayloadManager } from './PayloadManager'
import { InternalEventBusInterface } from '@standardnotes/services' import { InternalEventBusInterface } from '@standardnotes/services'
import { ContentType } from '@standardnotes/domain-core' import { ContentType } from '@standardnotes/domain-core'
import { LoggerInterface } from '@standardnotes/utils'
describe('payload manager', () => { describe('payload manager', () => {
let payloadManager: PayloadManager let payloadManager: PayloadManager
let internalEventBus: InternalEventBusInterface let internalEventBus: InternalEventBusInterface
let logger: LoggerInterface
beforeEach(() => { beforeEach(() => {
internalEventBus = {} as jest.Mocked<InternalEventBusInterface> internalEventBus = {} as jest.Mocked<InternalEventBusInterface>
internalEventBus.publish = jest.fn() internalEventBus.publish = jest.fn()
payloadManager = new PayloadManager(internalEventBus) logger = {} as jest.Mocked<LoggerInterface>
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 () => { it('emitting a payload should emit as-is and not merge on top of existing payload', async () => {

View File

@@ -1,6 +1,6 @@
import { ContentType } from '@standardnotes/domain-core' import { ContentType } from '@standardnotes/domain-core'
import { PayloadsChangeObserver, QueueElement, PayloadsChangeObserverCallback, EmitQueue } from './Types' import { PayloadsChangeObserver, QueueElement, PayloadsChangeObserverCallback, EmitQueue } from './Types'
import { removeFromArray, Uuids } from '@standardnotes/utils' import { LoggerInterface, removeFromArray, Uuids } from '@standardnotes/utils'
import { import {
DeltaFileImport, DeltaFileImport,
isDeletedPayload, isDeletedPayload,
@@ -42,7 +42,10 @@ export class PayloadManager extends AbstractService implements PayloadManagerInt
public collection: PayloadCollection<FullyFormedPayloadInterface> public collection: PayloadCollection<FullyFormedPayloadInterface>
private emitQueue: EmitQueue<FullyFormedPayloadInterface> = [] private emitQueue: EmitQueue<FullyFormedPayloadInterface> = []
constructor(protected override internalEventBus: InternalEventBusInterface) { constructor(
private logger: LoggerInterface,
protected override internalEventBus: InternalEventBusInterface,
) {
super(internalEventBus) super(internalEventBus)
this.collection = new PayloadCollection() this.collection = new PayloadCollection()
} }
@@ -183,7 +186,7 @@ export class PayloadManager extends AbstractService implements PayloadManagerInt
continue continue
} }
this.log( this.logger.debug(
'applying payload', 'applying payload',
apply.uuid, apply.uuid,
'globalDirtyIndexAtLastSync', 'globalDirtyIndexAtLastSync',

View File

@@ -7,7 +7,7 @@ import {
HttpResponse, HttpResponse,
isErrorResponse, isErrorResponse,
RawSyncResponse, RawSyncResponse,
UserEventServerHash, NotificationServerHash,
AsymmetricMessageServerHash, AsymmetricMessageServerHash,
getErrorFromErrorResponse, getErrorFromErrorResponse,
} from '@standardnotes/responses' } from '@standardnotes/responses'
@@ -29,7 +29,7 @@ export class ServerSyncResponse {
readonly asymmetricMessages: AsymmetricMessageServerHash[] readonly asymmetricMessages: AsymmetricMessageServerHash[]
readonly vaults: SharedVaultServerHash[] readonly vaults: SharedVaultServerHash[]
readonly vaultInvites: SharedVaultInviteServerHash[] readonly vaultInvites: SharedVaultInviteServerHash[]
readonly userEvents: UserEventServerHash[] readonly userEvents: NotificationServerHash[]
private readonly rawConflictObjects: ConflictParams[] private readonly rawConflictObjects: ConflictParams[]

View File

@@ -1,7 +1,7 @@
import { ConflictParams, ConflictType } from '@standardnotes/responses' import { ConflictParams, ConflictType } from '@standardnotes/responses'
import { log, LoggingDomain } from './../../Logging'
import { AccountSyncOperation } from '@Lib/Services/Sync/Account/Operation' import { AccountSyncOperation } from '@Lib/Services/Sync/Account/Operation'
import { import {
LoggerInterface,
Uuids, Uuids,
extendArray, extendArray,
isNotUndefined, isNotUndefined,
@@ -80,7 +80,7 @@ import {
isChunkFullEntry, isChunkFullEntry,
SyncEventReceivedSharedVaultInvitesData, SyncEventReceivedSharedVaultInvitesData,
SyncEventReceivedRemoteSharedVaultsData, SyncEventReceivedRemoteSharedVaultsData,
SyncEventReceivedUserEventsData, SyncEventReceivedNotificationsData,
SyncEventReceivedAsymmetricMessagesData, SyncEventReceivedAsymmetricMessagesData,
SyncOpStatus, SyncOpStatus,
} from '@standardnotes/services' } from '@standardnotes/services'
@@ -160,6 +160,7 @@ export class SyncService
private device: DeviceInterface, private device: DeviceInterface,
private identifier: string, private identifier: string,
private readonly options: ApplicationSyncOptions, private readonly options: ApplicationSyncOptions,
private logger: LoggerInterface,
protected override internalEventBus: InternalEventBusInterface, protected override internalEventBus: InternalEventBusInterface,
) { ) {
super(internalEventBus) super(internalEventBus)
@@ -258,7 +259,7 @@ export class SyncService
} }
public async loadDatabasePayloads(): Promise<void> { public async loadDatabasePayloads(): Promise<void> {
log(LoggingDomain.DatabaseLoad, 'Loading database payloads') this.logger.debug('Loading database payloads')
if (this.databaseLoaded) { if (this.databaseLoaded) {
throw 'Attempting to initialize already initialized local database.' throw 'Attempting to initialize already initialized local database.'
@@ -353,7 +354,7 @@ export class SyncService
currentPosition?: number, currentPosition?: number,
payloadCount?: 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 encrypted: EncryptedPayloadInterface[] = []
const nonencrypted: (DecryptedPayloadInterface | DeletedPayloadInterface)[] = [] const nonencrypted: (DecryptedPayloadInterface | DeletedPayloadInterface)[] = []
@@ -419,7 +420,7 @@ export class SyncService
} }
public async markAllItemsAsNeedingSyncAndPersist(): Promise<void> { public async markAllItemsAsNeedingSyncAndPersist(): Promise<void> {
log(LoggingDomain.Sync, 'Marking all items as needing sync') this.logger.debug('Marking all items as needing sync')
const items = this.itemManager.items const items = this.itemManager.items
const payloads = items.map((item) => { const payloads = items.map((item) => {
@@ -485,7 +486,7 @@ export class SyncService
const promise = this.spawnQueue[0] const promise = this.spawnQueue[0]
removeFromIndex(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({ return this.sync({
queueStrategy: SyncQueueStrategy.ForceSpawnNew, queueStrategy: SyncQueueStrategy.ForceSpawnNew,
@@ -547,7 +548,7 @@ export class SyncService
public async sync(options: Partial<SyncOptions> = {}): Promise<unknown> { public async sync(options: Partial<SyncOptions> = {}): Promise<unknown> {
if (this.clientLocked) { if (this.clientLocked) {
log(LoggingDomain.Sync, 'Sync locked by client') this.logger.debug('Sync locked by client')
return return
} }
@@ -613,8 +614,7 @@ export class SyncService
if (shouldExecuteSync) { if (shouldExecuteSync) {
this.syncLock = true this.syncLock = true
} else { } else {
log( this.logger.debug(
LoggingDomain.Sync,
!canExecuteSync !canExecuteSync
? 'Another function call has begun preparing for sync.' ? 'Another function call has begun preparing for sync.'
: syncInProgress : syncInProgress
@@ -727,8 +727,7 @@ export class SyncService
payloads: (DeletedPayloadInterface | DecryptedPayloadInterface)[], payloads: (DeletedPayloadInterface | DecryptedPayloadInterface)[],
options: SyncOptions, options: SyncOptions,
) { ) {
log( this.logger.debug(
LoggingDomain.Sync,
'Syncing offline user', 'Syncing offline user',
'source:', 'source:',
SyncSource[options.source], SyncSource[options.source],
@@ -812,8 +811,7 @@ export class SyncService
}, },
) )
log( this.logger.debug(
LoggingDomain.Sync,
'Syncing online user', 'Syncing online user',
'source', 'source',
SyncSource[options.source], SyncSource[options.source],
@@ -925,7 +923,7 @@ export class SyncService
} }
private async handleOfflineResponse(response: OfflineSyncResponse) { private async handleOfflineResponse(response: OfflineSyncResponse) {
log(LoggingDomain.Sync, 'Offline Sync Response', response) this.logger.debug('Offline Sync Response', response)
const masterCollection = this.payloadManager.getMasterCollection() const masterCollection = this.payloadManager.getMasterCollection()
@@ -943,7 +941,7 @@ export class SyncService
} }
private handleErrorServerResponse(response: ServerSyncResponse) { private handleErrorServerResponse(response: ServerSyncResponse) {
log(LoggingDomain.Sync, 'Sync Error', response) this.logger.debug('Sync Error', response)
if (response.status === INVALID_SESSION_RESPONSE_STATUS) { if (response.status === INVALID_SESSION_RESPONSE_STATUS) {
void this.notifyEvent(SyncEvent.InvalidSession) void this.notifyEvent(SyncEvent.InvalidSession)
@@ -968,7 +966,10 @@ export class SyncService
const historyMap = this.historyService.getHistoryMapCopy() const historyMap = this.historyService.getHistoryMapCopy()
if (response.userEvents && response.userEvents.length > 0) { 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) { if (response.asymmetricMessages && response.asymmetricMessages.length > 0) {
@@ -1003,8 +1004,7 @@ export class SyncService
historyMap, historyMap,
) )
log( this.logger.debug(
LoggingDomain.Sync,
'Online Sync Response', 'Online Sync Response',
'Operator ID', 'Operator ID',
operation.id, operation.id,
@@ -1263,7 +1263,7 @@ export class SyncService
} }
private async syncAgainByHandlingRequestsWaitingInResolveQueue(options: SyncOptions) { 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({ const promise = this.sync({
source: SyncSource.ResolveQueue, source: SyncSource.ResolveQueue,
checkIntegrity: options.checkIntegrity, checkIntegrity: options.checkIntegrity,

View File

@@ -112,7 +112,7 @@ describe('application instances', () => {
await app.lock() await app.lock()
}) })
describe.skip('signOut()', () => { describe('signOut()', () => {
let testNote1 let testNote1
let confirmAlert let confirmAlert
let deinit let deinit
@@ -129,7 +129,7 @@ describe('application instances', () => {
beforeEach(async () => { beforeEach(async () => {
testSNApp = await Factory.createAndInitializeApplication('test-application') testSNApp = await Factory.createAndInitializeApplication('test-application')
testNote1 = await Factory.createMappedNote(testSNApp, 'Note 1', 'This is a test note!', false) 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') deinit = sinon.spy(testSNApp, 'deinit')
}) })
@@ -164,7 +164,7 @@ describe('application instances', () => {
it('cancels sign out if confirmation dialog is rejected', async () => { it('cancels sign out if confirmation dialog is rejected', async () => {
confirmAlert.restore() 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.mutator.setItemDirty(testNote1)
await testSNApp.user.signOut() await testSNApp.user.signOut()

View File

@@ -207,7 +207,6 @@ describe('basic auth', function () {
await specContext.launch() await specContext.launch()
await specContext.register() await specContext.register()
await specContext.signout() await specContext.signout()
await specContext.deinit()
specContext = await Factory.createAppContextWithFakeCrypto(Math.random(), uppercase, password) specContext = await Factory.createAppContextWithFakeCrypto(Math.random(), uppercase, password)
@@ -217,6 +216,7 @@ describe('basic auth', function () {
expect(response).to.be.ok expect(response).to.be.ok
expect(response.data.error).to.not.be.ok expect(response.data.error).to.not.be.ok
expect(await specContext.application.encryption.getRootKey()).to.be.ok expect(await specContext.application.encryption.getRootKey()).to.be.ok
await specContext.deinit()
}).timeout(20000) }).timeout(20000)
it('can sign into account regardless of whitespace', async function () { it('can sign into account regardless of whitespace', async function () {
@@ -232,7 +232,6 @@ describe('basic auth', function () {
await specContext.launch() await specContext.launch()
await specContext.register() await specContext.register()
await specContext.signout() await specContext.signout()
await specContext.deinit()
specContext = await Factory.createAppContextWithFakeCrypto(Math.random(), withspace, password) specContext = await Factory.createAppContextWithFakeCrypto(Math.random(), withspace, password)
await specContext.launch() await specContext.launch()
@@ -241,6 +240,7 @@ describe('basic auth', function () {
expect(response).to.be.ok expect(response).to.be.ok
expect(response.data.error).to.not.be.ok expect(response.data.error).to.not.be.ok
expect(await specContext.application.encryption.getRootKey()).to.be.ok expect(await specContext.application.encryption.getRootKey()).to.be.ok
await specContext.deinit()
}).timeout(20000) }).timeout(20000)
it('fails login with wrong password', async function () { it('fails login with wrong password', async function () {
@@ -367,7 +367,7 @@ describe('basic auth', function () {
it('successfully changes password', changePassword).timeout(40000) 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 passcode = 'passcode'
const promptValueReply = (prompts) => { const promptValueReply = (prompts) => {
const values = [] const values = []
@@ -393,7 +393,7 @@ describe('basic auth', function () {
context.application.submitValuesForChallenge(challenge, initialValues) context.application.submitValuesForChallenge(challenge, initialValues)
}, },
}) })
await context.application.setPasscode(passcode) await context.application.addPasscode(passcode)
await changePassword.bind(this)() await changePassword.bind(this)()
}).timeout(20000) }).timeout(20000)
@@ -550,6 +550,8 @@ describe('basic auth', function () {
expect(response.status).to.equal(401) expect(response.status).to.equal(401)
expect(response.data.error.message).to.equal('Operation not allowed.') expect(response.data.error.message).to.equal('Operation not allowed.')
await secondContext.deinit()
}) })
}) })
}) })

View File

@@ -81,7 +81,6 @@ describe('backups', function () {
}) })
it('passcode + account backup file should have correct number of items', async function () { it('passcode + account backup file should have correct number of items', async function () {
this.timeout(10000)
const passcode = 'passcode' const passcode = 'passcode'
await this.application.register(this.email, this.password) await this.application.register(this.email, this.password)
Factory.handlePasswordChallenges(this.application, this.password) Factory.handlePasswordChallenges(this.application, this.password)
@@ -101,7 +100,7 @@ describe('backups', function () {
// Encrypted backup with authorization // Encrypted backup with authorization
const authorizedEncryptedData = await this.application.createEncryptedBackupFileForAutomatedDesktopBackups() const authorizedEncryptedData = await this.application.createEncryptedBackupFileForAutomatedDesktopBackups()
expect(authorizedEncryptedData.items.length).to.equal(BaseItemCounts.DefaultItemsWithAccount + 2) expect(authorizedEncryptedData.items.length).to.equal(BaseItemCounts.DefaultItemsWithAccount + 2)
}) }).timeout(10000)
it('backup file item should have correct fields', async function () { it('backup file item should have correct fields', async function () {
await Factory.createSyncedNote(this.application) await Factory.createSyncedNote(this.application)

View File

@@ -133,11 +133,12 @@ describe('features', () => {
await promise 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(() => { sinon.stub(application.legacyApi, 'isThirdPartyHostUsed').callsFake(() => {
return false 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 extensionKey = UuidGenerator.GenerateUuid().split('-').join('')
const promise = new Promise((resolve) => { const promise = new Promise((resolve) => {
application.streamItems(ContentType.TYPES.ExtensionRepo, ({ changed }) => { application.streamItems(ContentType.TYPES.ExtensionRepo, ({ changed }) => {

View File

@@ -404,7 +404,7 @@ describe('history manager', () => {
expect(noteHistory.length).to.equal(expectedRevisions) expect(noteHistory.length).to.equal(expectedRevisions)
expect(dupeHistory.length).to.equal(expectedRevisions + 1) expect(dupeHistory.length).to.equal(expectedRevisions + 1)
}) }).timeout(Factory.ThirtySecondTimeout)
it('can decrypt revisions for duplicate_of items', async function () { it('can decrypt revisions for duplicate_of items', async function () {
const note = await Factory.createSyncedNote(this.application) const note = await Factory.createSyncedNote(this.application)

View File

@@ -449,6 +449,7 @@ describe('keys', function () {
}, },
}) })
expect(payload.items_key_id).to.equal(newDefaultItemsKey.uuid) expect(payload.items_key_id).to.equal(newDefaultItemsKey.uuid)
await Factory.safeDeinit(application)
}) })
it('compares root keys', async function () { it('compares root keys', async function () {
@@ -642,7 +643,7 @@ describe('keys', function () {
await contextB.deinit() 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. * 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 * 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) const notePayload = rawPayloads.find((p) => p.content_type === ContentType.TYPES.Note)
expect(notePayload.items_key_id).to.equal(itemsKey.uuid) expect(notePayload.items_key_id).to.equal(itemsKey.uuid)
await otherClient.deinit()
}) })
}) })

View File

@@ -47,6 +47,30 @@ export class AppContext {
this.host, this.host,
this.crypto || new FakeWebCrypto(), 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() { get sessions() {
@@ -253,10 +277,10 @@ export class AppContext {
awaitNextSucessfulSync() { awaitNextSucessfulSync() {
return new Promise((resolve) => { return new Promise((resolve) => {
const removeObserver = this.application.sync.addEventObserver((event) => { const removeObserver = this.application.sync.addEventObserver((event, data) => {
if (event === SyncEvent.SyncCompletedWithAllItemsUploadedAndDownloaded) { if (event === SyncEvent.SyncCompletedWithAllItemsUploadedAndDownloaded) {
removeObserver() 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() { resolveWithConflicts() {
return new Promise((resolve) => { return new Promise((resolve) => {
this.application.sync.addEventObserver((event, response) => { this.application.sync.addEventObserver((event, response) => {
@@ -359,10 +393,16 @@ export class AppContext {
} }
resolveWhenAsyncFunctionCompletes(object, functionName) { 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) => { return new Promise((resolve) => {
sinon.stub(object, functionName).callsFake(async (params) => { sinon.stub(object, functionName).callsFake(async (params) => {
object[functionName].restore() object[functionName].restore()
const result = await object[functionName](params) const result = await originalFunction(params)
resolve() resolve()
return result return result
}) })
@@ -370,23 +410,26 @@ export class AppContext {
} }
spyOnFunctionResult(object, functionName) { spyOnFunctionResult(object, functionName) {
return new Promise((resolve) => { const originalFunction = object[functionName].bind(object)
sinon.stub(object, functionName).callsFake(async (params) => { return new Promise((resolve, reject) => {
object[functionName].restore() try {
const result = await object[functionName](params) sinon.stub(object, functionName).callsFake(async (params) => {
resolve(result) const result = await originalFunction(params)
return result object[functionName].restore()
}) setTimeout(() => {
resolve(result)
}, 0)
return result
})
} catch (err) {
reject(err)
}
}) })
} }
resolveWhenAsymmetricMessageProcessingCompletes() {
return this.resolveWhenAsyncFunctionCompletes(this.asymmetric, 'handleRemoteReceivedAsymmetricMessages')
}
resolveWhenUserMessagesProcessingCompletes() { resolveWhenUserMessagesProcessingCompletes() {
const objectToSpy = this.application.dependencies.get(TYPES.UserEventService) const objectToSpy = this.application.dependencies.get(TYPES.NotificationService)
return this.resolveWhenAsyncFunctionCompletes(objectToSpy, 'handleReceivedUserEvents') return this.resolveWhenAsyncFunctionCompletes(objectToSpy, 'handleReceivedNotifications')
} }
resolveWhenAllInboundAsymmetricMessagesAreDeleted() { 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) { async sync(options) {
await this.application.sync.sync(options || { awaitAll: true }) await this.application.sync.sync(options || { awaitAll: true })
} }
@@ -628,6 +659,10 @@ export class AppContext {
console.warn('Anticipating a console error with message:', message) 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 = {}) { async activatePaidSubscriptionForUser(options = {}) {
const dateInAnHour = new Date() const dateInAnHour = new Date()
dateInAnHour.setHours(dateInAnHour.getHours() + 1) dateInAnHour.setHours(dateInAnHour.getHours() + 1)
@@ -655,17 +690,17 @@ export class AppContext {
await Utils.sleep(2) await Utils.sleep(2)
} catch (error) { } catch (error) {
console.warn( 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 { try {
await HomeServer.activatePremiumFeatures(this.email, options.subscriptionPlanName, options.expiresAt) 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) { } catch (error) {
console.warn( 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}`,
) )
} }
} }

View File

@@ -1,7 +1,8 @@
import * as Factory from './factory.js' import * as Factory from './factory.js'
import * as Utils from './Utils.js'
export const createContactContext = async () => { export const createContactContext = async () => {
const contactContext = await Factory.createAppContextWithRealCrypto() const contactContext = await Factory.createVaultsContextWithRealCrypto()
await contactContext.launch() await contactContext.launch()
await contactContext.register() await contactContext.register()
@@ -15,6 +16,8 @@ export const createTrustedContactForUserOfContext = async (
contextAddingNewContact, contextAddingNewContact,
contextImportingContactInfoFrom, contextImportingContactInfoFrom,
) => { ) => {
const syncPromisme = contextAddingNewContact.awaitNextSucessfulSync()
const contact = await contextAddingNewContact.contacts.createOrEditTrustedContact({ const contact = await contextAddingNewContact.contacts.createOrEditTrustedContact({
name: 'John Doe', name: 'John Doe',
publicKey: contextImportingContactInfoFrom.publicKey, publicKey: contextImportingContactInfoFrom.publicKey,
@@ -22,6 +25,8 @@ export const createTrustedContactForUserOfContext = async (
contactUuid: contextImportingContactInfoFrom.userUuid, contactUuid: contextImportingContactInfoFrom.userUuid,
}) })
await syncPromisme
return contact 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 } = const { sharedVault, contact, contactContext, deinitContactContext } =
await createSharedVaultWithUnacceptedButTrustedInvite(context, permission) await createSharedVaultWithUnacceptedButTrustedInvite(context, permission)
@@ -44,7 +77,7 @@ export const createSharedVaultWithAcceptedInvite = async (context, permission =
await acceptAllInvites(contactContext) await acceptAllInvites(contactContext)
await promise await Utils.awaitPromiseOrThrow(promise, 2.0, 'Waiting for vault to sync')
const contactVault = contactContext.vaults.getVault({ keySystemIdentifier: sharedVault.systemIdentifier }) const contactVault = contactContext.vaults.getVault({ keySystemIdentifier: sharedVault.systemIdentifier })
@@ -61,7 +94,10 @@ export const createSharedVaultWithAcceptedInviteAndNote = async (
) )
const note = await context.createSyncedNote('foo', 'bar') const note = await context.createSyncedNote('foo', 'bar')
const updatedNote = await moveItemToVault(context, sharedVault, note) const updatedNote = await moveItemToVault(context, sharedVault, note)
const promise = contactContext.awaitNextSucessfulSync()
await contactContext.sync() await contactContext.sync()
await Utils.awaitPromiseOrThrow(promise, 2.0, 'Waiting for contactContext to sync added note')
return { sharedVault, note: updatedNote, contact, contactContext, deinitContactContext } return { sharedVault, note: updatedNote, contact, contactContext, deinitContactContext }
} }
@@ -76,34 +112,20 @@ export const createSharedVaultWithUnacceptedButTrustedInvite = async (
const contact = await createTrustedContactForUserOfContext(context, contactContext) const contact = await createTrustedContactForUserOfContext(context, contactContext)
await createTrustedContactForUserOfContext(contactContext, context) await createTrustedContactForUserOfContext(contactContext, context)
const inviteOrError = await context.vaultInvites.inviteContactToSharedVault(sharedVault, contact, permission) const invite = await inviteContext(context, contactContext, sharedVault, contact, permission)
if (inviteOrError.isFailed()) {
throw new Error(inviteOrError.getError())
}
const invite = inviteOrError.getValue()
await contactContext.sync()
return { sharedVault, contact, contactContext, deinitContactContext, invite } return { sharedVault, contact, contactContext, deinitContactContext, invite }
} }
export const createSharedVaultAndInviteContact = async ( export const createSharedVaultAndInviteContact = async (
createInContext, context,
inviteContext, contactContext,
inviteContact, contact,
permission = SharedVaultUserPermission.PERMISSIONS.Write, permission = SharedVaultUserPermission.PERMISSIONS.Write,
) => { ) => {
const sharedVault = await createSharedVault(createInContext) const sharedVault = await createSharedVault(context)
await createInContext.vaultInvites.inviteContactToSharedVault(sharedVault, inviteContact, permission) await inviteContext(context, contactContext, sharedVault, contact, permission)
const promise = inviteContext.awaitNextSyncSharedVaultFromScratchEvent()
await inviteContext.sync()
await acceptAllInvites(inviteContext)
await promise
return { sharedVault } return { sharedVault }
} }
@@ -118,20 +140,33 @@ export const createSharedVaultWithUnacceptedAndUntrustedInvite = async (
const contact = await createTrustedContactForUserOfContext(context, contactContext) const contact = await createTrustedContactForUserOfContext(context, contactContext)
const invite = (await context.vaultInvites.inviteContactToSharedVault(sharedVault, contact, permission)).getValue() const invite = (await context.vaultInvites.inviteContactToSharedVault(sharedVault, contact, permission)).getValue()
const promise = contactContext.resolveWhenAsyncFunctionCompletes(contactContext.vaultInvites, 'processInboundInvites')
await contactContext.sync() await contactContext.sync()
await Utils.awaitPromiseOrThrow(
promise,
2.0,
'[createSharedVaultWithUnacceptedAndUntrustedInvite] Waiting to process invites',
)
return { sharedVault, contact, contactContext, deinitContactContext, invite } 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 } = const { contactContext: thirdPartyContext, deinitContactContext: deinitThirdPartyContext } =
await createContactContext() await createContactContext()
const thirdPartyContact = await createTrustedContactForUserOfContext(context, thirdPartyContext) 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 } return { thirdPartyContext, thirdPartyContact, deinitThirdPartyContext }
} }

View File

@@ -11,15 +11,15 @@ export async function safeDeinit(application) {
await application.storage.awaitPersist() await application.storage.awaitPersist()
/** Limit waiting to 1s */ /** Limit waiting to 1s */
await Promise.race([sleep(1), application.sync?.awaitCurrentSyncs()]) await Promise.race([sleep(1, 'Deinit'), application.sync?.awaitCurrentSyncs()])
await application.prepareForDeinit() await application.prepareForDeinit()
application.deinit(DeinitMode.Soft, DeinitSource.SignOut) application.deinit(DeinitMode.Soft, DeinitSource.SignOut)
} }
export async function sleep(seconds) { export async function sleep(seconds, reason) {
console.warn(`Test sleeping for ${seconds}s`) console.warn(`Test sleeping for ${seconds}s. Reason: ${reason}`)
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
setTimeout(function () { setTimeout(function () {
@@ -32,3 +32,22 @@ export function generateUuid() {
const crypto = new FakeWebCrypto() const crypto = new FakeWebCrypto()
return crypto.generateUUID() return crypto.generateUUID()
} }
export async function awaitPromiseOrThrow(promise, maxWait, reason) {
let timer = undefined
// Create a promise that rejects in <maxWait> 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
})
}

View File

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

View File

@@ -2,6 +2,7 @@
/* eslint-disable no-undef */ /* eslint-disable no-undef */
import FakeWebCrypto from './fake_web_crypto.js' import FakeWebCrypto from './fake_web_crypto.js'
import { AppContext } from './AppContext.js' import { AppContext } from './AppContext.js'
import { VaultsContext } from './VaultsContext.js'
import * as Applications from './Applications.js' import * as Applications from './Applications.js'
import * as Defaults from './Defaults.js' import * as Defaults from './Defaults.js'
import * as Utils from './Utils.js' import * as Utils from './Utils.js'
@@ -57,6 +58,16 @@ export async function createAppContext({ identifier, crypto, email, password, ho
return context 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) { export function disableIntegrityAutoHeal(application) {
application.sync.emitOutOfSyncRemotePayloads = () => { application.sync.emitOutOfSyncRemotePayloads = () => {
console.warn('Integrity self-healing is disabled for this test') console.warn('Integrity self-healing is disabled for this test')
@@ -288,7 +299,7 @@ export function tomorrow() {
} }
export async function sleep(seconds, reason) { export async function sleep(seconds, reason) {
console.log('Sleeping for reason', reason) console.log('[Factory] Sleeping for reason', reason)
return Utils.sleep(seconds) return Utils.sleep(seconds)
} }

View File

@@ -843,8 +843,6 @@ describe('importing', function () {
}) })
it('importing another accounts notes/tags should correctly keep relationships', async function () { it('importing another accounts notes/tags should correctly keep relationships', async function () {
this.timeout(Factory.TwentySecondTimeout)
await setup({ fakeCrypto: true }) await setup({ fakeCrypto: true })
await Factory.registerUserToApplication({ await Factory.registerUserToApplication({
@@ -881,5 +879,5 @@ describe('importing', function () {
const importedTag = application.items.getDisplayableTags()[0] const importedTag = application.items.getDisplayableTags()[0]
expect(application.items.referencesForItem(importedTag).length).to.equal(1) expect(application.items.referencesForItem(importedTag).length).to.equal(1)
expect(application.items.itemsReferencingItem(importedNote).length).to.equal(1) expect(application.items.itemsReferencingItem(importedNote).length).to.equal(1)
}) }).timeout(Factory.TwentySecondTimeout)
}) })

View File

@@ -4,7 +4,9 @@ const expect = chai.expect
describe('note display criteria', function () { describe('note display criteria', function () {
beforeEach(async 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.itemManager = new ItemManager(this.payloadManager)
this.mutator = new MutatorService(this.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 () { describe.skip('multiple tags', function () {
it('normal note', async function () { it('normal note', async function () {
await this.createNote() await this.createNote()
/**
* This test presently fails because the compound predicate created
* when using multiple views is an AND predicate instead of OR
*/
expect( expect(
notesAndFilesMatchingOptions( notesAndFilesMatchingOptions(
{ {

View File

@@ -6,7 +6,8 @@ const expect = chai.expect
describe('payload manager', () => { describe('payload manager', () => {
beforeEach(async function () { beforeEach(async function () {
this.payloadManager = new PayloadManager() const logger = new Logger('test')
this.payloadManager = new PayloadManager(logger)
this.createNotePayload = async () => { this.createNotePayload = async () => {
return new DecryptedPayload({ return new DecryptedPayload({
uuid: Factory.generateUuidish(), uuid: Factory.generateUuidish(),

View File

@@ -58,16 +58,15 @@ describe('preferences', function () {
}) })
it('emits an event when preferences change', async function () { it('emits an event when preferences change', async function () {
let callTimes = 0 const promise = new Promise((resolve) => {
this.application.addEventObserver(() => { this.application.addEventObserver(() => {
callTimes++ resolve()
}, ApplicationEvent.PreferencesChanged) }, ApplicationEvent.PreferencesChanged)
callTimes += 1 })
await Factory.sleep(0) /** Await next tick */
expect(callTimes).to.equal(1) /** App start */
await register.call(this)
await this.application.setPreference('editorLeft', 300) 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 () { it('discards existing preferences when signing in', async function () {

View File

@@ -46,8 +46,6 @@ describe('server session', function () {
} }
it('should succeed when a sync request is perfomed with an expired access token', async function () { it('should succeed when a sync request is perfomed with an expired access token', async function () {
this.timeout(Factory.TwentySecondTimeout)
await Factory.registerUserToApplication({ await Factory.registerUserToApplication({
application: this.application, application: this.application,
email: this.email, email: this.email,
@@ -59,7 +57,7 @@ describe('server session', function () {
const response = await this.application.legacyApi.sync([]) const response = await this.application.legacyApi.sync([])
expect(response.status).to.equal(200) expect(response.status).to.equal(200)
}) }).timeout(Factory.TwentySecondTimeout)
it('should return the new session in the response when refreshed', async function () { it('should return the new session in the response when refreshed', async function () {
await Factory.registerUserToApplication({ 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 () { it('should be refreshed on any api call if access token is expired', async function () {
this.timeout(Factory.TwentySecondTimeout)
await Factory.registerUserToApplication({ await Factory.registerUserToApplication({
application: this.application, application: this.application,
email: this.email, email: this.email,
@@ -103,11 +99,9 @@ describe('server session', function () {
expect(sessionBeforeSync.accessToken.expiresAt).to.be.lessThan(sessionAfterSync.accessToken.expiresAt) expect(sessionBeforeSync.accessToken.expiresAt).to.be.lessThan(sessionAfterSync.accessToken.expiresAt)
// New token should expire in the future. // New token should expire in the future.
expect(sessionAfterSync.accessToken.expiresAt).to.be.greaterThan(Date.now()) expect(sessionAfterSync.accessToken.expiresAt).to.be.greaterThan(Date.now())
}) }).timeout(Factory.TwentySecondTimeout)
it('should not deadlock while renewing session', async function () { it('should not deadlock while renewing session', async function () {
this.timeout(Factory.TwentySecondTimeout)
await Factory.registerUserToApplication({ await Factory.registerUserToApplication({
application: this.application, application: this.application,
email: this.email, email: this.email,
@@ -125,7 +119,7 @@ describe('server session', function () {
const sessionAfterSync = this.application.legacyApi.getSession() const sessionAfterSync = this.application.legacyApi.getSession()
expect(sessionAfterSync.accessToken.expiresAt).to.be.greaterThan(Date.now()) 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 () { it('should succeed when a sync request is perfomed after signing into an ephemeral session', async function () {
await Factory.registerUserToApplication({ 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 () { it('sign out request should be performed successfully and terminate session with expired access token', async function () {
this.timeout(Factory.TwentySecondTimeout)
await Factory.registerUserToApplication({ await Factory.registerUserToApplication({
application: this.application, application: this.application,
email: this.email, email: this.email,
@@ -218,11 +210,9 @@ describe('server session', function () {
expect(syncResponse.status).to.equal(401) expect(syncResponse.status).to.equal(401)
expect(syncResponse.data.error.tag).to.equal('invalid-auth') expect(syncResponse.data.error.tag).to.equal('invalid-auth')
expect(syncResponse.data.error.message).to.equal('Invalid login credentials.') 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 () { it('change email request should be successful with a valid access token', async function () {
this.timeout(Factory.TwentySecondTimeout)
let { application, password } = await Factory.createAndInitSimpleAppContext({ let { application, password } = await Factory.createAndInitSimpleAppContext({
registerUser: true, registerUser: true,
}) })
@@ -241,11 +231,9 @@ describe('server session', function () {
expect(loginResponse).to.be.ok expect(loginResponse).to.be.ok
expect(loginResponse.status).to.equal(200) expect(loginResponse.status).to.equal(200)
await Factory.safeDeinit(application) await Factory.safeDeinit(application)
}) }).timeout(Factory.TwentySecondTimeout)
it('change email request should fail with an invalid access token', async function () { it('change email request should fail with an invalid access token', async function () {
this.timeout(Factory.TwentySecondTimeout)
let { application, password } = await Factory.createAndInitSimpleAppContext({ let { application, password } = await Factory.createAndInitSimpleAppContext({
registerUser: true, registerUser: true,
}) })
@@ -266,14 +254,13 @@ describe('server session', function () {
expect(changeEmailResponse.error.message).to.equal('Invalid login credentials.') expect(changeEmailResponse.error.message).to.equal('Invalid login credentials.')
await Factory.safeDeinit(application) await Factory.safeDeinit(application)
}) }).timeout(Factory.TwentySecondTimeout)
it('change email request should fail with an expired refresh token', async function () { it('change email request should fail with an expired refresh token', async function () {
this.timeout(Factory.ThirtySecondTimeout) let { application, password } = await Factory.createAndInitSimpleAppContext({
let { application, email, password } = await Factory.createAndInitSimpleAppContext({
registerUser: true, registerUser: true,
}) })
application.sync.lockSyncing()
/** Waiting for the refresh token to expire. */ /** Waiting for the refresh token to expire. */
await sleepUntilSessionExpires(application, false) await sleepUntilSessionExpires(application, false)
@@ -285,11 +272,9 @@ describe('server session', function () {
expect(changeEmailResponse.error.message).to.equal('Invalid login credentials.') expect(changeEmailResponse.error.message).to.equal('Invalid login credentials.')
await Factory.safeDeinit(application) await Factory.safeDeinit(application)
}) }).timeout(Factory.ThirtySecondTimeout)
it('change password request should be successful with a valid access token', async function () { it('change password request should be successful with a valid access token', async function () {
this.timeout(Factory.TwentySecondTimeout)
await Factory.registerUserToApplication({ await Factory.registerUserToApplication({
application: this.application, application: this.application,
email: this.email, email: this.email,
@@ -309,11 +294,9 @@ describe('server session', function () {
expect(loginResponse).to.be.ok expect(loginResponse).to.be.ok
expect(loginResponse.status).to.be.equal(200) 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 () { it('change password request should be successful after the expired access token is refreshed', async function () {
this.timeout(Factory.TwentySecondTimeout)
await Factory.registerUserToApplication({ await Factory.registerUserToApplication({
application: this.application, application: this.application,
email: this.email, email: this.email,
@@ -336,7 +319,7 @@ describe('server session', function () {
expect(loginResponse).to.be.ok expect(loginResponse).to.be.ok
expect(loginResponse.status).to.be.equal(200) expect(loginResponse.status).to.be.equal(200)
}) }).timeout(Factory.TwentySecondTimeout)
it('change password request should fail with an invalid access token', async function () { it('change password request should fail with an invalid access token', async function () {
await Factory.registerUserToApplication({ await Factory.registerUserToApplication({
@@ -360,14 +343,14 @@ describe('server session', function () {
}) })
it('change password request should fail with an expired refresh token', async function () { it('change password request should fail with an expired refresh token', async function () {
this.timeout(Factory.TwentySecondTimeout)
await Factory.registerUserToApplication({ await Factory.registerUserToApplication({
application: this.application, application: this.application,
email: this.email, email: this.email,
password: this.password, password: this.password,
}) })
this.application.sync.lockSyncing()
/** Waiting for the refresh token to expire. */ /** Waiting for the refresh token to expire. */
await sleepUntilSessionExpires(this.application, false) 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 () { it('should fail when renewing a session with an expired refresh token', async function () {
this.timeout(Factory.TwentySecondTimeout)
await Factory.registerUserToApplication({ await Factory.registerUserToApplication({
application: this.application, application: this.application,
email: this.email, email: this.email,
password: this.password, password: this.password,
}) })
this.application.sync.lockSyncing()
await sleepUntilSessionExpires(this.application, false) await sleepUntilSessionExpires(this.application, false)
const refreshSessionResponse = await this.application.legacyApi.refreshSession() const refreshSessionResponse = await this.application.legacyApi.refreshSession()
@@ -424,7 +407,7 @@ describe('server session', function () {
expect(syncResponse.status).to.equal(401) expect(syncResponse.status).to.equal(401)
expect(syncResponse.data.error.tag).to.equal('invalid-auth') expect(syncResponse.data.error.tag).to.equal('invalid-auth')
expect(syncResponse.data.error.message).to.equal('Invalid login credentials.') 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 () { it('should fail when renewing a session with an invalid refresh token', async function () {
await Factory.registerUserToApplication({ await Factory.registerUserToApplication({
@@ -474,8 +457,6 @@ describe('server session', function () {
}) })
it('notes should be synced as expected after refreshing a session', async function () { it('notes should be synced as expected after refreshing a session', async function () {
this.timeout(Factory.TwentySecondTimeout)
await Factory.registerUserToApplication({ await Factory.registerUserToApplication({
application: this.application, application: this.application,
email: this.email, email: this.email,
@@ -500,7 +481,7 @@ describe('server session', function () {
const noteResult = await this.application.items.findItem(aNoteBeforeSync.uuid) const noteResult = await this.application.items.findItem(aNoteBeforeSync.uuid)
expect(aNoteBeforeSync.isItemContentEqualWith(noteResult)).to.equal(true) expect(aNoteBeforeSync.isItemContentEqualWith(noteResult)).to.equal(true)
} }
}) }).timeout(Factory.TwentySecondTimeout)
it('changing password on one client should not invalidate other sessions', async function () { it('changing password on one client should not invalidate other sessions', async function () {
await Factory.registerUserToApplication({ await Factory.registerUserToApplication({
@@ -635,8 +616,6 @@ describe('server session', function () {
}) })
it('revoking a session should destroy local data', async function () { it('revoking a session should destroy local data', async function () {
this.timeout(Factory.TwentySecondTimeout)
Factory.handlePasswordChallenges(this.application, this.password) Factory.handlePasswordChallenges(this.application, this.password)
await Factory.registerUserToApplication({ await Factory.registerUserToApplication({
application: this.application, application: this.application,
@@ -662,11 +641,9 @@ describe('server session', function () {
const deviceInterface = new WebDeviceInterface() const deviceInterface = new WebDeviceInterface()
const payloads = await deviceInterface.getAllDatabaseEntries(app2identifier) const payloads = await deviceInterface.getAllDatabaseEntries(app2identifier)
expect(payloads).to.be.empty expect(payloads).to.be.empty
}) }).timeout(Factory.TwentySecondTimeout)
it('revoking other sessions should destroy their local data', async function () { it('revoking other sessions should destroy their local data', async function () {
this.timeout(Factory.TwentySecondTimeout)
Factory.handlePasswordChallenges(this.application, this.password) Factory.handlePasswordChallenges(this.application, this.password)
await Factory.registerUserToApplication({ await Factory.registerUserToApplication({
application: this.application, application: this.application,
@@ -690,7 +667,7 @@ describe('server session', function () {
const deviceInterface = new WebDeviceInterface() const deviceInterface = new WebDeviceInterface()
const payloads = await deviceInterface.getAllDatabaseEntries(app2identifier) const payloads = await deviceInterface.getAllDatabaseEntries(app2identifier)
expect(payloads).to.be.empty expect(payloads).to.be.empty
}) }).timeout(Factory.TwentySecondTimeout)
it('signing out with invalid session token should still delete local data', async function () { it('signing out with invalid session token should still delete local data', async function () {
await Factory.registerUserToApplication({ await Factory.registerUserToApplication({

View File

@@ -14,7 +14,6 @@ describe('settings service', function () {
let application let application
let context let context
let subscriptionId = 2001
beforeEach(async function () { beforeEach(async function () {
localStorage.clear() localStorage.clear()

View File

@@ -7,17 +7,19 @@ const expect = chai.expect
describe('storage manager', function () { describe('storage manager', function () {
this.timeout(Factory.TenSecondTimeout) this.timeout(Factory.TenSecondTimeout)
/** /**
* Items are saved in localStorage in tests. * 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 () { beforeEach(async function () {
localStorage.clear() localStorage.clear()
this.expectedKeyCount = BASE_KEY_COUNT this.expectedKeyCount = BASE_KEY_COUNT
this.context = await Factory.createAppContext() this.context = await Factory.createAppContext()
await this.context.launch() await this.context.launch()
this.application = this.context.application this.application = this.context.application
this.email = UuidGenerator.GenerateUuid() this.email = UuidGenerator.GenerateUuid()
this.password = UuidGenerator.GenerateUuid() this.password = UuidGenerator.GenerateUuid()
@@ -62,18 +64,19 @@ describe('storage manager', function () {
expect(keychainValue.serverPassword).to.not.be.ok 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({ await Factory.registerUserToApplication({
application: this.application, application: this.application,
email: this.email, email: this.email,
password: this.password, password: this.password,
ephemeral: false, ephemeral: false,
}) })
const key = 'foo' const key = 'foo'
const value = 'bar' const value = 'bar'
await this.application.storage.setValue(key, value) 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) const retrievedValue = await this.application.storage.getValue(key)
expect(retrievedValue).to.equal(value) expect(retrievedValue).to.equal(value)
}) })

View File

@@ -479,8 +479,7 @@ describe('online conflict handling', function () {
await this.sharedFinalAssertions() await this.sharedFinalAssertions()
}) })
/** Temporarily skipping due to long run time */ it('handles stale data in bulk', async function () {
it.skip('handles stale data in bulk', async function () {
/** This number must be greater than the pagination limit per sync request. /** 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 * For example if the limit per request is 150 items sent/received, this number should
* be something like 160. */ * be something like 160. */
@@ -728,43 +727,39 @@ describe('online conflict handling', function () {
await this.sharedFinalAssertions() await this.sharedFinalAssertions()
}) })
/** Temporarily skipping due to long run time */ it('registering for account with bulk offline data belonging to another account should be error-free', async function () {
it.skip( /**
'registering for account with bulk offline data belonging to another account should be error-free', * When performing a multi-page sync request where we are uploading data imported from a backup,
async function () { * 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.
* When performing a multi-page sync request where we are uploading data imported from a backup, * This causes a problem because when that second page is returned as conflicts, we will be looking
* if the first page of the sync request returns conflicted items keys, we rotate their UUID. * for an items_key_id that no longer exists (has been rotated). Rather than modifying the entire
* The second page of sync waiting to be sent up is still encrypted with the old items key UUID. * sync paradigm to allow multi-page requests to consider side-effects of each page, we will instead
* This causes a problem because when that second page is returned as conflicts, we will be looking * take the approach of making sure the decryption function is liberal with regards to searching
* for an items_key_id that no longer exists (has been rotated). Rather than modifying the entire * for the right items key. It will now consider (as a result of this test) an items key as being
* sync paradigm to allow multi-page requests to consider side-effects of each page, we will instead * the correct key to decrypt an item if the itemskey.uuid == item.items_key_id OR if the itemsKey.duplicateOf
* take the approach of making sure the decryption function is liberal with regards to searching * value is equal to item.items_key_id.
* 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 */ /** Create bulk data belonging to another account and sync */
const largeItemCount = SyncUpDownLimit + 10 const largeItemCount = SyncUpDownLimit + 10
await Factory.createManyMappedNotes(this.application, largeItemCount) await Factory.createManyMappedNotes(this.application, largeItemCount)
await this.application.sync.sync(syncOptions) await this.application.sync.sync(syncOptions)
const priorData = this.application.items.items const priorData = this.application.items.items
/** Register new account and import this same data */ /** Register new account and import this same data */
const newApp = await Factory.signOutApplicationAndReturnNew(this.application) const newApp = await Factory.signOutApplicationAndReturnNew(this.application)
await Factory.registerUserToApplication({ await Factory.registerUserToApplication({
application: newApp, application: newApp,
email: Utils.generateUuid(), email: Utils.generateUuid(),
password: Utils.generateUuid(), password: Utils.generateUuid(),
}) })
await newApp.mutator.emitItemsFromPayloads(priorData.map((i) => i.payload)) await newApp.mutator.emitItemsFromPayloads(priorData.map((i) => i.payload))
await newApp.sync.markAllItemsAsNeedingSyncAndPersist() await newApp.sync.markAllItemsAsNeedingSyncAndPersist()
await newApp.sync.sync(syncOptions) await newApp.sync.sync(syncOptions)
expect(newApp.payloads.invalidPayloads.length).to.equal(0) expect(newApp.payloads.invalidPayloads.length).to.equal(0)
await Factory.safeDeinit(newApp) await Factory.safeDeinit(newApp)
}, }).timeout(80000)
).timeout(80000)
it('importing data belonging to another account should not result in duplication', async function () { it('importing data belonging to another account should not result in duplication', async function () {
/** Create primary account and export data */ /** Create primary account and export data */

View File

@@ -528,8 +528,7 @@ describe('online syncing', function () {
await this.application.sync.sync(syncOptions) await this.application.sync.sync(syncOptions)
}) })
/** Temporarily skipping due to long run time */ it('should handle uploading with sync pagination', async function () {
it.skip('should handle uploading with sync pagination', async function () {
const largeItemCount = SyncUpDownLimit + 10 const largeItemCount = SyncUpDownLimit + 10
for (let i = 0; i < largeItemCount; i++) { for (let i = 0; i < largeItemCount; i++) {
const note = await Factory.createMappedNote(this.application) const note = await Factory.createMappedNote(this.application)
@@ -541,10 +540,9 @@ describe('online syncing', function () {
await this.application.sync.sync(syncOptions) await this.application.sync.sync(syncOptions)
const rawPayloads = await this.application.storage.getAllRawPayloads() const rawPayloads = await this.application.storage.getAllRawPayloads()
expect(rawPayloads.length).to.equal(this.expectedItemCount) expect(rawPayloads.length).to.equal(this.expectedItemCount)
}).timeout(15000) }).timeout(Factory.TwentySecondTimeout)
/** Temporarily skipping due to long run time */ it('should handle downloading with sync pagination', async function () {
it.skip('should handle downloading with sync pagination', async function () {
const largeItemCount = SyncUpDownLimit + 10 const largeItemCount = SyncUpDownLimit + 10
for (let i = 0; i < largeItemCount; i++) { for (let i = 0; i < largeItemCount; i++) {
const note = await Factory.createMappedNote(this.application) const note = await Factory.createMappedNote(this.application)

View File

@@ -17,7 +17,7 @@ describe('asymmetric messages', function () {
beforeEach(async function () { beforeEach(async function () {
localStorage.clear() localStorage.clear()
context = await Factory.createAppContextWithRealCrypto() context = await Factory.createVaultsContextWithRealCrypto()
await context.launch() await context.launch()
await context.register() await context.register()
@@ -29,7 +29,7 @@ describe('asymmetric messages', function () {
contactContext.lockSyncing() contactContext.lockSyncing()
await context.vaults.changeVaultNameAndDescription(sharedVault, { await context.changeVaultName(sharedVault, {
name: 'new vault name', name: 'new vault name',
description: 'new vault description', description: 'new vault description',
}) })
@@ -38,11 +38,8 @@ describe('asymmetric messages', function () {
get: () => 'invalid user uuid', get: () => 'invalid user uuid',
}) })
const completedProcessingMessagesPromise = contactContext.resolveWhenAsymmetricMessageProcessingCompletes()
contactContext.unlockSyncing() contactContext.unlockSyncing()
await contactContext.sync() await contactContext.syncAndAwaitMessageProcessing()
await completedProcessingMessagesPromise
const updatedVault = contactContext.vaults.getVault({ keySystemIdentifier: sharedVault.systemIdentifier }) const updatedVault = contactContext.vaults.getVault({ keySystemIdentifier: sharedVault.systemIdentifier })
@@ -53,33 +50,22 @@ describe('asymmetric messages', function () {
}) })
it('should delete message after processing it', async () => { 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 = { await context.changeVaultName(sharedVault, {
current: { name: 'New Name',
encryption: context.encryption.getKeyPair(), description: 'New Description',
signing: context.encryption.getSigningKeyPair(), })
},
previous: {
encryption: context.encryption.getKeyPair(),
signing: context.encryption.getSigningKeyPair(),
},
}
await context.contacts.sendOwnContactChangeEventToAllContacts(eventData)
const deleteFunction = sinon.spy(contactContext.asymmetric, 'deleteMessageAfterProcessing') const deleteFunction = sinon.spy(contactContext.asymmetric, 'deleteMessageAfterProcessing')
const promise = contactContext.resolveWhenAsymmetricMessageProcessingCompletes() await contactContext.syncAndAwaitMessageProcessing()
await contactContext.sync()
await promise
expect(deleteFunction.callCount).to.equal(1) expect(deleteFunction.callCount).to.equal(1)
const messages = await contactContext.asymmetric.getInboundMessages() const messages = await contactContext.asymmetric.getInboundMessages()
expect(messages.length).to.equal(0) expect(messages.getValue().length).to.equal(0)
await deinitContactContext() await deinitContactContext()
}) })
@@ -106,10 +92,7 @@ describe('asymmetric messages', function () {
await sendContactSharePromise await sendContactSharePromise
const completedProcessingMessagesPromise = contactContext.resolveWhenAsymmetricMessageProcessingCompletes() await contactContext.syncAndAwaitMessageProcessing()
await contactContext.sync()
await completedProcessingMessagesPromise
const updatedContact = contactContext.contacts.findContact(thirdPartyContext.userUuid) const updatedContact = contactContext.contacts.findContact(thirdPartyContext.userUuid)
expect(updatedContact.name).to.equal('Changed 3rd Party Name') expect(updatedContact.name).to.equal('Changed 3rd Party Name')
@@ -122,7 +105,10 @@ describe('asymmetric messages', function () {
const { sharedVault, contactContext, deinitContactContext } = const { sharedVault, contactContext, deinitContactContext } =
await Collaboration.createSharedVaultWithAcceptedInvite(context) await Collaboration.createSharedVaultWithAcceptedInvite(context)
const handleInitialContactShareMessage = contactContext.resolveWhenAsymmetricMessageProcessingCompletes() const handleInitialContactShareMessage = contactContext.resolveWhenAsyncFunctionCompletes(
contactContext.asymmetric,
'handleRemoteReceivedAsymmetricMessages',
)
const { thirdPartyContext, deinitThirdPartyContext } = await Collaboration.inviteNewPartyToSharedVault( const { thirdPartyContext, deinitThirdPartyContext } = await Collaboration.inviteNewPartyToSharedVault(
context, context,
@@ -162,6 +148,7 @@ describe('asymmetric messages', function () {
await Collaboration.acceptAllInvites(thirdPartyContext) await Collaboration.acceptAllInvites(thirdPartyContext)
await contactContext.sync() await contactContext.sync()
contactContext.lockSyncing()
const sendContactSharePromise = context.resolveWhenSharedVaultServiceSendsContactShareMessage() const sendContactSharePromise = context.resolveWhenSharedVaultServiceSendsContactShareMessage()
@@ -179,6 +166,7 @@ describe('asymmetric messages', function () {
const thirdPartySpy = sinon.spy(thirdPartyContext.asymmetric, 'handleTrustedContactShareMessage') const thirdPartySpy = sinon.spy(thirdPartyContext.asymmetric, 'handleTrustedContactShareMessage')
await context.sync() await context.sync()
contactContext.unlockSyncing()
await contactContext.sync() await contactContext.sync()
await thirdPartyContext.sync() await thirdPartyContext.sync()
@@ -194,12 +182,26 @@ describe('asymmetric messages', function () {
const { sharedVault, contactContext, deinitContactContext } = const { sharedVault, contactContext, deinitContactContext } =
await Collaboration.createSharedVaultWithAcceptedInvite(context) 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 context.vaults.rotateVaultRootKey(sharedVault)
await promise
const firstPartySpy = sinon.spy(context.asymmetric, 'handleTrustedSharedVaultRootKeyChangedMessage') const firstPartySpy = sinon.spy(context.asymmetric, 'handleTrustedSharedVaultRootKeyChangedMessage')
const secondPartySpy = sinon.spy(contactContext.asymmetric, 'handleTrustedSharedVaultRootKeyChangedMessage') const secondPartySpy = sinon.spy(contactContext.asymmetric, 'handleTrustedSharedVaultRootKeyChangedMessage')
await context.sync() await context.sync()
contactContext.unlockSyncing()
await contactContext.sync() await contactContext.sync()
expect(firstPartySpy.callCount).to.equal(0) expect(firstPartySpy.callCount).to.equal(0)
@@ -212,7 +214,7 @@ describe('asymmetric messages', function () {
const { sharedVault, contactContext, deinitContactContext } = const { sharedVault, contactContext, deinitContactContext } =
await Collaboration.createSharedVaultWithAcceptedInvite(context) await Collaboration.createSharedVaultWithAcceptedInvite(context)
await context.vaults.changeVaultNameAndDescription(sharedVault, { await context.changeVaultName(sharedVault, {
name: 'New Name', name: 'New Name',
description: 'New Description', description: 'New Description',
}) })
@@ -238,20 +240,13 @@ describe('asymmetric messages', function () {
contactContext.lockSyncing() contactContext.lockSyncing()
const sendPromise = context.resolveWhenAsyncFunctionCompletes(
context.contacts,
'sendOwnContactChangeEventToAllContacts',
)
await context.changePassword('new password') await context.changePassword('new password')
await sendPromise
const firstPartySpy = sinon.spy(context.asymmetric, 'handleTrustedSenderKeypairChangedMessage') const firstPartySpy = sinon.spy(context.asymmetric, 'handleTrustedSenderKeypairChangedMessage')
const secondPartySpy = sinon.spy(contactContext.asymmetric, 'handleTrustedSenderKeypairChangedMessage') const secondPartySpy = sinon.spy(contactContext.asymmetric, 'handleTrustedSenderKeypairChangedMessage')
const completedProcessingMessagesPromise = contactContext.resolveWhenAsymmetricMessageProcessingCompletes()
contactContext.unlockSyncing() contactContext.unlockSyncing()
await contactContext.sync() await contactContext.syncAndAwaitMessageProcessing()
await completedProcessingMessagesPromise
expect(firstPartySpy.callCount).to.equal(0) expect(firstPartySpy.callCount).to.equal(0)
expect(secondPartySpy.callCount).to.equal(1) expect(secondPartySpy.callCount).to.equal(1)
@@ -269,14 +264,12 @@ describe('asymmetric messages', function () {
await context.changePassword('new password') await context.changePassword('new password')
await context.vaults.changeVaultNameAndDescription(sharedVault, { await context.changeVaultName(sharedVault, {
name: 'New Name', name: 'New Name',
description: 'New Description', description: 'New Description',
}) })
const completedProcessingMessagesPromise = contactContext.resolveWhenAsymmetricMessageProcessingCompletes() await contactContext.syncAndAwaitMessageProcessing()
await contactContext.sync()
await completedProcessingMessagesPromise
const updatedVault = contactContext.vaults.getVault({ keySystemIdentifier: sharedVault.systemIdentifier }) const updatedVault = contactContext.vaults.getVault({ keySystemIdentifier: sharedVault.systemIdentifier })
expect(updatedVault.name).to.equal('New Name') expect(updatedVault.name).to.equal('New Name')
@@ -295,16 +288,14 @@ describe('asymmetric messages', function () {
await context.changePassword('new password') await context.changePassword('new password')
await context.vaults.changeVaultNameAndDescription(sharedVault, { await context.changeVaultName(sharedVault, {
name: 'New Name', name: 'New Name',
description: 'New Description', description: 'New Description',
}) })
context.lockSyncing() context.lockSyncing()
const completedProcessingMessagesPromise = contactContext.resolveWhenAsymmetricMessageProcessingCompletes() await contactContext.syncAndAwaitMessageProcessing()
await contactContext.sync()
await completedProcessingMessagesPromise
/** /**
* There's really no good way to await the exact call since * 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) await context.sleep(0.25)
const messages = await context.asymmetric.getInboundMessages() const messages = await context.asymmetric.getInboundMessages()
expect(messages.length).to.equal(0) expect(messages.getValue().length).to.equal(0)
await deinitContactContext() 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() const { contactContext, deinitContactContext } = await Collaboration.createContactContext()
await Collaboration.createTrustedContactForUserOfContext(context, contactContext) await Collaboration.createTrustedContactForUserOfContext(context, contactContext)
await Collaboration.createTrustedContactForUserOfContext(contactContext, context) await Collaboration.createTrustedContactForUserOfContext(contactContext, context)
@@ -326,9 +317,7 @@ describe('asymmetric messages', function () {
await context.changePassword('new_password') await context.changePassword('new_password')
const completedProcessingMessagesPromise = contactContext.resolveWhenAsymmetricMessageProcessingCompletes() await contactContext.syncAndAwaitMessageProcessing()
await contactContext.sync()
await completedProcessingMessagesPromise
const updatedContact = contactContext.contacts.findContact(context.userUuid) const updatedContact = contactContext.contacts.findContact(context.userUuid)
@@ -341,7 +330,7 @@ describe('asymmetric messages', function () {
await deinitContactContext() 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 { contactContext, deinitContactContext } = await Collaboration.createSharedVaultWithAcceptedInvite(context)
const oldKeyPair = context.encryption.getKeyPair() const oldKeyPair = context.encryption.getKeyPair()
@@ -352,9 +341,7 @@ describe('asymmetric messages', function () {
const secondPartySpy = sinon.spy(contactContext.asymmetric, 'handleTrustedSenderKeypairChangedMessage') const secondPartySpy = sinon.spy(contactContext.asymmetric, 'handleTrustedSenderKeypairChangedMessage')
await context.sync() await context.sync()
const completedProcessingMessagesPromise = contactContext.resolveWhenAsymmetricMessageProcessingCompletes() await contactContext.syncAndAwaitMessageProcessing()
await contactContext.sync()
await completedProcessingMessagesPromise
const message = secondPartySpy.args[0][0] const message = secondPartySpy.args[0][0]
const encryptedMessage = message.encrypted_message const encryptedMessage = message.encrypted_message
@@ -376,9 +363,7 @@ describe('asymmetric messages', function () {
const newKeyPair = context.encryption.getKeyPair() const newKeyPair = context.encryption.getKeyPair()
const newSigningKeyPair = context.encryption.getSigningKeyPair() const newSigningKeyPair = context.encryption.getSigningKeyPair()
const completedProcessingMessagesPromise = contactContext.resolveWhenAsymmetricMessageProcessingCompletes() await contactContext.syncAndAwaitMessageProcessing()
await contactContext.sync()
await completedProcessingMessagesPromise
const updatedContact = contactContext.contacts.findContact(context.userUuid) const updatedContact = contactContext.contacts.findContact(context.userUuid)
expect(updatedContact.publicKeySet.encryption).to.equal(newKeyPair.publicKey) expect(updatedContact.publicKeySet.encryption).to.equal(newKeyPair.publicKey)
@@ -395,7 +380,7 @@ describe('asymmetric messages', function () {
contactContext.lockSyncing() contactContext.lockSyncing()
await context.vaults.changeVaultNameAndDescription(sharedVault, { await context.changeVaultName(sharedVault, {
name: 'New Name', name: 'New Name',
description: 'New Description', description: 'New Description',
}) })
@@ -405,7 +390,7 @@ describe('asymmetric messages', function () {
await promise await promise
const messages = await contactContext.asymmetric.getInboundMessages() 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 }) const updatedVault = contactContext.vaults.getVault({ keySystemIdentifier: sharedVault.systemIdentifier })
expect(updatedVault.name).to.not.equal('New Name') expect(updatedVault.name).to.not.equal('New Name')
@@ -413,4 +398,85 @@ describe('asymmetric messages', function () {
await deinitContactContext() 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)
}) })

View File

@@ -17,7 +17,7 @@ describe('shared vault conflicts', function () {
beforeEach(async function () { beforeEach(async function () {
localStorage.clear() localStorage.clear()
context = await Factory.createAppContextWithRealCrypto() context = await Factory.createVaultsContextWithRealCrypto()
await context.launch() await context.launch()
await context.register() await context.register()

View File

@@ -17,7 +17,7 @@ describe('contacts', function () {
beforeEach(async function () { beforeEach(async function () {
localStorage.clear() localStorage.clear()
context = await Factory.createAppContextWithRealCrypto() context = await Factory.createVaultsContextWithRealCrypto()
await context.launch() await context.launch()
await context.register() await context.register()
@@ -101,6 +101,5 @@ describe('contacts', function () {
await deinitContactContext() 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 () => {})
})
}) })

View File

@@ -17,7 +17,7 @@ describe('shared vault crypto', function () {
beforeEach(async function () { beforeEach(async function () {
localStorage.clear() localStorage.clear()
context = await Factory.createAppContextWithRealCrypto() context = await Factory.createVaultsContextWithRealCrypto()
await context.launch() await context.launch()
await context.register() await context.register()
@@ -28,11 +28,13 @@ describe('shared vault crypto', function () {
const appIdentifier = context.identifier const appIdentifier = context.identifier
await context.deinit() await context.deinit()
let recreatedContext = await Factory.createAppContextWithRealCrypto(appIdentifier) let recreatedContext = await Factory.createVaultsContextWithRealCrypto(appIdentifier)
await recreatedContext.launch() await recreatedContext.launch()
expect(recreatedContext.encryption.getKeyPair()).to.not.be.undefined expect(recreatedContext.encryption.getKeyPair()).to.not.be.undefined
expect(recreatedContext.encryption.getSigningKeyPair()).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 () => { 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() 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 } = const { note, contactContext, deinitContactContext } =
await Collaboration.createSharedVaultWithAcceptedInviteAndNote(context) 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 */ /** Override decrypt result to return failing signature */
const objectToSpy = context.encryption const objectToSpy = context.encryption
@@ -103,6 +107,7 @@ describe('shared vault crypto', function () {
const decryptedPayloads = await objectToSpy.decryptSplit(split) const decryptedPayloads = await objectToSpy.decryptSplit(split)
expect(decryptedPayloads.length).to.equal(1) expect(decryptedPayloads.length).to.equal(1)
expect(decryptedPayloads[0].content_type).to.equal(ContentType.TYPES.Note)
const payload = decryptedPayloads[0] const payload = decryptedPayloads[0]
const mutatedPayload = new DecryptedPayload({ const mutatedPayload = new DecryptedPayload({
@@ -118,6 +123,7 @@ describe('shared vault crypto', function () {
return [mutatedPayload] return [mutatedPayload]
}) })
await context.sync() await context.sync()
let updatedNote = context.items.findItem(note.uuid) let updatedNote = context.items.findItem(note.uuid)
@@ -127,7 +133,7 @@ describe('shared vault crypto', function () {
const appIdentifier = context.identifier const appIdentifier = context.identifier
await context.deinit() await context.deinit()
let recreatedContext = await Factory.createAppContextWithRealCrypto(appIdentifier) let recreatedContext = await Factory.createVaultsContextWithRealCrypto(appIdentifier)
await recreatedContext.launch() await recreatedContext.launch()
updatedNote = recreatedContext.items.findItem(note.uuid) updatedNote = recreatedContext.items.findItem(note.uuid)
@@ -140,7 +146,7 @@ describe('shared vault crypto', function () {
await recreatedContext.deinit() await recreatedContext.deinit()
recreatedContext = await Factory.createAppContextWithRealCrypto(appIdentifier) recreatedContext = await Factory.createVaultsContextWithRealCrypto(appIdentifier)
await recreatedContext.launch() await recreatedContext.launch()
/** Decrypting from storage will now verify current user symmetric signature only */ /** Decrypting from storage will now verify current user symmetric signature only */

View File

@@ -18,7 +18,7 @@ describe('shared vault deletion', function () {
beforeEach(async function () { beforeEach(async function () {
localStorage.clear() localStorage.clear()
context = await Factory.createAppContextWithRealCrypto() context = await Factory.createVaultsContextWithRealCrypto()
await context.launch() await context.launch()
await context.register() await context.register()

View File

@@ -5,7 +5,7 @@ import * as Collaboration from '../lib/Collaboration.js'
chai.use(chaiAsPromised) chai.use(chaiAsPromised)
const expect = chai.expect const expect = chai.expect
describe.skip('shared vault files', function () { describe('shared vault files', function () {
this.timeout(Factory.TwentySecondTimeout) this.timeout(Factory.TwentySecondTimeout)
let context let context
@@ -19,7 +19,7 @@ describe.skip('shared vault files', function () {
beforeEach(async function () { beforeEach(async function () {
localStorage.clear() localStorage.clear()
context = await Factory.createAppContextWithRealCrypto() context = await Factory.createVaultsContextWithRealCrypto()
await context.launch() await context.launch()
await context.register() await context.register()
@@ -29,7 +29,7 @@ describe.skip('shared vault files', function () {
}) })
describe('private vaults', () => { 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 vault = await Collaboration.createPrivateVault(context)
const response = await fetch('/mocha/assets/small_file.md') const response = await fetch('/mocha/assets/small_file.md')
const buffer = new Uint8Array(await response.arrayBuffer()) 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 sharedVault = await Collaboration.createSharedVault(context)
const response = await fetch('/mocha/assets/small_file.md') const response = await fetch('/mocha/assets/small_file.md')
const buffer = new Uint8Array(await response.arrayBuffer()) const buffer = new Uint8Array(await response.arrayBuffer())

View File

@@ -17,7 +17,7 @@ describe.skip('vault importing', function () {
beforeEach(async function () { beforeEach(async function () {
localStorage.clear() localStorage.clear()
context = await Factory.createAppContextWithRealCrypto() context = await Factory.createVaultsContextWithRealCrypto()
await context.launch() await context.launch()
await context.register() await context.register()
@@ -39,7 +39,7 @@ describe.skip('vault importing', function () {
const backupData = await context.application.createEncryptedBackupFileForAutomatedDesktopBackups() const backupData = await context.application.createEncryptedBackupFileForAutomatedDesktopBackups()
const otherContext = await Factory.createAppContextWithRealCrypto() const otherContext = await Factory.createVaultsContextWithRealCrypto()
await otherContext.launch() await otherContext.launch()
await otherContext.application.importData(backupData) await otherContext.application.importData(backupData)

View File

@@ -17,7 +17,7 @@ describe('shared vault invites', function () {
beforeEach(async function () { beforeEach(async function () {
localStorage.clear() localStorage.clear()
context = await Factory.createAppContextWithRealCrypto() context = await Factory.createVaultsContextWithRealCrypto()
await context.launch() await context.launch()
await context.register() await context.register()
}) })

View File

@@ -17,7 +17,7 @@ describe('shared vault items', function () {
beforeEach(async function () { beforeEach(async function () {
localStorage.clear() localStorage.clear()
context = await Factory.createAppContextWithRealCrypto() context = await Factory.createVaultsContextWithRealCrypto()
await context.launch() await context.launch()
await context.register() await context.register()
@@ -38,13 +38,19 @@ describe('shared vault items', function () {
it('should add item to shared vault with contact', async () => { it('should add item to shared vault with contact', async () => {
const note = await context.createSyncedNote('foo', 'bar') 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 Collaboration.moveItemToVault(context, sharedVault, note)
await contactContext.sync()
const updatedNote = context.items.findItem(note.uuid) const updatedNote = context.items.findItem(note.uuid)
expect(updatedNote.key_system_identifier).to.equal(sharedVault.systemIdentifier) 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() 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 () => { 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) await Collaboration.createSharedVaultWithAcceptedInviteAndNote(context)
const { sharedVault: otherSharedVault } = await Collaboration.createSharedVaultAndInviteContact( const { sharedVault: otherSharedVault } = await Collaboration.createSharedVaultAndInviteContact(
@@ -126,6 +132,8 @@ describe('shared vault items', function () {
contact, contact,
) )
await Collaboration.acceptAllInvites(contactContext)
const updatedNote = await Collaboration.moveItemToVault(context, otherSharedVault, note) const updatedNote = await Collaboration.moveItemToVault(context, otherSharedVault, note)
expect(updatedNote.key_system_identifier).to.equal(otherSharedVault.systemIdentifier) expect(updatedNote.key_system_identifier).to.equal(otherSharedVault.systemIdentifier)
@@ -134,7 +142,6 @@ describe('shared vault items', function () {
await contactContext.sync() await contactContext.sync()
const receivedNote = contactContext.items.findItem(note.uuid) const receivedNote = contactContext.items.findItem(note.uuid)
expect(receivedNote).to.not.be.undefined
expect(receivedNote.title).to.equal(note.title) expect(receivedNote.title).to.equal(note.title)
expect(receivedNote.key_system_identifier).to.equal(otherSharedVault.systemIdentifier) expect(receivedNote.key_system_identifier).to.equal(otherSharedVault.systemIdentifier)
expect(receivedNote.shared_vault_uuid).to.equal(otherSharedVault.sharing.sharedVaultUuid) expect(receivedNote.shared_vault_uuid).to.equal(otherSharedVault.sharing.sharedVaultUuid)

View File

@@ -16,7 +16,7 @@ describe('vault key management', function () {
beforeEach(async function () { beforeEach(async function () {
localStorage.clear() localStorage.clear()
context = await Factory.createAppContextWithRealCrypto() context = await Factory.createVaultsContextWithRealCrypto()
await context.launch() await context.launch()
}) })

View File

@@ -17,7 +17,7 @@ describe('vault key rotation', function () {
beforeEach(async function () { beforeEach(async function () {
localStorage.clear() localStorage.clear()
context = await Factory.createAppContextWithRealCrypto() context = await Factory.createVaultsContextWithRealCrypto()
await context.launch() await context.launch()
await context.register() await context.register()
@@ -99,7 +99,7 @@ describe('vault key rotation', function () {
await context.vaults.rotateVaultRootKey(sharedVault) await context.vaults.rotateVaultRootKey(sharedVault)
await promise await promise
const outboundMessages = await context.asymmetric.getOutboundMessages() const outboundMessages = (await context.asymmetric.getOutboundMessages()).getValue()
const expectedMessages = ['root key change', 'vault metadata change'] const expectedMessages = ['root key change', 'vault metadata change']
expect(outboundMessages.length).to.equal(expectedMessages.length) expect(outboundMessages.length).to.equal(expectedMessages.length)
@@ -154,10 +154,8 @@ describe('vault key rotation', function () {
await context.vaults.rotateVaultRootKey(sharedVault) await context.vaults.rotateVaultRootKey(sharedVault)
await promise await promise
const contactPromise = contactContext.resolveWhenAsymmetricMessageProcessingCompletes()
contactContext.unlockSyncing() contactContext.unlockSyncing()
await contactContext.sync() await contactContext.syncAndAwaitMessageProcessing()
await contactPromise
const newPrimaryItemsKey = contactContext.keys.getPrimaryKeySystemItemsKey(sharedVault.systemIdentifier) const newPrimaryItemsKey = contactContext.keys.getPrimaryKeySystemItemsKey(sharedVault.systemIdentifier)
expect(newPrimaryItemsKey).to.not.be.undefined expect(newPrimaryItemsKey).to.not.be.undefined
@@ -217,7 +215,7 @@ describe('vault key rotation', function () {
await context.vaults.rotateVaultRootKey(sharedVault) await context.vaults.rotateVaultRootKey(sharedVault)
await firstPromise await firstPromise
const asymmetricMessageAfterFirstChange = await context.asymmetric.getOutboundMessages() const asymmetricMessageAfterFirstChange = (await context.asymmetric.getOutboundMessages()).getValue()
const expectedMessages = ['root key change', 'vault metadata change'] const expectedMessages = ['root key change', 'vault metadata change']
expect(asymmetricMessageAfterFirstChange.length).to.equal(expectedMessages.length) expect(asymmetricMessageAfterFirstChange.length).to.equal(expectedMessages.length)
@@ -227,7 +225,7 @@ describe('vault key rotation', function () {
await context.vaults.rotateVaultRootKey(sharedVault) await context.vaults.rotateVaultRootKey(sharedVault)
await secondPromise await secondPromise
const asymmetricMessageAfterSecondChange = await context.asymmetric.getOutboundMessages() const asymmetricMessageAfterSecondChange = (await context.asymmetric.getOutboundMessages()).getValue()
expect(asymmetricMessageAfterSecondChange.length).to.equal(expectedMessages.length) expect(asymmetricMessageAfterSecondChange.length).to.equal(expectedMessages.length)
const messageAfterSecondChange = asymmetricMessageAfterSecondChange[0] const messageAfterSecondChange = asymmetricMessageAfterSecondChange[0]

View File

@@ -17,13 +17,13 @@ describe('keypair change', function () {
beforeEach(async function () { beforeEach(async function () {
localStorage.clear() localStorage.clear()
context = await Factory.createAppContextWithRealCrypto() context = await Factory.createVaultsContextWithRealCrypto()
await context.launch() await context.launch()
await context.register() 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 } = const { note, contactContext, deinitContactContext } =
await Collaboration.createSharedVaultWithAcceptedInviteAndNote(context) await Collaboration.createSharedVaultWithAcceptedInviteAndNote(context)
@@ -39,20 +39,24 @@ describe('keypair change', function () {
publicKeyChain.push(context.publicKey) publicKeyChain.push(context.publicKey)
signingPublicKeyChain.push(context.signingPublicKey) signingPublicKeyChain.push(context.signingPublicKey)
await contactContext.runAnyRequestToPreventRefreshTokenFromExpiring()
await context.changePassword('new_password-2') await context.changePassword('new_password-2')
publicKeyChain.push(context.publicKey) publicKeyChain.push(context.publicKey)
signingPublicKeyChain.push(context.signingPublicKey) signingPublicKeyChain.push(context.signingPublicKey)
await contactContext.runAnyRequestToPreventRefreshTokenFromExpiring()
await context.changePassword('new_password-3') await context.changePassword('new_password-3')
publicKeyChain.push(context.publicKey) publicKeyChain.push(context.publicKey)
signingPublicKeyChain.push(context.signingPublicKey) signingPublicKeyChain.push(context.signingPublicKey)
await contactContext.runAnyRequestToPreventRefreshTokenFromExpiring()
await context.changeNoteTitleAndSync(note, 'new title') await context.changeNoteTitleAndSync(note, 'new title')
contactContext.unlockSyncing() contactContext.unlockSyncing()
const promise = contactContext.resolveWhenAsymmetricMessageProcessingCompletes() await contactContext.syncAndAwaitMessageProcessing()
await contactContext.sync()
await promise
const originatorContact = contactContext.contacts.findContact(context.userUuid) const originatorContact = contactContext.contacts.findContact(context.userUuid)
let currentKeySet = originatorContact.publicKeySet let currentKeySet = originatorContact.publicKeySet
@@ -70,7 +74,7 @@ describe('keypair change', function () {
expect(receivedNote.signatureData.result.passes).to.be.true expect(receivedNote.signatureData.result.passes).to.be.true
await deinitContactContext() await deinitContactContext()
}) }).timeout(Factory.ThirtySecondTimeout)
it('should not trust messages sent with previous key pair', async () => { it('should not trust messages sent with previous key pair', async () => {
const { sharedVault, contactContext, deinitContactContext } = const { sharedVault, contactContext, deinitContactContext } =
@@ -86,15 +90,13 @@ describe('keypair change', function () {
sinon.stub(context.encryption, 'getKeyPair').returns(previousKeyPair) sinon.stub(context.encryption, 'getKeyPair').returns(previousKeyPair)
sinon.stub(context.encryption, 'getSigningKeyPair').returns(previousSigningKeyPair) sinon.stub(context.encryption, 'getSigningKeyPair').returns(previousSigningKeyPair)
await context.vaults.changeVaultNameAndDescription(sharedVault, { await context.changeVaultName(sharedVault, {
name: 'New Name', name: 'New Name',
description: 'New Description', description: 'New Description',
}) })
contactContext.unlockSyncing() contactContext.unlockSyncing()
const promise = contactContext.resolveWhenAsymmetricMessageProcessingCompletes() await contactContext.syncAndAwaitMessageProcessing()
await contactContext.sync()
await promise
const updatedVault = contactContext.vaults.getVault({ keySystemIdentifier: sharedVault.systemIdentifier }) const updatedVault = contactContext.vaults.getVault({ keySystemIdentifier: sharedVault.systemIdentifier })
expect(updatedVault.name).to.equal(sharedVault.name) expect(updatedVault.name).to.equal(sharedVault.name)
@@ -133,23 +135,18 @@ describe('keypair change', function () {
contactContext.lockSyncing() contactContext.lockSyncing()
await context.vaults.changeVaultNameAndDescription(sharedVault, { await context.changeVaultName(sharedVault, {
name: 'New Name', name: 'New Name',
description: 'New Description', description: 'New Description',
}) })
const originalMessages = await contactContext.asymmetric.getInboundMessages() const originalMessages = (await contactContext.asymmetric.getInboundMessages()).getValue()
expect(originalMessages.length).to.equal(1) expect(originalMessages.length).to.equal(1)
const originalMessage = originalMessages[0] const originalMessage = originalMessages[0]
const promise = context.resolveWhenAsyncFunctionCompletes(
context.application.dependencies.get(TYPES.HandleKeyPairChange),
'execute',
)
await context.changePassword('new_password') 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'] const expectedMessages = ['keypair-change', 'vault-change']
expect(updatedMessages.length).to.equal(expectedMessages.length) expect(updatedMessages.length).to.equal(expectedMessages.length)

View File

@@ -17,7 +17,7 @@ describe('shared vault permissions', function () {
beforeEach(async function () { beforeEach(async function () {
localStorage.clear() localStorage.clear()
context = await Factory.createAppContextWithRealCrypto() context = await Factory.createVaultsContextWithRealCrypto()
await context.launch() await context.launch()
await context.register() await context.register()

View File

@@ -18,7 +18,7 @@ describe('public key cryptography', function () {
beforeEach(async function () { beforeEach(async function () {
localStorage.clear() localStorage.clear()
context = await Factory.createAppContextWithRealCrypto() context = await Factory.createVaultsContextWithRealCrypto()
await context.launch() await context.launch()
await context.register() await context.register()
@@ -40,7 +40,7 @@ describe('public key cryptography', function () {
const password = context.password const password = context.password
await context.signout() await context.signout()
const recreatedContext = await Factory.createAppContextWithRealCrypto() const recreatedContext = await Factory.createVaultsContextWithRealCrypto()
await recreatedContext.launch() await recreatedContext.launch()
recreatedContext.email = email recreatedContext.email = email
recreatedContext.password = password recreatedContext.password = password
@@ -51,6 +51,8 @@ describe('public key cryptography', function () {
expect(recreatedContext.sessions.getSigningPublicKey()).to.not.be.undefined expect(recreatedContext.sessions.getSigningPublicKey()).to.not.be.undefined
expect(recreatedContext.encryption.getSigningKeyPair().privateKey).to.not.be.undefined expect(recreatedContext.encryption.getSigningKeyPair().privateKey).to.not.be.undefined
await recreatedContext.deinit()
}) })
it('should rotate keypair during password change', async () => { 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 () => { 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.launch()
await newContext.register() await newContext.register()
@@ -94,5 +96,7 @@ describe('public key cryptography', function () {
expect(result.error).to.be.undefined expect(result.error).to.be.undefined
expect(newContext.application.sessions.isUserMissingKeyPair()).to.be.false expect(newContext.application.sessions.isUserMissingKeyPair()).to.be.false
await newContext.deinit()
}) })
}) })

View File

@@ -10,15 +10,10 @@ describe('shared vaults', function () {
let context let context
let vaults let vaults
afterEach(async function () {
await context.deinit()
localStorage.clear()
})
beforeEach(async function () { beforeEach(async function () {
localStorage.clear() localStorage.clear()
context = await Factory.createAppContextWithRealCrypto() context = await Factory.createVaultsContextWithRealCrypto()
await context.launch() await context.launch()
await context.register() await context.register()
@@ -26,11 +21,18 @@ describe('shared vaults', function () {
vaults = context.vaults vaults = context.vaults
}) })
afterEach(async function () {
await context.deinit()
localStorage.clear()
})
it('should update vault name and description', async () => { it('should update vault name and description', async () => {
const { sharedVault, contactContext, deinitContactContext } = const { sharedVault, contactContext, deinitContactContext } =
await Collaboration.createSharedVaultWithAcceptedInvite(context) await Collaboration.createSharedVaultWithAcceptedInvite(context)
await vaults.changeVaultNameAndDescription(sharedVault, { contactContext.lockSyncing()
await context.changeVaultName(sharedVault, {
name: 'new vault name', name: 'new vault name',
description: 'new vault description', description: 'new vault description',
}) })
@@ -39,9 +41,8 @@ describe('shared vaults', function () {
expect(updatedVault.name).to.equal('new vault name') expect(updatedVault.name).to.equal('new vault name')
expect(updatedVault.description).to.equal('new vault description') expect(updatedVault.description).to.equal('new vault description')
const promise = contactContext.resolveWhenAsymmetricMessageProcessingCompletes() contactContext.unlockSyncing()
await contactContext.sync() await contactContext.syncAndAwaitMessageProcessing()
await promise
const contactVault = contactContext.vaults.getVault({ keySystemIdentifier: sharedVault.systemIdentifier }) const contactVault = contactContext.vaults.getVault({ keySystemIdentifier: sharedVault.systemIdentifier })
expect(contactVault.name).to.equal('new vault name') 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.getPrimaryKeySystemRootKey(sharedVault.systemIdentifier)).to.be.undefined
expect(contactContext.keys.getKeySystemItemsKeys(sharedVault.systemIdentifier)).to.be.empty 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() await recreatedContext.launch()
expect(recreatedContext.vaults.getVault({ keySystemIdentifier: sharedVault.systemIdentifier })).to.be.undefined 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.getPrimaryKeySystemRootKey(sharedVault.systemIdentifier)).to.be.undefined
expect(contactContext.keys.getKeySystemItemsKeys(sharedVault.systemIdentifier)).to.be.empty 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() await recreatedContext.launch()
expect(recreatedContext.vaults.getVault({ keySystemIdentifier: sharedVault.systemIdentifier })).to.be.undefined expect(recreatedContext.vaults.getVault({ keySystemIdentifier: sharedVault.systemIdentifier })).to.be.undefined
@@ -127,4 +128,52 @@ describe('shared vaults', function () {
await deinitThirdPartyContext() 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)
})
}) })

View File

@@ -17,7 +17,7 @@ describe('signatures', function () {
beforeEach(async function () { beforeEach(async function () {
localStorage.clear() localStorage.clear()
context = await Factory.createAppContextWithRealCrypto() context = await Factory.createVaultsContextWithRealCrypto()
await context.launch() await context.launch()
await context.register() await context.register()

View File

@@ -17,7 +17,7 @@ describe('vaults', function () {
beforeEach(async function () { beforeEach(async function () {
localStorage.clear() localStorage.clear()
context = await Factory.createAppContextWithRealCrypto() context = await Factory.createVaultsContextWithRealCrypto()
await context.launch() await context.launch()
@@ -77,7 +77,7 @@ describe('vaults', function () {
await vaults.moveItemToVault(vault, note) await vaults.moveItemToVault(vault, note)
await context.deinit() await context.deinit()
const recreatedContext = await Factory.createAppContextWithRealCrypto(appIdentifier) const recreatedContext = await Factory.createVaultsContextWithRealCrypto(appIdentifier)
await recreatedContext.launch() await recreatedContext.launch()
const updatedNote = recreatedContext.items.findItem(note.uuid) const updatedNote = recreatedContext.items.findItem(note.uuid)
@@ -101,7 +101,7 @@ describe('vaults', function () {
await context.deinit() await context.deinit()
const recreatedContext = await Factory.createAppContextWithRealCrypto(appIdentifier) const recreatedContext = await Factory.createVaultsContextWithRealCrypto(appIdentifier)
await recreatedContext.launch() await recreatedContext.launch()
const notes = recreatedContext.notes const notes = recreatedContext.notes
@@ -128,7 +128,7 @@ describe('vaults', function () {
await context.deinit() await context.deinit()
const recreatedContext = await Factory.createAppContextWithRealCrypto(appIdentifier) const recreatedContext = await Factory.createVaultsContextWithRealCrypto(appIdentifier)
await recreatedContext.launch() await recreatedContext.launch()
const updatedNote = recreatedContext.items.findItem(note.uuid) const updatedNote = recreatedContext.items.findItem(note.uuid)
@@ -181,7 +181,7 @@ describe('vaults', function () {
await vaults.moveItemToVault(vault, note) await vaults.moveItemToVault(vault, note)
await context.deinit() await context.deinit()
const recreatedContext = await Factory.createAppContextWithRealCrypto(appIdentifier) const recreatedContext = await Factory.createVaultsContextWithRealCrypto(appIdentifier)
await recreatedContext.launch() await recreatedContext.launch()
const updatedNote = recreatedContext.items.findItem(note.uuid) const updatedNote = recreatedContext.items.findItem(note.uuid)

View File

@@ -0,0 +1 @@
export type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'none'

View File

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

View File

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

View File

@@ -1,8 +1,11 @@
export * from './Date/DateUtils' export * from './Date/DateUtils'
export * from './Deferred/Deferred' export * from './Deferred/Deferred'
export * from './Logger/Logger'
export * from './Logger/LoggerInterface'
export * from './Logger/LogLevel'
export * from './Utils/ClassNames' export * from './Utils/ClassNames'
export * from './Utils/Utils'
export * from './Utils/Debounce' export * from './Utils/Debounce'
export * from './Utils/Utils'
export * from './Uuid/Utils' export * from './Uuid/Utils'
export * from './Uuid/UuidGenerator' export * from './Uuid/UuidGenerator'
export * from './Uuid/UuidMap' export * from './Uuid/UuidMap'