SendingUpdatesTests.js 52 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984
  1. import { expect } from 'chai'
  2. import Settings from '@overleaf/settings'
  3. import assert from 'assert'
  4. import async from 'async'
  5. import crypto from 'crypto'
  6. import mongodb from 'mongodb-legacy'
  7. import nock from 'nock'
  8. import * as ProjectHistoryClient from './helpers/ProjectHistoryClient.js'
  9. import * as ProjectHistoryApp from './helpers/ProjectHistoryApp.js'
  10. const { ObjectId } = mongodb
  11. const MockHistoryStore = () => nock('http://localhost:3100')
  12. const MockFileStore = () => nock('http://localhost:3009')
  13. const MockWeb = () => nock('http://localhost:3000')
  14. // Some helper methods to make the tests more compact
  15. function slTextUpdate(historyId, doc, userId, v, ts, op) {
  16. return {
  17. projectHistoryId: historyId,
  18. doc: doc.id,
  19. op,
  20. v,
  21. meta: {
  22. user_id: userId,
  23. ts: ts.getTime(),
  24. pathname: doc.pathname,
  25. doc_length: doc.length,
  26. },
  27. }
  28. }
  29. function slAddDocUpdate(historyId, doc, userId, ts, docLines) {
  30. return {
  31. projectHistoryId: historyId,
  32. pathname: doc.pathname,
  33. docLines,
  34. doc: doc.id,
  35. meta: { user_id: userId, ts: ts.getTime() },
  36. }
  37. }
  38. function slAddDocUpdateWithVersion(
  39. historyId,
  40. doc,
  41. userId,
  42. ts,
  43. docLines,
  44. projectVersion
  45. ) {
  46. const result = slAddDocUpdate(historyId, doc, userId, ts, docLines)
  47. result.version = projectVersion
  48. return result
  49. }
  50. function slAddFileUpdate(historyId, file, userId, ts, projectId) {
  51. return {
  52. projectHistoryId: historyId,
  53. pathname: file.pathname,
  54. url: `http://localhost:3009/project/${projectId}/file/${file.id}`,
  55. file: file.id,
  56. meta: { user_id: userId, ts: ts.getTime() },
  57. }
  58. }
  59. function slRenameUpdate(historyId, doc, userId, ts, pathname, newPathname) {
  60. return {
  61. projectHistoryId: historyId,
  62. pathname,
  63. new_pathname: newPathname,
  64. doc: doc.id,
  65. meta: { user_id: userId, ts: ts.getTime() },
  66. }
  67. }
  68. function olTextUpdate(doc, userId, ts, textOperation, v) {
  69. return {
  70. v2Authors: [userId],
  71. timestamp: ts.toJSON(),
  72. authors: [],
  73. operations: [
  74. {
  75. pathname: doc.pathname.replace(/^\//, ''), // Strip leading /
  76. textOperation,
  77. },
  78. ],
  79. v2DocVersions: {
  80. [doc.id]: {
  81. pathname: doc.pathname.replace(/^\//, ''), // Strip leading /
  82. v: v || 1,
  83. },
  84. },
  85. }
  86. }
  87. function olTextUpdates(doc, userId, ts, textOperations, v) {
  88. return {
  89. v2Authors: [userId],
  90. timestamp: ts.toJSON(),
  91. authors: [],
  92. operations: textOperations.map(textOperation => ({
  93. // Strip leading /
  94. pathname: doc.pathname.replace(/^\//, ''),
  95. textOperation,
  96. })),
  97. v2DocVersions: {
  98. [doc.id]: {
  99. pathname: doc.pathname.replace(/^\//, ''), // Strip leading /
  100. v: v || 1,
  101. },
  102. },
  103. }
  104. }
  105. function olRenameUpdate(doc, userId, ts, pathname, newPathname) {
  106. return {
  107. v2Authors: [userId],
  108. timestamp: ts.toJSON(),
  109. authors: [],
  110. operations: [
  111. {
  112. pathname,
  113. newPathname,
  114. },
  115. ],
  116. }
  117. }
  118. function olAddDocUpdate(doc, userId, ts, fileHash) {
  119. return {
  120. v2Authors: [userId],
  121. timestamp: ts.toJSON(),
  122. authors: [],
  123. operations: [
  124. {
  125. pathname: doc.pathname.replace(/^\//, ''), // Strip leading /
  126. file: {
  127. hash: fileHash,
  128. },
  129. },
  130. ],
  131. }
  132. }
  133. function olAddDocUpdateWithVersion(doc, userId, ts, fileHash, version) {
  134. const result = olAddDocUpdate(doc, userId, ts, fileHash)
  135. result.projectVersion = version
  136. return result
  137. }
  138. function olAddFileUpdate(file, userId, ts, fileHash) {
  139. return {
  140. v2Authors: [userId],
  141. timestamp: ts.toJSON(),
  142. authors: [],
  143. operations: [
  144. {
  145. pathname: file.pathname.replace(/^\//, ''), // Strip leading /
  146. file: {
  147. hash: fileHash,
  148. },
  149. },
  150. ],
  151. }
  152. }
  153. describe('Sending Updates', function () {
  154. const historyId = new ObjectId().toString()
  155. beforeEach(function (done) {
  156. this.timestamp = new Date()
  157. ProjectHistoryApp.ensureRunning(error => {
  158. if (error) {
  159. return done(error)
  160. }
  161. this.userId = new ObjectId().toString()
  162. this.projectId = new ObjectId().toString()
  163. this.docId = new ObjectId().toString()
  164. this.doc = {
  165. id: this.docId,
  166. pathname: '/main.tex',
  167. length: 5,
  168. }
  169. MockHistoryStore().post('/api/projects').reply(200, {
  170. projectId: historyId,
  171. })
  172. MockWeb()
  173. .get(`/project/${this.projectId}/details`)
  174. .reply(200, {
  175. name: 'Test Project',
  176. overleaf: {
  177. history: {
  178. id: historyId,
  179. },
  180. },
  181. })
  182. ProjectHistoryClient.initializeProject(historyId, done)
  183. })
  184. })
  185. afterEach(function () {
  186. nock.cleanAll()
  187. })
  188. describe('basic update types', function () {
  189. beforeEach(function () {
  190. MockHistoryStore()
  191. .get(`/api/projects/${historyId}/latest/history`)
  192. .reply(200, {
  193. chunk: {
  194. startVersion: 0,
  195. history: {
  196. snapshot: {},
  197. changes: [],
  198. },
  199. },
  200. })
  201. })
  202. it('should send add doc updates to the history store', function (done) {
  203. const fileHash = '0a207c060e61f3b88eaee0a8cd0696f46fb155eb'
  204. const createBlob = MockHistoryStore()
  205. .put(`/api/projects/${historyId}/blobs/${fileHash}`, 'a\nb')
  206. .reply(201)
  207. const addFile = MockHistoryStore()
  208. .post(`/api/projects/${historyId}/legacy_changes`, body => {
  209. expect(body).to.deep.equal([
  210. olAddDocUpdate(this.doc, this.userId, this.timestamp, fileHash),
  211. ])
  212. return true
  213. })
  214. .query({ end_version: 0 })
  215. .reply(204)
  216. async.series(
  217. [
  218. cb => {
  219. ProjectHistoryClient.pushRawUpdate(
  220. this.projectId,
  221. slAddDocUpdate(
  222. historyId,
  223. this.doc,
  224. this.userId,
  225. this.timestamp,
  226. 'a\nb'
  227. ),
  228. cb
  229. )
  230. },
  231. cb => {
  232. ProjectHistoryClient.flushProject(this.projectId, cb)
  233. },
  234. ],
  235. error => {
  236. if (error) {
  237. return done(error)
  238. }
  239. assert(
  240. createBlob.isDone(),
  241. '/api/projects/:historyId/blobs/:hash should have been called'
  242. )
  243. assert(
  244. addFile.isDone(),
  245. `/api/projects/${historyId}/changes should have been called`
  246. )
  247. done()
  248. }
  249. )
  250. })
  251. it('should strip non-BMP characters in add doc updates before sending to the history store', function (done) {
  252. const fileHash = '11509fe05a41f9cdc51ea081342b5a4fc7c8d0fc'
  253. const createBlob = MockHistoryStore()
  254. .put(
  255. `/api/projects/${historyId}/blobs/${fileHash}`,
  256. 'a\nb\uFFFD\uFFFDc'
  257. )
  258. .reply(201)
  259. const addFile = MockHistoryStore()
  260. .post(`/api/projects/${historyId}/legacy_changes`, body => {
  261. expect(body).to.deep.equal([
  262. olAddDocUpdate(this.doc, this.userId, this.timestamp, fileHash),
  263. ])
  264. return true
  265. })
  266. .query({ end_version: 0 })
  267. .reply(204)
  268. async.series(
  269. [
  270. cb => {
  271. ProjectHistoryClient.pushRawUpdate(
  272. this.projectId,
  273. slAddDocUpdate(
  274. historyId,
  275. this.doc,
  276. this.userId,
  277. this.timestamp,
  278. 'a\nb\uD800\uDC00c'
  279. ),
  280. cb
  281. )
  282. },
  283. cb => {
  284. ProjectHistoryClient.flushProject(this.projectId, cb)
  285. },
  286. ],
  287. error => {
  288. if (error) {
  289. return done(error)
  290. }
  291. assert(
  292. createBlob.isDone(),
  293. '/api/projects/:historyId/blobs/:hash should have been called'
  294. )
  295. assert(
  296. addFile.isDone(),
  297. `/api/projects/${historyId}/changes should have been called`
  298. )
  299. done()
  300. }
  301. )
  302. })
  303. it('should send text updates to the history store', function (done) {
  304. const createChange = MockHistoryStore()
  305. .post(`/api/projects/${historyId}/legacy_changes`, body => {
  306. expect(body).to.deep.equal([
  307. olTextUpdate(this.doc, this.userId, this.timestamp, [3, '\nc', 2]),
  308. ])
  309. return true
  310. })
  311. .query({ end_version: 0 })
  312. .reply(204)
  313. async.series(
  314. [
  315. cb => {
  316. ProjectHistoryClient.pushRawUpdate(
  317. this.projectId,
  318. slTextUpdate(
  319. historyId,
  320. this.doc,
  321. this.userId,
  322. 1,
  323. this.timestamp,
  324. [{ p: 3, i: '\nc' }]
  325. ),
  326. cb
  327. )
  328. },
  329. cb => {
  330. ProjectHistoryClient.flushProject(this.projectId, cb)
  331. },
  332. ],
  333. error => {
  334. if (error) {
  335. return done(error)
  336. }
  337. assert(
  338. createChange.isDone(),
  339. `/api/projects/${historyId}/changes should have been called`
  340. )
  341. done()
  342. }
  343. )
  344. })
  345. it('should send renames to the history store', function (done) {
  346. const createChange = MockHistoryStore()
  347. .post(`/api/projects/${historyId}/legacy_changes`, body => {
  348. expect(body).to.deep.equal([
  349. olRenameUpdate(
  350. this.doc,
  351. this.userId,
  352. this.timestamp,
  353. 'main.tex',
  354. 'main2.tex'
  355. ),
  356. ])
  357. return true
  358. })
  359. .query({ end_version: 0 })
  360. .reply(204)
  361. async.series(
  362. [
  363. cb => {
  364. ProjectHistoryClient.pushRawUpdate(
  365. this.projectId,
  366. slRenameUpdate(
  367. historyId,
  368. this.doc,
  369. this.userId,
  370. this.timestamp,
  371. '/main.tex',
  372. '/main2.tex'
  373. ),
  374. cb
  375. )
  376. },
  377. cb => {
  378. ProjectHistoryClient.flushProject(this.projectId, cb)
  379. },
  380. ],
  381. error => {
  382. if (error) {
  383. return done(error)
  384. }
  385. assert(
  386. createChange.isDone(),
  387. `/api/projects/${historyId}/changes should have been called`
  388. )
  389. done()
  390. }
  391. )
  392. })
  393. it('should send add file updates to the history store', function (done) {
  394. const file = {
  395. id: new ObjectId().toString(),
  396. pathname: '/test.png',
  397. contents: Buffer.from([1, 2, 3]),
  398. hash: 'aed2973e4b8a7ff1b30ff5c4751e5a2b38989e74',
  399. }
  400. const fileStoreRequest = MockFileStore()
  401. .get(`/project/${this.projectId}/file/${file.id}`)
  402. .reply(200, file.contents)
  403. const createBlob = MockHistoryStore()
  404. .put(`/api/projects/${historyId}/blobs/${file.hash}`, file.contents)
  405. .reply(201)
  406. const addFile = MockHistoryStore()
  407. .post(`/api/projects/${historyId}/legacy_changes`, body => {
  408. expect(body).to.deep.equal([
  409. olAddFileUpdate(file, this.userId, this.timestamp, file.hash),
  410. ])
  411. return true
  412. })
  413. .query({ end_version: 0 })
  414. .reply(204)
  415. async.series(
  416. [
  417. cb => {
  418. ProjectHistoryClient.pushRawUpdate(
  419. this.projectId,
  420. slAddFileUpdate(
  421. historyId,
  422. file,
  423. this.userId,
  424. this.timestamp,
  425. this.projectId
  426. ),
  427. cb
  428. )
  429. },
  430. cb => {
  431. ProjectHistoryClient.flushProject(this.projectId, cb)
  432. },
  433. ],
  434. error => {
  435. if (error) {
  436. return done(error)
  437. }
  438. assert(
  439. fileStoreRequest.isDone(),
  440. `/project/${this.projectId}/file/${file.id} should have been called`
  441. )
  442. assert(
  443. createBlob.isDone(),
  444. `/api/projects/${historyId}/latest/files should have been called`
  445. )
  446. assert(
  447. addFile.isDone(),
  448. `/api/projects/${historyId}/latest/files should have been called`
  449. )
  450. done()
  451. }
  452. )
  453. })
  454. it('should send a stub to the history store when the file is large', function (done) {
  455. const fileContents = Buffer.alloc(Settings.maxFileSizeInBytes + 1, 'X')
  456. const fileSize = Buffer.byteLength(fileContents)
  457. const fileHash = crypto
  458. .createHash('sha1')
  459. .update('blob ' + fileSize + '\x00')
  460. .update(fileContents, 'utf8')
  461. .digest('hex')
  462. const file = {
  463. id: new ObjectId().toString(),
  464. pathname: '/large.png',
  465. contents: fileContents,
  466. hash: fileHash,
  467. }
  468. const stubContents = [
  469. 'FileTooLargeError v1',
  470. 'File too large to be stored in history service',
  471. `id project-${this.projectId}-file-${file.id}`,
  472. `size ${fileSize} bytes`,
  473. `hash ${fileHash}`,
  474. '\0', // null byte to make this a binary file
  475. ].join('\n')
  476. const stubHash = crypto
  477. .createHash('sha1')
  478. .update('blob ' + Buffer.byteLength(stubContents) + '\x00')
  479. .update(stubContents, 'utf8')
  480. .digest('hex')
  481. const stub = {
  482. id: file.id,
  483. pathname: file.pathname,
  484. contents: stubContents,
  485. hash: stubHash,
  486. }
  487. const fileStoreRequest = MockFileStore()
  488. .get(`/project/${this.projectId}/file/${file.id}`)
  489. .reply(200, file.contents)
  490. const createBlob = MockHistoryStore()
  491. .put(`/api/projects/${historyId}/blobs/${stub.hash}`, stub.contents)
  492. .reply(201)
  493. const addFile = MockHistoryStore()
  494. .post(`/api/projects/${historyId}/legacy_changes`, body => {
  495. expect(body).to.deep.equal([
  496. olAddFileUpdate(stub, this.userId, this.timestamp, stub.hash),
  497. ])
  498. return true
  499. })
  500. .query({ end_version: 0 })
  501. .reply(204)
  502. async.series(
  503. [
  504. cb => {
  505. ProjectHistoryClient.pushRawUpdate(
  506. this.projectId,
  507. slAddFileUpdate(
  508. historyId,
  509. file,
  510. this.userId,
  511. this.timestamp,
  512. this.projectId
  513. ),
  514. cb
  515. )
  516. },
  517. cb => {
  518. ProjectHistoryClient.flushProject(this.projectId, cb)
  519. },
  520. ],
  521. error => {
  522. if (error) {
  523. return done(error)
  524. }
  525. assert(
  526. addFile.isDone(),
  527. `/api/projects/${historyId}/latest/files should have been called`
  528. )
  529. assert(
  530. createBlob.isDone(),
  531. `/api/projects/${historyId}/latest/files should have been called`
  532. )
  533. assert(
  534. fileStoreRequest.isDone(),
  535. `/project/${this.projectId}/file/${file.id} should have been called`
  536. )
  537. done()
  538. }
  539. )
  540. })
  541. it('should ignore comment ops', function (done) {
  542. const createChange = MockHistoryStore()
  543. .post(`/api/projects/${historyId}/legacy_changes`, body => {
  544. expect(body).to.deep.equal([
  545. olTextUpdate(this.doc, this.userId, this.timestamp, [3, '\nc', 2]),
  546. ])
  547. return true
  548. })
  549. .query({ end_version: 0 })
  550. .reply(204)
  551. async.series(
  552. [
  553. cb => {
  554. ProjectHistoryClient.pushRawUpdate(
  555. this.projectId,
  556. slTextUpdate(
  557. historyId,
  558. this.doc,
  559. this.userId,
  560. 1,
  561. this.timestamp,
  562. [
  563. { p: 3, i: '\nc' },
  564. { p: 3, c: '\nc' },
  565. ]
  566. ),
  567. cb
  568. )
  569. },
  570. cb => {
  571. ProjectHistoryClient.pushRawUpdate(
  572. this.projectId,
  573. slTextUpdate(
  574. historyId,
  575. this.doc,
  576. this.userId,
  577. 2,
  578. this.timestamp,
  579. [{ p: 2, c: 'b' }]
  580. ),
  581. cb
  582. )
  583. },
  584. cb => {
  585. ProjectHistoryClient.flushProject(this.projectId, cb)
  586. },
  587. ],
  588. error => {
  589. if (error) {
  590. return done(error)
  591. }
  592. assert(
  593. createChange.isDone(),
  594. `/api/projects/${historyId}/changes should have been called`
  595. )
  596. done()
  597. }
  598. )
  599. })
  600. it('should be able to process lots of updates in batches', function (done) {
  601. const BATCH_SIZE = 500
  602. const createFirstChangeBatch = MockHistoryStore()
  603. .post(`/api/projects/${historyId}/legacy_changes`, body => {
  604. expect(body).to.deep.equal([
  605. olTextUpdate(
  606. this.doc,
  607. this.userId,
  608. this.timestamp,
  609. ['a'.repeat(BATCH_SIZE), 6],
  610. BATCH_SIZE - 1
  611. ),
  612. ])
  613. return true
  614. })
  615. .query({ end_version: 0 })
  616. .reply(204)
  617. const createSecondChangeBatch = MockHistoryStore()
  618. .post(`/api/projects/${historyId}/legacy_changes`, body => {
  619. expect(body).to.deep.equal([
  620. olTextUpdate(
  621. this.doc,
  622. this.userId,
  623. this.timestamp,
  624. ['a'.repeat(50), BATCH_SIZE + 6],
  625. BATCH_SIZE - 1 + 50
  626. ),
  627. ])
  628. return true
  629. })
  630. .query({ end_version: 500 })
  631. .reply(204)
  632. // these need mocking again for the second batch
  633. MockHistoryStore()
  634. .get(`/api/projects/${historyId}/latest/history`)
  635. .reply(200, {
  636. chunk: {
  637. startVersion: BATCH_SIZE,
  638. history: {
  639. snapshot: {},
  640. changes: [],
  641. },
  642. },
  643. })
  644. MockWeb()
  645. .get(`/project/${this.projectId}/details`)
  646. .reply(200, {
  647. name: 'Test Project',
  648. overleaf: {
  649. history: {
  650. id: historyId,
  651. },
  652. },
  653. })
  654. const pushChange = (n, cb) => {
  655. this.doc.length += 1
  656. ProjectHistoryClient.pushRawUpdate(
  657. this.projectId,
  658. slTextUpdate(historyId, this.doc, this.userId, n, this.timestamp, [
  659. { p: 0, i: 'a' },
  660. ]),
  661. cb
  662. )
  663. }
  664. async.series(
  665. [
  666. cb => {
  667. async.times(BATCH_SIZE + 50, pushChange, cb)
  668. },
  669. cb => {
  670. ProjectHistoryClient.flushProject(this.projectId, cb)
  671. },
  672. ],
  673. error => {
  674. if (error) {
  675. return done(error)
  676. }
  677. assert(
  678. createFirstChangeBatch.isDone(),
  679. `/api/projects/${historyId}/changes should have been called for the first batch`
  680. )
  681. assert(
  682. createSecondChangeBatch.isDone(),
  683. `/api/projects/${historyId}/changes should have been called for the second batch`
  684. )
  685. done()
  686. }
  687. )
  688. })
  689. })
  690. describe('compressing updates', function () {
  691. beforeEach(function () {
  692. MockHistoryStore()
  693. .get(`/api/projects/${historyId}/latest/history`)
  694. .reply(200, {
  695. chunk: {
  696. startVersion: 0,
  697. history: {
  698. snapshot: {},
  699. changes: [],
  700. },
  701. },
  702. })
  703. })
  704. it('should concat adjacent text updates', function (done) {
  705. const createChange = MockHistoryStore()
  706. .post(`/api/projects/${historyId}/legacy_changes`, body => {
  707. expect(body).to.deep.equal([
  708. olTextUpdate(
  709. this.doc,
  710. this.userId,
  711. this.timestamp,
  712. [3, 'foobaz', 2],
  713. 2
  714. ),
  715. ])
  716. return true
  717. })
  718. .query({ end_version: 0 })
  719. .reply(204)
  720. async.series(
  721. [
  722. cb => {
  723. ProjectHistoryClient.pushRawUpdate(
  724. this.projectId,
  725. slTextUpdate(
  726. historyId,
  727. this.doc,
  728. this.userId,
  729. 1,
  730. this.timestamp,
  731. [
  732. { p: 3, i: 'foobar' },
  733. { p: 6, d: 'bar' },
  734. ]
  735. ),
  736. cb
  737. )
  738. this.doc.length += 3
  739. },
  740. cb => {
  741. ProjectHistoryClient.pushRawUpdate(
  742. this.projectId,
  743. slTextUpdate(
  744. historyId,
  745. this.doc,
  746. this.userId,
  747. 2,
  748. this.timestamp,
  749. [{ p: 6, i: 'baz' }]
  750. ),
  751. cb
  752. )
  753. },
  754. cb => {
  755. ProjectHistoryClient.flushProject(this.projectId, cb)
  756. },
  757. ],
  758. error => {
  759. if (error) {
  760. return done(error)
  761. }
  762. assert(
  763. createChange.isDone(),
  764. `/api/projects/${historyId}/changes should have been called`
  765. )
  766. done()
  767. }
  768. )
  769. })
  770. it('should take the timestamp of the first update', function (done) {
  771. const timestamp1 = new Date(this.timestamp)
  772. const timestamp2 = new Date(this.timestamp.getTime() + 10000)
  773. const createChange = MockHistoryStore()
  774. .post(`/api/projects/${historyId}/legacy_changes`, body => {
  775. expect(body).to.deep.equal([
  776. olTextUpdate(
  777. this.doc,
  778. this.userId,
  779. timestamp1,
  780. [3, 'foobaz', 2],
  781. 2
  782. ),
  783. ])
  784. return true
  785. })
  786. .query({ end_version: 0 })
  787. .reply(204)
  788. async.series(
  789. [
  790. cb => {
  791. ProjectHistoryClient.pushRawUpdate(
  792. this.projectId,
  793. slTextUpdate(historyId, this.doc, this.userId, 1, timestamp1, [
  794. { p: 3, i: 'foo' },
  795. ]),
  796. cb
  797. )
  798. this.doc.length += 3
  799. },
  800. cb => {
  801. ProjectHistoryClient.pushRawUpdate(
  802. this.projectId,
  803. slTextUpdate(historyId, this.doc, this.userId, 2, timestamp2, [
  804. { p: 6, i: 'baz' },
  805. ]),
  806. cb
  807. )
  808. },
  809. cb => {
  810. ProjectHistoryClient.flushProject(this.projectId, cb)
  811. },
  812. ],
  813. error => {
  814. if (error) {
  815. return done(error)
  816. }
  817. assert(
  818. createChange.isDone(),
  819. `/api/projects/${historyId}/changes should have been called`
  820. )
  821. done()
  822. }
  823. )
  824. })
  825. it('should not concat updates more than 60 seconds apart', function (done) {
  826. const timestamp1 = new Date(this.timestamp)
  827. const timestamp2 = new Date(this.timestamp.getTime() + 120000)
  828. const createChange = MockHistoryStore()
  829. .post(`/api/projects/${historyId}/legacy_changes`, body => {
  830. expect(body).to.deep.equal([
  831. olTextUpdate(this.doc, this.userId, timestamp1, [3, 'foo', 2], 1),
  832. olTextUpdate(this.doc, this.userId, timestamp2, [6, 'baz', 2], 2),
  833. ])
  834. return true
  835. })
  836. .query({ end_version: 0 })
  837. .reply(204)
  838. async.series(
  839. [
  840. cb => {
  841. ProjectHistoryClient.pushRawUpdate(
  842. this.projectId,
  843. slTextUpdate(historyId, this.doc, this.userId, 1, timestamp1, [
  844. { p: 3, i: 'foo' },
  845. ]),
  846. cb
  847. )
  848. this.doc.length += 3
  849. },
  850. cb => {
  851. ProjectHistoryClient.pushRawUpdate(
  852. this.projectId,
  853. slTextUpdate(historyId, this.doc, this.userId, 2, timestamp2, [
  854. { p: 6, i: 'baz' },
  855. ]),
  856. cb
  857. )
  858. },
  859. cb => {
  860. ProjectHistoryClient.flushProject(this.projectId, cb)
  861. },
  862. ],
  863. error => {
  864. if (error) {
  865. return done(error)
  866. }
  867. assert(
  868. createChange.isDone(),
  869. `/api/projects/${historyId}/changes should have been called`
  870. )
  871. done()
  872. }
  873. )
  874. })
  875. it('should not concat updates with different user_ids', function (done) {
  876. const userId1 = new ObjectId().toString()
  877. const userId2 = new ObjectId().toString()
  878. const createChange = MockHistoryStore()
  879. .post(`/api/projects/${historyId}/legacy_changes`, body => {
  880. expect(body).to.deep.equal([
  881. olTextUpdate(this.doc, userId1, this.timestamp, [3, 'foo', 2], 1),
  882. olTextUpdate(this.doc, userId2, this.timestamp, [6, 'baz', 2], 2),
  883. ])
  884. return true
  885. })
  886. .query({ end_version: 0 })
  887. .reply(204)
  888. async.series(
  889. [
  890. cb => {
  891. ProjectHistoryClient.pushRawUpdate(
  892. this.projectId,
  893. slTextUpdate(historyId, this.doc, userId1, 1, this.timestamp, [
  894. { p: 3, i: 'foo' },
  895. ]),
  896. cb
  897. )
  898. this.doc.length += 3
  899. },
  900. cb => {
  901. ProjectHistoryClient.pushRawUpdate(
  902. this.projectId,
  903. slTextUpdate(historyId, this.doc, userId2, 2, this.timestamp, [
  904. { p: 6, i: 'baz' },
  905. ]),
  906. cb
  907. )
  908. },
  909. cb => {
  910. ProjectHistoryClient.flushProject(this.projectId, cb)
  911. },
  912. ],
  913. error => {
  914. if (error) {
  915. return done(error)
  916. }
  917. assert(
  918. createChange.isDone(),
  919. `/api/projects/${historyId}/changes should have been called`
  920. )
  921. done()
  922. }
  923. )
  924. })
  925. it('should not concat updates with different docs', function (done) {
  926. const doc1 = {
  927. id: new ObjectId().toString(),
  928. pathname: '/doc1.tex',
  929. length: 10,
  930. }
  931. const doc2 = {
  932. id: new ObjectId().toString(),
  933. pathname: '/doc2.tex',
  934. length: 10,
  935. }
  936. const createChange = MockHistoryStore()
  937. .post(`/api/projects/${historyId}/legacy_changes`, body => {
  938. expect(body).to.deep.equal([
  939. olTextUpdate(doc1, this.userId, this.timestamp, [3, 'foo', 7], 1),
  940. olTextUpdate(doc2, this.userId, this.timestamp, [6, 'baz', 4], 2),
  941. ])
  942. return true
  943. })
  944. .query({ end_version: 0 })
  945. .reply(204)
  946. async.series(
  947. [
  948. cb => {
  949. ProjectHistoryClient.pushRawUpdate(
  950. this.projectId,
  951. slTextUpdate(historyId, doc1, this.userId, 1, this.timestamp, [
  952. { p: 3, i: 'foo' },
  953. ]),
  954. cb
  955. )
  956. },
  957. cb => {
  958. ProjectHistoryClient.pushRawUpdate(
  959. this.projectId,
  960. slTextUpdate(historyId, doc2, this.userId, 2, this.timestamp, [
  961. { p: 6, i: 'baz' },
  962. ]),
  963. cb
  964. )
  965. },
  966. cb => {
  967. ProjectHistoryClient.flushProject(this.projectId, cb)
  968. },
  969. ],
  970. error => {
  971. if (error) {
  972. return done(error)
  973. }
  974. assert(
  975. createChange.isDone(),
  976. `/api/projects/${historyId}/changes should have been called`
  977. )
  978. done()
  979. }
  980. )
  981. })
  982. it('should not send updates without any ops', function (done) {
  983. const createChange = MockHistoryStore()
  984. .post(`/api/projects/${historyId}/legacy_changes`, body => {
  985. expect(body).to.deep.equal([])
  986. return true
  987. })
  988. .query({ end_version: 0 })
  989. .reply(204)
  990. async.series(
  991. [
  992. cb => {
  993. // These blank ops can get sent by doc-updater on setDocs from Dropbox that don't change anything
  994. ProjectHistoryClient.pushRawUpdate(
  995. this.projectId,
  996. slTextUpdate(
  997. historyId,
  998. this.doc,
  999. this.userId,
  1000. 1,
  1001. this.timestamp,
  1002. []
  1003. ),
  1004. cb
  1005. )
  1006. },
  1007. cb => {
  1008. ProjectHistoryClient.flushProject(this.projectId, cb)
  1009. },
  1010. ],
  1011. error => {
  1012. if (error) {
  1013. return done(error)
  1014. }
  1015. assert(
  1016. !createChange.isDone(),
  1017. `/api/projects/${historyId}/changes should not have been called`
  1018. )
  1019. done()
  1020. }
  1021. )
  1022. })
  1023. it('should not send ops that compress to nothing', function (done) {
  1024. const createChange = MockHistoryStore()
  1025. .post(`/api/projects/${historyId}/legacy_changes`, body => {
  1026. expect(body).to.deep.equal([])
  1027. return true
  1028. })
  1029. .query({ end_version: 0 })
  1030. .reply(204)
  1031. async.series(
  1032. [
  1033. cb => {
  1034. ProjectHistoryClient.pushRawUpdate(
  1035. this.projectId,
  1036. slTextUpdate(
  1037. historyId,
  1038. this.doc,
  1039. this.userId,
  1040. 1,
  1041. this.timestamp,
  1042. [{ i: 'foo', p: 3 }]
  1043. ),
  1044. cb
  1045. )
  1046. this.doc.length += 3
  1047. },
  1048. cb => {
  1049. ProjectHistoryClient.pushRawUpdate(
  1050. this.projectId,
  1051. slTextUpdate(
  1052. historyId,
  1053. this.doc,
  1054. this.userId,
  1055. 2,
  1056. this.timestamp,
  1057. [{ d: 'foo', p: 3 }]
  1058. ),
  1059. cb
  1060. )
  1061. },
  1062. cb => {
  1063. ProjectHistoryClient.flushProject(this.projectId, cb)
  1064. },
  1065. ],
  1066. error => {
  1067. if (error) {
  1068. return done(error)
  1069. }
  1070. assert(
  1071. !createChange.isDone(),
  1072. `/api/projects/${historyId}/changes should not have been called`
  1073. )
  1074. done()
  1075. }
  1076. )
  1077. })
  1078. it('should not send ops from a diff that are blank', function (done) {
  1079. this.doc.length = 300
  1080. // Test case taken from a real life document where it was generating blank insert and
  1081. // delete ops from a diff, and the blank delete was erroring on the OL history from
  1082. // a text operation like [42, 0, 512], where the 0 was invalid.
  1083. const createChange = MockHistoryStore()
  1084. .post(`/api/projects/${historyId}/legacy_changes`, body => {
  1085. expect(body).to.deep.equal([
  1086. olTextUpdates(this.doc, this.userId, this.timestamp, [
  1087. [
  1088. 87,
  1089. -1,
  1090. 67,
  1091. '|l|ll|}\n\\hline',
  1092. -4,
  1093. 30,
  1094. ' \\hline',
  1095. 87,
  1096. ' \\\\ \\hline',
  1097. 24,
  1098. ],
  1099. ]),
  1100. ])
  1101. return true
  1102. })
  1103. .query({ end_version: 0 })
  1104. .reply(204)
  1105. async.series(
  1106. [
  1107. cb => {
  1108. ProjectHistoryClient.pushRawUpdate(
  1109. this.projectId,
  1110. slTextUpdate(
  1111. historyId,
  1112. this.doc,
  1113. this.userId,
  1114. 1,
  1115. this.timestamp,
  1116. [
  1117. {
  1118. p: 73,
  1119. d: '\\begin{table}[h]\n\\centering\n\\caption{My caption}\n\\label{my-label}\n\\begin{tabular}{lll}\n & A & B \\\\\nLiter t up & 2 & 1 \\\\\nLiter Whiskey & 1 & 2 \\\\\nPris pr. liter & 200 & 250\n\\end{tabular}\n\\end{table}',
  1120. },
  1121. {
  1122. p: 73,
  1123. i: '\\begin{table}[]\n\\centering\n\\caption{My caption}\n\\label{my-label}\n\\begin{tabular}{|l|ll|}\n\\hline\n & A & B \\\\ \\hline\nLiter t up & 2 & 1 \\\\\nLiter Whiskey & 1 & 2 \\\\\nPris pr. liter & 200 & 250 \\\\ \\hline\n\\end{tabular}\n\\end{table}',
  1124. },
  1125. ]
  1126. ),
  1127. cb
  1128. )
  1129. },
  1130. cb => {
  1131. ProjectHistoryClient.flushProject(this.projectId, cb)
  1132. },
  1133. ],
  1134. error => {
  1135. if (error) {
  1136. return done(error)
  1137. }
  1138. assert(
  1139. createChange.isDone(),
  1140. `/api/projects/${historyId}/changes should have been called`
  1141. )
  1142. done()
  1143. }
  1144. )
  1145. })
  1146. it('should not concat text updates across project structure ops', function (done) {
  1147. const newDoc = {
  1148. id: new ObjectId().toString(),
  1149. pathname: '/main.tex',
  1150. hash: '0a207c060e61f3b88eaee0a8cd0696f46fb155eb',
  1151. docLines: 'a\nb',
  1152. }
  1153. MockHistoryStore()
  1154. .put(`/api/projects/${historyId}/blobs/${newDoc.hash}`)
  1155. .reply(201)
  1156. const createChange = MockHistoryStore()
  1157. .post(`/api/projects/${historyId}/legacy_changes`, body => {
  1158. expect(body).to.deep.equal([
  1159. olTextUpdate(
  1160. this.doc,
  1161. this.userId,
  1162. this.timestamp,
  1163. [3, 'foo', 2],
  1164. 1
  1165. ),
  1166. olAddDocUpdate(newDoc, this.userId, this.timestamp, newDoc.hash),
  1167. olTextUpdate(
  1168. this.doc,
  1169. this.userId,
  1170. this.timestamp,
  1171. [6, 'baz', 2],
  1172. 2
  1173. ),
  1174. ])
  1175. return true
  1176. })
  1177. .query({ end_version: 0 })
  1178. .reply(204)
  1179. async.series(
  1180. [
  1181. cb => {
  1182. ProjectHistoryClient.pushRawUpdate(
  1183. this.projectId,
  1184. slTextUpdate(
  1185. historyId,
  1186. this.doc,
  1187. this.userId,
  1188. 1,
  1189. this.timestamp,
  1190. [
  1191. { p: 3, i: 'foobar' },
  1192. { p: 6, d: 'bar' },
  1193. ]
  1194. ),
  1195. cb
  1196. )
  1197. this.doc.length += 3
  1198. },
  1199. cb => {
  1200. ProjectHistoryClient.pushRawUpdate(
  1201. this.projectId,
  1202. slAddDocUpdate(
  1203. historyId,
  1204. newDoc,
  1205. this.userId,
  1206. this.timestamp,
  1207. newDoc.docLines
  1208. ),
  1209. cb
  1210. )
  1211. },
  1212. cb => {
  1213. ProjectHistoryClient.pushRawUpdate(
  1214. this.projectId,
  1215. slTextUpdate(
  1216. historyId,
  1217. this.doc,
  1218. this.userId,
  1219. 2,
  1220. this.timestamp,
  1221. [{ p: 6, i: 'baz' }]
  1222. ),
  1223. cb
  1224. )
  1225. },
  1226. cb => {
  1227. ProjectHistoryClient.flushProject(this.projectId, cb)
  1228. },
  1229. ],
  1230. error => {
  1231. if (error) {
  1232. return done(error)
  1233. }
  1234. assert(
  1235. createChange.isDone(),
  1236. `/api/projects/${historyId}/changes should have been called`
  1237. )
  1238. done()
  1239. }
  1240. )
  1241. })
  1242. it('should track the doc length when splitting ops', function (done) {
  1243. this.doc.length = 10
  1244. const createChange = MockHistoryStore()
  1245. .post(`/api/projects/${historyId}/legacy_changes`, body => {
  1246. expect(body).to.deep.equal([
  1247. olTextUpdate(this.doc, this.userId, this.timestamp, [3, -3, 4], 1),
  1248. olTextUpdate(
  1249. this.doc,
  1250. this.userId,
  1251. this.timestamp,
  1252. [3, 'barbaz', 4],
  1253. 2
  1254. ), // This has a base length of 10
  1255. ])
  1256. return true
  1257. })
  1258. .query({ end_version: 0 })
  1259. .reply(204)
  1260. async.series(
  1261. [
  1262. cb => {
  1263. ProjectHistoryClient.pushRawUpdate(
  1264. this.projectId,
  1265. slTextUpdate(
  1266. historyId,
  1267. this.doc,
  1268. this.userId,
  1269. 1,
  1270. this.timestamp,
  1271. [
  1272. { p: 3, d: 'foo' },
  1273. { p: 3, i: 'bar' }, // Make sure the length of the op generated from this is 7, not 10
  1274. ]
  1275. ),
  1276. cb
  1277. )
  1278. },
  1279. cb => {
  1280. ProjectHistoryClient.pushRawUpdate(
  1281. this.projectId,
  1282. slTextUpdate(
  1283. historyId,
  1284. this.doc,
  1285. this.userId,
  1286. 2,
  1287. this.timestamp,
  1288. [{ p: 6, i: 'baz' }]
  1289. ),
  1290. cb
  1291. )
  1292. },
  1293. cb => {
  1294. ProjectHistoryClient.flushProject(this.projectId, cb)
  1295. },
  1296. ],
  1297. error => {
  1298. if (error) {
  1299. return done(error)
  1300. }
  1301. assert(
  1302. createChange.isDone(),
  1303. `/api/projects/${historyId}/changes should have been called`
  1304. )
  1305. done()
  1306. }
  1307. )
  1308. })
  1309. })
  1310. describe('with bad pathnames', function () {
  1311. beforeEach(function () {
  1312. MockHistoryStore()
  1313. .get(`/api/projects/${historyId}/latest/history`)
  1314. .reply(200, {
  1315. chunk: {
  1316. startVersion: 0,
  1317. history: {
  1318. snapshot: {},
  1319. changes: [],
  1320. },
  1321. },
  1322. })
  1323. })
  1324. it('should replace \\ with _ and workaround * in pathnames', function (done) {
  1325. const doc = {
  1326. id: this.doc.id,
  1327. pathname: '\\main.tex',
  1328. hash: 'b07b6b7a27667965f733943737124395c7577bea',
  1329. docLines: 'aaabbbccc',
  1330. length: 9,
  1331. }
  1332. MockHistoryStore()
  1333. .put(`/api/projects/${historyId}/blobs/${doc.hash}`)
  1334. .reply(201)
  1335. const createChange = MockHistoryStore()
  1336. .post(`/api/projects/${historyId}/legacy_changes`, body => {
  1337. expect(body).to.deep.equal([
  1338. olAddDocUpdate(
  1339. { id: doc.id, pathname: '_main.tex' },
  1340. this.userId,
  1341. this.timestamp,
  1342. doc.hash
  1343. ),
  1344. olRenameUpdate(
  1345. { id: doc.id, pathname: '_main.tex' },
  1346. this.userId,
  1347. this.timestamp,
  1348. '_main.tex',
  1349. '_main2.tex'
  1350. ),
  1351. olTextUpdate(
  1352. { id: doc.id, pathname: '_main2.tex' },
  1353. this.userId,
  1354. this.timestamp,
  1355. [3, 'foo', 6],
  1356. 2
  1357. ),
  1358. olRenameUpdate(
  1359. { id: doc.id, pathname: '_main2.tex' },
  1360. this.userId,
  1361. this.timestamp,
  1362. '_main2.tex',
  1363. '_main__ASTERISK__.tex'
  1364. ),
  1365. ])
  1366. return true
  1367. })
  1368. .query({ end_version: 0 })
  1369. .reply(204)
  1370. async.series(
  1371. [
  1372. cb => {
  1373. ProjectHistoryClient.pushRawUpdate(
  1374. this.projectId,
  1375. slAddDocUpdate(
  1376. historyId,
  1377. doc,
  1378. this.userId,
  1379. this.timestamp,
  1380. doc.docLines
  1381. ),
  1382. cb
  1383. )
  1384. },
  1385. cb => {
  1386. ProjectHistoryClient.pushRawUpdate(
  1387. this.projectId,
  1388. slRenameUpdate(
  1389. historyId,
  1390. doc,
  1391. this.userId,
  1392. this.timestamp,
  1393. '/\\main.tex',
  1394. '/\\main2.tex'
  1395. ),
  1396. cb
  1397. )
  1398. doc.pathname = '\\main2.tex'
  1399. },
  1400. cb => {
  1401. ProjectHistoryClient.pushRawUpdate(
  1402. this.projectId,
  1403. slTextUpdate(historyId, doc, this.userId, 2, this.timestamp, [
  1404. { p: 3, i: 'foo' },
  1405. ]),
  1406. cb
  1407. )
  1408. },
  1409. cb => {
  1410. ProjectHistoryClient.pushRawUpdate(
  1411. this.projectId,
  1412. slRenameUpdate(
  1413. historyId,
  1414. doc,
  1415. this.userId,
  1416. this.timestamp,
  1417. '/\\main2.tex',
  1418. '/\\main*.tex'
  1419. ),
  1420. cb
  1421. )
  1422. doc.pathname = '\\main*.tex'
  1423. },
  1424. cb => {
  1425. ProjectHistoryClient.flushProject(this.projectId, cb)
  1426. },
  1427. ],
  1428. error => {
  1429. if (error) {
  1430. return done(error)
  1431. }
  1432. assert(
  1433. createChange.isDone(),
  1434. `/api/projects/${historyId}/changes should have been called`
  1435. )
  1436. done()
  1437. }
  1438. )
  1439. })
  1440. it('should workaround pathnames beginning with spaces', function (done) {
  1441. const doc = {
  1442. id: this.doc.id,
  1443. pathname: 'main.tex',
  1444. hash: 'b07b6b7a27667965f733943737124395c7577bea',
  1445. docLines: 'aaabbbccc',
  1446. length: 9,
  1447. }
  1448. MockHistoryStore()
  1449. .put(`/api/projects/${historyId}/blobs/${doc.hash}`)
  1450. .reply(201)
  1451. const createChange = MockHistoryStore()
  1452. .post(`/api/projects/${historyId}/legacy_changes`, body => {
  1453. expect(body).to.deep.equal([
  1454. olAddDocUpdate(
  1455. { id: doc.id, pathname: 'main.tex' },
  1456. this.userId,
  1457. this.timestamp,
  1458. doc.hash
  1459. ),
  1460. olRenameUpdate(
  1461. { id: doc.id },
  1462. this.userId,
  1463. this.timestamp,
  1464. 'main.tex',
  1465. 'foo/__SPACE__main.tex'
  1466. ),
  1467. ])
  1468. return true
  1469. })
  1470. .query({ end_version: 0 })
  1471. .reply(204)
  1472. async.series(
  1473. [
  1474. cb => {
  1475. ProjectHistoryClient.pushRawUpdate(
  1476. this.projectId,
  1477. slAddDocUpdate(
  1478. historyId,
  1479. doc,
  1480. this.userId,
  1481. this.timestamp,
  1482. doc.docLines
  1483. ),
  1484. cb
  1485. )
  1486. },
  1487. cb => {
  1488. ProjectHistoryClient.pushRawUpdate(
  1489. this.projectId,
  1490. slRenameUpdate(
  1491. historyId,
  1492. doc,
  1493. this.userId,
  1494. this.timestamp,
  1495. '/main.tex',
  1496. '/foo/ main.tex'
  1497. ),
  1498. cb
  1499. )
  1500. doc.pathname = '/foo/ main.tex'
  1501. },
  1502. cb => {
  1503. ProjectHistoryClient.flushProject(this.projectId, cb)
  1504. },
  1505. ],
  1506. error => {
  1507. if (error) {
  1508. return done(error)
  1509. }
  1510. assert(
  1511. createChange.isDone(),
  1512. `/api/projects/${historyId}/changes should have been called`
  1513. )
  1514. done()
  1515. }
  1516. )
  1517. })
  1518. })
  1519. describe('with bad response from filestore', function () {
  1520. beforeEach(function () {
  1521. MockHistoryStore()
  1522. .get(`/api/projects/${historyId}/latest/history`)
  1523. .reply(200, {
  1524. chunk: {
  1525. startVersion: 0,
  1526. history: {
  1527. snapshot: {},
  1528. changes: [],
  1529. },
  1530. },
  1531. })
  1532. })
  1533. it('should return a 500 if the filestore returns a 500', function (done) {
  1534. const file = {
  1535. id: new ObjectId().toString(),
  1536. pathname: '/test.png',
  1537. contents: Buffer.from([1, 2, 3]),
  1538. hash: 'aed2973e4b8a7ff1b30ff5c4751e5a2b38989e74',
  1539. }
  1540. const fileStoreRequest = MockFileStore()
  1541. .get(`/project/${this.projectId}/file/${file.id}`)
  1542. .reply(500)
  1543. const createBlob = MockHistoryStore()
  1544. .put(`/api/projects/${historyId}/blobs/${file.hash}`, file.contents)
  1545. .reply(201)
  1546. const addFile = MockHistoryStore()
  1547. .post(`/api/projects/${historyId}/legacy_changes`, body => {
  1548. expect(body).to.deep.equal([
  1549. olAddFileUpdate(file, this.userId, this.timestamp, file.hash),
  1550. ])
  1551. return true
  1552. })
  1553. .query({ end_version: 0 })
  1554. .reply(204)
  1555. async.series(
  1556. [
  1557. cb => {
  1558. ProjectHistoryClient.pushRawUpdate(
  1559. this.projectId,
  1560. slAddFileUpdate(
  1561. historyId,
  1562. file,
  1563. this.userId,
  1564. this.timestamp,
  1565. this.projectId
  1566. ),
  1567. cb
  1568. )
  1569. },
  1570. cb => {
  1571. ProjectHistoryClient.flushProject(
  1572. this.projectId,
  1573. { allowErrors: true },
  1574. (error, res) => {
  1575. if (error) {
  1576. return cb(error)
  1577. }
  1578. expect(res.statusCode).to.equal(500)
  1579. cb()
  1580. }
  1581. )
  1582. },
  1583. ],
  1584. error => {
  1585. if (error) {
  1586. return done(error)
  1587. }
  1588. assert(
  1589. fileStoreRequest.isDone(),
  1590. `/project/${this.projectId}/file/${file.id} should have been called`
  1591. )
  1592. assert(
  1593. !createBlob.isDone(),
  1594. `/api/projects/${historyId}/latest/files should not have been called`
  1595. )
  1596. assert(
  1597. !addFile.isDone(),
  1598. `/api/projects/${historyId}/latest/files should not have been called`
  1599. )
  1600. done()
  1601. }
  1602. )
  1603. })
  1604. it('should return a 500 if the filestore request errors', function (done) {
  1605. const file = {
  1606. id: new ObjectId().toString(),
  1607. pathname: '/test.png',
  1608. contents: Buffer.from([1, 2, 3]),
  1609. hash: 'aed2973e4b8a7ff1b30ff5c4751e5a2b38989e74',
  1610. }
  1611. const fileStoreRequest = MockFileStore()
  1612. .get(`/project/${this.projectId}/file/${file.id}`)
  1613. .replyWithError('oh no!')
  1614. const createBlob = MockHistoryStore()
  1615. .put(`/api/projects/${historyId}/blobs/${file.hash}`, file.contents)
  1616. .reply(201)
  1617. const addFile = MockHistoryStore()
  1618. .post(`/api/projects/${historyId}/legacy_changes`, body => {
  1619. expect(body).to.deep.equal([
  1620. olAddFileUpdate(file, this.userId, this.timestamp, file.hash),
  1621. ])
  1622. return true
  1623. })
  1624. .query({ end_version: 0 })
  1625. .reply(204)
  1626. async.series(
  1627. [
  1628. cb => {
  1629. ProjectHistoryClient.pushRawUpdate(
  1630. this.projectId,
  1631. slAddFileUpdate(
  1632. historyId,
  1633. file,
  1634. this.userId,
  1635. this.timestamp,
  1636. this.projectId
  1637. ),
  1638. cb
  1639. )
  1640. },
  1641. cb => {
  1642. ProjectHistoryClient.flushProject(
  1643. this.projectId,
  1644. { allowErrors: true },
  1645. (error, res) => {
  1646. if (error) {
  1647. return cb(error)
  1648. }
  1649. expect(res.statusCode).to.equal(500)
  1650. cb()
  1651. }
  1652. )
  1653. },
  1654. ],
  1655. error => {
  1656. if (error) {
  1657. return done(error)
  1658. }
  1659. assert(
  1660. fileStoreRequest.isDone(),
  1661. `/project/${this.projectId}/file/${file.id} should have been called`
  1662. )
  1663. assert(
  1664. !createBlob.isDone(),
  1665. `/api/projects/${historyId}/latest/files should not have been called`
  1666. )
  1667. assert(
  1668. !addFile.isDone(),
  1669. `/api/projects/${historyId}/latest/files should not have been called`
  1670. )
  1671. done()
  1672. }
  1673. )
  1674. })
  1675. })
  1676. describe('with an existing projectVersion field', function () {
  1677. beforeEach(function () {
  1678. MockHistoryStore()
  1679. .get(`/api/projects/${historyId}/latest/history`)
  1680. .reply(200, {
  1681. chunk: {
  1682. startVersion: 0,
  1683. history: {
  1684. snapshot: { projectVersion: '100.0' },
  1685. changes: [],
  1686. },
  1687. },
  1688. })
  1689. })
  1690. it('should discard project structure updates which have already been applied', function (done) {
  1691. const newDoc = []
  1692. for (let i = 0; i <= 2; i++) {
  1693. newDoc[i] = {
  1694. id: new ObjectId().toString(),
  1695. pathname: `/main${i}.tex`,
  1696. hash: '0a207c060e61f3b88eaee0a8cd0696f46fb155eb',
  1697. docLines: 'a\nb',
  1698. }
  1699. }
  1700. MockHistoryStore()
  1701. .put(`/api/projects/${historyId}/blobs/${newDoc[0].hash}`)
  1702. .times(3)
  1703. .reply(201)
  1704. const createChange = MockHistoryStore()
  1705. .post(`/api/projects/${historyId}/legacy_changes`, body => {
  1706. expect(body).to.deep.equal([
  1707. olAddDocUpdateWithVersion(
  1708. newDoc[1],
  1709. this.userId,
  1710. this.timestamp,
  1711. newDoc[1].hash,
  1712. '101.0'
  1713. ),
  1714. olAddDocUpdateWithVersion(
  1715. newDoc[2],
  1716. this.userId,
  1717. this.timestamp,
  1718. newDoc[2].hash,
  1719. '102.0'
  1720. ),
  1721. ])
  1722. return true
  1723. })
  1724. .query({ end_version: 0 })
  1725. .reply(204)
  1726. async.series(
  1727. [
  1728. cb => {
  1729. ProjectHistoryClient.pushRawUpdate(
  1730. this.projectId,
  1731. slAddDocUpdateWithVersion(
  1732. historyId,
  1733. newDoc[0],
  1734. this.userId,
  1735. this.timestamp,
  1736. newDoc[0].docLines,
  1737. '100.0'
  1738. ),
  1739. cb
  1740. )
  1741. },
  1742. cb => {
  1743. ProjectHistoryClient.pushRawUpdate(
  1744. this.projectId,
  1745. slAddDocUpdateWithVersion(
  1746. historyId,
  1747. newDoc[1],
  1748. this.userId,
  1749. this.timestamp,
  1750. newDoc[1].docLines,
  1751. '101.0'
  1752. ),
  1753. cb
  1754. )
  1755. },
  1756. cb => {
  1757. ProjectHistoryClient.pushRawUpdate(
  1758. this.projectId,
  1759. slAddDocUpdateWithVersion(
  1760. historyId,
  1761. newDoc[2],
  1762. this.userId,
  1763. this.timestamp,
  1764. newDoc[2].docLines,
  1765. '102.0'
  1766. ),
  1767. cb
  1768. )
  1769. },
  1770. cb => {
  1771. ProjectHistoryClient.flushProject(this.projectId, cb)
  1772. },
  1773. ],
  1774. error => {
  1775. if (error) {
  1776. return done(error)
  1777. }
  1778. assert(
  1779. createChange.isDone(),
  1780. `/api/projects/${historyId}/changes should have been called`
  1781. )
  1782. done()
  1783. }
  1784. )
  1785. })
  1786. })
  1787. describe('with an existing docVersions field', function () {
  1788. beforeEach(function () {
  1789. MockHistoryStore()
  1790. .get(`/api/projects/${historyId}/latest/history`)
  1791. .reply(200, {
  1792. chunk: {
  1793. startVersion: 0,
  1794. history: {
  1795. snapshot: { v2DocVersions: { [this.doc.id]: { v: 100 } } }, // version 100 below already applied
  1796. changes: [],
  1797. },
  1798. },
  1799. })
  1800. })
  1801. it('should discard doc updates which have already been applied', function (done) {
  1802. const createChange = MockHistoryStore()
  1803. .post(`/api/projects/${historyId}/legacy_changes`, body => {
  1804. expect(body).to.deep.equal([
  1805. olTextUpdate(
  1806. this.doc,
  1807. this.userId,
  1808. this.timestamp,
  1809. [6, 'baz', 2],
  1810. 101
  1811. ),
  1812. ])
  1813. return true
  1814. })
  1815. .query({ end_version: 0 })
  1816. .reply(204)
  1817. async.series(
  1818. [
  1819. cb => {
  1820. ProjectHistoryClient.pushRawUpdate(
  1821. this.projectId,
  1822. slTextUpdate(
  1823. historyId,
  1824. this.doc,
  1825. this.userId,
  1826. 100,
  1827. this.timestamp,
  1828. [
  1829. { p: 3, i: 'foobar' }, // these ops should be skipped
  1830. { p: 6, d: 'bar' },
  1831. ]
  1832. ),
  1833. cb
  1834. )
  1835. this.doc.length += 3
  1836. },
  1837. cb => {
  1838. ProjectHistoryClient.pushRawUpdate(
  1839. this.projectId,
  1840. slTextUpdate(
  1841. historyId,
  1842. this.doc,
  1843. this.userId,
  1844. 101,
  1845. this.timestamp,
  1846. [
  1847. { p: 6, i: 'baz' }, // this op should be applied
  1848. ]
  1849. ),
  1850. cb
  1851. )
  1852. },
  1853. cb => {
  1854. ProjectHistoryClient.flushProject(this.projectId, cb)
  1855. },
  1856. ],
  1857. error => {
  1858. if (error) {
  1859. return done(error)
  1860. }
  1861. assert(
  1862. createChange.isDone(),
  1863. `/api/projects/${historyId}/changes should have been called`
  1864. )
  1865. done()
  1866. }
  1867. )
  1868. })
  1869. })
  1870. })