SendingUpdatesTests.js 52 KB

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