UrlCachingTests.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617
  1. /* eslint-disable
  2. no-unused-vars,
  3. */
  4. // TODO: This file was created by bulk-decaffeinate.
  5. // Fix any style issues and re-enable lint.
  6. /*
  7. * decaffeinate suggestions:
  8. * DS102: Remove unnecessary code created because of implicit returns
  9. * DS207: Consider shorter variations of null checks
  10. * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  11. */
  12. const express = require('express')
  13. const Path = require('node:path')
  14. const Client = require('./helpers/Client')
  15. const sinon = require('sinon')
  16. const ClsiApp = require('./helpers/ClsiApp')
  17. const request = require('request')
  18. const Settings = require('@overleaf/settings')
  19. const Server = {
  20. run() {
  21. const app = express()
  22. const staticServer = express.static(Path.join(__dirname, '../fixtures/'))
  23. const alreadyFailed = new Map()
  24. app.get('/fail/:times/:id', (req, res) => {
  25. this.getFile(req.url)
  26. const soFar = alreadyFailed.get(req.params.id) || 0
  27. const wanted = parseInt(req.params.times, 10)
  28. if (soFar < wanted) {
  29. alreadyFailed.set(req.params.id, soFar + 1)
  30. res.status(503).end()
  31. } else {
  32. res.send('THE CONTENT')
  33. }
  34. })
  35. app.get('/not-found', (req, res, next) => {
  36. this.getFile(req.url)
  37. res.status(404).end()
  38. })
  39. app.get('/project/:projectId/file/:fileId', (req, res, next) => {
  40. this.getFile(req.url)
  41. return res.send(`${req.params.projectId}:${req.params.fileId}`)
  42. })
  43. app.get('/bucket/:bucket/key/*', (req, res, next) => {
  44. this.getFile(req.url)
  45. return res.send(`${req.params.bucket}:${req.params[0]}`)
  46. })
  47. app.get('/:random_id/*', (req, res, next) => {
  48. this.getFile(req.url)
  49. req.url = `/${req.params[0]}`
  50. return staticServer(req, res, next)
  51. })
  52. Client.startFakeFilestoreApp(app)
  53. },
  54. getFile() {},
  55. randomId() {
  56. return Math.random().toString(16).slice(2)
  57. },
  58. }
  59. describe('Url Caching', function () {
  60. Server.run()
  61. describe('Retries', function () {
  62. before(function (done) {
  63. this.project_id = Client.randomId()
  64. this.happyFile = `${Server.randomId()}/lion.png`
  65. this.retryFileOnce = `fail/1/${Server.randomId()}`
  66. this.retryFileTwice = `fail/2/${Server.randomId()}`
  67. this.fatalFile = `fail/42/${Server.randomId()}`
  68. this.request = {
  69. resources: [
  70. {
  71. path: 'main.tex',
  72. content: `\
  73. \\documentclass{article}
  74. \\usepackage{graphicx}
  75. \\begin{document}
  76. \\includegraphics{lion.png}
  77. \\end{document}\
  78. `,
  79. },
  80. {
  81. path: 'lion.png',
  82. url: `http://filestore/${this.happyFile}`,
  83. },
  84. {
  85. path: 'foo.tex',
  86. url: `http://filestore/${this.retryFileOnce}`,
  87. },
  88. {
  89. path: 'foo.tex',
  90. url: `http://filestore/${this.retryFileTwice}`,
  91. },
  92. {
  93. path: 'foo.tex',
  94. url: `http://filestore/${this.fatalFile}`,
  95. },
  96. ],
  97. }
  98. sinon.spy(Server, 'getFile')
  99. ClsiApp.ensureRunning(() => {
  100. Client.compile(this.project_id, this.request, (error, res, body) => {
  101. this.error = error
  102. this.res = res
  103. this.body = body
  104. done()
  105. })
  106. })
  107. })
  108. after(function () {
  109. Server.getFile.restore()
  110. })
  111. function expectNFilestoreRequests(file, count) {
  112. Server.getFile.args.filter(a => a[0] === file).should.have.length(count)
  113. }
  114. it('should download the happy file once', function () {
  115. expectNFilestoreRequests(`/${this.happyFile}`, 1)
  116. })
  117. it('should retry the download of the unhappy files', function () {
  118. expectNFilestoreRequests(`/${this.retryFileOnce}`, 2)
  119. expectNFilestoreRequests(`/${this.retryFileTwice}`, 3)
  120. expectNFilestoreRequests(`/${this.fatalFile}`, 3)
  121. })
  122. })
  123. describe('Downloading an image for the first time', function () {
  124. before(function (done) {
  125. this.project_id = Client.randomId()
  126. this.file = `${Server.randomId()}/lion.png`
  127. this.request = {
  128. resources: [
  129. {
  130. path: 'main.tex',
  131. content: `\
  132. \\documentclass{article}
  133. \\usepackage{graphicx}
  134. \\begin{document}
  135. \\includegraphics{lion.png}
  136. \\end{document}\
  137. `,
  138. },
  139. {
  140. path: 'lion.png',
  141. url: `http://filestore/${this.file}`,
  142. },
  143. ],
  144. }
  145. sinon.spy(Server, 'getFile')
  146. return ClsiApp.ensureRunning(() => {
  147. return Client.compile(
  148. this.project_id,
  149. this.request,
  150. (error, res, body) => {
  151. this.error = error
  152. this.res = res
  153. this.body = body
  154. return done()
  155. }
  156. )
  157. })
  158. })
  159. afterEach(function () {
  160. return Server.getFile.restore()
  161. })
  162. return it('should download the image', function () {
  163. return Server.getFile.calledWith(`/${this.file}`).should.equal(true)
  164. })
  165. })
  166. describe('When an image is in the cache and the last modified date is unchanged', function () {
  167. before(function (done) {
  168. this.project_id = Client.randomId()
  169. this.file = `${Server.randomId()}/lion.png`
  170. this.request = {
  171. resources: [
  172. {
  173. path: 'main.tex',
  174. content: `\
  175. \\documentclass{article}
  176. \\usepackage{graphicx}
  177. \\begin{document}
  178. \\includegraphics{lion.png}
  179. \\end{document}\
  180. `,
  181. },
  182. (this.image_resource = {
  183. path: 'lion.png',
  184. url: `http://filestore/${this.file}`,
  185. modified: Date.now(),
  186. }),
  187. ],
  188. }
  189. return Client.compile(
  190. this.project_id,
  191. this.request,
  192. (error, res, body) => {
  193. this.error = error
  194. this.res = res
  195. this.body = body
  196. sinon.spy(Server, 'getFile')
  197. return Client.compile(
  198. this.project_id,
  199. this.request,
  200. (error1, res1, body1) => {
  201. this.error = error1
  202. this.res = res1
  203. this.body = body1
  204. return done()
  205. }
  206. )
  207. }
  208. )
  209. })
  210. after(function () {
  211. return Server.getFile.restore()
  212. })
  213. it('should not download the image again', function () {
  214. return Server.getFile.called.should.equal(false)
  215. })
  216. it('should gather metrics', function (done) {
  217. request.get(`${Settings.apis.clsi.url}/metrics`, (err, res, body) => {
  218. if (err) return done(err)
  219. body
  220. .split('\n')
  221. .some(line => {
  222. return (
  223. line.startsWith('url_source') && line.includes('path="unknown"')
  224. )
  225. })
  226. .should.equal(true)
  227. done()
  228. })
  229. })
  230. })
  231. describe('When an image is in the cache and the last modified date is advanced', function () {
  232. before(function (done) {
  233. this.project_id = Client.randomId()
  234. this.file = `${Server.randomId()}/lion.png`
  235. this.request = {
  236. resources: [
  237. {
  238. path: 'main.tex',
  239. content: `\
  240. \\documentclass{article}
  241. \\usepackage{graphicx}
  242. \\begin{document}
  243. \\includegraphics{lion.png}
  244. \\end{document}\
  245. `,
  246. },
  247. (this.image_resource = {
  248. path: 'lion.png',
  249. url: `http://filestore/${this.file}`,
  250. modified: (this.last_modified = Date.now()),
  251. }),
  252. ],
  253. }
  254. return Client.compile(
  255. this.project_id,
  256. this.request,
  257. (error, res, body) => {
  258. this.error = error
  259. this.res = res
  260. this.body = body
  261. sinon.spy(Server, 'getFile')
  262. this.image_resource.modified = new Date(this.last_modified + 3000)
  263. return Client.compile(
  264. this.project_id,
  265. this.request,
  266. (error1, res1, body1) => {
  267. this.error = error1
  268. this.res = res1
  269. this.body = body1
  270. return done()
  271. }
  272. )
  273. }
  274. )
  275. })
  276. afterEach(function () {
  277. return Server.getFile.restore()
  278. })
  279. return it('should download the image again', function () {
  280. return Server.getFile.called.should.equal(true)
  281. })
  282. })
  283. describe('When an image is in the cache and the last modified date is further in the past', function () {
  284. before(function (done) {
  285. this.project_id = Client.randomId()
  286. this.file = `${Server.randomId()}/lion.png`
  287. this.request = {
  288. resources: [
  289. {
  290. path: 'main.tex',
  291. content: `\
  292. \\documentclass{article}
  293. \\usepackage{graphicx}
  294. \\begin{document}
  295. \\includegraphics{lion.png}
  296. \\end{document}\
  297. `,
  298. },
  299. (this.image_resource = {
  300. path: 'lion.png',
  301. url: `http://filestore/${this.file}`,
  302. modified: (this.last_modified = Date.now()),
  303. }),
  304. ],
  305. }
  306. return Client.compile(
  307. this.project_id,
  308. this.request,
  309. (error, res, body) => {
  310. this.error = error
  311. this.res = res
  312. this.body = body
  313. sinon.spy(Server, 'getFile')
  314. this.image_resource.modified = new Date(this.last_modified - 3000)
  315. return Client.compile(
  316. this.project_id,
  317. this.request,
  318. (error1, res1, body1) => {
  319. this.error = error1
  320. this.res = res1
  321. this.body = body1
  322. return done()
  323. }
  324. )
  325. }
  326. )
  327. })
  328. afterEach(function () {
  329. return Server.getFile.restore()
  330. })
  331. return it('should download the other revision', function () {
  332. return Server.getFile.called.should.equal(true)
  333. })
  334. })
  335. describe('When an image is in the cache and the last modified date is not specified', function () {
  336. before(function (done) {
  337. this.project_id = Client.randomId()
  338. this.file = `${Server.randomId()}/lion.png`
  339. this.request = {
  340. resources: [
  341. {
  342. path: 'main.tex',
  343. content: `\
  344. \\documentclass{article}
  345. \\usepackage{graphicx}
  346. \\begin{document}
  347. \\includegraphics{lion.png}
  348. \\end{document}\
  349. `,
  350. },
  351. (this.image_resource = {
  352. path: 'lion.png',
  353. url: `http://filestore/${this.file}`,
  354. modified: (this.last_modified = Date.now()),
  355. }),
  356. ],
  357. }
  358. return Client.compile(
  359. this.project_id,
  360. this.request,
  361. (error, res, body) => {
  362. this.error = error
  363. this.res = res
  364. this.body = body
  365. sinon.spy(Server, 'getFile')
  366. delete this.image_resource.modified
  367. return Client.compile(
  368. this.project_id,
  369. this.request,
  370. (error1, res1, body1) => {
  371. this.error = error1
  372. this.res = res1
  373. this.body = body1
  374. return done()
  375. }
  376. )
  377. }
  378. )
  379. })
  380. afterEach(function () {
  381. return Server.getFile.restore()
  382. })
  383. return it('should download the image again', function () {
  384. return Server.getFile.called.should.equal(true)
  385. })
  386. })
  387. describe('After clearing the cache', function () {
  388. before(function (done) {
  389. this.project_id = Client.randomId()
  390. this.file = `${Server.randomId()}/lion.png`
  391. this.request = {
  392. resources: [
  393. {
  394. path: 'main.tex',
  395. content: `\
  396. \\documentclass{article}
  397. \\usepackage{graphicx}
  398. \\begin{document}
  399. \\includegraphics{lion.png}
  400. \\end{document}\
  401. `,
  402. },
  403. (this.image_resource = {
  404. path: 'lion.png',
  405. url: `http://filestore/${this.file}`,
  406. modified: (this.last_modified = Date.now()),
  407. }),
  408. ],
  409. }
  410. return Client.compile(this.project_id, this.request, error => {
  411. if (error != null) {
  412. throw error
  413. }
  414. return Client.clearCache(this.project_id, (error, res, body) => {
  415. if (error != null) {
  416. throw error
  417. }
  418. sinon.spy(Server, 'getFile')
  419. return Client.compile(
  420. this.project_id,
  421. this.request,
  422. (error1, res1, body1) => {
  423. this.error = error1
  424. this.res = res1
  425. this.body = body1
  426. return done()
  427. }
  428. )
  429. })
  430. })
  431. })
  432. afterEach(function () {
  433. return Server.getFile.restore()
  434. })
  435. return it('should download the image again', function () {
  436. return Server.getFile.called.should.equal(true)
  437. })
  438. })
  439. describe('fallbackURL', function () {
  440. describe('when the primary resource is available', function () {
  441. before(function (done) {
  442. this.project_id = Client.randomId()
  443. this.file = `/project/${Server.randomId()}/file/${Server.randomId()}`
  444. this.fallback = `/bucket/project-blobs/key/ab/cd/${Server.randomId()}`
  445. this.request = {
  446. resources: [
  447. {
  448. path: 'main.tex',
  449. content: `\
  450. \\documentclass{article}
  451. \\usepackage{graphicx}
  452. \\begin{document}
  453. \\includegraphics{lion.png}
  454. \\end{document}\
  455. `,
  456. },
  457. {
  458. path: 'lion.png',
  459. url: `http://filestore${this.file}`,
  460. fallbackURL: `http://filestore${this.fallback}`,
  461. },
  462. ],
  463. }
  464. sinon.spy(Server, 'getFile')
  465. return ClsiApp.ensureRunning(() => {
  466. return Client.compile(
  467. this.project_id,
  468. this.request,
  469. (error, res, body) => {
  470. this.error = error
  471. this.res = res
  472. this.body = body
  473. return done()
  474. }
  475. )
  476. })
  477. })
  478. after(function () {
  479. return Server.getFile.restore()
  480. })
  481. it('should download from the primary', function () {
  482. Server.getFile.calledWith(this.file).should.equal(true)
  483. })
  484. it('should not download from the fallback', function () {
  485. Server.getFile.calledWith(this.fallback).should.equal(false)
  486. })
  487. it('should gather metrics', function (done) {
  488. request.get(`${Settings.apis.clsi.url}/metrics`, (err, res, body) => {
  489. if (err) return done(err)
  490. body
  491. .split('\n')
  492. .some(line => {
  493. return (
  494. line.startsWith('url_source') &&
  495. line.includes('path="user-files"')
  496. )
  497. })
  498. .should.equal(true)
  499. done()
  500. })
  501. })
  502. })
  503. describe('when the primary resource is not available', function () {
  504. before(function (done) {
  505. this.project_id = Client.randomId()
  506. this.file = `/project/${Server.randomId()}/file/${Server.randomId()}`
  507. this.fallback = `/bucket/project-blobs/key/ab/cd/${Server.randomId()}`
  508. this.request = {
  509. resources: [
  510. {
  511. path: 'main.tex',
  512. content: `\
  513. \\documentclass{article}
  514. \\usepackage{graphicx}
  515. \\begin{document}
  516. \\includegraphics{lion.png}
  517. \\end{document}\
  518. `,
  519. },
  520. {
  521. path: 'lion.png',
  522. url: `http://filestore/not-found`,
  523. fallbackURL: `http://filestore${this.fallback}`,
  524. },
  525. ],
  526. }
  527. sinon.spy(Server, 'getFile')
  528. return ClsiApp.ensureRunning(() => {
  529. return Client.compile(
  530. this.project_id,
  531. this.request,
  532. (error, res, body) => {
  533. this.error = error
  534. this.res = res
  535. this.body = body
  536. return done()
  537. }
  538. )
  539. })
  540. })
  541. after(function () {
  542. return Server.getFile.restore()
  543. })
  544. it('should download from the fallback', function () {
  545. Server.getFile.calledWith(`/not-found`).should.equal(true)
  546. Server.getFile.calledWith(this.fallback).should.equal(true)
  547. })
  548. it('should gather metrics', function (done) {
  549. request.get(`${Settings.apis.clsi.url}/metrics`, (err, res, body) => {
  550. if (err) return done(err)
  551. body
  552. .split('\n')
  553. .some(line => {
  554. return (
  555. line.startsWith('url_source') &&
  556. line.includes('path="project-blobs"')
  557. )
  558. })
  559. .should.equal(true)
  560. done()
  561. })
  562. })
  563. })
  564. })
  565. })