ApplyingUpdatesToProjectStructureTests.js 14 KB

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