`, and ``.
@font-family-monospace: Menlo, Monaco, Consolas, "Courier New", monospace;
@font-family-base: @font-family-sans-serif;
@@ -550,7 +545,7 @@
//## Define alert colors, border radius, and padding.
@alert-padding: 15px;
-@alert-border-radius: @border-radius-base;
+@alert-border-radius: 0;
@alert-link-font-weight: bold;
@alert-success-bg: @state-success-bg;
@@ -898,25 +893,28 @@
@toolbar-btn-active-color : white;
@toolbar-btn-active-bg-color : @link-color;
@toolbar-btn-active-shadow : inset 0 3px 5px rgba(0, 0, 0, 0.225);
+@toolbar-font-size : 12px;
@toolbar-alt-bg-color : #fafafa;
@toolbar-icon-btn-color : @gray-light;
@toolbar-icon-btn-hover-color : @gray-dark;
@toolbar-icon-btn-hover-shadow : 0 1px 0 rgba(0, 0, 0, 0.25);
@toolbar-icon-btn-hover-boxshadow : inset 0 3px 5px rgba(0, 0, 0, 0.225);
-@toolbar-border-bottom : 1px solid @toolbar-border-color;
+@toolbar-border-bottom : 1px solid @toolbar-border-color;
+@toolbar-small-height : 32px;
+@toolbar-tall-height : 58px;
// Editor file-tree
-@file-tree-bg : transparent;
-@file-tree-line-height : 2.6;
-@file-tree-item-color : @gray-darker;
-@file-tree-item-toggle-color : @gray;
-@file-tree-item-icon-color : @gray-light;
-@file-tree-item-input-color : inherit;
-@file-tree-item-folder-color : lighten(desaturate(@link-color, 10%), 5%);
-@file-tree-item-hover-bg : @gray-lightest;
-@file-tree-item-selected-bg : transparent;
-@file-tree-multiselect-bg : lighten(@brand-info, 40%);
-@file-tree-multiselect-hover-bg : lighten(@brand-info, 30%);
+@file-tree-bg : transparent;
+@file-tree-line-height : 2.6;
+@file-tree-item-color : @gray-darker;
+@file-tree-item-toggle-color : @gray;
+@file-tree-item-icon-color : @gray-light;
+@file-tree-item-input-color : inherit;
+@file-tree-item-folder-color : lighten(desaturate(@link-color, 10%), 5%);
+@file-tree-item-hover-bg : @gray-lightest;
+@file-tree-item-selected-bg : transparent;
+@file-tree-multiselect-bg : lighten(@brand-info, 40%);
+@file-tree-multiselect-hover-bg : lighten(@brand-info, 30%);
// Editor resizers
@editor-resizer-bg-color : #F4F4F4;
@@ -925,6 +923,28 @@
@editor-toggler-hover-bg-color : #DDD;
@synctex-controls-z-index : 3;
@synctex-controls-padding : 0 2px;
+
+// Chat
+@chat-bg : transparent;
+@chat-message-color : @text-color;
+@chat-message-date-color : @gray-light;
+@chat-message-name-color : @gray-light;
+@chat-message-box-shadow : -1px 2px 3px #ddd;
+@chat-message-border-radius : 0;
+@chat-message-padding : @line-height-computed / 2;
+@chat-message-weight : normal;
+@chat-new-message-bg : @gray-lightest;
+@chat-new-message-textarea-bg : #FFF;
+@chat-new-message-textarea-color : @gray-dark;
+
+// PDF
+@pdf-top-offset : @toolbar-tall-height;
+@pdf-bg : transparent;
+@pdfjs-bg : @gray-lighter;
+@pdf-page-shadow-color : #000;
+@log-line-no-color : @gray;
+@log-hints-color : @gray-dark;
+
// Tags
@tag-border-radius : 0.25em;
@tag-bg-color : @label-default-bg;
diff --git a/services/web/public/stylesheets/core/ol-variables.less b/services/web/public/stylesheets/core/ol-variables.less
index 329f1005f1..2485dd3853 100644
--- a/services/web/public/stylesheets/core/ol-variables.less
+++ b/services/web/public/stylesheets/core/ol-variables.less
@@ -1,6 +1,9 @@
@import "./_common-variables.less";
@is-overleaf: true;
+
+@font-family-sans-serif: "Lato", sans-serif;
+
@header-height: 68px;
@footer-height: 50px;
@@ -64,6 +67,28 @@
@btn-info-bg : @ol-blue;
@btn-info-border : transparent;
+// Alerts
+@alert-padding : 15px;
+@alert-border-radius : @border-radius-base;
+@alert-link-font-weight : bold;
+
+@alert-success-bg : @brand-success;
+@alert-success-text : #FFF;
+@alert-success-border: transparent;
+
+@alert-info-bg : @brand-info;
+@alert-info-text : #FFF;
+@alert-info-border : transparent;
+
+@alert-warning-bg : @brand-warning;
+@alert-warning-text : #FFF;
+@alert-warning-border: transparent;
+
+@alert-danger-bg : @brand-danger;
+@alert-danger-text : #FFF;
+@alert-danger-border : transparent;
+
+
// Tags
@tag-border-radius : 9999px;
@tag-bg-color : @ol-green;
@@ -177,19 +202,22 @@
@toolbar-icon-btn-hover-shadow : none;
@toolbar-border-bottom : 1px solid @toolbar-border-color;
@toolbar-icon-btn-hover-boxshadow : none;
+@toolbar-font-size : 13px;
+
// Editor file-tree
-@file-tree-bg : @ol-blue-gray-4;
-@file-tree-line-height : 2.05;
-@file-tree-item-color : #FFF;
-@file-tree-item-input-color : @ol-blue-gray-5;
-@file-tree-item-toggle-color : @ol-blue-gray-2;
-@file-tree-item-icon-color : @ol-blue-gray-2;
-@file-tree-item-folder-color : @ol-blue-gray-2;
-@file-tree-item-hover-bg : @ol-blue-gray-5;
-@file-tree-item-selected-bg : @ol-green;
-@file-tree-multiselect-bg : @ol-blue;
-@file-tree-multiselect-hover-bg : @ol-dark-blue;
-@file-tree-droppable-bg-color : tint(@ol-green, 5%);
+@file-tree-bg : @ol-blue-gray-4;
+@file-tree-line-height : 2.05;
+@file-tree-item-color : #FFF;
+@file-tree-item-input-color : @ol-blue-gray-5;
+@file-tree-item-toggle-color : @ol-blue-gray-2;
+@file-tree-item-icon-color : @ol-blue-gray-2;
+@file-tree-item-folder-color : @ol-blue-gray-2;
+@file-tree-item-hover-bg : @ol-blue-gray-5;
+@file-tree-item-selected-bg : @ol-green;
+@file-tree-multiselect-bg : @ol-blue;
+@file-tree-multiselect-hover-bg : @ol-dark-blue;
+@file-tree-droppable-bg-color : tint(@ol-green, 5%);
+
// Editor resizers
@editor-resizer-bg-color : @ol-blue-gray-6;
@editor-resizer-bg-color-dragging : transparent;
@@ -197,6 +225,31 @@
@editor-toggler-hover-bg-color : @ol-green;
@synctex-controls-z-index : 6;
@synctex-controls-padding : 0;
+@editor-border-color : @ol-blue-gray-5;
+
+// Chat
+@chat-bg : @ol-blue-gray-5;
+@chat-message-color : #FFF;
+@chat-message-name-color : #FFF;
+@chat-message-date-color : @ol-blue-gray-2;
+@chat-message-box-shadow : none;
+@chat-message-padding : 5px 10px;
+@chat-message-border-radius : @border-radius-large;
+@chat-message-weight : bold;
+@chat-new-message-bg : @ol-blue-gray-4;
+@chat-new-message-textarea-bg : @ol-blue-gray-1;
+@chat-new-message-textarea-color : @ol-blue-gray-6;
+
+// PDF
+@pdf-top-offset : @toolbar-small-height;
+@pdf-bg : @ol-blue-gray-1;
+@pdfjs-bg : transparent;
+@pdf-page-shadow-color : rgba(0, 0, 0, 0.5);
+@log-line-no-color : #FFF;
+@log-hints-color : @ol-blue-gray-4;
+
+//== Colors
+//
//## Gray and brand colors for use across Bootstrap.
@gray-darker: #252525;
@gray-dark: #505050;
@@ -216,9 +269,9 @@
@brand-primary: @ol-green;
@brand-success: @green;
-@brand-info: @ol-dark-green;
+@brand-info: @ol-blue;
@brand-warning: @orange;
-@brand-danger: #E03A06;
+@brand-danger: @ol-red;
@editor-loading-logo-padding-top: 115.44%;
@editor-loading-logo-background-url: url(/img/ol-brand/overleaf-o-grey.svg);
diff --git a/services/web/public/stylesheets/ol-style.less b/services/web/public/stylesheets/ol-style.less
index 2a9a140611..540d106ab3 100644
--- a/services/web/public/stylesheets/ol-style.less
+++ b/services/web/public/stylesheets/ol-style.less
@@ -1,3 +1,6 @@
+@import url(https://fonts.googleapis.com/css?family=Lato:300,400,700&subset=latin-ext);
+@import url(https://fonts.googleapis.com/css?family=Merriweather:400,400i,700,700i);
+
// Core variables and mixins
@import "core/ol-variables.less";
@import "app/ol-style-guide.less";
diff --git a/services/web/public/stylesheets/style.less b/services/web/public/stylesheets/style.less
index 760f378719..ae5820453f 100755
--- a/services/web/public/stylesheets/style.less
+++ b/services/web/public/stylesheets/style.less
@@ -1,3 +1,6 @@
+@import url(https://fonts.googleapis.com/css?family=Open+Sans:300,400,600,700);
+@import url(https://fonts.googleapis.com/css?family=Merriweather:400,400i,700,700i);
+
// Core variables and mixins
@import "core/variables.less";
@import "_style_includes.less";
\ No newline at end of file
diff --git a/services/web/test/acceptance/coffee/ProjectStructureTests.coffee b/services/web/test/acceptance/coffee/ProjectStructureTests.coffee
index e54d4fac9d..e037deb5a3 100644
--- a/services/web/test/acceptance/coffee/ProjectStructureTests.coffee
+++ b/services/web/test/acceptance/coffee/ProjectStructureTests.coffee
@@ -59,7 +59,7 @@ describe "ProjectStructureChanges", ->
@dup_project_id = body.project_id
done()
- it "should version the dosc created", ->
+ it "should version the docs created", ->
updates = MockDocUpdaterApi.getProjectStructureUpdates(@dup_project_id).docUpdates
expect(updates.length).to.equal(2)
_.each updates, (update) =>
@@ -91,6 +91,7 @@ describe "ProjectStructureChanges", ->
throw error if error?
if res.statusCode < 200 || res.statusCode >= 300
throw new Error("failed to add doc #{res.statusCode}")
+ @example_doc_id = body._id
done()
it "should version the doc added", ->
@@ -162,6 +163,8 @@ describe "ProjectStructureChanges", ->
if res.statusCode < 200 || res.statusCode >= 300
throw new Error("failed to upload file #{res.statusCode}")
+ @example_file_id = JSON.parse(body).entity_id
+
updates = MockDocUpdaterApi.getProjectStructureUpdates(@example_project_id).fileUpdates
expect(updates.length).to.equal(1)
update = updates[0]
@@ -199,6 +202,120 @@ describe "ProjectStructureChanges", ->
done()
+ describe "moving entities", ->
+ before (done) ->
+ @owner.request.post {
+ uri: "project/#{@example_project_id}/folder",
+ formData:
+ name: 'foo'
+ }, (error, res, body) =>
+ throw error if error?
+ @example_folder_id_1 = JSON.parse(body)._id
+ done()
+
+ beforeEach () ->
+ MockDocUpdaterApi.clearProjectStructureUpdates()
+
+ it "should version moving a doc", (done) ->
+ @owner.request.post {
+ uri: "project/#{@example_project_id}/Doc/#{@example_doc_id}/move",
+ json:
+ folder_id: @example_folder_id_1
+ }, (error, res, body) =>
+ throw error if error?
+ if res.statusCode < 200 || res.statusCode >= 300
+ throw new Error("failed to move doc #{res.statusCode}")
+
+ updates = MockDocUpdaterApi.getProjectStructureUpdates(@example_project_id).docUpdates
+ expect(updates.length).to.equal(1)
+ update = updates[0]
+ expect(update.userId).to.equal(@owner._id)
+ expect(update.pathname).to.equal("/new.tex")
+ expect(update.newPathname).to.equal("/foo/new.tex")
+
+ done()
+
+ it "should version moving a file", (done) ->
+ @owner.request.post {
+ uri: "project/#{@example_project_id}/File/#{@example_file_id}/move",
+ json:
+ folder_id: @example_folder_id_1
+ }, (error, res, body) =>
+ throw error if error?
+ if res.statusCode < 200 || res.statusCode >= 300
+ throw new Error("failed to move file #{res.statusCode}")
+
+ updates = MockDocUpdaterApi.getProjectStructureUpdates(@example_project_id).fileUpdates
+ expect(updates.length).to.equal(1)
+ update = updates[0]
+ expect(update.userId).to.equal(@owner._id)
+ expect(update.pathname).to.equal("/1pixel.png")
+ expect(update.newPathname).to.equal("/foo/1pixel.png")
+
+ done()
+
+ it "should version moving a folder", (done) ->
+ @owner.request.post {
+ uri: "project/#{@example_project_id}/folder",
+ formData:
+ name: 'bar'
+ }, (error, res, body) =>
+ throw error if error?
+ @example_folder_id_2 = JSON.parse(body)._id
+
+ @owner.request.post {
+ uri: "project/#{@example_project_id}/Folder/#{@example_folder_id_1}/move",
+ json:
+ folder_id: @example_folder_id_2
+ }, (error, res, body) =>
+ throw error if error?
+ if res.statusCode < 200 || res.statusCode >= 300
+ throw new Error("failed to move folder #{res.statusCode}")
+
+ updates = MockDocUpdaterApi.getProjectStructureUpdates(@example_project_id).docUpdates
+ expect(updates.length).to.equal(1)
+ update = updates[0]
+ expect(update.userId).to.equal(@owner._id)
+ expect(update.pathname).to.equal("/foo/new.tex")
+ expect(update.newPathname).to.equal("/bar/foo/new.tex")
+
+ updates = MockDocUpdaterApi.getProjectStructureUpdates(@example_project_id).fileUpdates
+ expect(updates.length).to.equal(1)
+ update = updates[0]
+ expect(update.userId).to.equal(@owner._id)
+ expect(update.pathname).to.equal("/foo/1pixel.png")
+ expect(update.newPathname).to.equal("/bar/foo/1pixel.png")
+
+ done()
+
+ describe "deleting entities", ->
+ beforeEach () ->
+ MockDocUpdaterApi.clearProjectStructureUpdates()
+
+ it "should version deleting a folder", (done) ->
+ @owner.request.delete {
+ uri: "project/#{@example_project_id}/Folder/#{@example_folder_id_2}",
+ }, (error, res, body) =>
+ throw error if error?
+ if res.statusCode < 200 || res.statusCode >= 300
+ throw new Error("failed to delete folder #{res.statusCode}")
+
+ updates = MockDocUpdaterApi.getProjectStructureUpdates(@example_project_id).docUpdates
+ expect(updates.length).to.equal(1)
+ update = updates[0]
+ expect(update.userId).to.equal(@owner._id)
+ expect(update.pathname).to.equal("/bar/foo/new.tex")
+ expect(update.newPathname).to.equal("")
+
+ updates = MockDocUpdaterApi.getProjectStructureUpdates(@example_project_id).fileUpdates
+ expect(updates.length).to.equal(1)
+ update = updates[0]
+ expect(update.userId).to.equal(@owner._id)
+ expect(update.pathname).to.equal("/bar/foo/1pixel.png")
+ expect(update.newPathname).to.equal("")
+
+ done()
+
describe "tpds", ->
before (done) ->
@tpds_project_name = "tpds-project-#{new ObjectId().toString()}"
@@ -305,3 +422,25 @@ describe "ProjectStructureChanges", ->
done()
image_file.pipe(req)
+
+ it "should version deleting a doc", (done) ->
+ req = @owner.request.delete {
+ uri: "/user/#{@owner._id}/update/#{@tpds_project_name}/test.tex",
+ auth:
+ user: _.keys(Settings.httpAuthUsers)[0]
+ pass: _.values(Settings.httpAuthUsers)[0]
+ sendImmediately: true
+ }, (error, res, body) =>
+ throw error if error?
+ if res.statusCode < 200 || res.statusCode >= 300
+ throw new Error("failed to delete doc #{res.statusCode}")
+
+ updates = MockDocUpdaterApi.getProjectStructureUpdates(@tpds_project_id).docUpdates
+ expect(updates.length).to.equal(1)
+ update = updates[0]
+ expect(update.userId).to.equal(@owner._id)
+ expect(update.pathname).to.equal("/test.tex")
+ expect(update.newPathname).to.equal("")
+
+ done()
+
diff --git a/services/web/test/acceptance/coffee/helpers/MockDocUpdaterApi.coffee b/services/web/test/acceptance/coffee/helpers/MockDocUpdaterApi.coffee
index b00cd6b173..b21fb1adab 100644
--- a/services/web/test/acceptance/coffee/helpers/MockDocUpdaterApi.coffee
+++ b/services/web/test/acceptance/coffee/helpers/MockDocUpdaterApi.coffee
@@ -33,6 +33,9 @@ module.exports = MockDocUpdaterApi =
@addProjectStructureUpdates(project_id, userId, docUpdates, fileUpdates)
res.sendStatus 200
+ app.delete "/project/:project_id/doc/:doc_id", (req, res, next) =>
+ res.send 204
+
app.listen 3003, (error) ->
throw error if error?
.on "error", (error) ->
diff --git a/services/web/test/acceptance/coffee/helpers/MockDocstoreApi.coffee b/services/web/test/acceptance/coffee/helpers/MockDocstoreApi.coffee
index c5b003ac75..631538fd89 100644
--- a/services/web/test/acceptance/coffee/helpers/MockDocstoreApi.coffee
+++ b/services/web/test/acceptance/coffee/helpers/MockDocstoreApi.coffee
@@ -23,6 +23,16 @@ module.exports = MockDocStoreApi =
docs = (doc for doc_id, doc of @docs[req.params.project_id])
res.send JSON.stringify docs
+ app.delete "/project/:project_id/doc/:doc_id", (req, res, next) =>
+ {project_id, doc_id} = req.params
+ if !@docs[project_id]?
+ res.send 404
+ else if !@docs[project_id][doc_id]?
+ res.send 404
+ else
+ @docs[project_id][doc_id] = undefined
+ res.send 204
+
app.listen 3016, (error) ->
throw error if error?
.on "error", (error) ->
diff --git a/services/web/test/unit/coffee/DocumentUpdater/DocumentUpdaterHandlerTests.coffee b/services/web/test/unit/coffee/DocumentUpdater/DocumentUpdaterHandlerTests.coffee
index 14ccaa3a33..a4e0a4dc53 100644
--- a/services/web/test/unit/coffee/DocumentUpdater/DocumentUpdaterHandlerTests.coffee
+++ b/services/web/test/unit/coffee/DocumentUpdater/DocumentUpdaterHandlerTests.coffee
@@ -393,7 +393,7 @@ describe 'DocumentUpdaterHandler', ->
describe "with project history disabled", ->
beforeEach ->
- @settings.apis.project_history.enabled = false
+ @settings.apis.project_history.sendProjectStructureOps = false
@request.post = sinon.stub()
@handler.updateProjectStructure @project_id, @user_id, {}, @callback
@@ -406,7 +406,7 @@ describe 'DocumentUpdaterHandler', ->
describe "with project history enabled", ->
beforeEach ->
- @settings.apis.project_history.enabled = true
+ @settings.apis.project_history.sendProjectStructureOps = true
@url = "#{@settings.apis.documentupdater.url}/project/#{@project_id}"
@request.post = sinon.stub().callsArgWith(1, null, {statusCode: 204}, "")
@@ -478,14 +478,22 @@ describe 'DocumentUpdaterHandler', ->
.should.equal true
done()
- describe "when a doc has been deleted", ->
- it 'should do nothing', (done) ->
+ describe "when an entity has been deleted", ->
+ it 'should end the structure update to the document updater', (done) ->
@docId = new ObjectId()
@changes = oldDocs: [
{ path: '/foo', docLines: 'a\nb', doc: _id: @docId }
]
+ docUpdates = [
+ id: @docId.toString(),
+ pathname: '/foo',
+ newPathname: ''
+ ]
+
@handler.updateProjectStructure @project_id, @user_id, @changes, () =>
- @request.post.called.should.equal false
+ @request.post
+ .calledWith(url: @url, json: {docUpdates, fileUpdates: [], userId: @user_id})
+ .should.equal true
done()
diff --git a/services/web/test/unit/coffee/Editor/EditorControllerTests.coffee b/services/web/test/unit/coffee/Editor/EditorControllerTests.coffee
index 4e1af79b46..85760f510d 100644
--- a/services/web/test/unit/coffee/Editor/EditorControllerTests.coffee
+++ b/services/web/test/unit/coffee/Editor/EditorControllerTests.coffee
@@ -376,58 +376,53 @@ describe "EditorController", ->
err.should.equal "timed out"
done()
-
describe "deleteEntity", ->
-
beforeEach ->
@LockManager.getLock.callsArgWith(1)
@LockManager.releaseLock.callsArgWith(1)
- @EditorController.deleteEntityWithoutLock = sinon.stub().callsArgWith(4)
+ @EditorController.deleteEntityWithoutLock = sinon.stub().callsArgWith(5)
it "should call deleteEntityWithoutLock", (done)->
- @EditorController.deleteEntity @project_id, @entity_id, @type, @source, =>
- @EditorController.deleteEntityWithoutLock.calledWith(@project_id, @entity_id, @type, @source).should.equal true
+ @EditorController.deleteEntity @project_id, @entity_id, @type, @source, @user_id, =>
+ @EditorController.deleteEntityWithoutLock
+ .calledWith(@project_id, @entity_id, @type, @source, @user_id)
+ .should.equal true
done()
it "should take the lock", (done)->
- @EditorController.deleteEntity @project_id, @entity_id, @type, @source, =>
+ @EditorController.deleteEntity @project_id, @entity_id, @type, @source, @user_id, =>
@LockManager.getLock.calledWith(@project_id).should.equal true
done()
it "should release the lock", (done)->
- @EditorController.deleteEntity @project_id, @entity_id, @type, @source, (error)=>
+ @EditorController.deleteEntity @project_id, @entity_id, @type, @source, @user_id, (error) =>
@LockManager.releaseLock.calledWith(@project_id).should.equal true
done()
it "should error if it can't cat the lock", (done)->
@LockManager.getLock = sinon.stub().callsArgWith(1, "timed out")
- @EditorController.deleteEntity @project_id, @entity_id, @type, @source, (err)=>
- expect(err).to.exist
- err.should.equal "timed out"
- done()
-
-
+ @EditorController.deleteEntity @project_id, @entity_id, @type, @source, @user_id, (error) =>
+ expect(error).to.exist
+ error.should.equal "timed out"
+ done()
describe 'deleteEntityWithoutLock', ->
- beforeEach ->
- @ProjectEntityHandler.deleteEntity = (project_id, entity_id, type, callback)-> callback()
+ beforeEach (done) ->
@entity_id = "entity_id_here"
@type = "doc"
@EditorRealTimeController.emitToRoom = sinon.stub()
+ @ProjectEntityHandler.deleteEntity = sinon.stub().callsArg(4)
+ @EditorController.deleteEntityWithoutLock @project_id, @entity_id, @type, @source, @user_id, done
- it 'should delete the folder using the project entity handler', (done)->
- mock = sinon.mock(@ProjectEntityHandler).expects("deleteEntity").withArgs(@project_id, @entity_id, @type).callsArg(3)
+ it 'should delete the folder using the project entity handler', ->
+ @ProjectEntityHandler.deleteEntity
+ .calledWith(@project_id, @entity_id, @type, @user_id)
+ .should.equal.true
- @EditorController.deleteEntityWithoutLock @project_id, @entity_id, @type, @source, ->
- mock.verify()
- done()
-
- it 'notify users an entity has been deleted', (done)->
- @EditorController.deleteEntityWithoutLock @project_id, @entity_id, @type, @source, =>
- @EditorRealTimeController.emitToRoom
- .calledWith(@project_id, "removeEntity", @entity_id, @source)
- .should.equal true
- done()
+ it 'notify users an entity has been deleted', ->
+ @EditorRealTimeController.emitToRoom
+ .calledWith(@project_id, "removeEntity", @entity_id, @source)
+ .should.equal true
describe "getting a list of project paths", ->
diff --git a/services/web/test/unit/coffee/Editor/EditorHttpControllerTests.coffee b/services/web/test/unit/coffee/Editor/EditorHttpControllerTests.coffee
index e05846c5d5..38419e6b46 100644
--- a/services/web/test/unit/coffee/Editor/EditorHttpControllerTests.coffee
+++ b/services/web/test/unit/coffee/Editor/EditorHttpControllerTests.coffee
@@ -331,12 +331,12 @@ describe "EditorHttpController", ->
Project_id: @project_id
entity_id: @entity_id = "entity-id-123"
entity_type: @entity_type = "entity-type"
- @EditorController.deleteEntity = sinon.stub().callsArg(4)
+ @EditorController.deleteEntity = sinon.stub().callsArg(5)
@EditorHttpController.deleteEntity @req, @res
it "should call EditorController.deleteEntity", ->
@EditorController.deleteEntity
- .calledWith(@project_id, @entity_id, @entity_type, "editor")
+ .calledWith(@project_id, @entity_id, @entity_type, "editor", @userId)
.should.equal true
it "should send back a success response", ->
diff --git a/services/web/test/unit/coffee/History/HistoryControllerTests.coffee b/services/web/test/unit/coffee/History/HistoryControllerTests.coffee
index e03189526a..f0669a5902 100644
--- a/services/web/test/unit/coffee/History/HistoryControllerTests.coffee
+++ b/services/web/test/unit/coffee/History/HistoryControllerTests.coffee
@@ -31,7 +31,7 @@ describe "HistoryController", ->
describe "for a project with project history", ->
beforeEach ->
- @ProjectDetailsHandler.getDetails = sinon.stub().callsArgWith(1, null, {overleaf:{history:{display:true}}})
+ @ProjectDetailsHandler.getDetails = sinon.stub().callsArgWith(1, null, {overleaf:{history:{id: 42, display:true}}})
@HistoryController.selectHistoryApi @req, @res, @next
it "should set the flag for project history to true", ->
@@ -57,93 +57,55 @@ describe "HistoryController", ->
on: (event, handler) -> @events[event] = handler
@request.returns @proxy
- describe "with project history enabled", ->
+ describe "for a project with the project history flag", ->
beforeEach ->
- @settings.apis.project_history.enabled = true
+ @req.useProjectHistory = true
+ @HistoryController.proxyToHistoryApi @req, @res, @next
- describe "for a project with the project history flag", ->
- beforeEach ->
- @req.useProjectHistory = true
- @HistoryController.proxyToHistoryApi @req, @res, @next
+ it "should get the user id", ->
+ @AuthenticationController.getLoggedInUserId
+ .calledWith(@req)
+ .should.equal true
- it "should get the user id", ->
- @AuthenticationController.getLoggedInUserId
- .calledWith(@req)
- .should.equal true
+ it "should call the project history api", ->
+ @request
+ .calledWith({
+ url: "#{@settings.apis.project_history.url}#{@req.url}"
+ method: @req.method
+ headers:
+ "X-User-Id": @user_id
+ })
+ .should.equal true
- it "should call the project history api", ->
- @request
- .calledWith({
- url: "#{@settings.apis.project_history.url}#{@req.url}"
- method: @req.method
- headers:
- "X-User-Id": @user_id
- })
- .should.equal true
+ it "should pipe the response to the client", ->
+ @proxy.pipe
+ .calledWith(@res)
+ .should.equal true
- it "should pipe the response to the client", ->
- @proxy.pipe
- .calledWith(@res)
- .should.equal true
-
- describe "for a project without the project history flag", ->
- beforeEach ->
- @req.useProjectHistory = false
- @HistoryController.proxyToHistoryApi @req, @res, @next
-
- it "should get the user id", ->
- @AuthenticationController.getLoggedInUserId
- .calledWith(@req)
- .should.equal true
-
- it "should call the track changes api", ->
- @request
- .calledWith({
- url: "#{@settings.apis.trackchanges.url}#{@req.url}"
- method: @req.method
- headers:
- "X-User-Id": @user_id
- })
- .should.equal true
-
- it "should pipe the response to the client", ->
- @proxy.pipe
- .calledWith(@res)
- .should.equal true
-
- describe "with project history disabled", ->
+ describe "for a project without the project history flag", ->
beforeEach ->
- @settings.apis.project_history.enabled = false
+ @req.useProjectHistory = false
+ @HistoryController.proxyToHistoryApi @req, @res, @next
- describe "for a project with the project history flag", ->
- beforeEach ->
- @req.useProjectHistory = true
- @HistoryController.proxyToHistoryApi @req, @res, @next
+ it "should get the user id", ->
+ @AuthenticationController.getLoggedInUserId
+ .calledWith(@req)
+ .should.equal true
- it "should call the track changes api", ->
- @request
- .calledWith({
- url: "#{@settings.apis.trackchanges.url}#{@req.url}"
- method: @req.method
- headers:
- "X-User-Id": @user_id
- })
- .should.equal true
+ it "should call the track changes api", ->
+ @request
+ .calledWith({
+ url: "#{@settings.apis.trackchanges.url}#{@req.url}"
+ method: @req.method
+ headers:
+ "X-User-Id": @user_id
+ })
+ .should.equal true
- describe "for a project without the project history flag", ->
- beforeEach ->
- @req.useProjectHistory = false
- @HistoryController.proxyToHistoryApi @req, @res, @next
-
- it "should call the track changes api", ->
- @request
- .calledWith({
- url: "#{@settings.apis.trackchanges.url}#{@req.url}"
- method: @req.method
- headers:
- "X-User-Id": @user_id
- })
- .should.equal true
+ it "should pipe the response to the client", ->
+ @proxy.pipe
+ .calledWith(@res)
+ .should.equal true
describe "with an error", ->
beforeEach ->
@@ -152,68 +114,3 @@ describe "HistoryController", ->
it "should pass the error up the call chain", ->
@next.calledWith(@error).should.equal true
-
- describe "initializeProject", ->
- describe "with project history enabled", ->
- beforeEach ->
- @settings.apis.project_history.enabled = true
-
- describe "project history returns a successful response", ->
- beforeEach ->
- @overleaf_id = 1234
- @res = statusCode: 200
- @body = JSON.stringify(project: id: @overleaf_id)
- @request.post = sinon.stub().callsArgWith(1, null, @res, @body)
-
- @HistoryController.initializeProject @callback
-
- it "should call the project history api", ->
- @request.post.calledWith(
- url: "#{@settings.apis.project_history.url}/project"
- ).should.equal true
-
- it "should return the callback with the overleaf id", ->
- @callback.calledWithExactly(null, { @overleaf_id }).should.equal true
-
- describe "project history returns a response without the project id", ->
- beforeEach ->
- @res = statusCode: 200
- @body = JSON.stringify(project: {})
- @request.post = sinon.stub().callsArgWith(1, null, @res, @body)
-
- @HistoryController.initializeProject @callback
-
- it "should return the callback with an error", ->
- @callback
- .calledWith(sinon.match.has("message", "project-history did not provide an id"))
- .should.equal true
-
- describe "project history returns a unsuccessful response", ->
- beforeEach ->
- @res = statusCode: 404
- @request.post = sinon.stub().callsArgWith(1, null, @res)
-
- @HistoryController.initializeProject @callback
-
- it "should return the callback with an error", ->
- @callback
- .calledWith(sinon.match.has("message", "project-history returned a non-success status code: 404"))
- .should.equal true
-
- describe "project history errors", ->
- beforeEach ->
- @error = sinon.stub()
- @request.post = sinon.stub().callsArgWith(1, @error)
-
- @HistoryController.initializeProject @callback
-
- it "should return the callback with the error", ->
- @callback.calledWithExactly(@error).should.equal true
-
- describe "with project history disabled", ->
- beforeEach ->
- @settings.apis.project_history.enabled = false
- @HistoryController.initializeProject @callback
-
- it "should return the callback", ->
- @callback.calledWithExactly().should.equal true
diff --git a/services/web/test/unit/coffee/History/HistoryManagerTests.coffee b/services/web/test/unit/coffee/History/HistoryManagerTests.coffee
new file mode 100644
index 0000000000..9b5f80df04
--- /dev/null
+++ b/services/web/test/unit/coffee/History/HistoryManagerTests.coffee
@@ -0,0 +1,86 @@
+chai = require('chai')
+chai.should()
+sinon = require("sinon")
+modulePath = "../../../../app/js/Features/History/HistoryManager"
+SandboxedModule = require('sandboxed-module')
+
+describe "HistoryManager", ->
+ beforeEach ->
+ @callback = sinon.stub()
+ @user_id = "user-id-123"
+ @AuthenticationController =
+ getLoggedInUserId: sinon.stub().returns(@user_id)
+ @HistoryManager = SandboxedModule.require modulePath, requires:
+ "request" : @request = sinon.stub()
+ "settings-sharelatex": @settings = {}
+ @settings.apis =
+ trackchanges:
+ enabled: false
+ url: "http://trackchanges.example.com"
+ project_history:
+ url: "http://project_history.example.com"
+
+ describe "initializeProject", ->
+ describe "with project history enabled", ->
+ beforeEach ->
+ @settings.apis.project_history.initializeHistoryForNewProjects = true
+
+ describe "project history returns a successful response", ->
+ beforeEach ->
+ @overleaf_id = 1234
+ @res = statusCode: 200
+ @body = JSON.stringify(project: id: @overleaf_id)
+ @request.post = sinon.stub().callsArgWith(1, null, @res, @body)
+
+ @HistoryManager.initializeProject @callback
+
+ it "should call the project history api", ->
+ @request.post.calledWith(
+ url: "#{@settings.apis.project_history.url}/project"
+ ).should.equal true
+
+ it "should return the callback with the overleaf id", ->
+ @callback.calledWithExactly(null, { @overleaf_id }).should.equal true
+
+ describe "project history returns a response without the project id", ->
+ beforeEach ->
+ @res = statusCode: 200
+ @body = JSON.stringify(project: {})
+ @request.post = sinon.stub().callsArgWith(1, null, @res, @body)
+
+ @HistoryManager.initializeProject @callback
+
+ it "should return the callback with an error", ->
+ @callback
+ .calledWith(sinon.match.has("message", "project-history did not provide an id"))
+ .should.equal true
+
+ describe "project history returns a unsuccessful response", ->
+ beforeEach ->
+ @res = statusCode: 404
+ @request.post = sinon.stub().callsArgWith(1, null, @res)
+
+ @HistoryManager.initializeProject @callback
+
+ it "should return the callback with an error", ->
+ @callback
+ .calledWith(sinon.match.has("message", "project-history returned a non-success status code: 404"))
+ .should.equal true
+
+ describe "project history errors", ->
+ beforeEach ->
+ @error = sinon.stub()
+ @request.post = sinon.stub().callsArgWith(1, @error)
+
+ @HistoryManager.initializeProject @callback
+
+ it "should return the callback with the error", ->
+ @callback.calledWithExactly(@error).should.equal true
+
+ describe "with project history disabled", ->
+ beforeEach ->
+ @settings.apis.project_history.initializeHistoryForNewProjects = false
+ @HistoryManager.initializeProject @callback
+
+ it "should return the callback", ->
+ @callback.calledWithExactly().should.equal true
diff --git a/services/web/test/unit/coffee/Project/ProjectCreationHandlerTests.coffee b/services/web/test/unit/coffee/Project/ProjectCreationHandlerTests.coffee
index 470b538bd9..378e909984 100644
--- a/services/web/test/unit/coffee/Project/ProjectCreationHandlerTests.coffee
+++ b/services/web/test/unit/coffee/Project/ProjectCreationHandlerTests.coffee
@@ -38,7 +38,7 @@ describe 'ProjectCreationHandler', ->
setRootDoc: sinon.stub().callsArg(2)
@ProjectDetailsHandler =
validateProjectName: sinon.stub().yields()
- @HistoryController =
+ @HistoryManager =
initializeProject: sinon.stub().callsArg(0)
@user =
@@ -53,7 +53,7 @@ describe 'ProjectCreationHandler', ->
'../../models/User': User:@User
'../../models/Project':{Project:@ProjectModel}
'../../models/Folder':{Folder:@FolderModel}
- '../History/HistoryController': @HistoryController
+ '../History/HistoryManager': @HistoryManager
'./ProjectEntityHandler':@ProjectEntityHandler
"./ProjectDetailsHandler":@ProjectDetailsHandler
"settings-sharelatex": @Settings = {}
@@ -66,7 +66,7 @@ describe 'ProjectCreationHandler', ->
describe 'Creating a Blank project', ->
beforeEach ->
@overleaf_id = 1234
- @HistoryController.initializeProject = sinon.stub().callsArgWith(0, null, { @overleaf_id })
+ @HistoryManager.initializeProject = sinon.stub().callsArgWith(0, null, { @overleaf_id })
@ProjectModel::save = sinon.stub().callsArg(0)
describe "successfully", ->
@@ -83,7 +83,7 @@ describe 'ProjectCreationHandler', ->
it "should initialize the project overleaf if history id not provided", (done)->
@handler.createBlankProject ownerId, projectName, done
- @HistoryController.initializeProject.calledWith().should.equal true
+ @HistoryManager.initializeProject.calledWith().should.equal true
it "should set the overleaf id if overleaf id not provided", (done)->
@handler.createBlankProject ownerId, projectName, (err, project)=>
diff --git a/services/web/test/unit/coffee/Project/ProjectDuplicatorTests.coffee b/services/web/test/unit/coffee/Project/ProjectDuplicatorTests.coffee
index b489014e7e..b7afb79f83 100644
--- a/services/web/test/unit/coffee/Project/ProjectDuplicatorTests.coffee
+++ b/services/web/test/unit/coffee/Project/ProjectDuplicatorTests.coffee
@@ -64,7 +64,7 @@ describe 'ProjectDuplicator', ->
@projectOptionsHandler =
setCompiler : sinon.stub()
@entityHandler =
- addDocWithProject: sinon.stub().callsArgWith(5, null, {name:"somDoc"})
+ addDoc: sinon.stub().callsArgWith(5, null, {name:"somDoc"})
copyFileFromExistingProjectWithProject: sinon.stub().callsArgWith(5)
setRootDoc: sinon.stub()
addFolderWithProject: sinon.stub().callsArgWith(3, null, @newFolder)
@@ -112,13 +112,13 @@ describe 'ProjectDuplicator', ->
done()
it 'should use the same compiler', (done)->
- @entityHandler.addDocWithProject.callsArgWith(5, null, @rootFolder.docs[0], @owner._id)
+ @entityHandler.addDoc.callsArgWith(5, null, @rootFolder.docs[0], @owner._id)
@duplicator.duplicate @owner, @old_project_id, "", (err, newProject)=>
@projectOptionsHandler.setCompiler.calledWith(@stubbedNewProject._id, @project.compiler).should.equal true
done()
it 'should use the same root doc', (done)->
- @entityHandler.addDocWithProject.callsArgWith(5, null, @rootFolder.docs[0], @owner._id)
+ @entityHandler.addDoc.callsArgWith(5, null, @rootFolder.docs[0], @owner._id)
@duplicator.duplicate @owner, @old_project_id, "", (err, newProject)=>
@entityHandler.setRootDoc.calledWith(@stubbedNewProject._id, @rootFolder.docs[0]._id).should.equal true
done()
@@ -139,13 +139,13 @@ describe 'ProjectDuplicator', ->
it 'should copy all the docs', (done)->
@duplicator.duplicate @owner, @old_project_id, "", (err, newProject)=>
@DocstoreManager.getAllDocs.calledWith(@old_project_id).should.equal true
- @entityHandler.addDocWithProject
+ @entityHandler.addDoc
.calledWith(@stubbedNewProject, @stubbedNewProject.rootFolder[0]._id, @doc0.name, @doc0_lines, @owner._id)
.should.equal true
- @entityHandler.addDocWithProject
+ @entityHandler.addDoc
.calledWith(@stubbedNewProject, @newFolder._id, @doc1.name, @doc1_lines, @owner._id)
.should.equal true
- @entityHandler.addDocWithProject
+ @entityHandler.addDoc
.calledWith(@stubbedNewProject, @newFolder._id, @doc2.name, @doc2_lines, @owner._id)
.should.equal true
done()
diff --git a/services/web/test/unit/coffee/Project/ProjectEntityHandlerTests.coffee b/services/web/test/unit/coffee/Project/ProjectEntityHandlerTests.coffee
index f62690e226..a4ac6a7f02 100644
--- a/services/web/test/unit/coffee/Project/ProjectEntityHandlerTests.coffee
+++ b/services/web/test/unit/coffee/Project/ProjectEntityHandlerTests.coffee
@@ -157,13 +157,13 @@ describe 'ProjectEntityHandler', ->
@ProjectGetter.getProject.callsArgWith(2, null, @project)
@tpdsUpdateSender.deleteEntity = sinon.stub().callsArg(1)
@ProjectEntityHandler._removeElementFromMongoArray = sinon.stub().callsArg(3)
- @ProjectEntityHandler._cleanUpEntity = sinon.stub().callsArg(3)
+ @ProjectEntityHandler._cleanUpEntity = sinon.stub().callsArg(5)
@path = mongo: "mongo.path", fileSystem: "/file/system/path"
@projectLocator.findElement = sinon.stub().callsArgWith(1, null, @entity = { _id: entity_id }, @path)
describe "deleting from Mongo", ->
beforeEach (done) ->
- @ProjectEntityHandler.deleteEntity project_id, entity_id, @type = 'file', done
+ @ProjectEntityHandler.deleteEntity project_id, entity_id, @type = 'file', userId, done
it "should retreive the path", ->
@projectLocator.findElement.called.should.equal true
@@ -182,7 +182,7 @@ describe 'ProjectEntityHandler', ->
it "should clean up the entity from the rest of the system", ->
@ProjectEntityHandler._cleanUpEntity
- .calledWith(@project, @entity, @type)
+ .calledWith(@project, @entity, @type, @path.fileSystem, userId)
.should.equal true
describe "_cleanUpEntity", ->
@@ -193,7 +193,9 @@ describe 'ProjectEntityHandler', ->
describe "a file", ->
beforeEach (done) ->
- @ProjectEntityHandler._cleanUpEntity @project, _id: @entity_id, 'file', done
+ @path = "/file/system/path.png"
+ @entity = _id: @entity_id
+ @ProjectEntityHandler._cleanUpEntity @project, @entity, 'file', @path, userId, done
it "should delete the file from FileStoreHandler", ->
@FileStoreHandler.deleteFile.calledWith(project_id, @entity_id).should.equal true
@@ -201,38 +203,56 @@ describe 'ProjectEntityHandler', ->
it "should not attempt to delete from the document updater", ->
@documentUpdaterHandler.deleteDoc.called.should.equal false
+ it "should should send the update to the doc updater", ->
+ oldFiles = [ file: @entity, path: @path ]
+ @documentUpdaterHandler.updateProjectStructure
+ .calledWith(project_id, userId, {oldFiles})
+ .should.equal true
+
describe "a doc", ->
beforeEach (done) ->
- @ProjectEntityHandler._cleanUpDoc = sinon.stub().callsArg(2)
- @ProjectEntityHandler._cleanUpEntity @project, @entity = {_id: @entity_id}, 'doc', done
+ @path = "/file/system/path.tex"
+ @ProjectEntityHandler._cleanUpDoc = sinon.stub().callsArg(4)
+ @entity = {_id: @entity_id}
+ @ProjectEntityHandler._cleanUpEntity @project, @entity, 'doc', @path, userId, done
it "should clean up the doc", ->
@ProjectEntityHandler._cleanUpDoc
- .calledWith(@project, @entity)
+ .calledWith(@project, @entity, @path, userId)
.should.equal true
describe "a folder", ->
beforeEach (done) ->
@folder =
folders: [
- fileRefs: [ @file1 = {_id: "file-id-1" } ]
- docs: [ @doc1 = { _id: "doc-id-1" } ]
+ name: "subfolder"
+ fileRefs: [ @file1 = { _id: "file-id-1", name: "file-name-1"} ]
+ docs: [ @doc1 = { _id: "doc-id-1", name: "doc-name-1" } ]
folders: []
]
- fileRefs: [ @file2 = { _id: "file-id-2" } ]
- docs: [ @doc2 = { _id: "doc-id-2" } ]
+ fileRefs: [ @file2 = { _id: "file-id-2", name: "file-name-2" } ]
+ docs: [ @doc2 = { _id: "doc-id-2", name: "doc-name-2" } ]
- @ProjectEntityHandler._cleanUpDoc = sinon.stub().callsArg(2)
- @ProjectEntityHandler._cleanUpFile = sinon.stub().callsArg(2)
- @ProjectEntityHandler._cleanUpEntity @project, @folder, "folder", done
+ @ProjectEntityHandler._cleanUpDoc = sinon.stub().callsArg(4)
+ @ProjectEntityHandler._cleanUpFile = sinon.stub().callsArg(4)
+ path = "/folder"
+ @ProjectEntityHandler._cleanUpEntity @project, @folder, "folder", path, userId, done
it "should clean up all sub files", ->
- @ProjectEntityHandler._cleanUpFile.calledWith(@project, @file1).should.equal true
- @ProjectEntityHandler._cleanUpFile.calledWith(@project, @file2).should.equal true
+ @ProjectEntityHandler._cleanUpFile
+ .calledWith(@project, @file1, "/folder/subfolder/file-name-1", userId)
+ .should.equal true
+ @ProjectEntityHandler._cleanUpFile
+ .calledWith(@project, @file2, "/folder/file-name-2", userId)
+ .should.equal true
it "should clean up all sub docs", ->
- @ProjectEntityHandler._cleanUpDoc.calledWith(@project, @doc1).should.equal true
- @ProjectEntityHandler._cleanUpDoc.calledWith(@project, @doc2).should.equal true
+ @ProjectEntityHandler._cleanUpDoc
+ .calledWith(@project, @doc1, "/folder/subfolder/doc-name-1", userId)
+ .should.equal true
+ @ProjectEntityHandler._cleanUpDoc
+ .calledWith(@project, @doc2, "/folder/doc-name-2", userId)
+ .should.equal true
describe 'moveEntity', ->
beforeEach ->
@@ -496,6 +516,51 @@ describe 'ProjectEntityHandler', ->
.calledWith(project_id, userId, {newDocs})
.should.equal true
+ describe 'addDocWithoutUpdatingHistory', ->
+ beforeEach ->
+ @name = "some new doc"
+ @lines = ['1234','abc']
+ @path = "/path/to/doc"
+
+ @ProjectEntityHandler._putElement = sinon.stub().callsArgWith(4, null, {path:{fileSystem:@path}})
+ @callback = sinon.stub()
+ @tpdsUpdateSender.addDoc = sinon.stub().callsArg(1)
+ @DocstoreManager.updateDoc = sinon.stub().yields(null, true, 0)
+
+ @ProjectEntityHandler.addDocWithoutUpdatingHistory project_id, folder_id, @name, @lines, userId, @callback
+
+ # Created doc
+ @doc = @ProjectEntityHandler._putElement.args[0][2]
+ @doc.name.should.equal @name
+ expect(@doc.lines).to.be.undefined
+
+ it 'should call put element', ->
+ @ProjectEntityHandler._putElement.calledWith(@project, folder_id, @doc).should.equal true
+
+ it 'should return doc and parent folder', ->
+ @callback.calledWith(null, @doc, folder_id).should.equal true
+
+ it 'should call third party data store', ->
+ @tpdsUpdateSender.addDoc
+ .calledWith({
+ project_id: project_id
+ doc_id: doc_id
+ path: @path
+ project_name: @project.name
+ rev: 0
+ })
+ .should.equal true
+
+ it "should send the doc lines to the doc store", ->
+ @DocstoreManager.updateDoc
+ .calledWith(project_id, @doc._id.toString(), @lines)
+ .should.equal true
+
+ it "should not should send the change in project structure to the doc updater", () ->
+ @documentUpdaterHandler.updateProjectStructure
+ .called
+ .should.equal false
+
describe "restoreDoc", ->
beforeEach ->
@name = "doc-name"
@@ -584,6 +649,12 @@ describe 'ProjectEntityHandler', ->
@ProjectEntityHandler.addFile project_id, folder_id, fileName, {}, userId, () ->
+ it "should not send the change in project structure to the doc updater when called as addFileWithoutUpdatingHistory", (done) ->
+ @documentUpdaterHandler.updateProjectStructure = sinon.stub().yields()
+ @ProjectEntityHandler.addFileWithoutUpdatingHistory project_id, folder_id, fileName, {}, userId, () =>
+ @documentUpdaterHandler.updateProjectStructure.called.should.equal false
+ done()
+
describe 'replaceFile', ->
beforeEach ->
@projectLocator
@@ -1116,6 +1187,7 @@ describe 'ProjectEntityHandler', ->
@doc =
_id: ObjectId()
name: "test.tex"
+ @path = "/path/to/doc"
@ProjectEntityHandler.unsetRootDoc = sinon.stub().callsArg(1)
@ProjectEntityHandler._insertDeletedDocReference = sinon.stub().callsArg(2)
@documentUpdaterHandler.deleteDoc = sinon.stub().callsArg(2)
@@ -1125,7 +1197,7 @@ describe 'ProjectEntityHandler', ->
describe "when the doc is the root doc", ->
beforeEach ->
@project.rootDoc_id = @doc._id
- @ProjectEntityHandler._cleanUpDoc @project, @doc, @callback
+ @ProjectEntityHandler._cleanUpDoc @project, @doc, @path, userId, @callback
it "should unset the root doc", ->
@ProjectEntityHandler.unsetRootDoc
@@ -1146,13 +1218,19 @@ describe 'ProjectEntityHandler', ->
.calledWith(project_id, @doc._id.toString())
.should.equal true
+ it "should should send the update to the doc updater", ->
+ oldDocs = [ doc: @doc, path: @path ]
+ @documentUpdaterHandler.updateProjectStructure
+ .calledWith(project_id, userId, {oldDocs})
+ .should.equal true
+
it "should call the callback", ->
@callback.called.should.equal true
describe "when the doc is not the root doc", ->
beforeEach ->
@project.rootDoc_id = ObjectId()
- @ProjectEntityHandler._cleanUpDoc @project, @doc, @callback
+ @ProjectEntityHandler._cleanUpDoc @project, @doc, @path, userId, @callback
it "should not unset the root doc", ->
@ProjectEntityHandler.unsetRootDoc.called.should.equal false
diff --git a/services/web/test/unit/coffee/Subscription/SubscriptionUpdaterTests.coffee b/services/web/test/unit/coffee/Subscription/SubscriptionUpdaterTests.coffee
index 87ff474a0a..93479a03a1 100644
--- a/services/web/test/unit/coffee/Subscription/SubscriptionUpdaterTests.coffee
+++ b/services/web/test/unit/coffee/Subscription/SubscriptionUpdaterTests.coffee
@@ -57,6 +57,7 @@ describe "SubscriptionUpdater", ->
@ReferalAllocator = assignBonus:sinon.stub().callsArgWith(1)
@ReferalAllocator.cock = true
+ @Modules = {hooks: {fire: sinon.stub().callsArgWith(2, null, null)}}
@SubscriptionUpdater = SandboxedModule.require modulePath, requires:
'../../models/Subscription': Subscription:@SubscriptionModel
'./UserFeaturesUpdater': @UserFeaturesUpdater
@@ -65,6 +66,7 @@ describe "SubscriptionUpdater", ->
"logger-sharelatex": log:->
'settings-sharelatex': @Settings
"../Referal/ReferalAllocator" : @ReferalAllocator
+ '../../infrastructure/Modules': @Modules
describe "syncSubscription", ->
@@ -204,10 +206,22 @@ describe "SubscriptionUpdater", ->
assert.equal args[1], @groupSubscription.planCode
done()
+ it "should call updateFeatures with the overleaf subscription if set", (done)->
+ @SubscriptionLocator.getUsersSubscription.callsArgWith(1, null)
+ @SubscriptionLocator.getGroupSubscriptionMemberOf.callsArgWith(1, null, null)
+ @Modules.hooks.fire = sinon.stub().callsArgWith(2, null, ['ol_pro'])
+
+ @SubscriptionUpdater._setUsersMinimumFeatures @adminUser._id, (err)=>
+ args = @UserFeaturesUpdater.updateFeatures.args[0]
+ assert.equal args[0], @adminUser._id
+ assert.equal args[1], 'ol_pro'
+ done()
+
it "should call not call updateFeatures with users subscription if the subscription plan code is the default one (downgraded)", (done)->
@subscription.planCode = @Settings.defaultPlanCode
@SubscriptionLocator.getUsersSubscription.callsArgWith(1, null, @subscription)
@SubscriptionLocator.getGroupSubscriptionMemberOf.callsArgWith(1, null, @groupSubscription)
+ @Modules.hooks.fire = sinon.stub().callsArgWith(2, null, null)
@SubscriptionUpdater._setUsersMinimumFeatures @adminuser_id, (err)=>
args = @UserFeaturesUpdater.updateFeatures.args[0]
assert.equal args[0], @adminUser._id
@@ -218,6 +232,7 @@ describe "SubscriptionUpdater", ->
it "should call updateFeatures with default if there are no subscriptions for user", (done)->
@SubscriptionLocator.getUsersSubscription.callsArgWith(1, null)
@SubscriptionLocator.getGroupSubscriptionMemberOf.callsArgWith(1, null)
+ @Modules.hooks.fire = sinon.stub().callsArgWith(2, null, null)
@SubscriptionUpdater._setUsersMinimumFeatures @adminuser_id, (err)=>
args = @UserFeaturesUpdater.updateFeatures.args[0]
assert.equal args[0], @adminUser._id
@@ -263,3 +278,13 @@ describe "SubscriptionUpdater", ->
@SubscriptionUpdater._setUsersMinimumFeatures
.calledWith(user_id)
.should.equal true
+
+ describe 'refreshSubscription', ->
+ beforeEach ->
+ @SubscriptionUpdater._setUsersMinimumFeatures = sinon.stub()
+ .callsArgWith(1, null)
+
+ it 'should call to _setUsersMinimumFeatures', ->
+ @SubscriptionUpdater.refreshSubscription(@adminUser._id, ()->)
+ @SubscriptionUpdater._setUsersMinimumFeatures.callCount.should.equal 1
+ @SubscriptionUpdater._setUsersMinimumFeatures.calledWith(@adminUser._id).should.equal true
diff --git a/services/web/test/unit/coffee/ThirdPartyDataStore/TpdsUpdateHandlerTests.coffee b/services/web/test/unit/coffee/ThirdPartyDataStore/TpdsUpdateHandlerTests.coffee
index dedd3ea7c8..3984d6341f 100644
--- a/services/web/test/unit/coffee/ThirdPartyDataStore/TpdsUpdateHandlerTests.coffee
+++ b/services/web/test/unit/coffee/ThirdPartyDataStore/TpdsUpdateHandlerTests.coffee
@@ -8,7 +8,7 @@ describe 'TpdsUpdateHandler', ->
beforeEach ->
@requestQueuer = {}
@updateMerger =
- deleteUpdate: (user_id, path, source, cb)->cb()
+ deleteUpdate: (user_id, project_id, path, source, cb)->cb()
mergeUpdate:(user_id, project_id, path, update, source, cb)->cb()
@editorController = {}
@project_id = "dsjajilknaksdn"
@@ -107,11 +107,13 @@ describe 'TpdsUpdateHandler', ->
it 'should call deleteEntity in the collaberation manager', (done)->
path = "/delete/this"
update = {}
- @updateMerger.deleteUpdate = sinon.stub().callsArg(3)
+ @updateMerger.deleteUpdate = sinon.stub().callsArg(4)
@handler.deleteUpdate @user_id, @project.name, path, @source, =>
@projectDeleter.markAsDeletedByExternalSource.calledWith(@project._id).should.equal false
- @updateMerger.deleteUpdate.calledWith(@project_id, path, @source).should.equal true
+ @updateMerger.deleteUpdate
+ .calledWith(@user_id, @project_id, path, @source)
+ .should.equal true
done()
it 'should mark the project as deleted by external source if path is a single slash', (done)->
diff --git a/services/web/test/unit/coffee/ThirdPartyDataStore/UpdateMergerTests.coffee b/services/web/test/unit/coffee/ThirdPartyDataStore/UpdateMergerTests.coffee
index e20419765f..cb2aa059ea 100644
--- a/services/web/test/unit/coffee/ThirdPartyDataStore/UpdateMergerTests.coffee
+++ b/services/web/test/unit/coffee/ThirdPartyDataStore/UpdateMergerTests.coffee
@@ -145,13 +145,13 @@ describe 'UpdateMerger :', ->
it 'should get the element id', ->
@projectLocator.findElementByPath = sinon.spy()
- @updateMerger.deleteUpdate @project_id, @path, @source, ->
+ @updateMerger.deleteUpdate @user_id, @project_id, @path, @source, ->
@projectLocator.findElementByPath.calledWith(@project_id, @path).should.equal true
it 'should delete the entity in the editor controller with the correct type', (done)->
@entity.lines = []
- mock = sinon.mock(@editorController).expects("deleteEntity").withArgs(@project_id, @entity_id, @type, @source).callsArg(4)
- @updateMerger.deleteUpdate @project_id, @path, @source, ->
+ mock = sinon.mock(@editorController).expects("deleteEntity").withArgs(@project_id, @entity_id, @type, @source, @user_id).callsArg(5)
+ @updateMerger.deleteUpdate @user_id, @project_id, @path, @source, ->
mock.verify()
done()
diff --git a/services/web/test/unit_frontend/coffee/HistoryManagerV2Tests.coffee b/services/web/test/unit_frontend/coffee/HistoryManagerV2Tests.coffee
new file mode 100644
index 0000000000..9ad7ba9a2e
--- /dev/null
+++ b/services/web/test/unit_frontend/coffee/HistoryManagerV2Tests.coffee
@@ -0,0 +1,134 @@
+Path = require 'path'
+SandboxedModule = require "sandboxed-module"
+modulePath = Path.join __dirname, '../../../public/js/ide/history/HistoryV2Manager'
+sinon = require("sinon")
+expect = require("chai").expect
+
+describe "HistoryV2Manager", ->
+ beforeEach ->
+ @moment = {}
+ @ColorManager = {}
+ SandboxedModule.require modulePath, globals:
+ "define": (dependencies, builder) =>
+ @HistoryV2Manager = builder(@moment, @ColorManager)
+
+ @scope =
+ $watch: sinon.stub()
+ $on: sinon.stub()
+ @ide = {}
+
+ @historyManager = new @HistoryV2Manager(@ide, @scope)
+
+ it "should setup the history scope on intialization", ->
+ expect(@scope.history).to.deep.equal({
+ isV2: true
+ updates: []
+ nextBeforeTimestamp: null
+ atEnd: false
+ selection: {
+ updates: []
+ pathname: null
+ range: {
+ fromV: null
+ toV: null
+ }
+ }
+ diff: null
+ })
+
+ describe "_perDocSummaryOfUpdates", ->
+ it "should return the range of updates for the docs", ->
+ result = @historyManager._perDocSummaryOfUpdates([{
+ pathnames: ["main.tex"]
+ fromV: 7, toV: 9
+ },{
+ pathnames: ["main.tex", "foo.tex"]
+ fromV: 4, toV: 6
+ },{
+ pathnames: ["main.tex"]
+ fromV: 3, toV: 3
+ },{
+ pathnames: ["foo.tex"]
+ fromV: 0, toV: 2
+ }])
+
+ expect(result).to.deep.equal({
+ "main.tex": { fromV: 3, toV: 9 },
+ "foo.tex": { fromV: 0, toV: 6 }
+ })
+
+ it "should track renames", ->
+ result = @historyManager._perDocSummaryOfUpdates([{
+ pathnames: ["main2.tex"]
+ fromV: 5, toV: 9
+ },{
+ project_ops: [{
+ rename: {
+ pathname: "main1.tex",
+ newPathname: "main2.tex"
+ }
+ }],
+ fromV: 4, toV: 4
+ },{
+ pathnames: ["main1.tex"]
+ fromV: 3, toV: 3
+ },{
+ project_ops: [{
+ rename: {
+ pathname: "main0.tex",
+ newPathname: "main1.tex"
+ }
+ }],
+ fromV: 2, toV: 2
+ },{
+ pathnames: ["main0.tex"]
+ fromV: 0, toV: 1
+ }])
+
+ expect(result).to.deep.equal({
+ "main0.tex": { fromV: 0, toV: 9 }
+ })
+
+ it "should track single renames", ->
+ result = @historyManager._perDocSummaryOfUpdates([{
+ project_ops: [{
+ rename: {
+ pathname: "main1.tex",
+ newPathname: "main2.tex"
+ }
+ }],
+ fromV: 4, toV: 5
+ }])
+
+ expect(result).to.deep.equal({
+ "main1.tex": { fromV: 4, toV: 5 }
+ })
+
+ it "should track additions", ->
+ result = @historyManager._perDocSummaryOfUpdates([{
+ project_ops: [{
+ add:
+ pathname: "main.tex"
+ }]
+ fromV: 0, toV: 1
+ }, {
+ pathnames: ["main.tex"]
+ fromV: 1, toV: 4
+ }])
+
+ expect(result).to.deep.equal({
+ "main.tex": { fromV: 0, toV: 4 }
+ })
+
+ it "should track single additions", ->
+ result = @historyManager._perDocSummaryOfUpdates([{
+ project_ops: [{
+ add:
+ pathname: "main.tex"
+ }]
+ fromV: 0, toV: 1
+ }])
+
+ expect(result).to.deep.equal({
+ "main.tex": { fromV: 0, toV: 1 }
+ })