ProjectHistoryRedisManagerTests.js 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300
  1. const sinon = require('sinon')
  2. const { expect } = require('chai')
  3. const modulePath = '../../../../app/js/ProjectHistoryRedisManager.js'
  4. const SandboxedModule = require('sandboxed-module')
  5. const tk = require('timekeeper')
  6. describe('ProjectHistoryRedisManager', function () {
  7. beforeEach(function () {
  8. this.project_id = 'project-id-123'
  9. this.projectHistoryId = 'history-id-123'
  10. this.user_id = 'user-id-123'
  11. this.rclient = {}
  12. this.source = 'editor'
  13. tk.freeze(new Date())
  14. this.Limits = {
  15. docIsTooLarge: sinon.stub().returns(false),
  16. }
  17. this.ProjectHistoryRedisManager = SandboxedModule.require(modulePath, {
  18. requires: {
  19. '@overleaf/settings': (this.settings = {
  20. max_doc_length: 123,
  21. redis: {
  22. project_history: {
  23. key_schema: {
  24. projectHistoryOps({ project_id: projectId }) {
  25. return `ProjectHistory:Ops:${projectId}`
  26. },
  27. projectHistoryFirstOpTimestamp({ project_id: projectId }) {
  28. return `ProjectHistory:FirstOpTimestamp:${projectId}`
  29. },
  30. },
  31. },
  32. },
  33. }),
  34. '@overleaf/redis-wrapper': {
  35. createClient: () => this.rclient,
  36. },
  37. './Metrics': (this.metrics = { summary: sinon.stub() }),
  38. './Limits': this.Limits,
  39. },
  40. })
  41. })
  42. afterEach(function () {
  43. tk.reset()
  44. })
  45. describe('queueOps', function () {
  46. beforeEach(async function () {
  47. this.ops = ['mock-op-1', 'mock-op-2']
  48. this.multi = { exec: sinon.stub().resolves([1]) }
  49. this.multi.rpush = sinon.stub()
  50. this.multi.setnx = sinon.stub()
  51. this.rclient.multi = () => this.multi
  52. await this.ProjectHistoryRedisManager.promises.queueOps(
  53. this.project_id,
  54. ...this.ops
  55. )
  56. })
  57. it('should queue an update', function () {
  58. this.multi.rpush
  59. .calledWithExactly(
  60. `ProjectHistory:Ops:${this.project_id}`,
  61. this.ops[0],
  62. this.ops[1]
  63. )
  64. .should.equal(true)
  65. })
  66. it('should set the queue timestamp if not present', function () {
  67. this.multi.setnx
  68. .calledWithExactly(
  69. `ProjectHistory:FirstOpTimestamp:${this.project_id}`,
  70. Date.now()
  71. )
  72. .should.equal(true)
  73. })
  74. })
  75. describe('queueRenameEntity', function () {
  76. beforeEach(async function () {
  77. this.file_id = 1234
  78. this.rawUpdate = {
  79. pathname: (this.pathname = '/old'),
  80. newPathname: (this.newPathname = '/new'),
  81. version: (this.version = 2),
  82. }
  83. this.ProjectHistoryRedisManager.promises.queueOps = sinon
  84. .stub()
  85. .resolves()
  86. await this.ProjectHistoryRedisManager.promises.queueRenameEntity(
  87. this.project_id,
  88. this.projectHistoryId,
  89. 'file',
  90. this.file_id,
  91. this.user_id,
  92. this.rawUpdate,
  93. this.source
  94. )
  95. })
  96. it('should queue an update', function () {
  97. const update = {
  98. pathname: this.pathname,
  99. new_pathname: this.newPathname,
  100. meta: {
  101. user_id: this.user_id,
  102. ts: new Date(),
  103. source: this.source,
  104. },
  105. version: this.version,
  106. projectHistoryId: this.projectHistoryId,
  107. file: this.file_id,
  108. }
  109. this.ProjectHistoryRedisManager.promises.queueOps
  110. .calledWithExactly(this.project_id, JSON.stringify(update))
  111. .should.equal(true)
  112. })
  113. })
  114. describe('queueAddEntity', function () {
  115. beforeEach(async function () {
  116. this.doc_id = 1234
  117. this.rawUpdate = {
  118. pathname: (this.pathname = '/old'),
  119. docLines: (this.docLines = 'a\nb'),
  120. version: (this.version = 2),
  121. url: (this.url = 'filestore.example.com'),
  122. }
  123. this.ProjectHistoryRedisManager.promises.queueOps = sinon
  124. .stub()
  125. .resolves()
  126. await this.ProjectHistoryRedisManager.promises.queueAddEntity(
  127. this.project_id,
  128. this.projectHistoryId,
  129. 'doc',
  130. this.doc_id,
  131. this.user_id,
  132. this.rawUpdate,
  133. this.source
  134. )
  135. })
  136. it('should queue an update', function () {
  137. const update = {
  138. pathname: this.pathname,
  139. docLines: this.docLines,
  140. url: this.url,
  141. meta: {
  142. user_id: this.user_id,
  143. ts: new Date(),
  144. source: this.source,
  145. },
  146. version: this.version,
  147. projectHistoryId: this.projectHistoryId,
  148. doc: this.doc_id,
  149. }
  150. this.ProjectHistoryRedisManager.promises.queueOps
  151. .calledWithExactly(this.project_id, JSON.stringify(update))
  152. .should.equal(true)
  153. })
  154. describe('queueResyncProjectStructure', function () {
  155. it('should queue an update', function () {})
  156. })
  157. describe('queueResyncDocContent', function () {
  158. beforeEach(function () {
  159. this.doc_id = 1234
  160. this.lines = ['one', 'two']
  161. this.ranges = {
  162. changes: [{ op: { i: 'ne', p: 1 } }, { op: { d: 'deleted', p: 3 } }],
  163. }
  164. this.version = 2
  165. this.pathname = '/path'
  166. this.ProjectHistoryRedisManager.promises.queueOps = sinon
  167. .stub()
  168. .resolves()
  169. })
  170. describe('with a good doc', function () {
  171. beforeEach(async function () {
  172. this.update = {
  173. resyncDocContent: {
  174. content: 'one\ntwo',
  175. version: this.version,
  176. },
  177. projectHistoryId: this.projectHistoryId,
  178. path: this.pathname,
  179. doc: this.doc_id,
  180. meta: { ts: new Date() },
  181. }
  182. await this.ProjectHistoryRedisManager.promises.queueResyncDocContent(
  183. this.project_id,
  184. this.projectHistoryId,
  185. this.doc_id,
  186. this.lines,
  187. this.ranges,
  188. this.version,
  189. this.pathname,
  190. false
  191. )
  192. })
  193. it('should check if the doc is too large', function () {
  194. this.Limits.docIsTooLarge
  195. .calledWith(
  196. JSON.stringify(this.update).length,
  197. this.lines,
  198. this.settings.max_doc_length
  199. )
  200. .should.equal(true)
  201. })
  202. it('should queue an update', function () {
  203. this.ProjectHistoryRedisManager.promises.queueOps
  204. .calledWithExactly(this.project_id, JSON.stringify(this.update))
  205. .should.equal(true)
  206. })
  207. })
  208. describe('with a doc that is too large', function () {
  209. beforeEach(async function () {
  210. this.Limits.docIsTooLarge.returns(true)
  211. await expect(
  212. this.ProjectHistoryRedisManager.promises.queueResyncDocContent(
  213. this.project_id,
  214. this.projectHistoryId,
  215. this.doc_id,
  216. this.lines,
  217. this.ranges,
  218. this.version,
  219. this.pathname,
  220. false
  221. )
  222. ).to.be.rejected
  223. })
  224. it('should not queue an update if the doc is too large', function () {
  225. this.ProjectHistoryRedisManager.promises.queueOps.called.should.equal(
  226. false
  227. )
  228. })
  229. })
  230. describe('when history ranges support is enabled', function () {
  231. beforeEach(async function () {
  232. this.update = {
  233. resyncDocContent: {
  234. content: 'onedeleted\ntwo',
  235. version: this.version,
  236. ranges: this.ranges,
  237. },
  238. projectHistoryId: this.projectHistoryId,
  239. path: this.pathname,
  240. doc: this.doc_id,
  241. meta: { ts: new Date() },
  242. }
  243. await this.ProjectHistoryRedisManager.promises.queueResyncDocContent(
  244. this.project_id,
  245. this.projectHistoryId,
  246. this.doc_id,
  247. this.lines,
  248. this.ranges,
  249. this.version,
  250. this.pathname,
  251. true
  252. )
  253. })
  254. it('should include tracked deletes in the update', function () {
  255. this.ProjectHistoryRedisManager.promises.queueOps.should.have.been.calledWithExactly(
  256. this.project_id,
  257. JSON.stringify(this.update)
  258. )
  259. })
  260. it('should check the doc length without tracked deletes', function () {
  261. this.Limits.docIsTooLarge.should.have.been.calledWith(
  262. JSON.stringify(this.update).length,
  263. this.lines,
  264. this.settings.max_doc_length
  265. )
  266. })
  267. })
  268. })
  269. })
  270. })