updateProjectTests.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403
  1. const sinon = require('sinon')
  2. const modulePath = '../../../../app/js/ProjectManager.js'
  3. const SandboxedModule = require('sandboxed-module')
  4. const _ = require('lodash')
  5. describe('ProjectManager', function () {
  6. beforeEach(function () {
  7. this.RedisManager = {}
  8. this.ProjectHistoryRedisManager = {
  9. queueRenameEntity: sinon.stub().yields(),
  10. queueAddEntity: sinon.stub().yields()
  11. }
  12. this.DocumentManager = {
  13. renameDocWithLock: sinon.stub().yields()
  14. }
  15. this.HistoryManager = {
  16. flushProjectChangesAsync: sinon.stub(),
  17. shouldFlushHistoryOps: sinon.stub().returns(false)
  18. }
  19. this.Metrics = {
  20. Timer: class Timer {}
  21. }
  22. this.Metrics.Timer.prototype.done = sinon.stub()
  23. this.logger = {
  24. log: sinon.stub(),
  25. error: sinon.stub()
  26. }
  27. this.ProjectManager = SandboxedModule.require(modulePath, {
  28. requires: {
  29. './RedisManager': this.RedisManager,
  30. './ProjectHistoryRedisManager': this.ProjectHistoryRedisManager,
  31. './DocumentManager': this.DocumentManager,
  32. 'logger-sharelatex': this.logger,
  33. './HistoryManager': this.HistoryManager,
  34. './Metrics': this.Metrics
  35. }
  36. })
  37. this.project_id = 'project-id-123'
  38. this.projectHistoryId = 'history-id-123'
  39. this.user_id = 'user-id-123'
  40. this.version = 1234567
  41. this.callback = sinon.stub()
  42. })
  43. describe('updateProjectWithLocks', function () {
  44. describe('rename operations', function () {
  45. beforeEach(function () {
  46. this.firstDocUpdate = {
  47. type: 'rename-doc',
  48. id: 1,
  49. pathname: 'foo',
  50. newPathname: 'foo'
  51. }
  52. this.secondDocUpdate = {
  53. type: 'rename-doc',
  54. id: 2,
  55. pathname: 'bar',
  56. newPathname: 'bar2'
  57. }
  58. this.firstFileUpdate = {
  59. type: 'rename-file',
  60. id: 2,
  61. pathname: 'bar',
  62. newPathname: 'bar2'
  63. }
  64. this.updates = [
  65. this.firstDocUpdate,
  66. this.secondDocUpdate,
  67. this.firstFileUpdate
  68. ]
  69. })
  70. describe('successfully', function () {
  71. beforeEach(function () {
  72. this.ProjectManager.updateProjectWithLocks(
  73. this.project_id,
  74. this.projectHistoryId,
  75. this.user_id,
  76. this.updates,
  77. this.version,
  78. this.callback
  79. )
  80. })
  81. it('should rename the docs in the updates', function () {
  82. const firstDocUpdateWithVersion = _.extend({}, this.firstDocUpdate, {
  83. version: `${this.version}.0`
  84. })
  85. const secondDocUpdateWithVersion = _.extend(
  86. {},
  87. this.secondDocUpdate,
  88. { version: `${this.version}.1` }
  89. )
  90. this.DocumentManager.renameDocWithLock
  91. .calledWith(
  92. this.project_id,
  93. this.firstDocUpdate.id,
  94. this.user_id,
  95. firstDocUpdateWithVersion,
  96. this.projectHistoryId
  97. )
  98. .should.equal(true)
  99. this.DocumentManager.renameDocWithLock
  100. .calledWith(
  101. this.project_id,
  102. this.secondDocUpdate.id,
  103. this.user_id,
  104. secondDocUpdateWithVersion,
  105. this.projectHistoryId
  106. )
  107. .should.equal(true)
  108. })
  109. it('should rename the files in the updates', function () {
  110. const firstFileUpdateWithVersion = _.extend(
  111. {},
  112. this.firstFileUpdate,
  113. { version: `${this.version}.2` }
  114. )
  115. this.ProjectHistoryRedisManager.queueRenameEntity
  116. .calledWith(
  117. this.project_id,
  118. this.projectHistoryId,
  119. 'file',
  120. this.firstFileUpdate.id,
  121. this.user_id,
  122. firstFileUpdateWithVersion
  123. )
  124. .should.equal(true)
  125. })
  126. it('should not flush the history', function () {
  127. this.HistoryManager.flushProjectChangesAsync
  128. .calledWith(this.project_id)
  129. .should.equal(false)
  130. })
  131. it('should call the callback', function () {
  132. this.callback.called.should.equal(true)
  133. })
  134. })
  135. describe('when renaming a doc fails', function () {
  136. beforeEach(function () {
  137. this.error = new Error('error')
  138. this.DocumentManager.renameDocWithLock.yields(this.error)
  139. this.ProjectManager.updateProjectWithLocks(
  140. this.project_id,
  141. this.projectHistoryId,
  142. this.user_id,
  143. this.updates,
  144. this.version,
  145. this.callback
  146. )
  147. })
  148. it('should call the callback with the error', function () {
  149. this.callback.calledWith(this.error).should.equal(true)
  150. })
  151. })
  152. describe('when renaming a file fails', function () {
  153. beforeEach(function () {
  154. this.error = new Error('error')
  155. this.ProjectHistoryRedisManager.queueRenameEntity.yields(this.error)
  156. this.ProjectManager.updateProjectWithLocks(
  157. this.project_id,
  158. this.projectHistoryId,
  159. this.user_id,
  160. this.updates,
  161. this.version,
  162. this.callback
  163. )
  164. })
  165. it('should call the callback with the error', function () {
  166. this.callback.calledWith(this.error).should.equal(true)
  167. })
  168. })
  169. describe('with enough ops to flush', function () {
  170. beforeEach(function () {
  171. this.HistoryManager.shouldFlushHistoryOps.returns(true)
  172. this.ProjectManager.updateProjectWithLocks(
  173. this.project_id,
  174. this.projectHistoryId,
  175. this.user_id,
  176. this.updates,
  177. this.version,
  178. this.callback
  179. )
  180. })
  181. it('should flush the history', function () {
  182. this.HistoryManager.flushProjectChangesAsync
  183. .calledWith(this.project_id)
  184. .should.equal(true)
  185. })
  186. })
  187. })
  188. describe('add operations', function () {
  189. beforeEach(function () {
  190. this.firstDocUpdate = {
  191. type: 'add-doc',
  192. id: 1,
  193. docLines: 'a\nb'
  194. }
  195. this.secondDocUpdate = {
  196. type: 'add-doc',
  197. id: 2,
  198. docLines: 'a\nb'
  199. }
  200. this.firstFileUpdate = {
  201. type: 'add-file',
  202. id: 3,
  203. url: 'filestore.example.com/2'
  204. }
  205. this.secondFileUpdate = {
  206. type: 'add-file',
  207. id: 4,
  208. url: 'filestore.example.com/3'
  209. }
  210. this.updates = [
  211. this.firstDocUpdate,
  212. this.secondDocUpdate,
  213. this.firstFileUpdate,
  214. this.secondFileUpdate
  215. ]
  216. })
  217. describe('successfully', function () {
  218. beforeEach(function () {
  219. this.ProjectManager.updateProjectWithLocks(
  220. this.project_id,
  221. this.projectHistoryId,
  222. this.user_id,
  223. this.updates,
  224. this.version,
  225. this.callback
  226. )
  227. })
  228. it('should add the docs in the updates', function () {
  229. const firstDocUpdateWithVersion = _.extend({}, this.firstDocUpdate, {
  230. version: `${this.version}.0`
  231. })
  232. const secondDocUpdateWithVersion = _.extend(
  233. {},
  234. this.secondDocUpdate,
  235. { version: `${this.version}.1` }
  236. )
  237. this.ProjectHistoryRedisManager.queueAddEntity
  238. .getCall(0)
  239. .calledWith(
  240. this.project_id,
  241. this.projectHistoryId,
  242. 'doc',
  243. this.firstDocUpdate.id,
  244. this.user_id,
  245. firstDocUpdateWithVersion
  246. )
  247. .should.equal(true)
  248. this.ProjectHistoryRedisManager.queueAddEntity
  249. .getCall(1)
  250. .calledWith(
  251. this.project_id,
  252. this.projectHistoryId,
  253. 'doc',
  254. this.secondDocUpdate.id,
  255. this.user_id,
  256. secondDocUpdateWithVersion
  257. )
  258. .should.equal(true)
  259. })
  260. it('should add the files in the updates', function () {
  261. const firstFileUpdateWithVersion = _.extend(
  262. {},
  263. this.firstFileUpdate,
  264. { version: `${this.version}.2` }
  265. )
  266. const secondFileUpdateWithVersion = _.extend(
  267. {},
  268. this.secondFileUpdate,
  269. { version: `${this.version}.3` }
  270. )
  271. this.ProjectHistoryRedisManager.queueAddEntity
  272. .getCall(2)
  273. .calledWith(
  274. this.project_id,
  275. this.projectHistoryId,
  276. 'file',
  277. this.firstFileUpdate.id,
  278. this.user_id,
  279. firstFileUpdateWithVersion
  280. )
  281. .should.equal(true)
  282. this.ProjectHistoryRedisManager.queueAddEntity
  283. .getCall(3)
  284. .calledWith(
  285. this.project_id,
  286. this.projectHistoryId,
  287. 'file',
  288. this.secondFileUpdate.id,
  289. this.user_id,
  290. secondFileUpdateWithVersion
  291. )
  292. .should.equal(true)
  293. })
  294. it('should not flush the history', function () {
  295. this.HistoryManager.flushProjectChangesAsync
  296. .calledWith(this.project_id)
  297. .should.equal(false)
  298. })
  299. it('should call the callback', function () {
  300. this.callback.called.should.equal(true)
  301. })
  302. })
  303. describe('when adding a doc fails', function () {
  304. beforeEach(function () {
  305. this.error = new Error('error')
  306. this.ProjectHistoryRedisManager.queueAddEntity.yields(this.error)
  307. this.ProjectManager.updateProjectWithLocks(
  308. this.project_id,
  309. this.projectHistoryId,
  310. this.user_id,
  311. this.updates,
  312. this.version,
  313. this.callback
  314. )
  315. })
  316. it('should call the callback with the error', function () {
  317. this.callback.calledWith(this.error).should.equal(true)
  318. })
  319. })
  320. describe('when adding a file fails', function () {
  321. beforeEach(function () {
  322. this.error = new Error('error')
  323. this.ProjectHistoryRedisManager.queueAddEntity.yields(this.error)
  324. this.ProjectManager.updateProjectWithLocks(
  325. this.project_id,
  326. this.projectHistoryId,
  327. this.user_id,
  328. this.updates,
  329. this.version,
  330. this.callback
  331. )
  332. })
  333. it('should call the callback with the error', function () {
  334. this.callback.calledWith(this.error).should.equal(true)
  335. })
  336. })
  337. describe('with enough ops to flush', function () {
  338. beforeEach(function () {
  339. this.HistoryManager.shouldFlushHistoryOps.returns(true)
  340. this.ProjectManager.updateProjectWithLocks(
  341. this.project_id,
  342. this.projectHistoryId,
  343. this.user_id,
  344. this.updates,
  345. this.version,
  346. this.callback
  347. )
  348. })
  349. it('should flush the history', function () {
  350. this.HistoryManager.flushProjectChangesAsync
  351. .calledWith(this.project_id)
  352. .should.equal(true)
  353. })
  354. })
  355. })
  356. describe('when given an unknown operation type', function () {
  357. beforeEach(function () {
  358. this.updates = [{ type: 'brew-coffee' }]
  359. this.ProjectManager.updateProjectWithLocks(
  360. this.project_id,
  361. this.projectHistoryId,
  362. this.user_id,
  363. this.updates,
  364. this.version,
  365. this.callback
  366. )
  367. })
  368. it('should call back with an error', function () {
  369. this.callback.calledWith(sinon.match.instanceOf(Error)).should.be.true
  370. })
  371. })
  372. })
  373. })