overleaf/services/project-history/test/unit/js/SnapshotManager/SnapshotManagerTests.js

502 lines
15 KiB
JavaScript
Raw Normal View History

/* eslint-disable
no-return-assign,
no-undef,
no-unused-vars,
*/
// TODO: This file was created by bulk-decaffeinate.
// Fix any style issues and re-enable lint.
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
import sinon from 'sinon'
import { expect } from 'chai'
import { strict as esmock } from 'esmock'
import Core from 'overleaf-editor-core'
import BPromise from 'bluebird'
import * as Errors from '../../../../app/js/Errors.js'
const MODULE_PATH = '../../../../app/js/SnapshotManager.js'
describe('SnapshotManager', function () {
beforeEach(async function () {
this.HistoryStoreManager = {
getBlobStore: sinon.stub(),
getChunkAtVersion: sinon.stub(),
getMostRecentChunk: sinon.stub(),
getProjectBlobStream: sinon.stub(),
}
this.WebApiManager = {
getHistoryId: sinon.stub(),
}
this.SnapshotManager = await esmock(MODULE_PATH, {
'overleaf-editor-core': Core,
'../../../../app/js/HistoryStoreManager.js': this.HistoryStoreManager,
'../../../../app/js/WebApiManager.js': this.WebApiManager,
'../../../../app/js/Errors.js': Errors,
})
this.projectId = 'project-id-123'
this.historyId = 'ol-project-id-123'
return (this.callback = sinon.stub())
})
describe('getFileSnapshotStream', function () {
beforeEach(function () {
this.WebApiManager.getHistoryId.yields(null, this.historyId)
return this.HistoryStoreManager.getChunkAtVersion.yields(null, {
chunk: {
history: {
snapshot: {
files: {
'main.tex': {
hash: '35c9bd86574d61dcadbce2fdd3d4a0684272c6ea',
stringLength: 41,
},
'binary.png': {
hash: 'c6654ea913979e13e22022653d284444f284a172',
byteLength: 41,
},
},
},
changes: [
{
operations: [
{
pathname: 'main.tex',
textOperation: [41, '\n\nSeven eight'],
},
],
timestamp: '2017-12-04T10:29:17.786Z',
authors: [31],
},
{
operations: [
{
pathname: 'main.tex',
textOperation: [54, ' nine'],
},
],
timestamp: '2017-12-04T10:29:22.905Z',
authors: [31],
},
],
},
startVersion: 3,
authors: [
{
id: 31,
email: 'james.allen@overleaf.com',
name: 'James',
},
],
},
})
})
describe('of a text file', function () {
beforeEach(function (done) {
this.HistoryStoreManager.getBlobStore.withArgs(this.historyId).returns({
getString: BPromise.promisify(
(this.getString = sinon.stub().yields(
null,
`\
Hello world
One two three
Four five six\
`.replace(/^\t/g, '')
))
),
})
this.SnapshotManager.getFileSnapshotStream(
this.projectId,
5,
'main.tex',
(error, stream) => {
this.stream = stream
return done(error)
}
)
})
it('should get the overleaf id', function () {
return this.WebApiManager.getHistoryId
.calledWith(this.projectId)
.should.equal(true)
})
it('should get the chunk', function () {
return this.HistoryStoreManager.getChunkAtVersion
.calledWith(this.projectId, this.historyId, 5)
.should.equal(true)
})
it('should get the blob of the starting snapshot', function () {
return this.getString
.calledWith('35c9bd86574d61dcadbce2fdd3d4a0684272c6ea')
.should.equal(true)
})
it('should return a string stream with the text content', function () {
return expect(this.stream.read().toString()).to.equal(
`\
Hello world
One two three
Four five six
Seven eight nine\
`.replace(/^\t/g, '')
)
})
describe('on blob store error', function () {
beforeEach(function () {
this.error = new Error('ESOCKETTIMEDOUT')
this.HistoryStoreManager.getBlobStore
.withArgs(this.historyId)
.returns({
getString: BPromise.promisify(sinon.stub().throws(this.error)),
})
})
it('should call back with error', function (done) {
this.SnapshotManager.getFileSnapshotStream(
this.projectId,
5,
'main.tex',
error => {
expect(error).to.exist
expect(error.name).to.equal(this.error.name)
done()
}
)
})
})
})
describe('of a binary file', function () {
beforeEach(function (done) {
this.HistoryStoreManager.getProjectBlobStream
.withArgs(this.historyId)
.yields(null, (this.stream = 'mock-stream'))
return this.SnapshotManager.getFileSnapshotStream(
this.projectId,
5,
'binary.png',
(error, returnedStream) => {
this.returnedStream = returnedStream
return done(error)
}
)
})
it('should get the overleaf id', function () {
return this.WebApiManager.getHistoryId
.calledWith(this.projectId)
.should.equal(true)
})
it('should get the chunk', function () {
return this.HistoryStoreManager.getChunkAtVersion
.calledWith(this.projectId, this.historyId, 5)
.should.equal(true)
})
it('should get the blob of the starting snapshot', function () {
return this.HistoryStoreManager.getProjectBlobStream
.calledWith(
this.historyId,
'c6654ea913979e13e22022653d284444f284a172'
)
.should.equal(true)
})
return it('should return a stream with the blob content', function () {
return expect(this.returnedStream).to.equal(this.stream)
})
})
return describe("when the file doesn't exist", function () {
beforeEach(function (done) {
return this.SnapshotManager.getFileSnapshotStream(
this.projectId,
5,
'not-here.png',
(error, returnedStream) => {
this.error = error
this.returnedStream = returnedStream
return done()
}
)
})
return it('should return a NotFoundError', function () {
expect(this.error).to.exist
expect(this.error.message).to.equal('not-here.png not found')
return expect(this.error).to.be.an.instanceof(Errors.NotFoundError)
})
})
})
describe('getProjectSnapshot', function () {
beforeEach(function () {
this.WebApiManager.getHistoryId.yields(null, this.historyId)
return this.HistoryStoreManager.getChunkAtVersion.yields(null, {
chunk: (this.chunk = {
history: {
snapshot: {
files: {
'main.tex': {
hash: '35c9bd86574d61dcadbce2fdd3d4a0684272c6ea',
stringLength: 41,
},
'unchanged.tex': {
hash: '35c9bd86574d61dcadbce2fdd3d4a0684272c6ea',
stringLength: 41,
},
'binary.png': {
hash: 'c6654ea913979e13e22022653d284444f284a172',
byteLength: 41,
},
},
},
changes: [
{
operations: [
{
pathname: 'main.tex',
textOperation: [41, '\n\nSeven eight'],
},
],
timestamp: '2017-12-04T10:29:17.786Z',
authors: [31],
},
{
operations: [
{
pathname: 'main.tex',
textOperation: [54, ' nine'],
},
],
timestamp: '2017-12-04T10:29:22.905Z',
authors: [31],
},
],
},
startVersion: 3,
authors: [
{
id: 31,
email: 'james.allen@overleaf.com',
name: 'James',
},
],
}),
})
})
describe('of project', function () {
beforeEach(function (done) {
this.HistoryStoreManager.getBlobStore.withArgs(this.historyId).returns({
getString: BPromise.promisify(
(this.getString = sinon.stub().yields(
null,
`\
Hello world
One two three
Four five six\
`.replace(/^\t/g, '')
))
),
})
this.SnapshotManager.getProjectSnapshot(
this.projectId,
5,
(error, data) => {
this.data = data
done(error)
}
)
})
it('should get the overleaf id', function () {
return this.WebApiManager.getHistoryId
.calledWith(this.projectId)
.should.equal(true)
})
it('should get the chunk', function () {
return this.HistoryStoreManager.getChunkAtVersion
.calledWith(this.projectId, this.historyId, 5)
.should.equal(true)
})
return it('should produce the snapshot file data', function () {
expect(this.data).to.have.all.keys(['files', 'projectId'])
expect(this.data.projectId).to.equal('project-id-123')
expect(this.data.files['main.tex']).to.exist
expect(this.data.files['unchanged.tex']).to.exist
expect(this.data.files['binary.png']).to.exist
// files with operations in the chunk should return content only
expect(this.data.files['main.tex'].data.content).to.equal(
'Hello world\n\nOne two three\n\nFour five six\n\nSeven eight nine'
)
expect(this.data.files['main.tex'].data.hash).to.not.exist
// unchanged files in the chunk should return hash only
expect(this.data.files['unchanged.tex'].data.hash).to.equal(
'35c9bd86574d61dcadbce2fdd3d4a0684272c6ea'
)
expect(this.data.files['unchanged.tex'].data.content).to.not.exist
return expect(this.data.files['binary.png'].data.hash).to.equal(
'c6654ea913979e13e22022653d284444f284a172'
)
})
})
describe('on blob store error', function () {
beforeEach(function () {
this.error = new Error('ESOCKETTIMEDOUT')
this.HistoryStoreManager.getBlobStore.withArgs(this.historyId).returns({
getString: BPromise.promisify(sinon.stub().yields(this.error)),
})
})
it('should call back with error', function (done) {
this.SnapshotManager.getProjectSnapshot(this.projectId, 5, error => {
expect(error).to.exist
expect(error.message).to.equal(this.error.message)
done()
})
})
})
})
return describe('getLatestSnapshot', function () {
describe('for a project', function () {
beforeEach(function (done) {
this.HistoryStoreManager.getMostRecentChunk.yields(null, {
chunk: (this.chunk = {
history: {
snapshot: {
files: {
'main.tex': {
hash: '35c9bd86574d61dcadbce2fdd3d4a0684272c6ea',
stringLength: 41,
},
'binary.png': {
hash: 'c6654ea913979e13e22022653d284444f284a172',
byteLength: 41,
},
},
},
changes: [
{
operations: [
{
pathname: 'main.tex',
textOperation: [41, '\n\nSeven eight'],
},
],
timestamp: '2017-12-04T10:29:17.786Z',
authors: [31],
},
{
operations: [
{
pathname: 'main.tex',
textOperation: [54, ' nine'],
},
],
timestamp: '2017-12-04T10:29:22.905Z',
authors: [31],
},
],
},
startVersion: 3,
authors: [
{
id: 31,
email: 'james.allen@overleaf.com',
name: 'James',
},
],
}),
})
this.HistoryStoreManager.getBlobStore.withArgs(this.historyId).returns({
getString: BPromise.promisify(
(this.getString = sinon.stub().yields(
null,
`\
Hello world
One two three
Four five six\
`.replace(/^\t/g, '')
))
),
})
this.SnapshotManager.getLatestSnapshot(
this.projectId,
this.historyId,
(error, data) => {
this.data = data
done(error)
}
)
})
it('should get the chunk', function () {
return this.HistoryStoreManager.getMostRecentChunk
.calledWith(this.projectId, this.historyId)
.should.equal(true)
})
return it('should produce the snapshot file data', function () {
expect(this.data).to.have.all.keys(['main.tex', 'binary.png'])
expect(this.data['main.tex']).to.exist
expect(this.data['binary.png']).to.exist
expect(this.data['main.tex'].getStringLength()).to.equal(59)
expect(this.data['binary.png'].getByteLength()).to.equal(41)
return expect(this.data['binary.png'].getHash()).to.equal(
'c6654ea913979e13e22022653d284444f284a172'
)
})
})
return describe('when the chunk is empty', function () {
beforeEach(function (done) {
this.HistoryStoreManager.getMostRecentChunk.yields(null)
return this.SnapshotManager.getLatestSnapshot(
this.projectId,
this.historyId,
(error, data) => {
this.error = error
this.data = data
return done()
}
)
})
it('should get the chunk', function () {
return this.HistoryStoreManager.getMostRecentChunk
.calledWith(this.projectId, this.historyId)
.should.equal(true)
})
return it('return an error', function () {
expect(this.error).to.exist
return expect(this.error.message).to.equal('undefined chunk')
})
})
})
})