mirror of
https://github.com/Start9Labs/patch-db.git
synced 2026-03-26 10:21:53 +00:00
move client from separate repo
This commit is contained in:
26
client/tests/mocks/bootstrapper.mock.ts
Normal file
26
client/tests/mocks/bootstrapper.mock.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { Bootstrapper, DBCache } from '../../lib/patch-db'
|
||||
|
||||
export class MockBootstrapper<T> implements Bootstrapper<T> {
|
||||
|
||||
constructor (
|
||||
private sequence: number = 0,
|
||||
private data: T = { } as T,
|
||||
) { }
|
||||
|
||||
async init (): Promise<DBCache<T>> {
|
||||
return {
|
||||
sequence: this.sequence,
|
||||
data: this.data as T,
|
||||
}
|
||||
}
|
||||
|
||||
async update (cache: DBCache<T>): Promise<void> {
|
||||
this.sequence = cache.sequence
|
||||
this.data = cache.data
|
||||
}
|
||||
|
||||
async clear (): Promise<void> {
|
||||
this.sequence = 0
|
||||
this.data = { } as T
|
||||
}
|
||||
}
|
||||
17
client/tests/mocks/http.mock.ts
Normal file
17
client/tests/mocks/http.mock.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { Http } from '../../lib/patch-db'
|
||||
import { Revision, Dump } from '../../lib/sequence-store'
|
||||
|
||||
export class MockHttp<T> implements Http<T> {
|
||||
constructor (private readonly mockData: {
|
||||
getSequences: Revision[],
|
||||
getDump: Dump<T>
|
||||
}) { }
|
||||
|
||||
getRevisions (): Promise<Revision[]> {
|
||||
return Promise.resolve(this.mockData.getSequences)
|
||||
}
|
||||
|
||||
getDump (): Promise<Dump<T>> {
|
||||
return Promise.resolve(this.mockData.getDump)
|
||||
}
|
||||
}
|
||||
1
client/tests/mocks/mock-data.json
Normal file
1
client/tests/mocks/mock-data.json
Normal file
File diff suppressed because one or more lines are too long
18
client/tests/mocks/source.mock.ts
Normal file
18
client/tests/mocks/source.mock.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { Observable } from 'rxjs'
|
||||
import { UpdateReal } from '../../lib/sequence-store'
|
||||
import { Source } from '../../lib/source/source'
|
||||
|
||||
export class MockSource<T> implements Source<T> {
|
||||
|
||||
constructor (
|
||||
private readonly mockData: Observable<UpdateReal<T>>,
|
||||
) { }
|
||||
|
||||
watch$ (): Observable<UpdateReal<T>> {
|
||||
return this.mockData
|
||||
}
|
||||
|
||||
start (): void { }
|
||||
|
||||
stop (): void { }
|
||||
}
|
||||
90
client/tests/patch-db.test.ts
Normal file
90
client/tests/patch-db.test.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import { expect } from 'chai'
|
||||
import { TestScheduler } from 'rxjs/testing'
|
||||
import { PatchDB } from '../lib/patch-db'
|
||||
import { MockSource } from './mocks/source.mock'
|
||||
import { MockHttp } from './mocks/http.mock'
|
||||
import { PatchOp } from '../lib/patch-db'
|
||||
import { from } from 'rxjs'
|
||||
import { MockBootstrapper } from './mocks/bootstrapper.mock'
|
||||
import { UpdateReal } from '../lib/sequence-store'
|
||||
import { RemoveOperation } from 'fast-json-patch'
|
||||
import 'chai-string'
|
||||
|
||||
type Test = { a: string, b: number[], c: object, newKey?: string }
|
||||
|
||||
describe('patch db', function () {
|
||||
let scheduler: TestScheduler
|
||||
|
||||
beforeEach(() => {
|
||||
scheduler = new TestScheduler((actual, expected) => {
|
||||
// console.log('actual', JSON.stringify(actual))
|
||||
// console.log('expected', JSON.stringify(expected))
|
||||
expect(actual).eql(expected)
|
||||
})
|
||||
})
|
||||
|
||||
it('dumps', () => {
|
||||
scheduler.run(({ expectObservable, cold }) => {
|
||||
const initialData: Test = { a: 'value', b: [1, 2, 3], c: { d: 1, e: 2, f: 3 } }
|
||||
const bootstrapper = new MockBootstrapper(0, initialData)
|
||||
const http = new MockHttp( { getSequences: [], getDump: { id: 0, value: { }, expireId: null } } )
|
||||
const updates = {
|
||||
a: { id: 1, value: { a: 'value1', b: [1, 2, 3], c: { d: 1, e: 2, f: 3 } }, expireId: null },
|
||||
b: { id: 3, value: { a: 'value3', b: [1, 2, 3], c: { d: 1, e: 2, f: 3 } }, expireId: null },
|
||||
c: { id: 2, value: { a: 'value2', b: [1, 2, 3], c: { d: 1, e: 2, f: 3 } }, expireId: null }, // ooo for fun
|
||||
}
|
||||
const source = new MockSource<Test>(
|
||||
cold(Object.keys(updates).join(''), updates),
|
||||
)
|
||||
|
||||
PatchDB.init({ sources: [source], http, bootstrapper }).then(pdb => {
|
||||
pdb.sync$().subscribe()
|
||||
expectObservable(pdb.store.watch$()).toBe('ab-', { a: updates.a.value, b: updates.b.value })
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('replaces + adds', () => {
|
||||
scheduler.run( ({ expectObservable, cold }) => {
|
||||
const initialData: Test = { a: 'value', b: [1, 2, 3], c: { d: 1, e: 2, f: 3 } }
|
||||
const finalStore: Test = { a: 'value1', b: [1, 2, 3], c: { d: 1, e: 2, f: 3 }, newKey: 'newValue' }
|
||||
const bootstrapper = new MockBootstrapper(0, initialData )
|
||||
const http = new MockHttp({ getSequences: [], getDump: { id: 0, value: { }, expireId: null } } )
|
||||
const updates = {
|
||||
a: { id: 1, value: { a: 'value1', b: [1, 2, 3], c: { d: 1, e: 2, f: 3 } }, expireId: null },
|
||||
b: { id: 2, patch: [{ op: PatchOp.ADD, value: 'newValue', path: '/newKey' }], expireId: null},
|
||||
}
|
||||
const source = new MockSource<Test>(
|
||||
cold(Object.keys(updates).join(''), updates),
|
||||
)
|
||||
|
||||
PatchDB.init({ sources: [source], http, bootstrapper }).then(pdb => {
|
||||
pdb.sync$().subscribe()
|
||||
expectObservable(pdb.store.watch$()).toBe('ab', { a: updates.a.value, b: finalStore })
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('gets db dump with invalid patch', done => {
|
||||
const initialData: Test = { a: 'value', b: [1, 2, 3], c: { d: 1, e: 2, f: 3 } }
|
||||
const finalStore: Test = { a: 'value1', b: [1, 2, 3], c: { d: 1, e: 2, f: 3 }, newKey: 'newValue' }
|
||||
const bootstrapper = new MockBootstrapper(0, initialData)
|
||||
const http = new MockHttp({ getSequences: [], getDump: { id: 2, value: finalStore, expireId: null } })
|
||||
const updates: UpdateReal<any>[] = [
|
||||
{ id: 1, value: { a: 'value1', b: [1, 2, 3], c: { d: 1, e: 2, f: 3 } }, expireId: null },
|
||||
{ id: 2, patch: [{ op: PatchOp.REMOVE, path: '/newKey' } as RemoveOperation], expireId: null},
|
||||
]
|
||||
const source = new MockSource<Test>(
|
||||
from(updates),
|
||||
)
|
||||
|
||||
PatchDB.init({ sources: [source], http, bootstrapper }).then(pdb => {
|
||||
let counter = 0
|
||||
pdb.store.watch$().subscribe(i => {
|
||||
counter ++
|
||||
if (counter === 2) done()
|
||||
})
|
||||
pdb.sync$().subscribe()
|
||||
})
|
||||
})
|
||||
})
|
||||
195
client/tests/seq.test.ts
Normal file
195
client/tests/seq.test.ts
Normal file
@@ -0,0 +1,195 @@
|
||||
import { expect } from 'chai'
|
||||
import { PatchOp } from '../lib/patch-db'
|
||||
import { TestScheduler } from 'rxjs/testing'
|
||||
import { Result, SequenceStore } from '../lib/sequence-store'
|
||||
import { concatMap, map } from 'rxjs/operators'
|
||||
import { Store } from '../lib/store'
|
||||
import { RemoveOperation } from 'fast-json-patch'
|
||||
import 'chai-string'
|
||||
|
||||
type TestStore = { a: string, b: number[], c?: { [key: string]: number } }
|
||||
describe('sequence store', function () {
|
||||
let scheduler: TestScheduler
|
||||
beforeEach(() => {
|
||||
scheduler = new TestScheduler((actual, expected) => {
|
||||
// console.log('actual', JSON.stringify(actual))
|
||||
// console.log('expected', JSON.stringify(expected))
|
||||
expect(actual).eql(expected)
|
||||
})
|
||||
})
|
||||
|
||||
it('dumps', done => {
|
||||
const initialStore: TestStore = { a: 'value', b: [1, 2, 3], c: { d: 1, e: 2, f: 3 } }
|
||||
const finalStore: TestStore = { a: 'valueX', b: [0], c: { g: 10 } }
|
||||
const store = new Store(initialStore)
|
||||
const toTest = new SequenceStore(store, 0)
|
||||
|
||||
toTest.update$({ id: 5, value: finalStore, expireId: null }).subscribe(() => {
|
||||
expect(toTest.store.peek).eql(finalStore)
|
||||
expect(toTest.sequence).eql(5)
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('ignores dump for id too low', done => {
|
||||
const initialStore: TestStore = { a: 'value', b: [1, 2, 3], c: { d: 1, e: 2, f: 3 } }
|
||||
const finalStore: TestStore = { a: 'valueX', b: [0], c: { g: 10 } }
|
||||
const store = new Store(initialStore)
|
||||
const toTest = new SequenceStore(store, 0)
|
||||
|
||||
toTest.update$({ id: 5, value: finalStore, expireId: null }).pipe(concatMap(() =>
|
||||
toTest.update$({ id: 4, value: initialStore, expireId: null }),
|
||||
)).subscribe(() => {
|
||||
expect(toTest.store.peek).eql(finalStore)
|
||||
expect(toTest.sequence).eql(5)
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('revises', done => {
|
||||
const initialStore: TestStore = { a: 'value', b: [1, 2, 3], c: { d: 1, e: 2, f: 3 } }
|
||||
const finalStore: TestStore = { a: 'value', b: [1, 2, 3], c: { g: 10 } }
|
||||
const store = new Store(initialStore)
|
||||
const toTest = new SequenceStore(store, 0)
|
||||
|
||||
toTest.update$({ id: 1, patch: [{ op: PatchOp.REPLACE, value: finalStore.c, path: '/c' }], expireId: null }).subscribe(() => {
|
||||
expect(toTest.store.peek).eql(finalStore)
|
||||
expect(toTest.sequence).eql(1)
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('saves a revision when not next in line', done => {
|
||||
const initialStore: TestStore = { a: 'value', b: [1, 2, 3], c: { d: 1, e: 2, f: 3 } }
|
||||
const finalStore: TestStore = { a: 'value', b: [1, 2, 3], c: { g: 10 } }
|
||||
const store = new Store(initialStore)
|
||||
const toTest = new SequenceStore(store, 0)
|
||||
|
||||
toTest.update$({ id: 2, patch: [{ op: PatchOp.REPLACE, value: finalStore.c, path: '/c' }], expireId: null }).subscribe(() => {
|
||||
expect(toTest.store.peek).eql(initialStore)
|
||||
expect(toTest.sequence).eql(0)
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('applies saved revisions when contiguous revisions become available', done => {
|
||||
const initialStore: TestStore = { a: 'value', b: [1, 2, 3], c: { d: 1, e: 2, f: 3 } }
|
||||
const finalStore: TestStore = { a: 'value', b: [1, 2, 3, 4], c: { g: 10 } }
|
||||
const store = new Store(initialStore)
|
||||
const toTest = new SequenceStore(store, 0)
|
||||
|
||||
toTest.update$({ id: 2, patch: [{ op: PatchOp.REPLACE, value: finalStore.c, path: '/c' }], expireId: null }).pipe(concatMap(() =>
|
||||
toTest.update$({ id: 1, patch: [{ op: PatchOp.ADD, value: 4, path: '/b/-' }], expireId: null }),
|
||||
)).subscribe(() => {
|
||||
expect(toTest.store.peek).eql(finalStore)
|
||||
expect(toTest.sequence).eql(2)
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('applies saved revisions when contiguous revisions become available part 2', done => {
|
||||
const initialStore: TestStore = { a: 'value', b: [1, 2, 3], c: { d: 1, e: 2, f: 3 } }
|
||||
const finalStore: TestStore = { a: 'value2', b: [0], c: { g: 10 } }
|
||||
const store = new Store(initialStore)
|
||||
const toTest = new SequenceStore(store, 0)
|
||||
|
||||
toTest.update$({ id: 2, patch: [{ op: PatchOp.REPLACE, value: finalStore.c, path: '/c' }], expireId: null }).pipe(concatMap(() =>
|
||||
toTest.update$({ id: 1, value: { a: 'value2', b: [0], c: { d: 1, e: 2, f: 3 } }, expireId: null }),
|
||||
)).subscribe(() => {
|
||||
expect(toTest.store.peek).eql(finalStore)
|
||||
expect(toTest.sequence).eql(2)
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('wipes out stashed patches when sequence is force updated', done => {
|
||||
const initialStore: TestStore = { a: 'value', b: [1, 2, 3], c: { d: 1, e: 2, f: 3 } }
|
||||
const finalStore: TestStore = { a: 'value2', b: [0], c: { g: 10 } }
|
||||
const store = new Store(initialStore)
|
||||
const toTest = new SequenceStore(store, 0)
|
||||
|
||||
// patch gets stashed
|
||||
expect(toTest.viewRevisions().length).eql(0)
|
||||
|
||||
toTest.update$({ id: 2, patch: [{ op: PatchOp.REPLACE, value: finalStore.c, path: '/c' }], expireId: null }).pipe(
|
||||
map(res => expect(res).eql(Result.STASHED) && expect(toTest.viewRevisions().length).eql(1)),
|
||||
concatMap(() => toTest.update$({ id: 3, value: finalStore, expireId: null })),
|
||||
map(res => expect(res).eql(Result.DUMPED) && expect(toTest.viewRevisions().length).eql(0)),
|
||||
).subscribe(() => done())
|
||||
})
|
||||
|
||||
it('emits sequence + state on updates (revisions)', () => {
|
||||
scheduler.run( ({ expectObservable, cold }) => {
|
||||
const initialStore: TestStore = { a: 'value', b: [1, 2, 3], c: { d: 1, e: 2, f: 3 } }
|
||||
const finalStore: TestStore = { a: 'value2', b: [0], c: { g: 10 } }
|
||||
|
||||
const store = new Store(initialStore)
|
||||
const toTest = new SequenceStore(store, 0)
|
||||
const expectedStream = 'ab'
|
||||
|
||||
cold('-b').subscribe(() => {
|
||||
toTest.update$({ id: 3, value: finalStore, expireId: null }).subscribe()
|
||||
})
|
||||
|
||||
expectObservable(toTest.watch$().pipe(
|
||||
map(cache => ({ sequence: cache.sequence, contents: cache.data})),
|
||||
)).toBe(expectedStream, {
|
||||
a: { sequence: 0, contents: initialStore },
|
||||
b: { sequence: 3, contents: finalStore },
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('emits sequence + state on updates (patch)', () => {
|
||||
scheduler.run( ({ expectObservable, cold }) => {
|
||||
const initialStore: TestStore = { a: 'value', b: [1, 2, 3], c: { d: 1, e: 2, f: 3 } }
|
||||
const finalStore: TestStore = { a: 'value', b: [1, 2, 3], c: { d: 1, e: 2, f: 3, g: 4 } }
|
||||
|
||||
const store = new Store(initialStore)
|
||||
const toTest = new SequenceStore(store, 0)
|
||||
const expectedStream = 'ab'
|
||||
|
||||
cold('-b').subscribe(() => {
|
||||
toTest.update$({ id: 1, patch: [{ op: PatchOp.ADD, path: '/c/g', value: 4 }], expireId: null }).subscribe()
|
||||
})
|
||||
|
||||
expectObservable(toTest.watch$().pipe(
|
||||
map(cache => ({ sequence: cache.sequence, contents: cache.data })),
|
||||
)).toBe(expectedStream, {
|
||||
a: { sequence: 0, contents: initialStore },
|
||||
b: { sequence: 1, contents: finalStore },
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('errors bubble out in results', done => {
|
||||
const initialStore : TestStore = { a: 'value' , b: [1, 2, 3], c: { d: 1, e: 2, f: 3 } }
|
||||
const intermediaryStore : TestStore = { a: 'value' , b: [1, 2, 3] }
|
||||
const finalStore : TestStore = { a: 'value' , b: [1, 2, 3] }
|
||||
|
||||
const store = new Store(initialStore)
|
||||
const toTest = new SequenceStore(store, 0)
|
||||
|
||||
const patch1 = {
|
||||
id: 1,
|
||||
patch: [{ op: PatchOp.REMOVE, path: '/c' } as RemoveOperation],
|
||||
expireId: null,
|
||||
}
|
||||
|
||||
const patch2 = {
|
||||
id: 2,
|
||||
patch: [{ op: PatchOp.ADD, value: 4, path: '/c/g' }],
|
||||
expireId: null,
|
||||
}
|
||||
|
||||
toTest.update$(patch1).pipe(
|
||||
map(res => expect(res).eql(Result.REVISED) && expect(toTest.store.peek).eql(intermediaryStore)),
|
||||
concatMap(() => toTest.update$(patch2)),
|
||||
).subscribe(res => {
|
||||
expect(res).eql(Result.ERROR)
|
||||
expect(toTest.store.peek).eql(finalStore)
|
||||
expect(toTest.sequence).eql(1)
|
||||
done()
|
||||
})
|
||||
})
|
||||
})
|
||||
214
client/tests/store.test.ts
Normal file
214
client/tests/store.test.ts
Normal file
@@ -0,0 +1,214 @@
|
||||
import { expect } from 'chai'
|
||||
import { PatchOp } from '../lib/patch-db'
|
||||
import { TestScheduler } from 'rxjs/testing'
|
||||
import { Store } from '../lib/store'
|
||||
import { tap } from 'rxjs/operators'
|
||||
import { AddOperation, RemoveOperation, ReplaceOperation } from 'fast-json-patch'
|
||||
import 'chai-string'
|
||||
|
||||
describe('rx store', function () {
|
||||
let scheduler: TestScheduler
|
||||
beforeEach(() => {
|
||||
scheduler = new TestScheduler((actual, expected) => {
|
||||
// console.log('actual', JSON.stringify(actual))
|
||||
// console.log('expected', JSON.stringify(expected))
|
||||
expect(actual).eql(expected)
|
||||
})
|
||||
})
|
||||
|
||||
it('returns old and new store state', () => {
|
||||
const initialStore = { a: 'value', b: [1, 2, 3], c: { d: 1, e: 2, f: 3 } }
|
||||
const expectedFinalStore = { a: 'value', b: [1, 2, 3], c: { d: 1, e: 2, f: 3 }, newKey: 'newValue', newKey2: 'newValue2', newKey3: 'newValue3' }
|
||||
const toTest = new Store(initialStore)
|
||||
const add: AddOperation<string> = { op: PatchOp.ADD, value: 'newValue', path: '/newKey' }
|
||||
const add2: AddOperation<string> = { op: PatchOp.ADD, value: 'newValue2', path: '/newKey2' }
|
||||
const add3: AddOperation<string> = { op: PatchOp.ADD, value: 'newValue3', path: '/newKey3' }
|
||||
|
||||
const { oldDocument, newDocument} = toTest.applyPatchDocument([add, add2, add3])
|
||||
expect(oldDocument).eql(initialStore)
|
||||
expect(newDocument).eql(expectedFinalStore)
|
||||
})
|
||||
|
||||
it('adds', () => {
|
||||
scheduler.run( ({ expectObservable, cold }) => {
|
||||
const initialStore = { a: 'value', b: [1, 2, 3], c: { d: 1, e: 2, f: 3 } }
|
||||
const expectedIntermediateStore = { a: 'value', b: [1, 2, 3], c: { d: 1, e: 2, f: 3 }, newKey: 'newValue', newKey2: 'newValue2' }
|
||||
const expectedFinalStore = { a: 'value', b: [1, 2, 3], c: { d: 1, e: 2, f: 3 }, newKey: 'newValue', newKey2: 'newValue2', newKey3: 'newValue3' }
|
||||
const toTest = new Store(initialStore)
|
||||
const add: AddOperation<string> = { op: PatchOp.ADD, value: 'newValue', path: '/newKey' }
|
||||
const add2: AddOperation<string> = { op: PatchOp.ADD, value: 'newValue2', path: '/newKey2' }
|
||||
const add3: AddOperation<string> = { op: PatchOp.ADD, value: 'newValue3', path: '/newKey3' }
|
||||
const expectedStream = 'abc'
|
||||
|
||||
cold('-bc', { b: [add, add2], c: [add3] }).subscribe(i => toTest.applyPatchDocument(i))
|
||||
expectObservable(toTest.watch$()).toBe(expectedStream, { a: initialStore, b: expectedIntermediateStore, c: expectedFinalStore })
|
||||
})
|
||||
})
|
||||
|
||||
it('adds + revises + removes', () => {
|
||||
scheduler.run( ({ expectObservable, cold }) => {
|
||||
const initialStore = { a: 'value', b: [1, 2, 3], c: { d: 1, e: 2, f: 3 } }
|
||||
const expectedFinalStore = { a: 'value', b: [1, 2, 3], newKey: 'newValue', newKey2: 'newValue3' }
|
||||
const toTest = new Store(initialStore)
|
||||
const add: AddOperation<string> = { op: PatchOp.ADD, value: 'newValue', path: '/newKey' }
|
||||
const add2: AddOperation<string> = { op: PatchOp.ADD, value: 'newValue2', path: '/newKey2' }
|
||||
const revise: ReplaceOperation<string> = { op: PatchOp.REPLACE, value: 'newValue3', path: '/newKey2' }
|
||||
const remove: RemoveOperation = { op: PatchOp.REMOVE, path: '/c' }
|
||||
const expectedStream = 'ab'
|
||||
|
||||
cold('-b').subscribe(_ => toTest.applyPatchDocument([add, add2, revise, remove]))
|
||||
expectObservable(toTest.watch$()).toBe(expectedStream, { a: initialStore, b: expectedFinalStore })
|
||||
})
|
||||
})
|
||||
|
||||
it('serializes', done => {
|
||||
const initialStore = { a: 'value', b: [1, 2, 3], c: { d: 1, e: 2, f: 3 } }
|
||||
const intermediaryStore = { a: 'value', b: [1, 2, 3], newKey: 'newValue', c: { d: 1, e: 2, f: 3 } }
|
||||
const toTest = new Store(initialStore)
|
||||
|
||||
const add: AddOperation<string> = { op: PatchOp.ADD, value: 'newValue', path: '/newKey' }
|
||||
const unAdd: RemoveOperation = { op: PatchOp.REMOVE, path: '/newKey' }
|
||||
|
||||
let i = 0
|
||||
toTest.watch$().subscribe(t => {
|
||||
if (i === 0) { expect(t).eql(initialStore) }
|
||||
if (i === 1) { expect(t).eql(intermediaryStore) }
|
||||
if (i === 2) { expect(t).eql(initialStore); done() }
|
||||
i += 1
|
||||
})
|
||||
toTest.applyPatchDocument([add])
|
||||
toTest.applyPatchDocument([unAdd])
|
||||
})
|
||||
|
||||
it('doesnt apply invalid patches', done => {
|
||||
const initialStore = { a: 'value', b: [1, 2, 3], c: { d: 1, e: 2, f: 3 } }
|
||||
const toTest = new Store(initialStore)
|
||||
|
||||
const removeValid: RemoveOperation = { op: PatchOp.REMOVE, path: '/b' }
|
||||
const removeInvalid: RemoveOperation = { op: PatchOp.REMOVE, path: '/newKey' }
|
||||
try {
|
||||
toTest.applyPatchDocument([removeValid, removeInvalid])
|
||||
expect(true).eql('We expected an error here')
|
||||
} catch (e) {
|
||||
toTest.watch$().subscribe(t => {
|
||||
expect(t).eql(initialStore)
|
||||
done()
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
it('emits undefined when key disappears', done => {
|
||||
const initialStore = { a: 'value', b: [1, 2, 3], c: { d: 1, e: 2, f: 3 } }
|
||||
const store = new Store(initialStore)
|
||||
|
||||
const remove: RemoveOperation = { op: PatchOp.REMOVE, path: '/c' }
|
||||
let counter = 0
|
||||
store.watch$('c', 'd').pipe(tap(() => counter++)).subscribe({
|
||||
next: i => {
|
||||
if (counter === 1) expect(i).eql(initialStore.c.d)
|
||||
if (counter === 2) expect(i).eql(undefined)
|
||||
if (counter === 2) done()
|
||||
},
|
||||
})
|
||||
store.applyPatchDocument([remove])
|
||||
})
|
||||
|
||||
it('when key returns, sub continues', done => {
|
||||
const initialStore = { a: 'value', b: [1, 2, 3], c: { d: 1, e: 2, f: 3 } }
|
||||
const store = new Store(initialStore)
|
||||
|
||||
const remove: RemoveOperation = { op: PatchOp.REMOVE, path: '/c' }
|
||||
const reAdd: AddOperation<{ d: number }> = { op: PatchOp.ADD, path: '/c', value: { d: 1 } }
|
||||
let counter = 0
|
||||
store.watch$('c', 'd').pipe(tap(() => counter++)).subscribe({
|
||||
next: i => {
|
||||
if (counter === 1) expect(i).eql(initialStore.c.d)
|
||||
if (counter === 2) expect(i).eql(undefined)
|
||||
if (counter === 3) expect(i).eql(reAdd.value.d)
|
||||
if (counter === 3) done()
|
||||
},
|
||||
})
|
||||
store.applyPatchDocument([remove])
|
||||
store.applyPatchDocument([reAdd])
|
||||
})
|
||||
|
||||
it('watches a single property', () => {
|
||||
scheduler.run( ({ expectObservable, cold }) => {
|
||||
const initialStore = { a: 'value', b: [1, 2, 3], c: { d: 1, e: 2, f: 3 } }
|
||||
const store = new Store(initialStore)
|
||||
const toTest = store.watch$('b', 1)
|
||||
|
||||
const revise: ReplaceOperation<number> = { op: PatchOp.REPLACE, value: 4, path: '/b/1' }
|
||||
|
||||
const expectedStream = 'ab'
|
||||
|
||||
cold('-b').subscribe(_ => store.applyPatchDocument([revise]))
|
||||
expectObservable(toTest).toBe(expectedStream, { a: initialStore.b[1], b: revise.value })
|
||||
})
|
||||
})
|
||||
|
||||
it('property only emits if it is updated', () => {
|
||||
scheduler.run( ({ expectObservable, cold }) => {
|
||||
const initialStore = { a: 'value', b: [1, 2, 3], c: { d: 1, e: 2, f: 3 } }
|
||||
const store = new Store(initialStore)
|
||||
const toTest = store.watch$('b', 0)
|
||||
|
||||
const revise: ReplaceOperation<number> = { op: PatchOp.REPLACE, value: 4, path: '/b/1' }
|
||||
|
||||
const expectedStream = 'a-'
|
||||
|
||||
cold('-b').subscribe(_ => store.applyPatchDocument([revise]))
|
||||
expectObservable(toTest).toBe(expectedStream, { a: initialStore.b[0] })
|
||||
})
|
||||
})
|
||||
|
||||
it('only does the last updates', () => {
|
||||
scheduler.run( ({ expectObservable, cold }) => {
|
||||
const initialStore = { a: 'value', b: [1, 2, 3], c: { d: 1, e: 2, f: 3 } }
|
||||
const store = new Store(initialStore)
|
||||
const toTest = store.watch$('b', 1)
|
||||
|
||||
const revise1: ReplaceOperation<number> = { op: PatchOp.REPLACE, value: 4, path: '/b/1' }
|
||||
const revise2: ReplaceOperation<number> = { op: PatchOp.REPLACE, value: 5, path: '/b/1' }
|
||||
|
||||
const expectedStream = 'ab'
|
||||
|
||||
cold('-b').subscribe(_ => store.applyPatchDocument([revise1, revise2]))
|
||||
expectObservable(toTest).toBe(expectedStream, { a: initialStore.b[1], b: revise2.value })
|
||||
})
|
||||
})
|
||||
|
||||
it('emits multiple updates', () => {
|
||||
scheduler.run( ({ expectObservable, cold }) => {
|
||||
const initialStore = { a: 'value', b: [1, 2, 3], c: { d: 1, e: 2, f: 3 } }
|
||||
const store = new Store(initialStore)
|
||||
const toTest = store.watch$('b', 1)
|
||||
|
||||
const revise1: ReplaceOperation<number> = { op: PatchOp.REPLACE, value: 4, path: '/b/1' }
|
||||
const revise2: ReplaceOperation<number> = { op: PatchOp.REPLACE, value: 5, path: '/b/1' }
|
||||
|
||||
const expectedStream = 'abc'
|
||||
|
||||
cold('-bc', { b: revise1, c: revise2 }).subscribe(i => {
|
||||
store.applyPatchDocument([i])
|
||||
})
|
||||
expectObservable(toTest).toBe(expectedStream, { a: initialStore.b[1], b: revise1.value, c: revise2.value})
|
||||
})
|
||||
})
|
||||
|
||||
it('does a BIG store', () => {
|
||||
scheduler.run( ({ expectObservable, cold }) => {
|
||||
const fatty = require('./mocks/mock-data.json')
|
||||
const store = new Store(fatty)
|
||||
const toTest = store.watch$('kind')
|
||||
|
||||
const revise: ReplaceOperation<string> = { op: PatchOp.REPLACE, value: 'testing', path: '/kind' }
|
||||
const expectedStream = 'ab'
|
||||
|
||||
cold('-b', { b: revise }).subscribe(i => {
|
||||
store.applyPatchDocument([i])
|
||||
})
|
||||
expectObservable(toTest).toBe(expectedStream, { a: fatty['kind'], b: revise.value })
|
||||
})
|
||||
})
|
||||
})
|
||||
158
client/tests/temp-seq.test.ts
Normal file
158
client/tests/temp-seq.test.ts
Normal file
@@ -0,0 +1,158 @@
|
||||
import { expect } from 'chai'
|
||||
import { PatchOp } from '../lib/patch-db'
|
||||
import { TestScheduler } from 'rxjs/testing'
|
||||
import { Result, SequenceStore } from '../lib/sequence-store'
|
||||
import { concatMap, map } from 'rxjs/operators'
|
||||
import { Store } from '../lib/store'
|
||||
import { RemoveOperation } from 'fast-json-patch'
|
||||
import 'chai-string'
|
||||
|
||||
type TestStore = { a: string, b: number[], c?: { [key: string]: number } }
|
||||
describe('sequence store temp functionality', function () {
|
||||
let scheduler: TestScheduler
|
||||
beforeEach(() => {
|
||||
scheduler = new TestScheduler( (actual, expected) => {
|
||||
// console.log('actual', JSON.stringify(actual))
|
||||
// console.log('expected', JSON.stringify(expected))
|
||||
expect(actual).eql(expected)
|
||||
})
|
||||
})
|
||||
|
||||
it('applies a temp patch', done => {
|
||||
const initialStore: TestStore = { a: 'value', b: [1, 2, 3], c: { d: 1, e: 2, f: 3 } }
|
||||
const finalStore: TestStore = { a: 'value', b: [1, 2, 3], c: { g: 10 } }
|
||||
|
||||
const store = new Store(initialStore)
|
||||
const toTest = new SequenceStore(store, 0)
|
||||
|
||||
toTest.update$({
|
||||
patch: [{ op: PatchOp.REPLACE, value: finalStore.c, path: '/c' }],
|
||||
expiredBy: 'expireMe',
|
||||
}).subscribe(() => {
|
||||
expect(toTest.store.peek).eql(finalStore)
|
||||
expect(toTest.sequence).eql(0)
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('applies multiple temp patches', done => {
|
||||
const initialStore: TestStore = { a: 'value', b: [1, 2, 3], c: { d: 1, e: 2, f: 3 } }
|
||||
const finalStore: TestStore = { a: 'value', b: [0], c: { g: 10 } }
|
||||
|
||||
const store = new Store(initialStore)
|
||||
const toTest = new SequenceStore(store, 0)
|
||||
|
||||
const tempPatch1 = {
|
||||
patch: [{ op: PatchOp.REPLACE, value: finalStore.c, path: '/c' }],
|
||||
expiredBy: 'expireMe1',
|
||||
}
|
||||
|
||||
const tempPatch2 = {
|
||||
patch: [{ op: PatchOp.REPLACE, value: finalStore.b, path: '/b' }],
|
||||
expiredBy: 'expireMe2',
|
||||
}
|
||||
|
||||
toTest.update$(tempPatch1).pipe(concatMap(() =>
|
||||
toTest.update$(tempPatch2),
|
||||
)).subscribe(() => {
|
||||
expect(toTest.store.peek).eql(finalStore)
|
||||
expect(toTest.sequence).eql(0)
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('expires a temp patch', done => {
|
||||
const initialStore: TestStore = { a: 'value', b: [1, 2, 3], c: { d: 1, e: 2, f: 3 } }
|
||||
const intermediaryStore: TestStore = { a: 'value', b: [1, 2, 3], c: { g: 10 } }
|
||||
const finalStore: TestStore = { a: 'value', b: [0], c: { d: 1, e: 2, f: 3 } }
|
||||
|
||||
const store = new Store(initialStore)
|
||||
const toTest = new SequenceStore(store, 0)
|
||||
|
||||
const tempPatch = {
|
||||
patch: [{ op: PatchOp.REPLACE, value: intermediaryStore.c, path: '/c' }],
|
||||
expiredBy: 'expireMe',
|
||||
}
|
||||
|
||||
const expirePatch = {
|
||||
id: 1,
|
||||
patch: [{ op: PatchOp.REPLACE, value: finalStore.b, path: '/b' }],
|
||||
expireId: 'expireMe',
|
||||
}
|
||||
|
||||
toTest.update$(tempPatch).pipe(
|
||||
map(() => expect(toTest.store.peek).eql(intermediaryStore)),
|
||||
concatMap(() => toTest.update$(expirePatch)),
|
||||
).subscribe(() => {
|
||||
expect(toTest.store.peek).eql(finalStore)
|
||||
expect(toTest.sequence).eql(1)
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('expires a temp patch beneath a second temp patch', done => {
|
||||
const initialStore : TestStore = { a: 'value' , b: [1, 2, 3], c: { d: 1, e: 2, f: 3 } }
|
||||
const intermediaryStore : TestStore = { a: 'value' , b: [0] , c: { g: 10 } }
|
||||
const finalStore : TestStore = { a: 'valueX', b: [0] , c: { d: 1, e: 2, f: 3 } }
|
||||
|
||||
const store = new Store(initialStore)
|
||||
const toTest = new SequenceStore(store, 0)
|
||||
|
||||
const tempPatch = {
|
||||
patch: [{ op: PatchOp.REPLACE, value: intermediaryStore.c, path: '/c' }],
|
||||
expiredBy: 'expireMe',
|
||||
}
|
||||
|
||||
const tempPatch2 = {
|
||||
patch: [{ op: PatchOp.REPLACE, value: intermediaryStore.b, path: '/b' }],
|
||||
expiredBy: 'expireMe2',
|
||||
}
|
||||
|
||||
const expirePatch = {
|
||||
id: 1,
|
||||
patch: [{ op: PatchOp.REPLACE, value: finalStore.a, path: '/a' }],
|
||||
expireId: 'expireMe',
|
||||
}
|
||||
|
||||
toTest.update$(tempPatch).pipe(
|
||||
concatMap(() => toTest.update$(tempPatch2)),
|
||||
map(() => expect(toTest.store.peek).eql(intermediaryStore)),
|
||||
concatMap(() => toTest.update$(expirePatch)),
|
||||
).subscribe(() => {
|
||||
expect(toTest.store.peek).eql(finalStore)
|
||||
expect(toTest.sequence).eql(1)
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('real patches are genuinely added beneath', done => {
|
||||
const initialStore : TestStore = { a: 'value' , b: [1, 2, 3], c: { d: 1, e: 2, f: 3 } }
|
||||
const intermediaryStore : TestStore = { a: 'value' , b: [1, 2, 3] }
|
||||
const finalStore : TestStore = { a: 'value' , b: [1, 2, 3] }
|
||||
|
||||
const store = new Store(initialStore)
|
||||
const toTest = new SequenceStore(store, 0)
|
||||
|
||||
const tempPatch = {
|
||||
patch: [{ op: PatchOp.REMOVE, path: '/c' } as RemoveOperation],
|
||||
expiredBy: 'expireMe',
|
||||
}
|
||||
|
||||
// this patch would error if the above had been a real patch and not a temp
|
||||
const realPatch = {
|
||||
id: 1,
|
||||
patch: [{ op: PatchOp.ADD, value: 4, path: '/c/g' }],
|
||||
expireId: null,
|
||||
}
|
||||
|
||||
toTest.update$(tempPatch).pipe(
|
||||
map(res => expect(res).eql(Result.TEMP) && expect(toTest.store.peek).eql(intermediaryStore)),
|
||||
concatMap(() => toTest.update$(realPatch)),
|
||||
).subscribe(res => {
|
||||
expect(res).eql(Result.REVISED)
|
||||
expect(toTest.store.peek).eql(finalStore)
|
||||
expect(toTest.sequence).eql(1)
|
||||
done()
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user