Files
standardnotes-app-web/packages/services/src/Domain/AsymmetricMessage/AsymmetricMessageService.spec.ts
Karol Sójko bd2172b773 chore: handle messages via websockets (#2473)
* chore: handle messages via websockets

* chore: update domain events

* fix: specs

* fix: adjust server revision creation delay
2023-09-01 13:07:15 +02:00

378 lines
14 KiB
TypeScript

import { GetKeyPairs } from './../Encryption/UseCase/GetKeyPairs'
import { GetVault } from './../Vault/UseCase/GetVault'
import { SessionsClientInterface } from './../Session/SessionsClientInterface'
import { EncryptionProviderInterface } from './../Encryption/EncryptionProviderInterface'
import { GetUntrustedPayload } from './UseCase/GetUntrustedPayload'
import { GetInboundMessages } from './UseCase/GetInboundMessages'
import { GetOutboundMessages } from './UseCase/GetOutboundMessages'
import { HandleRootKeyChangedMessage } from './UseCase/HandleRootKeyChangedMessage'
import { GetTrustedPayload } from './UseCase/GetTrustedPayload'
import { ReplaceContactData } from './../Contacts/UseCase/ReplaceContactData'
import { FindContact } from './../Contacts/UseCase/FindContact'
import { CreateOrEditContact } from './../Contacts/UseCase/CreateOrEditContact'
import { MutatorClientInterface } from './../Mutator/MutatorClientInterface'
import { AsymmetricMessageServer } from '@standardnotes/api'
import { AsymmetricMessageService } from './AsymmetricMessageService'
import { InternalEventBusInterface } from '../Internal/InternalEventBusInterface'
import { SyncServiceInterface } from '../Sync/SyncServiceInterface'
import { AsymmetricMessageServerHash } from '@standardnotes/responses'
import {
AsymmetricMessagePayloadType,
AsymmetricMessageSenderKeypairChanged,
AsymmetricMessageSharedVaultInvite,
AsymmetricMessageSharedVaultMetadataChanged,
AsymmetricMessageSharedVaultRootKeyChanged,
AsymmetricMessageTrustedContactShare,
KeySystemRootKeyContentSpecialized,
TrustedContactInterface,
} from '@standardnotes/models'
import { Result } from '@standardnotes/domain-core'
describe('AsymmetricMessageService', () => {
let sync: jest.Mocked<SyncServiceInterface>
let mutator: jest.Mocked<MutatorClientInterface>
let encryption: jest.Mocked<EncryptionProviderInterface>
let sessions: jest.Mocked<SessionsClientInterface>
let service: AsymmetricMessageService
beforeEach(() => {
const messageServer = {} as jest.Mocked<AsymmetricMessageServer>
messageServer.deleteMessage = jest.fn()
encryption = {} as jest.Mocked<EncryptionProviderInterface>
const createOrEditContact = {} as jest.Mocked<CreateOrEditContact>
const findContact = {} as jest.Mocked<FindContact>
const replaceContactData = {} as jest.Mocked<ReplaceContactData>
const getTrustedPayload = {} as jest.Mocked<GetTrustedPayload>
const getVault = {} as jest.Mocked<GetVault>
const handleRootKeyChangedMessage = {} as jest.Mocked<HandleRootKeyChangedMessage>
const getOutboundMessagesUseCase = {} as jest.Mocked<GetOutboundMessages>
const getInboundMessagesUseCase = {} as jest.Mocked<GetInboundMessages>
const getUntrustedPayload = {} as jest.Mocked<GetUntrustedPayload>
const getKeyPairs = {} as jest.Mocked<GetKeyPairs>
sync = {} as jest.Mocked<SyncServiceInterface>
sync.sync = jest.fn()
mutator = {} as jest.Mocked<MutatorClientInterface>
mutator.changeItem = jest.fn()
const eventBus = {} as jest.Mocked<InternalEventBusInterface>
eventBus.addEventHandler = jest.fn()
service = new AsymmetricMessageService(
encryption,
mutator,
sessions,
sync,
messageServer,
createOrEditContact,
findContact,
replaceContactData,
getTrustedPayload,
getVault,
handleRootKeyChangedMessage,
getOutboundMessagesUseCase,
getInboundMessagesUseCase,
getUntrustedPayload,
getKeyPairs,
eventBus,
)
})
describe('sortServerMessages', () => {
it('should prioritize keypair changed messages over other messages', () => {
const messages: AsymmetricMessageServerHash[] = [
{
uuid: 'keypair-changed-message',
recipient_uuid: '1',
sender_uuid: '2',
encrypted_message: 'encrypted_message',
created_at_timestamp: 2,
updated_at_timestamp: 2,
replaceability_identifier: null,
},
{
uuid: 'misc-message',
recipient_uuid: '1',
sender_uuid: '2',
encrypted_message: 'encrypted_message',
created_at_timestamp: 1,
updated_at_timestamp: 1,
replaceability_identifier: null,
},
]
service.getUntrustedMessagePayload = jest.fn()
service.getServerMessageType = jest.fn().mockImplementation((message) => {
if (message.uuid === 'keypair-changed-message') {
return AsymmetricMessagePayloadType.SenderKeypairChanged
} else {
return AsymmetricMessagePayloadType.ContactShare
}
})
const sorted = service.sortServerMessages(messages)
expect(sorted[0].uuid).toEqual('keypair-changed-message')
expect(sorted[1].uuid).toEqual('misc-message')
const reverseSorted = service.sortServerMessages(messages.reverse())
expect(reverseSorted[0].uuid).toEqual('keypair-changed-message')
expect(reverseSorted[1].uuid).toEqual('misc-message')
})
})
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,
replaceability_identifier: null,
}
const decryptedMessagePayload: AsymmetricMessageTrustedContactShare = {
type: AsymmetricMessagePayloadType.ContactShare,
data: {
recipientUuid: '1',
trustedContact: {} as TrustedContactInterface,
},
}
service.getTrustedMessagePayload = service.getUntrustedMessagePayload = jest
.fn()
.mockReturnValue(Result.ok(decryptedMessagePayload))
service.handleTrustedContactShareMessage = jest.fn()
await service.handleTrustedMessageResult(message, decryptedMessagePayload)
expect(service.handleTrustedContactShareMessage).toHaveBeenCalledTimes(1)
service.handleTrustedContactShareMessage = jest.fn()
await service.handleTrustedMessageResult(message, decryptedMessagePayload)
expect(service.handleTrustedContactShareMessage).toHaveBeenCalledTimes(0)
})
})
it('should process incoming messages oldest first', async () => {
const messages: AsymmetricMessageServerHash[] = [
{
uuid: 'newer-message',
recipient_uuid: '1',
sender_uuid: '2',
encrypted_message: 'encrypted_message',
created_at_timestamp: 2,
updated_at_timestamp: 2,
replaceability_identifier: null,
},
{
uuid: 'older-message',
recipient_uuid: '1',
sender_uuid: '2',
encrypted_message: 'encrypted_message',
created_at_timestamp: 1,
updated_at_timestamp: 1,
replaceability_identifier: null,
},
]
const trustedPayloadMock = { type: AsymmetricMessagePayloadType.ContactShare, data: { recipientUuid: '1' } }
service.getTrustedMessagePayload = service.getUntrustedMessagePayload = jest
.fn()
.mockReturnValue(Result.ok(trustedPayloadMock))
const handleTrustedContactShareMessageMock = jest.fn()
service.handleTrustedContactShareMessage = handleTrustedContactShareMessageMock
await service.handleRemoteReceivedAsymmetricMessages(messages)
expect(handleTrustedContactShareMessageMock.mock.calls[0][0]).toEqual(messages[1])
expect(handleTrustedContactShareMessageMock.mock.calls[1][0]).toEqual(messages[0])
})
it('should handle ContactShare message', async () => {
const message: AsymmetricMessageServerHash = {
uuid: 'message',
recipient_uuid: '1',
sender_uuid: '2',
encrypted_message: 'encrypted_message',
created_at_timestamp: 2,
updated_at_timestamp: 2,
replaceability_identifier: null,
}
const decryptedMessagePayload: AsymmetricMessageTrustedContactShare = {
type: AsymmetricMessagePayloadType.ContactShare,
data: {
recipientUuid: '1',
trustedContact: {} as TrustedContactInterface,
},
}
service.handleTrustedContactShareMessage = jest.fn()
service.getTrustedMessagePayload = service.getUntrustedMessagePayload = jest
.fn()
.mockReturnValue(Result.ok(decryptedMessagePayload))
await service.handleRemoteReceivedAsymmetricMessages([message])
expect(service.handleTrustedContactShareMessage).toHaveBeenCalledWith(message, decryptedMessagePayload)
})
it('should handle SenderKeypairChanged message', async () => {
const message: AsymmetricMessageServerHash = {
uuid: 'message',
recipient_uuid: '1',
sender_uuid: '2',
encrypted_message: 'encrypted_message',
created_at_timestamp: 2,
updated_at_timestamp: 2,
replaceability_identifier: null,
}
const decryptedMessagePayload: AsymmetricMessageSenderKeypairChanged = {
type: AsymmetricMessagePayloadType.SenderKeypairChanged,
data: {
recipientUuid: '1',
newEncryptionPublicKey: 'new-encryption-public-key',
newSigningPublicKey: 'new-signing-public-key',
},
}
service.handleTrustedSenderKeypairChangedMessage = jest.fn()
service.getTrustedMessagePayload = service.getUntrustedMessagePayload = jest
.fn()
.mockReturnValue(Result.ok(decryptedMessagePayload))
await service.handleRemoteReceivedAsymmetricMessages([message])
expect(service.handleTrustedSenderKeypairChangedMessage).toHaveBeenCalledWith(message, decryptedMessagePayload)
})
it('should handle SharedVaultRootKeyChanged message', async () => {
const message: AsymmetricMessageServerHash = {
uuid: 'message',
recipient_uuid: '1',
sender_uuid: '2',
encrypted_message: 'encrypted_message',
created_at_timestamp: 2,
updated_at_timestamp: 2,
replaceability_identifier: null,
}
const decryptedMessagePayload: AsymmetricMessageSharedVaultRootKeyChanged = {
type: AsymmetricMessagePayloadType.SharedVaultRootKeyChanged,
data: {
recipientUuid: '1',
rootKey: {} as KeySystemRootKeyContentSpecialized,
},
}
service.handleTrustedSharedVaultRootKeyChangedMessage = jest.fn()
service.getTrustedMessagePayload = service.getUntrustedMessagePayload = jest
.fn()
.mockReturnValue(Result.ok(decryptedMessagePayload))
await service.handleRemoteReceivedAsymmetricMessages([message])
expect(service.handleTrustedSharedVaultRootKeyChangedMessage).toHaveBeenCalledWith(message, decryptedMessagePayload)
})
it('should handle SharedVaultMetadataChanged message', async () => {
const message: AsymmetricMessageServerHash = {
uuid: 'message',
recipient_uuid: '1',
sender_uuid: '2',
encrypted_message: 'encrypted_message',
created_at_timestamp: 2,
updated_at_timestamp: 2,
replaceability_identifier: null,
}
const decryptedMessagePayload: AsymmetricMessageSharedVaultMetadataChanged = {
type: AsymmetricMessagePayloadType.SharedVaultMetadataChanged,
data: {
recipientUuid: '1',
sharedVaultUuid: 'shared-vault-uuid',
name: 'Vault name',
description: 'Vault description',
},
}
service.handleTrustedVaultMetadataChangedMessage = jest.fn()
service.getTrustedMessagePayload = service.getUntrustedMessagePayload = jest
.fn()
.mockReturnValue(Result.ok(decryptedMessagePayload))
await service.handleRemoteReceivedAsymmetricMessages([message])
expect(service.handleTrustedVaultMetadataChangedMessage).toHaveBeenCalledWith(message, decryptedMessagePayload)
})
it('should throw if message type is SharedVaultInvite', async () => {
const message: AsymmetricMessageServerHash = {
uuid: 'message',
recipient_uuid: '1',
sender_uuid: '2',
encrypted_message: 'encrypted_message',
created_at_timestamp: 2,
updated_at_timestamp: 2,
replaceability_identifier: null,
}
const decryptedMessagePayload: AsymmetricMessageSharedVaultInvite = {
type: AsymmetricMessagePayloadType.SharedVaultInvite,
data: {
recipientUuid: '1',
},
} as AsymmetricMessageSharedVaultInvite
service.getTrustedMessagePayload = service.getUntrustedMessagePayload = jest
.fn()
.mockReturnValue(Result.ok(decryptedMessagePayload))
await expect(service.handleRemoteReceivedAsymmetricMessages([message])).rejects.toThrow(
'Shared vault invites payloads are not handled as part of asymmetric messages',
)
})
it('should delete message from server after processing', async () => {
const message: AsymmetricMessageServerHash = {
uuid: 'message',
recipient_uuid: '1',
sender_uuid: '2',
encrypted_message: 'encrypted_message',
created_at_timestamp: 2,
updated_at_timestamp: 2,
replaceability_identifier: null,
}
const decryptedMessagePayload: AsymmetricMessageTrustedContactShare = {
type: AsymmetricMessagePayloadType.ContactShare,
data: {
recipientUuid: '1',
trustedContact: {} as TrustedContactInterface,
},
}
service.deleteMessageAfterProcessing = jest.fn()
service.handleTrustedContactShareMessage = jest.fn()
service.getTrustedMessagePayload = service.getUntrustedMessagePayload = jest
.fn()
.mockReturnValue(Result.ok(decryptedMessagePayload))
await service.handleRemoteReceivedAsymmetricMessages([message])
expect(service.deleteMessageAfterProcessing).toHaveBeenCalled()
})
})