SettingADocumentTests.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545
  1. const sinon = require('sinon')
  2. const { expect } = require('chai')
  3. const Settings = require('@overleaf/settings')
  4. const docUpdaterRedis = require('@overleaf/redis-wrapper').createClient(
  5. Settings.redis.documentupdater
  6. )
  7. const Keys = Settings.redis.documentupdater.key_schema
  8. const MockTrackChangesApi = require('./helpers/MockTrackChangesApi')
  9. const MockProjectHistoryApi = require('./helpers/MockProjectHistoryApi')
  10. const MockWebApi = require('./helpers/MockWebApi')
  11. const DocUpdaterClient = require('./helpers/DocUpdaterClient')
  12. const DocUpdaterApp = require('./helpers/DocUpdaterApp')
  13. describe('Setting a document', function () {
  14. let numberOfReceivedUpdates = 0
  15. before(function (done) {
  16. DocUpdaterClient.subscribeToAppliedOps(() => {
  17. numberOfReceivedUpdates++
  18. })
  19. this.lines = ['one', 'two', 'three']
  20. this.version = 42
  21. this.update = {
  22. doc: this.doc_id,
  23. op: [
  24. {
  25. i: 'one and a half\n',
  26. p: 4,
  27. },
  28. ],
  29. v: this.version,
  30. }
  31. this.result = ['one', 'one and a half', 'two', 'three']
  32. this.newLines = ['these', 'are', 'the', 'new', 'lines']
  33. this.source = 'dropbox'
  34. this.user_id = 'user-id-123'
  35. sinon.spy(MockTrackChangesApi, 'flushDoc')
  36. sinon.spy(MockProjectHistoryApi, 'flushProject')
  37. sinon.spy(MockWebApi, 'setDocument')
  38. DocUpdaterApp.ensureRunning(done)
  39. })
  40. after(function () {
  41. MockTrackChangesApi.flushDoc.restore()
  42. MockProjectHistoryApi.flushProject.restore()
  43. MockWebApi.setDocument.restore()
  44. })
  45. describe('when the updated doc exists in the doc updater', function () {
  46. before(function (done) {
  47. numberOfReceivedUpdates = 0
  48. this.project_id = DocUpdaterClient.randomId()
  49. this.doc_id = DocUpdaterClient.randomId()
  50. MockWebApi.insertDoc(this.project_id, this.doc_id, {
  51. lines: this.lines,
  52. version: this.version,
  53. })
  54. DocUpdaterClient.preloadDoc(this.project_id, this.doc_id, error => {
  55. if (error) {
  56. throw error
  57. }
  58. DocUpdaterClient.sendUpdate(
  59. this.project_id,
  60. this.doc_id,
  61. this.update,
  62. error => {
  63. if (error) {
  64. throw error
  65. }
  66. setTimeout(() => {
  67. DocUpdaterClient.setDocLines(
  68. this.project_id,
  69. this.doc_id,
  70. this.newLines,
  71. this.source,
  72. this.user_id,
  73. false,
  74. (error, res, body) => {
  75. if (error) {
  76. return done(error)
  77. }
  78. this.statusCode = res.statusCode
  79. this.body = body
  80. done()
  81. }
  82. )
  83. }, 200)
  84. }
  85. )
  86. })
  87. })
  88. after(function () {
  89. MockTrackChangesApi.flushDoc.resetHistory()
  90. MockProjectHistoryApi.flushProject.resetHistory()
  91. MockWebApi.setDocument.resetHistory()
  92. })
  93. it('should return a 200 status code', function () {
  94. this.statusCode.should.equal(200)
  95. })
  96. it('should emit two updates (from sendUpdate and setDocLines)', function () {
  97. expect(numberOfReceivedUpdates).to.equal(2)
  98. })
  99. it('should send the updated doc lines and version to the web api', function () {
  100. MockWebApi.setDocument
  101. .calledWith(this.project_id, this.doc_id, this.newLines)
  102. .should.equal(true)
  103. })
  104. it('should update the lines in the doc updater', function (done) {
  105. DocUpdaterClient.getDoc(
  106. this.project_id,
  107. this.doc_id,
  108. (error, res, doc) => {
  109. if (error) {
  110. return done(error)
  111. }
  112. doc.lines.should.deep.equal(this.newLines)
  113. done()
  114. }
  115. )
  116. })
  117. it('should bump the version in the doc updater', function (done) {
  118. DocUpdaterClient.getDoc(
  119. this.project_id,
  120. this.doc_id,
  121. (error, res, doc) => {
  122. if (error) {
  123. return done(error)
  124. }
  125. doc.version.should.equal(this.version + 2)
  126. done()
  127. }
  128. )
  129. })
  130. it('should leave the document in redis', function (done) {
  131. docUpdaterRedis.get(
  132. Keys.docLines({ doc_id: this.doc_id }),
  133. (error, lines) => {
  134. if (error) {
  135. throw error
  136. }
  137. expect(JSON.parse(lines)).to.deep.equal(this.newLines)
  138. done()
  139. }
  140. )
  141. })
  142. it('should return the mongo rev in the json response', function () {
  143. this.body.should.deep.equal({ rev: '123' })
  144. })
  145. describe('when doc has the same contents', function () {
  146. beforeEach(function (done) {
  147. numberOfReceivedUpdates = 0
  148. DocUpdaterClient.setDocLines(
  149. this.project_id,
  150. this.doc_id,
  151. this.newLines,
  152. this.source,
  153. this.user_id,
  154. false,
  155. (error, res, body) => {
  156. if (error) {
  157. return done(error)
  158. }
  159. this.statusCode = res.statusCode
  160. this.body = body
  161. done()
  162. }
  163. )
  164. })
  165. it('should not bump the version in doc updater', function (done) {
  166. DocUpdaterClient.getDoc(
  167. this.project_id,
  168. this.doc_id,
  169. (error, res, doc) => {
  170. if (error) {
  171. return done(error)
  172. }
  173. doc.version.should.equal(this.version + 2)
  174. done()
  175. }
  176. )
  177. })
  178. it('should not emit any updates', function (done) {
  179. setTimeout(() => {
  180. expect(numberOfReceivedUpdates).to.equal(0)
  181. done()
  182. }, 100) // delay by 100ms: make sure we do not check too early!
  183. })
  184. })
  185. })
  186. describe('when the updated doc does not exist in the doc updater', function () {
  187. before(function (done) {
  188. this.project_id = DocUpdaterClient.randomId()
  189. this.doc_id = DocUpdaterClient.randomId()
  190. numberOfReceivedUpdates = 0
  191. MockWebApi.insertDoc(this.project_id, this.doc_id, {
  192. lines: this.lines,
  193. version: this.version,
  194. })
  195. DocUpdaterClient.setDocLines(
  196. this.project_id,
  197. this.doc_id,
  198. this.newLines,
  199. this.source,
  200. this.user_id,
  201. false,
  202. (error, res, body) => {
  203. if (error) {
  204. return done(error)
  205. }
  206. this.statusCode = res.statusCode
  207. this.body = body
  208. setTimeout(done, 200)
  209. }
  210. )
  211. })
  212. after(function () {
  213. MockTrackChangesApi.flushDoc.resetHistory()
  214. MockProjectHistoryApi.flushProject.resetHistory()
  215. MockWebApi.setDocument.resetHistory()
  216. })
  217. it('should return a 200 status code', function () {
  218. this.statusCode.should.equal(200)
  219. })
  220. it('should emit an update', function () {
  221. expect(numberOfReceivedUpdates).to.equal(1)
  222. })
  223. it('should send the updated doc lines to the web api', function () {
  224. MockWebApi.setDocument
  225. .calledWith(this.project_id, this.doc_id, this.newLines)
  226. .should.equal(true)
  227. })
  228. it('should flush track changes', function () {
  229. MockTrackChangesApi.flushDoc.calledWith(this.doc_id).should.equal(true)
  230. })
  231. it('should flush project history', function () {
  232. MockProjectHistoryApi.flushProject
  233. .calledWith(this.project_id)
  234. .should.equal(true)
  235. })
  236. it('should remove the document from redis', function (done) {
  237. docUpdaterRedis.get(
  238. Keys.docLines({ doc_id: this.doc_id }),
  239. (error, lines) => {
  240. if (error) {
  241. throw error
  242. }
  243. expect(lines).to.not.exist
  244. done()
  245. }
  246. )
  247. })
  248. it('should return the mongo rev in the json response', function () {
  249. this.body.should.deep.equal({ rev: '123' })
  250. })
  251. })
  252. const DOC_TOO_LARGE_TEST_CASES = [
  253. {
  254. desc: 'when the updated doc is too large for the body parser',
  255. size: Settings.maxJsonRequestSize,
  256. expectedStatusCode: 413,
  257. },
  258. {
  259. desc: 'when the updated doc is larger than the HTTP controller limit',
  260. size: Settings.max_doc_length,
  261. expectedStatusCode: 406,
  262. },
  263. ]
  264. DOC_TOO_LARGE_TEST_CASES.forEach(testCase => {
  265. describe(testCase.desc, function () {
  266. before(function (done) {
  267. this.project_id = DocUpdaterClient.randomId()
  268. this.doc_id = DocUpdaterClient.randomId()
  269. MockWebApi.insertDoc(this.project_id, this.doc_id, {
  270. lines: this.lines,
  271. version: this.version,
  272. })
  273. this.newLines = []
  274. while (JSON.stringify(this.newLines).length <= testCase.size) {
  275. this.newLines.push('(a long line of text)'.repeat(10000))
  276. }
  277. DocUpdaterClient.setDocLines(
  278. this.project_id,
  279. this.doc_id,
  280. this.newLines,
  281. this.source,
  282. this.user_id,
  283. false,
  284. (error, res, body) => {
  285. if (error) {
  286. return done(error)
  287. }
  288. this.statusCode = res.statusCode
  289. setTimeout(done, 200)
  290. }
  291. )
  292. })
  293. after(function () {
  294. MockTrackChangesApi.flushDoc.resetHistory()
  295. MockProjectHistoryApi.flushProject.resetHistory()
  296. MockWebApi.setDocument.resetHistory()
  297. })
  298. it(`should return a ${testCase.expectedStatusCode} status code`, function () {
  299. this.statusCode.should.equal(testCase.expectedStatusCode)
  300. })
  301. it('should not send the updated doc lines to the web api', function () {
  302. MockWebApi.setDocument.called.should.equal(false)
  303. })
  304. it('should not flush track changes', function () {
  305. MockTrackChangesApi.flushDoc.called.should.equal(false)
  306. })
  307. it('should not flush project history', function () {
  308. MockProjectHistoryApi.flushProject.called.should.equal(false)
  309. })
  310. })
  311. })
  312. describe('when the updated doc is large but under the bodyParser and HTTPController size limit', function () {
  313. before(function (done) {
  314. this.project_id = DocUpdaterClient.randomId()
  315. this.doc_id = DocUpdaterClient.randomId()
  316. MockWebApi.insertDoc(this.project_id, this.doc_id, {
  317. lines: this.lines,
  318. version: this.version,
  319. })
  320. this.newLines = []
  321. while (JSON.stringify(this.newLines).length < 2 * 1024 * 1024) {
  322. // limit in HTTPController
  323. this.newLines.push('(a long line of text)'.repeat(10000))
  324. }
  325. this.newLines.pop() // remove the line which took it over the limit
  326. DocUpdaterClient.setDocLines(
  327. this.project_id,
  328. this.doc_id,
  329. this.newLines,
  330. this.source,
  331. this.user_id,
  332. false,
  333. (error, res, body) => {
  334. if (error) {
  335. return done(error)
  336. }
  337. this.statusCode = res.statusCode
  338. this.body = body
  339. setTimeout(done, 200)
  340. }
  341. )
  342. })
  343. after(function () {
  344. MockTrackChangesApi.flushDoc.resetHistory()
  345. MockProjectHistoryApi.flushProject.resetHistory()
  346. MockWebApi.setDocument.resetHistory()
  347. })
  348. it('should return a 200 status code', function () {
  349. this.statusCode.should.equal(200)
  350. })
  351. it('should send the updated doc lines to the web api', function () {
  352. MockWebApi.setDocument
  353. .calledWith(this.project_id, this.doc_id, this.newLines)
  354. .should.equal(true)
  355. })
  356. it('should return the mongo rev in the json response', function () {
  357. this.body.should.deep.equal({ rev: '123' })
  358. })
  359. })
  360. describe('with track changes', function () {
  361. before(function () {
  362. this.lines = ['one', 'one and a half', 'two', 'three']
  363. this.id_seed = '587357bd35e64f6157'
  364. this.update = {
  365. doc: this.doc_id,
  366. op: [
  367. {
  368. d: 'one and a half\n',
  369. p: 4,
  370. },
  371. ],
  372. meta: {
  373. tc: this.id_seed,
  374. user_id: this.user_id,
  375. },
  376. v: this.version,
  377. }
  378. })
  379. describe('with the undo flag', function () {
  380. before(function (done) {
  381. this.project_id = DocUpdaterClient.randomId()
  382. this.doc_id = DocUpdaterClient.randomId()
  383. MockWebApi.insertDoc(this.project_id, this.doc_id, {
  384. lines: this.lines,
  385. version: this.version,
  386. })
  387. DocUpdaterClient.preloadDoc(this.project_id, this.doc_id, error => {
  388. if (error) {
  389. throw error
  390. }
  391. DocUpdaterClient.sendUpdate(
  392. this.project_id,
  393. this.doc_id,
  394. this.update,
  395. error => {
  396. if (error) {
  397. throw error
  398. }
  399. // Go back to old lines, with undo flag
  400. DocUpdaterClient.setDocLines(
  401. this.project_id,
  402. this.doc_id,
  403. this.lines,
  404. this.source,
  405. this.user_id,
  406. true,
  407. (error, res, body) => {
  408. if (error) {
  409. return done(error)
  410. }
  411. this.statusCode = res.statusCode
  412. setTimeout(done, 200)
  413. }
  414. )
  415. }
  416. )
  417. })
  418. })
  419. after(function () {
  420. MockTrackChangesApi.flushDoc.resetHistory()
  421. MockProjectHistoryApi.flushProject.resetHistory()
  422. MockWebApi.setDocument.resetHistory()
  423. })
  424. it('should undo the tracked changes', function (done) {
  425. DocUpdaterClient.getDoc(
  426. this.project_id,
  427. this.doc_id,
  428. (error, res, data) => {
  429. if (error) {
  430. throw error
  431. }
  432. const { ranges } = data
  433. expect(ranges.changes).to.be.undefined
  434. done()
  435. }
  436. )
  437. })
  438. })
  439. describe('without the undo flag', function () {
  440. before(function (done) {
  441. this.project_id = DocUpdaterClient.randomId()
  442. this.doc_id = DocUpdaterClient.randomId()
  443. MockWebApi.insertDoc(this.project_id, this.doc_id, {
  444. lines: this.lines,
  445. version: this.version,
  446. })
  447. DocUpdaterClient.preloadDoc(this.project_id, this.doc_id, error => {
  448. if (error) {
  449. throw error
  450. }
  451. DocUpdaterClient.sendUpdate(
  452. this.project_id,
  453. this.doc_id,
  454. this.update,
  455. error => {
  456. if (error) {
  457. throw error
  458. }
  459. // Go back to old lines, without undo flag
  460. DocUpdaterClient.setDocLines(
  461. this.project_id,
  462. this.doc_id,
  463. this.lines,
  464. this.source,
  465. this.user_id,
  466. false,
  467. (error, res, body) => {
  468. if (error) {
  469. return done(error)
  470. }
  471. this.statusCode = res.statusCode
  472. setTimeout(done, 200)
  473. }
  474. )
  475. }
  476. )
  477. })
  478. })
  479. after(function () {
  480. MockTrackChangesApi.flushDoc.resetHistory()
  481. MockProjectHistoryApi.flushProject.resetHistory()
  482. MockWebApi.setDocument.resetHistory()
  483. })
  484. it('should not undo the tracked changes', function (done) {
  485. DocUpdaterClient.getDoc(
  486. this.project_id,
  487. this.doc_id,
  488. (error, res, data) => {
  489. if (error) {
  490. throw error
  491. }
  492. const { ranges } = data
  493. expect(ranges.changes.length).to.equal(1)
  494. done()
  495. }
  496. )
  497. })
  498. })
  499. })
  500. })