ProjectHistoryRedisManagerTests.js 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263
  1. const sinon = require('sinon')
  2. const modulePath = '../../../../app/js/ProjectHistoryRedisManager.js'
  3. const SandboxedModule = require('sandboxed-module')
  4. const tk = require('timekeeper')
  5. describe('ProjectHistoryRedisManager', function () {
  6. beforeEach(function () {
  7. this.project_id = 'project-id-123'
  8. this.projectHistoryId = 'history-id-123'
  9. this.user_id = 'user-id-123'
  10. this.callback = sinon.stub()
  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(function () {
  47. this.ops = ['mock-op-1', 'mock-op-2']
  48. this.multi = { exec: sinon.stub() }
  49. this.multi.rpush = sinon.stub()
  50. this.multi.setnx = sinon.stub()
  51. this.rclient.multi = () => this.multi
  52. // @rclient = multi: () => @multi
  53. this.ProjectHistoryRedisManager.queueOps(
  54. this.project_id,
  55. ...this.ops,
  56. this.callback
  57. )
  58. })
  59. it('should queue an update', function () {
  60. this.multi.rpush
  61. .calledWithExactly(
  62. `ProjectHistory:Ops:${this.project_id}`,
  63. this.ops[0],
  64. this.ops[1]
  65. )
  66. .should.equal(true)
  67. })
  68. it('should set the queue timestamp if not present', function () {
  69. this.multi.setnx
  70. .calledWithExactly(
  71. `ProjectHistory:FirstOpTimestamp:${this.project_id}`,
  72. Date.now()
  73. )
  74. .should.equal(true)
  75. })
  76. })
  77. describe('queueRenameEntity', function () {
  78. beforeEach(function () {
  79. this.file_id = 1234
  80. this.rawUpdate = {
  81. pathname: (this.pathname = '/old'),
  82. newPathname: (this.newPathname = '/new'),
  83. version: (this.version = 2),
  84. }
  85. this.ProjectHistoryRedisManager.queueOps = sinon.stub()
  86. this.ProjectHistoryRedisManager.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. this.callback
  95. )
  96. })
  97. it('should queue an update', function () {
  98. const update = {
  99. pathname: this.pathname,
  100. new_pathname: this.newPathname,
  101. meta: {
  102. user_id: this.user_id,
  103. ts: new Date(),
  104. source: this.source,
  105. },
  106. version: this.version,
  107. projectHistoryId: this.projectHistoryId,
  108. file: this.file_id,
  109. }
  110. this.ProjectHistoryRedisManager.queueOps
  111. .calledWithExactly(
  112. this.project_id,
  113. JSON.stringify(update),
  114. this.callback
  115. )
  116. .should.equal(true)
  117. })
  118. })
  119. describe('queueAddEntity', function () {
  120. beforeEach(function () {
  121. this.rclient.rpush = sinon.stub().yields()
  122. this.doc_id = 1234
  123. this.rawUpdate = {
  124. pathname: (this.pathname = '/old'),
  125. docLines: (this.docLines = 'a\nb'),
  126. version: (this.version = 2),
  127. url: (this.url = 'filestore.example.com'),
  128. }
  129. this.ProjectHistoryRedisManager.queueOps = sinon.stub()
  130. this.ProjectHistoryRedisManager.queueAddEntity(
  131. this.project_id,
  132. this.projectHistoryId,
  133. 'doc',
  134. this.doc_id,
  135. this.user_id,
  136. this.rawUpdate,
  137. this.source,
  138. this.callback
  139. )
  140. })
  141. it('should queue an update', function () {
  142. const update = {
  143. pathname: this.pathname,
  144. docLines: this.docLines,
  145. url: this.url,
  146. meta: {
  147. user_id: this.user_id,
  148. ts: new Date(),
  149. source: this.source,
  150. },
  151. version: this.version,
  152. projectHistoryId: this.projectHistoryId,
  153. doc: this.doc_id,
  154. }
  155. this.ProjectHistoryRedisManager.queueOps
  156. .calledWithExactly(
  157. this.project_id,
  158. JSON.stringify(update),
  159. this.callback
  160. )
  161. .should.equal(true)
  162. })
  163. describe('queueResyncProjectStructure', function () {
  164. it('should queue an update', function () {})
  165. })
  166. describe('queueResyncDocContent', function () {
  167. beforeEach(function () {
  168. this.doc_id = 1234
  169. this.lines = ['one', 'two']
  170. this.version = 2
  171. this.pathname = '/path'
  172. this.update = {
  173. resyncDocContent: {
  174. content: this.lines.join('\n'),
  175. version: this.version,
  176. },
  177. projectHistoryId: this.projectHistoryId,
  178. path: this.pathname,
  179. doc: this.doc_id,
  180. meta: {
  181. ts: new Date(),
  182. },
  183. }
  184. this.ProjectHistoryRedisManager.queueOps = sinon.stub()
  185. })
  186. describe('with a good doc', function () {
  187. beforeEach(function () {
  188. this.ProjectHistoryRedisManager.queueResyncDocContent(
  189. this.project_id,
  190. this.projectHistoryId,
  191. this.doc_id,
  192. this.lines,
  193. this.version,
  194. this.pathname,
  195. this.callback
  196. )
  197. })
  198. it('should check if the doc is too large', function () {
  199. this.Limits.docIsTooLarge
  200. .calledWith(
  201. JSON.stringify(this.update).length,
  202. this.lines,
  203. this.settings.max_doc_length
  204. )
  205. .should.equal(true)
  206. })
  207. it('should queue an update', function () {
  208. this.ProjectHistoryRedisManager.queueOps
  209. .calledWithExactly(
  210. this.project_id,
  211. JSON.stringify(this.update),
  212. this.callback
  213. )
  214. .should.equal(true)
  215. })
  216. })
  217. describe('with a doc that is too large', function () {
  218. beforeEach(function () {
  219. this.Limits.docIsTooLarge.returns(true)
  220. this.ProjectHistoryRedisManager.queueResyncDocContent(
  221. this.project_id,
  222. this.projectHistoryId,
  223. this.doc_id,
  224. this.lines,
  225. this.version,
  226. this.pathname,
  227. this.callback
  228. )
  229. })
  230. it('should not queue an update if the doc is too large', function () {
  231. this.ProjectHistoryRedisManager.queueOps.called.should.equal(false)
  232. this.callback
  233. .calledWith(sinon.match.instanceOf(Error))
  234. .should.equal(true)
  235. })
  236. })
  237. })
  238. })
  239. })