ApplyingUpdatesToProjectStructureTests.js 15 KB

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