SettingADocumentTests.js 13 KB

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