From ef49b0d3f8ab76dfa63a4c691feb9f35ad65c46f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Karol=20S=C3=B3jko?= Date: Mon, 10 Jul 2023 13:10:31 +0200 Subject: [PATCH] feat: sending messages. (#651) * feat: sending messages. Co-authored-by: Mo * fix: messages repository. Co-authored-by: Mo --------- Co-authored-by: Mo --- .../src/Domain/Message/Message.spec.ts | 18 +++ .../Message/MessageRepositoryInterface.ts | 4 + .../SendMessageToUser.spec.ts | 107 ++++++++++++++++++ .../SendMessageToUser/SendMessageToUser.ts | 59 ++++++++++ .../SendMessageToUser/SendMessageToUserDTO.ts | 6 + .../Infra/TypeORM/TypeORMMessageRepository.ts | 21 ++++ 6 files changed, 215 insertions(+) create mode 100644 packages/syncing-server/src/Domain/Message/Message.spec.ts create mode 100644 packages/syncing-server/src/Domain/UseCase/Messaging/SendMessageToUser/SendMessageToUser.spec.ts create mode 100644 packages/syncing-server/src/Domain/UseCase/Messaging/SendMessageToUser/SendMessageToUser.ts create mode 100644 packages/syncing-server/src/Domain/UseCase/Messaging/SendMessageToUser/SendMessageToUserDTO.ts diff --git a/packages/syncing-server/src/Domain/Message/Message.spec.ts b/packages/syncing-server/src/Domain/Message/Message.spec.ts new file mode 100644 index 000000000..a681a83f1 --- /dev/null +++ b/packages/syncing-server/src/Domain/Message/Message.spec.ts @@ -0,0 +1,18 @@ +import { Timestamps, Uuid } from '@standardnotes/domain-core' + +import { Message } from './Message' + +describe('Message', () => { + it('should create an entity', () => { + const entityOrError = Message.create({ + timestamps: Timestamps.create(123456789, 123456789).getValue(), + recipientUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(), + senderUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(), + encryptedMessage: 'encryptedMessage', + replaceabilityIdentifier: 'replaceabilityIdentifier', + }) + + expect(entityOrError.isFailed()).toBeFalsy() + expect(entityOrError.getValue().id).not.toBeNull() + }) +}) diff --git a/packages/syncing-server/src/Domain/Message/MessageRepositoryInterface.ts b/packages/syncing-server/src/Domain/Message/MessageRepositoryInterface.ts index 5f664056d..b4812f4a4 100644 --- a/packages/syncing-server/src/Domain/Message/MessageRepositoryInterface.ts +++ b/packages/syncing-server/src/Domain/Message/MessageRepositoryInterface.ts @@ -4,6 +4,10 @@ import { Message } from './Message' export interface MessageRepositoryInterface { findByUuid: (uuid: Uuid) => Promise + findByRecipientUuidAndReplaceabilityIdentifier: (dto: { + recipientUuid: Uuid + replaceabilityIdentifier: string + }) => Promise save(message: Message): Promise remove(message: Message): Promise } diff --git a/packages/syncing-server/src/Domain/UseCase/Messaging/SendMessageToUser/SendMessageToUser.spec.ts b/packages/syncing-server/src/Domain/UseCase/Messaging/SendMessageToUser/SendMessageToUser.spec.ts new file mode 100644 index 000000000..5452bf156 --- /dev/null +++ b/packages/syncing-server/src/Domain/UseCase/Messaging/SendMessageToUser/SendMessageToUser.spec.ts @@ -0,0 +1,107 @@ +import { TimerInterface } from '@standardnotes/time' +import { MessageRepositoryInterface } from '../../../Message/MessageRepositoryInterface' +import { SendMessageToUser } from './SendMessageToUser' +import { Message } from '../../../Message/Message' +import { Result } from '@standardnotes/domain-core' + +describe('SendMessageToUser', () => { + let messageRepository: MessageRepositoryInterface + let timer: TimerInterface + let existingMessage: Message + + const createUseCase = () => new SendMessageToUser(messageRepository, timer) + + beforeEach(() => { + existingMessage = {} as jest.Mocked + + messageRepository = {} as jest.Mocked + messageRepository.findByRecipientUuidAndReplaceabilityIdentifier = jest.fn().mockReturnValue(null) + messageRepository.remove = jest.fn() + messageRepository.save = jest.fn() + + timer = {} as jest.Mocked + timer.getTimestampInMicroseconds = jest.fn().mockReturnValue(123456789) + }) + + it('saves a new message', async () => { + const useCase = createUseCase() + + const result = await useCase.execute({ + recipientUuid: '00000000-0000-0000-0000-000000000000', + senderUuid: '00000000-0000-0000-0000-000000000000', + encryptedMessage: 'encrypted-message', + }) + + expect(result.isFailed()).toBeFalsy() + }) + + it('removes existing message with the same replaceability identifier', async () => { + messageRepository.findByRecipientUuidAndReplaceabilityIdentifier = jest.fn().mockReturnValue(existingMessage) + const useCase = createUseCase() + + const result = await useCase.execute({ + recipientUuid: '00000000-0000-0000-0000-000000000000', + senderUuid: '00000000-0000-0000-0000-000000000000', + encryptedMessage: 'encrypted-message', + replaceabilityIdentifier: 'replaceability-identifier', + }) + + expect(result.isFailed()).toBeFalsy() + expect(messageRepository.remove).toHaveBeenCalledWith(existingMessage) + }) + + it('returns error when recipient uuid is invalid', async () => { + const useCase = createUseCase() + + const result = await useCase.execute({ + recipientUuid: 'invalid-uuid', + senderUuid: '00000000-0000-0000-0000-000000000000', + encryptedMessage: 'encrypted-message', + }) + + expect(result.isFailed()).toBeTruthy() + }) + + it('returns error when sender uuid is invalid', async () => { + const useCase = createUseCase() + + const result = await useCase.execute({ + recipientUuid: '00000000-0000-0000-0000-000000000000', + senderUuid: 'invalid-uuid', + encryptedMessage: 'encrypted-message', + }) + + expect(result.isFailed()).toBeTruthy() + }) + + it('returns error when message is empty', async () => { + const useCase = createUseCase() + + const result = await useCase.execute({ + recipientUuid: '00000000-0000-0000-0000-000000000000', + senderUuid: '00000000-0000-0000-0000-000000000000', + encryptedMessage: '', + }) + + expect(result.isFailed()).toBeTruthy() + }) + + it('returns error when message fails to create', async () => { + const mock = jest.spyOn(Message, 'create') + mock.mockImplementation(() => { + return Result.fail('Oops') + }) + + const useCase = createUseCase() + + const result = await useCase.execute({ + recipientUuid: '00000000-0000-0000-0000-000000000000', + senderUuid: '00000000-0000-0000-0000-000000000000', + encryptedMessage: 'encrypted-message', + }) + + expect(result.isFailed()).toBeTruthy() + + mock.mockRestore() + }) +}) diff --git a/packages/syncing-server/src/Domain/UseCase/Messaging/SendMessageToUser/SendMessageToUser.ts b/packages/syncing-server/src/Domain/UseCase/Messaging/SendMessageToUser/SendMessageToUser.ts new file mode 100644 index 000000000..63090a4c8 --- /dev/null +++ b/packages/syncing-server/src/Domain/UseCase/Messaging/SendMessageToUser/SendMessageToUser.ts @@ -0,0 +1,59 @@ +import { Result, Timestamps, UseCaseInterface, Uuid, Validator } from '@standardnotes/domain-core' +import { TimerInterface } from '@standardnotes/time' + +import { SendMessageToUserDTO } from './SendMessageToUserDTO' +import { MessageRepositoryInterface } from '../../../Message/MessageRepositoryInterface' +import { Message } from '../../../Message/Message' + +export class SendMessageToUser implements UseCaseInterface { + constructor(private messageRepository: MessageRepositoryInterface, private timer: TimerInterface) {} + + async execute(dto: SendMessageToUserDTO): Promise> { + const recipientUuidOrError = Uuid.create(dto.recipientUuid) + if (recipientUuidOrError.isFailed()) { + return Result.fail(recipientUuidOrError.getError()) + } + const recipientUuid = recipientUuidOrError.getValue() + + const senderUuidOrError = Uuid.create(dto.senderUuid) + if (senderUuidOrError.isFailed()) { + return Result.fail(senderUuidOrError.getError()) + } + const senderUuid = senderUuidOrError.getValue() + + const validateNotEmptyMessage = Validator.isNotEmpty(dto.encryptedMessage) + if (validateNotEmptyMessage.isFailed()) { + return Result.fail(validateNotEmptyMessage.getError()) + } + + if (dto.replaceabilityIdentifier) { + const existingMessage = await this.messageRepository.findByRecipientUuidAndReplaceabilityIdentifier({ + recipientUuid, + replaceabilityIdentifier: dto.replaceabilityIdentifier, + }) + + if (existingMessage) { + await this.messageRepository.remove(existingMessage) + } + } + + const messageOrError = Message.create({ + recipientUuid, + senderUuid, + encryptedMessage: dto.encryptedMessage, + timestamps: Timestamps.create( + this.timer.getTimestampInMicroseconds(), + this.timer.getTimestampInMicroseconds(), + ).getValue(), + replaceabilityIdentifier: dto.replaceabilityIdentifier ?? null, + }) + if (messageOrError.isFailed()) { + return Result.fail(messageOrError.getError()) + } + const message = messageOrError.getValue() + + await this.messageRepository.save(message) + + return Result.ok(message) + } +} diff --git a/packages/syncing-server/src/Domain/UseCase/Messaging/SendMessageToUser/SendMessageToUserDTO.ts b/packages/syncing-server/src/Domain/UseCase/Messaging/SendMessageToUser/SendMessageToUserDTO.ts new file mode 100644 index 000000000..f39728a2d --- /dev/null +++ b/packages/syncing-server/src/Domain/UseCase/Messaging/SendMessageToUser/SendMessageToUserDTO.ts @@ -0,0 +1,6 @@ +export interface SendMessageToUserDTO { + recipientUuid: string + senderUuid: string + encryptedMessage: string + replaceabilityIdentifier?: string +} diff --git a/packages/syncing-server/src/Infra/TypeORM/TypeORMMessageRepository.ts b/packages/syncing-server/src/Infra/TypeORM/TypeORMMessageRepository.ts index b0ec93e69..ef1bfe6a5 100644 --- a/packages/syncing-server/src/Infra/TypeORM/TypeORMMessageRepository.ts +++ b/packages/syncing-server/src/Infra/TypeORM/TypeORMMessageRepository.ts @@ -11,6 +11,27 @@ export class TypeORMMessageRepository implements MessageRepositoryInterface { private mapper: MapperInterface, ) {} + async findByRecipientUuidAndReplaceabilityIdentifier(dto: { + recipientUuid: Uuid + replaceabilityIdentifier: string + }): Promise { + const persistence = await this.ormRepository + .createQueryBuilder('message') + .where('message.recipientUuid = :recipientUuid', { + recipientUuid: dto.recipientUuid.value, + }) + .andWhere('message.replaceabilityIdentifier = :replaceabilityIdentifier', { + replaceabilityIdentifier: dto.replaceabilityIdentifier, + }) + .getOne() + + if (persistence === null) { + return null + } + + return this.mapper.toDomain(persistence) + } + async findByUuid(uuid: Uuid): Promise { const persistence = await this.ormRepository .createQueryBuilder('message')