浏览代码

feat: add invite users to a shared vault. (#636)

Co-authored-by: Mo <mo@standardnotes.com>
Karol Sójko 2 年之前
父节点
当前提交
5dc5507039

+ 1 - 0
packages/syncing-server/src/Domain/SharedVault/User/Invite/SharedVaultInviteRepositoryInterface.ts

@@ -6,4 +6,5 @@ export interface SharedVaultInviteRepositoryInterface {
   findByUuid(sharedVaultInviteUuid: Uuid): Promise<SharedVaultInvite | null>
   save(sharedVaultInvite: SharedVaultInvite): Promise<void>
   remove(sharedVaultInvite: SharedVaultInvite): Promise<void>
+  findByUserUuidAndSharedVaultUuid(dto: { userUuid: Uuid; sharedVaultUuid: Uuid }): Promise<SharedVaultInvite | null>
 }

+ 191 - 0
packages/syncing-server/src/Domain/UseCase/InviteUserToSharedVault/InviteUserToSharedVault.spec.ts

@@ -0,0 +1,191 @@
+import { TimerInterface } from '@standardnotes/time'
+import { SharedVaultRepositoryInterface } from '../../SharedVault/SharedVaultRepositoryInterface'
+import { SharedVaultInviteRepositoryInterface } from '../../SharedVault/User/Invite/SharedVaultInviteRepositoryInterface'
+import { InviteUserToSharedVault } from './InviteUserToSharedVault'
+import { SharedVault } from '../../SharedVault/SharedVault'
+import { SharedVaultInvite } from '../../SharedVault/User/Invite/SharedVaultInvite'
+import { Uuid, Timestamps, Result } from '@standardnotes/domain-core'
+import { SharedVaultUserPermission } from '../../SharedVault/User/SharedVaultUserPermission'
+
+describe('InviteUserToSharedVault', () => {
+  let sharedVaultRepository: SharedVaultRepositoryInterface
+  let sharedVaultInviteRepository: SharedVaultInviteRepositoryInterface
+  let timer: TimerInterface
+  let sharedVault: SharedVault
+
+  const createUseCase = () => new InviteUserToSharedVault(sharedVaultRepository, sharedVaultInviteRepository, timer)
+
+  beforeEach(() => {
+    sharedVault = SharedVault.create({
+      fileUploadBytesLimit: 100,
+      fileUploadBytesUsed: 2,
+      userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
+      timestamps: Timestamps.create(123, 123).getValue(),
+    }).getValue()
+    sharedVaultRepository = {} as jest.Mocked<SharedVaultRepositoryInterface>
+    sharedVaultRepository.findByUuid = jest.fn().mockResolvedValue(sharedVault)
+
+    sharedVaultInviteRepository = {} as jest.Mocked<SharedVaultInviteRepositoryInterface>
+    sharedVaultInviteRepository.findByUserUuidAndSharedVaultUuid = jest.fn().mockResolvedValue(null)
+    sharedVaultInviteRepository.save = jest.fn()
+    sharedVaultInviteRepository.remove = jest.fn()
+
+    timer = {} as jest.Mocked<TimerInterface>
+    timer.getTimestampInMicroseconds = jest.fn().mockReturnValue(123)
+  })
+
+  it('should return a failure result if the shared vault uuid is invalid', async () => {
+    const useCase = createUseCase()
+
+    const result = await useCase.execute({
+      sharedVaultUuid: 'invalid',
+      senderUuid: '00000000-0000-0000-0000-000000000000',
+      recipientUuid: '00000000-0000-0000-0000-000000000000',
+      permission: SharedVaultUserPermission.PERMISSIONS.Read,
+      encryptedMessage: 'encryptedMessage',
+    })
+
+    expect(result.isFailed()).toBe(true)
+    expect(result.getError()).toBe('Given value is not a valid uuid: invalid')
+  })
+
+  it('should return a failure result if the shared vault does not exist', async () => {
+    const useCase = createUseCase()
+    sharedVaultRepository.findByUuid = jest.fn().mockResolvedValue(undefined)
+
+    const result = await useCase.execute({
+      sharedVaultUuid: '00000000-0000-0000-0000-000000000000',
+      senderUuid: '00000000-0000-0000-0000-000000000000',
+      recipientUuid: '00000000-0000-0000-0000-000000000000',
+      permission: SharedVaultUserPermission.PERMISSIONS.Read,
+      encryptedMessage: 'encryptedMessage',
+    })
+
+    expect(result.isFailed()).toBe(true)
+    expect(result.getError()).toBe('Attempting to invite a user to a non-existent shared vault')
+  })
+
+  it('should return a failure result if the sender uuid is invalid', async () => {
+    const useCase = createUseCase()
+
+    const result = await useCase.execute({
+      sharedVaultUuid: '00000000-0000-0000-0000-000000000000',
+      senderUuid: 'invalid',
+      recipientUuid: '00000000-0000-0000-0000-000000000000',
+      permission: SharedVaultUserPermission.PERMISSIONS.Read,
+      encryptedMessage: 'encryptedMessage',
+    })
+
+    expect(result.isFailed()).toBe(true)
+    expect(result.getError()).toBe('Given value is not a valid uuid: invalid')
+  })
+
+  it('should return a failure result if the recipient uuid is invalid', async () => {
+    const useCase = createUseCase()
+    sharedVaultRepository.findByUuid = jest.fn().mockResolvedValue(sharedVault)
+
+    const result = await useCase.execute({
+      sharedVaultUuid: '00000000-0000-0000-0000-000000000000',
+      senderUuid: '00000000-0000-0000-0000-000000000000',
+      recipientUuid: 'invalid',
+      permission: SharedVaultUserPermission.PERMISSIONS.Read,
+      encryptedMessage: 'encryptedMessage',
+    })
+
+    expect(result.isFailed()).toBe(true)
+    expect(result.getError()).toBe('Given value is not a valid uuid: invalid')
+  })
+
+  it('should remove an already existing invite', async () => {
+    const useCase = createUseCase()
+    sharedVaultInviteRepository.findByUserUuidAndSharedVaultUuid = jest
+      .fn()
+      .mockResolvedValue({} as jest.Mocked<SharedVaultInvite>)
+
+    const result = await useCase.execute({
+      sharedVaultUuid: '00000000-0000-0000-0000-000000000000',
+      senderUuid: '00000000-0000-0000-0000-000000000000',
+      recipientUuid: '00000000-0000-0000-0000-000000000000',
+      permission: SharedVaultUserPermission.PERMISSIONS.Read,
+      encryptedMessage: 'encryptedMessage',
+    })
+
+    expect(result.isFailed()).toBe(false)
+    expect(sharedVaultInviteRepository.remove).toHaveBeenCalled()
+  })
+
+  it('should create a shared vault invite', async () => {
+    const useCase = createUseCase()
+
+    const result = await useCase.execute({
+      sharedVaultUuid: '00000000-0000-0000-0000-000000000000',
+      senderUuid: '00000000-0000-0000-0000-000000000000',
+      recipientUuid: '00000000-0000-0000-0000-000000000000',
+      permission: SharedVaultUserPermission.PERMISSIONS.Read,
+      encryptedMessage: 'encryptedMessage',
+    })
+
+    expect(result.isFailed()).toBe(false)
+    expect(result.getValue().props.sharedVaultUuid.value).toBe('00000000-0000-0000-0000-000000000000')
+  })
+
+  it('should return a failure if the permission is invalid', async () => {
+    const useCase = createUseCase()
+
+    const result = await useCase.execute({
+      sharedVaultUuid: '00000000-0000-0000-0000-000000000000',
+      senderUuid: '00000000-0000-0000-0000-000000000000',
+      recipientUuid: '00000000-0000-0000-0000-000000000000',
+      permission: 'invalid',
+      encryptedMessage: 'encryptedMessage',
+    })
+
+    expect(result.isFailed()).toBe(true)
+    expect(result.getError()).toBe('Invalid shared vault user permission invalid')
+  })
+
+  it('should return a failure if the sender is not the owner of the shared vault', async () => {
+    const useCase = createUseCase()
+
+    sharedVault = SharedVault.create({
+      fileUploadBytesLimit: 100,
+      fileUploadBytesUsed: 2,
+      userUuid: Uuid.create('10000000-0000-0000-0000-000000000000').getValue(),
+      timestamps: Timestamps.create(123, 123).getValue(),
+    }).getValue()
+    sharedVaultRepository.findByUuid = jest.fn().mockResolvedValue(sharedVault)
+
+    const result = await useCase.execute({
+      sharedVaultUuid: '00000000-0000-0000-0000-000000000000',
+      senderUuid: '00000000-0000-0000-0000-000000000001',
+      recipientUuid: '00000000-0000-0000-0000-000000000000',
+      permission: SharedVaultUserPermission.PERMISSIONS.Read,
+      encryptedMessage: 'encryptedMessage',
+    })
+
+    expect(result.isFailed()).toBe(true)
+    expect(result.getError()).toBe('Only the owner of a shared vault can invite users to it')
+  })
+
+  it('should return a failure if the shared vault invite could not be created', async () => {
+    const useCase = createUseCase()
+
+    const mockSharedVaultInvite = jest.spyOn(SharedVaultInvite, 'create')
+    mockSharedVaultInvite.mockImplementation(() => {
+      return Result.fail('Oops')
+    })
+
+    const result = await useCase.execute({
+      sharedVaultUuid: '00000000-0000-0000-0000-000000000000',
+      senderUuid: '00000000-0000-0000-0000-000000000000',
+      recipientUuid: '00000000-0000-0000-0000-000000000000',
+      permission: SharedVaultUserPermission.PERMISSIONS.Read,
+      encryptedMessage: 'encryptedMessage',
+    })
+
+    expect(result.isFailed()).toBe(true)
+    expect(result.getError()).toBe('Oops')
+
+    mockSharedVaultInvite.mockRestore()
+  })
+})

+ 77 - 0
packages/syncing-server/src/Domain/UseCase/InviteUserToSharedVault/InviteUserToSharedVault.ts

@@ -0,0 +1,77 @@
+import { Result, Timestamps, UseCaseInterface, Uuid } from '@standardnotes/domain-core'
+import { SharedVaultInvite } from '../../SharedVault/User/Invite/SharedVaultInvite'
+import { SharedVaultRepositoryInterface } from '../../SharedVault/SharedVaultRepositoryInterface'
+import { InviteUserToSharedVaultDTO } from './InviteUserToSharedVaultDTO'
+import { SharedVaultInviteRepositoryInterface } from '../../SharedVault/User/Invite/SharedVaultInviteRepositoryInterface'
+import { TimerInterface } from '@standardnotes/time'
+import { SharedVaultUserPermission } from '../../SharedVault/User/SharedVaultUserPermission'
+
+export class InviteUserToSharedVault implements UseCaseInterface<SharedVaultInvite> {
+  constructor(
+    private sharedVaultRepository: SharedVaultRepositoryInterface,
+    private sharedVaultInviteRepository: SharedVaultInviteRepositoryInterface,
+    private timer: TimerInterface,
+  ) {}
+  async execute(dto: InviteUserToSharedVaultDTO): Promise<Result<SharedVaultInvite>> {
+    const sharedVaultUuidOrError = Uuid.create(dto.sharedVaultUuid)
+    if (sharedVaultUuidOrError.isFailed()) {
+      return Result.fail(sharedVaultUuidOrError.getError())
+    }
+    const sharedVaultUuid = sharedVaultUuidOrError.getValue()
+
+    const senderUuidOrError = Uuid.create(dto.senderUuid)
+    if (senderUuidOrError.isFailed()) {
+      return Result.fail(senderUuidOrError.getError())
+    }
+    const senderUuid = senderUuidOrError.getValue()
+
+    const recipientUuidOrError = Uuid.create(dto.recipientUuid)
+    if (recipientUuidOrError.isFailed()) {
+      return Result.fail(recipientUuidOrError.getError())
+    }
+    const recipientUuid = recipientUuidOrError.getValue()
+
+    const permissionOrError = SharedVaultUserPermission.create(dto.permission)
+    if (permissionOrError.isFailed()) {
+      return Result.fail(permissionOrError.getError())
+    }
+    const permission = permissionOrError.getValue()
+
+    const sharedVault = await this.sharedVaultRepository.findByUuid(sharedVaultUuid)
+    if (!sharedVault) {
+      return Result.fail('Attempting to invite a user to a non-existent shared vault')
+    }
+
+    if (sharedVault.props.userUuid.value !== senderUuid.value) {
+      return Result.fail('Only the owner of a shared vault can invite users to it')
+    }
+
+    const existingInvite = await this.sharedVaultInviteRepository.findByUserUuidAndSharedVaultUuid({
+      userUuid: recipientUuid,
+      sharedVaultUuid,
+    })
+    if (existingInvite) {
+      await this.sharedVaultInviteRepository.remove(existingInvite)
+    }
+
+    const sharedVaultInviteOrError = SharedVaultInvite.create({
+      encryptedMessage: dto.encryptedMessage,
+      userUuid: recipientUuid,
+      sharedVaultUuid,
+      senderUuid,
+      permission,
+      timestamps: Timestamps.create(
+        this.timer.getTimestampInMicroseconds(),
+        this.timer.getTimestampInMicroseconds(),
+      ).getValue(),
+    })
+    if (sharedVaultInviteOrError.isFailed()) {
+      return Result.fail(sharedVaultInviteOrError.getError())
+    }
+    const sharedVaultInvite = sharedVaultInviteOrError.getValue()
+
+    await this.sharedVaultInviteRepository.save(sharedVaultInvite)
+
+    return Result.ok(sharedVaultInvite)
+  }
+}

+ 7 - 0
packages/syncing-server/src/Domain/UseCase/InviteUserToSharedVault/InviteUserToSharedVaultDTO.ts

@@ -0,0 +1,7 @@
+export interface InviteUserToSharedVaultDTO {
+  sharedVaultUuid: string
+  senderUuid: string
+  recipientUuid: string
+  encryptedMessage: string
+  permission: string
+}

+ 22 - 1
packages/syncing-server/src/Infra/TypeORM/TypeORMSharedVaultInviteRepository.ts

@@ -11,6 +11,27 @@ export class TypeORMSharedVaultInviteRepository implements SharedVaultInviteRepo
     private mapper: MapperInterface<SharedVaultInvite, TypeORMSharedVaultInvite>,
   ) {}
 
+  async findByUserUuidAndSharedVaultUuid(dto: {
+    userUuid: Uuid
+    sharedVaultUuid: Uuid
+  }): Promise<SharedVaultInvite | null> {
+    const persistence = await this.ormRepository
+      .createQueryBuilder('shared_vault_invite')
+      .where('shared_vault_invite.user_uuid = :uuid', {
+        uuid: dto.userUuid.value,
+      })
+      .andWhere('shared_vault_invite.shared_vault_uuid = :sharedVaultUuid', {
+        sharedVaultUuid: dto.sharedVaultUuid.value,
+      })
+      .getOne()
+
+    if (persistence === null) {
+      return null
+    }
+
+    return this.mapper.toDomain(persistence)
+  }
+
   async save(sharedVaultInvite: SharedVaultInvite): Promise<void> {
     const persistence = this.mapper.toProjection(sharedVaultInvite)
 
@@ -21,7 +42,7 @@ export class TypeORMSharedVaultInviteRepository implements SharedVaultInviteRepo
     const persistence = await this.ormRepository
       .createQueryBuilder('shared_vault_invite')
       .where('shared_vault_invite.uuid = :uuid', {
-        uuid: uuid.toString(),
+        uuid: uuid.value,
       })
       .getOne()