UpdateTranslatorTests.js 21 KB

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