Explorar el Código

feat: sending messages. (#651)

* feat: sending messages.

Co-authored-by: Mo <mo@standardnotes.com>

* fix: messages repository.

Co-authored-by: Mo <mo@standardnotes.com>

---------

Co-authored-by: Mo <mo@standardnotes.com>
Karol Sójko hace 1 año
padre
commit
ef49b0d3f8

+ 18 - 0
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()
+  })
+})

+ 4 - 0
packages/syncing-server/src/Domain/Message/MessageRepositoryInterface.ts

@@ -4,6 +4,10 @@ import { Message } from './Message'
 
 export interface MessageRepositoryInterface {
   findByUuid: (uuid: Uuid) => Promise<Message | null>
+  findByRecipientUuidAndReplaceabilityIdentifier: (dto: {
+    recipientUuid: Uuid
+    replaceabilityIdentifier: string
+  }) => Promise<Message | null>
   save(message: Message): Promise<void>
   remove(message: Message): Promise<void>
 }

+ 107 - 0
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<Message>
+
+    messageRepository = {} as jest.Mocked<MessageRepositoryInterface>
+    messageRepository.findByRecipientUuidAndReplaceabilityIdentifier = jest.fn().mockReturnValue(null)
+    messageRepository.remove = jest.fn()
+    messageRepository.save = jest.fn()
+
+    timer = {} as jest.Mocked<TimerInterface>
+    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()
+  })
+})

+ 59 - 0
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<Message> {
+  constructor(private messageRepository: MessageRepositoryInterface, private timer: TimerInterface) {}
+
+  async execute(dto: SendMessageToUserDTO): Promise<Result<Message>> {
+    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)
+  }
+}

+ 6 - 0
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
+}

+ 21 - 0
packages/syncing-server/src/Infra/TypeORM/TypeORMMessageRepository.ts

@@ -11,6 +11,27 @@ export class TypeORMMessageRepository implements MessageRepositoryInterface {
     private mapper: MapperInterface<Message, TypeORMMessage>,
   ) {}
 
+  async findByRecipientUuidAndReplaceabilityIdentifier(dto: {
+    recipientUuid: Uuid
+    replaceabilityIdentifier: string
+  }): Promise<Message | null> {
+    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<Message | null> {
     const persistence = await this.ormRepository
       .createQueryBuilder('message')