mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-05-27 02:51:57 +02:00
[web] Restore main documents with metadata as docs GitOrigin-RevId: f3664689704e9098c2b9e317d65e4ab2633320cb
869 lines
28 KiB
JavaScript
869 lines
28 KiB
JavaScript
const SandboxedModule = require('sandboxed-module')
|
|
const sinon = require('sinon')
|
|
const modulePath = require('path').join(
|
|
__dirname,
|
|
'../../../../app/src/Features/History/RestoreManager'
|
|
)
|
|
const Errors = require('../../../../app/src/Features/Errors/Errors')
|
|
const tk = require('timekeeper')
|
|
const moment = require('moment')
|
|
const { expect } = require('chai')
|
|
|
|
describe('RestoreManager', function () {
|
|
beforeEach(function () {
|
|
tk.freeze(Date.now()) // freeze the time for these tests
|
|
this.RestoreManager = SandboxedModule.require(modulePath, {
|
|
requires: {
|
|
'@overleaf/settings': {},
|
|
'../../infrastructure/FileWriter': (this.FileWriter = { promises: {} }),
|
|
'../Uploads/FileSystemImportManager': (this.FileSystemImportManager = {
|
|
promises: {},
|
|
}),
|
|
'../Editor/EditorController': (this.EditorController = {
|
|
promises: {},
|
|
}),
|
|
'../Project/ProjectLocator': (this.ProjectLocator = { promises: {} }),
|
|
'../DocumentUpdater/DocumentUpdaterHandler':
|
|
(this.DocumentUpdaterHandler = {
|
|
promises: { flushProjectToMongo: sinon.stub().resolves() },
|
|
}),
|
|
'../Docstore/DocstoreManager': (this.DocstoreManager = {
|
|
promises: {},
|
|
}),
|
|
'../Chat/ChatApiHandler': (this.ChatApiHandler = { promises: {} }),
|
|
'../Chat/ChatManager': (this.ChatManager = { promises: {} }),
|
|
'../Editor/EditorRealTimeController': (this.EditorRealTimeController =
|
|
{}),
|
|
'../Project/ProjectGetter': (this.ProjectGetter = { promises: {} }),
|
|
'../Project/ProjectEntityHandler': (this.ProjectEntityHandler = {
|
|
promises: {},
|
|
}),
|
|
},
|
|
})
|
|
this.user_id = 'mock-user-id'
|
|
this.project_id = 'mock-project-id'
|
|
this.version = 42
|
|
})
|
|
|
|
afterEach(function () {
|
|
tk.reset()
|
|
})
|
|
|
|
describe('restoreFileFromV2', function () {
|
|
beforeEach(function () {
|
|
this.RestoreManager.promises._writeFileVersionToDisk = sinon
|
|
.stub()
|
|
.resolves((this.fsPath = '/tmp/path/on/disk'))
|
|
this.RestoreManager.promises._findOrCreateFolder = sinon
|
|
.stub()
|
|
.resolves((this.folder_id = 'mock-folder-id'))
|
|
this.FileSystemImportManager.promises.addEntity = sinon
|
|
.stub()
|
|
.resolves((this.entity = 'mock-entity'))
|
|
})
|
|
|
|
describe('with a file not in a folder', function () {
|
|
beforeEach(async function () {
|
|
this.pathname = 'foo.tex'
|
|
this.result = await this.RestoreManager.promises.restoreFileFromV2(
|
|
this.user_id,
|
|
this.project_id,
|
|
this.version,
|
|
this.pathname
|
|
)
|
|
})
|
|
|
|
it('should write the file version to disk', function () {
|
|
this.RestoreManager.promises._writeFileVersionToDisk
|
|
.calledWith(this.project_id, this.version, this.pathname)
|
|
.should.equal(true)
|
|
})
|
|
|
|
it('should find the root folder', function () {
|
|
this.RestoreManager.promises._findOrCreateFolder
|
|
.calledWith(this.project_id, '')
|
|
.should.equal(true)
|
|
})
|
|
|
|
it('should add the entity', function () {
|
|
this.FileSystemImportManager.promises.addEntity
|
|
.calledWith(
|
|
this.user_id,
|
|
this.project_id,
|
|
this.folder_id,
|
|
'foo.tex',
|
|
this.fsPath,
|
|
false
|
|
)
|
|
.should.equal(true)
|
|
})
|
|
|
|
it('should return the entity', function () {
|
|
expect(this.result).to.equal(this.entity)
|
|
})
|
|
})
|
|
|
|
describe('with a file in a folder', function () {
|
|
beforeEach(async function () {
|
|
this.pathname = 'foo/bar.tex'
|
|
await this.RestoreManager.promises.restoreFileFromV2(
|
|
this.user_id,
|
|
this.project_id,
|
|
this.version,
|
|
this.pathname
|
|
)
|
|
})
|
|
|
|
it('should find the folder', function () {
|
|
this.RestoreManager.promises._findOrCreateFolder
|
|
.calledWith(this.project_id, 'foo')
|
|
.should.equal(true)
|
|
})
|
|
|
|
it('should add the entity by its basename', function () {
|
|
this.FileSystemImportManager.promises.addEntity
|
|
.calledWith(
|
|
this.user_id,
|
|
this.project_id,
|
|
this.folder_id,
|
|
'bar.tex',
|
|
this.fsPath,
|
|
false
|
|
)
|
|
.should.equal(true)
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('_findOrCreateFolder', function () {
|
|
beforeEach(async function () {
|
|
this.EditorController.promises.mkdirp = sinon.stub().resolves({
|
|
newFolders: [],
|
|
lastFolder: { _id: (this.folder_id = 'mock-folder-id') },
|
|
})
|
|
this.result = await this.RestoreManager.promises._findOrCreateFolder(
|
|
this.project_id,
|
|
'folder/name'
|
|
)
|
|
})
|
|
|
|
it('should look up or create the folder', function () {
|
|
this.EditorController.promises.mkdirp
|
|
.calledWith(this.project_id, 'folder/name')
|
|
.should.equal(true)
|
|
})
|
|
|
|
it('should return the folder_id', function () {
|
|
expect(this.result).to.equal(this.folder_id)
|
|
})
|
|
})
|
|
|
|
describe('_addEntityWithUniqueName', function () {
|
|
beforeEach(function () {
|
|
this.addEntityWithName = sinon.stub()
|
|
this.name = 'foo.tex'
|
|
})
|
|
|
|
describe('with a valid name', function () {
|
|
beforeEach(async function () {
|
|
this.addEntityWithName.resolves((this.entity = 'mock-entity'))
|
|
this.result =
|
|
await this.RestoreManager.promises._addEntityWithUniqueName(
|
|
this.addEntityWithName,
|
|
this.name
|
|
)
|
|
})
|
|
|
|
it('should add the entity', function () {
|
|
this.addEntityWithName.calledWith(this.name).should.equal(true)
|
|
})
|
|
|
|
it('should return the entity', function () {
|
|
expect(this.result).to.equal(this.entity)
|
|
})
|
|
})
|
|
|
|
describe('with a duplicate name', function () {
|
|
beforeEach(async function () {
|
|
this.addEntityWithName.rejects(new Errors.DuplicateNameError())
|
|
this.addEntityWithName
|
|
.onSecondCall()
|
|
.resolves((this.entity = 'mock-entity'))
|
|
this.result =
|
|
await this.RestoreManager.promises._addEntityWithUniqueName(
|
|
this.addEntityWithName,
|
|
this.name
|
|
)
|
|
})
|
|
|
|
it('should try to add the entity with its original name', function () {
|
|
this.addEntityWithName.calledWith('foo.tex').should.equal(true)
|
|
})
|
|
|
|
it('should try to add the entity with a unique name', function () {
|
|
const date = moment(new Date()).format('Do MMM YY H:mm:ss')
|
|
this.addEntityWithName
|
|
.calledWith(`foo (Restored on ${date}).tex`)
|
|
.should.equal(true)
|
|
})
|
|
|
|
it('should return the entity', function () {
|
|
expect(this.result).to.equal(this.entity)
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('revertFile', function () {
|
|
beforeEach(function () {
|
|
this.ProjectGetter.promises.getProject = sinon.stub()
|
|
this.ProjectGetter.promises.getProject
|
|
.withArgs(this.project_id)
|
|
.resolves({ overleaf: { history: { rangesSupportEnabled: true } } })
|
|
this.RestoreManager.promises._writeFileVersionToDisk = sinon
|
|
.stub()
|
|
.resolves((this.fsPath = '/tmp/path/on/disk'))
|
|
this.RestoreManager.promises._findOrCreateFolder = sinon
|
|
.stub()
|
|
.resolves((this.folder_id = 'mock-folder-id'))
|
|
this.FileSystemImportManager.promises.addEntity = sinon
|
|
.stub()
|
|
.resolves((this.entity = 'mock-entity'))
|
|
this.RestoreManager.promises._getRangesFromHistory = sinon
|
|
.stub()
|
|
.rejects()
|
|
this.RestoreManager.promises._getMetadataFromHistory = sinon
|
|
.stub()
|
|
.resolves({ metadata: undefined })
|
|
})
|
|
|
|
describe('reverting a project without ranges support', function () {
|
|
beforeEach(function () {
|
|
this.ProjectGetter.promises.getProject = sinon.stub().resolves({
|
|
overleaf: { history: { rangesSupportEnabled: false } },
|
|
})
|
|
})
|
|
|
|
it('should throw an error', async function () {
|
|
await expect(
|
|
this.RestoreManager.promises.revertFile(
|
|
this.user_id,
|
|
this.project_id,
|
|
this.version,
|
|
this.pathname
|
|
)
|
|
).to.eventually.be.rejectedWith('project does not have ranges support')
|
|
})
|
|
})
|
|
|
|
describe('reverting a document with ranges', function () {
|
|
beforeEach(function () {
|
|
this.pathname = 'foo.tex'
|
|
this.comments = [
|
|
{ op: { t: 'comment-in-other-doc', p: 0, c: 'foo' } },
|
|
{ op: { t: 'single-comment', p: 10, c: 'bar' } },
|
|
{ op: { t: 'deleted-comment', p: 20, c: 'baz' } },
|
|
]
|
|
this.remappedComments = [
|
|
{ op: { t: 'duplicate-comment', p: 0, c: 'foo' } },
|
|
{ op: { t: 'single-comment', p: 10, c: 'bar' } },
|
|
]
|
|
this.ProjectLocator.promises.findElementByPath = sinon.stub().rejects()
|
|
this.DocstoreManager.promises.getAllRanges = sinon.stub().resolves([
|
|
{
|
|
ranges: {
|
|
comments: this.comments.slice(0, 1),
|
|
},
|
|
},
|
|
])
|
|
this.ChatApiHandler.promises.duplicateCommentThreads = sinon
|
|
.stub()
|
|
.resolves({
|
|
newThreads: {
|
|
'comment-in-other-doc': {
|
|
duplicateId: 'duplicate-comment',
|
|
},
|
|
},
|
|
})
|
|
this.ChatApiHandler.promises.generateThreadData = sinon.stub().resolves(
|
|
(this.threadData = {
|
|
'single-comment': {
|
|
messages: [
|
|
{
|
|
content: 'message-content',
|
|
timestamp: '2024-01-01T00:00:00.000Z',
|
|
user_id: 'user-1',
|
|
},
|
|
],
|
|
},
|
|
'duplicate-comment': {
|
|
messages: [
|
|
{
|
|
content: 'another message',
|
|
timestamp: '2024-01-01T00:00:00.000Z',
|
|
user_id: 'user-1',
|
|
},
|
|
],
|
|
},
|
|
})
|
|
)
|
|
this.ChatManager.promises.injectUserInfoIntoThreads = sinon
|
|
.stub()
|
|
.resolves(this.threadData)
|
|
|
|
this.EditorRealTimeController.emitToRoom = sinon.stub()
|
|
this.tracked_changes = [
|
|
{
|
|
op: { pos: 4, i: 'bar' },
|
|
metadata: { ts: '2024-01-01T00:00:00.000Z', user_id: 'user-1' },
|
|
},
|
|
{
|
|
op: { pos: 8, d: 'qux' },
|
|
metadata: { ts: '2024-01-01T00:00:00.000Z', user_id: 'user-2' },
|
|
},
|
|
]
|
|
this.FileSystemImportManager.promises.importFile = sinon
|
|
.stub()
|
|
.resolves({ type: 'doc', lines: ['foo', 'bar', 'baz'] })
|
|
this.RestoreManager.promises._getRangesFromHistory = sinon
|
|
.stub()
|
|
.resolves({
|
|
changes: this.tracked_changes,
|
|
comments: this.comments,
|
|
})
|
|
this.RestoreManager.promises._getUpdatesFromHistory = sinon
|
|
.stub()
|
|
.resolves([
|
|
{ toV: this.version, meta: { end_ts: (this.endTs = new Date()) } },
|
|
])
|
|
this.EditorController.promises.addDocWithRanges = sinon
|
|
.stub()
|
|
.resolves((this.addedFile = { _id: 'mock-doc', type: 'doc' }))
|
|
})
|
|
|
|
describe("when reverting a file that doesn't current exist", function () {
|
|
beforeEach(async function () {
|
|
this.data = await this.RestoreManager.promises.revertFile(
|
|
this.user_id,
|
|
this.project_id,
|
|
this.version,
|
|
this.pathname
|
|
)
|
|
})
|
|
|
|
it('should flush the document before fetching ranges', function () {
|
|
expect(
|
|
this.DocumentUpdaterHandler.promises.flushProjectToMongo
|
|
).to.have.been.calledBefore(
|
|
this.DocstoreManager.promises.getAllRanges
|
|
)
|
|
})
|
|
|
|
it('should import the file', function () {
|
|
expect(
|
|
this.EditorController.promises.addDocWithRanges
|
|
).to.have.been.calledWith(
|
|
this.project_id,
|
|
this.folder_id,
|
|
'foo.tex',
|
|
['foo', 'bar', 'baz'],
|
|
{ changes: this.tracked_changes, comments: this.remappedComments }
|
|
)
|
|
})
|
|
|
|
it('should return the created entity', function () {
|
|
expect(this.data).to.deep.equal(this.addedFile)
|
|
})
|
|
|
|
it('should look up ranges', function () {
|
|
expect(
|
|
this.RestoreManager.promises._getRangesFromHistory
|
|
).to.have.been.calledWith(
|
|
this.project_id,
|
|
this.version,
|
|
this.pathname
|
|
)
|
|
})
|
|
})
|
|
|
|
describe('with an existing file in the current project', function () {
|
|
beforeEach(async function () {
|
|
this.ProjectLocator.promises.findElementByPath = sinon
|
|
.stub()
|
|
.resolves({ type: 'file', element: { _id: 'mock-file-id' } })
|
|
this.EditorController.promises.deleteEntity = sinon.stub().resolves()
|
|
|
|
this.data = await this.RestoreManager.promises.revertFile(
|
|
this.user_id,
|
|
this.project_id,
|
|
this.version,
|
|
this.pathname
|
|
)
|
|
})
|
|
|
|
it('should delete the existing file', async function () {
|
|
expect(
|
|
this.EditorController.promises.deleteEntity
|
|
).to.have.been.calledWith(
|
|
this.project_id,
|
|
'mock-file-id',
|
|
'file',
|
|
{
|
|
kind: 'file-restore',
|
|
path: this.pathname,
|
|
version: this.version,
|
|
timestamp: new Date(this.endTs).toISOString(),
|
|
},
|
|
this.user_id
|
|
)
|
|
})
|
|
})
|
|
|
|
describe('with an existing document in the current project', function () {
|
|
beforeEach(async function () {
|
|
this.ProjectLocator.promises.findElementByPath = sinon
|
|
.stub()
|
|
.resolves({ type: 'doc', element: { _id: 'mock-file-id' } })
|
|
this.EditorController.promises.deleteEntity = sinon.stub().resolves()
|
|
|
|
this.data = await this.RestoreManager.promises.revertFile(
|
|
this.user_id,
|
|
this.project_id,
|
|
this.version,
|
|
this.pathname
|
|
)
|
|
})
|
|
|
|
it('should delete the existing document', async function () {
|
|
expect(
|
|
this.EditorController.promises.deleteEntity
|
|
).to.have.been.calledWith(
|
|
this.project_id,
|
|
'mock-file-id',
|
|
'doc',
|
|
{
|
|
kind: 'file-restore',
|
|
path: this.pathname,
|
|
version: this.version,
|
|
timestamp: new Date(this.endTs).toISOString(),
|
|
},
|
|
this.user_id
|
|
)
|
|
})
|
|
|
|
it('should delete the document before flushing', function () {
|
|
expect(
|
|
this.EditorController.promises.deleteEntity
|
|
).to.have.been.calledBefore(
|
|
this.DocumentUpdaterHandler.promises.flushProjectToMongo
|
|
)
|
|
})
|
|
|
|
it('should flush the document before fetching ranges', function () {
|
|
expect(
|
|
this.DocumentUpdaterHandler.promises.flushProjectToMongo
|
|
).to.have.been.calledBefore(
|
|
this.DocstoreManager.promises.getAllRanges
|
|
)
|
|
})
|
|
|
|
it('should import the file', function () {
|
|
expect(
|
|
this.EditorController.promises.addDocWithRanges
|
|
).to.have.been.calledWith(
|
|
this.project_id,
|
|
this.folder_id,
|
|
'foo.tex',
|
|
['foo', 'bar', 'baz'],
|
|
{ changes: this.tracked_changes, comments: this.remappedComments },
|
|
{
|
|
kind: 'file-restore',
|
|
path: this.pathname,
|
|
version: this.version,
|
|
timestamp: new Date(this.endTs).toISOString(),
|
|
}
|
|
)
|
|
})
|
|
|
|
it('should return the created entity', function () {
|
|
expect(this.data).to.deep.equal(this.addedFile)
|
|
})
|
|
|
|
it('should look up ranges', function () {
|
|
expect(
|
|
this.RestoreManager.promises._getRangesFromHistory
|
|
).to.have.been.calledWith(
|
|
this.project_id,
|
|
this.version,
|
|
this.pathname
|
|
)
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('reverting a file or document with metadata', function () {
|
|
beforeEach(function () {
|
|
this.ProjectLocator.promises.findElementByPath = sinon.stub().rejects()
|
|
this.EditorController.promises.addDocWithRanges = sinon.stub()
|
|
this.RestoreManager.promises._getUpdatesFromHistory = sinon
|
|
.stub()
|
|
.resolves([
|
|
{ toV: this.version, meta: { end_ts: (this.endTs = new Date()) } },
|
|
])
|
|
|
|
this.EditorController.promises.upsertFile = sinon
|
|
.stub()
|
|
.resolves({ _id: 'mock-file-id', type: 'file' })
|
|
this.RestoreManager.promises._getRangesFromHistory = sinon
|
|
.stub()
|
|
.resolves({
|
|
changes: [],
|
|
comments: [],
|
|
})
|
|
this.EditorController.promises.addDocWithRanges = sinon
|
|
.stub()
|
|
.resolves((this.addedFile = { _id: 'mock-doc-id', type: 'doc' }))
|
|
|
|
this.DocstoreManager.promises.getAllRanges = sinon.stub().resolves([])
|
|
this.ChatApiHandler.promises.generateThreadData = sinon
|
|
.stub()
|
|
.resolves({})
|
|
this.ChatManager.promises.injectUserInfoIntoThreads = sinon
|
|
.stub()
|
|
.resolves({})
|
|
this.EditorRealTimeController.emitToRoom = sinon.stub()
|
|
})
|
|
|
|
describe('when reverting a linked file', function () {
|
|
beforeEach(async function () {
|
|
this.pathname = 'foo.png'
|
|
this.FileSystemImportManager.promises.importFile = sinon
|
|
.stub()
|
|
.resolves({ type: 'file' })
|
|
this.RestoreManager.promises._getMetadataFromHistory = sinon
|
|
.stub()
|
|
.resolves({ metadata: { provider: 'bar' } })
|
|
this.result = await this.RestoreManager.promises.revertFile(
|
|
this.user_id,
|
|
this.project_id,
|
|
this.version,
|
|
this.pathname
|
|
)
|
|
})
|
|
|
|
it('should revert it as a file', function () {
|
|
expect(this.result).to.deep.equal({
|
|
_id: 'mock-file-id',
|
|
type: 'file',
|
|
})
|
|
})
|
|
|
|
it('should upload to the project as a file', function () {
|
|
expect(
|
|
this.EditorController.promises.upsertFile
|
|
).to.have.been.calledWith(
|
|
this.project_id,
|
|
'mock-folder-id',
|
|
'foo.png',
|
|
this.fsPath,
|
|
{ provider: 'bar' },
|
|
{
|
|
kind: 'file-restore',
|
|
path: this.pathname,
|
|
version: this.version,
|
|
timestamp: new Date(this.endTs).toISOString(),
|
|
},
|
|
this.user_id
|
|
)
|
|
})
|
|
|
|
it('should not look up ranges', function () {
|
|
expect(this.RestoreManager.promises._getRangesFromHistory).to.not.have
|
|
.been.called
|
|
})
|
|
|
|
it('should not try to add a document', function () {
|
|
expect(this.EditorController.promises.addDocWithRanges).to.not.have
|
|
.been.called
|
|
})
|
|
})
|
|
|
|
describe('when reverting a linked document with provider', function () {
|
|
beforeEach(async function () {
|
|
this.pathname = 'foo.tex'
|
|
this.FileSystemImportManager.promises.importFile = sinon
|
|
.stub()
|
|
.resolves({ type: 'doc', lines: ['foo', 'bar', 'baz'] })
|
|
this.RestoreManager.promises._getMetadataFromHistory = sinon
|
|
.stub()
|
|
.resolves({ metadata: { provider: 'bar' } })
|
|
this.result = await this.RestoreManager.promises.revertFile(
|
|
this.user_id,
|
|
this.project_id,
|
|
this.version,
|
|
this.pathname
|
|
)
|
|
})
|
|
|
|
it('should revert it as a file', function () {
|
|
expect(this.result).to.deep.equal({
|
|
_id: 'mock-file-id',
|
|
type: 'file',
|
|
})
|
|
})
|
|
|
|
it('should upload to the project as a file', function () {
|
|
expect(
|
|
this.EditorController.promises.upsertFile
|
|
).to.have.been.calledWith(
|
|
this.project_id,
|
|
'mock-folder-id',
|
|
'foo.tex',
|
|
this.fsPath,
|
|
{ provider: 'bar' },
|
|
{
|
|
kind: 'file-restore',
|
|
path: this.pathname,
|
|
version: this.version,
|
|
timestamp: new Date(this.endTs).toISOString(),
|
|
},
|
|
this.user_id
|
|
)
|
|
})
|
|
|
|
it('should not look up ranges', function () {
|
|
expect(this.RestoreManager.promises._getRangesFromHistory).to.not.have
|
|
.been.called
|
|
})
|
|
|
|
it('should not try to add a document', function () {
|
|
expect(this.EditorController.promises.addDocWithRanges).to.not.have
|
|
.been.called
|
|
})
|
|
})
|
|
|
|
describe('when reverting a linked document with { main: true }', function () {
|
|
beforeEach(async function () {
|
|
this.pathname = 'foo.tex'
|
|
this.FileSystemImportManager.promises.importFile = sinon
|
|
.stub()
|
|
.resolves({ type: 'doc', lines: ['foo', 'bar', 'baz'] })
|
|
this.RestoreManager.promises._getMetadataFromHistory = sinon
|
|
.stub()
|
|
.resolves({ metadata: { main: true } })
|
|
this.result = await this.RestoreManager.promises.revertFile(
|
|
this.user_id,
|
|
this.project_id,
|
|
this.version,
|
|
this.pathname
|
|
)
|
|
})
|
|
|
|
it('should revert it as a document', function () {
|
|
expect(this.result).to.deep.equal({
|
|
_id: 'mock-doc-id',
|
|
type: 'doc',
|
|
})
|
|
})
|
|
|
|
it('should not upload to the project as a file', function () {
|
|
expect(this.EditorController.promises.upsertFile).to.not.have.been
|
|
.called
|
|
})
|
|
|
|
it('should look up ranges', function () {
|
|
expect(this.RestoreManager.promises._getRangesFromHistory).to.have
|
|
.been.called
|
|
})
|
|
|
|
it('should add the document', function () {
|
|
expect(
|
|
this.EditorController.promises.addDocWithRanges
|
|
).to.have.been.calledWith(
|
|
this.project_id,
|
|
this.folder_id,
|
|
'foo.tex',
|
|
['foo', 'bar', 'baz'],
|
|
{ changes: [], comments: [] }
|
|
)
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('when reverting a binary file', function () {
|
|
beforeEach(async function () {
|
|
this.pathname = 'foo.png'
|
|
this.FileSystemImportManager.promises.importFile = sinon
|
|
.stub()
|
|
.resolves({ type: 'file' })
|
|
this.EditorController.promises.upsertFile = sinon
|
|
.stub()
|
|
.resolves({ _id: 'mock-file-id', type: 'file' })
|
|
this.EditorController.promises.deleteEntity = sinon.stub().resolves()
|
|
this.RestoreManager.promises._getUpdatesFromHistory = sinon
|
|
.stub()
|
|
.resolves([{ toV: this.version, meta: { end_ts: Date.now() } }])
|
|
})
|
|
|
|
it('should return the created entity if file exists', async function () {
|
|
this.ProjectLocator.promises.findElementByPath = sinon
|
|
.stub()
|
|
.resolves({ type: 'file', element: { _id: 'existing-file-id' } })
|
|
|
|
const revertRes = await this.RestoreManager.promises.revertFile(
|
|
this.user_id,
|
|
this.project_id,
|
|
this.version,
|
|
this.pathname
|
|
)
|
|
|
|
expect(revertRes).to.deep.equal({ _id: 'mock-file-id', type: 'file' })
|
|
})
|
|
|
|
it('should return the created entity if file does not exists', async function () {
|
|
this.ProjectLocator.promises.findElementByPath = sinon.stub().rejects()
|
|
|
|
const revertRes = await this.RestoreManager.promises.revertFile(
|
|
this.user_id,
|
|
this.project_id,
|
|
this.version,
|
|
this.pathname
|
|
)
|
|
|
|
expect(revertRes).to.deep.equal({ _id: 'mock-file-id', type: 'file' })
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('revertProject', function () {
|
|
beforeEach(function () {
|
|
this.ProjectGetter.promises.getProject = sinon.stub()
|
|
this.ProjectGetter.promises.getProject
|
|
.withArgs(this.project_id)
|
|
.resolves({ overleaf: { history: { rangesSupportEnabled: true } } })
|
|
this.RestoreManager.promises.revertFile = sinon.stub().resolves()
|
|
this.RestoreManager.promises._getProjectPathsAtVersion = sinon
|
|
.stub()
|
|
.resolves([])
|
|
this.ProjectEntityHandler.promises.getAllEntities = sinon
|
|
.stub()
|
|
.resolves({ docs: [], files: [] })
|
|
this.EditorController.promises.deleteEntityWithPath = sinon
|
|
.stub()
|
|
.resolves()
|
|
this.RestoreManager.promises._getUpdatesFromHistory = sinon
|
|
.stub()
|
|
.resolves([
|
|
{ toV: this.version, meta: { end_ts: (this.end_ts = Date.now()) } },
|
|
])
|
|
})
|
|
|
|
describe('reverting a project without ranges support', function () {
|
|
beforeEach(function () {
|
|
this.ProjectGetter.promises.getProject = sinon.stub().resolves({
|
|
overleaf: { history: { rangesSupportEnabled: false } },
|
|
})
|
|
})
|
|
|
|
it('should throw an error', async function () {
|
|
await expect(
|
|
this.RestoreManager.promises.revertProject(
|
|
this.user_id,
|
|
this.project_id,
|
|
this.version
|
|
)
|
|
).to.eventually.be.rejectedWith('project does not have ranges support')
|
|
})
|
|
})
|
|
|
|
describe('for a project with overlap in current files and old files', function () {
|
|
beforeEach(async function () {
|
|
this.ProjectEntityHandler.promises.getAllEntities = sinon
|
|
.stub()
|
|
.resolves({
|
|
docs: [{ path: '/main.tex' }, { path: '/new-file.tex' }],
|
|
files: [{ path: '/figures/image.png' }],
|
|
})
|
|
this.RestoreManager.promises._getProjectPathsAtVersion = sinon
|
|
.stub()
|
|
.resolves(['main.tex', 'figures/image.png', 'since-deleted.tex'])
|
|
|
|
await this.RestoreManager.promises.revertProject(
|
|
this.user_id,
|
|
this.project_id,
|
|
this.version
|
|
)
|
|
this.origin = {
|
|
kind: 'project-restore',
|
|
version: this.version,
|
|
timestamp: new Date(this.end_ts).toISOString(),
|
|
}
|
|
})
|
|
|
|
it('should delete the old files', function () {
|
|
expect(
|
|
this.EditorController.promises.deleteEntityWithPath
|
|
).to.have.been.calledWith(
|
|
this.project_id,
|
|
'new-file.tex',
|
|
this.origin,
|
|
this.user_id
|
|
)
|
|
})
|
|
|
|
it('should not delete the current files', function () {
|
|
expect(
|
|
this.EditorController.promises.deleteEntityWithPath
|
|
).to.not.have.been.calledWith(
|
|
this.project_id,
|
|
'main.tex',
|
|
this.origin,
|
|
this.user_id
|
|
)
|
|
|
|
expect(
|
|
this.EditorController.promises.deleteEntityWithPath
|
|
).to.not.have.been.calledWith(
|
|
this.project_id,
|
|
'figures/image.png',
|
|
this.origin,
|
|
this.user_id
|
|
)
|
|
})
|
|
|
|
it('should revert the old files', function () {
|
|
expect(this.RestoreManager.promises.revertFile).to.have.been.calledWith(
|
|
this.user_id,
|
|
this.project_id,
|
|
this.version,
|
|
'main.tex'
|
|
)
|
|
|
|
expect(this.RestoreManager.promises.revertFile).to.have.been.calledWith(
|
|
this.user_id,
|
|
this.project_id,
|
|
this.version,
|
|
'figures/image.png'
|
|
)
|
|
|
|
expect(this.RestoreManager.promises.revertFile).to.have.been.calledWith(
|
|
this.user_id,
|
|
this.project_id,
|
|
this.version,
|
|
'since-deleted.tex'
|
|
)
|
|
})
|
|
|
|
it('should not revert the current files', function () {
|
|
expect(
|
|
this.RestoreManager.promises.revertFile
|
|
).to.not.have.been.calledWith(
|
|
this.user_id,
|
|
this.project_id,
|
|
this.version,
|
|
'new-file.tex'
|
|
)
|
|
})
|
|
})
|
|
})
|
|
})
|