소스 검색

feat: accept and decline shared vault invites (#645)

* feat: accept shared vault invite.

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

* feat: decline shared vault invite.

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

---------

Co-authored-by: Mo <mo@standardnotes.com>
Karol Sójko 1 년 전
부모
커밋
92f96ddb84

+ 107 - 0
packages/syncing-server/src/Domain/UseCase/AcceptInviteToSharedVault/AcceptInviteToSharedVault.spec.ts

@@ -0,0 +1,107 @@
+import { Result, Timestamps, Uuid } from '@standardnotes/domain-core'
+import { SharedVaultInviteRepositoryInterface } from '../../SharedVault/User/Invite/SharedVaultInviteRepositoryInterface'
+import { AddUserToSharedVault } from '../AddUserToSharedVault/AddUserToSharedVault'
+import { AcceptInviteToSharedVault } from './AcceptInviteToSharedVault'
+import { SharedVaultInvite } from '../../SharedVault/User/Invite/SharedVaultInvite'
+import { SharedVaultUserPermission } from '../../SharedVault/User/SharedVaultUserPermission'
+
+describe('AcceptInviteToSharedVault', () => {
+  let addUserToSharedVault: AddUserToSharedVault
+  let sharedVaultInviteRepository: SharedVaultInviteRepositoryInterface
+  let invite: SharedVaultInvite
+
+  const createUseCase = () => new AcceptInviteToSharedVault(addUserToSharedVault, sharedVaultInviteRepository)
+
+  beforeEach(() => {
+    invite = SharedVaultInvite.create({
+      sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
+      userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
+      senderUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
+      encryptedMessage: 'encrypted-message',
+      permission: SharedVaultUserPermission.create(SharedVaultUserPermission.PERMISSIONS.Read).getValue(),
+      timestamps: Timestamps.create(123, 123).getValue(),
+    }).getValue()
+
+    addUserToSharedVault = {} as jest.Mocked<AddUserToSharedVault>
+    addUserToSharedVault.execute = jest.fn().mockReturnValue(Result.ok())
+
+    sharedVaultInviteRepository = {} as jest.Mocked<SharedVaultInviteRepositoryInterface>
+    sharedVaultInviteRepository.findByUuid = jest.fn().mockResolvedValue(invite)
+    sharedVaultInviteRepository.remove = jest.fn()
+  })
+
+  it('should fail if invite uuid is invalid', async () => {
+    const useCase = createUseCase()
+
+    const result = await useCase.execute({
+      inviteUuid: 'invalid',
+      originatorUuid: '00000000-0000-0000-0000-000000000000',
+    })
+
+    expect(result.isFailed()).toBe(true)
+    expect(result.getError()).toBe('Given value is not a valid uuid: invalid')
+  })
+
+  it('should fail if originator uuid is invalid', async () => {
+    const useCase = createUseCase()
+
+    const result = await useCase.execute({
+      inviteUuid: '00000000-0000-0000-0000-000000000000',
+      originatorUuid: 'invalid',
+    })
+
+    expect(result.isFailed()).toBe(true)
+    expect(result.getError()).toBe('Given value is not a valid uuid: invalid')
+  })
+
+  it('should fail if invite is not found', async () => {
+    sharedVaultInviteRepository.findByUuid = jest.fn().mockResolvedValue(null)
+
+    const useCase = createUseCase()
+
+    const result = await useCase.execute({
+      inviteUuid: '00000000-0000-0000-0000-000000000000',
+      originatorUuid: '00000000-0000-0000-0000-000000000000',
+    })
+
+    expect(result.isFailed()).toBe(true)
+    expect(result.getError()).toBe('Invite not found')
+  })
+
+  it('should fail if originator is not the recipient of the invite', async () => {
+    const useCase = createUseCase()
+
+    const result = await useCase.execute({
+      inviteUuid: '00000000-0000-0000-0000-000000000000',
+      originatorUuid: '00000000-0000-0000-0000-000000000001',
+    })
+
+    expect(result.isFailed()).toBe(true)
+    expect(result.getError()).toBe('Only the recipient of the invite can accept it')
+  })
+
+  it('should fail if adding user to shared vault fails', async () => {
+    addUserToSharedVault.execute = jest.fn().mockReturnValue(Result.fail('Failed to add user to shared vault'))
+
+    const useCase = createUseCase()
+
+    const result = await useCase.execute({
+      inviteUuid: '00000000-0000-0000-0000-000000000000',
+      originatorUuid: '00000000-0000-0000-0000-000000000000',
+    })
+
+    expect(result.isFailed()).toBe(true)
+    expect(result.getError()).toBe('Failed to add user to shared vault')
+  })
+
+  it('should delete invite after adding user to shared vault', async () => {
+    const useCase = createUseCase()
+
+    await useCase.execute({
+      inviteUuid: '00000000-0000-0000-0000-000000000000',
+      originatorUuid: '00000000-0000-0000-0000-000000000000',
+    })
+
+    expect(sharedVaultInviteRepository.remove).toHaveBeenCalled()
+  })
+})

+ 47 - 0
packages/syncing-server/src/Domain/UseCase/AcceptInviteToSharedVault/AcceptInviteToSharedVault.ts

@@ -0,0 +1,47 @@
+import { Result, UseCaseInterface, Uuid } from '@standardnotes/domain-core'
+import { AcceptInviteToSharedVaultDTO } from './AcceptInviteToSharedVaultDTO'
+import { SharedVaultInviteRepositoryInterface } from '../../SharedVault/User/Invite/SharedVaultInviteRepositoryInterface'
+import { AddUserToSharedVault } from '../AddUserToSharedVault/AddUserToSharedVault'
+
+export class AcceptInviteToSharedVault implements UseCaseInterface<void> {
+  constructor(
+    private addUserToSharedVault: AddUserToSharedVault,
+    private sharedVaultInviteRepository: SharedVaultInviteRepositoryInterface,
+  ) {}
+
+  async execute(dto: AcceptInviteToSharedVaultDTO): Promise<Result<void>> {
+    const inviteUuidOrError = Uuid.create(dto.inviteUuid)
+    if (inviteUuidOrError.isFailed()) {
+      return Result.fail(inviteUuidOrError.getError())
+    }
+    const inviteUuid = inviteUuidOrError.getValue()
+
+    const originatorUuidOrError = Uuid.create(dto.originatorUuid)
+    if (originatorUuidOrError.isFailed()) {
+      return Result.fail(originatorUuidOrError.getError())
+    }
+    const originatorUuid = originatorUuidOrError.getValue()
+
+    const invite = await this.sharedVaultInviteRepository.findByUuid(inviteUuid)
+    if (!invite) {
+      return Result.fail('Invite not found')
+    }
+
+    if (!invite.props.userUuid.equals(originatorUuid)) {
+      return Result.fail('Only the recipient of the invite can accept it')
+    }
+
+    const result = await this.addUserToSharedVault.execute({
+      sharedVaultUuid: invite.props.sharedVaultUuid.value,
+      userUuid: invite.props.userUuid.value,
+      permission: invite.props.permission.value,
+    })
+    if (result.isFailed()) {
+      return Result.fail(result.getError())
+    }
+
+    await this.sharedVaultInviteRepository.remove(invite)
+
+    return Result.ok()
+  }
+}

+ 4 - 0
packages/syncing-server/src/Domain/UseCase/AcceptInviteToSharedVault/AcceptInviteToSharedVaultDTO.ts

@@ -0,0 +1,4 @@
+export interface AcceptInviteToSharedVaultDTO {
+  inviteUuid: string
+  originatorUuid: string
+}

+ 88 - 0
packages/syncing-server/src/Domain/UseCase/DeclineInviteToSharedVault/DeclineInviteToSharedVault.spec.ts

@@ -0,0 +1,88 @@
+import { Timestamps, Uuid } from '@standardnotes/domain-core'
+import { SharedVaultInviteRepositoryInterface } from '../../SharedVault/User/Invite/SharedVaultInviteRepositoryInterface'
+import { DeclineInviteToSharedVault } from './DeclineInviteToSharedVault'
+import { SharedVaultInvite } from '../../SharedVault/User/Invite/SharedVaultInvite'
+import { SharedVaultUserPermission } from '../../SharedVault/User/SharedVaultUserPermission'
+
+describe('DeclineInviteToSharedVault', () => {
+  let sharedVaultInviteRepository: SharedVaultInviteRepositoryInterface
+  let invite: SharedVaultInvite
+
+  const createUseCase = () => new DeclineInviteToSharedVault(sharedVaultInviteRepository)
+
+  beforeEach(() => {
+    invite = SharedVaultInvite.create({
+      sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
+      userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
+      senderUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
+      encryptedMessage: 'encrypted-message',
+      permission: SharedVaultUserPermission.create(SharedVaultUserPermission.PERMISSIONS.Read).getValue(),
+      timestamps: Timestamps.create(123, 123).getValue(),
+    }).getValue()
+
+    sharedVaultInviteRepository = {} as jest.Mocked<SharedVaultInviteRepositoryInterface>
+    sharedVaultInviteRepository.findByUuid = jest.fn().mockResolvedValue(invite)
+    sharedVaultInviteRepository.remove = jest.fn()
+  })
+
+  it('should fail if invite uuid is invalid', async () => {
+    const useCase = createUseCase()
+
+    const result = await useCase.execute({
+      inviteUuid: 'invalid',
+      originatorUuid: '00000000-0000-0000-0000-000000000000',
+    })
+
+    expect(result.isFailed()).toBe(true)
+    expect(result.getError()).toBe('Given value is not a valid uuid: invalid')
+  })
+
+  it('should fail if originator uuid is invalid', async () => {
+    const useCase = createUseCase()
+
+    const result = await useCase.execute({
+      inviteUuid: '00000000-0000-0000-0000-000000000000',
+      originatorUuid: 'invalid',
+    })
+
+    expect(result.isFailed()).toBe(true)
+    expect(result.getError()).toBe('Given value is not a valid uuid: invalid')
+  })
+
+  it('should fail if invite is not found', async () => {
+    sharedVaultInviteRepository.findByUuid = jest.fn().mockResolvedValue(null)
+
+    const useCase = createUseCase()
+
+    const result = await useCase.execute({
+      inviteUuid: '00000000-0000-0000-0000-000000000000',
+      originatorUuid: '00000000-0000-0000-0000-000000000000',
+    })
+
+    expect(result.isFailed()).toBe(true)
+    expect(result.getError()).toBe('Invite not found')
+  })
+
+  it('should fail if originator is not the recipient of the invite', async () => {
+    const useCase = createUseCase()
+
+    const result = await useCase.execute({
+      inviteUuid: '00000000-0000-0000-0000-000000000000',
+      originatorUuid: '00000000-0000-0000-0000-000000000001',
+    })
+
+    expect(result.isFailed()).toBe(true)
+    expect(result.getError()).toBe('Only the recipient of the invite can decline it')
+  })
+
+  it('should delete invite', async () => {
+    const useCase = createUseCase()
+
+    await useCase.execute({
+      inviteUuid: '00000000-0000-0000-0000-000000000000',
+      originatorUuid: '00000000-0000-0000-0000-000000000000',
+    })
+
+    expect(sharedVaultInviteRepository.remove).toHaveBeenCalled()
+  })
+})

+ 34 - 0
packages/syncing-server/src/Domain/UseCase/DeclineInviteToSharedVault/DeclineInviteToSharedVault.ts

@@ -0,0 +1,34 @@
+import { Result, UseCaseInterface, Uuid } from '@standardnotes/domain-core'
+import { DeclineInviteToSharedVaultDTO } from './DeclineInviteToSharedVaultDTO'
+import { SharedVaultInviteRepositoryInterface } from '../../SharedVault/User/Invite/SharedVaultInviteRepositoryInterface'
+
+export class DeclineInviteToSharedVault implements UseCaseInterface<void> {
+  constructor(private sharedVaultInviteRepository: SharedVaultInviteRepositoryInterface) {}
+
+  async execute(dto: DeclineInviteToSharedVaultDTO): Promise<Result<void>> {
+    const inviteUuidOrError = Uuid.create(dto.inviteUuid)
+    if (inviteUuidOrError.isFailed()) {
+      return Result.fail(inviteUuidOrError.getError())
+    }
+    const inviteUuid = inviteUuidOrError.getValue()
+
+    const originatorUuidOrError = Uuid.create(dto.originatorUuid)
+    if (originatorUuidOrError.isFailed()) {
+      return Result.fail(originatorUuidOrError.getError())
+    }
+    const originatorUuid = originatorUuidOrError.getValue()
+
+    const invite = await this.sharedVaultInviteRepository.findByUuid(inviteUuid)
+    if (!invite) {
+      return Result.fail('Invite not found')
+    }
+
+    if (!invite.props.userUuid.equals(originatorUuid)) {
+      return Result.fail('Only the recipient of the invite can decline it')
+    }
+
+    await this.sharedVaultInviteRepository.remove(invite)
+
+    return Result.ok()
+  }
+}

+ 4 - 0
packages/syncing-server/src/Domain/UseCase/DeclineInviteToSharedVault/DeclineInviteToSharedVaultDTO.ts

@@ -0,0 +1,4 @@
+export interface DeclineInviteToSharedVaultDTO {
+  inviteUuid: string
+  originatorUuid: string
+}