Explorar el Código

feat: add use case for creating shared vaults and adding users to it. (#633)

Co-authored-by: Mo <mo@standardnotes.com>
Karol Sójko hace 2 años
padre
commit
4df8c3b2e5

+ 17 - 0
packages/syncing-server/src/Domain/SharedVault/SharedVault.spec.ts

@@ -0,0 +1,17 @@
+import { Timestamps, Uuid } from '@standardnotes/domain-core'
+
+import { SharedVault } from './SharedVault'
+
+describe('SharedVault', () => {
+  it('should create an entity', () => {
+    const entityOrError = SharedVault.create({
+      fileUploadBytesLimit: 1_000_000,
+      fileUploadBytesUsed: 0,
+      timestamps: Timestamps.create(123456789, 123456789).getValue(),
+      userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
+    })
+
+    expect(entityOrError.isFailed()).toBeFalsy()
+    expect(entityOrError.getValue().id).not.toBeNull()
+  })
+})

+ 18 - 0
packages/syncing-server/src/Domain/SharedVault/User/SharedVaultUser.spec.ts

@@ -0,0 +1,18 @@
+import { Timestamps, Uuid } from '@standardnotes/domain-core'
+
+import { SharedVaultUser } from './SharedVaultUser'
+import { SharedVaultUserPermission } from './SharedVaultUserPermission'
+
+describe('SharedVaultUser', () => {
+  it('should create an entity', () => {
+    const entityOrError = SharedVaultUser.create({
+      permission: SharedVaultUserPermission.create('read').getValue(),
+      sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
+      timestamps: Timestamps.create(123456789, 123456789).getValue(),
+      userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
+    })
+
+    expect(entityOrError.isFailed()).toBeFalsy()
+    expect(entityOrError.getValue().id).not.toBeNull()
+  })
+})

+ 16 - 0
packages/syncing-server/src/Domain/SharedVault/User/SharedVaultUserPermission.spec.ts

@@ -0,0 +1,16 @@
+import { SharedVaultUserPermission } from './SharedVaultUserPermission'
+
+describe('SharedVaultUserPermission', () => {
+  it('should create a value object', () => {
+    const valueOrError = SharedVaultUserPermission.create('read')
+
+    expect(valueOrError.isFailed()).toBeFalsy()
+    expect(valueOrError.getValue().value).toEqual('read')
+  })
+
+  it('should not create an invalid value object', () => {
+    const valueOrError = SharedVaultUserPermission.create('TEST')
+
+    expect(valueOrError.isFailed()).toBeTruthy()
+  })
+})

+ 139 - 0
packages/syncing-server/src/Domain/UseCase/AddUserToSharedVault/AddUserToSharedVault.spec.ts

@@ -0,0 +1,139 @@
+import { TimerInterface } from '@standardnotes/time'
+import { SharedVaultRepositoryInterface } from '../../SharedVault/SharedVaultRepositoryInterface'
+import { SharedVaultUserRepositoryInterface } from '../../SharedVault/User/SharedVaultUserRepositoryInterface'
+import { RemoveUserEvents } from '../RemoveUserEvents/RemoveUserEvents'
+import { AddUserToSharedVault } from './AddUserToSharedVault'
+import { Result } from '@standardnotes/domain-core'
+import { SharedVault } from '../../SharedVault/SharedVault'
+import { SharedVaultUser } from '../../SharedVault/User/SharedVaultUser'
+
+describe('AddUserToSharedVault', () => {
+  let removeUserEvents: RemoveUserEvents
+  let sharedVaultRepository: SharedVaultRepositoryInterface
+  let sharedVaultUserRepository: SharedVaultUserRepositoryInterface
+  let timer: TimerInterface
+  let sharedVault: SharedVault
+
+  const validUuid = '00000000-0000-0000-0000-000000000000'
+
+  const createUseCase = () =>
+    new AddUserToSharedVault(removeUserEvents, sharedVaultRepository, sharedVaultUserRepository, timer)
+
+  beforeEach(() => {
+    removeUserEvents = {} as jest.Mocked<RemoveUserEvents>
+    removeUserEvents.execute = jest.fn().mockResolvedValue(Result.ok())
+
+    sharedVault = {} as jest.Mocked<SharedVault>
+
+    sharedVaultRepository = {} as jest.Mocked<SharedVaultRepositoryInterface>
+    sharedVaultRepository.findByUuid = jest.fn().mockResolvedValue(sharedVault)
+
+    sharedVaultUserRepository = {} as jest.Mocked<SharedVaultUserRepositoryInterface>
+    sharedVaultUserRepository.save = jest.fn()
+
+    timer = {} as jest.Mocked<TimerInterface>
+    timer.getTimestampInMicroseconds = jest.fn().mockReturnValue(123456789)
+  })
+
+  it('should return a failure result if the shared vault uuid is invalid', async () => {
+    const useCase = createUseCase()
+
+    const result = await useCase.execute({
+      sharedVaultUuid: 'invalid-uuid',
+      userUuid: validUuid,
+      permission: 'read',
+    })
+
+    expect(result.isFailed()).toBe(true)
+    expect(result.getError()).toBe('Given value is not a valid uuid: invalid-uuid')
+  })
+
+  it('should return a failure result if the user uuid is invalid', async () => {
+    const useCase = createUseCase()
+
+    const result = await useCase.execute({
+      sharedVaultUuid: validUuid,
+      userUuid: 'invalid-uuid',
+      permission: 'read',
+    })
+
+    expect(result.isFailed()).toBe(true)
+    expect(result.getError()).toBe('Given value is not a valid uuid: invalid-uuid')
+  })
+
+  it('should return a failure result if the permission is invalid', async () => {
+    const useCase = createUseCase()
+
+    const result = await useCase.execute({
+      sharedVaultUuid: validUuid,
+      userUuid: validUuid,
+      permission: 'test',
+    })
+
+    expect(result.isFailed()).toBe(true)
+    expect(result.getError()).toBe('Invalid shared vault user permission test')
+  })
+
+  it('should return a failure result if the shared vault does not exist', async () => {
+    const useCase = createUseCase()
+
+    sharedVaultRepository.findByUuid = jest.fn().mockResolvedValueOnce(null)
+
+    const result = await useCase.execute({
+      sharedVaultUuid: validUuid,
+      userUuid: validUuid,
+      permission: 'read',
+    })
+
+    expect(result.isFailed()).toBe(true)
+    expect(result.getError()).toBe('Attempting to add a shared vault user to a non-existent shared vault')
+  })
+
+  it('should return a failure result if removing user events fails', async () => {
+    const useCase = createUseCase()
+
+    removeUserEvents.execute = jest.fn().mockResolvedValueOnce(Result.fail('test'))
+
+    const result = await useCase.execute({
+      sharedVaultUuid: validUuid,
+      userUuid: validUuid,
+      permission: 'read',
+    })
+
+    expect(result.isFailed()).toBe(true)
+    expect(result.getError()).toBe('test')
+  })
+
+  it('should return a failure result if creating the shared vault user fails', async () => {
+    const useCase = createUseCase()
+
+    const mockSharedVaultUser = jest.spyOn(SharedVaultUser, 'create')
+    mockSharedVaultUser.mockImplementation(() => {
+      return Result.fail('Oops')
+    })
+
+    const result = await useCase.execute({
+      sharedVaultUuid: validUuid,
+      userUuid: validUuid,
+      permission: 'read',
+    })
+
+    expect(result.isFailed()).toBe(true)
+    expect(result.getError()).toBe('Oops')
+
+    mockSharedVaultUser.mockRestore()
+  })
+
+  it('should add a user to a shared vault', async () => {
+    const useCase = createUseCase()
+
+    const result = await useCase.execute({
+      sharedVaultUuid: validUuid,
+      userUuid: validUuid,
+      permission: 'read',
+    })
+
+    expect(result.isFailed()).toBe(false)
+    expect(sharedVaultUserRepository.save).toHaveBeenCalled()
+  })
+})

+ 71 - 0
packages/syncing-server/src/Domain/UseCase/AddUserToSharedVault/AddUserToSharedVault.ts

@@ -0,0 +1,71 @@
+import { Result, Timestamps, UseCaseInterface, Uuid } from '@standardnotes/domain-core'
+import { TimerInterface } from '@standardnotes/time'
+
+import { AddUserToSharedVaultDTO } from './AddUserToSharedVaultDTO'
+import { SharedVaultRepositoryInterface } from '../../SharedVault/SharedVaultRepositoryInterface'
+import { SharedVaultUser } from '../../SharedVault/User/SharedVaultUser'
+import { SharedVaultUserRepositoryInterface } from '../../SharedVault/User/SharedVaultUserRepositoryInterface'
+import { SharedVaultUserPermission } from '../../SharedVault/User/SharedVaultUserPermission'
+import { RemoveUserEvents } from '../RemoveUserEvents/RemoveUserEvents'
+
+export class AddUserToSharedVault implements UseCaseInterface<SharedVaultUser> {
+  constructor(
+    private removeUserEvents: RemoveUserEvents,
+    private sharedVaultRepository: SharedVaultRepositoryInterface,
+    private sharedVaultUserRepository: SharedVaultUserRepositoryInterface,
+    private timer: TimerInterface,
+  ) {}
+
+  async execute(dto: AddUserToSharedVaultDTO): Promise<Result<SharedVaultUser>> {
+    const sharedVaultUuidOrError = Uuid.create(dto.sharedVaultUuid)
+    if (sharedVaultUuidOrError.isFailed()) {
+      return Result.fail(sharedVaultUuidOrError.getError())
+    }
+    const sharedVaultUuid = sharedVaultUuidOrError.getValue()
+
+    const sharedVault = await this.sharedVaultRepository.findByUuid(sharedVaultUuid)
+    if (!sharedVault) {
+      return Result.fail('Attempting to add a shared vault user to a non-existent shared vault')
+    }
+
+    const userUuidOrError = Uuid.create(dto.userUuid)
+    if (userUuidOrError.isFailed()) {
+      return Result.fail(userUuidOrError.getError())
+    }
+    const userUuid = userUuidOrError.getValue()
+
+    const permissionOrError = SharedVaultUserPermission.create(dto.permission)
+    if (permissionOrError.isFailed()) {
+      return Result.fail(permissionOrError.getError())
+    }
+    const permission = permissionOrError.getValue()
+
+    const removingEventsResult = await this.removeUserEvents.execute({
+      sharedVaultUuid: sharedVaultUuid.value,
+      userUuid: userUuid.value,
+    })
+    if (removingEventsResult.isFailed()) {
+      return Result.fail(removingEventsResult.getError())
+    }
+
+    const timestamps = Timestamps.create(
+      this.timer.getTimestampInMicroseconds(),
+      this.timer.getTimestampInMicroseconds(),
+    ).getValue()
+
+    const sharedVaultUserOrError = SharedVaultUser.create({
+      userUuid,
+      sharedVaultUuid,
+      permission,
+      timestamps,
+    })
+    if (sharedVaultUserOrError.isFailed()) {
+      return Result.fail(sharedVaultUserOrError.getError())
+    }
+    const sharedVaultUser = sharedVaultUserOrError.getValue()
+
+    await this.sharedVaultUserRepository.save(sharedVaultUser)
+
+    return Result.ok(sharedVaultUser)
+  }
+}

+ 5 - 0
packages/syncing-server/src/Domain/UseCase/AddUserToSharedVault/AddUserToSharedVaultDTO.ts

@@ -0,0 +1,5 @@
+export interface AddUserToSharedVaultDTO {
+  sharedVaultUuid: string
+  userUuid: string
+  permission: string
+}

+ 83 - 0
packages/syncing-server/src/Domain/UseCase/CreateSharedVault/CreateSharedVault.spec.ts

@@ -0,0 +1,83 @@
+import { TimerInterface } from '@standardnotes/time'
+import { Result } from '@standardnotes/domain-core'
+
+import { SharedVaultRepositoryInterface } from '../../SharedVault/SharedVaultRepositoryInterface'
+import { AddUserToSharedVault } from '../AddUserToSharedVault/AddUserToSharedVault'
+import { CreateSharedVault } from './CreateSharedVault'
+import { SharedVault } from '../../SharedVault/SharedVault'
+
+describe('CreateSharedVault', () => {
+  let addUserToSharedVault: AddUserToSharedVault
+  let sharedVaultRepository: SharedVaultRepositoryInterface
+  let timer: TimerInterface
+
+  const createUseCase = () => new CreateSharedVault(addUserToSharedVault, sharedVaultRepository, timer)
+
+  beforeEach(() => {
+    addUserToSharedVault = {} as jest.Mocked<AddUserToSharedVault>
+    addUserToSharedVault.execute = jest.fn().mockResolvedValue(Result.ok())
+
+    sharedVaultRepository = {} as jest.Mocked<SharedVaultRepositoryInterface>
+    sharedVaultRepository.save = jest.fn()
+
+    timer = {} as jest.Mocked<TimerInterface>
+    timer.getTimestampInMicroseconds = jest.fn().mockReturnValue(123456789)
+  })
+
+  it('should return a failure result if the user uuid is invalid', async () => {
+    const useCase = createUseCase()
+
+    const result = await useCase.execute({
+      userUuid: 'invalid-uuid',
+    })
+
+    expect(result.isFailed()).toBe(true)
+    expect(result.getError()).toBe('Given value is not a valid uuid: invalid-uuid')
+  })
+
+  it('should return a failure result if the shared vault could not be created', async () => {
+    const useCase = createUseCase()
+
+    const mockSharedVault = jest.spyOn(SharedVault, 'create')
+    mockSharedVault.mockImplementation(() => {
+      return Result.fail('Oops')
+    })
+
+    const result = await useCase.execute({
+      userUuid: '00000000-0000-0000-0000-000000000000',
+    })
+
+    expect(result.isFailed()).toBe(true)
+    expect(result.getError()).toBe('Oops')
+
+    mockSharedVault.mockRestore()
+  })
+
+  it('should return a failure result if the user could not be added to the shared vault', async () => {
+    const useCase = createUseCase()
+
+    addUserToSharedVault.execute = jest.fn().mockResolvedValue(Result.fail('Oops'))
+
+    const result = await useCase.execute({
+      userUuid: '00000000-0000-0000-0000-000000000000',
+    })
+
+    expect(result.isFailed()).toBe(true)
+    expect(result.getError()).toBe('Oops')
+  })
+
+  it('should create a shared vault', async () => {
+    const useCase = createUseCase()
+
+    await useCase.execute({
+      userUuid: '00000000-0000-0000-0000-000000000000',
+    })
+
+    expect(addUserToSharedVault.execute).toHaveBeenCalledWith({
+      sharedVaultUuid: expect.any(String),
+      userUuid: '00000000-0000-0000-0000-000000000000',
+      permission: 'admin',
+    })
+    expect(sharedVaultRepository.save).toHaveBeenCalled()
+  })
+})

+ 55 - 0
packages/syncing-server/src/Domain/UseCase/CreateSharedVault/CreateSharedVault.ts

@@ -0,0 +1,55 @@
+import { Result, Timestamps, UseCaseInterface, Uuid } from '@standardnotes/domain-core'
+import { CreateSharedVaultResult } from './CreateSharedVaultResult'
+import { CreateSharedVaultDTO } from './CreateSharedVaultDTO'
+import { TimerInterface } from '@standardnotes/time'
+import { SharedVaultRepositoryInterface } from '../../SharedVault/SharedVaultRepositoryInterface'
+import { AddUserToSharedVault } from '../AddUserToSharedVault/AddUserToSharedVault'
+import { SharedVault } from '../../SharedVault/SharedVault'
+
+export class CreateSharedVault implements UseCaseInterface<CreateSharedVaultResult> {
+  private readonly FILE_UPLOAD_BYTES_LIMIT = 1_000_000
+
+  constructor(
+    private addUserToSharedVault: AddUserToSharedVault,
+    private sharedVaultRepository: SharedVaultRepositoryInterface,
+    private timer: TimerInterface,
+  ) {}
+
+  async execute(dto: CreateSharedVaultDTO): Promise<Result<CreateSharedVaultResult>> {
+    const userUuidOrError = Uuid.create(dto.userUuid)
+    if (userUuidOrError.isFailed()) {
+      return Result.fail(userUuidOrError.getError())
+    }
+    const userUuid = userUuidOrError.getValue()
+
+    const timestamps = Timestamps.create(
+      this.timer.getTimestampInMicroseconds(),
+      this.timer.getTimestampInMicroseconds(),
+    ).getValue()
+
+    const sharedVaultOrError = SharedVault.create({
+      fileUploadBytesLimit: this.FILE_UPLOAD_BYTES_LIMIT,
+      fileUploadBytesUsed: 0,
+      userUuid,
+      timestamps,
+    })
+    if (sharedVaultOrError.isFailed()) {
+      return Result.fail(sharedVaultOrError.getError())
+    }
+    const sharedVault = sharedVaultOrError.getValue()
+
+    await this.sharedVaultRepository.save(sharedVault)
+
+    const sharedVaultUserOrError = await this.addUserToSharedVault.execute({
+      sharedVaultUuid: sharedVault.id.toString(),
+      userUuid: dto.userUuid,
+      permission: 'admin',
+    })
+    if (sharedVaultUserOrError.isFailed()) {
+      return Result.fail(sharedVaultUserOrError.getError())
+    }
+    const sharedVaultUser = sharedVaultUserOrError.getValue()
+
+    return Result.ok({ sharedVault, sharedVaultUser })
+  }
+}

+ 3 - 0
packages/syncing-server/src/Domain/UseCase/CreateSharedVault/CreateSharedVaultDTO.ts

@@ -0,0 +1,3 @@
+export interface CreateSharedVaultDTO {
+  userUuid: string
+}

+ 7 - 0
packages/syncing-server/src/Domain/UseCase/CreateSharedVault/CreateSharedVaultResult.ts

@@ -0,0 +1,7 @@
+import { SharedVault } from '../../SharedVault/SharedVault'
+import { SharedVaultUser } from '../../SharedVault/User/SharedVaultUser'
+
+export interface CreateSharedVaultResult {
+  sharedVaultUser: SharedVaultUser
+  sharedVault: SharedVault
+}

+ 9 - 0
packages/syncing-server/src/Domain/UseCase/RemoveUserEvents/RemoveUserEvents.ts

@@ -0,0 +1,9 @@
+import { Result, UseCaseInterface } from '@standardnotes/domain-core'
+
+import { RemoveUserEventsDTO } from './RemoveUserEventsDTO'
+
+export class RemoveUserEvents implements UseCaseInterface<void> {
+  async execute(_dto: RemoveUserEventsDTO): Promise<Result<void>> {
+    throw new Error('Method not implemented.')
+  }
+}

+ 4 - 0
packages/syncing-server/src/Domain/UseCase/RemoveUserEvents/RemoveUserEventsDTO.ts

@@ -0,0 +1,4 @@
+export interface RemoveUserEventsDTO {
+  sharedVaultUuid: string
+  userUuid: string
+}