feat: add services package

This commit is contained in:
Karol Sójko
2022-07-05 20:51:42 +02:00
parent b614c71e79
commit fbfed0a05c
85 changed files with 2418 additions and 28 deletions

View File

@@ -0,0 +1,5 @@
import { CheckIntegrityResponse, IntegrityPayload } from '@standardnotes/responses'
export interface IntegrityApiInterface {
checkIntegrity(integrityPayloads: IntegrityPayload[]): Promise<CheckIntegrityResponse>
}

View File

@@ -0,0 +1,4 @@
/* istanbul ignore file */
export enum IntegrityEvent {
IntegrityCheckCompleted = 'IntegrityCheckCompleted',
}

View File

@@ -0,0 +1,7 @@
import { ServerItemResponse } from '@standardnotes/responses'
import { SyncSource } from '../Sync/SyncSource'
export type IntegrityEventPayload = {
rawPayloads: ServerItemResponse[]
source: SyncSource
}

View File

@@ -0,0 +1,160 @@
import { TransferPayload } from '@standardnotes/models'
import { SyncEvent } from '../Event/SyncEvent'
import { InternalEventBusInterface } from '../Internal/InternalEventBusInterface'
import { ItemsServerInterface } from '../Item/ItemsServerInterface'
import { SyncSource } from '../Sync/SyncSource'
import { IntegrityApiInterface } from './IntegrityApiInterface'
import { IntegrityService } from './IntegrityService'
import { PayloadManagerInterface } from '../Payloads/PayloadManagerInterface'
import { IntegrityPayload } from '@standardnotes/responses'
describe('IntegrityService', () => {
let integrityApi: IntegrityApiInterface
let itemApi: ItemsServerInterface
let payloadManager: PayloadManagerInterface
let internalEventBus: InternalEventBusInterface
const createService = () => new IntegrityService(integrityApi, itemApi, payloadManager, internalEventBus)
beforeEach(() => {
integrityApi = {} as jest.Mocked<IntegrityApiInterface>
integrityApi.checkIntegrity = jest.fn()
itemApi = {} as jest.Mocked<ItemsServerInterface>
itemApi.getSingleItem = jest.fn()
payloadManager = {} as jest.Mocked<PayloadManagerInterface>
payloadManager.integrityPayloads = []
internalEventBus = {} as jest.Mocked<InternalEventBusInterface>
internalEventBus.publishSync = jest.fn()
})
it('should check integrity of payloads and publish mismatches', async () => {
integrityApi.checkIntegrity = jest.fn().mockReturnValue({
data: {
mismatches: [{ uuid: '1-2-3', updated_at_timestamp: 234 } as IntegrityPayload],
},
})
itemApi.getSingleItem = jest.fn().mockReturnValue({
data: {
item: {
uuid: '1-2-3',
content: 'foobar',
} as Partial<TransferPayload>,
},
})
await createService().handleEvent({
type: SyncEvent.SyncRequestsIntegrityCheck,
payload: {
integrityPayloads: [{ uuid: '1-2-3', updated_at_timestamp: 123 } as IntegrityPayload],
source: SyncSource.AfterDownloadFirst,
},
})
expect(internalEventBus.publishSync).toHaveBeenCalledWith(
{
payload: {
rawPayloads: [
{
content: 'foobar',
uuid: '1-2-3',
},
],
source: 5,
},
type: 'IntegrityCheckCompleted',
},
'SEQUENCE',
)
})
it('should publish empty mismatches if everything is in sync', async () => {
integrityApi.checkIntegrity = jest.fn().mockReturnValue({
data: {
mismatches: [],
},
})
await createService().handleEvent({
type: SyncEvent.SyncRequestsIntegrityCheck,
payload: {
integrityPayloads: [{ uuid: '1-2-3', updated_at_timestamp: 123 } as IntegrityPayload],
source: SyncSource.AfterDownloadFirst,
},
})
expect(internalEventBus.publishSync).toHaveBeenCalledWith(
{
payload: {
rawPayloads: [],
source: 5,
},
type: 'IntegrityCheckCompleted',
},
'SEQUENCE',
)
})
it('should not publish mismatches if checking integrity fails', async () => {
integrityApi.checkIntegrity = jest.fn().mockReturnValue({
error: 'Ooops',
})
await createService().handleEvent({
type: SyncEvent.SyncRequestsIntegrityCheck,
payload: {
integrityPayloads: [{ uuid: '1-2-3', updated_at_timestamp: 123 } as IntegrityPayload],
source: SyncSource.AfterDownloadFirst,
},
})
expect(internalEventBus.publishSync).not.toHaveBeenCalled()
})
it('should publish empty mismatches if fetching items fails', async () => {
integrityApi.checkIntegrity = jest.fn().mockReturnValue({
data: {
mismatches: [{ uuid: '1-2-3', updated_at_timestamp: 234 } as IntegrityPayload],
},
})
itemApi.getSingleItem = jest.fn().mockReturnValue({
error: 'Ooops',
})
await createService().handleEvent({
type: SyncEvent.SyncRequestsIntegrityCheck,
payload: {
integrityPayloads: [{ uuid: '1-2-3', updated_at_timestamp: 123 } as IntegrityPayload],
source: SyncSource.AfterDownloadFirst,
},
})
expect(internalEventBus.publishSync).toHaveBeenCalledWith(
{
payload: {
rawPayloads: [],
source: 5,
},
type: 'IntegrityCheckCompleted',
},
'SEQUENCE',
)
})
it('should not handle different event types', async () => {
await createService().handleEvent({
type: SyncEvent.SyncCompletedWithAllItemsUploaded,
payload: {
integrityPayloads: [{ uuid: '1-2-3', updated_at_timestamp: 123 } as IntegrityPayload],
source: SyncSource.AfterDownloadFirst,
},
})
expect(integrityApi.checkIntegrity).not.toHaveBeenCalled()
expect(itemApi.getSingleItem).not.toHaveBeenCalled()
expect(internalEventBus.publishSync).not.toHaveBeenCalled()
})
})

View File

@@ -0,0 +1,62 @@
import { IntegrityEvent } from './IntegrityEvent'
import { AbstractService } from '../Service/AbstractService'
import { ItemsServerInterface } from '../Item/ItemsServerInterface'
import { IntegrityApiInterface } from './IntegrityApiInterface'
import { GetSingleItemResponse } from '@standardnotes/responses'
import { InternalEventHandlerInterface } from '../Internal/InternalEventHandlerInterface'
import { InternalEventInterface } from '../Internal/InternalEventInterface'
import { InternalEventBusInterface } from '../Internal/InternalEventBusInterface'
import { SyncEvent } from '../Event/SyncEvent'
import { IntegrityEventPayload } from './IntegrityEventPayload'
import { SyncSource } from '../Sync/SyncSource'
import { PayloadManagerInterface } from '../Payloads/PayloadManagerInterface'
export class IntegrityService
extends AbstractService<IntegrityEvent, IntegrityEventPayload>
implements InternalEventHandlerInterface
{
constructor(
private integrityApi: IntegrityApiInterface,
private itemApi: ItemsServerInterface,
private payloadManager: PayloadManagerInterface,
protected override internalEventBus: InternalEventBusInterface,
) {
super(internalEventBus)
}
async handleEvent(event: InternalEventInterface): Promise<void> {
if (event.type !== SyncEvent.SyncRequestsIntegrityCheck) {
return
}
const integrityCheckResponse = await this.integrityApi.checkIntegrity(this.payloadManager.integrityPayloads)
if (integrityCheckResponse.error !== undefined) {
this.log(`Could not obtain integrity check: ${integrityCheckResponse.error}`)
return
}
const serverItemResponsePromises: Promise<GetSingleItemResponse>[] = []
for (const mismatch of integrityCheckResponse.data.mismatches) {
serverItemResponsePromises.push(this.itemApi.getSingleItem(mismatch.uuid))
}
const serverItemResponses = await Promise.all(serverItemResponsePromises)
const rawPayloads = []
for (const serverItemResponse of serverItemResponses) {
if (serverItemResponse.data === undefined || serverItemResponse.error || !('item' in serverItemResponse.data)) {
this.log(`Could not obtain item for integrity adjustments: ${serverItemResponse.error}`)
continue
}
rawPayloads.push(serverItemResponse.data.item)
}
await this.notifyEventSync(IntegrityEvent.IntegrityCheckCompleted, {
rawPayloads: rawPayloads,
source: (event.payload as { source: SyncSource }).source,
})
}
}