ApplyingUpdatesToADocTests.js 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311
  1. const sinon = require('sinon')
  2. const { expect } = require('chai')
  3. const async = require('async')
  4. const Settings = require('@overleaf/settings')
  5. const rclientProjectHistory = require('@overleaf/redis-wrapper').createClient(
  6. Settings.redis.project_history
  7. )
  8. const rclientDU = require('@overleaf/redis-wrapper').createClient(
  9. Settings.redis.documentupdater
  10. )
  11. const Keys = Settings.redis.documentupdater.key_schema
  12. const ProjectHistoryKeys = Settings.redis.project_history.key_schema
  13. const MockWebApi = require('./helpers/MockWebApi')
  14. const DocUpdaterClient = require('./helpers/DocUpdaterClient')
  15. const DocUpdaterApp = require('./helpers/DocUpdaterApp')
  16. describe('Applying updates to a doc', function () {
  17. beforeEach(function (done) {
  18. sinon.spy(MockWebApi, 'getDocument')
  19. this.lines = ['one', 'two', 'three']
  20. this.version = 42
  21. this.op = {
  22. i: 'one and a half\n',
  23. p: 4,
  24. }
  25. this.project_id = DocUpdaterClient.randomId()
  26. this.doc_id = DocUpdaterClient.randomId()
  27. this.update = {
  28. doc: this.doc_id,
  29. op: [this.op],
  30. v: this.version,
  31. }
  32. this.historyOTUpdate = {
  33. doc: this.doc_id,
  34. op: [{ textOperation: [4, 'one and a half\n', 9] }],
  35. v: this.version,
  36. meta: { source: 'random-publicId' },
  37. }
  38. this.result = ['one', 'one and a half', 'two', 'three']
  39. DocUpdaterApp.ensureRunning(done)
  40. })
  41. afterEach(function () {
  42. sinon.restore()
  43. })
  44. describe('when the document is not loaded', function () {
  45. beforeEach(function (done) {
  46. this.startTime = Date.now()
  47. MockWebApi.insertDoc(this.project_id, this.doc_id, {
  48. lines: this.lines,
  49. version: this.version,
  50. })
  51. DocUpdaterClient.sendUpdate(
  52. this.project_id,
  53. this.doc_id,
  54. this.update,
  55. error => {
  56. if (error != null) {
  57. throw error
  58. }
  59. setTimeout(() => {
  60. rclientProjectHistory.get(
  61. ProjectHistoryKeys.projectHistoryFirstOpTimestamp({
  62. project_id: this.project_id,
  63. }),
  64. (error, result) => {
  65. if (error != null) {
  66. throw error
  67. }
  68. result = parseInt(result, 10)
  69. this.firstOpTimestamp = result
  70. done()
  71. }
  72. )
  73. }, 200)
  74. }
  75. )
  76. })
  77. it('should load the document from the web API', function () {
  78. MockWebApi.getDocument
  79. .calledWith(this.project_id, this.doc_id)
  80. .should.equal(true)
  81. })
  82. it('should update the doc', function (done) {
  83. DocUpdaterClient.getDoc(
  84. this.project_id,
  85. this.doc_id,
  86. (error, res, doc) => {
  87. if (error) done(error)
  88. doc.lines.should.deep.equal(this.result)
  89. done()
  90. }
  91. )
  92. })
  93. it('should push the applied updates to the project history changes api', function (done) {
  94. rclientProjectHistory.lrange(
  95. ProjectHistoryKeys.projectHistoryOps({ project_id: this.project_id }),
  96. 0,
  97. -1,
  98. (error, updates) => {
  99. if (error != null) {
  100. throw error
  101. }
  102. JSON.parse(updates[0]).op.should.deep.equal([this.op])
  103. done()
  104. }
  105. )
  106. })
  107. it('should set the first op timestamp', function () {
  108. this.firstOpTimestamp.should.be.within(this.startTime, Date.now())
  109. })
  110. it('should yield last updated time', function (done) {
  111. DocUpdaterClient.getProjectLastUpdatedAt(
  112. this.project_id,
  113. (error, res, body) => {
  114. if (error != null) {
  115. throw error
  116. }
  117. res.statusCode.should.equal(200)
  118. body.lastUpdatedAt.should.be.within(this.startTime, Date.now())
  119. done()
  120. }
  121. )
  122. })
  123. it('should yield no last updated time for another project', function (done) {
  124. DocUpdaterClient.getProjectLastUpdatedAt(
  125. DocUpdaterClient.randomId(),
  126. (error, res, body) => {
  127. if (error != null) {
  128. throw error
  129. }
  130. res.statusCode.should.equal(200)
  131. body.should.deep.equal({})
  132. done()
  133. }
  134. )
  135. })
  136. describe('when sending another update', function () {
  137. beforeEach(function (done) {
  138. this.timeout(10000)
  139. this.second_update = Object.assign({}, this.update)
  140. this.second_update.v = this.version + 1
  141. this.secondStartTime = Date.now()
  142. DocUpdaterClient.sendUpdate(
  143. this.project_id,
  144. this.doc_id,
  145. this.second_update,
  146. error => {
  147. if (error != null) {
  148. throw error
  149. }
  150. setTimeout(done, 200)
  151. }
  152. )
  153. })
  154. it('should update the doc', function (done) {
  155. DocUpdaterClient.getDoc(
  156. this.project_id,
  157. this.doc_id,
  158. (error, res, doc) => {
  159. if (error) done(error)
  160. doc.lines.should.deep.equal([
  161. 'one',
  162. 'one and a half',
  163. 'one and a half',
  164. 'two',
  165. 'three',
  166. ])
  167. done()
  168. }
  169. )
  170. })
  171. it('should not change the first op timestamp', function (done) {
  172. rclientProjectHistory.get(
  173. ProjectHistoryKeys.projectHistoryFirstOpTimestamp({
  174. project_id: this.project_id,
  175. }),
  176. (error, result) => {
  177. if (error != null) {
  178. throw error
  179. }
  180. result = parseInt(result, 10)
  181. result.should.equal(this.firstOpTimestamp)
  182. done()
  183. }
  184. )
  185. })
  186. it('should yield last updated time', function (done) {
  187. DocUpdaterClient.getProjectLastUpdatedAt(
  188. this.project_id,
  189. (error, res, body) => {
  190. if (error != null) {
  191. throw error
  192. }
  193. res.statusCode.should.equal(200)
  194. body.lastUpdatedAt.should.be.within(
  195. this.secondStartTime,
  196. Date.now()
  197. )
  198. done()
  199. }
  200. )
  201. })
  202. })
  203. describe('when another client is sending a concurrent update', function () {
  204. beforeEach(function (done) {
  205. this.timeout(10000)
  206. this.otherUpdate = {
  207. doc: this.doc_id,
  208. op: [{ p: 8, i: 'two and a half\n' }],
  209. v: this.version,
  210. meta: { source: 'other-random-publicId' },
  211. }
  212. this.secondStartTime = Date.now()
  213. DocUpdaterClient.sendUpdate(
  214. this.project_id,
  215. this.doc_id,
  216. this.otherUpdate,
  217. error => {
  218. if (error != null) {
  219. throw error
  220. }
  221. setTimeout(done, 200)
  222. }
  223. )
  224. })
  225. it('should update the doc', function (done) {
  226. DocUpdaterClient.getDoc(
  227. this.project_id,
  228. this.doc_id,
  229. (error, res, doc) => {
  230. if (error) done(error)
  231. doc.lines.should.deep.equal([
  232. 'one',
  233. 'one and a half',
  234. 'two',
  235. 'two and a half',
  236. 'three',
  237. ])
  238. done()
  239. }
  240. )
  241. })
  242. it('should not change the first op timestamp', function (done) {
  243. rclientProjectHistory.get(
  244. ProjectHistoryKeys.projectHistoryFirstOpTimestamp({
  245. project_id: this.project_id,
  246. }),
  247. (error, result) => {
  248. if (error != null) {
  249. throw error
  250. }
  251. result = parseInt(result, 10)
  252. result.should.equal(this.firstOpTimestamp)
  253. done()
  254. }
  255. )
  256. })
  257. it('should yield last updated time', function (done) {
  258. DocUpdaterClient.getProjectLastUpdatedAt(
  259. this.project_id,
  260. (error, res, body) => {
  261. if (error != null) {
  262. throw error
  263. }
  264. res.statusCode.should.equal(200)
  265. body.lastUpdatedAt.should.be.within(
  266. this.secondStartTime,
  267. Date.now()
  268. )
  269. done()
  270. }
  271. )
  272. })
  273. })
  274. })
  275. describe('when the document is not loaded (history-ot)', function () {
  276. beforeEach(function (done) {
  277. this.startTime = Date.now()
  278. MockWebApi.insertDoc(this.project_id, this.doc_id, {
  279. lines: this.lines,
  280. version: this.version,
  281. otMigrationStage: 1,
  282. })
  283. DocUpdaterClient.sendUpdate(
  284. this.project_id,
  285. this.doc_id,
  286. this.historyOTUpdate,
  287. error => {
  288. if (error != null) {
  289. throw error
  290. }
  291. setTimeout(() => {
  292. rclientProjectHistory.get(
  293. ProjectHistoryKeys.projectHistoryFirstOpTimestamp({
  294. project_id: this.project_id,
  295. }),
  296. (error, result) => {
  297. if (error != null) {
  298. throw error
  299. }
  300. result = parseInt(result, 10)
  301. this.firstOpTimestamp = result
  302. done()
  303. }
  304. )
  305. }, 200)
  306. }
  307. )
  308. })
  309. it('should load the document from the web API', function () {
  310. MockWebApi.getDocument
  311. .calledWith(this.project_id, this.doc_id)
  312. .should.equal(true)
  313. })
  314. it('should update the doc', function (done) {
  315. DocUpdaterClient.getDoc(
  316. this.project_id,
  317. this.doc_id,
  318. (error, res, doc) => {
  319. if (error) done(error)
  320. doc.lines.should.deep.equal(this.result)
  321. done()
  322. }
  323. )
  324. })
  325. it('should push the applied updates to the project history changes api', function (done) {
  326. rclientProjectHistory.lrange(
  327. ProjectHistoryKeys.projectHistoryOps({ project_id: this.project_id }),
  328. 0,
  329. -1,
  330. (error, updates) => {
  331. if (error != null) {
  332. throw error
  333. }
  334. JSON.parse(updates[0]).op.should.deep.equal(this.historyOTUpdate.op)
  335. JSON.parse(updates[0]).meta.pathname.should.equal('/a/b/c.tex')
  336. done()
  337. }
  338. )
  339. })
  340. it('should set the first op timestamp', function () {
  341. this.firstOpTimestamp.should.be.within(this.startTime, Date.now())
  342. })
  343. it('should yield last updated time', function (done) {
  344. DocUpdaterClient.getProjectLastUpdatedAt(
  345. this.project_id,
  346. (error, res, body) => {
  347. if (error != null) {
  348. throw error
  349. }
  350. res.statusCode.should.equal(200)
  351. body.lastUpdatedAt.should.be.within(this.startTime, Date.now())
  352. done()
  353. }
  354. )
  355. })
  356. it('should yield no last updated time for another project', function (done) {
  357. DocUpdaterClient.getProjectLastUpdatedAt(
  358. DocUpdaterClient.randomId(),
  359. (error, res, body) => {
  360. if (error != null) {
  361. throw error
  362. }
  363. res.statusCode.should.equal(200)
  364. body.should.deep.equal({})
  365. done()
  366. }
  367. )
  368. })
  369. describe('when sending another update', function () {
  370. beforeEach(function (done) {
  371. this.timeout(10000)
  372. this.second_update = Object.assign({}, this.historyOTUpdate)
  373. this.second_update.op = [
  374. {
  375. textOperation: [4, 'one and a half\n', 24],
  376. },
  377. ]
  378. this.second_update.v = this.version + 1
  379. this.secondStartTime = Date.now()
  380. DocUpdaterClient.sendUpdate(
  381. this.project_id,
  382. this.doc_id,
  383. this.second_update,
  384. error => {
  385. if (error != null) {
  386. throw error
  387. }
  388. setTimeout(done, 200)
  389. }
  390. )
  391. })
  392. it('should update the doc', function (done) {
  393. DocUpdaterClient.getDoc(
  394. this.project_id,
  395. this.doc_id,
  396. (error, res, doc) => {
  397. if (error) done(error)
  398. doc.lines.should.deep.equal([
  399. 'one',
  400. 'one and a half',
  401. 'one and a half',
  402. 'two',
  403. 'three',
  404. ])
  405. done()
  406. }
  407. )
  408. })
  409. it('should not change the first op timestamp', function (done) {
  410. rclientProjectHistory.get(
  411. ProjectHistoryKeys.projectHistoryFirstOpTimestamp({
  412. project_id: this.project_id,
  413. }),
  414. (error, result) => {
  415. if (error != null) {
  416. throw error
  417. }
  418. result = parseInt(result, 10)
  419. result.should.equal(this.firstOpTimestamp)
  420. done()
  421. }
  422. )
  423. })
  424. it('should yield last updated time', function (done) {
  425. DocUpdaterClient.getProjectLastUpdatedAt(
  426. this.project_id,
  427. (error, res, body) => {
  428. if (error != null) {
  429. throw error
  430. }
  431. res.statusCode.should.equal(200)
  432. body.lastUpdatedAt.should.be.within(
  433. this.secondStartTime,
  434. Date.now()
  435. )
  436. done()
  437. }
  438. )
  439. })
  440. })
  441. describe('when another client is sending a concurrent update', function () {
  442. beforeEach(function (done) {
  443. this.timeout(10000)
  444. this.otherUpdate = {
  445. doc: this.doc_id,
  446. op: [{ textOperation: [8, 'two and a half\n', 5] }],
  447. v: this.version,
  448. meta: { source: 'other-random-publicId' },
  449. }
  450. this.secondStartTime = Date.now()
  451. DocUpdaterClient.sendUpdate(
  452. this.project_id,
  453. this.doc_id,
  454. this.otherUpdate,
  455. error => {
  456. if (error != null) {
  457. throw error
  458. }
  459. setTimeout(done, 200)
  460. }
  461. )
  462. })
  463. it('should update the doc', function (done) {
  464. DocUpdaterClient.getDoc(
  465. this.project_id,
  466. this.doc_id,
  467. (error, res, doc) => {
  468. if (error) done(error)
  469. doc.lines.should.deep.equal([
  470. 'one',
  471. 'one and a half',
  472. 'two',
  473. 'two and a half',
  474. 'three',
  475. ])
  476. done()
  477. }
  478. )
  479. })
  480. it('should not change the first op timestamp', function (done) {
  481. rclientProjectHistory.get(
  482. ProjectHistoryKeys.projectHistoryFirstOpTimestamp({
  483. project_id: this.project_id,
  484. }),
  485. (error, result) => {
  486. if (error != null) {
  487. throw error
  488. }
  489. result = parseInt(result, 10)
  490. result.should.equal(this.firstOpTimestamp)
  491. done()
  492. }
  493. )
  494. })
  495. it('should yield last updated time', function (done) {
  496. DocUpdaterClient.getProjectLastUpdatedAt(
  497. this.project_id,
  498. (error, res, body) => {
  499. if (error != null) {
  500. throw error
  501. }
  502. res.statusCode.should.equal(200)
  503. body.lastUpdatedAt.should.be.within(
  504. this.secondStartTime,
  505. Date.now()
  506. )
  507. done()
  508. }
  509. )
  510. })
  511. })
  512. })
  513. describe('when the document is loaded', function () {
  514. beforeEach(function (done) {
  515. MockWebApi.insertDoc(this.project_id, this.doc_id, {
  516. lines: this.lines,
  517. version: this.version,
  518. })
  519. DocUpdaterClient.preloadDoc(this.project_id, this.doc_id, error => {
  520. if (error != null) {
  521. throw error
  522. }
  523. sinon.resetHistory()
  524. DocUpdaterClient.sendUpdate(
  525. this.project_id,
  526. this.doc_id,
  527. this.update,
  528. error => {
  529. if (error != null) {
  530. throw error
  531. }
  532. setTimeout(done, 200)
  533. }
  534. )
  535. })
  536. })
  537. it('should not need to call the web api', function () {
  538. MockWebApi.getDocument.called.should.equal(false)
  539. })
  540. it('should update the doc', function (done) {
  541. DocUpdaterClient.getDoc(
  542. this.project_id,
  543. this.doc_id,
  544. (error, res, doc) => {
  545. if (error) return done(error)
  546. doc.lines.should.deep.equal(this.result)
  547. done()
  548. }
  549. )
  550. })
  551. it('should push the applied updates to the project history changes api', function (done) {
  552. rclientProjectHistory.lrange(
  553. ProjectHistoryKeys.projectHistoryOps({ project_id: this.project_id }),
  554. 0,
  555. -1,
  556. (error, updates) => {
  557. if (error) return done(error)
  558. JSON.parse(updates[0]).op.should.deep.equal([this.op])
  559. done()
  560. }
  561. )
  562. })
  563. })
  564. describe('when the document is loaded and is using project-history only', function () {
  565. beforeEach(function (done) {
  566. MockWebApi.insertDoc(this.project_id, this.doc_id, {
  567. lines: this.lines,
  568. version: this.version,
  569. })
  570. DocUpdaterClient.preloadDoc(this.project_id, this.doc_id, error => {
  571. if (error != null) {
  572. throw error
  573. }
  574. sinon.resetHistory()
  575. DocUpdaterClient.sendUpdate(
  576. this.project_id,
  577. this.doc_id,
  578. this.update,
  579. error => {
  580. if (error != null) {
  581. throw error
  582. }
  583. setTimeout(done, 200)
  584. }
  585. )
  586. })
  587. })
  588. it('should update the doc', function (done) {
  589. DocUpdaterClient.getDoc(
  590. this.project_id,
  591. this.doc_id,
  592. (error, res, doc) => {
  593. if (error) return done(error)
  594. doc.lines.should.deep.equal(this.result)
  595. done()
  596. }
  597. )
  598. })
  599. it('should push the applied updates to the project history changes api', function (done) {
  600. rclientProjectHistory.lrange(
  601. ProjectHistoryKeys.projectHistoryOps({ project_id: this.project_id }),
  602. 0,
  603. -1,
  604. (error, updates) => {
  605. if (error) return done(error)
  606. JSON.parse(updates[0]).op.should.deep.equal([this.op])
  607. done()
  608. }
  609. )
  610. })
  611. })
  612. describe('when the document is loaded (history-ot)', function () {
  613. beforeEach(function (done) {
  614. MockWebApi.insertDoc(this.project_id, this.doc_id, {
  615. lines: this.lines,
  616. version: this.version,
  617. otMigrationStage: 1,
  618. })
  619. DocUpdaterClient.preloadDoc(this.project_id, this.doc_id, error => {
  620. if (error != null) {
  621. throw error
  622. }
  623. DocUpdaterClient.sendUpdate(
  624. this.project_id,
  625. this.doc_id,
  626. this.historyOTUpdate,
  627. error => {
  628. if (error != null) {
  629. throw error
  630. }
  631. setTimeout(done, 200)
  632. }
  633. )
  634. })
  635. })
  636. it('should update the doc', function (done) {
  637. DocUpdaterClient.getDoc(
  638. this.project_id,
  639. this.doc_id,
  640. (error, res, doc) => {
  641. if (error) return done(error)
  642. doc.lines.should.deep.equal(this.result)
  643. done()
  644. }
  645. )
  646. })
  647. it('should push the applied updates to the project history changes api', function (done) {
  648. rclientProjectHistory.lrange(
  649. ProjectHistoryKeys.projectHistoryOps({ project_id: this.project_id }),
  650. 0,
  651. -1,
  652. (error, updates) => {
  653. if (error) return done(error)
  654. JSON.parse(updates[0]).op.should.deep.equal(this.historyOTUpdate.op)
  655. JSON.parse(updates[0]).meta.pathname.should.equal('/a/b/c.tex')
  656. done()
  657. }
  658. )
  659. })
  660. })
  661. describe('when the document has been deleted', function () {
  662. describe('when the ops come in a single linear order', function () {
  663. beforeEach(function (done) {
  664. const lines = ['', '', '']
  665. MockWebApi.insertDoc(this.project_id, this.doc_id, {
  666. lines,
  667. version: 0,
  668. })
  669. this.updates = [
  670. { doc_id: this.doc_id, v: 0, op: [{ i: 'h', p: 0 }] },
  671. { doc_id: this.doc_id, v: 1, op: [{ i: 'e', p: 1 }] },
  672. { doc_id: this.doc_id, v: 2, op: [{ i: 'l', p: 2 }] },
  673. { doc_id: this.doc_id, v: 3, op: [{ i: 'l', p: 3 }] },
  674. { doc_id: this.doc_id, v: 4, op: [{ i: 'o', p: 4 }] },
  675. { doc_id: this.doc_id, v: 5, op: [{ i: ' ', p: 5 }] },
  676. { doc_id: this.doc_id, v: 6, op: [{ i: 'w', p: 6 }] },
  677. { doc_id: this.doc_id, v: 7, op: [{ i: 'o', p: 7 }] },
  678. { doc_id: this.doc_id, v: 8, op: [{ i: 'r', p: 8 }] },
  679. { doc_id: this.doc_id, v: 9, op: [{ i: 'l', p: 9 }] },
  680. { doc_id: this.doc_id, v: 10, op: [{ i: 'd', p: 10 }] },
  681. ]
  682. this.my_result = ['hello world', '', '']
  683. const actions = []
  684. for (const update of this.updates.slice(0, 6)) {
  685. actions.push(callback =>
  686. DocUpdaterClient.sendUpdate(
  687. this.project_id,
  688. this.doc_id,
  689. update,
  690. callback
  691. )
  692. )
  693. }
  694. actions.push(callback =>
  695. DocUpdaterClient.deleteDoc(this.project_id, this.doc_id, callback)
  696. )
  697. for (const update of this.updates.slice(6)) {
  698. actions.push(callback =>
  699. DocUpdaterClient.sendUpdate(
  700. this.project_id,
  701. this.doc_id,
  702. update,
  703. callback
  704. )
  705. )
  706. }
  707. // process updates
  708. actions.push(cb =>
  709. DocUpdaterClient.getDoc(this.project_id, this.doc_id, cb)
  710. )
  711. async.series(actions, done)
  712. })
  713. it('should be able to continue applying updates when the project has been deleted', function (done) {
  714. DocUpdaterClient.getDoc(
  715. this.project_id,
  716. this.doc_id,
  717. (error, res, doc) => {
  718. if (error) return done(error)
  719. doc.lines.should.deep.equal(this.my_result)
  720. done()
  721. }
  722. )
  723. })
  724. it('should store the doc ops in the correct order', function (done) {
  725. rclientDU.lrange(
  726. Keys.docOps({ doc_id: this.doc_id }),
  727. 0,
  728. -1,
  729. (error, updates) => {
  730. if (error) return done(error)
  731. updates = updates.map(u => JSON.parse(u))
  732. for (let i = 0; i < this.updates.length; i++) {
  733. const appliedUpdate = this.updates[i]
  734. appliedUpdate.op.should.deep.equal(updates[i].op)
  735. }
  736. done()
  737. }
  738. )
  739. })
  740. })
  741. describe('when older ops come in after the delete', function () {
  742. beforeEach(function (done) {
  743. const lines = ['', '', '']
  744. MockWebApi.insertDoc(this.project_id, this.doc_id, {
  745. lines,
  746. version: 0,
  747. })
  748. this.updates = [
  749. { doc_id: this.doc_id, v: 0, op: [{ i: 'h', p: 0 }] },
  750. { doc_id: this.doc_id, v: 1, op: [{ i: 'e', p: 1 }] },
  751. { doc_id: this.doc_id, v: 2, op: [{ i: 'l', p: 2 }] },
  752. { doc_id: this.doc_id, v: 3, op: [{ i: 'l', p: 3 }] },
  753. { doc_id: this.doc_id, v: 4, op: [{ i: 'o', p: 4 }] },
  754. { doc_id: this.doc_id, v: 0, op: [{ i: 'world', p: 1 }] },
  755. ]
  756. this.my_result = ['hello', 'world', '']
  757. done()
  758. })
  759. it('should be able to continue applying updates when the project has been deleted', function (done) {
  760. let update
  761. const actions = []
  762. for (update of this.updates.slice(0, 5)) {
  763. ;(update => {
  764. actions.push(callback =>
  765. DocUpdaterClient.sendUpdate(
  766. this.project_id,
  767. this.doc_id,
  768. update,
  769. callback
  770. )
  771. )
  772. })(update)
  773. }
  774. actions.push(callback =>
  775. DocUpdaterClient.deleteDoc(this.project_id, this.doc_id, callback)
  776. )
  777. for (update of this.updates.slice(5)) {
  778. ;(update => {
  779. actions.push(callback =>
  780. DocUpdaterClient.sendUpdate(
  781. this.project_id,
  782. this.doc_id,
  783. update,
  784. callback
  785. )
  786. )
  787. })(update)
  788. }
  789. async.series(actions, error => {
  790. if (error != null) {
  791. throw error
  792. }
  793. DocUpdaterClient.getDoc(
  794. this.project_id,
  795. this.doc_id,
  796. (error, res, doc) => {
  797. if (error) return done(error)
  798. doc.lines.should.deep.equal(this.my_result)
  799. done()
  800. }
  801. )
  802. })
  803. })
  804. })
  805. })
  806. describe('with a broken update', function () {
  807. beforeEach(function (done) {
  808. this.broken_update = {
  809. doc: this.doc_id,
  810. v: this.version,
  811. op: [{ d: 'not the correct content', p: 0 }],
  812. }
  813. MockWebApi.insertDoc(this.project_id, this.doc_id, {
  814. lines: this.lines,
  815. version: this.version,
  816. })
  817. DocUpdaterClient.subscribeToAppliedOps(
  818. (this.messageCallback = sinon.stub())
  819. )
  820. DocUpdaterClient.sendUpdate(
  821. this.project_id,
  822. this.doc_id,
  823. this.broken_update,
  824. error => {
  825. if (error != null) {
  826. throw error
  827. }
  828. setTimeout(done, 200)
  829. }
  830. )
  831. })
  832. it('should not update the doc', function (done) {
  833. DocUpdaterClient.getDoc(
  834. this.project_id,
  835. this.doc_id,
  836. (error, res, doc) => {
  837. if (error) return done(error)
  838. doc.lines.should.deep.equal(this.lines)
  839. done()
  840. }
  841. )
  842. })
  843. it('should send a message with an error', function () {
  844. this.messageCallback.called.should.equal(true)
  845. const [channel, message] = this.messageCallback.args[0]
  846. channel.should.equal('applied-ops')
  847. JSON.parse(message).should.deep.include({
  848. project_id: this.project_id,
  849. doc_id: this.doc_id,
  850. error: 'Delete component does not match',
  851. })
  852. })
  853. })
  854. describe('with a broken update (history-ot)', function () {
  855. beforeEach(function (done) {
  856. this.broken_update = {
  857. doc: this.doc_id,
  858. v: this.version,
  859. op: [{ textOperation: [99, -1] }],
  860. meta: { source: '42' },
  861. }
  862. MockWebApi.insertDoc(this.project_id, this.doc_id, {
  863. lines: this.lines,
  864. version: this.version,
  865. otMigrationStage: 1,
  866. })
  867. DocUpdaterClient.subscribeToAppliedOps(
  868. (this.messageCallback = sinon.stub())
  869. )
  870. DocUpdaterClient.sendUpdate(
  871. this.project_id,
  872. this.doc_id,
  873. this.broken_update,
  874. error => {
  875. if (error != null) {
  876. throw error
  877. }
  878. setTimeout(done, 200)
  879. }
  880. )
  881. })
  882. it('should not update the doc', function (done) {
  883. DocUpdaterClient.getDoc(
  884. this.project_id,
  885. this.doc_id,
  886. (error, res, doc) => {
  887. if (error) return done(error)
  888. doc.lines.should.deep.equal(this.lines)
  889. done()
  890. }
  891. )
  892. })
  893. it('should send a message with an error', function () {
  894. this.messageCallback.called.should.equal(true)
  895. const [channel, message] = this.messageCallback.args[0]
  896. channel.should.equal('applied-ops')
  897. JSON.parse(message).should.deep.include({
  898. project_id: this.project_id,
  899. doc_id: this.doc_id,
  900. error:
  901. "The operation's base length must be equal to the string's length.",
  902. })
  903. })
  904. })
  905. describe('when mixing ot types (sharejs-text-ot -> history-ot)', function () {
  906. beforeEach(function (done) {
  907. MockWebApi.insertDoc(this.project_id, this.doc_id, {
  908. lines: this.lines,
  909. version: this.version,
  910. otMigrationStage: 0,
  911. })
  912. DocUpdaterClient.subscribeToAppliedOps(
  913. (this.messageCallback = sinon.stub())
  914. )
  915. DocUpdaterClient.sendUpdate(
  916. this.project_id,
  917. this.doc_id,
  918. this.historyOTUpdate,
  919. error => {
  920. if (error != null) {
  921. throw error
  922. }
  923. setTimeout(done, 200)
  924. }
  925. )
  926. })
  927. it('should not update the doc', function (done) {
  928. DocUpdaterClient.getDoc(
  929. this.project_id,
  930. this.doc_id,
  931. (error, res, doc) => {
  932. if (error) return done(error)
  933. doc.lines.should.deep.equal(this.lines)
  934. done()
  935. }
  936. )
  937. })
  938. it('should send a message with an error', function () {
  939. this.messageCallback.called.should.equal(true)
  940. const [channel, message] = this.messageCallback.args[0]
  941. channel.should.equal('applied-ops')
  942. JSON.parse(message).should.deep.include({
  943. project_id: this.project_id,
  944. doc_id: this.doc_id,
  945. error: 'ot type mismatch',
  946. })
  947. })
  948. })
  949. describe('when mixing ot types (history-ot -> sharejs-text-ot)', function () {
  950. beforeEach(function (done) {
  951. MockWebApi.insertDoc(this.project_id, this.doc_id, {
  952. lines: this.lines,
  953. version: this.version,
  954. otMigrationStage: 1,
  955. })
  956. DocUpdaterClient.subscribeToAppliedOps(
  957. (this.messageCallback = sinon.stub())
  958. )
  959. DocUpdaterClient.sendUpdate(
  960. this.project_id,
  961. this.doc_id,
  962. this.update,
  963. error => {
  964. if (error != null) {
  965. throw error
  966. }
  967. setTimeout(done, 200)
  968. }
  969. )
  970. })
  971. it('should not update the doc', function (done) {
  972. DocUpdaterClient.getDoc(
  973. this.project_id,
  974. this.doc_id,
  975. (error, res, doc) => {
  976. if (error) return done(error)
  977. doc.lines.should.deep.equal(this.lines)
  978. done()
  979. }
  980. )
  981. })
  982. it('should send a message with an error', function () {
  983. this.messageCallback.called.should.equal(true)
  984. const [channel, message] = this.messageCallback.args[0]
  985. channel.should.equal('applied-ops')
  986. JSON.parse(message).should.deep.include({
  987. project_id: this.project_id,
  988. doc_id: this.doc_id,
  989. error: 'ot type mismatch',
  990. })
  991. })
  992. })
  993. describe('when there is no version in Mongo', function () {
  994. beforeEach(function (done) {
  995. MockWebApi.insertDoc(this.project_id, this.doc_id, {
  996. lines: this.lines,
  997. })
  998. const update = {
  999. doc: this.doc_id,
  1000. op: this.update.op,
  1001. v: 0,
  1002. }
  1003. DocUpdaterClient.sendUpdate(
  1004. this.project_id,
  1005. this.doc_id,
  1006. update,
  1007. error => {
  1008. if (error != null) {
  1009. throw error
  1010. }
  1011. setTimeout(done, 200)
  1012. }
  1013. )
  1014. })
  1015. it('should update the doc (using version = 0)', function (done) {
  1016. DocUpdaterClient.getDoc(
  1017. this.project_id,
  1018. this.doc_id,
  1019. (error, res, doc) => {
  1020. if (error) return done(error)
  1021. doc.lines.should.deep.equal(this.result)
  1022. done()
  1023. }
  1024. )
  1025. })
  1026. })
  1027. describe('when the sending duplicate ops', function () {
  1028. beforeEach(function (done) {
  1029. MockWebApi.insertDoc(this.project_id, this.doc_id, {
  1030. lines: this.lines,
  1031. version: this.version,
  1032. })
  1033. DocUpdaterClient.subscribeToAppliedOps(
  1034. (this.messageCallback = sinon.stub())
  1035. )
  1036. // One user delete 'one', the next turns it into 'once'. The second becomes a NOP.
  1037. DocUpdaterClient.sendUpdate(
  1038. this.project_id,
  1039. this.doc_id,
  1040. {
  1041. doc: this.doc_id,
  1042. op: [
  1043. {
  1044. i: 'one and a half\n',
  1045. p: 4,
  1046. },
  1047. ],
  1048. v: this.version,
  1049. meta: {
  1050. source: 'ikHceq3yfAdQYzBo4-xZ',
  1051. },
  1052. },
  1053. error => {
  1054. if (error != null) {
  1055. throw error
  1056. }
  1057. setTimeout(() => {
  1058. DocUpdaterClient.sendUpdate(
  1059. this.project_id,
  1060. this.doc_id,
  1061. {
  1062. doc: this.doc_id,
  1063. op: [
  1064. {
  1065. i: 'one and a half\n',
  1066. p: 4,
  1067. },
  1068. ],
  1069. v: this.version,
  1070. dupIfSource: ['ikHceq3yfAdQYzBo4-xZ'],
  1071. meta: {
  1072. source: 'ikHceq3yfAdQYzBo4-xZ',
  1073. },
  1074. },
  1075. error => {
  1076. if (error != null) {
  1077. throw error
  1078. }
  1079. setTimeout(done, 200)
  1080. }
  1081. )
  1082. }, 200)
  1083. }
  1084. )
  1085. })
  1086. it('should update the doc', function (done) {
  1087. DocUpdaterClient.getDoc(
  1088. this.project_id,
  1089. this.doc_id,
  1090. (error, res, doc) => {
  1091. if (error) return done(error)
  1092. doc.lines.should.deep.equal(this.result)
  1093. done()
  1094. }
  1095. )
  1096. })
  1097. it('should return a message about duplicate ops', function () {
  1098. this.messageCallback.calledTwice.should.equal(true)
  1099. this.messageCallback.args[0][0].should.equal('applied-ops')
  1100. expect(JSON.parse(this.messageCallback.args[0][1]).op.dup).to.be.undefined
  1101. this.messageCallback.args[1][0].should.equal('applied-ops')
  1102. expect(JSON.parse(this.messageCallback.args[1][1]).op.dup).to.equal(true)
  1103. })
  1104. })
  1105. describe('when sending duplicate ops (history-ot)', function () {
  1106. beforeEach(function (done) {
  1107. MockWebApi.insertDoc(this.project_id, this.doc_id, {
  1108. lines: this.lines,
  1109. version: this.version,
  1110. otMigrationStage: 1,
  1111. })
  1112. DocUpdaterClient.subscribeToAppliedOps(
  1113. (this.messageCallback = sinon.stub())
  1114. )
  1115. // One user delete 'one', the next turns it into 'once'. The second becomes a NOP.
  1116. DocUpdaterClient.sendUpdate(
  1117. this.project_id,
  1118. this.doc_id,
  1119. {
  1120. doc: this.doc_id,
  1121. op: [{ textOperation: [4, 'one and a half\n', 9] }],
  1122. v: this.version,
  1123. meta: {
  1124. source: 'ikHceq3yfAdQYzBo4-xZ',
  1125. },
  1126. },
  1127. error => {
  1128. if (error != null) {
  1129. throw error
  1130. }
  1131. setTimeout(() => {
  1132. DocUpdaterClient.sendUpdate(
  1133. this.project_id,
  1134. this.doc_id,
  1135. {
  1136. doc: this.doc_id,
  1137. op: [
  1138. {
  1139. textOperation: [4, 'one and a half\n', 9],
  1140. },
  1141. ],
  1142. v: this.version,
  1143. dupIfSource: ['ikHceq3yfAdQYzBo4-xZ'],
  1144. meta: {
  1145. source: 'ikHceq3yfAdQYzBo4-xZ',
  1146. },
  1147. },
  1148. error => {
  1149. if (error != null) {
  1150. throw error
  1151. }
  1152. setTimeout(done, 200)
  1153. }
  1154. )
  1155. }, 200)
  1156. }
  1157. )
  1158. })
  1159. it('should update the doc', function (done) {
  1160. DocUpdaterClient.getDoc(
  1161. this.project_id,
  1162. this.doc_id,
  1163. (error, res, doc) => {
  1164. if (error) return done(error)
  1165. doc.lines.should.deep.equal(this.result)
  1166. done()
  1167. }
  1168. )
  1169. })
  1170. it('should return a message about duplicate ops', function () {
  1171. this.messageCallback.calledTwice.should.equal(true)
  1172. this.messageCallback.args[0][0].should.equal('applied-ops')
  1173. expect(JSON.parse(this.messageCallback.args[0][1]).op.dup).to.be.undefined
  1174. this.messageCallback.args[1][0].should.equal('applied-ops')
  1175. expect(JSON.parse(this.messageCallback.args[1][1]).op.dup).to.equal(true)
  1176. })
  1177. })
  1178. describe('when sending updates for a non-existing doc id', function () {
  1179. beforeEach(function (done) {
  1180. this.non_existing = {
  1181. doc: this.doc_id,
  1182. v: this.version,
  1183. op: [{ d: 'content', p: 0 }],
  1184. }
  1185. DocUpdaterClient.subscribeToAppliedOps(
  1186. (this.messageCallback = sinon.stub())
  1187. )
  1188. DocUpdaterClient.sendUpdate(
  1189. this.project_id,
  1190. this.doc_id,
  1191. this.non_existing,
  1192. error => {
  1193. if (error != null) {
  1194. throw error
  1195. }
  1196. setTimeout(done, 200)
  1197. }
  1198. )
  1199. })
  1200. it('should not update or create a doc', function (done) {
  1201. DocUpdaterClient.getDoc(
  1202. this.project_id,
  1203. this.doc_id,
  1204. (error, res, doc) => {
  1205. if (error) return done(error)
  1206. res.statusCode.should.equal(404)
  1207. done()
  1208. }
  1209. )
  1210. })
  1211. it('should send a message with an error', function () {
  1212. this.messageCallback.called.should.equal(true)
  1213. const [channel, message] = this.messageCallback.args[0]
  1214. channel.should.equal('applied-ops')
  1215. JSON.parse(message).should.deep.include({
  1216. project_id: this.project_id,
  1217. doc_id: this.doc_id,
  1218. error: `doc not not found: /project/${this.project_id}/doc/${this.doc_id}`,
  1219. })
  1220. })
  1221. })
  1222. })