ApplyingUpdatesToProjectStructureTests.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671
  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('deleting a file', function () {
  66. before(function (done) {
  67. this.project_id = DocUpdaterClient.randomId()
  68. this.fileUpdate = {
  69. type: 'rename-file',
  70. id: DocUpdaterClient.randomId(),
  71. pathname: '/file-path',
  72. newPathname: '',
  73. }
  74. this.updates = [this.fileUpdate]
  75. DocUpdaterClient.sendProjectUpdate(
  76. this.project_id,
  77. this.user_id,
  78. this.updates,
  79. this.version,
  80. error => {
  81. if (error) {
  82. return done(error)
  83. }
  84. setTimeout(done, 200)
  85. }
  86. )
  87. })
  88. it('should push the applied file renames to the project history api', function (done) {
  89. rclientProjectHistory.lrange(
  90. ProjectHistoryKeys.projectHistoryOps({ project_id: this.project_id }),
  91. 0,
  92. -1,
  93. (error, updates) => {
  94. if (error) {
  95. return done(error)
  96. }
  97. const update = JSON.parse(updates[0])
  98. update.file.should.equal(this.fileUpdate.id)
  99. update.pathname.should.equal('/file-path')
  100. update.new_pathname.should.equal('')
  101. update.meta.user_id.should.equal(this.user_id)
  102. update.meta.ts.should.be.a('string')
  103. update.version.should.equal(`${this.version}.0`)
  104. done()
  105. }
  106. )
  107. })
  108. })
  109. describe('renaming a document', function () {
  110. before(function () {
  111. this.update = {
  112. type: 'rename-doc',
  113. id: DocUpdaterClient.randomId(),
  114. pathname: '/doc-path',
  115. newPathname: '/new-doc-path',
  116. }
  117. this.updates = [this.update]
  118. })
  119. describe('when the document is not loaded', function () {
  120. before(function (done) {
  121. this.project_id = DocUpdaterClient.randomId()
  122. DocUpdaterClient.sendProjectUpdate(
  123. this.project_id,
  124. this.user_id,
  125. this.updates,
  126. this.version,
  127. error => {
  128. if (error) {
  129. return done(error)
  130. }
  131. setTimeout(done, 200)
  132. }
  133. )
  134. })
  135. it('should push the applied doc renames to the project history api', function (done) {
  136. rclientProjectHistory.lrange(
  137. ProjectHistoryKeys.projectHistoryOps({ project_id: this.project_id }),
  138. 0,
  139. -1,
  140. (error, updates) => {
  141. if (error) {
  142. return done(error)
  143. }
  144. const update = JSON.parse(updates[0])
  145. update.doc.should.equal(this.update.id)
  146. update.pathname.should.equal('/doc-path')
  147. update.new_pathname.should.equal('/new-doc-path')
  148. update.meta.user_id.should.equal(this.user_id)
  149. update.meta.ts.should.be.a('string')
  150. update.version.should.equal(`${this.version}.0`)
  151. done()
  152. }
  153. )
  154. })
  155. })
  156. describe('when the document is loaded', function () {
  157. before(function (done) {
  158. this.project_id = DocUpdaterClient.randomId()
  159. MockWebApi.insertDoc(this.project_id, this.update.id, {})
  160. DocUpdaterClient.preloadDoc(this.project_id, this.update.id, error => {
  161. if (error) {
  162. return done(error)
  163. }
  164. sinon.spy(MockWebApi, 'getDocument')
  165. DocUpdaterClient.sendProjectUpdate(
  166. this.project_id,
  167. this.user_id,
  168. this.updates,
  169. this.version,
  170. error => {
  171. if (error) {
  172. return done(error)
  173. }
  174. setTimeout(done, 200)
  175. }
  176. )
  177. })
  178. })
  179. after(function () {
  180. MockWebApi.getDocument.restore()
  181. })
  182. it('should update the doc', function (done) {
  183. DocUpdaterClient.getDoc(
  184. this.project_id,
  185. this.update.id,
  186. (error, res, doc) => {
  187. if (error) {
  188. return done(error)
  189. }
  190. doc.pathname.should.equal(this.update.newPathname)
  191. done()
  192. }
  193. )
  194. })
  195. it('should push the applied doc renames to the project history api', function (done) {
  196. rclientProjectHistory.lrange(
  197. ProjectHistoryKeys.projectHistoryOps({ project_id: this.project_id }),
  198. 0,
  199. -1,
  200. (error, updates) => {
  201. if (error) {
  202. return done(error)
  203. }
  204. const update = JSON.parse(updates[0])
  205. update.doc.should.equal(this.update.id)
  206. update.pathname.should.equal('/doc-path')
  207. update.new_pathname.should.equal('/new-doc-path')
  208. update.meta.user_id.should.equal(this.user_id)
  209. update.meta.ts.should.be.a('string')
  210. update.version.should.equal(`${this.version}.0`)
  211. done()
  212. }
  213. )
  214. })
  215. })
  216. })
  217. describe('renaming multiple documents and files', function () {
  218. before(function () {
  219. this.docUpdate0 = {
  220. type: 'rename-doc',
  221. id: DocUpdaterClient.randomId(),
  222. pathname: '/doc-path0',
  223. newPathname: '/new-doc-path0',
  224. }
  225. this.docUpdate1 = {
  226. type: 'rename-doc',
  227. id: DocUpdaterClient.randomId(),
  228. pathname: '/doc-path1',
  229. newPathname: '/new-doc-path1',
  230. }
  231. this.fileUpdate0 = {
  232. type: 'rename-file',
  233. id: DocUpdaterClient.randomId(),
  234. pathname: '/file-path0',
  235. newPathname: '/new-file-path0',
  236. }
  237. this.fileUpdate1 = {
  238. type: 'rename-file',
  239. id: DocUpdaterClient.randomId(),
  240. pathname: '/file-path1',
  241. newPathname: '/new-file-path1',
  242. }
  243. this.updates = [
  244. this.docUpdate0,
  245. this.docUpdate1,
  246. this.fileUpdate0,
  247. this.fileUpdate1,
  248. ]
  249. })
  250. describe('when the documents are not loaded', function () {
  251. before(function (done) {
  252. this.project_id = DocUpdaterClient.randomId()
  253. DocUpdaterClient.sendProjectUpdate(
  254. this.project_id,
  255. this.user_id,
  256. this.updates,
  257. this.version,
  258. error => {
  259. if (error) {
  260. return done(error)
  261. }
  262. setTimeout(done, 200)
  263. }
  264. )
  265. })
  266. it('should push the applied doc renames to the project history api', function (done) {
  267. rclientProjectHistory.lrange(
  268. ProjectHistoryKeys.projectHistoryOps({ project_id: this.project_id }),
  269. 0,
  270. -1,
  271. (error, updates) => {
  272. if (error) {
  273. return done(error)
  274. }
  275. let update = JSON.parse(updates[0])
  276. update.doc.should.equal(this.docUpdate0.id)
  277. update.pathname.should.equal('/doc-path0')
  278. update.new_pathname.should.equal('/new-doc-path0')
  279. update.meta.user_id.should.equal(this.user_id)
  280. update.meta.ts.should.be.a('string')
  281. update.version.should.equal(`${this.version}.0`)
  282. update = JSON.parse(updates[1])
  283. update.doc.should.equal(this.docUpdate1.id)
  284. update.pathname.should.equal('/doc-path1')
  285. update.new_pathname.should.equal('/new-doc-path1')
  286. update.meta.user_id.should.equal(this.user_id)
  287. update.meta.ts.should.be.a('string')
  288. update.version.should.equal(`${this.version}.1`)
  289. update = JSON.parse(updates[2])
  290. update.file.should.equal(this.fileUpdate0.id)
  291. update.pathname.should.equal('/file-path0')
  292. update.new_pathname.should.equal('/new-file-path0')
  293. update.meta.user_id.should.equal(this.user_id)
  294. update.meta.ts.should.be.a('string')
  295. update.version.should.equal(`${this.version}.2`)
  296. update = JSON.parse(updates[3])
  297. update.file.should.equal(this.fileUpdate1.id)
  298. update.pathname.should.equal('/file-path1')
  299. update.new_pathname.should.equal('/new-file-path1')
  300. update.meta.user_id.should.equal(this.user_id)
  301. update.meta.ts.should.be.a('string')
  302. update.version.should.equal(`${this.version}.3`)
  303. done()
  304. }
  305. )
  306. })
  307. })
  308. })
  309. describe('deleting a document', function () {
  310. before(function () {
  311. this.update = {
  312. type: 'rename-doc',
  313. id: DocUpdaterClient.randomId(),
  314. pathname: '/doc-path',
  315. newPathname: '',
  316. }
  317. this.updates = [this.update]
  318. })
  319. describe('when the document is not loaded', function () {
  320. before(function (done) {
  321. this.project_id = DocUpdaterClient.randomId()
  322. DocUpdaterClient.sendProjectUpdate(
  323. this.project_id,
  324. this.user_id,
  325. this.updates,
  326. this.version,
  327. error => {
  328. if (error) {
  329. return done(error)
  330. }
  331. setTimeout(done, 200)
  332. }
  333. )
  334. })
  335. it('should push the applied doc update to the project history api', function (done) {
  336. rclientProjectHistory.lrange(
  337. ProjectHistoryKeys.projectHistoryOps({ project_id: this.project_id }),
  338. 0,
  339. -1,
  340. (error, updates) => {
  341. if (error) {
  342. return done(error)
  343. }
  344. const update = JSON.parse(updates[0])
  345. update.doc.should.equal(this.update.id)
  346. update.pathname.should.equal('/doc-path')
  347. update.new_pathname.should.equal('')
  348. update.meta.user_id.should.equal(this.user_id)
  349. update.meta.ts.should.be.a('string')
  350. update.version.should.equal(`${this.version}.0`)
  351. done()
  352. }
  353. )
  354. })
  355. })
  356. describe('when the document is loaded', function () {
  357. before(function (done) {
  358. this.project_id = DocUpdaterClient.randomId()
  359. MockWebApi.insertDoc(this.project_id, this.update.id, {})
  360. DocUpdaterClient.preloadDoc(this.project_id, this.update.id, error => {
  361. if (error) {
  362. return done(error)
  363. }
  364. sinon.spy(MockWebApi, 'getDocument')
  365. DocUpdaterClient.sendProjectUpdate(
  366. this.project_id,
  367. this.user_id,
  368. this.updates,
  369. this.version,
  370. error => {
  371. if (error) {
  372. return done(error)
  373. }
  374. setTimeout(done, 200)
  375. }
  376. )
  377. })
  378. })
  379. after(function () {
  380. MockWebApi.getDocument.restore()
  381. })
  382. it('should not modify the doc', function (done) {
  383. DocUpdaterClient.getDoc(
  384. this.project_id,
  385. this.update.id,
  386. (error, res, doc) => {
  387. if (error) {
  388. return done(error)
  389. }
  390. doc.pathname.should.equal('/a/b/c.tex') // default pathname from MockWebApi
  391. done()
  392. }
  393. )
  394. })
  395. it('should push the applied doc update to the project history api', function (done) {
  396. rclientProjectHistory.lrange(
  397. ProjectHistoryKeys.projectHistoryOps({ project_id: this.project_id }),
  398. 0,
  399. -1,
  400. (error, updates) => {
  401. if (error) {
  402. return done(error)
  403. }
  404. const update = JSON.parse(updates[0])
  405. update.doc.should.equal(this.update.id)
  406. update.pathname.should.equal('/doc-path')
  407. update.new_pathname.should.equal('')
  408. update.meta.user_id.should.equal(this.user_id)
  409. update.meta.ts.should.be.a('string')
  410. update.version.should.equal(`${this.version}.0`)
  411. done()
  412. }
  413. )
  414. })
  415. })
  416. })
  417. describe('adding a file', function () {
  418. before(function (done) {
  419. this.project_id = DocUpdaterClient.randomId()
  420. this.fileUpdate = {
  421. type: 'add-file',
  422. id: DocUpdaterClient.randomId(),
  423. pathname: '/file-path',
  424. url: 'filestore.example.com',
  425. }
  426. this.updates = [this.fileUpdate]
  427. DocUpdaterClient.sendProjectUpdate(
  428. this.project_id,
  429. this.user_id,
  430. this.updates,
  431. this.version,
  432. error => {
  433. if (error) {
  434. return done(error)
  435. }
  436. setTimeout(done, 200)
  437. }
  438. )
  439. })
  440. it('should push the file addition to the project history api', function (done) {
  441. rclientProjectHistory.lrange(
  442. ProjectHistoryKeys.projectHistoryOps({ project_id: this.project_id }),
  443. 0,
  444. -1,
  445. (error, updates) => {
  446. if (error) {
  447. return done(error)
  448. }
  449. const update = JSON.parse(updates[0])
  450. update.file.should.equal(this.fileUpdate.id)
  451. update.pathname.should.equal('/file-path')
  452. update.url.should.equal('filestore.example.com')
  453. update.meta.user_id.should.equal(this.user_id)
  454. update.meta.ts.should.be.a('string')
  455. update.version.should.equal(`${this.version}.0`)
  456. done()
  457. }
  458. )
  459. })
  460. })
  461. describe('adding a doc', function () {
  462. before(function (done) {
  463. this.project_id = DocUpdaterClient.randomId()
  464. this.docUpdate = {
  465. type: 'add-doc',
  466. id: DocUpdaterClient.randomId(),
  467. pathname: '/file-path',
  468. docLines: 'a\nb',
  469. }
  470. this.updates = [this.docUpdate]
  471. DocUpdaterClient.sendProjectUpdate(
  472. this.project_id,
  473. this.user_id,
  474. this.updates,
  475. this.version,
  476. error => {
  477. if (error) {
  478. return done(error)
  479. }
  480. setTimeout(done, 200)
  481. }
  482. )
  483. })
  484. it('should push the doc addition to the project history api', function (done) {
  485. rclientProjectHistory.lrange(
  486. ProjectHistoryKeys.projectHistoryOps({ project_id: this.project_id }),
  487. 0,
  488. -1,
  489. (error, updates) => {
  490. if (error) {
  491. return done(error)
  492. }
  493. const update = JSON.parse(updates[0])
  494. update.doc.should.equal(this.docUpdate.id)
  495. update.pathname.should.equal('/file-path')
  496. update.docLines.should.equal('a\nb')
  497. update.meta.user_id.should.equal(this.user_id)
  498. update.meta.ts.should.be.a('string')
  499. update.version.should.equal(`${this.version}.0`)
  500. done()
  501. }
  502. )
  503. })
  504. })
  505. describe('with enough updates to flush to the history service', function () {
  506. before(function (done) {
  507. this.project_id = DocUpdaterClient.randomId()
  508. this.user_id = DocUpdaterClient.randomId()
  509. this.version0 = 12345
  510. this.version1 = this.version0 + 1
  511. const updates = []
  512. for (let v = 0; v <= 599; v++) {
  513. // Should flush after 500 ops
  514. updates.push({
  515. type: 'add-doc',
  516. id: DocUpdaterClient.randomId(),
  517. pathname: '/file-' + v,
  518. docLines: 'a\nb',
  519. })
  520. }
  521. sinon.spy(MockProjectHistoryApi, 'flushProject')
  522. // Send updates in chunks to causes multiple flushes
  523. const projectId = this.project_id
  524. const userId = this.project_id
  525. DocUpdaterClient.sendProjectUpdate(
  526. projectId,
  527. userId,
  528. updates.slice(0, 250),
  529. this.version0,
  530. function (error) {
  531. if (error) {
  532. return done(error)
  533. }
  534. DocUpdaterClient.sendProjectUpdate(
  535. projectId,
  536. userId,
  537. updates.slice(250),
  538. this.version1,
  539. error => {
  540. if (error) {
  541. return done(error)
  542. }
  543. setTimeout(done, 2000)
  544. }
  545. )
  546. }
  547. )
  548. })
  549. after(function () {
  550. MockProjectHistoryApi.flushProject.restore()
  551. })
  552. it('should flush project history', function () {
  553. MockProjectHistoryApi.flushProject
  554. .calledWith(this.project_id)
  555. .should.equal(true)
  556. })
  557. })
  558. describe('with too few updates to flush to the history service', function () {
  559. before(function (done) {
  560. this.project_id = DocUpdaterClient.randomId()
  561. this.user_id = DocUpdaterClient.randomId()
  562. this.version0 = 12345
  563. this.version1 = this.version0 + 1
  564. const updates = []
  565. for (let v = 0; v <= 42; v++) {
  566. // Should flush after 500 ops
  567. updates.push({
  568. type: 'add-doc',
  569. id: DocUpdaterClient.randomId(),
  570. pathname: '/file-' + v,
  571. docLines: 'a\nb',
  572. })
  573. }
  574. sinon.spy(MockProjectHistoryApi, 'flushProject')
  575. // Send updates in chunks
  576. const projectId = this.project_id
  577. const userId = this.project_id
  578. DocUpdaterClient.sendProjectUpdate(
  579. projectId,
  580. userId,
  581. updates.slice(0, 10),
  582. this.version0,
  583. function (error) {
  584. if (error) {
  585. return done(error)
  586. }
  587. DocUpdaterClient.sendProjectUpdate(
  588. projectId,
  589. userId,
  590. updates.slice(10),
  591. this.version1,
  592. error => {
  593. if (error) {
  594. return done(error)
  595. }
  596. setTimeout(done, 2000)
  597. }
  598. )
  599. }
  600. )
  601. })
  602. after(function () {
  603. MockProjectHistoryApi.flushProject.restore()
  604. })
  605. it('should not flush project history', function () {
  606. MockProjectHistoryApi.flushProject
  607. .calledWith(this.project_id)
  608. .should.equal(false)
  609. })
  610. })
  611. })