UpdateTranslatorTests.js 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838
  1. import sinon from 'sinon'
  2. import { expect } from 'chai'
  3. import { strict as esmock } from 'esmock'
  4. import Core from 'overleaf-editor-core'
  5. const MODULE_PATH = '../../../../app/js/UpdateTranslator.js'
  6. describe('UpdateTranslator', function () {
  7. beforeEach(async function () {
  8. this.UpdateTranslator = await esmock(MODULE_PATH, {
  9. 'overleaf-editor-core': Core,
  10. })
  11. this.callback = sinon.stub()
  12. this.project_id = '59bfd450e3028c4d40a1e9aa'
  13. this.doc_id = '59bfd450e3028c4d40a1e9ab'
  14. this.file_id = '59bfd450e3028c4d40a1easd'
  15. this.user_id = '59bb9051abf6e8682a269b64'
  16. this.version = 0
  17. this.timestamp = new Date().toJSON()
  18. this.mockBlobHash = '12345abc12345abc12345abc12345abc12345abc'
  19. })
  20. describe('convertToChanges', function () {
  21. it('can translate doc additions', function (done) {
  22. const updates = [
  23. {
  24. update: {
  25. doc: this.doc_id,
  26. pathname: '/main.tex',
  27. docLines: 'a\nb',
  28. meta: {
  29. user_id: this.user_id,
  30. ts: this.timestamp,
  31. },
  32. },
  33. blobHash: this.mockBlobHash,
  34. },
  35. ]
  36. const assertion = (error, changes) => {
  37. changes = changes.map(change => change.toRaw())
  38. expect(error).to.be.null
  39. expect(changes).to.deep.equal([
  40. {
  41. authors: [],
  42. operations: [
  43. {
  44. pathname: 'main.tex',
  45. file: {
  46. hash: this.mockBlobHash,
  47. },
  48. },
  49. ],
  50. v2Authors: [this.user_id],
  51. timestamp: this.timestamp,
  52. },
  53. ])
  54. done()
  55. }
  56. this.UpdateTranslator.convertToChanges(
  57. this.project_id,
  58. updates,
  59. assertion
  60. )
  61. })
  62. it('can translate file additions', function (done) {
  63. const updates = [
  64. {
  65. update: {
  66. file: this.file_id,
  67. pathname: '/test.png',
  68. url: 'filestore.example.com/test.png',
  69. meta: {
  70. user_id: this.user_id,
  71. ts: this.timestamp,
  72. },
  73. },
  74. blobHash: this.mockBlobHash,
  75. },
  76. ]
  77. const assertion = (error, changes) => {
  78. changes = changes.map(change => change.toRaw())
  79. expect(error).to.be.null
  80. expect(changes).to.deep.equal([
  81. {
  82. authors: [],
  83. operations: [
  84. {
  85. pathname: 'test.png',
  86. file: {
  87. hash: this.mockBlobHash,
  88. },
  89. },
  90. ],
  91. v2Authors: [this.user_id],
  92. timestamp: this.timestamp,
  93. },
  94. ])
  95. done()
  96. }
  97. this.UpdateTranslator.convertToChanges(
  98. this.project_id,
  99. updates,
  100. assertion
  101. )
  102. })
  103. it('can translate doc renames', function (done) {
  104. const updates = [
  105. {
  106. update: {
  107. doc: this.doc_id,
  108. pathname: '/main.tex',
  109. new_pathname: '/new_main.tex',
  110. meta: {
  111. user_id: this.user_id,
  112. ts: this.timestamp,
  113. },
  114. },
  115. },
  116. ]
  117. const assertion = (error, changes) => {
  118. changes = changes.map(change => change.toRaw())
  119. expect(error).to.be.null
  120. expect(changes).to.deep.equal([
  121. {
  122. authors: [],
  123. operations: [
  124. {
  125. pathname: 'main.tex',
  126. newPathname: 'new_main.tex',
  127. },
  128. ],
  129. v2Authors: [this.user_id],
  130. timestamp: this.timestamp,
  131. },
  132. ])
  133. done()
  134. }
  135. this.UpdateTranslator.convertToChanges(
  136. this.project_id,
  137. updates,
  138. assertion
  139. )
  140. })
  141. it('can translate file renames', function (done) {
  142. const updates = [
  143. {
  144. update: {
  145. file: this.file_id,
  146. pathname: '/test.png',
  147. new_pathname: '/new_test.png',
  148. meta: {
  149. user_id: this.user_id,
  150. ts: this.timestamp,
  151. },
  152. },
  153. },
  154. ]
  155. const assertion = (error, changes) => {
  156. changes = changes.map(change => change.toRaw())
  157. expect(error).to.be.null
  158. expect(changes).to.deep.equal([
  159. {
  160. authors: [],
  161. operations: [
  162. {
  163. pathname: 'test.png',
  164. newPathname: 'new_test.png',
  165. },
  166. ],
  167. v2Authors: [this.user_id],
  168. timestamp: this.timestamp,
  169. },
  170. ])
  171. done()
  172. }
  173. this.UpdateTranslator.convertToChanges(
  174. this.project_id,
  175. updates,
  176. assertion
  177. )
  178. })
  179. it('can translate multiple updates with the correct versions', function (done) {
  180. const updates = [
  181. {
  182. update: {
  183. doc: this.doc_id,
  184. pathname: '/main.tex',
  185. docLines: 'a\nb',
  186. meta: {
  187. user_id: this.user_id,
  188. ts: this.timestamp,
  189. },
  190. },
  191. blobHash: this.mockBlobHash,
  192. },
  193. {
  194. update: {
  195. file: this.file_id,
  196. pathname: '/test.png',
  197. url: 'filestore.example.com/test.png',
  198. meta: {
  199. user_id: this.user_id,
  200. ts: this.timestamp,
  201. },
  202. },
  203. blobHash: this.mockBlobHash,
  204. },
  205. ]
  206. const assertion = (error, changes) => {
  207. changes = changes.map(change => change.toRaw())
  208. expect(error).to.be.null
  209. expect(changes).to.deep.equal([
  210. {
  211. authors: [],
  212. operations: [
  213. {
  214. pathname: 'main.tex',
  215. file: {
  216. hash: this.mockBlobHash,
  217. },
  218. },
  219. ],
  220. v2Authors: [this.user_id],
  221. timestamp: this.timestamp,
  222. },
  223. {
  224. authors: [],
  225. operations: [
  226. {
  227. pathname: 'test.png',
  228. file: {
  229. hash: this.mockBlobHash,
  230. },
  231. },
  232. ],
  233. v2Authors: [this.user_id],
  234. timestamp: this.timestamp,
  235. },
  236. ])
  237. done()
  238. }
  239. this.UpdateTranslator.convertToChanges(
  240. this.project_id,
  241. updates,
  242. assertion
  243. )
  244. })
  245. it('returns an error if the update has an unknown format', function (done) {
  246. const updates = [
  247. {
  248. update: {
  249. foo: 'bar',
  250. },
  251. },
  252. ]
  253. const assertion = (error, changes) => {
  254. expect(error)
  255. .to.exist.and.be.instanceof(Error)
  256. .and.have.property('message', 'update with unknown format')
  257. done()
  258. }
  259. this.UpdateTranslator.convertToChanges(
  260. this.project_id,
  261. updates,
  262. assertion
  263. )
  264. })
  265. it('replaces backslashes with underscores in pathnames', function (done) {
  266. const updates = [
  267. {
  268. update: {
  269. doc: this.doc_id,
  270. pathname: '/\\main\\foo.tex',
  271. new_pathname: '/\\new_main\\foo\\bar.tex',
  272. meta: {
  273. user_id: this.user_id,
  274. ts: this.timestamp,
  275. },
  276. },
  277. },
  278. ]
  279. const assertion = (error, changes) => {
  280. changes = changes.map(change => change.toRaw())
  281. expect(error).to.be.null
  282. expect(changes).to.deep.equal([
  283. {
  284. authors: [],
  285. operations: [
  286. {
  287. pathname: '_main_foo.tex',
  288. newPathname: '_new_main_foo_bar.tex',
  289. },
  290. ],
  291. v2Authors: [this.user_id],
  292. timestamp: this.timestamp,
  293. },
  294. ])
  295. done()
  296. }
  297. this.UpdateTranslator.convertToChanges(
  298. this.project_id,
  299. updates,
  300. assertion
  301. )
  302. })
  303. it('replaces leading asterisks with __ASTERISK__ in pathnames', function (done) {
  304. const updates = [
  305. {
  306. update: {
  307. file: this.file_id,
  308. pathname: '/test*test.png',
  309. meta: {
  310. user_id: this.user_id,
  311. ts: this.timestamp,
  312. },
  313. url: 'filestore.example.com/test*test.png',
  314. },
  315. blobHash: this.mockBlobHash,
  316. },
  317. ]
  318. const assertion = (error, changes) => {
  319. changes = changes.map(change => change.toRaw())
  320. expect(error).to.be.null
  321. expect(changes).to.deep.equal([
  322. {
  323. authors: [],
  324. operations: [
  325. {
  326. pathname: 'test__ASTERISK__test.png',
  327. file: {
  328. hash: this.mockBlobHash,
  329. },
  330. },
  331. ],
  332. v2Authors: [this.user_id],
  333. timestamp: this.timestamp,
  334. },
  335. ])
  336. done()
  337. }
  338. this.UpdateTranslator.convertToChanges(
  339. this.project_id,
  340. updates,
  341. assertion
  342. )
  343. })
  344. it('replaces a leading space for top-level files with __SPACE__', function (done) {
  345. const updates = [
  346. {
  347. update: {
  348. file: this.file_id,
  349. pathname: '/ test.png',
  350. meta: {
  351. user_id: this.user_id,
  352. ts: this.timestamp,
  353. },
  354. url: 'filestore.example.com/test.png',
  355. },
  356. blobHash: this.mockBlobHash,
  357. },
  358. ]
  359. const assertion = (error, changes) => {
  360. changes = changes.map(change => change.toRaw())
  361. expect(error).to.be.null
  362. expect(changes).to.deep.equal([
  363. {
  364. authors: [],
  365. operations: [
  366. {
  367. pathname: '__SPACE__test.png',
  368. file: {
  369. hash: this.mockBlobHash,
  370. },
  371. },
  372. ],
  373. v2Authors: [this.user_id],
  374. timestamp: this.timestamp,
  375. },
  376. ])
  377. done()
  378. }
  379. this.UpdateTranslator.convertToChanges(
  380. this.project_id,
  381. updates,
  382. assertion
  383. )
  384. })
  385. it('replaces leading spaces of files in subfolders with __SPACE__', function (done) {
  386. const updates = [
  387. {
  388. update: {
  389. file: this.file_id,
  390. pathname: '/folder/ test.png',
  391. meta: {
  392. user_id: this.user_id,
  393. ts: this.timestamp,
  394. },
  395. url: 'filestore.example.com/folder/test.png',
  396. },
  397. blobHash: this.mockBlobHash,
  398. },
  399. ]
  400. const assertion = (error, changes) => {
  401. changes = changes.map(change => change.toRaw())
  402. expect(error).to.be.null
  403. expect(changes).to.deep.equal([
  404. {
  405. authors: [],
  406. operations: [
  407. {
  408. pathname: 'folder/__SPACE__test.png',
  409. file: {
  410. hash: this.mockBlobHash,
  411. },
  412. },
  413. ],
  414. v2Authors: [this.user_id],
  415. timestamp: this.timestamp,
  416. },
  417. ])
  418. done()
  419. }
  420. this.UpdateTranslator.convertToChanges(
  421. this.project_id,
  422. updates,
  423. assertion
  424. )
  425. })
  426. describe('text updates', function () {
  427. it('can translate insertions', function (done) {
  428. const updates = [
  429. {
  430. update: {
  431. doc: this.doc_id,
  432. op: [
  433. { p: 3, i: 'foo' },
  434. { p: 15, i: 'bar' },
  435. ],
  436. v: this.version,
  437. meta: {
  438. user_id: this.user_id,
  439. ts: new Date(this.timestamp).getTime(),
  440. pathname: '/main.tex',
  441. doc_length: 20,
  442. source: 'some-editor-id',
  443. },
  444. },
  445. },
  446. ]
  447. const assertion = (error, changes) => {
  448. changes = changes.map(change => change.toRaw())
  449. expect(error).to.be.null
  450. expect(changes).to.deep.equal([
  451. {
  452. authors: [],
  453. operations: [
  454. {
  455. pathname: 'main.tex',
  456. textOperation: [3, 'foo', 9, 'bar', 8],
  457. },
  458. ],
  459. v2Authors: [this.user_id],
  460. timestamp: this.timestamp,
  461. v2DocVersions: {
  462. '59bfd450e3028c4d40a1e9ab': {
  463. pathname: 'main.tex',
  464. v: 0,
  465. },
  466. },
  467. },
  468. ])
  469. done()
  470. }
  471. this.UpdateTranslator.convertToChanges(
  472. this.project_id,
  473. updates,
  474. assertion
  475. )
  476. })
  477. it('can translate deletions', function (done) {
  478. const updates = [
  479. {
  480. update: {
  481. doc: this.doc_id,
  482. op: [
  483. { p: 3, d: 'lo' },
  484. { p: 10, d: 'bar' },
  485. ],
  486. v: this.version,
  487. meta: {
  488. user_id: this.user_id,
  489. ts: new Date(this.timestamp).getTime(),
  490. pathname: '/main.tex',
  491. doc_length: 20,
  492. },
  493. },
  494. },
  495. ]
  496. const assertion = (error, changes) => {
  497. changes = changes.map(change => change.toRaw())
  498. expect(error).to.be.null
  499. expect(changes).to.deep.equal([
  500. {
  501. authors: [],
  502. operations: [
  503. {
  504. pathname: 'main.tex',
  505. textOperation: [3, -2, 7, -3, 5],
  506. },
  507. ],
  508. v2Authors: [this.user_id],
  509. timestamp: this.timestamp,
  510. v2DocVersions: {
  511. '59bfd450e3028c4d40a1e9ab': {
  512. pathname: 'main.tex',
  513. v: 0,
  514. },
  515. },
  516. },
  517. ])
  518. done()
  519. }
  520. this.UpdateTranslator.convertToChanges(
  521. this.project_id,
  522. updates,
  523. assertion
  524. )
  525. })
  526. it('can translate insertions at the start and end (with zero retained)', function (done) {
  527. const updates = [
  528. {
  529. update: {
  530. doc: this.doc_id,
  531. op: [
  532. { p: 0, i: 'foo' },
  533. { p: 23, i: 'bar' },
  534. { p: 0, d: 'foo' },
  535. { p: 20, d: 'bar' },
  536. ],
  537. v: this.version,
  538. meta: {
  539. user_id: this.user_id,
  540. ts: new Date(this.timestamp).getTime(),
  541. pathname: '/main.tex',
  542. doc_length: 20,
  543. },
  544. },
  545. },
  546. ]
  547. const assertion = (error, changes) => {
  548. changes = changes.map(change => change.toRaw())
  549. expect(error).to.be.null
  550. expect(changes).to.deep.equal([
  551. {
  552. authors: [],
  553. operations: [
  554. {
  555. pathname: 'main.tex',
  556. textOperation: [20],
  557. },
  558. ],
  559. v2Authors: [this.user_id],
  560. timestamp: this.timestamp,
  561. v2DocVersions: {
  562. '59bfd450e3028c4d40a1e9ab': {
  563. pathname: 'main.tex',
  564. v: 0,
  565. },
  566. },
  567. },
  568. ])
  569. done()
  570. }
  571. this.UpdateTranslator.convertToChanges(
  572. this.project_id,
  573. updates,
  574. assertion
  575. )
  576. })
  577. it('can handle operations in non-linear offset order', function (done) {
  578. const updates = [
  579. {
  580. update: {
  581. doc: this.doc_id,
  582. op: [
  583. { p: 15, i: 'foo' },
  584. { p: 3, i: 'bar' },
  585. ],
  586. v: this.version,
  587. meta: {
  588. user_id: this.user_id,
  589. ts: this.timestamp,
  590. pathname: '/main.tex',
  591. doc_length: 20,
  592. },
  593. },
  594. },
  595. ]
  596. const assertion = (error, changes) => {
  597. changes = changes.map(change => change.toRaw())
  598. expect(error).to.be.null
  599. expect(changes).to.deep.equal([
  600. {
  601. authors: [],
  602. operations: [
  603. {
  604. pathname: 'main.tex',
  605. textOperation: [3, 'bar', 12, 'foo', 5],
  606. },
  607. ],
  608. v2Authors: [this.user_id],
  609. timestamp: this.timestamp,
  610. v2DocVersions: {
  611. '59bfd450e3028c4d40a1e9ab': {
  612. pathname: 'main.tex',
  613. v: 0,
  614. },
  615. },
  616. },
  617. ])
  618. done()
  619. }
  620. this.UpdateTranslator.convertToChanges(
  621. this.project_id,
  622. updates,
  623. assertion
  624. )
  625. })
  626. it('can ignore comment ops', function (done) {
  627. const updates = [
  628. {
  629. update: {
  630. doc: this.doc_id,
  631. op: [
  632. { p: 0, i: 'foo' },
  633. { p: 5, c: 'bar' },
  634. { p: 10, i: 'baz' },
  635. ],
  636. v: this.version,
  637. meta: {
  638. user_id: this.user_id,
  639. ts: new Date(this.timestamp).getTime(),
  640. pathname: '/main.tex',
  641. doc_length: 20,
  642. },
  643. },
  644. },
  645. ]
  646. const assertion = (error, changes) => {
  647. changes = changes.map(change => change.toRaw())
  648. expect(error).to.be.null
  649. expect(changes).to.deep.equal([
  650. {
  651. authors: [],
  652. operations: [
  653. {
  654. pathname: 'main.tex',
  655. textOperation: ['foo', 7, 'baz', 13],
  656. },
  657. ],
  658. v2Authors: [this.user_id],
  659. timestamp: this.timestamp,
  660. v2DocVersions: {
  661. '59bfd450e3028c4d40a1e9ab': {
  662. pathname: 'main.tex',
  663. v: 0,
  664. },
  665. },
  666. },
  667. ])
  668. done()
  669. }
  670. this.UpdateTranslator.convertToChanges(
  671. this.project_id,
  672. updates,
  673. assertion
  674. )
  675. })
  676. it('handles insertions after the end of the document', function (done) {
  677. const updates = [
  678. {
  679. update: {
  680. doc: this.doc_id,
  681. op: [{ p: 3, i: '\\' }],
  682. v: this.version,
  683. meta: {
  684. user_id: this.user_id,
  685. ts: new Date(this.timestamp).getTime(),
  686. pathname: '/main.tex',
  687. doc_length: 2,
  688. },
  689. },
  690. },
  691. ]
  692. const assertion = (error, changes) => {
  693. changes = changes.map(change => change.toRaw())
  694. expect(error).to.be.null
  695. expect(changes).to.deep.equal([
  696. {
  697. authors: [],
  698. operations: [
  699. {
  700. pathname: 'main.tex',
  701. textOperation: [2, '\\'],
  702. },
  703. ],
  704. v2Authors: [this.user_id],
  705. timestamp: this.timestamp,
  706. v2DocVersions: {
  707. '59bfd450e3028c4d40a1e9ab': {
  708. pathname: 'main.tex',
  709. v: 0,
  710. },
  711. },
  712. },
  713. ])
  714. done()
  715. }
  716. this.UpdateTranslator.convertToChanges(
  717. this.project_id,
  718. updates,
  719. assertion
  720. )
  721. })
  722. it('translates external source metadata into an origin', function (done) {
  723. const updates = [
  724. {
  725. update: {
  726. doc: this.doc_id,
  727. op: [{ p: 3, i: 'foo' }],
  728. v: this.version,
  729. meta: {
  730. user_id: this.user_id,
  731. ts: new Date(this.timestamp).getTime(),
  732. pathname: '/main.tex',
  733. doc_length: 20,
  734. type: 'external',
  735. source: 'dropbox',
  736. },
  737. },
  738. },
  739. ]
  740. const assertion = (error, changes) => {
  741. changes = changes.map(change => change.toRaw())
  742. expect(error).to.be.null
  743. expect(changes).to.deep.equal([
  744. {
  745. authors: [],
  746. operations: [
  747. {
  748. pathname: 'main.tex',
  749. textOperation: [3, 'foo', 17],
  750. },
  751. ],
  752. v2Authors: [this.user_id],
  753. timestamp: this.timestamp,
  754. v2DocVersions: {
  755. '59bfd450e3028c4d40a1e9ab': {
  756. pathname: 'main.tex',
  757. v: 0,
  758. },
  759. },
  760. origin: { kind: 'dropbox' },
  761. },
  762. ])
  763. done()
  764. }
  765. this.UpdateTranslator.convertToChanges(
  766. this.project_id,
  767. updates,
  768. assertion
  769. )
  770. })
  771. it('errors on unexpected ops', function (done) {
  772. const updates = [
  773. {
  774. update: {
  775. doc: this.doc_id,
  776. op: [{ p: 5, z: 'bar' }],
  777. v: this.version,
  778. meta: {
  779. user_id: this.user_id,
  780. ts: new Date(this.timestamp).getTime(),
  781. pathname: '/main.tex',
  782. doc_length: 20,
  783. },
  784. },
  785. },
  786. ]
  787. const assertion = (error, changes) => {
  788. expect(error.message).to.equal('unexpected op type')
  789. done()
  790. }
  791. this.UpdateTranslator.convertToChanges(
  792. this.project_id,
  793. updates,
  794. assertion
  795. )
  796. })
  797. })
  798. })
  799. })