HistoryManagerTests.js 8.1 KB

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