feat(syncing-server): add designated survivors in fetching shared vaults response (#844)

This commit is contained in:
Karol Sójko
2023-09-22 13:15:11 +02:00
committed by GitHub
parent d50c4440c2
commit bcd95cdbe9
11 changed files with 134 additions and 15 deletions
@@ -871,6 +871,7 @@ export class ContainerConfigLoader {
.bind<DesignateSurvivor>(TYPES.Sync_DesignateSurvivor)
.toConstantValue(
new DesignateSurvivor(
container.get<SharedVaultRepositoryInterface>(TYPES.Sync_SharedVaultRepository),
container.get<SharedVaultUserRepositoryInterface>(TYPES.Sync_SharedVaultUserRepository),
container.get<TimerInterface>(TYPES.Sync_Timer),
container.get<DomainEventFactoryInterface>(TYPES.Sync_DomainEventFactory),
@@ -8,4 +8,5 @@ export interface SharedVaultUserRepositoryInterface {
remove(sharedVault: SharedVaultUser): Promise<void>
removeBySharedVaultUuid(sharedVaultUuid: Uuid): Promise<void>
findByUserUuidAndSharedVaultUuid(dto: { userUuid: Uuid; sharedVaultUuid: Uuid }): Promise<SharedVaultUser | null>
findDesignatedSurvivorBySharedVaultUuid(sharedVaultUuid: Uuid): Promise<SharedVaultUser | null>
}
@@ -5,8 +5,12 @@ import { DesignateSurvivor } from './DesignateSurvivor'
import { TimerInterface } from '@standardnotes/time'
import { DomainEventInterface, DomainEventPublisherInterface } from '@standardnotes/domain-events'
import { DomainEventFactoryInterface } from '../../../Event/DomainEventFactoryInterface'
import { SharedVault } from '../../../SharedVault/SharedVault'
import { SharedVaultRepositoryInterface } from '../../../SharedVault/SharedVaultRepositoryInterface'
describe('DesignateSurvivor', () => {
let sharedVault: SharedVault
let sharedVaultRepository: SharedVaultRepositoryInterface
let sharedVaultUserRepository: SharedVaultUserRepositoryInterface
let sharedVaultUser: SharedVaultUser
let sharedVaultOwner: SharedVaultUser
@@ -15,9 +19,25 @@ describe('DesignateSurvivor', () => {
let domainEventPublisher: DomainEventPublisherInterface
const createUseCase = () =>
new DesignateSurvivor(sharedVaultUserRepository, timer, domainEventFactory, domainEventPublisher)
new DesignateSurvivor(
sharedVaultRepository,
sharedVaultUserRepository,
timer,
domainEventFactory,
domainEventPublisher,
)
beforeEach(() => {
sharedVault = SharedVault.create({
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
timestamps: Timestamps.create(123, 123).getValue(),
fileUploadBytesUsed: 123,
}).getValue()
sharedVaultRepository = {} as jest.Mocked<SharedVaultRepositoryInterface>
sharedVaultRepository.findByUuid = jest.fn().mockReturnValue(sharedVault)
sharedVaultRepository.save = jest.fn()
timer = {} as jest.Mocked<TimerInterface>
timer.getTimestampInMicroseconds = jest.fn().mockReturnValue(123)
@@ -86,6 +106,20 @@ describe('DesignateSurvivor', () => {
expect(result.isFailed()).toBe(true)
})
it('should fail if shared vault is not found', async () => {
sharedVaultRepository.findByUuid = jest.fn().mockReturnValue(null)
const useCase = createUseCase()
const result = await useCase.execute({
sharedVaultUuid: '00000000-0000-0000-0000-000000000000',
userUuid: '00000000-0000-0000-0000-000000000000',
originatorUuid: '00000000-0000-0000-0000-000000000002',
})
expect(result.isFailed()).toBe(true)
})
it('should fail if shared vault user is not found', async () => {
sharedVaultUserRepository.findBySharedVaultUuid = jest.fn().mockReturnValue([sharedVaultOwner])
@@ -12,9 +12,11 @@ import {
import { SharedVaultUserRepositoryInterface } from '../../../SharedVault/User/SharedVaultUserRepositoryInterface'
import { DesignateSurvivorDTO } from './DesignateSurvivorDTO'
import { DomainEventFactoryInterface } from '../../../Event/DomainEventFactoryInterface'
import { SharedVaultRepositoryInterface } from '../../../SharedVault/SharedVaultRepositoryInterface'
export class DesignateSurvivor implements UseCaseInterface<void> {
constructor(
private sharedVaultRepository: SharedVaultRepositoryInterface,
private sharedVaultUserRepository: SharedVaultUserRepositoryInterface,
private timer: TimerInterface,
private domainEventFactory: DomainEventFactoryInterface,
@@ -40,6 +42,11 @@ export class DesignateSurvivor implements UseCaseInterface<void> {
}
const originatorUuid = originatorUuidOrError.getValue()
const sharedVault = await this.sharedVaultRepository.findByUuid(sharedVaultUuid)
if (!sharedVault) {
return Result.fail('Shared vault not found')
}
const sharedVaultUsers = await this.sharedVaultUserRepository.findBySharedVaultUuid(sharedVaultUuid)
let sharedVaultExistingSurvivor: SharedVaultUser | undefined
let toBeDesignatedAsASurvivor: SharedVaultUser | undefined
@@ -92,6 +99,13 @@ export class DesignateSurvivor implements UseCaseInterface<void> {
}),
)
sharedVault.props.timestamps = Timestamps.create(
sharedVault.props.timestamps.createdAt,
this.timer.getTimestampInMicroseconds(),
).getValue()
await this.sharedVaultRepository.save(sharedVault)
return Result.ok()
}
}
@@ -40,7 +40,7 @@ describe('GetSharedVaults', () => {
userUuid: '00000000-0000-0000-0000-000000000000',
})
expect(result.getValue()).toEqual([sharedVault])
expect(result.getValue().sharedVaults).toEqual([sharedVault])
})
it('returns empty array if no shared vaults found', async () => {
@@ -52,7 +52,7 @@ describe('GetSharedVaults', () => {
userUuid: '00000000-0000-0000-0000-000000000000',
})
expect(result.getValue()).toEqual([])
expect(result.getValue().sharedVaults).toEqual([])
})
it('returns error if user uuid is invalid', async () => {
@@ -64,4 +64,17 @@ describe('GetSharedVaults', () => {
expect(result.isFailed()).toBeTruthy()
})
it('should fetch designated survivors if includeDesignatedSurvivors is true', async () => {
sharedVaultUserRepository.findDesignatedSurvivorBySharedVaultUuid = jest.fn().mockResolvedValue(sharedVaultUser)
const useCase = createUseCase()
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
includeDesignatedSurvivors: true,
})
expect(result.getValue().designatedSurvivors).toEqual([sharedVaultUser])
})
})
@@ -1,17 +1,28 @@
import { Result, UseCaseInterface, Uuid } from '@standardnotes/domain-core'
import { Result, SharedVaultUser, UseCaseInterface, Uuid } from '@standardnotes/domain-core'
import { SharedVault } from '../../../SharedVault/SharedVault'
import { GetSharedVaultsDTO } from './GetSharedVaultsDTO'
import { SharedVaultUserRepositoryInterface } from '../../../SharedVault/User/SharedVaultUserRepositoryInterface'
import { SharedVaultRepositoryInterface } from '../../../SharedVault/SharedVaultRepositoryInterface'
export class GetSharedVaults implements UseCaseInterface<SharedVault[]> {
export class GetSharedVaults
implements
UseCaseInterface<{
sharedVaults: SharedVault[]
designatedSurvivors: SharedVaultUser[]
}>
{
constructor(
private sharedVaultUserRepository: SharedVaultUserRepositoryInterface,
private sharedVaultRepository: SharedVaultRepositoryInterface,
) {}
async execute(dto: GetSharedVaultsDTO): Promise<Result<SharedVault[]>> {
async execute(dto: GetSharedVaultsDTO): Promise<
Result<{
sharedVaults: SharedVault[]
designatedSurvivors: SharedVaultUser[]
}>
> {
const userUuidOrError = Uuid.create(dto.userUuid)
if (userUuidOrError.isFailed()) {
return Result.fail(userUuidOrError.getError())
@@ -25,11 +36,29 @@ export class GetSharedVaults implements UseCaseInterface<SharedVault[]> {
)
if (sharedVaultUuids.length === 0) {
return Result.ok([])
return Result.ok({
sharedVaults: [],
designatedSurvivors: [],
})
}
const sharedVaults = await this.sharedVaultRepository.findByUuids(sharedVaultUuids, dto.lastSyncTime)
return Result.ok(sharedVaults)
const designatedSurvivors = []
if (dto.includeDesignatedSurvivors) {
for (const sharedVault of sharedVaults) {
const designatedSurvivor = await this.sharedVaultUserRepository.findDesignatedSurvivorBySharedVaultUuid(
sharedVault.uuid,
)
if (designatedSurvivor) {
designatedSurvivors.push(designatedSurvivor)
}
}
}
return Result.ok({
sharedVaults,
designatedSurvivors,
})
}
}
@@ -1,4 +1,5 @@
export interface GetSharedVaultsDTO {
userUuid: string
includeDesignatedSurvivors?: boolean
lastSyncTime?: number
}
@@ -136,7 +136,7 @@ describe('SyncItems', () => {
itemRepositoryResolver.resolve = jest.fn().mockReturnValue(itemRepository)
getSharedVaultsUseCase = {} as jest.Mocked<GetSharedVaults>
getSharedVaultsUseCase.execute = jest.fn().mockReturnValue(Result.ok([]))
getSharedVaultsUseCase.execute = jest.fn().mockReturnValue(Result.ok({ sharedVaults: [], designatedSurivors: [] }))
getSharedVaultInvitesSentToUserUseCase = {} as jest.Mocked<GetSharedVaultInvitesSentToUser>
getSharedVaultInvitesSentToUserUseCase.execute = jest.fn().mockReturnValue(Result.ok([]))
@@ -73,12 +73,13 @@ export class SyncItems implements UseCaseInterface<SyncItemsResponse> {
const sharedVaultsOrError = await this.getSharedVaultsUseCase.execute({
userUuid: dto.userUuid,
includeDesignatedSurvivors: false,
lastSyncTime: getItemsResult.lastSyncTime ?? undefined,
})
if (sharedVaultsOrError.isFailed()) {
return Result.fail(sharedVaultsOrError.getError())
}
const sharedVaults = sharedVaultsOrError.getValue()
const sharedVaultsResult = sharedVaultsOrError.getValue()
const sharedVaultInvitesOrError = await this.getSharedVaultInvitesSentToUserUseCase.execute({
userUuid: dto.userUuid,
@@ -114,7 +115,7 @@ export class SyncItems implements UseCaseInterface<SyncItemsResponse> {
conflicts: saveItemsResult.conflicts,
cursorToken: getItemsResult.cursorToken,
sharedVaultInvites,
sharedVaults,
sharedVaults: sharedVaultsResult.sharedVaults,
messages,
notifications,
}
@@ -36,23 +36,30 @@ export class BaseSharedVaultsController extends BaseHttpController {
}
async getSharedVaults(_request: Request, response: Response): Promise<results.JsonResult> {
const result = await this.getSharedVaultsUseCase.execute({
const resultOrError = await this.getSharedVaultsUseCase.execute({
userUuid: response.locals.user.uuid,
includeDesignatedSurvivors: true,
})
if (result.isFailed()) {
if (resultOrError.isFailed()) {
return this.json(
{
error: {
message: result.getError(),
message: resultOrError.getError(),
},
},
HttpStatusCode.BadRequest,
)
}
const result = resultOrError.getValue()
return this.json({
sharedVaults: result.getValue().map((sharedVault) => this.sharedVaultHttpMapper.toProjection(sharedVault)),
sharedVaults: result.sharedVaults.map((sharedVault) => this.sharedVaultHttpMapper.toProjection(sharedVault)),
designatedSurvivors: result.designatedSurvivors.map((designatedSurvivor) => ({
sharedVaultUuid: designatedSurvivor.props.sharedVaultUuid.value,
userUuid: designatedSurvivor.props.userUuid.value,
})),
})
}
@@ -10,6 +10,24 @@ export class TypeORMSharedVaultUserRepository implements SharedVaultUserReposito
private mapper: MapperInterface<SharedVaultUser, TypeORMSharedVaultUser>,
) {}
async findDesignatedSurvivorBySharedVaultUuid(sharedVaultUuid: Uuid): Promise<SharedVaultUser | null> {
const persistence = await this.ormRepository
.createQueryBuilder('shared_vault_user')
.where('shared_vault_user.shared_vault_uuid = :sharedVaultUuid', {
sharedVaultUuid: sharedVaultUuid.value,
})
.andWhere('shared_vault_user.is_designated_survivor = :isDesignatedSurvivor', {
isDesignatedSurvivor: true,
})
.getOne()
if (persistence === null) {
return null
}
return this.mapper.toDomain(persistence)
}
async removeBySharedVaultUuid(sharedVaultUuid: Uuid): Promise<void> {
await this.ormRepository
.createQueryBuilder('shared_vault_user')