ApplyingUpdatesToProjectStructureTests.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503
  1. const sinon = require('sinon')
  2. const Settings = require('@overleaf/settings')
  3. const rclientProjectHistory = require('@overleaf/redis-wrapper').createClient(
  4. Settings.redis.project_history
  5. )
  6. const ProjectHistoryKeys = Settings.redis.project_history.key_schema
  7. const MockProjectHistoryApi = require('./helpers/MockProjectHistoryApi')
  8. const MockWebApi = require('./helpers/MockWebApi')
  9. const DocUpdaterClient = require('./helpers/DocUpdaterClient')
  10. const DocUpdaterApp = require('./helpers/DocUpdaterApp')
  11. describe("Applying updates to a project's structure", function () {
  12. before(function () {
  13. this.user_id = 'user-id-123'
  14. this.version = 1234
  15. })
  16. describe('renaming a file', function () {
  17. before(function (done) {
  18. this.project_id = DocUpdaterClient.randomId()
  19. this.fileUpdate = {
  20. type: 'rename-file',
  21. id: DocUpdaterClient.randomId(),
  22. pathname: '/file-path',
  23. newPathname: '/new-file-path',
  24. }
  25. this.updates = [this.fileUpdate]
  26. DocUpdaterApp.ensureRunning(error => {
  27. if (error) {
  28. return done(error)
  29. }
  30. DocUpdaterClient.sendProjectUpdate(
  31. this.project_id,
  32. this.user_id,
  33. this.updates,
  34. this.version,
  35. error => {
  36. if (error) {
  37. return done(error)
  38. }
  39. setTimeout(done, 200)
  40. }
  41. )
  42. })
  43. })
  44. it('should push the applied file renames to the project history api', function (done) {
  45. rclientProjectHistory.lrange(
  46. ProjectHistoryKeys.projectHistoryOps({ project_id: this.project_id }),
  47. 0,
  48. -1,
  49. (error, updates) => {
  50. if (error) {
  51. return done(error)
  52. }
  53. const update = JSON.parse(updates[0])
  54. update.file.should.equal(this.fileUpdate.id)
  55. update.pathname.should.equal('/file-path')
  56. update.new_pathname.should.equal('/new-file-path')
  57. update.meta.user_id.should.equal(this.user_id)
  58. update.meta.ts.should.be.a('string')
  59. update.version.should.equal(`${this.version}.0`)
  60. done()
  61. }
  62. )
  63. })
  64. })
  65. describe('renaming a document', function () {
  66. before(function () {
  67. this.update = {
  68. type: 'rename-doc',
  69. id: DocUpdaterClient.randomId(),
  70. pathname: '/doc-path',
  71. newPathname: '/new-doc-path',
  72. }
  73. this.updates = [this.update]
  74. })
  75. describe('when the document is not loaded', function () {
  76. before(function (done) {
  77. this.project_id = DocUpdaterClient.randomId()
  78. DocUpdaterClient.sendProjectUpdate(
  79. this.project_id,
  80. this.user_id,
  81. this.updates,
  82. this.version,
  83. error => {
  84. if (error) {
  85. return done(error)
  86. }
  87. setTimeout(done, 200)
  88. }
  89. )
  90. })
  91. it('should push the applied doc renames to the project history api', function (done) {
  92. rclientProjectHistory.lrange(
  93. ProjectHistoryKeys.projectHistoryOps({ project_id: this.project_id }),
  94. 0,
  95. -1,
  96. (error, updates) => {
  97. if (error) {
  98. return done(error)
  99. }
  100. const update = JSON.parse(updates[0])
  101. update.doc.should.equal(this.update.id)
  102. update.pathname.should.equal('/doc-path')
  103. update.new_pathname.should.equal('/new-doc-path')
  104. update.meta.user_id.should.equal(this.user_id)
  105. update.meta.ts.should.be.a('string')
  106. update.version.should.equal(`${this.version}.0`)
  107. done()
  108. }
  109. )
  110. })
  111. })
  112. describe('when the document is loaded', function () {
  113. before(function (done) {
  114. this.project_id = DocUpdaterClient.randomId()
  115. MockWebApi.insertDoc(this.project_id, this.update.id, {})
  116. DocUpdaterClient.preloadDoc(this.project_id, this.update.id, error => {
  117. if (error) {
  118. return done(error)
  119. }
  120. sinon.spy(MockWebApi, 'getDocument')
  121. DocUpdaterClient.sendProjectUpdate(
  122. this.project_id,
  123. this.user_id,
  124. this.updates,
  125. this.version,
  126. error => {
  127. if (error) {
  128. return done(error)
  129. }
  130. setTimeout(done, 200)
  131. }
  132. )
  133. })
  134. })
  135. after(function () {
  136. MockWebApi.getDocument.restore()
  137. })
  138. it('should update the doc', function (done) {
  139. DocUpdaterClient.getDoc(
  140. this.project_id,
  141. this.update.id,
  142. (error, res, doc) => {
  143. if (error) {
  144. return done(error)
  145. }
  146. doc.pathname.should.equal(this.update.newPathname)
  147. done()
  148. }
  149. )
  150. })
  151. it('should push the applied doc renames to the project history api', function (done) {
  152. rclientProjectHistory.lrange(
  153. ProjectHistoryKeys.projectHistoryOps({ project_id: this.project_id }),
  154. 0,
  155. -1,
  156. (error, updates) => {
  157. if (error) {
  158. return done(error)
  159. }
  160. const update = JSON.parse(updates[0])
  161. update.doc.should.equal(this.update.id)
  162. update.pathname.should.equal('/doc-path')
  163. update.new_pathname.should.equal('/new-doc-path')
  164. update.meta.user_id.should.equal(this.user_id)
  165. update.meta.ts.should.be.a('string')
  166. update.version.should.equal(`${this.version}.0`)
  167. done()
  168. }
  169. )
  170. })
  171. })
  172. })
  173. describe('renaming multiple documents and files', function () {
  174. before(function () {
  175. this.docUpdate0 = {
  176. type: 'rename-doc',
  177. id: DocUpdaterClient.randomId(),
  178. pathname: '/doc-path0',
  179. newPathname: '/new-doc-path0',
  180. }
  181. this.docUpdate1 = {
  182. type: 'rename-doc',
  183. id: DocUpdaterClient.randomId(),
  184. pathname: '/doc-path1',
  185. newPathname: '/new-doc-path1',
  186. }
  187. this.fileUpdate0 = {
  188. type: 'rename-file',
  189. id: DocUpdaterClient.randomId(),
  190. pathname: '/file-path0',
  191. newPathname: '/new-file-path0',
  192. }
  193. this.fileUpdate1 = {
  194. type: 'rename-file',
  195. id: DocUpdaterClient.randomId(),
  196. pathname: '/file-path1',
  197. newPathname: '/new-file-path1',
  198. }
  199. this.updates = [
  200. this.docUpdate0,
  201. this.docUpdate1,
  202. this.fileUpdate0,
  203. this.fileUpdate1,
  204. ]
  205. })
  206. describe('when the documents are not loaded', function () {
  207. before(function (done) {
  208. this.project_id = DocUpdaterClient.randomId()
  209. DocUpdaterClient.sendProjectUpdate(
  210. this.project_id,
  211. this.user_id,
  212. this.updates,
  213. this.version,
  214. error => {
  215. if (error) {
  216. return done(error)
  217. }
  218. setTimeout(done, 200)
  219. }
  220. )
  221. })
  222. it('should push the applied doc renames to the project history api', function (done) {
  223. rclientProjectHistory.lrange(
  224. ProjectHistoryKeys.projectHistoryOps({ project_id: this.project_id }),
  225. 0,
  226. -1,
  227. (error, updates) => {
  228. if (error) {
  229. return done(error)
  230. }
  231. let update = JSON.parse(updates[0])
  232. update.doc.should.equal(this.docUpdate0.id)
  233. update.pathname.should.equal('/doc-path0')
  234. update.new_pathname.should.equal('/new-doc-path0')
  235. update.meta.user_id.should.equal(this.user_id)
  236. update.meta.ts.should.be.a('string')
  237. update.version.should.equal(`${this.version}.0`)
  238. update = JSON.parse(updates[1])
  239. update.doc.should.equal(this.docUpdate1.id)
  240. update.pathname.should.equal('/doc-path1')
  241. update.new_pathname.should.equal('/new-doc-path1')
  242. update.meta.user_id.should.equal(this.user_id)
  243. update.meta.ts.should.be.a('string')
  244. update.version.should.equal(`${this.version}.1`)
  245. update = JSON.parse(updates[2])
  246. update.file.should.equal(this.fileUpdate0.id)
  247. update.pathname.should.equal('/file-path0')
  248. update.new_pathname.should.equal('/new-file-path0')
  249. update.meta.user_id.should.equal(this.user_id)
  250. update.meta.ts.should.be.a('string')
  251. update.version.should.equal(`${this.version}.2`)
  252. update = JSON.parse(updates[3])
  253. update.file.should.equal(this.fileUpdate1.id)
  254. update.pathname.should.equal('/file-path1')
  255. update.new_pathname.should.equal('/new-file-path1')
  256. update.meta.user_id.should.equal(this.user_id)
  257. update.meta.ts.should.be.a('string')
  258. update.version.should.equal(`${this.version}.3`)
  259. done()
  260. }
  261. )
  262. })
  263. })
  264. })
  265. describe('adding a file', function () {
  266. before(function (done) {
  267. this.project_id = DocUpdaterClient.randomId()
  268. this.fileUpdate = {
  269. type: 'add-file',
  270. id: DocUpdaterClient.randomId(),
  271. pathname: '/file-path',
  272. url: 'filestore.example.com',
  273. }
  274. this.updates = [this.fileUpdate]
  275. DocUpdaterClient.sendProjectUpdate(
  276. this.project_id,
  277. this.user_id,
  278. this.updates,
  279. this.version,
  280. error => {
  281. if (error) {
  282. return done(error)
  283. }
  284. setTimeout(done, 200)
  285. }
  286. )
  287. })
  288. it('should push the file addition to the project history api', function (done) {
  289. rclientProjectHistory.lrange(
  290. ProjectHistoryKeys.projectHistoryOps({ project_id: this.project_id }),
  291. 0,
  292. -1,
  293. (error, updates) => {
  294. if (error) {
  295. return done(error)
  296. }
  297. const update = JSON.parse(updates[0])
  298. update.file.should.equal(this.fileUpdate.id)
  299. update.pathname.should.equal('/file-path')
  300. update.url.should.equal('filestore.example.com')
  301. update.meta.user_id.should.equal(this.user_id)
  302. update.meta.ts.should.be.a('string')
  303. update.version.should.equal(`${this.version}.0`)
  304. done()
  305. }
  306. )
  307. })
  308. })
  309. describe('adding a doc', function () {
  310. before(function (done) {
  311. this.project_id = DocUpdaterClient.randomId()
  312. this.docUpdate = {
  313. type: 'add-doc',
  314. id: DocUpdaterClient.randomId(),
  315. pathname: '/file-path',
  316. docLines: 'a\nb',
  317. }
  318. this.updates = [this.docUpdate]
  319. DocUpdaterClient.sendProjectUpdate(
  320. this.project_id,
  321. this.user_id,
  322. this.updates,
  323. this.version,
  324. error => {
  325. if (error) {
  326. return done(error)
  327. }
  328. setTimeout(done, 200)
  329. }
  330. )
  331. })
  332. it('should push the doc addition to the project history api', function (done) {
  333. rclientProjectHistory.lrange(
  334. ProjectHistoryKeys.projectHistoryOps({ project_id: this.project_id }),
  335. 0,
  336. -1,
  337. (error, updates) => {
  338. if (error) {
  339. return done(error)
  340. }
  341. const update = JSON.parse(updates[0])
  342. update.doc.should.equal(this.docUpdate.id)
  343. update.pathname.should.equal('/file-path')
  344. update.docLines.should.equal('a\nb')
  345. update.meta.user_id.should.equal(this.user_id)
  346. update.meta.ts.should.be.a('string')
  347. update.version.should.equal(`${this.version}.0`)
  348. done()
  349. }
  350. )
  351. })
  352. })
  353. describe('with enough updates to flush to the history service', function () {
  354. before(function (done) {
  355. this.project_id = DocUpdaterClient.randomId()
  356. this.user_id = DocUpdaterClient.randomId()
  357. this.version0 = 12345
  358. this.version1 = this.version0 + 1
  359. const updates = []
  360. for (let v = 0; v <= 599; v++) {
  361. // Should flush after 500 ops
  362. updates.push({
  363. type: 'add-doc',
  364. id: DocUpdaterClient.randomId(),
  365. pathname: '/file-' + v,
  366. docLines: 'a\nb',
  367. })
  368. }
  369. sinon.spy(MockProjectHistoryApi, 'flushProject')
  370. // Send updates in chunks to causes multiple flushes
  371. const projectId = this.project_id
  372. const userId = this.project_id
  373. DocUpdaterClient.sendProjectUpdate(
  374. projectId,
  375. userId,
  376. updates.slice(0, 250),
  377. this.version0,
  378. function (error) {
  379. if (error) {
  380. return done(error)
  381. }
  382. DocUpdaterClient.sendProjectUpdate(
  383. projectId,
  384. userId,
  385. updates.slice(250),
  386. this.version1,
  387. error => {
  388. if (error) {
  389. return done(error)
  390. }
  391. setTimeout(done, 2000)
  392. }
  393. )
  394. }
  395. )
  396. })
  397. after(function () {
  398. MockProjectHistoryApi.flushProject.restore()
  399. })
  400. it('should flush project history', function () {
  401. MockProjectHistoryApi.flushProject
  402. .calledWith(this.project_id)
  403. .should.equal(true)
  404. })
  405. })
  406. describe('with too few updates to flush to the history service', function () {
  407. before(function (done) {
  408. this.project_id = DocUpdaterClient.randomId()
  409. this.user_id = DocUpdaterClient.randomId()
  410. this.version0 = 12345
  411. this.version1 = this.version0 + 1
  412. const updates = []
  413. for (let v = 0; v <= 42; v++) {
  414. // Should flush after 500 ops
  415. updates.push({
  416. type: 'add-doc',
  417. id: DocUpdaterClient.randomId(),
  418. pathname: '/file-' + v,
  419. docLines: 'a\nb',
  420. })
  421. }
  422. sinon.spy(MockProjectHistoryApi, 'flushProject')
  423. // Send updates in chunks
  424. const projectId = this.project_id
  425. const userId = this.project_id
  426. DocUpdaterClient.sendProjectUpdate(
  427. projectId,
  428. userId,
  429. updates.slice(0, 10),
  430. this.version0,
  431. function (error) {
  432. if (error) {
  433. return done(error)
  434. }
  435. DocUpdaterClient.sendProjectUpdate(
  436. projectId,
  437. userId,
  438. updates.slice(10),
  439. this.version1,
  440. error => {
  441. if (error) {
  442. return done(error)
  443. }
  444. setTimeout(done, 2000)
  445. }
  446. )
  447. }
  448. )
  449. })
  450. after(function () {
  451. MockProjectHistoryApi.flushProject.restore()
  452. })
  453. it('should not flush project history', function () {
  454. MockProjectHistoryApi.flushProject
  455. .calledWith(this.project_id)
  456. .should.equal(false)
  457. })
  458. })
  459. })