ApplyingUpdatesToProjectStructureTests.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507
  1. const sinon = require('sinon')
  2. const Settings = require('settings-sharelatex')
  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(
  117. this.project_id,
  118. this.update.id,
  119. (error) => {
  120. if (error) {
  121. return done(error)
  122. }
  123. sinon.spy(MockWebApi, 'getDocument')
  124. DocUpdaterClient.sendProjectUpdate(
  125. this.project_id,
  126. this.user_id,
  127. this.updates,
  128. this.version,
  129. (error) => {
  130. if (error) {
  131. return done(error)
  132. }
  133. setTimeout(done, 200)
  134. }
  135. )
  136. }
  137. )
  138. })
  139. after(function () {
  140. MockWebApi.getDocument.restore()
  141. })
  142. it('should update the doc', function (done) {
  143. DocUpdaterClient.getDoc(
  144. this.project_id,
  145. this.update.id,
  146. (error, res, doc) => {
  147. if (error) {
  148. return done(error)
  149. }
  150. doc.pathname.should.equal(this.update.newPathname)
  151. done()
  152. }
  153. )
  154. })
  155. it('should push the applied doc renames to the project history api', function (done) {
  156. rclientProjectHistory.lrange(
  157. ProjectHistoryKeys.projectHistoryOps({ project_id: this.project_id }),
  158. 0,
  159. -1,
  160. (error, updates) => {
  161. if (error) {
  162. return done(error)
  163. }
  164. const update = JSON.parse(updates[0])
  165. update.doc.should.equal(this.update.id)
  166. update.pathname.should.equal('/doc-path')
  167. update.new_pathname.should.equal('/new-doc-path')
  168. update.meta.user_id.should.equal(this.user_id)
  169. update.meta.ts.should.be.a('string')
  170. update.version.should.equal(`${this.version}.0`)
  171. done()
  172. }
  173. )
  174. })
  175. })
  176. })
  177. describe('renaming multiple documents and files', function () {
  178. before(function () {
  179. this.docUpdate0 = {
  180. type: 'rename-doc',
  181. id: DocUpdaterClient.randomId(),
  182. pathname: '/doc-path0',
  183. newPathname: '/new-doc-path0'
  184. }
  185. this.docUpdate1 = {
  186. type: 'rename-doc',
  187. id: DocUpdaterClient.randomId(),
  188. pathname: '/doc-path1',
  189. newPathname: '/new-doc-path1'
  190. }
  191. this.fileUpdate0 = {
  192. type: 'rename-file',
  193. id: DocUpdaterClient.randomId(),
  194. pathname: '/file-path0',
  195. newPathname: '/new-file-path0'
  196. }
  197. this.fileUpdate1 = {
  198. type: 'rename-file',
  199. id: DocUpdaterClient.randomId(),
  200. pathname: '/file-path1',
  201. newPathname: '/new-file-path1'
  202. }
  203. this.updates = [
  204. this.docUpdate0,
  205. this.docUpdate1,
  206. this.fileUpdate0,
  207. this.fileUpdate1
  208. ]
  209. })
  210. describe('when the documents are not loaded', function () {
  211. before(function (done) {
  212. this.project_id = DocUpdaterClient.randomId()
  213. DocUpdaterClient.sendProjectUpdate(
  214. this.project_id,
  215. this.user_id,
  216. this.updates,
  217. this.version,
  218. (error) => {
  219. if (error) {
  220. return done(error)
  221. }
  222. setTimeout(done, 200)
  223. }
  224. )
  225. })
  226. it('should push the applied doc renames to the project history api', function (done) {
  227. rclientProjectHistory.lrange(
  228. ProjectHistoryKeys.projectHistoryOps({ project_id: this.project_id }),
  229. 0,
  230. -1,
  231. (error, updates) => {
  232. if (error) {
  233. return done(error)
  234. }
  235. let update = JSON.parse(updates[0])
  236. update.doc.should.equal(this.docUpdate0.id)
  237. update.pathname.should.equal('/doc-path0')
  238. update.new_pathname.should.equal('/new-doc-path0')
  239. update.meta.user_id.should.equal(this.user_id)
  240. update.meta.ts.should.be.a('string')
  241. update.version.should.equal(`${this.version}.0`)
  242. update = JSON.parse(updates[1])
  243. update.doc.should.equal(this.docUpdate1.id)
  244. update.pathname.should.equal('/doc-path1')
  245. update.new_pathname.should.equal('/new-doc-path1')
  246. update.meta.user_id.should.equal(this.user_id)
  247. update.meta.ts.should.be.a('string')
  248. update.version.should.equal(`${this.version}.1`)
  249. update = JSON.parse(updates[2])
  250. update.file.should.equal(this.fileUpdate0.id)
  251. update.pathname.should.equal('/file-path0')
  252. update.new_pathname.should.equal('/new-file-path0')
  253. update.meta.user_id.should.equal(this.user_id)
  254. update.meta.ts.should.be.a('string')
  255. update.version.should.equal(`${this.version}.2`)
  256. update = JSON.parse(updates[3])
  257. update.file.should.equal(this.fileUpdate1.id)
  258. update.pathname.should.equal('/file-path1')
  259. update.new_pathname.should.equal('/new-file-path1')
  260. update.meta.user_id.should.equal(this.user_id)
  261. update.meta.ts.should.be.a('string')
  262. update.version.should.equal(`${this.version}.3`)
  263. done()
  264. }
  265. )
  266. })
  267. })
  268. })
  269. describe('adding a file', function () {
  270. before(function (done) {
  271. this.project_id = DocUpdaterClient.randomId()
  272. this.fileUpdate = {
  273. type: 'add-file',
  274. id: DocUpdaterClient.randomId(),
  275. pathname: '/file-path',
  276. url: 'filestore.example.com'
  277. }
  278. this.updates = [this.fileUpdate]
  279. DocUpdaterClient.sendProjectUpdate(
  280. this.project_id,
  281. this.user_id,
  282. this.updates,
  283. this.version,
  284. (error) => {
  285. if (error) {
  286. return done(error)
  287. }
  288. setTimeout(done, 200)
  289. }
  290. )
  291. })
  292. it('should push the file addition to the project history api', function (done) {
  293. rclientProjectHistory.lrange(
  294. ProjectHistoryKeys.projectHistoryOps({ project_id: this.project_id }),
  295. 0,
  296. -1,
  297. (error, updates) => {
  298. if (error) {
  299. return done(error)
  300. }
  301. const update = JSON.parse(updates[0])
  302. update.file.should.equal(this.fileUpdate.id)
  303. update.pathname.should.equal('/file-path')
  304. update.url.should.equal('filestore.example.com')
  305. update.meta.user_id.should.equal(this.user_id)
  306. update.meta.ts.should.be.a('string')
  307. update.version.should.equal(`${this.version}.0`)
  308. done()
  309. }
  310. )
  311. })
  312. })
  313. describe('adding a doc', function () {
  314. before(function (done) {
  315. this.project_id = DocUpdaterClient.randomId()
  316. this.docUpdate = {
  317. type: 'add-doc',
  318. id: DocUpdaterClient.randomId(),
  319. pathname: '/file-path',
  320. docLines: 'a\nb'
  321. }
  322. this.updates = [this.docUpdate]
  323. DocUpdaterClient.sendProjectUpdate(
  324. this.project_id,
  325. this.user_id,
  326. this.updates,
  327. this.version,
  328. (error) => {
  329. if (error) {
  330. return done(error)
  331. }
  332. setTimeout(done, 200)
  333. }
  334. )
  335. })
  336. it('should push the doc addition to the project history api', function (done) {
  337. rclientProjectHistory.lrange(
  338. ProjectHistoryKeys.projectHistoryOps({ project_id: this.project_id }),
  339. 0,
  340. -1,
  341. (error, updates) => {
  342. if (error) {
  343. return done(error)
  344. }
  345. const update = JSON.parse(updates[0])
  346. update.doc.should.equal(this.docUpdate.id)
  347. update.pathname.should.equal('/file-path')
  348. update.docLines.should.equal('a\nb')
  349. update.meta.user_id.should.equal(this.user_id)
  350. update.meta.ts.should.be.a('string')
  351. update.version.should.equal(`${this.version}.0`)
  352. done()
  353. }
  354. )
  355. })
  356. })
  357. describe('with enough updates to flush to the history service', function () {
  358. before(function (done) {
  359. this.project_id = DocUpdaterClient.randomId()
  360. this.user_id = DocUpdaterClient.randomId()
  361. this.version0 = 12345
  362. this.version1 = this.version0 + 1
  363. const updates = []
  364. for (let v = 0; v <= 599; v++) {
  365. // Should flush after 500 ops
  366. updates.push({
  367. type: 'add-doc',
  368. id: DocUpdaterClient.randomId(),
  369. pathname: '/file-' + v,
  370. docLines: 'a\nb'
  371. })
  372. }
  373. sinon.spy(MockProjectHistoryApi, 'flushProject')
  374. // Send updates in chunks to causes multiple flushes
  375. const projectId = this.project_id
  376. const userId = this.project_id
  377. DocUpdaterClient.sendProjectUpdate(
  378. projectId,
  379. userId,
  380. updates.slice(0, 250),
  381. this.version0,
  382. function (error) {
  383. if (error) {
  384. return done(error)
  385. }
  386. DocUpdaterClient.sendProjectUpdate(
  387. projectId,
  388. userId,
  389. updates.slice(250),
  390. this.version1,
  391. (error) => {
  392. if (error) {
  393. return done(error)
  394. }
  395. setTimeout(done, 2000)
  396. }
  397. )
  398. }
  399. )
  400. })
  401. after(function () {
  402. MockProjectHistoryApi.flushProject.restore()
  403. })
  404. it('should flush project history', function () {
  405. MockProjectHistoryApi.flushProject
  406. .calledWith(this.project_id)
  407. .should.equal(true)
  408. })
  409. })
  410. describe('with too few updates to flush to the history service', function () {
  411. before(function (done) {
  412. this.project_id = DocUpdaterClient.randomId()
  413. this.user_id = DocUpdaterClient.randomId()
  414. this.version0 = 12345
  415. this.version1 = this.version0 + 1
  416. const updates = []
  417. for (let v = 0; v <= 42; v++) {
  418. // Should flush after 500 ops
  419. updates.push({
  420. type: 'add-doc',
  421. id: DocUpdaterClient.randomId(),
  422. pathname: '/file-' + v,
  423. docLines: 'a\nb'
  424. })
  425. }
  426. sinon.spy(MockProjectHistoryApi, 'flushProject')
  427. // Send updates in chunks
  428. const projectId = this.project_id
  429. const userId = this.project_id
  430. DocUpdaterClient.sendProjectUpdate(
  431. projectId,
  432. userId,
  433. updates.slice(0, 10),
  434. this.version0,
  435. function (error) {
  436. if (error) {
  437. return done(error)
  438. }
  439. DocUpdaterClient.sendProjectUpdate(
  440. projectId,
  441. userId,
  442. updates.slice(10),
  443. this.version1,
  444. (error) => {
  445. if (error) {
  446. return done(error)
  447. }
  448. setTimeout(done, 2000)
  449. }
  450. )
  451. }
  452. )
  453. })
  454. after(function () {
  455. MockProjectHistoryApi.flushProject.restore()
  456. })
  457. it('should not flush project history', function () {
  458. MockProjectHistoryApi.flushProject
  459. .calledWith(this.project_id)
  460. .should.equal(false)
  461. })
  462. })
  463. })