Merge pull request #13327 from overleaf/msm-web-track-changes-cleanup
[web] Cleanup track-changes from web GitOrigin-RevId: 8cef709ec5e91e4ffe8cd06826038ed84f36ef67
This commit is contained in:
@@ -1,394 +0,0 @@
|
||||
const { ObjectId } = require('mongodb')
|
||||
const {
|
||||
db,
|
||||
READ_PREFERENCE_SECONDARY,
|
||||
} = require('../../../../app/src/infrastructure/mongodb')
|
||||
const Settings = require('@overleaf/settings')
|
||||
|
||||
const ProjectHistoryHandler = require('../../../../app/src/Features/Project/ProjectHistoryHandler')
|
||||
const HistoryManager = require('../../../../app/src/Features/History/HistoryManager')
|
||||
const ProjectHistoryController = require('./ProjectHistoryController')
|
||||
const ProjectEntityHandler = require('../../../../app/src/Features/Project/ProjectEntityHandler')
|
||||
const ProjectEntityUpdateHandler = require('../../../../app/src/Features/Project/ProjectEntityUpdateHandler')
|
||||
const DocumentUpdaterHandler = require('../../../../app/src/Features/DocumentUpdater/DocumentUpdaterHandler')
|
||||
|
||||
// Timestamp of when 'Enable history for SL in background' release
|
||||
const ID_WHEN_FULL_PROJECT_HISTORY_ENABLED =
|
||||
Settings.apis.project_history?.idWhenFullProjectHistoryEnabled // was '5a8d8a370000000000000000'
|
||||
const DATETIME_WHEN_FULL_PROJECT_HISTORY_ENABLED =
|
||||
ID_WHEN_FULL_PROJECT_HISTORY_ENABLED
|
||||
? new ObjectId(ID_WHEN_FULL_PROJECT_HISTORY_ENABLED).getTimestamp()
|
||||
: null
|
||||
|
||||
async function countProjects(query = {}) {
|
||||
const count = await db.projects.countDocuments(query)
|
||||
return count
|
||||
}
|
||||
|
||||
async function countDocHistory(query = {}) {
|
||||
const count = await db.docHistory.countDocuments(query)
|
||||
return count
|
||||
}
|
||||
|
||||
async function findProjects(query = {}, projection = {}) {
|
||||
const projects = await db.projects.find(query).project(projection).toArray()
|
||||
return projects
|
||||
}
|
||||
|
||||
async function determineProjectHistoryType(project) {
|
||||
if (project.overleaf && project.overleaf.history) {
|
||||
if (project.overleaf.history.upgradeFailed) {
|
||||
return 'UpgradeFailed'
|
||||
}
|
||||
if (project.overleaf.history.conversionFailed) {
|
||||
return 'ConversionFailed'
|
||||
}
|
||||
}
|
||||
if (
|
||||
project.overleaf &&
|
||||
project.overleaf.history &&
|
||||
project.overleaf.history.id
|
||||
) {
|
||||
if (project.overleaf.history.display) {
|
||||
// v2: full project history, do nothing
|
||||
return 'V2'
|
||||
} else {
|
||||
if (projectCreatedAfterFullProjectHistoryEnabled(project)) {
|
||||
// IF project initialised after full project history enabled for all projects
|
||||
// THEN project history should contain all information we need, without intervention
|
||||
return 'V1WithoutConversion'
|
||||
} else {
|
||||
// ELSE SL history may predate full project history
|
||||
// THEN delete full project history and convert their SL history to full project history
|
||||
// --
|
||||
// TODO: how to verify this, can get rough start date of SL history, but not full project history
|
||||
const preserveHistory = await shouldPreserveHistory(project)
|
||||
const anyDocHistory = await anyDocHistoryExists(project)
|
||||
const anyDocHistoryIndex = await anyDocHistoryIndexExists(project)
|
||||
if (preserveHistory) {
|
||||
if (anyDocHistory || anyDocHistoryIndex) {
|
||||
// if SL history exists that we need to preserve, then we must convert
|
||||
return 'V1WithConversion'
|
||||
} else {
|
||||
// otherwise just upgrade without conversion
|
||||
return 'V1WithoutConversion'
|
||||
}
|
||||
} else {
|
||||
// if preserveHistory false, then max 7 days of SL history
|
||||
// but v1 already record to both histories, so safe to upgrade
|
||||
return 'V1WithoutConversion'
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const preserveHistory = await shouldPreserveHistory(project)
|
||||
const anyDocHistory = await anyDocHistoryExists(project)
|
||||
const anyDocHistoryIndex = await anyDocHistoryIndexExists(project)
|
||||
if (anyDocHistory || anyDocHistoryIndex) {
|
||||
// IF there is SL history ->
|
||||
if (preserveHistory) {
|
||||
// that needs to be preserved:
|
||||
// THEN initialise full project history and convert SL history to full project history
|
||||
return 'NoneWithConversion'
|
||||
} else {
|
||||
return 'NoneWithTemporaryHistory'
|
||||
}
|
||||
} else {
|
||||
// ELSE there is not any SL history ->
|
||||
// THEN initialise full project history and sync with current content
|
||||
return 'NoneWithoutConversion'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function upgradeProject(project, options) {
|
||||
const historyType = await determineProjectHistoryType(project)
|
||||
if (historyType === 'V2') {
|
||||
return { historyType, upgraded: true }
|
||||
}
|
||||
const upgradeFn = getUpgradeFunctionForType(historyType)
|
||||
if (!upgradeFn) {
|
||||
return { error: 'unsupported history type' }
|
||||
}
|
||||
if (options.forceClean) {
|
||||
try {
|
||||
const projectId = project._id
|
||||
// delete any existing history stored in the mongo backend
|
||||
await HistoryManager.promises.deleteProject(projectId, projectId)
|
||||
// unset overleaf.history.id to prevent the migration script from failing on checks
|
||||
await db.projects.updateOne(
|
||||
{ _id: projectId },
|
||||
{ $unset: { 'overleaf.history.id': '' } }
|
||||
)
|
||||
} catch (err) {
|
||||
// failed to delete existing history, but we can try to continue
|
||||
}
|
||||
}
|
||||
const result = await upgradeFn(project, options)
|
||||
result.historyType = historyType
|
||||
return result
|
||||
}
|
||||
|
||||
// Do upgrades/conversion:
|
||||
|
||||
function getUpgradeFunctionForType(historyType) {
|
||||
return UpgradeFunctionMapping[historyType]
|
||||
}
|
||||
|
||||
const UpgradeFunctionMapping = {
|
||||
NoneWithoutConversion: doUpgradeForNoneWithoutConversion,
|
||||
UpgradeFailed: doUpgradeForNoneWithoutConversion,
|
||||
ConversionFailed: doUpgradeForNoneWithConversion,
|
||||
V1WithoutConversion: doUpgradeForV1WithoutConversion,
|
||||
V1WithConversion: doUpgradeForV1WithConversion,
|
||||
NoneWithConversion: doUpgradeForNoneWithConversion,
|
||||
NoneWithTemporaryHistory: doUpgradeForNoneWithConversion,
|
||||
}
|
||||
|
||||
async function doUpgradeForV1WithoutConversion(project) {
|
||||
await db.projects.updateOne(
|
||||
{ _id: project._id },
|
||||
{
|
||||
$set: {
|
||||
'overleaf.history.display': true,
|
||||
'overleaf.history.upgradedAt': new Date(),
|
||||
'overleaf.history.upgradeReason': `v1-without-sl-history`,
|
||||
},
|
||||
}
|
||||
)
|
||||
return { upgraded: true }
|
||||
}
|
||||
|
||||
async function doUpgradeForV1WithConversion(project) {
|
||||
const result = {}
|
||||
const projectId = project._id
|
||||
// migrateProjectHistory expects project id as a string
|
||||
const projectIdString = project._id.toString()
|
||||
try {
|
||||
// We treat these essentially as None projects, the V1 history is irrelevant,
|
||||
// so we will delete it, and do a conversion as if we're a None project
|
||||
await ProjectHistoryController.deleteProjectHistory(projectIdString)
|
||||
await ProjectHistoryController.migrateProjectHistory(projectIdString)
|
||||
} catch (err) {
|
||||
// if migrateProjectHistory fails, it cleans up by deleting
|
||||
// the history and unsetting the history id
|
||||
// therefore a failed project will still look like a 'None with conversion' project
|
||||
result.error = err
|
||||
await db.projects.updateOne(
|
||||
{ _id: projectId },
|
||||
{
|
||||
$set: {
|
||||
'overleaf.history.conversionFailed': true,
|
||||
},
|
||||
}
|
||||
)
|
||||
return result
|
||||
}
|
||||
await db.projects.updateOne(
|
||||
{ _id: projectId },
|
||||
{
|
||||
$set: {
|
||||
'overleaf.history.upgradeReason': `v1-with-conversion`,
|
||||
},
|
||||
$unset: {
|
||||
'overleaf.history.upgradeFailed': true,
|
||||
'overleaf.history.conversionFailed': true,
|
||||
},
|
||||
}
|
||||
)
|
||||
result.upgraded = true
|
||||
return result
|
||||
}
|
||||
|
||||
async function doUpgradeForNoneWithoutConversion(project) {
|
||||
const result = {}
|
||||
const projectId = project._id
|
||||
try {
|
||||
// Logic originally from ProjectHistoryHandler.ensureHistoryExistsForProject
|
||||
// However sends a force resync project to project history instead
|
||||
// of a resync request to doc-updater
|
||||
let historyId = await ProjectHistoryHandler.promises.getHistoryId(projectId)
|
||||
if (historyId == null) {
|
||||
historyId = await HistoryManager.promises.initializeProject(projectId)
|
||||
if (historyId != null) {
|
||||
await ProjectHistoryHandler.promises.setHistoryId(projectId, historyId)
|
||||
}
|
||||
}
|
||||
// tell document updater to clear the docs, they will be reloaded with any new history id
|
||||
await DocumentUpdaterHandler.promises.flushProjectToMongoAndDelete(
|
||||
projectId
|
||||
)
|
||||
// now resync the project
|
||||
await HistoryManager.promises.resyncProject(projectId, {
|
||||
force: true,
|
||||
origin: { kind: 'history-migration' },
|
||||
})
|
||||
await HistoryManager.promises.flushProject(projectId)
|
||||
} catch (err) {
|
||||
result.error = err
|
||||
await db.projects.updateOne(
|
||||
{ _id: project._id },
|
||||
{
|
||||
$set: {
|
||||
'overleaf.history.upgradeFailed': true,
|
||||
},
|
||||
}
|
||||
)
|
||||
return result
|
||||
}
|
||||
await db.projects.updateOne(
|
||||
{ _id: project._id },
|
||||
{
|
||||
$set: {
|
||||
'overleaf.history.display': true,
|
||||
'overleaf.history.upgradedAt': new Date(),
|
||||
'overleaf.history.upgradeReason': `none-without-conversion`,
|
||||
},
|
||||
}
|
||||
)
|
||||
result.upgraded = true
|
||||
return result
|
||||
}
|
||||
|
||||
async function doUpgradeForNoneWithConversion(project, options = {}) {
|
||||
const result = {}
|
||||
const projectId = project._id
|
||||
// migrateProjectHistory expects project id as a string
|
||||
const projectIdString = project._id.toString()
|
||||
try {
|
||||
if (options.convertLargeDocsToFile) {
|
||||
result.convertedDocCount = await convertLargeDocsToFile(
|
||||
projectId,
|
||||
options.userId
|
||||
)
|
||||
}
|
||||
await ProjectHistoryController.migrateProjectHistory(
|
||||
projectIdString,
|
||||
options.migrationOptions
|
||||
)
|
||||
} catch (err) {
|
||||
// if migrateProjectHistory fails, it cleans up by deleting
|
||||
// the history and unsetting the history id
|
||||
// therefore a failed project will still look like a 'None with conversion' project
|
||||
result.error = err
|
||||
// We set a failed flag so future runs of the script don't automatically retry
|
||||
await db.projects.updateOne(
|
||||
{ _id: projectId },
|
||||
{
|
||||
$set: {
|
||||
'overleaf.history.conversionFailed': true,
|
||||
},
|
||||
}
|
||||
)
|
||||
return result
|
||||
}
|
||||
await db.projects.updateOne(
|
||||
{ _id: projectId },
|
||||
{
|
||||
$set: {
|
||||
'overleaf.history.upgradeReason':
|
||||
`none-with-conversion` + options.reason ? `/${options.reason}` : ``,
|
||||
},
|
||||
$unset: {
|
||||
'overleaf.history.upgradeFailed': true,
|
||||
'overleaf.history.conversionFailed': true,
|
||||
},
|
||||
}
|
||||
)
|
||||
result.upgraded = true
|
||||
return result
|
||||
}
|
||||
|
||||
// Util
|
||||
|
||||
function projectCreatedAfterFullProjectHistoryEnabled(project) {
|
||||
if (DATETIME_WHEN_FULL_PROJECT_HISTORY_ENABLED == null) {
|
||||
return false
|
||||
} else {
|
||||
return (
|
||||
project._id.getTimestamp() >= DATETIME_WHEN_FULL_PROJECT_HISTORY_ENABLED
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
async function shouldPreserveHistory(project) {
|
||||
return await db.projectHistoryMetaData.findOne(
|
||||
{
|
||||
$and: [
|
||||
{ project_id: { $eq: project._id } },
|
||||
{ preserveHistory: { $eq: true } },
|
||||
],
|
||||
},
|
||||
{ readPreference: READ_PREFERENCE_SECONDARY }
|
||||
)
|
||||
}
|
||||
|
||||
async function anyDocHistoryExists(project) {
|
||||
return await db.docHistory.findOne(
|
||||
{ project_id: { $eq: project._id } },
|
||||
{
|
||||
projection: { _id: 1 },
|
||||
readPreference: READ_PREFERENCE_SECONDARY,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
async function anyDocHistoryIndexExists(project) {
|
||||
return await db.docHistoryIndex.findOne(
|
||||
{ project_id: { $eq: project._id } },
|
||||
{
|
||||
projection: { _id: 1 },
|
||||
readPreference: READ_PREFERENCE_SECONDARY,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
async function convertLargeDocsToFile(projectId, userId) {
|
||||
const docs = await ProjectEntityHandler.promises.getAllDocs(projectId)
|
||||
let convertedDocCount = 0
|
||||
for (const doc of Object.values(docs)) {
|
||||
const sizeBound = JSON.stringify(doc.lines)
|
||||
if (docIsTooLarge(sizeBound, doc.lines, Settings.max_doc_length)) {
|
||||
await ProjectEntityUpdateHandler.promises.convertDocToFile(
|
||||
projectId,
|
||||
doc._id,
|
||||
userId,
|
||||
null
|
||||
)
|
||||
convertedDocCount++
|
||||
}
|
||||
}
|
||||
return convertedDocCount
|
||||
}
|
||||
|
||||
// check whether the total size of the document in characters exceeds the
|
||||
// maxDocLength.
|
||||
//
|
||||
// Copied from document-updater:
|
||||
// https://github.com/overleaf/internal/blob/74adfbebda5f3c2c37d9937f0db5c4106ecde492/services/document-updater/app/js/Limits.js#L18
|
||||
function docIsTooLarge(estimatedSize, lines, maxDocLength) {
|
||||
if (estimatedSize <= maxDocLength) {
|
||||
return false // definitely under the limit, no need to calculate the total size
|
||||
}
|
||||
// calculate the total size, bailing out early if the size limit is reached
|
||||
let size = 0
|
||||
for (const line of lines) {
|
||||
size += line.length + 1 // include the newline
|
||||
if (size > maxDocLength) return true
|
||||
}
|
||||
// since we didn't hit the limit in the loop, the document is within the allowed length
|
||||
return false
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
countProjects,
|
||||
countDocHistory,
|
||||
findProjects,
|
||||
determineProjectHistoryType,
|
||||
getUpgradeFunctionForType,
|
||||
upgradeProject,
|
||||
convertLargeDocsToFile,
|
||||
anyDocHistoryExists,
|
||||
anyDocHistoryIndexExists,
|
||||
doUpgradeForNoneWithConversion,
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1 +0,0 @@
|
||||
module.exports = {}
|
||||
-346
@@ -1,346 +0,0 @@
|
||||
const sinon = require('sinon')
|
||||
const nock = require('nock')
|
||||
const { expect } = require('chai')
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const SandboxedModule = require('sandboxed-module')
|
||||
const { ObjectId } = require('mongodb')
|
||||
const unzipper = require('unzipper')
|
||||
|
||||
const modulePath = '../../../app/src/ProjectHistoryController'
|
||||
|
||||
describe('ProjectHistoryController', function () {
|
||||
const projectId = ObjectId('611bd20c5d76a3c1bd0c7c13')
|
||||
const deletedFileId = ObjectId('60f6e92c6c14d84fb7a71ae1')
|
||||
const historyId = 123
|
||||
|
||||
let clock
|
||||
const now = new Date(Date.UTC(2021, 1, 1, 0, 0)).getTime()
|
||||
|
||||
before(async function () {
|
||||
clock = sinon.useFakeTimers({
|
||||
now,
|
||||
shouldAdvanceTime: true,
|
||||
})
|
||||
})
|
||||
|
||||
after(function () {
|
||||
// clock.runAll()
|
||||
clock.restore()
|
||||
})
|
||||
|
||||
beforeEach(function () {
|
||||
this.db = {
|
||||
users: {
|
||||
countDocuments: sinon.stub().yields(),
|
||||
},
|
||||
}
|
||||
|
||||
this.project = {
|
||||
_id: ObjectId('611bd20c5d76a3c1bd0c7c13'),
|
||||
name: 'My Test Project',
|
||||
rootDoc_id: ObjectId('611bd20c5d76a3c1bd0c7c15'),
|
||||
rootFolder: [
|
||||
{
|
||||
_id: ObjectId('611bd20c5d76a3c1bd0c7c12'),
|
||||
name: 'rootFolder',
|
||||
folders: [
|
||||
{
|
||||
_id: ObjectId('611bd242e64281c13303d6b5'),
|
||||
name: 'a folder',
|
||||
folders: [
|
||||
{
|
||||
_id: ObjectId('611bd247e64281c13303d6b7'),
|
||||
name: 'a subfolder',
|
||||
folders: [],
|
||||
fileRefs: [],
|
||||
docs: [
|
||||
{
|
||||
_id: ObjectId('611bd24ee64281c13303d6b9'),
|
||||
name: 'a renamed file in a subfolder.tex',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
fileRefs: [],
|
||||
docs: [],
|
||||
},
|
||||
{
|
||||
_id: ObjectId('611bd34ee64281c13303d6be'),
|
||||
name: 'images',
|
||||
folders: [],
|
||||
fileRefs: [
|
||||
{
|
||||
_id: ObjectId('611bd2bce64281c13303d6bb'),
|
||||
name: 'overleaf-white.svg',
|
||||
linkedFileData: {
|
||||
provider: 'url',
|
||||
url: 'https://cdn.overleaf.com/img/ol-brand/overleaf-white.svg',
|
||||
},
|
||||
created: '2021-08-17T15:16:12.753Z',
|
||||
},
|
||||
],
|
||||
docs: [],
|
||||
},
|
||||
],
|
||||
fileRefs: [
|
||||
{
|
||||
_id: ObjectId('611bd20c5d76a3c1bd0c7c19'),
|
||||
name: 'universe.jpg',
|
||||
linkedFileData: null,
|
||||
created: '2021-08-17T15:13:16.400Z',
|
||||
},
|
||||
],
|
||||
docs: [
|
||||
{
|
||||
_id: ObjectId('611bd20c5d76a3c1bd0c7c15'),
|
||||
name: 'main.tex',
|
||||
},
|
||||
{
|
||||
_id: ObjectId('611bd20c5d76a3c1bd0c7c17'),
|
||||
name: 'references.bib',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
compiler: 'pdflatex',
|
||||
description: '',
|
||||
deletedDocs: [],
|
||||
members: [],
|
||||
invites: [],
|
||||
owner: {
|
||||
_id: ObjectId('611572e24bff88527f61dccd'),
|
||||
first_name: 'Test',
|
||||
last_name: 'User',
|
||||
email: 'test@example.com',
|
||||
privileges: 'owner',
|
||||
signUpDate: '2021-08-12T19:13:38.462Z',
|
||||
},
|
||||
features: {},
|
||||
}
|
||||
|
||||
this.multi = {
|
||||
del: sinon.stub(),
|
||||
rpush: sinon.stub(),
|
||||
exec: sinon.stub().yields(null, 1),
|
||||
}
|
||||
|
||||
const { docs, folders } = this.project.rootFolder[0]
|
||||
|
||||
const allDocs = [...docs]
|
||||
|
||||
const processFolders = folders => {
|
||||
for (const folder of folders) {
|
||||
for (const doc of folder.docs) {
|
||||
allDocs.push(doc)
|
||||
}
|
||||
|
||||
if (folder.folders) {
|
||||
processFolders(folder.folders)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
processFolders(folders)
|
||||
|
||||
allDocs.forEach(doc => {
|
||||
doc.lines = [`this is the contents of ${doc.name}`]
|
||||
})
|
||||
|
||||
// handle Doc.find().lean().cursor()
|
||||
this.findDocs = sinon.stub().returns({
|
||||
lean: sinon.stub().returns({
|
||||
cursor: sinon.stub().returns(allDocs),
|
||||
}),
|
||||
})
|
||||
|
||||
// handle await Doc.findOne().lean() - single result, no cursor required
|
||||
this.findOneDoc = sinon.stub().callsFake(id => {
|
||||
const result = allDocs.find(doc => {
|
||||
return doc._id.toString() === id.toString()
|
||||
})
|
||||
return { lean: sinon.stub().resolves(result) }
|
||||
})
|
||||
|
||||
this.deletedFiles = [
|
||||
{
|
||||
_id: deletedFileId,
|
||||
name: 'testing.tex',
|
||||
deletedAt: new Date(),
|
||||
},
|
||||
]
|
||||
|
||||
// handle DeletedFile.find().lean().cursor()
|
||||
this.findDeletedFiles = sinon.stub().returns({
|
||||
lean: sinon
|
||||
.stub()
|
||||
.returns({ cursor: sinon.stub().returns(this.deletedFiles) }),
|
||||
})
|
||||
|
||||
this.ProjectGetter = {
|
||||
promises: {
|
||||
getProject: sinon.stub().resolves(this.project),
|
||||
},
|
||||
}
|
||||
|
||||
this.FileStoreHandler = {
|
||||
_buildUrl: (projectId, fileId) =>
|
||||
`http://filestore.test/${projectId}/${fileId}`,
|
||||
}
|
||||
|
||||
this.ProjectHistoryHandler = {
|
||||
promises: {
|
||||
setHistoryId: sinon.stub(),
|
||||
upgradeHistory: sinon.stub(),
|
||||
},
|
||||
}
|
||||
|
||||
this.ProjectEntityUpdateHandler = {
|
||||
promises: {
|
||||
resyncProjectHistory: sinon.stub(),
|
||||
},
|
||||
}
|
||||
|
||||
this.DocumentUpdaterHandler = {
|
||||
promises: {
|
||||
flushProjectToMongoAndDelete: sinon.stub(),
|
||||
},
|
||||
}
|
||||
|
||||
this.HistoryManager = {
|
||||
promises: {
|
||||
resyncProject: sinon.stub(),
|
||||
flushProject: sinon.stub(),
|
||||
initializeProject: sinon.stub().resolves(historyId),
|
||||
},
|
||||
}
|
||||
|
||||
this.settings = {
|
||||
redis: {
|
||||
project_history_migration: {
|
||||
key_schema: {
|
||||
projectHistoryOps({ projectId }) {
|
||||
return `ProjectHistory:Ops:{${projectId}}` // NOTE: the extra braces are intentional
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
apis: {
|
||||
documentupdater: {
|
||||
url: 'http://document-updater',
|
||||
},
|
||||
trackchanges: {
|
||||
url: 'http://track-changes',
|
||||
},
|
||||
project_history: {
|
||||
url: 'http://project-history',
|
||||
},
|
||||
},
|
||||
path: {
|
||||
projectHistories: 'data/projectHistories',
|
||||
},
|
||||
}
|
||||
|
||||
this.ProjectHistoryController = SandboxedModule.require(modulePath, {
|
||||
requires: {
|
||||
'../../../../app/src/Features/Project/ProjectGetter':
|
||||
this.ProjectGetter,
|
||||
'../../../../app/src/Features/FileStore/FileStoreHandler':
|
||||
this.FileStoreHandler,
|
||||
'../../../../app/src/Features/Project/ProjectHistoryHandler':
|
||||
this.ProjectHistoryHandler,
|
||||
'../../../../app/src/Features/Project/ProjectUpdateHandler':
|
||||
this.ProjectUpdateHandler,
|
||||
'../../../../app/src/Features/Project/ProjectEntityUpdateHandler':
|
||||
this.ProjectEntityUpdateHandler,
|
||||
'../../../../app/src/Features/History/HistoryManager':
|
||||
this.HistoryManager,
|
||||
'../../../../app/src/Features/DocumentUpdater/DocumentUpdaterHandler':
|
||||
this.DocumentUpdaterHandler,
|
||||
'../../../../app/src/models/Doc': {
|
||||
Doc: {
|
||||
find: this.findDocs,
|
||||
findOne: this.findOneDoc,
|
||||
},
|
||||
},
|
||||
'../../../../app/src/models/DeletedFile': {
|
||||
DeletedFile: {
|
||||
find: this.findDeletedFiles,
|
||||
},
|
||||
},
|
||||
'../../../../app/src/infrastructure/mongodb': {
|
||||
db: this.db,
|
||||
},
|
||||
'../../../../app/src/infrastructure/Mongoose': {
|
||||
Schema: {
|
||||
ObjectId: sinon.stub(),
|
||||
Types: {
|
||||
Mixed: sinon.stub(),
|
||||
},
|
||||
},
|
||||
},
|
||||
'../../../../app/src/infrastructure/RedisWrapper': {
|
||||
client: () => ({
|
||||
multi: () => this.multi,
|
||||
llen: sinon.stub().resolves(0),
|
||||
}),
|
||||
},
|
||||
unzipper: {
|
||||
Open: {
|
||||
file: () =>
|
||||
unzipper.Open.file(
|
||||
path.join(__dirname, 'data/track-changes-project.zip')
|
||||
),
|
||||
},
|
||||
},
|
||||
'@overleaf/settings': this.settings,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(function () {
|
||||
nock.cleanAll()
|
||||
})
|
||||
|
||||
it('migrates a project history', async function () {
|
||||
const readStream = fs.createReadStream(
|
||||
path.join(__dirname, 'data/track-changes-project.zip')
|
||||
)
|
||||
|
||||
nock(this.settings.apis.trackchanges.url)
|
||||
.get(`/project/${projectId}/zip`)
|
||||
.reply(200, readStream)
|
||||
|
||||
nock(this.settings.apis.project_history.url)
|
||||
.post(`/project`)
|
||||
.reply(200, { project: { id: historyId } })
|
||||
|
||||
await this.ProjectHistoryController.migrateProjectHistory(
|
||||
projectId.toString(),
|
||||
5
|
||||
)
|
||||
|
||||
expect(this.multi.exec).to.have.been.calledOnce
|
||||
expect(this.ProjectHistoryHandler.promises.setHistoryId).to.have.been
|
||||
.calledOnce
|
||||
// expect(this.ProjectEntityUpdateHandler.promises.resyncProjectHistory).to
|
||||
// .have.been.calledOnce
|
||||
expect(this.HistoryManager.promises.flushProject).to.have.been.calledTwice
|
||||
expect(this.multi.rpush).to.have.callCount(12)
|
||||
|
||||
const args = this.multi.rpush.args
|
||||
|
||||
const snapshotPath = path.join(
|
||||
__dirname,
|
||||
'data/migrate-project-history.snapshot.json'
|
||||
)
|
||||
|
||||
// const snapshot = JSON.stringify(args, null, 2)
|
||||
// await fs.promises.writeFile(snapshotPath, snapshot)
|
||||
|
||||
const json = await fs.promises.readFile(snapshotPath, 'utf-8')
|
||||
const expected = JSON.parse(json)
|
||||
|
||||
expect(args).to.deep.equal(expected)
|
||||
})
|
||||
})
|
||||
-50
@@ -1,50 +0,0 @@
|
||||
[
|
||||
[
|
||||
"ProjectHistory:Ops:{611bd20c5d76a3c1bd0c7c13}",
|
||||
"{\"file\":\"60f6e92c6c14d84fb7a71ae1\",\"pathname\":\"/_deleted/60f6e92c6c14d84fb7a71ae1/testing.tex\",\"meta\":{\"user_id\":null,\"ts\":\"2021-07-20T15:18:04.000Z\",\"origin\":{\"kind\":\"history-migration\"}},\"projectHistoryId\":123,\"url\":\"http://filestore.test/611bd20c5d76a3c1bd0c7c13/60f6e92c6c14d84fb7a71ae1\"}"
|
||||
],
|
||||
[
|
||||
"ProjectHistory:Ops:{611bd20c5d76a3c1bd0c7c13}",
|
||||
"{\"file\":\"60f6e92c6c14d84fb7a71ae1\",\"pathname\":\"/_deleted/60f6e92c6c14d84fb7a71ae1/testing.tex\",\"new_pathname\":\"\",\"meta\":{\"user_id\":null,\"ts\":\"2021-02-01T00:00:00.000Z\",\"origin\":{\"kind\":\"history-migration\"}},\"projectHistoryId\":123}"
|
||||
],
|
||||
[
|
||||
"ProjectHistory:Ops:{611bd20c5d76a3c1bd0c7c13}",
|
||||
"{\"doc\":\"611bd20c5d76a3c1bd0c7c15\",\"pathname\":\"/main.tex\",\"meta\":{\"user_id\":null,\"ts\":\"2021-08-17T15:13:16.000Z\",\"origin\":{\"kind\":\"history-migration\"}},\"projectHistoryId\":123,\"docLines\":\"\\\\documentclass{article}\\n\\\\usepackage[utf8]{inputenc}\\n\\n\\\\title{My Test Project}\\n\\\\author{alf.eaton+dev }\\n\\\\date{7 2021}\\n\\n\\\\usepackage{natbib}\\n\\\\usepackage{graphicx}\\n\\n\\\\begin{document}\\n\\n\\\\maketitle\\n\\n\\\\section{Introduction}\\nThere is a theory which states that if ever anyone discovers exactly what the Universe is for and why it is here, it will instantly disappear and be replaced by something even more bizarre and inexplicable.\\nThere is another theory which states that this has already happened.\\n\\n\\\\begin{figure}[h!]\\n\\\\centering\\n\\\\includegraphics[scale=1.7]{universe}\\n\\\\caption{The Universe}\\n\\\\label{fig:universe}\\n\\\\end{figure}\\n\\n\\\\section{Conclusion}\\n``I always thought something was fundamentally wrong with the universe'' \\\\citep{adams1995hitchhiker}\\n\\n\\\\bibliographystyle{plain}\\n\\\\bibliography{references}\\n\\\\end{document}\\n\"}"
|
||||
],
|
||||
[
|
||||
"ProjectHistory:Ops:{611bd20c5d76a3c1bd0c7c13}",
|
||||
"{\"doc\":\"611bd20c5d76a3c1bd0c7c17\",\"pathname\":\"/references.bib\",\"meta\":{\"user_id\":null,\"ts\":\"2021-08-17T15:13:16.000Z\",\"origin\":{\"kind\":\"history-migration\"}},\"projectHistoryId\":123,\"docLines\":\"this is the contents of references.bib\"}"
|
||||
],
|
||||
[
|
||||
"ProjectHistory:Ops:{611bd20c5d76a3c1bd0c7c13}",
|
||||
"{\"file\":\"611bd20c5d76a3c1bd0c7c19\",\"pathname\":\"/universe.jpg\",\"meta\":{\"user_id\":null,\"ts\":\"2021-08-17T15:13:16.000Z\",\"origin\":{\"kind\":\"history-migration\"}},\"projectHistoryId\":123,\"url\":\"http://filestore.test/611bd20c5d76a3c1bd0c7c13/611bd20c5d76a3c1bd0c7c19\"}"
|
||||
],
|
||||
[
|
||||
"ProjectHistory:Ops:{611bd20c5d76a3c1bd0c7c13}",
|
||||
"{\"doc\":\"611bd20c5d76a3c1bd0c7c15\",\"op\":[{\"p\":487,\"i\":\"\\n\\nAdding some text here.\"}],\"v\":1,\"lastV\":0,\"meta\":{\"user_id\":\"611572e24bff88527f61dccd\",\"ts\":1629213228148,\"pathname\":\"/main.tex\",\"doc_length\":805,\"origin\":{\"kind\":\"history-migration\"}},\"projectHistoryId\":123}"
|
||||
],
|
||||
[
|
||||
"ProjectHistory:Ops:{611bd20c5d76a3c1bd0c7c13}",
|
||||
"{\"doc\":\"611bd20c5d76a3c1bd0c7c15\",\"op\":[{\"p\":678,\"d\":\" something\"}],\"v\":2,\"lastV\":1,\"meta\":{\"user_id\":\"611572e24bff88527f61dccd\",\"ts\":1629213235181,\"pathname\":\"/main.tex\",\"doc_length\":829,\"origin\":{\"kind\":\"history-migration\"}},\"projectHistoryId\":123}"
|
||||
],
|
||||
[
|
||||
"ProjectHistory:Ops:{611bd20c5d76a3c1bd0c7c13}",
|
||||
"{\"doc\":\"611bd20c5d76a3c1bd0c7c15\",\"op\":[{\"d\":\" \",\"p\":722},{\"i\":\"\\n\",\"p\":722}],\"v\":3,\"lastV\":2,\"meta\":{\"user_id\":\"611572e24bff88527f61dccd\",\"ts\":1629213239472,\"pathname\":\"/main.tex\",\"doc_length\":819,\"origin\":{\"kind\":\"history-migration\"}},\"projectHistoryId\":123}"
|
||||
],
|
||||
[
|
||||
"ProjectHistory:Ops:{611bd20c5d76a3c1bd0c7c13}",
|
||||
"{\"doc\":\"611bd20c5d76a3c1bd0c7c15\",\"op\":[{\"p\":750,\"i\":\"\\n\\nAdding some text after deleting some text.\"}],\"v\":7,\"lastV\":6,\"meta\":{\"user_id\":\"611572e24bff88527f61dccd\",\"ts\":1629213241498,\"pathname\":\"/main.tex\",\"doc_length\":819,\"origin\":{\"kind\":\"history-migration\"}},\"projectHistoryId\":123}"
|
||||
],
|
||||
[
|
||||
"ProjectHistory:Ops:{611bd20c5d76a3c1bd0c7c13}",
|
||||
"{\"doc\":\"611bd24ee64281c13303d6b9\",\"pathname\":\"/a folder/a subfolder/a renamed file in a subfolder.tex\",\"meta\":{\"user_id\":null,\"ts\":\"2021-08-17T15:14:22.000Z\",\"origin\":{\"kind\":\"history-migration\"}},\"projectHistoryId\":123,\"docLines\":\"\"}"
|
||||
],
|
||||
[
|
||||
"ProjectHistory:Ops:{611bd20c5d76a3c1bd0c7c13}",
|
||||
"{\"doc\":\"611bd24ee64281c13303d6b9\",\"op\":[{\"p\":0,\"i\":\"Adding some content to the file in the subfolder.\"}],\"v\":2,\"lastV\":1,\"meta\":{\"user_id\":\"611572e24bff88527f61dccd\",\"ts\":1629213266076,\"pathname\":\"/a folder/a subfolder/a renamed file in a subfolder.tex\",\"doc_length\":0,\"origin\":{\"kind\":\"history-migration\"}},\"projectHistoryId\":123}"
|
||||
],
|
||||
[
|
||||
"ProjectHistory:Ops:{611bd20c5d76a3c1bd0c7c13}",
|
||||
"{\"file\":\"611bd2bce64281c13303d6bb\",\"pathname\":\"/images/overleaf-white.svg\",\"meta\":{\"user_id\":null,\"ts\":\"2021-08-17T15:16:12.000Z\",\"origin\":{\"kind\":\"history-migration\"}},\"projectHistoryId\":123,\"url\":\"http://filestore.test/611bd20c5d76a3c1bd0c7c13/611bd2bce64281c13303d6bb\"}"
|
||||
]
|
||||
]
|
||||
Binary file not shown.
Reference in New Issue
Block a user