ApplyingUpdatesToADocTests.js 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847
  1. /* eslint-disable
  2. camelcase,
  3. handle-callback-err,
  4. */
  5. // TODO: This file was created by bulk-decaffeinate.
  6. // Fix any style issues and re-enable lint.
  7. /*
  8. * decaffeinate suggestions:
  9. * DS101: Remove unnecessary use of Array.from
  10. * DS102: Remove unnecessary code created because of implicit returns
  11. * DS207: Consider shorter variations of null checks
  12. * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  13. */
  14. const sinon = require('sinon')
  15. const { expect } = require('chai')
  16. const async = require('async')
  17. const Settings = require('settings-sharelatex')
  18. const rclient_history = require('@overleaf/redis-wrapper').createClient(
  19. Settings.redis.history
  20. ) // note: this is track changes, not project-history
  21. const rclient_project_history = require('@overleaf/redis-wrapper').createClient(
  22. Settings.redis.project_history
  23. )
  24. const rclient_du = require('@overleaf/redis-wrapper').createClient(
  25. Settings.redis.documentupdater
  26. )
  27. const Keys = Settings.redis.documentupdater.key_schema
  28. const HistoryKeys = Settings.redis.history.key_schema
  29. const ProjectHistoryKeys = Settings.redis.project_history.key_schema
  30. const MockTrackChangesApi = require('./helpers/MockTrackChangesApi')
  31. const MockWebApi = require('./helpers/MockWebApi')
  32. const DocUpdaterClient = require('./helpers/DocUpdaterClient')
  33. const DocUpdaterApp = require('./helpers/DocUpdaterApp')
  34. describe('Applying updates to a doc', function () {
  35. before(function (done) {
  36. this.lines = ['one', 'two', 'three']
  37. this.version = 42
  38. this.update = {
  39. doc: this.doc_id,
  40. op: [
  41. {
  42. i: 'one and a half\n',
  43. p: 4
  44. }
  45. ],
  46. v: this.version
  47. }
  48. this.result = ['one', 'one and a half', 'two', 'three']
  49. return DocUpdaterApp.ensureRunning(done)
  50. })
  51. describe('when the document is not loaded', function () {
  52. before(function (done) {
  53. ;[this.project_id, this.doc_id] = Array.from([
  54. DocUpdaterClient.randomId(),
  55. DocUpdaterClient.randomId()
  56. ])
  57. sinon.spy(MockWebApi, 'getDocument')
  58. this.startTime = Date.now()
  59. MockWebApi.insertDoc(this.project_id, this.doc_id, {
  60. lines: this.lines,
  61. version: this.version
  62. })
  63. DocUpdaterClient.sendUpdate(
  64. this.project_id,
  65. this.doc_id,
  66. this.update,
  67. (error) => {
  68. if (error != null) {
  69. throw error
  70. }
  71. return setTimeout(done, 200)
  72. }
  73. )
  74. return null
  75. })
  76. after(function () {
  77. return MockWebApi.getDocument.restore()
  78. })
  79. it('should load the document from the web API', function () {
  80. return MockWebApi.getDocument
  81. .calledWith(this.project_id, this.doc_id)
  82. .should.equal(true)
  83. })
  84. it('should update the doc', function (done) {
  85. DocUpdaterClient.getDoc(
  86. this.project_id,
  87. this.doc_id,
  88. (error, res, doc) => {
  89. doc.lines.should.deep.equal(this.result)
  90. return done()
  91. }
  92. )
  93. return null
  94. })
  95. it('should push the applied updates to the track changes api', function (done) {
  96. rclient_history.lrange(
  97. HistoryKeys.uncompressedHistoryOps({ doc_id: this.doc_id }),
  98. 0,
  99. -1,
  100. (error, updates) => {
  101. if (error != null) {
  102. throw error
  103. }
  104. JSON.parse(updates[0]).op.should.deep.equal(this.update.op)
  105. return rclient_history.sismember(
  106. HistoryKeys.docsWithHistoryOps({ project_id: this.project_id }),
  107. this.doc_id,
  108. (error, result) => {
  109. if (error != null) {
  110. throw error
  111. }
  112. result.should.equal(1)
  113. return done()
  114. }
  115. )
  116. }
  117. )
  118. return null
  119. })
  120. it('should push the applied updates to the project history changes api', function (done) {
  121. rclient_project_history.lrange(
  122. ProjectHistoryKeys.projectHistoryOps({ project_id: this.project_id }),
  123. 0,
  124. -1,
  125. (error, updates) => {
  126. if (error != null) {
  127. throw error
  128. }
  129. JSON.parse(updates[0]).op.should.deep.equal(this.update.op)
  130. return done()
  131. }
  132. )
  133. return null
  134. })
  135. it('should set the first op timestamp', function (done) {
  136. rclient_project_history.get(
  137. ProjectHistoryKeys.projectHistoryFirstOpTimestamp({
  138. project_id: this.project_id
  139. }),
  140. (error, result) => {
  141. if (error != null) {
  142. throw error
  143. }
  144. result.should.be.within(this.startTime, Date.now())
  145. this.firstOpTimestamp = result
  146. return done()
  147. }
  148. )
  149. return null
  150. })
  151. return describe('when sending another update', function () {
  152. before(function (done) {
  153. this.timeout = 10000
  154. this.second_update = Object.create(this.update)
  155. this.second_update.v = this.version + 1
  156. DocUpdaterClient.sendUpdate(
  157. this.project_id,
  158. this.doc_id,
  159. this.second_update,
  160. (error) => {
  161. if (error != null) {
  162. throw error
  163. }
  164. return setTimeout(done, 200)
  165. }
  166. )
  167. return null
  168. })
  169. return it('should not change the first op timestamp', function (done) {
  170. rclient_project_history.get(
  171. ProjectHistoryKeys.projectHistoryFirstOpTimestamp({
  172. project_id: this.project_id
  173. }),
  174. (error, result) => {
  175. if (error != null) {
  176. throw error
  177. }
  178. result.should.equal(this.firstOpTimestamp)
  179. return done()
  180. }
  181. )
  182. return null
  183. })
  184. })
  185. })
  186. describe('when the document is loaded', function () {
  187. before(function (done) {
  188. ;[this.project_id, this.doc_id] = Array.from([
  189. DocUpdaterClient.randomId(),
  190. DocUpdaterClient.randomId()
  191. ])
  192. MockWebApi.insertDoc(this.project_id, this.doc_id, {
  193. lines: this.lines,
  194. version: this.version
  195. })
  196. DocUpdaterClient.preloadDoc(this.project_id, this.doc_id, (error) => {
  197. if (error != null) {
  198. throw error
  199. }
  200. sinon.spy(MockWebApi, 'getDocument')
  201. return DocUpdaterClient.sendUpdate(
  202. this.project_id,
  203. this.doc_id,
  204. this.update,
  205. (error) => {
  206. if (error != null) {
  207. throw error
  208. }
  209. return setTimeout(done, 200)
  210. }
  211. )
  212. })
  213. return null
  214. })
  215. after(function () {
  216. return MockWebApi.getDocument.restore()
  217. })
  218. it('should not need to call the web api', function () {
  219. return MockWebApi.getDocument.called.should.equal(false)
  220. })
  221. it('should update the doc', function (done) {
  222. DocUpdaterClient.getDoc(
  223. this.project_id,
  224. this.doc_id,
  225. (error, res, doc) => {
  226. doc.lines.should.deep.equal(this.result)
  227. return done()
  228. }
  229. )
  230. return null
  231. })
  232. it('should push the applied updates to the track changes api', function (done) {
  233. rclient_history.lrange(
  234. HistoryKeys.uncompressedHistoryOps({ doc_id: this.doc_id }),
  235. 0,
  236. -1,
  237. (error, updates) => {
  238. JSON.parse(updates[0]).op.should.deep.equal(this.update.op)
  239. return rclient_history.sismember(
  240. HistoryKeys.docsWithHistoryOps({ project_id: this.project_id }),
  241. this.doc_id,
  242. (error, result) => {
  243. result.should.equal(1)
  244. return done()
  245. }
  246. )
  247. }
  248. )
  249. return null
  250. })
  251. return it('should push the applied updates to the project history changes api', function (done) {
  252. rclient_project_history.lrange(
  253. ProjectHistoryKeys.projectHistoryOps({ project_id: this.project_id }),
  254. 0,
  255. -1,
  256. (error, updates) => {
  257. JSON.parse(updates[0]).op.should.deep.equal(this.update.op)
  258. return done()
  259. }
  260. )
  261. return null
  262. })
  263. })
  264. describe('when the document is loaded and is using project-history only', function () {
  265. before(function (done) {
  266. ;[this.project_id, this.doc_id] = Array.from([
  267. DocUpdaterClient.randomId(),
  268. DocUpdaterClient.randomId()
  269. ])
  270. MockWebApi.insertDoc(this.project_id, this.doc_id, {
  271. lines: this.lines,
  272. version: this.version,
  273. projectHistoryType: 'project-history'
  274. })
  275. DocUpdaterClient.preloadDoc(this.project_id, this.doc_id, (error) => {
  276. if (error != null) {
  277. throw error
  278. }
  279. sinon.spy(MockWebApi, 'getDocument')
  280. return DocUpdaterClient.sendUpdate(
  281. this.project_id,
  282. this.doc_id,
  283. this.update,
  284. (error) => {
  285. if (error != null) {
  286. throw error
  287. }
  288. return setTimeout(done, 200)
  289. }
  290. )
  291. })
  292. return null
  293. })
  294. after(function () {
  295. return MockWebApi.getDocument.restore()
  296. })
  297. it('should update the doc', function (done) {
  298. DocUpdaterClient.getDoc(
  299. this.project_id,
  300. this.doc_id,
  301. (error, res, doc) => {
  302. doc.lines.should.deep.equal(this.result)
  303. return done()
  304. }
  305. )
  306. return null
  307. })
  308. it('should not push any applied updates to the track changes api', function (done) {
  309. rclient_history.lrange(
  310. HistoryKeys.uncompressedHistoryOps({ doc_id: this.doc_id }),
  311. 0,
  312. -1,
  313. (error, updates) => {
  314. updates.length.should.equal(0)
  315. return done()
  316. }
  317. )
  318. return null
  319. })
  320. return it('should push the applied updates to the project history changes api', function (done) {
  321. rclient_project_history.lrange(
  322. ProjectHistoryKeys.projectHistoryOps({ project_id: this.project_id }),
  323. 0,
  324. -1,
  325. (error, updates) => {
  326. JSON.parse(updates[0]).op.should.deep.equal(this.update.op)
  327. return done()
  328. }
  329. )
  330. return null
  331. })
  332. })
  333. describe('when the document has been deleted', function () {
  334. describe('when the ops come in a single linear order', function () {
  335. before(function (done) {
  336. ;[this.project_id, this.doc_id] = Array.from([
  337. DocUpdaterClient.randomId(),
  338. DocUpdaterClient.randomId()
  339. ])
  340. const lines = ['', '', '']
  341. MockWebApi.insertDoc(this.project_id, this.doc_id, {
  342. lines,
  343. version: 0
  344. })
  345. this.updates = [
  346. { doc_id: this.doc_id, v: 0, op: [{ i: 'h', p: 0 }] },
  347. { doc_id: this.doc_id, v: 1, op: [{ i: 'e', p: 1 }] },
  348. { doc_id: this.doc_id, v: 2, op: [{ i: 'l', p: 2 }] },
  349. { doc_id: this.doc_id, v: 3, op: [{ i: 'l', p: 3 }] },
  350. { doc_id: this.doc_id, v: 4, op: [{ i: 'o', p: 4 }] },
  351. { doc_id: this.doc_id, v: 5, op: [{ i: ' ', p: 5 }] },
  352. { doc_id: this.doc_id, v: 6, op: [{ i: 'w', p: 6 }] },
  353. { doc_id: this.doc_id, v: 7, op: [{ i: 'o', p: 7 }] },
  354. { doc_id: this.doc_id, v: 8, op: [{ i: 'r', p: 8 }] },
  355. { doc_id: this.doc_id, v: 9, op: [{ i: 'l', p: 9 }] },
  356. { doc_id: this.doc_id, v: 10, op: [{ i: 'd', p: 10 }] }
  357. ]
  358. this.my_result = ['hello world', '', '']
  359. return done()
  360. })
  361. it('should be able to continue applying updates when the project has been deleted', function (done) {
  362. let update
  363. const actions = []
  364. for (update of Array.from(this.updates.slice(0, 6))) {
  365. ;((update) => {
  366. return actions.push((callback) =>
  367. DocUpdaterClient.sendUpdate(
  368. this.project_id,
  369. this.doc_id,
  370. update,
  371. callback
  372. )
  373. )
  374. })(update)
  375. }
  376. actions.push((callback) =>
  377. DocUpdaterClient.deleteDoc(this.project_id, this.doc_id, callback)
  378. )
  379. for (update of Array.from(this.updates.slice(6))) {
  380. ;((update) => {
  381. return actions.push((callback) =>
  382. DocUpdaterClient.sendUpdate(
  383. this.project_id,
  384. this.doc_id,
  385. update,
  386. callback
  387. )
  388. )
  389. })(update)
  390. }
  391. async.series(actions, (error) => {
  392. if (error != null) {
  393. throw error
  394. }
  395. return DocUpdaterClient.getDoc(
  396. this.project_id,
  397. this.doc_id,
  398. (error, res, doc) => {
  399. doc.lines.should.deep.equal(this.my_result)
  400. return done()
  401. }
  402. )
  403. })
  404. return null
  405. })
  406. it('should push the applied updates to the track changes api', function (done) {
  407. rclient_history.lrange(
  408. HistoryKeys.uncompressedHistoryOps({ doc_id: this.doc_id }),
  409. 0,
  410. -1,
  411. (error, updates) => {
  412. updates = Array.from(updates).map((u) => JSON.parse(u))
  413. for (let i = 0; i < this.updates.length; i++) {
  414. const appliedUpdate = this.updates[i]
  415. appliedUpdate.op.should.deep.equal(updates[i].op)
  416. }
  417. return rclient_history.sismember(
  418. HistoryKeys.docsWithHistoryOps({ project_id: this.project_id }),
  419. this.doc_id,
  420. (error, result) => {
  421. result.should.equal(1)
  422. return done()
  423. }
  424. )
  425. }
  426. )
  427. return null
  428. })
  429. return it('should store the doc ops in the correct order', function (done) {
  430. rclient_du.lrange(
  431. Keys.docOps({ doc_id: this.doc_id }),
  432. 0,
  433. -1,
  434. (error, updates) => {
  435. updates = Array.from(updates).map((u) => JSON.parse(u))
  436. for (let i = 0; i < this.updates.length; i++) {
  437. const appliedUpdate = this.updates[i]
  438. appliedUpdate.op.should.deep.equal(updates[i].op)
  439. }
  440. return done()
  441. }
  442. )
  443. return null
  444. })
  445. })
  446. return describe('when older ops come in after the delete', function () {
  447. before(function (done) {
  448. ;[this.project_id, this.doc_id] = Array.from([
  449. DocUpdaterClient.randomId(),
  450. DocUpdaterClient.randomId()
  451. ])
  452. const lines = ['', '', '']
  453. MockWebApi.insertDoc(this.project_id, this.doc_id, {
  454. lines,
  455. version: 0
  456. })
  457. this.updates = [
  458. { doc_id: this.doc_id, v: 0, op: [{ i: 'h', p: 0 }] },
  459. { doc_id: this.doc_id, v: 1, op: [{ i: 'e', p: 1 }] },
  460. { doc_id: this.doc_id, v: 2, op: [{ i: 'l', p: 2 }] },
  461. { doc_id: this.doc_id, v: 3, op: [{ i: 'l', p: 3 }] },
  462. { doc_id: this.doc_id, v: 4, op: [{ i: 'o', p: 4 }] },
  463. { doc_id: this.doc_id, v: 0, op: [{ i: 'world', p: 1 }] }
  464. ]
  465. this.my_result = ['hello', 'world', '']
  466. return done()
  467. })
  468. return it('should be able to continue applying updates when the project has been deleted', function (done) {
  469. let update
  470. const actions = []
  471. for (update of Array.from(this.updates.slice(0, 5))) {
  472. ;((update) => {
  473. return actions.push((callback) =>
  474. DocUpdaterClient.sendUpdate(
  475. this.project_id,
  476. this.doc_id,
  477. update,
  478. callback
  479. )
  480. )
  481. })(update)
  482. }
  483. actions.push((callback) =>
  484. DocUpdaterClient.deleteDoc(this.project_id, this.doc_id, callback)
  485. )
  486. for (update of Array.from(this.updates.slice(5))) {
  487. ;((update) => {
  488. return actions.push((callback) =>
  489. DocUpdaterClient.sendUpdate(
  490. this.project_id,
  491. this.doc_id,
  492. update,
  493. callback
  494. )
  495. )
  496. })(update)
  497. }
  498. async.series(actions, (error) => {
  499. if (error != null) {
  500. throw error
  501. }
  502. return DocUpdaterClient.getDoc(
  503. this.project_id,
  504. this.doc_id,
  505. (error, res, doc) => {
  506. doc.lines.should.deep.equal(this.my_result)
  507. return done()
  508. }
  509. )
  510. })
  511. return null
  512. })
  513. })
  514. })
  515. describe('with a broken update', function () {
  516. before(function (done) {
  517. ;[this.project_id, this.doc_id] = Array.from([
  518. DocUpdaterClient.randomId(),
  519. DocUpdaterClient.randomId()
  520. ])
  521. this.broken_update = {
  522. doc_id: this.doc_id,
  523. v: this.version,
  524. op: [{ d: 'not the correct content', p: 0 }]
  525. }
  526. MockWebApi.insertDoc(this.project_id, this.doc_id, {
  527. lines: this.lines,
  528. version: this.version
  529. })
  530. DocUpdaterClient.subscribeToAppliedOps(
  531. (this.messageCallback = sinon.stub())
  532. )
  533. DocUpdaterClient.sendUpdate(
  534. this.project_id,
  535. this.doc_id,
  536. this.broken_update,
  537. (error) => {
  538. if (error != null) {
  539. throw error
  540. }
  541. return setTimeout(done, 200)
  542. }
  543. )
  544. return null
  545. })
  546. it('should not update the doc', function (done) {
  547. DocUpdaterClient.getDoc(
  548. this.project_id,
  549. this.doc_id,
  550. (error, res, doc) => {
  551. doc.lines.should.deep.equal(this.lines)
  552. return done()
  553. }
  554. )
  555. return null
  556. })
  557. return it('should send a message with an error', function () {
  558. this.messageCallback.called.should.equal(true)
  559. const [channel, message] = Array.from(this.messageCallback.args[0])
  560. channel.should.equal('applied-ops')
  561. return JSON.parse(message).should.deep.include({
  562. project_id: this.project_id,
  563. doc_id: this.doc_id,
  564. error: 'Delete component does not match'
  565. })
  566. })
  567. })
  568. describe('with enough updates to flush to the track changes api', function () {
  569. before(function (done) {
  570. ;[this.project_id, this.doc_id] = Array.from([
  571. DocUpdaterClient.randomId(),
  572. DocUpdaterClient.randomId()
  573. ])
  574. const updates = []
  575. for (let v = 0; v <= 199; v++) {
  576. // Should flush after 100 ops
  577. updates.push({
  578. doc_id: this.doc_id,
  579. op: [{ i: v.toString(), p: 0 }],
  580. v
  581. })
  582. }
  583. sinon.spy(MockTrackChangesApi, 'flushDoc')
  584. MockWebApi.insertDoc(this.project_id, this.doc_id, {
  585. lines: this.lines,
  586. version: 0
  587. })
  588. // Send updates in chunks to causes multiple flushes
  589. const actions = []
  590. for (let i = 0; i <= 19; i++) {
  591. ;((i) => {
  592. return actions.push((cb) => {
  593. return DocUpdaterClient.sendUpdates(
  594. this.project_id,
  595. this.doc_id,
  596. updates.slice(i * 10, (i + 1) * 10),
  597. cb
  598. )
  599. })
  600. })(i)
  601. }
  602. async.series(actions, (error) => {
  603. if (error != null) {
  604. throw error
  605. }
  606. return setTimeout(done, 2000)
  607. })
  608. return null
  609. })
  610. after(function () {
  611. return MockTrackChangesApi.flushDoc.restore()
  612. })
  613. return it('should flush the doc twice', function () {
  614. return MockTrackChangesApi.flushDoc.calledTwice.should.equal(true)
  615. })
  616. })
  617. describe('when there is no version in Mongo', function () {
  618. before(function (done) {
  619. ;[this.project_id, this.doc_id] = Array.from([
  620. DocUpdaterClient.randomId(),
  621. DocUpdaterClient.randomId()
  622. ])
  623. MockWebApi.insertDoc(this.project_id, this.doc_id, {
  624. lines: this.lines
  625. })
  626. const update = {
  627. doc: this.doc_id,
  628. op: this.update.op,
  629. v: 0
  630. }
  631. DocUpdaterClient.sendUpdate(
  632. this.project_id,
  633. this.doc_id,
  634. update,
  635. (error) => {
  636. if (error != null) {
  637. throw error
  638. }
  639. return setTimeout(done, 200)
  640. }
  641. )
  642. return null
  643. })
  644. return it('should update the doc (using version = 0)', function (done) {
  645. DocUpdaterClient.getDoc(
  646. this.project_id,
  647. this.doc_id,
  648. (error, res, doc) => {
  649. doc.lines.should.deep.equal(this.result)
  650. return done()
  651. }
  652. )
  653. return null
  654. })
  655. })
  656. describe('when the sending duplicate ops', function () {
  657. before(function (done) {
  658. ;[this.project_id, this.doc_id] = Array.from([
  659. DocUpdaterClient.randomId(),
  660. DocUpdaterClient.randomId()
  661. ])
  662. MockWebApi.insertDoc(this.project_id, this.doc_id, {
  663. lines: this.lines,
  664. version: this.version
  665. })
  666. DocUpdaterClient.subscribeToAppliedOps(
  667. (this.messageCallback = sinon.stub())
  668. )
  669. // One user delete 'one', the next turns it into 'once'. The second becomes a NOP.
  670. DocUpdaterClient.sendUpdate(
  671. this.project_id,
  672. this.doc_id,
  673. {
  674. doc: this.doc_id,
  675. op: [
  676. {
  677. i: 'one and a half\n',
  678. p: 4
  679. }
  680. ],
  681. v: this.version,
  682. meta: {
  683. source: 'ikHceq3yfAdQYzBo4-xZ'
  684. }
  685. },
  686. (error) => {
  687. if (error != null) {
  688. throw error
  689. }
  690. return setTimeout(() => {
  691. return DocUpdaterClient.sendUpdate(
  692. this.project_id,
  693. this.doc_id,
  694. {
  695. doc: this.doc_id,
  696. op: [
  697. {
  698. i: 'one and a half\n',
  699. p: 4
  700. }
  701. ],
  702. v: this.version,
  703. dupIfSource: ['ikHceq3yfAdQYzBo4-xZ'],
  704. meta: {
  705. source: 'ikHceq3yfAdQYzBo4-xZ'
  706. }
  707. },
  708. (error) => {
  709. if (error != null) {
  710. throw error
  711. }
  712. return setTimeout(done, 200)
  713. }
  714. )
  715. }, 200)
  716. }
  717. )
  718. return null
  719. })
  720. it('should update the doc', function (done) {
  721. DocUpdaterClient.getDoc(
  722. this.project_id,
  723. this.doc_id,
  724. (error, res, doc) => {
  725. doc.lines.should.deep.equal(this.result)
  726. return done()
  727. }
  728. )
  729. return null
  730. })
  731. return it('should return a message about duplicate ops', function () {
  732. this.messageCallback.calledTwice.should.equal(true)
  733. this.messageCallback.args[0][0].should.equal('applied-ops')
  734. expect(JSON.parse(this.messageCallback.args[0][1]).op.dup).to.be.undefined
  735. this.messageCallback.args[1][0].should.equal('applied-ops')
  736. return expect(
  737. JSON.parse(this.messageCallback.args[1][1]).op.dup
  738. ).to.equal(true)
  739. })
  740. })
  741. return describe('when sending updates for a non-existing doc id', function () {
  742. before(function (done) {
  743. ;[this.project_id, this.doc_id] = Array.from([
  744. DocUpdaterClient.randomId(),
  745. DocUpdaterClient.randomId()
  746. ])
  747. this.non_existing = {
  748. doc_id: this.doc_id,
  749. v: this.version,
  750. op: [{ d: 'content', p: 0 }]
  751. }
  752. DocUpdaterClient.subscribeToAppliedOps(
  753. (this.messageCallback = sinon.stub())
  754. )
  755. DocUpdaterClient.sendUpdate(
  756. this.project_id,
  757. this.doc_id,
  758. this.non_existing,
  759. (error) => {
  760. if (error != null) {
  761. throw error
  762. }
  763. return setTimeout(done, 200)
  764. }
  765. )
  766. return null
  767. })
  768. it('should not update or create a doc', function (done) {
  769. DocUpdaterClient.getDoc(
  770. this.project_id,
  771. this.doc_id,
  772. (error, res, doc) => {
  773. res.statusCode.should.equal(404)
  774. return done()
  775. }
  776. )
  777. return null
  778. })
  779. return it('should send a message with an error', function () {
  780. this.messageCallback.called.should.equal(true)
  781. const [channel, message] = Array.from(this.messageCallback.args[0])
  782. channel.should.equal('applied-ops')
  783. return JSON.parse(message).should.deep.include({
  784. project_id: this.project_id,
  785. doc_id: this.doc_id,
  786. error: `doc not not found: /project/${this.project_id}/doc/${this.doc_id}`
  787. })
  788. })
  789. })
  790. })