mirror of
https://github.com/overleaf/overleaf.git
synced 2024-12-01 15:51:31 -05:00
7d7e5c9a8f
[project-history] Fix project history unit test GitOrigin-RevId: 4a8c5c2337ee8a06d46638ceba18d4a0f8742b42
639 lines
19 KiB
JavaScript
639 lines
19 KiB
JavaScript
/* 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)
|
|
this.ranges = {
|
|
comments: [],
|
|
trackedChanges: [
|
|
{
|
|
range: { pos: 4, length: 6 },
|
|
tracking: {
|
|
userId: 'user-1',
|
|
ts: '2024-01-01T00:00:00.000Z',
|
|
type: 'delete',
|
|
},
|
|
},
|
|
{
|
|
range: { pos: 35, length: 5 },
|
|
tracking: {
|
|
userId: 'user-1',
|
|
ts: '2024-01-01T00:00:00.000Z',
|
|
type: 'insert',
|
|
},
|
|
},
|
|
],
|
|
}
|
|
this.HistoryStoreManager.getChunkAtVersion.yields(null, {
|
|
chunk: {
|
|
history: {
|
|
snapshot: {
|
|
files: {
|
|
'main.tex': {
|
|
hash: '35c9bd86574d61dcadbce2fdd3d4a0684272c6ea',
|
|
stringLength: 41,
|
|
},
|
|
'file_with_ranges.tex': {
|
|
hash: '5d2781d78fa5a97b7bafa849fe933dfc9dc93eba',
|
|
rangesHash: '73061952d41ce54825e2fc1c36b4cf736d5fb62f',
|
|
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 with no tracked changes', function () {
|
|
beforeEach(function (done) {
|
|
this.HistoryStoreManager.getBlobStore.withArgs(this.historyId).returns({
|
|
getString: (this.getString = sinon.stub().resolves(
|
|
`\
|
|
Hello world
|
|
|
|
One two three
|
|
|
|
Four five six\
|
|
`.replace(/^\t/g, '')
|
|
)),
|
|
getObject: sinon.stub().rejects(),
|
|
})
|
|
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: sinon.stub().rejects(this.error),
|
|
getObject: sinon.stub().rejects(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 text file with tracked changes', function () {
|
|
beforeEach(function (done) {
|
|
this.HistoryStoreManager.getBlobStore.withArgs(this.historyId).returns({
|
|
getString: (this.getString = sinon
|
|
.stub()
|
|
.resolves('the quick brown fox jumps over the lazy dog')),
|
|
getObject: (this.getObject = sinon.stub().resolves(this.ranges)),
|
|
})
|
|
this.SnapshotManager.getFileSnapshotStream(
|
|
this.projectId,
|
|
5,
|
|
'file_with_ranges.tex',
|
|
(error, stream) => {
|
|
this.stream = stream
|
|
done(error)
|
|
}
|
|
)
|
|
})
|
|
|
|
it('should get the overleaf id', function () {
|
|
this.WebApiManager.getHistoryId
|
|
.calledWith(this.projectId)
|
|
.should.equal(true)
|
|
})
|
|
|
|
it('should get the chunk', function () {
|
|
this.HistoryStoreManager.getChunkAtVersion
|
|
.calledWith(this.projectId, this.historyId, 5)
|
|
.should.equal(true)
|
|
})
|
|
|
|
it('should get the blob of the starting snapshot', function () {
|
|
this.getString
|
|
.calledWith('5d2781d78fa5a97b7bafa849fe933dfc9dc93eba')
|
|
.should.equal(true)
|
|
})
|
|
|
|
it('should get the blob of the ranges', function () {
|
|
this.getObject
|
|
.calledWith('73061952d41ce54825e2fc1c36b4cf736d5fb62f')
|
|
.should.equal(true)
|
|
})
|
|
|
|
it('should return a string stream with the text content without the tracked deletes', function () {
|
|
expect(this.stream.read().toString()).to.equal(
|
|
'the brown fox jumps over the lazy dog'
|
|
)
|
|
})
|
|
})
|
|
|
|
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)
|
|
this.ranges = {
|
|
comments: [],
|
|
trackedChanges: [
|
|
{
|
|
range: { pos: 5, length: 6 },
|
|
tracking: {
|
|
userId: 'user-1',
|
|
ts: '2024-01-01T00:00:00.000Z',
|
|
type: 'delete',
|
|
},
|
|
},
|
|
{
|
|
range: { pos: 12, length: 5 },
|
|
tracking: {
|
|
userId: 'user-1',
|
|
ts: '2024-01-01T00:00:00.000Z',
|
|
type: 'insert',
|
|
},
|
|
},
|
|
],
|
|
}
|
|
return this.HistoryStoreManager.getChunkAtVersion.yields(null, {
|
|
chunk: (this.chunk = {
|
|
history: {
|
|
snapshot: {
|
|
files: {
|
|
'main.tex': {
|
|
hash: '35c9bd86574d61dcadbce2fdd3d4a0684272c6ea',
|
|
stringLength: 41,
|
|
},
|
|
'unchanged.tex': {
|
|
hash: '35c9bd86574d61dcadbce2fdd3d4a0684272c6ea',
|
|
stringLength: 41,
|
|
},
|
|
'with_ranges_unchanged.tex': {
|
|
hash: '35c9bd86574d61dcadbce2fdd3d4a0684272c6ea',
|
|
rangesHash: '2e59fe3dbd5310703f89236d589d0b35db169cdf',
|
|
stringLength: 41,
|
|
},
|
|
'with_ranges_changed.tex': {
|
|
hash: '35c9bd86574d61dcadbce2fdd3d4a0684272c6ea',
|
|
rangesHash: '2e59fe3dbd5310703f89236d589d0b35db169cdf',
|
|
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],
|
|
},
|
|
{
|
|
operations: [
|
|
{
|
|
pathname: 'with_ranges_changed.tex',
|
|
textOperation: [41, '\n\nSeven eight'],
|
|
},
|
|
],
|
|
timestamp: '2017-12-04T10:29:25.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: (this.getString = sinon.stub().resolves(
|
|
`\
|
|
Hello world
|
|
|
|
One two three
|
|
|
|
Four five six\
|
|
`
|
|
)),
|
|
getObject: (this.getObject = sinon.stub().resolves(this.ranges)),
|
|
})
|
|
this.SnapshotManager.getProjectSnapshot(
|
|
this.projectId,
|
|
6,
|
|
(error, data) => {
|
|
this.data = data
|
|
done(error)
|
|
}
|
|
)
|
|
})
|
|
|
|
it('should get the overleaf id', function () {
|
|
this.WebApiManager.getHistoryId
|
|
.calledWith(this.projectId)
|
|
.should.equal(true)
|
|
})
|
|
|
|
it('should get the chunk', function () {
|
|
this.HistoryStoreManager.getChunkAtVersion
|
|
.calledWith(this.projectId, this.historyId, 6)
|
|
.should.equal(true)
|
|
})
|
|
|
|
it('should get the ranges for the file with tracked changes', function () {
|
|
this.getObject.calledWith('2e59fe3dbd5310703f89236d589d0b35db169cdf')
|
|
})
|
|
|
|
it('should produce the snapshot file data', function () {
|
|
expect(this.data).to.deep.equal({
|
|
files: {
|
|
'main.tex': {
|
|
// files with operations in the chunk should return content only
|
|
data: {
|
|
content:
|
|
'Hello world\n\nOne two three\n\nFour five six\n\nSeven eight nine',
|
|
},
|
|
},
|
|
'unchanged.tex': {
|
|
// unchanged files in the chunk should return hash only
|
|
data: {
|
|
hash: '35c9bd86574d61dcadbce2fdd3d4a0684272c6ea',
|
|
},
|
|
},
|
|
'with_ranges_changed.tex': {
|
|
// files in the chunk with tracked changes should return content
|
|
// without the tracked deletes
|
|
data: {
|
|
content:
|
|
'Hello\n\nOne two three\n\nFour five six\n\nSeven eight',
|
|
},
|
|
},
|
|
'with_ranges_unchanged.tex': {
|
|
// files in the chunk with tracked changes should return content
|
|
// without the tracked deletes, even if they are unchanged
|
|
data: {
|
|
content: 'Hello\n\nOne two three\n\nFour five six',
|
|
},
|
|
},
|
|
'binary.png': {
|
|
// binary files in the chunk should return hash only
|
|
data: {
|
|
hash: 'c6654ea913979e13e22022653d284444f284a172',
|
|
},
|
|
},
|
|
},
|
|
projectId: 'project-id-123',
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('on blob store error', function () {
|
|
beforeEach(function () {
|
|
this.error = new Error('ESOCKETTIMEDOUT')
|
|
this.HistoryStoreManager.getBlobStore.withArgs(this.historyId).returns({
|
|
getString: sinon.stub().rejects(this.error),
|
|
getObject: sinon.stub().resolves(),
|
|
})
|
|
})
|
|
|
|
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: (this.getString = sinon.stub().resolves(
|
|
`\
|
|
Hello world
|
|
|
|
One two three
|
|
|
|
Four five six\
|
|
`.replace(/^\t/g, '')
|
|
)),
|
|
getObject: sinon.stub().rejects(),
|
|
})
|
|
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')
|
|
})
|
|
})
|
|
})
|
|
})
|