feat(syncing-server): refactor syncing to decouple getting and saving items (#659)

* feat(syncing-server): refactor syncing to decouple getting and saving items

* fix(syncing-server): item hash http representation mapping

* fix(syncing-server): remove redundant specs for inversify express controller
This commit is contained in:
Karol Sójko
2023-07-17 13:28:50 +02:00
committed by GitHub
parent af8f12c33a
commit cb74b23e45
71 changed files with 1366 additions and 1616 deletions
@@ -10,6 +10,7 @@ import { ApiVersion } from '../../../Domain/Api/ApiVersion'
import { SyncItems } from '../../../Domain/UseCase/Syncing/SyncItems/SyncItems'
import { HttpStatusCode } from '@standardnotes/responses'
import { ItemHttpRepresentation } from '../../../Mapping/Http/ItemHttpRepresentation'
import { ItemHash } from '../../../Domain/Item/ItemHash'
export class HomeServerItemsController extends BaseHttpController {
constructor(
@@ -30,9 +31,22 @@ export class HomeServerItemsController extends BaseHttpController {
}
async sync(request: Request, response: Response): Promise<results.JsonResult> {
let itemHashes = []
const itemHashes: ItemHash[] = []
if ('items' in request.body) {
itemHashes = request.body.items
for (const itemHashInput of request.body.items) {
const itemHashOrError = ItemHash.create({
...itemHashInput,
user_uuid: response.locals.user.uuid,
key_system_identifier: itemHashInput.key_system_identifier ?? null,
shared_vault_uuid: itemHashInput.shared_vault_uuid ?? null,
})
if (itemHashOrError.isFailed()) {
return this.json({ error: { message: itemHashOrError.getError() } }, HttpStatusCode.BadRequest)
}
itemHashes.push(itemHashOrError.getValue())
}
}
const syncResult = await this.syncItems.execute({
@@ -1,238 +0,0 @@
import 'reflect-metadata'
import * as express from 'express'
import { ContentType, MapperInterface, Result } from '@standardnotes/domain-core'
import { results } from 'inversify-express-utils'
import { InversifyExpressItemsController } from './InversifyExpressItemsController'
import { Item } from '../../Domain/Item/Item'
import { ApiVersion } from '../../Domain/Api/ApiVersion'
import { SyncResponse20200115 } from '../../Domain/Item/SyncResponse/SyncResponse20200115'
import { SyncResponseFactoryInterface } from '../../Domain/Item/SyncResponse/SyncResponseFactoryInterface'
import { SyncResponseFactoryResolverInterface } from '../../Domain/Item/SyncResponse/SyncResponseFactoryResolverInterface'
import { CheckIntegrity } from '../../Domain/UseCase/Syncing/CheckIntegrity/CheckIntegrity'
import { GetItem } from '../../Domain/UseCase/Syncing/GetItem/GetItem'
import { SyncItems } from '../../Domain/UseCase/Syncing/SyncItems/SyncItems'
import { ItemHttpRepresentation } from '../../Mapping/Http/ItemHttpRepresentation'
describe('InversifyExpressItemsController', () => {
let syncItems: SyncItems
let checkIntegrity: CheckIntegrity
let getItem: GetItem
let mapper: MapperInterface<Item, ItemHttpRepresentation>
let request: express.Request
let response: express.Response
let syncResponceFactoryResolver: SyncResponseFactoryResolverInterface
let syncResponseFactory: SyncResponseFactoryInterface
let syncResponse: SyncResponse20200115
const createController = () =>
new InversifyExpressItemsController(syncItems, checkIntegrity, getItem, mapper, syncResponceFactoryResolver)
beforeEach(() => {
mapper = {} as jest.Mocked<MapperInterface<Item, ItemHttpRepresentation>>
mapper.toProjection = jest.fn().mockReturnValue({ foo: 'bar' })
syncItems = {} as jest.Mocked<SyncItems>
syncItems.execute = jest.fn().mockReturnValue(Result.ok({ foo: 'bar' }))
checkIntegrity = {} as jest.Mocked<CheckIntegrity>
checkIntegrity.execute = jest.fn().mockReturnValue(Result.ok([{ uuid: '1-2-3', updated_at_timestamp: 2 }]))
getItem = {} as jest.Mocked<GetItem>
getItem.execute = jest.fn().mockReturnValue(Result.ok({} as jest.Mocked<Item>))
request = {
headers: {},
body: {},
params: {},
} as jest.Mocked<express.Request>
request.body.api = ApiVersion.v20200115
request.body.sync_token = 'MjoxNjE3MTk1MzQyLjc1ODEyMTc='
request.body.limit = 150
request.body.compute_integrity = false
request.headers['user-agent'] = 'Google Chrome'
request.body.items = [
{
content: 'test',
content_type: ContentType.TYPES.Note,
created_at: '2021-02-19T11:35:45.655Z',
deleted: false,
duplicate_of: null,
enc_item_key: 'test',
items_key_id: 'test',
updated_at: '2021-02-19T11:35:45.655Z',
uuid: '1-2-3',
},
]
response = {
locals: {},
} as jest.Mocked<express.Response>
response.locals.user = {
uuid: '123',
}
response.locals.freeUser = false
syncResponse = {} as jest.Mocked<SyncResponse20200115>
syncResponseFactory = {} as jest.Mocked<SyncResponseFactoryInterface>
syncResponseFactory.createResponse = jest.fn().mockReturnValue(syncResponse)
syncResponceFactoryResolver = {} as jest.Mocked<SyncResponseFactoryResolverInterface>
syncResponceFactoryResolver.resolveSyncResponseFactoryVersion = jest.fn().mockReturnValue(syncResponseFactory)
})
it('should get a single item', async () => {
request.params.uuid = '1-2-3'
const httpResponse = <results.JsonResult>await createController().getSingleItem(request, response)
const result = await httpResponse.executeAsync()
expect(getItem.execute).toHaveBeenCalledWith({
itemUuid: '1-2-3',
userUuid: '123',
})
expect(result.statusCode).toEqual(200)
})
it('should return 404 on a missing single item', async () => {
request.params.uuid = '1-2-3'
getItem.execute = jest.fn().mockReturnValue(Result.fail('Oops'))
const httpResponse = <results.NotFoundResult>await createController().getSingleItem(request, response)
const result = await httpResponse.executeAsync()
expect(getItem.execute).toHaveBeenCalledWith({
itemUuid: '1-2-3',
userUuid: '123',
})
expect(result.statusCode).toEqual(404)
})
it('should check items integrity', async () => {
request.body.integrityPayloads = [
{
uuid: '1-2-3',
updated_at_timestamp: 1,
},
]
const httpResponse = <results.JsonResult>await createController().checkItemsIntegrity(request, response)
const result = await httpResponse.executeAsync()
expect(checkIntegrity.execute).toHaveBeenCalledWith({
integrityPayloads: [
{
updated_at_timestamp: 1,
uuid: '1-2-3',
},
],
userUuid: '123',
freeUser: false,
})
expect(result.statusCode).toEqual(200)
expect(await result.content.readAsStringAsync()).toEqual(
'{"mismatches":[{"uuid":"1-2-3","updated_at_timestamp":2}]}',
)
})
it('should check items integrity with missing request parameter', async () => {
const httpResponse = <results.JsonResult>await createController().checkItemsIntegrity(request, response)
const result = await httpResponse.executeAsync()
expect(checkIntegrity.execute).toHaveBeenCalledWith({
integrityPayloads: [],
userUuid: '123',
freeUser: false,
})
expect(result.statusCode).toEqual(200)
expect(await result.content.readAsStringAsync()).toEqual(
'{"mismatches":[{"uuid":"1-2-3","updated_at_timestamp":2}]}',
)
})
it('should sync items', async () => {
const httpResponse = <results.JsonResult>await createController().sync(request, response)
const result = await httpResponse.executeAsync()
expect(syncItems.execute).toHaveBeenCalledWith({
apiVersion: '20200115',
computeIntegrityHash: false,
itemHashes: [
{
content: 'test',
content_type: 'Note',
created_at: '2021-02-19T11:35:45.655Z',
deleted: false,
duplicate_of: null,
enc_item_key: 'test',
items_key_id: 'test',
updated_at: '2021-02-19T11:35:45.655Z',
uuid: '1-2-3',
},
],
limit: 150,
syncToken: 'MjoxNjE3MTk1MzQyLjc1ODEyMTc=',
userUuid: '123',
sessionUuid: null,
})
expect(result.statusCode).toEqual(200)
})
it('should sync items with defaulting API version if none specified', async () => {
delete request.body.api
const httpResponse = <results.JsonResult>await createController().sync(request, response)
const result = await httpResponse.executeAsync()
expect(syncItems.execute).toHaveBeenCalledWith({
apiVersion: '20161215',
computeIntegrityHash: false,
itemHashes: [
{
content: 'test',
content_type: 'Note',
created_at: '2021-02-19T11:35:45.655Z',
deleted: false,
duplicate_of: null,
enc_item_key: 'test',
items_key_id: 'test',
updated_at: '2021-02-19T11:35:45.655Z',
uuid: '1-2-3',
},
],
limit: 150,
syncToken: 'MjoxNjE3MTk1MzQyLjc1ODEyMTc=',
userUuid: '123',
sessionUuid: null,
})
expect(result.statusCode).toEqual(200)
})
it('should sync items with no incoming items in request', async () => {
response.locals.session = { uuid: '2-3-4' }
delete request.body.items
const httpResponse = <results.JsonResult>await createController().sync(request, response)
const result = await httpResponse.executeAsync()
expect(syncItems.execute).toHaveBeenCalledWith({
apiVersion: '20200115',
computeIntegrityHash: false,
itemHashes: [],
limit: 150,
syncToken: 'MjoxNjE3MTk1MzQyLjc1ODEyMTc=',
userUuid: '123',
sessionUuid: '2-3-4',
})
expect(result.statusCode).toEqual(200)
})
})