SettingADocumentTests.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462
  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('redis-sharelatex').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.reset()
  86. MockProjectHistoryApi.flushProject.reset()
  87. MockWebApi.setDocument.reset()
  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.reset()
  162. MockProjectHistoryApi.flushProject.reset()
  163. MockWebApi.setDocument.reset()
  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. describe('when the updated doc is too large for the body parser', function () {
  195. before(function (done) {
  196. this.project_id = DocUpdaterClient.randomId()
  197. this.doc_id = DocUpdaterClient.randomId()
  198. MockWebApi.insertDoc(this.project_id, this.doc_id, {
  199. lines: this.lines,
  200. version: this.version
  201. })
  202. this.newLines = []
  203. while (
  204. JSON.stringify(this.newLines).length <= Settings.maxJsonRequestSize
  205. ) {
  206. this.newLines.push('(a long line of text)'.repeat(10000))
  207. }
  208. DocUpdaterClient.setDocLines(
  209. this.project_id,
  210. this.doc_id,
  211. this.newLines,
  212. this.source,
  213. this.user_id,
  214. false,
  215. (error, res, body) => {
  216. if (error) {
  217. return done(error)
  218. }
  219. this.statusCode = res.statusCode
  220. setTimeout(done, 200)
  221. }
  222. )
  223. })
  224. after(function () {
  225. MockTrackChangesApi.flushDoc.reset()
  226. MockProjectHistoryApi.flushProject.reset()
  227. MockWebApi.setDocument.reset()
  228. })
  229. it('should return a 413 status code', function () {
  230. this.statusCode.should.equal(413)
  231. })
  232. it('should not send the updated doc lines to the web api', function () {
  233. MockWebApi.setDocument.called.should.equal(false)
  234. })
  235. it('should not flush track changes', function () {
  236. MockTrackChangesApi.flushDoc.called.should.equal(false)
  237. })
  238. it('should not flush project history', function () {
  239. MockProjectHistoryApi.flushProject.called.should.equal(false)
  240. })
  241. })
  242. describe('when the updated doc is large but under the bodyParser and HTTPController size limit', function () {
  243. before(function (done) {
  244. this.project_id = DocUpdaterClient.randomId()
  245. this.doc_id = DocUpdaterClient.randomId()
  246. MockWebApi.insertDoc(this.project_id, this.doc_id, {
  247. lines: this.lines,
  248. version: this.version
  249. })
  250. this.newLines = []
  251. while (JSON.stringify(this.newLines).length < 2 * 1024 * 1024) {
  252. // limit in HTTPController
  253. this.newLines.push('(a long line of text)'.repeat(10000))
  254. }
  255. this.newLines.pop() // remove the line which took it over the limit
  256. DocUpdaterClient.setDocLines(
  257. this.project_id,
  258. this.doc_id,
  259. this.newLines,
  260. this.source,
  261. this.user_id,
  262. false,
  263. (error, res, body) => {
  264. if (error) {
  265. return done(error)
  266. }
  267. this.statusCode = res.statusCode
  268. setTimeout(done, 200)
  269. }
  270. )
  271. })
  272. after(function () {
  273. MockTrackChangesApi.flushDoc.reset()
  274. MockProjectHistoryApi.flushProject.reset()
  275. MockWebApi.setDocument.reset()
  276. })
  277. it('should return a 204 status code', function () {
  278. this.statusCode.should.equal(204)
  279. })
  280. it('should send the updated doc lines to the web api', function () {
  281. MockWebApi.setDocument
  282. .calledWith(this.project_id, this.doc_id, this.newLines)
  283. .should.equal(true)
  284. })
  285. })
  286. describe('with track changes', function () {
  287. before(function () {
  288. this.lines = ['one', 'one and a half', 'two', 'three']
  289. this.id_seed = '587357bd35e64f6157'
  290. this.update = {
  291. doc: this.doc_id,
  292. op: [
  293. {
  294. d: 'one and a half\n',
  295. p: 4
  296. }
  297. ],
  298. meta: {
  299. tc: this.id_seed,
  300. user_id: this.user_id
  301. },
  302. v: this.version
  303. }
  304. })
  305. describe('with the undo flag', function () {
  306. before(function (done) {
  307. this.project_id = DocUpdaterClient.randomId()
  308. this.doc_id = DocUpdaterClient.randomId()
  309. MockWebApi.insertDoc(this.project_id, this.doc_id, {
  310. lines: this.lines,
  311. version: this.version
  312. })
  313. DocUpdaterClient.preloadDoc(this.project_id, this.doc_id, (error) => {
  314. if (error) {
  315. throw error
  316. }
  317. DocUpdaterClient.sendUpdate(
  318. this.project_id,
  319. this.doc_id,
  320. this.update,
  321. (error) => {
  322. if (error) {
  323. throw error
  324. }
  325. // Go back to old lines, with undo flag
  326. DocUpdaterClient.setDocLines(
  327. this.project_id,
  328. this.doc_id,
  329. this.lines,
  330. this.source,
  331. this.user_id,
  332. true,
  333. (error, res, body) => {
  334. if (error) {
  335. return done(error)
  336. }
  337. this.statusCode = res.statusCode
  338. setTimeout(done, 200)
  339. }
  340. )
  341. }
  342. )
  343. })
  344. })
  345. after(function () {
  346. MockTrackChangesApi.flushDoc.reset()
  347. MockProjectHistoryApi.flushProject.reset()
  348. MockWebApi.setDocument.reset()
  349. })
  350. it('should undo the tracked changes', function (done) {
  351. DocUpdaterClient.getDoc(
  352. this.project_id,
  353. this.doc_id,
  354. (error, res, data) => {
  355. if (error) {
  356. throw error
  357. }
  358. const { ranges } = data
  359. expect(ranges.changes).to.be.undefined
  360. done()
  361. }
  362. )
  363. })
  364. })
  365. describe('without the undo flag', function () {
  366. before(function (done) {
  367. this.project_id = DocUpdaterClient.randomId()
  368. this.doc_id = DocUpdaterClient.randomId()
  369. MockWebApi.insertDoc(this.project_id, this.doc_id, {
  370. lines: this.lines,
  371. version: this.version
  372. })
  373. DocUpdaterClient.preloadDoc(this.project_id, this.doc_id, (error) => {
  374. if (error) {
  375. throw error
  376. }
  377. DocUpdaterClient.sendUpdate(
  378. this.project_id,
  379. this.doc_id,
  380. this.update,
  381. (error) => {
  382. if (error) {
  383. throw error
  384. }
  385. // Go back to old lines, without undo flag
  386. DocUpdaterClient.setDocLines(
  387. this.project_id,
  388. this.doc_id,
  389. this.lines,
  390. this.source,
  391. this.user_id,
  392. false,
  393. (error, res, body) => {
  394. if (error) {
  395. return done(error)
  396. }
  397. this.statusCode = res.statusCode
  398. setTimeout(done, 200)
  399. }
  400. )
  401. }
  402. )
  403. })
  404. })
  405. after(function () {
  406. MockTrackChangesApi.flushDoc.reset()
  407. MockProjectHistoryApi.flushProject.reset()
  408. MockWebApi.setDocument.reset()
  409. })
  410. it('should not undo the tracked changes', function (done) {
  411. DocUpdaterClient.getDoc(
  412. this.project_id,
  413. this.doc_id,
  414. (error, res, data) => {
  415. if (error) {
  416. throw error
  417. }
  418. const { ranges } = data
  419. expect(ranges.changes.length).to.equal(1)
  420. done()
  421. }
  422. )
  423. })
  424. })
  425. })
  426. })