GettingADocumentTests.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414
  1. const sinon = require('sinon')
  2. const { expect } = require('chai')
  3. const MockWebApi = require('./helpers/MockWebApi')
  4. const DocUpdaterClient = require('./helpers/DocUpdaterClient')
  5. const DocUpdaterApp = require('./helpers/DocUpdaterApp')
  6. const { RequestFailedError } = require('@overleaf/fetch-utils')
  7. const PersistenceManager = require('../../../app/js/PersistenceManager')
  8. describe('Getting a document', function () {
  9. before(async function () {
  10. this.lines = ['one', 'two', 'three']
  11. this.version = 42
  12. await DocUpdaterApp.ensureRunning()
  13. })
  14. describe('when the document is not loaded', function () {
  15. before(async function () {
  16. this.project_id = DocUpdaterClient.randomId()
  17. this.doc_id = DocUpdaterClient.randomId()
  18. sinon.spy(MockWebApi, 'getDocument')
  19. MockWebApi.insertDoc(this.project_id, this.doc_id, {
  20. lines: this.lines,
  21. version: this.version,
  22. })
  23. this.returnedDoc = await DocUpdaterClient.getDoc(
  24. this.project_id,
  25. this.doc_id
  26. )
  27. })
  28. after(function () {
  29. MockWebApi.getDocument.restore()
  30. })
  31. it('should load the document from the web API', function () {
  32. MockWebApi.getDocument
  33. .calledWith(this.project_id, this.doc_id)
  34. .should.equal(true)
  35. })
  36. it('should return the document lines', function () {
  37. this.returnedDoc.lines.should.deep.equal(this.lines)
  38. })
  39. it('should return the document at its current version', function () {
  40. this.returnedDoc.version.should.equal(this.version)
  41. })
  42. })
  43. describe('when the document is not loaded and the peek option is used', function () {
  44. before(async function () {
  45. const origGetDocumentController =
  46. MockWebApi.getDocumentController.bind(MockWebApi)
  47. sinon
  48. .stub(MockWebApi, 'getDocumentController')
  49. .callsFake((req, res, next) => {
  50. expect(req.query.peek).to.equal('true')
  51. return origGetDocumentController(req, res, next)
  52. })
  53. this.project_id = DocUpdaterClient.randomId()
  54. this.doc_id = DocUpdaterClient.randomId()
  55. sinon.spy(MockWebApi, 'getDocument')
  56. MockWebApi.insertDoc(this.project_id, this.doc_id, {
  57. lines: this.lines,
  58. version: this.version,
  59. })
  60. // This is only used by the resync code and not exposed on the HTTP
  61. // api so we are calling it directly.
  62. this.returnedDoc = await PersistenceManager.promises.getDoc(
  63. this.project_id,
  64. this.doc_id,
  65. { peek: true }
  66. )
  67. })
  68. after(function () {
  69. MockWebApi.getDocumentController.restore()
  70. MockWebApi.getDocument.restore()
  71. })
  72. it('should load the document from the web API with peek=true', function () {
  73. MockWebApi.getDocument
  74. .calledWith(this.project_id, this.doc_id)
  75. .should.equal(true)
  76. })
  77. it('should return the document lines', function () {
  78. this.returnedDoc.lines.should.deep.equal(this.lines)
  79. })
  80. it('should return the document at its current version', function () {
  81. this.returnedDoc.version.should.equal(this.version)
  82. })
  83. })
  84. describe('when the document is already loaded', function () {
  85. before(async function () {
  86. this.project_id = DocUpdaterClient.randomId()
  87. this.doc_id = DocUpdaterClient.randomId()
  88. MockWebApi.insertDoc(this.project_id, this.doc_id, {
  89. lines: this.lines,
  90. version: this.version,
  91. })
  92. await DocUpdaterClient.preloadDoc(this.project_id, this.doc_id)
  93. sinon.spy(MockWebApi, 'getDocument')
  94. this.returnedDoc = await DocUpdaterClient.getDoc(
  95. this.project_id,
  96. this.doc_id
  97. )
  98. })
  99. after(function () {
  100. MockWebApi.getDocument.restore()
  101. })
  102. it('should not load the document from the web API', function () {
  103. MockWebApi.getDocument.called.should.equal(false)
  104. })
  105. it('should return the document lines', function () {
  106. this.returnedDoc.lines.should.deep.equal(this.lines)
  107. })
  108. })
  109. describe('when the request asks for some recent ops', function () {
  110. before(async function () {
  111. this.project_id = DocUpdaterClient.randomId()
  112. this.doc_id = DocUpdaterClient.randomId()
  113. MockWebApi.insertDoc(this.project_id, this.doc_id, {
  114. lines: (this.lines = ['one', 'two', 'three']),
  115. })
  116. this.updates = __range__(0, 199, true).map(v => ({
  117. doc_id: this.doc_id,
  118. op: [{ i: v.toString(), p: 0 }],
  119. v,
  120. }))
  121. await DocUpdaterClient.sendUpdates(
  122. this.project_id,
  123. this.doc_id,
  124. this.updates
  125. )
  126. sinon.spy(MockWebApi, 'getDocument')
  127. })
  128. after(function () {
  129. MockWebApi.getDocument.restore()
  130. })
  131. describe('when the ops are loaded', function () {
  132. before(async function () {
  133. this.returnedDoc = await DocUpdaterClient.getDocAndRecentOps(
  134. this.project_id,
  135. this.doc_id,
  136. 190
  137. )
  138. })
  139. it('should return the recent ops', function () {
  140. this.returnedDoc.ops.length.should.equal(10)
  141. for (const [i, update] of this.updates.slice(190, -1).entries()) {
  142. this.returnedDoc.ops[i].op.should.deep.equal(update.op)
  143. }
  144. })
  145. })
  146. describe('when the ops are not all loaded', function () {
  147. it('should return UnprocessableEntity', async function () {
  148. // We only track 100 ops
  149. await expect(
  150. DocUpdaterClient.getDocAndRecentOps(this.project_id, this.doc_id, 10)
  151. )
  152. .to.be.rejectedWith(RequestFailedError)
  153. .and.eventually.have.nested.property('response.status', 422)
  154. })
  155. })
  156. })
  157. describe('when the document does not exist', function () {
  158. it('should return 404', async function () {
  159. const projectId = DocUpdaterClient.randomId()
  160. const docId = DocUpdaterClient.randomId()
  161. await expect(DocUpdaterClient.getDoc(projectId, docId))
  162. .to.be.rejectedWith(RequestFailedError)
  163. .and.eventually.have.nested.property('response.status', 404)
  164. })
  165. })
  166. describe('when the web api returns an error', function () {
  167. beforeEach(function () {
  168. sinon.stub(MockWebApi, 'getDocument').rejects(new Error('oops'))
  169. })
  170. afterEach(function () {
  171. MockWebApi.getDocument.restore()
  172. })
  173. it('should return 500', async function () {
  174. const projectId = DocUpdaterClient.randomId()
  175. const docId = DocUpdaterClient.randomId()
  176. await expect(DocUpdaterClient.getDoc(projectId, docId))
  177. .to.be.rejectedWith(RequestFailedError)
  178. .and.eventually.have.nested.property('response.status', 500)
  179. })
  180. it('should retry the request', async function () {
  181. const projectId = DocUpdaterClient.randomId()
  182. const docId = DocUpdaterClient.randomId()
  183. await expect(DocUpdaterClient.getDoc(projectId, docId))
  184. .to.be.rejectedWith(RequestFailedError)
  185. .and.eventually.have.nested.property('response.status', 500)
  186. expect(MockWebApi.getDocument).to.be.calledTwice
  187. })
  188. })
  189. describe('when the web api returns a retryable error on the first attempt', function () {
  190. beforeEach(function () {
  191. const origGetDocumentController =
  192. MockWebApi.getDocumentController.bind(MockWebApi)
  193. const getDocumentStub = sinon
  194. .stub(MockWebApi, 'getDocumentController')
  195. .onCall(0)
  196. .callsFake((req, res, next) => {
  197. res.destroy() // simulate a network error
  198. })
  199. getDocumentStub.onCall(1).callsFake(origGetDocumentController)
  200. })
  201. afterEach(function () {
  202. MockWebApi.getDocumentController.restore()
  203. })
  204. it('should return 200', async function () {
  205. const projectId = DocUpdaterClient.randomId()
  206. const docId = DocUpdaterClient.randomId()
  207. MockWebApi.insertDoc(projectId, docId, {
  208. lines: this.lines,
  209. version: this.version,
  210. })
  211. await expect(
  212. DocUpdaterClient.getDoc(projectId, docId)
  213. ).to.eventually.deep.include({ lines: this.lines, version: this.version })
  214. })
  215. it('should retry the request', async function () {
  216. const projectId = DocUpdaterClient.randomId()
  217. const docId = DocUpdaterClient.randomId()
  218. MockWebApi.insertDoc(projectId, docId, {
  219. lines: this.lines,
  220. version: this.version,
  221. })
  222. await expect(
  223. DocUpdaterClient.getDoc(projectId, docId)
  224. ).to.eventually.deep.include({ lines: this.lines, version: this.version })
  225. expect(MockWebApi.getDocumentController).to.be.calledTwice
  226. })
  227. })
  228. describe('when the web api returns a 413 error', function () {
  229. beforeEach(function () {
  230. sinon
  231. .stub(MockWebApi, 'getDocumentController')
  232. .callsFake((req, res, next) => {
  233. res.sendStatus(413)
  234. })
  235. })
  236. afterEach(function () {
  237. MockWebApi.getDocumentController.restore()
  238. })
  239. it('should return 413', async function () {
  240. const projectId = DocUpdaterClient.randomId()
  241. const docId = DocUpdaterClient.randomId()
  242. await expect(DocUpdaterClient.getDoc(projectId, docId))
  243. .to.be.rejectedWith(RequestFailedError)
  244. .and.eventually.have.nested.property('response.status', 413)
  245. })
  246. it('should not retry the request', async function () {
  247. const projectId = DocUpdaterClient.randomId()
  248. const docId = DocUpdaterClient.randomId()
  249. await expect(DocUpdaterClient.getDoc(projectId, docId))
  250. .to.be.rejectedWith(RequestFailedError)
  251. .and.eventually.have.nested.property('response.status', 413)
  252. expect(MockWebApi.getDocumentController).to.be.calledOnce
  253. })
  254. })
  255. describe('when the web api returns an incomplete doc', function () {
  256. afterEach(function () {
  257. MockWebApi.getDocument.restore()
  258. })
  259. it('should return an error for missing lines', async function () {
  260. const projectId = DocUpdaterClient.randomId()
  261. const docId = DocUpdaterClient.randomId()
  262. sinon
  263. .stub(MockWebApi, 'getDocument')
  264. .resolves({ version: 123, pathname: 'test' })
  265. await expect(DocUpdaterClient.getDoc(projectId, docId))
  266. .to.be.rejectedWith(RequestFailedError)
  267. .and.eventually.have.nested.property('response.status', 422)
  268. })
  269. it('should return an error for missing version', async function () {
  270. const projectId = DocUpdaterClient.randomId()
  271. const docId = DocUpdaterClient.randomId()
  272. sinon
  273. .stub(MockWebApi, 'getDocument')
  274. .resolves({ lines: [''], pathname: 'test' })
  275. await expect(DocUpdaterClient.getDoc(projectId, docId))
  276. .to.be.rejectedWith(RequestFailedError)
  277. .and.eventually.have.nested.property('response.status', 422)
  278. })
  279. it('should return an error for missing pathname', async function () {
  280. const projectId = DocUpdaterClient.randomId()
  281. const docId = DocUpdaterClient.randomId()
  282. sinon
  283. .stub(MockWebApi, 'getDocument')
  284. .resolves({ lines: [''], version: 123 })
  285. await expect(DocUpdaterClient.getDoc(projectId, docId))
  286. .to.be.rejectedWith(RequestFailedError)
  287. .and.eventually.have.nested.property('response.status', 422)
  288. })
  289. })
  290. describe('when the web api http request times out on the first request', function () {
  291. before(function (done) {
  292. this.project_id = DocUpdaterClient.randomId()
  293. this.doc_id = DocUpdaterClient.randomId()
  294. MockWebApi.insertDoc(this.project_id, this.doc_id, {
  295. lines: this.lines,
  296. version: this.version,
  297. })
  298. sinon
  299. .stub(MockWebApi, 'getDocument')
  300. .onFirstCall()
  301. .returns(
  302. new Promise(resolve => {
  303. setTimeout(() => resolve(null), 30_000)
  304. })
  305. )
  306. .callThrough() // subsequent requests return normally
  307. done()
  308. })
  309. after(function () {
  310. MockWebApi.getDocument.restore()
  311. })
  312. it('should retry the request and return the document', async function () {
  313. const returnedDoc = await DocUpdaterClient.getDoc(
  314. this.project_id,
  315. this.doc_id
  316. )
  317. expect(returnedDoc).to.deep.include({
  318. lines: this.lines,
  319. version: this.version,
  320. })
  321. })
  322. })
  323. describe('when the web api http request times out repeatedly', function () {
  324. before(function (done) {
  325. this.timeout = 10000
  326. sinon.stub(MockWebApi, 'getDocument').returns(
  327. new Promise(resolve => {
  328. setTimeout(() => resolve(null), 30_000)
  329. })
  330. )
  331. done()
  332. })
  333. after(function () {
  334. MockWebApi.getDocument.restore()
  335. })
  336. it('should return an error after two attempts', async function () {
  337. const projectId = DocUpdaterClient.randomId()
  338. const docId = DocUpdaterClient.randomId()
  339. const start = Date.now()
  340. await expect(DocUpdaterClient.getDoc(projectId, docId))
  341. .to.be.rejectedWith(RequestFailedError)
  342. .and.eventually.have.nested.property('response.status', 500)
  343. const delta = Date.now() - start
  344. expect(delta).to.be.above(10_000)
  345. expect(delta).to.be.below(20_000)
  346. })
  347. })
  348. })
  349. function __range__(left, right, inclusive) {
  350. const range = []
  351. const ascending = left < right
  352. const end = !inclusive ? right : ascending ? right + 1 : right - 1
  353. for (let i = left; ascending ? i < end : i > end; ascending ? i++ : i--) {
  354. range.push(i)
  355. }
  356. return range
  357. }