HistoryManagerTests.js 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306
  1. /* eslint-disable
  2. mocha/no-nested-tests,
  3. */
  4. const SandboxedModule = require('sandboxed-module')
  5. const sinon = require('sinon')
  6. const modulePath = require('node:path').join(
  7. __dirname,
  8. '../../../../app/js/HistoryManager'
  9. )
  10. describe('HistoryManager', function () {
  11. beforeEach(function () {
  12. this.HistoryManager = SandboxedModule.require(modulePath, {
  13. requires: {
  14. request: (this.request = {}),
  15. '@overleaf/settings': (this.Settings = {
  16. shortHistoryQueues: [],
  17. apis: {
  18. project_history: {
  19. url: 'http://project_history.example.com',
  20. },
  21. },
  22. }),
  23. './DocumentManager': (this.DocumentManager = {}),
  24. './RedisManager': (this.RedisManager = {}),
  25. './ProjectHistoryRedisManager': (this.ProjectHistoryRedisManager = {}),
  26. './Metrics': (this.metrics = { inc: sinon.stub() }),
  27. },
  28. })
  29. this.project_id = 'mock-project-id'
  30. this.callback = sinon.stub()
  31. })
  32. describe('flushProjectChangesAsync', function () {
  33. beforeEach(function () {
  34. this.request.post = sinon
  35. .stub()
  36. .callsArgWith(1, null, { statusCode: 204 })
  37. this.HistoryManager.flushProjectChangesAsync(this.project_id)
  38. })
  39. it('should send a request to the project history api', function () {
  40. this.request.post
  41. .calledWith({
  42. url: `${this.Settings.apis.project_history.url}/project/${this.project_id}/flush`,
  43. qs: { background: true },
  44. })
  45. .should.equal(true)
  46. })
  47. })
  48. describe('flushProjectChanges', function () {
  49. describe('in the normal case', function () {
  50. beforeEach(function (done) {
  51. this.request.post = sinon
  52. .stub()
  53. .callsArgWith(1, null, { statusCode: 204 })
  54. this.HistoryManager.flushProjectChanges(
  55. this.project_id,
  56. {
  57. background: true,
  58. },
  59. done
  60. )
  61. })
  62. it('should send a request to the project history api', function () {
  63. this.request.post
  64. .calledWith({
  65. url: `${this.Settings.apis.project_history.url}/project/${this.project_id}/flush`,
  66. qs: { background: true },
  67. })
  68. .should.equal(true)
  69. })
  70. })
  71. describe('with the skip_history_flush option', function () {
  72. beforeEach(function (done) {
  73. this.request.post = sinon.stub()
  74. this.HistoryManager.flushProjectChanges(
  75. this.project_id,
  76. {
  77. skip_history_flush: true,
  78. },
  79. done
  80. )
  81. })
  82. it('should not send a request to the project history api', function () {
  83. this.request.post.called.should.equal(false)
  84. })
  85. })
  86. })
  87. describe('recordAndFlushHistoryOps', function () {
  88. beforeEach(function () {
  89. this.ops = ['mock-ops']
  90. this.project_ops_length = 10
  91. this.HistoryManager.flushProjectChangesAsync = sinon.stub()
  92. })
  93. describe('with no ops', function () {
  94. beforeEach(function () {
  95. this.HistoryManager.recordAndFlushHistoryOps(
  96. this.project_id,
  97. [],
  98. this.project_ops_length
  99. )
  100. })
  101. it('should not flush project changes', function () {
  102. this.HistoryManager.flushProjectChangesAsync.called.should.equal(false)
  103. })
  104. })
  105. describe('with enough ops to flush project changes', function () {
  106. beforeEach(function () {
  107. this.HistoryManager.shouldFlushHistoryOps = sinon.stub()
  108. this.HistoryManager.shouldFlushHistoryOps
  109. .withArgs(this.project_id, this.project_ops_length)
  110. .returns(true)
  111. this.HistoryManager.recordAndFlushHistoryOps(
  112. this.project_id,
  113. this.ops,
  114. this.project_ops_length
  115. )
  116. })
  117. it('should flush project changes', function () {
  118. this.HistoryManager.flushProjectChangesAsync
  119. .calledWith(this.project_id)
  120. .should.equal(true)
  121. })
  122. })
  123. describe('with enough ops to flush doc changes', function () {
  124. beforeEach(function () {
  125. this.HistoryManager.shouldFlushHistoryOps = sinon.stub()
  126. this.HistoryManager.shouldFlushHistoryOps
  127. .withArgs(this.project_id, this.project_ops_length)
  128. .returns(false)
  129. this.HistoryManager.recordAndFlushHistoryOps(
  130. this.project_id,
  131. this.ops,
  132. this.project_ops_length
  133. )
  134. })
  135. it('should not flush project changes', function () {
  136. this.HistoryManager.flushProjectChangesAsync.called.should.equal(false)
  137. })
  138. })
  139. describe('shouldFlushHistoryOps', function () {
  140. it('should return false if the number of ops is not known', function () {
  141. this.HistoryManager.shouldFlushHistoryOps(
  142. this.project_id,
  143. null,
  144. ['a', 'b', 'c'].length,
  145. 1
  146. ).should.equal(false)
  147. })
  148. it("should return false if the updates didn't take us past the threshold", function () {
  149. // Currently there are 14 ops
  150. // Previously we were on 11 ops
  151. // We didn't pass over a multiple of 5
  152. this.HistoryManager.shouldFlushHistoryOps(
  153. this.project_id,
  154. 14,
  155. ['a', 'b', 'c'].length,
  156. 5
  157. ).should.equal(false)
  158. it('should return true if the updates took to the threshold', function () {})
  159. // Currently there are 15 ops
  160. // Previously we were on 12 ops
  161. // We've reached a new multiple of 5
  162. this.HistoryManager.shouldFlushHistoryOps(
  163. this.project_id,
  164. 15,
  165. ['a', 'b', 'c'].length,
  166. 5
  167. ).should.equal(true)
  168. })
  169. it('should return true if the updates took past the threshold', function () {
  170. // Currently there are 19 ops
  171. // Previously we were on 16 ops
  172. // We didn't pass over a multiple of 5
  173. this.HistoryManager.shouldFlushHistoryOps(
  174. this.project_id,
  175. 17,
  176. ['a', 'b', 'c'].length,
  177. 5
  178. ).should.equal(true)
  179. })
  180. it('should return true if the project has a short queue', function () {
  181. this.Settings.shortHistoryQueues = [this.project_id]
  182. this.HistoryManager.shouldFlushHistoryOps(
  183. this.project_id,
  184. 14,
  185. ['a', 'b', 'c'].length,
  186. 5
  187. ).should.equal(true)
  188. })
  189. })
  190. })
  191. describe('resyncProjectHistory', function () {
  192. beforeEach(function () {
  193. this.projectHistoryId = 'history-id-1234'
  194. this.docs = [
  195. {
  196. doc: this.doc_id,
  197. path: 'main.tex',
  198. },
  199. ]
  200. this.files = [
  201. {
  202. file: 'mock-file-id',
  203. path: 'universe.png',
  204. url: `www.filestore.test/${this.project_id}/mock-file-id`,
  205. },
  206. ]
  207. this.ProjectHistoryRedisManager.queueResyncProjectStructure = sinon
  208. .stub()
  209. .yields()
  210. this.DocumentManager.resyncDocContentsWithLock = sinon.stub().yields()
  211. })
  212. describe('full sync', function () {
  213. beforeEach(function () {
  214. this.HistoryManager.resyncProjectHistory(
  215. this.project_id,
  216. this.projectHistoryId,
  217. this.docs,
  218. this.files,
  219. {},
  220. this.callback
  221. )
  222. })
  223. it('should queue a project structure reync', function () {
  224. this.ProjectHistoryRedisManager.queueResyncProjectStructure
  225. .calledWith(
  226. this.project_id,
  227. this.projectHistoryId,
  228. this.docs,
  229. this.files
  230. )
  231. .should.equal(true)
  232. })
  233. it('should queue doc content reyncs', function () {
  234. this.DocumentManager.resyncDocContentsWithLock
  235. .calledWith(this.project_id, this.docs[0].doc, this.docs[0].path)
  236. .should.equal(true)
  237. })
  238. it('should call the callback', function () {
  239. this.callback.called.should.equal(true)
  240. })
  241. })
  242. describe('resyncProjectStructureOnly=true', function () {
  243. beforeEach(function () {
  244. this.HistoryManager.resyncProjectHistory(
  245. this.project_id,
  246. this.projectHistoryId,
  247. this.docs,
  248. this.files,
  249. { resyncProjectStructureOnly: true },
  250. this.callback
  251. )
  252. })
  253. it('should queue a project structure reync', function () {
  254. this.ProjectHistoryRedisManager.queueResyncProjectStructure
  255. .calledWith(
  256. this.project_id,
  257. this.projectHistoryId,
  258. this.docs,
  259. this.files,
  260. { resyncProjectStructureOnly: true }
  261. )
  262. .should.equal(true)
  263. })
  264. it('should not queue doc content reyncs', function () {
  265. this.DocumentManager.resyncDocContentsWithLock.called.should.equal(
  266. false
  267. )
  268. })
  269. it('should call the callback', function () {
  270. this.callback.called.should.equal(true)
  271. })
  272. })
  273. })
  274. })