|
@@ -1,5 +1,6 @@
|
|
|
'use strict'
|
|
'use strict'
|
|
|
|
|
|
|
|
|
|
+const { createHash } = require('node:crypto')
|
|
|
const { expect } = require('chai')
|
|
const { expect } = require('chai')
|
|
|
|
|
|
|
|
const cleanup = require('./support/cleanup')
|
|
const cleanup = require('./support/cleanup')
|
|
@@ -11,6 +12,8 @@ const persistChanges = storage.persistChanges
|
|
|
|
|
|
|
|
const core = require('overleaf-editor-core')
|
|
const core = require('overleaf-editor-core')
|
|
|
const AddFileOperation = core.AddFileOperation
|
|
const AddFileOperation = core.AddFileOperation
|
|
|
|
|
+const EditFileOperation = core.EditFileOperation
|
|
|
|
|
+const TextOperation = core.TextOperation
|
|
|
const Change = core.Change
|
|
const Change = core.Change
|
|
|
const Chunk = core.Chunk
|
|
const Chunk = core.Chunk
|
|
|
const File = core.File
|
|
const File = core.File
|
|
@@ -28,7 +31,7 @@ describe('persistChanges', function () {
|
|
|
farFuture.setTime(farFuture.getTime() + 7 * 24 * 3600 * 1000)
|
|
farFuture.setTime(farFuture.getTime() + 7 * 24 * 3600 * 1000)
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
- it('persists changes', function () {
|
|
|
|
|
|
|
+ it('persists changes', async function () {
|
|
|
const limitsToPersistImmediately = {
|
|
const limitsToPersistImmediately = {
|
|
|
minChangeTimestamp: farFuture,
|
|
minChangeTimestamp: farFuture,
|
|
|
maxChangeTimestamp: farFuture,
|
|
maxChangeTimestamp: farFuture,
|
|
@@ -41,29 +44,29 @@ describe('persistChanges', function () {
|
|
|
)
|
|
)
|
|
|
const changes = [change]
|
|
const changes = [change]
|
|
|
|
|
|
|
|
- return chunkStore
|
|
|
|
|
- .initializeProject(projectId)
|
|
|
|
|
- .then(() => {
|
|
|
|
|
- return persistChanges(projectId, changes, limitsToPersistImmediately, 0)
|
|
|
|
|
- })
|
|
|
|
|
- .then(result => {
|
|
|
|
|
- const history = new History(new Snapshot(), changes)
|
|
|
|
|
- const currentChunk = new Chunk(history, 0)
|
|
|
|
|
- expect(result).to.deep.equal({
|
|
|
|
|
- numberOfChangesPersisted: 1,
|
|
|
|
|
- originalEndVersion: 0,
|
|
|
|
|
- currentChunk,
|
|
|
|
|
- })
|
|
|
|
|
- return chunkStore.loadLatest(projectId)
|
|
|
|
|
- })
|
|
|
|
|
- .then(chunk => {
|
|
|
|
|
- expect(chunk.getStartVersion()).to.equal(0)
|
|
|
|
|
- expect(chunk.getEndVersion()).to.equal(1)
|
|
|
|
|
- expect(chunk.getChanges().length).to.equal(1)
|
|
|
|
|
- })
|
|
|
|
|
|
|
+ await chunkStore.initializeProject(projectId)
|
|
|
|
|
+ const result = await persistChanges(
|
|
|
|
|
+ projectId,
|
|
|
|
|
+ changes,
|
|
|
|
|
+ limitsToPersistImmediately,
|
|
|
|
|
+ 0
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ const history = new History(new Snapshot(), changes)
|
|
|
|
|
+ const currentChunk = new Chunk(history, 0)
|
|
|
|
|
+ expect(result).to.deep.equal({
|
|
|
|
|
+ numberOfChangesPersisted: 1,
|
|
|
|
|
+ originalEndVersion: 0,
|
|
|
|
|
+ currentChunk,
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ const chunk = await chunkStore.loadLatest(projectId)
|
|
|
|
|
+ expect(chunk.getStartVersion()).to.equal(0)
|
|
|
|
|
+ expect(chunk.getEndVersion()).to.equal(1)
|
|
|
|
|
+ expect(chunk.getChanges().length).to.equal(1)
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
- it('persists changes in two chunks', function () {
|
|
|
|
|
|
|
+ it('persists changes in two chunks', async function () {
|
|
|
const limitsToPersistImmediately = {
|
|
const limitsToPersistImmediately = {
|
|
|
maxChunkChanges: 1,
|
|
maxChunkChanges: 1,
|
|
|
minChangeTimestamp: farFuture,
|
|
minChangeTimestamp: farFuture,
|
|
@@ -82,36 +85,36 @@ describe('persistChanges', function () {
|
|
|
)
|
|
)
|
|
|
const changes = [firstChange, secondChange]
|
|
const changes = [firstChange, secondChange]
|
|
|
|
|
|
|
|
- return chunkStore
|
|
|
|
|
- .initializeProject(projectId)
|
|
|
|
|
- .then(() => {
|
|
|
|
|
- return persistChanges(projectId, changes, limitsToPersistImmediately, 0)
|
|
|
|
|
- })
|
|
|
|
|
- .then(result => {
|
|
|
|
|
- const snapshot = Snapshot.fromRaw({
|
|
|
|
|
- files: {
|
|
|
|
|
- 'a.tex': {
|
|
|
|
|
- content: '',
|
|
|
|
|
- },
|
|
|
|
|
- },
|
|
|
|
|
- })
|
|
|
|
|
- const history = new History(snapshot, [secondChange])
|
|
|
|
|
- const currentChunk = new Chunk(history, 1)
|
|
|
|
|
- expect(result).to.deep.equal({
|
|
|
|
|
- numberOfChangesPersisted: 2,
|
|
|
|
|
- originalEndVersion: 0,
|
|
|
|
|
- currentChunk,
|
|
|
|
|
- })
|
|
|
|
|
- return chunkStore.loadLatest(projectId)
|
|
|
|
|
- })
|
|
|
|
|
- .then(chunk => {
|
|
|
|
|
- expect(chunk.getStartVersion()).to.equal(1)
|
|
|
|
|
- expect(chunk.getEndVersion()).to.equal(2)
|
|
|
|
|
- expect(chunk.getChanges().length).to.equal(1)
|
|
|
|
|
- })
|
|
|
|
|
|
|
+ await chunkStore.initializeProject(projectId)
|
|
|
|
|
+ const result = await persistChanges(
|
|
|
|
|
+ projectId,
|
|
|
|
|
+ changes,
|
|
|
|
|
+ limitsToPersistImmediately,
|
|
|
|
|
+ 0
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ const snapshot = Snapshot.fromRaw({
|
|
|
|
|
+ files: {
|
|
|
|
|
+ 'a.tex': {
|
|
|
|
|
+ content: '',
|
|
|
|
|
+ },
|
|
|
|
|
+ },
|
|
|
|
|
+ })
|
|
|
|
|
+ const history = new History(snapshot, [secondChange])
|
|
|
|
|
+ const currentChunk = new Chunk(history, 1)
|
|
|
|
|
+ expect(result).to.deep.equal({
|
|
|
|
|
+ numberOfChangesPersisted: 2,
|
|
|
|
|
+ originalEndVersion: 0,
|
|
|
|
|
+ currentChunk,
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ const chunk = await chunkStore.loadLatest(projectId)
|
|
|
|
|
+ expect(chunk.getStartVersion()).to.equal(1)
|
|
|
|
|
+ expect(chunk.getEndVersion()).to.equal(2)
|
|
|
|
|
+ expect(chunk.getChanges().length).to.equal(1)
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
- it('persists the snapshot at the start of the chunk', function () {
|
|
|
|
|
|
|
+ it('persists the snapshot at the start of the chunk', async function () {
|
|
|
const limitsToPersistImmediately = {
|
|
const limitsToPersistImmediately = {
|
|
|
maxChunkChanges: 2,
|
|
maxChunkChanges: 2,
|
|
|
minChangeTimestamp: farFuture,
|
|
minChangeTimestamp: farFuture,
|
|
@@ -130,29 +133,29 @@ describe('persistChanges', function () {
|
|
|
)
|
|
)
|
|
|
const changes = [firstChange, secondChange]
|
|
const changes = [firstChange, secondChange]
|
|
|
|
|
|
|
|
- return chunkStore
|
|
|
|
|
- .initializeProject(projectId)
|
|
|
|
|
- .then(() => {
|
|
|
|
|
- return persistChanges(projectId, changes, limitsToPersistImmediately, 0)
|
|
|
|
|
- })
|
|
|
|
|
- .then(result => {
|
|
|
|
|
- const history = new History(new Snapshot(), changes)
|
|
|
|
|
- const currentChunk = new Chunk(history, 0)
|
|
|
|
|
- expect(result).to.deep.equal({
|
|
|
|
|
- numberOfChangesPersisted: 2,
|
|
|
|
|
- originalEndVersion: 0,
|
|
|
|
|
- currentChunk,
|
|
|
|
|
- })
|
|
|
|
|
- return chunkStore.loadLatest(projectId)
|
|
|
|
|
- })
|
|
|
|
|
- .then(chunk => {
|
|
|
|
|
- expect(chunk.getStartVersion()).to.equal(0)
|
|
|
|
|
- expect(chunk.getEndVersion()).to.equal(2)
|
|
|
|
|
- expect(chunk.getChanges().length).to.equal(2)
|
|
|
|
|
- })
|
|
|
|
|
|
|
+ await chunkStore.initializeProject(projectId)
|
|
|
|
|
+ const result = await persistChanges(
|
|
|
|
|
+ projectId,
|
|
|
|
|
+ changes,
|
|
|
|
|
+ limitsToPersistImmediately,
|
|
|
|
|
+ 0
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ const history = new History(new Snapshot(), changes)
|
|
|
|
|
+ const currentChunk = new Chunk(history, 0)
|
|
|
|
|
+ expect(result).to.deep.equal({
|
|
|
|
|
+ numberOfChangesPersisted: 2,
|
|
|
|
|
+ originalEndVersion: 0,
|
|
|
|
|
+ currentChunk,
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ const chunk = await chunkStore.loadLatest(projectId)
|
|
|
|
|
+ expect(chunk.getStartVersion()).to.equal(0)
|
|
|
|
|
+ expect(chunk.getEndVersion()).to.equal(2)
|
|
|
|
|
+ expect(chunk.getChanges().length).to.equal(2)
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
- it("errors if the version doesn't match the latest chunk", function () {
|
|
|
|
|
|
|
+ it("errors if the version doesn't match the latest chunk", async function () {
|
|
|
const limitsToPersistImmediately = {
|
|
const limitsToPersistImmediately = {
|
|
|
minChangeTimestamp: farFuture,
|
|
minChangeTimestamp: farFuture,
|
|
|
maxChangeTimestamp: farFuture,
|
|
maxChangeTimestamp: farFuture,
|
|
@@ -169,18 +172,82 @@ describe('persistChanges', function () {
|
|
|
[]
|
|
[]
|
|
|
)
|
|
)
|
|
|
const changes = [firstChange, secondChange]
|
|
const changes = [firstChange, secondChange]
|
|
|
- return chunkStore
|
|
|
|
|
- .initializeProject(projectId)
|
|
|
|
|
- .then(() => {
|
|
|
|
|
- return persistChanges(projectId, changes, limitsToPersistImmediately, 1)
|
|
|
|
|
- })
|
|
|
|
|
- .then(() => {
|
|
|
|
|
- expect.fail()
|
|
|
|
|
- })
|
|
|
|
|
- .catch(err => {
|
|
|
|
|
- expect(err.message).to.equal(
|
|
|
|
|
- 'client sent updates with end_version 1 but latest chunk has end_version 0'
|
|
|
|
|
- )
|
|
|
|
|
- })
|
|
|
|
|
|
|
+
|
|
|
|
|
+ await chunkStore.initializeProject(projectId)
|
|
|
|
|
+ await expect(
|
|
|
|
|
+ persistChanges(projectId, changes, limitsToPersistImmediately, 1)
|
|
|
|
|
+ ).to.be.rejectedWith(
|
|
|
|
|
+ 'client sent updates with end_version 1 but latest chunk has end_version 0'
|
|
|
|
|
+ )
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ describe('content hash validation', function () {
|
|
|
|
|
+ it('acccepts a change with a valid hash', async function () {
|
|
|
|
|
+ const limitsToPersistImmediately = {
|
|
|
|
|
+ minChangeTimestamp: farFuture,
|
|
|
|
|
+ maxChangeTimestamp: farFuture,
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const projectId = fixtures.docs.uninitializedProject.id
|
|
|
|
|
+ await chunkStore.initializeProject(projectId)
|
|
|
|
|
+ const textOperation = new TextOperation()
|
|
|
|
|
+ textOperation.insert('hello ')
|
|
|
|
|
+ textOperation.retain(5)
|
|
|
|
|
+ textOperation.contentHash = hashString('hello world')
|
|
|
|
|
+ const change = new Change(
|
|
|
|
|
+ [
|
|
|
|
|
+ new AddFileOperation('a.tex', File.fromString('world')),
|
|
|
|
|
+ new EditFileOperation('a.tex', textOperation),
|
|
|
|
|
+ ],
|
|
|
|
|
+ new Date(),
|
|
|
|
|
+ []
|
|
|
|
|
+ )
|
|
|
|
|
+ const changes = [change]
|
|
|
|
|
+
|
|
|
|
|
+ const result = await persistChanges(
|
|
|
|
|
+ projectId,
|
|
|
|
|
+ changes,
|
|
|
|
|
+ limitsToPersistImmediately,
|
|
|
|
|
+ 0
|
|
|
|
|
+ )
|
|
|
|
|
+ expect(result.numberOfChangesPersisted).to.equal(1)
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ it('acccepts a change with an invalid hash (only logs for now)', async function () {
|
|
|
|
|
+ const limitsToPersistImmediately = {
|
|
|
|
|
+ minChangeTimestamp: farFuture,
|
|
|
|
|
+ maxChangeTimestamp: farFuture,
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const projectId = fixtures.docs.uninitializedProject.id
|
|
|
|
|
+ await chunkStore.initializeProject(projectId)
|
|
|
|
|
+ const textOperation = new TextOperation()
|
|
|
|
|
+ textOperation.insert('hello ')
|
|
|
|
|
+ textOperation.retain(5)
|
|
|
|
|
+ textOperation.contentHash = hashString('bad hash')
|
|
|
|
|
+ const change = new Change(
|
|
|
|
|
+ [
|
|
|
|
|
+ new AddFileOperation('a.tex', File.fromString('world')),
|
|
|
|
|
+ new EditFileOperation('a.tex', textOperation),
|
|
|
|
|
+ ],
|
|
|
|
|
+ new Date(),
|
|
|
|
|
+ []
|
|
|
|
|
+ )
|
|
|
|
|
+ const changes = [change]
|
|
|
|
|
+
|
|
|
|
|
+ const result = await persistChanges(
|
|
|
|
|
+ projectId,
|
|
|
|
|
+ changes,
|
|
|
|
|
+ limitsToPersistImmediately,
|
|
|
|
|
+ 0
|
|
|
|
|
+ )
|
|
|
|
|
+ expect(result.numberOfChangesPersisted).to.equal(1)
|
|
|
|
|
+ })
|
|
|
})
|
|
})
|
|
|
})
|
|
})
|
|
|
|
|
+
|
|
|
|
|
+function hashString(s) {
|
|
|
|
|
+ const hash = createHash('sha-1')
|
|
|
|
|
+ hash.update(s)
|
|
|
|
|
+ return hash.digest('hex')
|
|
|
|
|
+}
|