diff --git a/services/web/app/coffee/Features/History/HistoryManager.coffee b/services/web/app/coffee/Features/History/HistoryManager.coffee index bef9773a6b..800585c0bf 100644 --- a/services/web/app/coffee/Features/History/HistoryManager.coffee +++ b/services/web/app/coffee/Features/History/HistoryManager.coffee @@ -27,6 +27,17 @@ module.exports = HistoryManager = error = new Error("project-history returned a non-success status code: #{res.statusCode}") callback error + flushProject: (project_id, callback = (error) ->) -> + request.post { + url: "#{settings.apis.project_history.url}/project/#{project_id}/flush" + }, (error, res, body)-> + return callback(error) if error? + if res.statusCode >= 200 and res.statusCode < 300 + callback() + else + error = new Error("project-history returned a non-success status code: #{res.statusCode}") + callback error + injectUserDetails: (data, callback = (error, data_with_users) ->) -> # data can be either: # { diff --git a/services/web/app/coffee/Features/Project/ProjectHistoryHandler.coffee b/services/web/app/coffee/Features/Project/ProjectHistoryHandler.coffee new file mode 100644 index 0000000000..2b59fe1d5f --- /dev/null +++ b/services/web/app/coffee/Features/Project/ProjectHistoryHandler.coffee @@ -0,0 +1,42 @@ +Project = require('../../models/Project').Project +ProjectDetailsHandler = require "./ProjectDetailsHandler" +logger = require('logger-sharelatex') +settings = require("settings-sharelatex") +HistoryManager = require "../History/HistoryManager" +ProjectEntityUpdateHandler = require "./ProjectEntityUpdateHandler" + +module.exports = ProjectHistoryHandler = + + setHistoryId: (project_id, history_id, callback = (err) ->) -> + # reject invalid history ids + return callback(new Error("invalid history id")) if !history_id or typeof(history_id) isnt 'number' + # use $exists:false to prevent overwriting any existing history id, atomically + Project.update {_id: project_id, "overleaf.history.id": {$exists:false}}, {"overleaf.history.id":history_id}, (err, result)-> + return callback(err) if err? + return callback(new Error("history exists")) if result?.n == 0 + callback() + + getHistoryId: (project_id, callback = (err, result) ->) -> + ProjectDetailsHandler.getDetails project_id, (err, project) -> + return callback(err) if err? # n.b. getDetails returns an error if the project doesn't exist + return callback(new Error("history exists")) if project.overleaf?.history?.id? + callback(null, project?.overleaf?.history?.id) + + ensureHistoryExistsForProject: (project_id, callback = (err) ->) -> + # We can only set a history id for a project that doesn't have one. The + # history id is cached in the project history service, and changing an + # existing value corrupts the history, leaving it in an irrecoverable + # state. Setting a history id when one wasn't present before is ok, + # because undefined history ids aren't cached. + ProjectHistoryHandler.getHistoryId project_id, (err, history_id) -> + return callback(err) if err? + return callback() if history_id? # history already exists, success + HistoryManager.initializeProject (err, history) -> + return callback(err) if err? + return callback(new Error("failed to initialize history id")) if !history?.overleaf_id + ProjectHistoryHandler.setHistoryId project_id, history.overleaf_id, (err) -> + return callback(err) if err? + ProjectEntityUpdateHandler.resyncProjectHistory project_id, (err) -> + return callback(err) if err? + logger.log {project_id: project_id, history_id: history.overleaf_id}, "started syncing project with new history id" + HistoryManager.flushProject project_id, callback diff --git a/services/web/test/unit/coffee/Project/ProjectHistoryHandlerTests.coffee b/services/web/test/unit/coffee/Project/ProjectHistoryHandlerTests.coffee new file mode 100644 index 0000000000..bb98b7bebc --- /dev/null +++ b/services/web/test/unit/coffee/Project/ProjectHistoryHandlerTests.coffee @@ -0,0 +1,105 @@ +chai = require('chai') +assert = require('chai').assert +should = chai.should() +expect = chai.expect +sinon = require 'sinon' +modulePath = "../../../../app/js/Features/Project/ProjectHistoryHandler" +SandboxedModule = require('sandboxed-module') +ObjectId = require("mongoose").Types.ObjectId + +describe 'ProjectHistoryHandler', -> + project_id = '4eecb1c1bffa66588e0000a1' + userId = 1234 + + beforeEach -> + @ProjectModel = class Project + constructor:(options)-> + @._id = project_id + @name = "project_name_here" + @rev = 0 + rootFolder:[@rootFolder] + @project = new @ProjectModel() + + @callback = sinon.stub() + + @ProjectHistoryHandler = SandboxedModule.require modulePath, requires: + 'logger-sharelatex': @logger = {log:sinon.stub(), error: sinon.stub(), err:->} + 'settings-sharelatex': @Settings = {} + '../../models/Project': Project:@ProjectModel + './ProjectDetailsHandler': @ProjectDetailsHandler = {} + '../History/HistoryManager': @HistoryManager = {} + './ProjectEntityUpdateHandler': @ProjectEntityUpdateHandler = {} + + describe "starting history for an existing project", -> + beforeEach -> + @newHistoryId = 123456789 + @HistoryManager.initializeProject = sinon.stub().callsArgWith(0, null, {overleaf_id: @newHistoryId}) + @HistoryManager.flushProject = sinon.stub().callsArg(1) + @ProjectEntityUpdateHandler.resyncProjectHistory = sinon.stub().callsArg(1) + + describe "when the history does not already exist", -> + beforeEach -> + @ProjectDetailsHandler.getDetails = sinon.stub().withArgs(project_id).callsArgWith(1, null, @project) + @ProjectModel.update = sinon.stub().callsArgWith(2,null,{n:1}) + @ProjectHistoryHandler.ensureHistoryExistsForProject project_id, @callback + + it "should get any existing history id for the project", -> + @ProjectDetailsHandler.getDetails + .calledWith(project_id) + .should.equal true + + it "should initialize a new history in the v1 history service", -> + @HistoryManager.initializeProject + .called.should.equal.true + + it "should set the new history id on the project", -> + @ProjectModel.update + .calledWith({_id: project_id, "overleaf.history.id": {$exists:false}}, {"overleaf.history.id":@newHistoryId}) + .should.equal true + + it "should resync the project history", -> + @ProjectEntityUpdateHandler.resyncProjectHistory + .calledWith(project_id) + .should.equal true + + it "should flush the project history", -> + @HistoryManager.flushProject + .calledWith(project_id) + .should.equal true + + it "should call the callback without an error", -> + @callback.called.should.equal true + + describe "when the history already exists", -> + beforeEach -> + @project.overleaf = {history: {id: 1234}} + @ProjectDetailsHandler.getDetails = sinon.stub().withArgs(project_id).callsArgWith(1, null, @project) + @ProjectModel.update = sinon.stub() + @ProjectHistoryHandler.ensureHistoryExistsForProject project_id, @callback + + it "should get any existing history id for the project", -> + @ProjectDetailsHandler.getDetails + .calledWith(project_id) + .should.equal true + + it "should not initialize a new history in the v1 history service", -> + @HistoryManager.initializeProject + .called.should.equal false + + it "should not set the new history id on the project", -> + @ProjectModel.update + .called + .should.equal false + + it "should not resync the project history", -> + @ProjectEntityUpdateHandler.resyncProjectHistory + .called + .should.equal false + + it "should not flush the project history", -> + @HistoryManager.flushProject + .called + .should.equal false + + it "should call the callback", -> + @callback.calledWith().should.equal true \ No newline at end of file