FilestoreTests.js 35 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000
  1. const chai = require('chai')
  2. const { expect } = chai
  3. const fs = require('fs')
  4. const Settings = require('@overleaf/settings')
  5. const Path = require('path')
  6. const FilestoreApp = require('./FilestoreApp')
  7. const TestHelper = require('./TestHelper')
  8. const fetch = require('node-fetch')
  9. const S3 = require('aws-sdk/clients/s3')
  10. const { promisify } = require('util')
  11. const { Storage } = require('@google-cloud/storage')
  12. const streamifier = require('streamifier')
  13. chai.use(require('chai-as-promised'))
  14. const { ObjectId } = require('mongodb')
  15. const ChildProcess = require('child_process')
  16. const fsWriteFile = promisify(fs.writeFile)
  17. const fsStat = promisify(fs.stat)
  18. const exec = promisify(ChildProcess.exec)
  19. const msleep = promisify(setTimeout)
  20. if (!process.env.AWS_ACCESS_KEY_ID) {
  21. throw new Error('please provide credentials for the AWS S3 test server')
  22. }
  23. process.on('unhandledRejection', e => {
  24. // eslint-disable-next-line no-console
  25. console.log('** Unhandled Promise Rejection **\n', e)
  26. throw e
  27. })
  28. // store settings for multiple backends, so that we can test each one.
  29. // fs will always be available - add others if they are configured
  30. const { BackendSettings, s3Config } = require('./TestConfig')
  31. const {
  32. AlreadyWrittenError,
  33. NotImplementedError,
  34. } = require('@overleaf/object-persistor/src/Errors')
  35. describe('Filestore', function () {
  36. this.timeout(1000 * 10)
  37. const filestoreUrl = `http://127.0.0.1:${Settings.internal.filestore.port}`
  38. const seenSockets = []
  39. async function expectNoSockets() {
  40. try {
  41. await msleep(1000)
  42. const { stdout } = await exec('ss -tn')
  43. const lines = stdout.split('\n')
  44. const header = lines.shift()
  45. const badSockets = []
  46. for (const socket of lines) {
  47. const fields = socket.split(' ').filter(part => part !== '')
  48. if (
  49. fields.length > 2 &&
  50. parseInt(fields[1]) &&
  51. !seenSockets.includes(socket)
  52. ) {
  53. badSockets.push(socket)
  54. seenSockets.push(socket)
  55. }
  56. }
  57. if (badSockets.length) {
  58. // eslint-disable-next-line no-console
  59. console.error(
  60. 'ERR: Sockets still have receive buffer after connection closed'
  61. )
  62. console.error(header)
  63. for (const socket of badSockets) {
  64. // eslint-disable-next-line no-console
  65. console.error(socket)
  66. }
  67. throw new Error('Sockets still open after connection closed')
  68. }
  69. } catch (err) {
  70. expect(err).not.to.exist
  71. }
  72. }
  73. // redefine the test suite for every available backend
  74. for (const [backendVariantWithShardNumber, backendSettings] of Object.entries(
  75. BackendSettings
  76. )) {
  77. describe(backendVariantWithShardNumber, function () {
  78. let app,
  79. previousEgress,
  80. previousIngress,
  81. metricPrefix,
  82. projectId,
  83. otherProjectId
  84. const BUCKET_NAMES = [
  85. process.env.GCS_USER_FILES_BUCKET_NAME,
  86. process.env.GCS_TEMPLATE_FILES_BUCKET_NAME,
  87. `${process.env.GCS_USER_FILES_BUCKET_NAME}-deleted`,
  88. `${process.env.GCS_TEMPLATE_FILES_BUCKET_NAME}-deleted`,
  89. ]
  90. before(async function () {
  91. // create the app with the relevant filestore settings
  92. Settings.filestore = backendSettings
  93. app = new FilestoreApp()
  94. await app.runServer()
  95. })
  96. if (backendSettings.gcs) {
  97. before(async function () {
  98. // create test buckets for gcs
  99. const storage = new Storage(Settings.filestore.gcs.endpoint)
  100. for (const bucketName of BUCKET_NAMES) {
  101. await storage.createBucket(bucketName)
  102. }
  103. })
  104. after(async function () {
  105. // tear down all the gcs buckets
  106. const storage = new Storage(Settings.filestore.gcs.endpoint)
  107. for (const bucketName of BUCKET_NAMES) {
  108. const bucket = storage.bucket(bucketName)
  109. await bucket.deleteFiles()
  110. await bucket.delete()
  111. }
  112. })
  113. }
  114. after(async function () {
  115. await msleep(3000)
  116. await app.stop()
  117. })
  118. beforeEach(async function () {
  119. // retrieve previous metrics from the app
  120. if (['s3', 'gcs'].includes(Settings.filestore.backend)) {
  121. metricPrefix = Settings.filestore.backend
  122. previousEgress = await TestHelper.getMetric(
  123. filestoreUrl,
  124. `${metricPrefix}_egress`
  125. )
  126. }
  127. projectId = new ObjectId().toString()
  128. otherProjectId = new ObjectId().toString()
  129. })
  130. it('should send a 200 for the status endpoint', async function () {
  131. const response = await fetch(`${filestoreUrl}/status`)
  132. expect(response.status).to.equal(200)
  133. const body = await response.text()
  134. expect(body).to.contain('filestore')
  135. expect(body).to.contain('up')
  136. })
  137. describe('with a file on the server', function () {
  138. let fileId, fileUrl, constantFileContent
  139. const localFileReadPath =
  140. '/tmp/filestore_acceptance_tests_file_read.txt'
  141. beforeEach(async function () {
  142. fileId = new ObjectId().toString()
  143. fileUrl = `${filestoreUrl}/project/${projectId}/file/${fileId}`
  144. constantFileContent = [
  145. 'hello world',
  146. `line 2 goes here ${Math.random()}`,
  147. 'there are 3 lines in all',
  148. ].join('\n')
  149. await fsWriteFile(localFileReadPath, constantFileContent)
  150. const readStream = fs.createReadStream(localFileReadPath)
  151. const res = await fetch(fileUrl, { method: 'POST', body: readStream })
  152. if (!res.ok) throw new Error(res.statusText)
  153. })
  154. beforeEach(async function retrievePreviousIngressMetrics() {
  155. // The upload request can bump the ingress metric.
  156. // The content hash validation might require a full download
  157. // in case the ETag field of the upload response is not a md5 sum.
  158. if (['s3', 'gcs'].includes(Settings.filestore.backend)) {
  159. previousIngress = await TestHelper.getMetric(
  160. filestoreUrl,
  161. `${metricPrefix}_ingress`
  162. )
  163. }
  164. })
  165. it('should return 404 for a non-existant id', async function () {
  166. const url = fileUrl + '___this_is_clearly_wrong___'
  167. const response = await fetch(url)
  168. expect(response.status).to.equal(404)
  169. })
  170. it('should return the file size on a HEAD request', async function () {
  171. const expectedLength = Buffer.byteLength(constantFileContent)
  172. const res = await fetch(fileUrl, { method: 'HEAD' })
  173. expect(res.status).to.equal(200)
  174. expect(res.headers.get('Content-Length')).to.equal(
  175. expectedLength.toString()
  176. )
  177. })
  178. it('should be able get the file back', async function () {
  179. const res = await fetch(fileUrl)
  180. const body = await res.text()
  181. expect(body).to.equal(constantFileContent)
  182. })
  183. it('should send a 200 for the health-check endpoint using the file', async function () {
  184. Settings.health_check = {
  185. project_id: projectId,
  186. file_id: fileId,
  187. }
  188. const response = await fetch(`${filestoreUrl}/health_check`)
  189. expect(response.status).to.equal(200)
  190. const body = await response.text()
  191. expect(body).to.equal('OK')
  192. })
  193. it('should not leak a socket', async function () {
  194. const res = await fetch(fileUrl)
  195. if (!res.ok) throw new Error(res.statusText)
  196. await res.text()
  197. await expectNoSockets()
  198. })
  199. it('should be able to get back the first 9 bytes of the file', async function () {
  200. const res = await fetch(fileUrl, { headers: { Range: 'bytes=0-8' } })
  201. const body = await res.text()
  202. expect(body).to.equal('hello wor')
  203. })
  204. it('should be able to get back bytes 4 through 10 of the file', async function () {
  205. const res = await fetch(fileUrl, { headers: { Range: 'bytes=4-10' } })
  206. const body = await res.text()
  207. expect(body).to.equal('o world')
  208. })
  209. it('should be able to delete the file', async function () {
  210. const response = await fetch(fileUrl, { method: 'DELETE' })
  211. expect(response.status).to.equal(204)
  212. const response2 = await fetch(fileUrl)
  213. expect(response2.status).to.equal(404)
  214. })
  215. it('should be able to copy files', async function () {
  216. const newProjectID = new ObjectId().toString()
  217. const newFileId = new ObjectId().toString()
  218. const newFileUrl = `${filestoreUrl}/project/${newProjectID}/file/${newFileId}`
  219. let response = await fetch(newFileUrl, {
  220. method: 'PUT',
  221. body: JSON.stringify({
  222. source: {
  223. project_id: projectId,
  224. file_id: fileId,
  225. },
  226. }),
  227. headers: {
  228. 'Content-Type': 'application/json',
  229. },
  230. })
  231. expect(response.status).to.equal(200)
  232. response = await fetch(fileUrl, { method: 'DELETE' })
  233. expect(response.status).to.equal(204)
  234. response = await fetch(newFileUrl)
  235. const body = await response.text()
  236. expect(body).to.equal(constantFileContent)
  237. })
  238. it('should be able to overwrite the file', async function () {
  239. const newContent = `here is some different content, ${Math.random()}`
  240. const readStream = streamifier.createReadStream(newContent)
  241. await fetch(fileUrl, { method: 'POST', body: readStream })
  242. const response = await fetch(fileUrl)
  243. const body = await response.text()
  244. expect(body).to.equal(newContent)
  245. })
  246. describe('IfNoneMatch', function () {
  247. if (backendSettings.backend === 'fs') {
  248. it('should refuse to handle IfNoneMatch', async function () {
  249. await expect(
  250. app.persistor.sendStream(
  251. Settings.filestore.stores.user_files,
  252. `${projectId}/${fileId}`,
  253. fs.createReadStream(localFileReadPath),
  254. { ifNoneMatch: '*' }
  255. )
  256. ).to.be.rejectedWith(NotImplementedError)
  257. })
  258. } else {
  259. it('should reject sendStream on the same key with IfNoneMatch', async function () {
  260. await expect(
  261. app.persistor.sendStream(
  262. Settings.filestore.stores.user_files,
  263. `${projectId}/${fileId}`,
  264. fs.createReadStream(localFileReadPath),
  265. { ifNoneMatch: '*' }
  266. )
  267. ).to.be.rejectedWith(AlreadyWrittenError)
  268. })
  269. it('should allow sendStream on a different key with IfNoneMatch', async function () {
  270. await app.persistor.sendStream(
  271. Settings.filestore.stores.user_files,
  272. `${projectId}/${fileId}-other`,
  273. fs.createReadStream(localFileReadPath),
  274. { ifNoneMatch: '*' }
  275. )
  276. })
  277. }
  278. })
  279. if (backendSettings.backend !== 'fs') {
  280. it('should record an egress metric for the upload', async function () {
  281. const metric = await TestHelper.getMetric(
  282. filestoreUrl,
  283. `${metricPrefix}_egress`
  284. )
  285. expect(metric - previousEgress).to.equal(constantFileContent.length)
  286. })
  287. it('should record an ingress metric when downloading the file', async function () {
  288. const response = await fetch(fileUrl)
  289. expect(response.ok).to.be.true
  290. await response.text()
  291. const metric = await TestHelper.getMetric(
  292. filestoreUrl,
  293. `${metricPrefix}_ingress`
  294. )
  295. expect(metric - previousIngress).to.equal(
  296. constantFileContent.length
  297. )
  298. })
  299. it('should record an ingress metric for a partial download', async function () {
  300. const response = await fetch(fileUrl, {
  301. headers: { Range: 'bytes=0-8' },
  302. })
  303. expect(response.ok).to.be.true
  304. await response.text()
  305. const metric = await TestHelper.getMetric(
  306. filestoreUrl,
  307. `${metricPrefix}_ingress`
  308. )
  309. expect(metric - previousIngress).to.equal(9)
  310. })
  311. }
  312. })
  313. describe('with multiple files', function () {
  314. let fileIds, fileUrls, otherFileUrls, projectUrl, otherProjectUrl
  315. const localFileReadPaths = [
  316. '/tmp/filestore_acceptance_tests_file_read_1.txt',
  317. '/tmp/filestore_acceptance_tests_file_read_2.txt',
  318. '/tmp/filestore_acceptance_tests_file_read_3.txt',
  319. ]
  320. const constantFileContents = [
  321. [
  322. 'hello world',
  323. `line 2 goes here ${Math.random()}`,
  324. 'there are 3 lines in all',
  325. ].join('\n'),
  326. [
  327. `for reference: ${Math.random()}`,
  328. 'cats are the best animals',
  329. 'wombats are a close second',
  330. ].join('\n'),
  331. [
  332. `another file: ${Math.random()}`,
  333. 'with multiple lines',
  334. 'the end',
  335. ].join('\n'),
  336. ]
  337. before(async function () {
  338. return await Promise.all([
  339. fsWriteFile(localFileReadPaths[0], constantFileContents[0]),
  340. fsWriteFile(localFileReadPaths[1], constantFileContents[1]),
  341. fsWriteFile(localFileReadPaths[2], constantFileContents[2]),
  342. ])
  343. })
  344. beforeEach(async function () {
  345. projectUrl = `${filestoreUrl}/project/${projectId}`
  346. otherProjectUrl = `${filestoreUrl}/project/${otherProjectId}`
  347. fileIds = [
  348. new ObjectId().toString(),
  349. new ObjectId().toString(),
  350. new ObjectId().toString(),
  351. ]
  352. fileUrls = [
  353. `${projectUrl}/file/${fileIds[0]}`,
  354. `${projectUrl}/file/${fileIds[1]}`,
  355. ]
  356. otherFileUrls = [`${otherProjectUrl}/file/${fileIds[2]}`]
  357. await Promise.all([
  358. fetch(fileUrls[0], {
  359. method: 'POST',
  360. body: fs.createReadStream(localFileReadPaths[0]),
  361. }),
  362. fetch(fileUrls[1], {
  363. method: 'POST',
  364. body: fs.createReadStream(localFileReadPaths[1]),
  365. }),
  366. fetch(otherFileUrls[0], {
  367. method: 'POST',
  368. body: fs.createReadStream(localFileReadPaths[2]),
  369. }),
  370. ])
  371. })
  372. it('should get the directory size', async function () {
  373. const response = await fetch(
  374. `${filestoreUrl}/project/${projectId}/size`
  375. )
  376. const body = await response.text()
  377. expect(parseInt(JSON.parse(body)['total bytes'])).to.equal(
  378. constantFileContents[0].length + constantFileContents[1].length
  379. )
  380. })
  381. it('should store the files', async function () {
  382. for (const index in fileUrls) {
  383. const response = await fetch(fileUrls[index])
  384. const body = await response.text()
  385. expect(body).to.equal(constantFileContents[index])
  386. }
  387. })
  388. it('should be able to delete the project', async function () {
  389. let response = await fetch(projectUrl, { method: 'DELETE' })
  390. expect(response.status).to.equal(204)
  391. for (const index in fileUrls) {
  392. response = await fetch(fileUrls[index])
  393. expect(response.status).to.equal(404)
  394. }
  395. })
  396. it('should not delete files in other projects', async function () {
  397. for (const index in otherFileUrls) {
  398. const response = await fetch(otherFileUrls[index])
  399. expect(response.status).to.equal(200)
  400. }
  401. })
  402. it('should not delete a partial project id', async function () {
  403. const response = await fetch(`${filestoreUrl}/project/5`, {
  404. method: 'DELETE',
  405. })
  406. expect(response.status).to.equal(400)
  407. })
  408. })
  409. describe('with a large file', function () {
  410. let fileId, fileUrl, largeFileContent, error
  411. beforeEach(async function () {
  412. fileId = new ObjectId().toString()
  413. fileUrl = `${filestoreUrl}/project/${projectId}/file/${fileId}`
  414. largeFileContent = '_wombat_'.repeat(1024 * 1024) // 8 megabytes
  415. largeFileContent += Math.random()
  416. const readStream = streamifier.createReadStream(largeFileContent)
  417. const res = await fetch(fileUrl, { method: 'POST', body: readStream })
  418. if (!res.ok) throw new Error(res.statusText)
  419. })
  420. it('should be able to get the file back', async function () {
  421. const response = await fetch(fileUrl)
  422. const body = await response.text()
  423. expect(body).to.equal(largeFileContent)
  424. })
  425. it('should not throw an error', function () {
  426. expect(error).not.to.exist
  427. })
  428. it('should not leak a socket', async function () {
  429. const response = await fetch(fileUrl)
  430. await response.text()
  431. await expectNoSockets()
  432. })
  433. it('should not leak a socket if the connection is aborted', async function () {
  434. const controller = new AbortController()
  435. const response = await fetch(fileUrl, { signal: controller.signal })
  436. expect(response.ok).to.be.true
  437. controller.abort()
  438. await expectNoSockets()
  439. })
  440. })
  441. if (
  442. (backendSettings.backend === 's3' && !backendSettings.fallback) ||
  443. (backendSettings.backend === 'gcs' &&
  444. backendSettings.fallback?.backend === 's3')
  445. ) {
  446. describe('with a file in a specific bucket', function () {
  447. let constantFileContent, fileId, fileUrl, bucketName
  448. beforeEach(async function () {
  449. constantFileContent = `This is a file in a different S3 bucket ${Math.random()}`
  450. fileId = new ObjectId().toString()
  451. bucketName = `random-bucket-${new ObjectId().toString()}`
  452. fileUrl = `${filestoreUrl}/bucket/${bucketName}/key/${fileId}`
  453. const cfg = s3Config()
  454. const s3ClientSettings = {
  455. credentials: {
  456. accessKeyId: process.env.MINIO_ROOT_USER,
  457. secretAccessKey: process.env.MINIO_ROOT_PASSWORD,
  458. },
  459. endpoint: cfg.endpoint,
  460. httpOptions: cfg.httpOptions,
  461. s3ForcePathStyle: cfg.pathStyle,
  462. }
  463. const s3 = new S3(s3ClientSettings)
  464. await s3
  465. .createBucket({
  466. Bucket: bucketName,
  467. })
  468. .promise()
  469. await s3
  470. .upload({
  471. Bucket: bucketName,
  472. Key: fileId,
  473. Body: constantFileContent,
  474. })
  475. .promise()
  476. })
  477. it('should get the file from the specified bucket', async function () {
  478. const response = await fetch(fileUrl)
  479. const body = await response.text()
  480. expect(body).to.equal(constantFileContent)
  481. })
  482. })
  483. }
  484. if (backendSettings.backend === 'gcs') {
  485. describe('when deleting a file in GCS', function () {
  486. let fileId, fileUrl, content, error, dateBefore, dateAfter
  487. beforeEach(async function () {
  488. fileId = new ObjectId()
  489. fileUrl = `${filestoreUrl}/project/${projectId}/file/${fileId}`
  490. content = '_wombat_' + Math.random()
  491. const readStream = streamifier.createReadStream(content)
  492. let res = await fetch(fileUrl, { method: 'POST', body: readStream })
  493. if (!res.ok) throw new Error(res.statusText)
  494. dateBefore = new Date()
  495. res = await fetch(fileUrl, { method: 'DELETE' })
  496. dateAfter = new Date()
  497. if (!res.ok) throw new Error(res.statusText)
  498. })
  499. it('should not throw an error', function () {
  500. expect(error).not.to.exist
  501. })
  502. it('should copy the file to the deleted-files bucket', async function () {
  503. let date = dateBefore
  504. const keys = []
  505. while (date <= dateAfter) {
  506. keys.push(`${projectId}/${fileId}-${date.toISOString()}`)
  507. date = new Date(date.getTime() + 1)
  508. }
  509. await TestHelper.expectPersistorToHaveSomeFile(
  510. app.persistor,
  511. `${Settings.filestore.stores.user_files}-deleted`,
  512. keys,
  513. content
  514. )
  515. })
  516. it('should remove the file from the original bucket', async function () {
  517. await TestHelper.expectPersistorNotToHaveFile(
  518. app.persistor,
  519. Settings.filestore.stores.user_files,
  520. `${projectId}/${fileId}`
  521. )
  522. })
  523. })
  524. }
  525. if (backendSettings.fallback) {
  526. describe('with a fallback', function () {
  527. let constantFileContent,
  528. fileId,
  529. fileKey,
  530. fileUrl,
  531. bucket,
  532. fallbackBucket
  533. beforeEach(function () {
  534. constantFileContent = `This is yet more file content ${Math.random()}`
  535. fileId = new ObjectId().toString()
  536. fileKey = `${projectId}/${fileId}`
  537. fileUrl = `${filestoreUrl}/project/${projectId}/file/${fileId}`
  538. bucket = Settings.filestore.stores.user_files
  539. fallbackBucket = Settings.filestore.fallback.buckets[bucket]
  540. })
  541. describe('with a file in the fallback bucket', function () {
  542. beforeEach(async function () {
  543. await TestHelper.uploadStringToPersistor(
  544. app.persistor.fallbackPersistor,
  545. fallbackBucket,
  546. fileKey,
  547. constantFileContent
  548. )
  549. })
  550. it('should not find file in the primary', async function () {
  551. await TestHelper.expectPersistorNotToHaveFile(
  552. app.persistor.primaryPersistor,
  553. bucket,
  554. fileKey
  555. )
  556. })
  557. it('should find the file in the fallback', async function () {
  558. await TestHelper.expectPersistorToHaveFile(
  559. app.persistor.fallbackPersistor,
  560. fallbackBucket,
  561. fileKey,
  562. constantFileContent
  563. )
  564. })
  565. describe('when copyOnMiss is disabled', function () {
  566. beforeEach(function () {
  567. app.persistor.settings.copyOnMiss = false
  568. })
  569. it('should fetch the file', async function () {
  570. const res = await fetch(fileUrl)
  571. const body = await res.text()
  572. expect(body).to.equal(constantFileContent)
  573. })
  574. it('should not copy the file to the primary', async function () {
  575. const response = await fetch(fileUrl)
  576. expect(response.ok).to.be.true
  577. await response.text()
  578. await TestHelper.expectPersistorNotToHaveFile(
  579. app.persistor.primaryPersistor,
  580. bucket,
  581. fileKey
  582. )
  583. })
  584. })
  585. describe('when copyOnMiss is enabled', function () {
  586. beforeEach(function () {
  587. app.persistor.settings.copyOnMiss = true
  588. })
  589. it('should fetch the file', async function () {
  590. const res = await fetch(fileUrl)
  591. const body = await res.text()
  592. expect(body).to.equal(constantFileContent)
  593. })
  594. it('copies the file to the primary', async function () {
  595. const response = await fetch(fileUrl)
  596. expect(response.ok).to.be.true
  597. await response.text()
  598. // wait for the file to copy in the background
  599. await msleep(1000)
  600. await TestHelper.expectPersistorToHaveFile(
  601. app.persistor.primaryPersistor,
  602. bucket,
  603. fileKey,
  604. constantFileContent
  605. )
  606. })
  607. })
  608. describe('when copying a file', function () {
  609. let newFileId, newFileUrl, newFileKey, opts
  610. beforeEach(function () {
  611. const newProjectID = new ObjectId().toString()
  612. newFileId = new ObjectId().toString()
  613. newFileUrl = `${filestoreUrl}/project/${newProjectID}/file/${newFileId}`
  614. newFileKey = `${newProjectID}/${newFileId}`
  615. opts = {
  616. method: 'put',
  617. body: JSON.stringify({
  618. source: {
  619. project_id: projectId,
  620. file_id: fileId,
  621. },
  622. }),
  623. headers: {
  624. 'Content-Type': 'application/json',
  625. },
  626. }
  627. })
  628. describe('when copyOnMiss is false', function () {
  629. beforeEach(async function () {
  630. app.persistor.settings.copyOnMiss = false
  631. const response = await fetch(newFileUrl, opts)
  632. expect(response.status).to.equal(200)
  633. })
  634. it('should leave the old file in the old bucket', async function () {
  635. await TestHelper.expectPersistorToHaveFile(
  636. app.persistor.fallbackPersistor,
  637. fallbackBucket,
  638. fileKey,
  639. constantFileContent
  640. )
  641. })
  642. it('should not create a new file in the old bucket', async function () {
  643. await TestHelper.expectPersistorNotToHaveFile(
  644. app.persistor.fallbackPersistor,
  645. fallbackBucket,
  646. newFileKey
  647. )
  648. })
  649. it('should create a new file in the new bucket', async function () {
  650. await TestHelper.expectPersistorToHaveFile(
  651. app.persistor.primaryPersistor,
  652. bucket,
  653. newFileKey,
  654. constantFileContent
  655. )
  656. })
  657. it('should not copy the old file to the primary with the old key', async function () {
  658. // wait for the file to copy in the background
  659. await msleep(1000)
  660. await TestHelper.expectPersistorNotToHaveFile(
  661. app.persistor.primaryPersistor,
  662. bucket,
  663. fileKey
  664. )
  665. })
  666. })
  667. describe('when copyOnMiss is true', function () {
  668. beforeEach(async function () {
  669. app.persistor.settings.copyOnMiss = true
  670. const response = await fetch(newFileUrl, opts)
  671. expect(response.status).to.equal(200)
  672. })
  673. it('should leave the old file in the old bucket', async function () {
  674. await TestHelper.expectPersistorToHaveFile(
  675. app.persistor.fallbackPersistor,
  676. fallbackBucket,
  677. fileKey,
  678. constantFileContent
  679. )
  680. })
  681. it('should not create a new file in the old bucket', async function () {
  682. await TestHelper.expectPersistorNotToHaveFile(
  683. app.persistor.fallbackPersistor,
  684. fallbackBucket,
  685. newFileKey
  686. )
  687. })
  688. it('should create a new file in the new bucket', async function () {
  689. await TestHelper.expectPersistorToHaveFile(
  690. app.persistor.primaryPersistor,
  691. bucket,
  692. newFileKey,
  693. constantFileContent
  694. )
  695. })
  696. it('should copy the old file to the primary with the old key', async function () {
  697. // wait for the file to copy in the background
  698. await msleep(1000)
  699. await TestHelper.expectPersistorToHaveFile(
  700. app.persistor.primaryPersistor,
  701. bucket,
  702. fileKey,
  703. constantFileContent
  704. )
  705. })
  706. })
  707. })
  708. })
  709. describe('when sending a file', function () {
  710. beforeEach(async function () {
  711. const readStream =
  712. streamifier.createReadStream(constantFileContent)
  713. const res = await fetch(fileUrl, {
  714. method: 'POST',
  715. body: readStream,
  716. })
  717. if (!res.ok) throw new Error(res.statusText)
  718. })
  719. it('should store the file on the primary', async function () {
  720. await TestHelper.expectPersistorToHaveFile(
  721. app.persistor.primaryPersistor,
  722. bucket,
  723. fileKey,
  724. constantFileContent
  725. )
  726. })
  727. it('should not store the file on the fallback', async function () {
  728. await TestHelper.expectPersistorNotToHaveFile(
  729. app.persistor.fallbackPersistor,
  730. fallbackBucket,
  731. `${projectId}/${fileId}`
  732. )
  733. })
  734. })
  735. describe('when deleting a file', function () {
  736. describe('when the file exists on the primary', function () {
  737. beforeEach(async function () {
  738. await TestHelper.uploadStringToPersistor(
  739. app.persistor.primaryPersistor,
  740. bucket,
  741. fileKey,
  742. constantFileContent
  743. )
  744. })
  745. it('should delete the file', async function () {
  746. const response1 = await fetch(fileUrl, { method: 'DELETE' })
  747. expect(response1.status).to.equal(204)
  748. const response2 = await fetch(fileUrl)
  749. expect(response2.status).to.equal(404)
  750. })
  751. })
  752. describe('when the file exists on the fallback', function () {
  753. beforeEach(async function () {
  754. await TestHelper.uploadStringToPersistor(
  755. app.persistor.fallbackPersistor,
  756. fallbackBucket,
  757. fileKey,
  758. constantFileContent
  759. )
  760. })
  761. it('should delete the file', async function () {
  762. const response1 = await fetch(fileUrl, { method: 'DELETE' })
  763. expect(response1.status).to.equal(204)
  764. const response2 = await fetch(fileUrl)
  765. expect(response2.status).to.equal(404)
  766. })
  767. })
  768. describe('when the file exists on both the primary and the fallback', function () {
  769. beforeEach(async function () {
  770. await TestHelper.uploadStringToPersistor(
  771. app.persistor.primaryPersistor,
  772. bucket,
  773. fileKey,
  774. constantFileContent
  775. )
  776. await TestHelper.uploadStringToPersistor(
  777. app.persistor.fallbackPersistor,
  778. fallbackBucket,
  779. fileKey,
  780. constantFileContent
  781. )
  782. })
  783. it('should delete the files', async function () {
  784. const response1 = await fetch(fileUrl, { method: 'DELETE' })
  785. expect(response1.status).to.equal(204)
  786. const response2 = await fetch(fileUrl)
  787. expect(response2.status).to.equal(404)
  788. })
  789. })
  790. describe('when the file does not exist', function () {
  791. it('should return return 204', async function () {
  792. // S3 doesn't give us a 404 when the object doesn't exist, so to stay
  793. // consistent we merrily return 204 ourselves here as well
  794. const response = await fetch(fileUrl, { method: 'DELETE' })
  795. expect(response.status).to.equal(204)
  796. })
  797. })
  798. })
  799. })
  800. }
  801. describe('with a pdf file', function () {
  802. let fileId, fileUrl, localFileSize
  803. const localFileReadPath = Path.resolve(
  804. __dirname,
  805. '../../fixtures/test.pdf'
  806. )
  807. beforeEach(async function () {
  808. fileId = new ObjectId().toString()
  809. fileUrl = `${filestoreUrl}/project/${projectId}/file/${fileId}`
  810. const stat = await fsStat(localFileReadPath)
  811. localFileSize = stat.size
  812. const readStream = fs.createReadStream(localFileReadPath)
  813. const res = await fetch(fileUrl, { method: 'POST', body: readStream })
  814. if (!res.ok) throw new Error(res.statusText)
  815. })
  816. it('should be able get the file back', async function () {
  817. const response = await fetch(fileUrl)
  818. const body = await response.text()
  819. expect(body.substring(0, 8)).to.equal('%PDF-1.5')
  820. })
  821. if (backendSettings.backend !== 'fs') {
  822. it('should record an egress metric for the upload', async function () {
  823. const metric = await TestHelper.getMetric(
  824. filestoreUrl,
  825. `${metricPrefix}_egress`
  826. )
  827. expect(metric - previousEgress).to.equal(localFileSize)
  828. })
  829. }
  830. describe('getting the preview image', function () {
  831. this.timeout(1000 * 20)
  832. let previewFileUrl
  833. beforeEach(function () {
  834. previewFileUrl = `${fileUrl}?style=preview`
  835. })
  836. it('should not time out', async function () {
  837. const response = await fetch(previewFileUrl)
  838. expect(response.status).to.equal(200)
  839. await response.arrayBuffer()
  840. })
  841. it('should respond with image data', async function () {
  842. // note: this test relies of the imagemagick conversion working
  843. const response = await fetch(previewFileUrl)
  844. const body = await response.text()
  845. expect(body.length).to.be.greaterThan(400)
  846. expect(body.substr(1, 3)).to.equal('PNG')
  847. })
  848. })
  849. describe('warming the cache', function () {
  850. this.timeout(1000 * 20)
  851. let previewFileUrl
  852. beforeEach(function () {
  853. previewFileUrl = `${fileUrl}?style=preview&cacheWarm=true`
  854. })
  855. it('should not time out', async function () {
  856. const response = await fetch(previewFileUrl)
  857. expect(response.status).to.equal(200)
  858. await response.arrayBuffer()
  859. })
  860. it('should not leak sockets', async function () {
  861. const response1 = await fetch(previewFileUrl)
  862. expect(response1.status).to.equal(200)
  863. // do not read the response body, should be destroyed immediately
  864. const response2 = await fetch(previewFileUrl)
  865. expect(response2.status).to.equal(200)
  866. // do not read the response body, should be destroyed immediately
  867. await expectNoSockets()
  868. })
  869. it("should respond with only an 'OK'", async function () {
  870. // note: this test relies of the imagemagick conversion working
  871. const response = await fetch(previewFileUrl)
  872. const body = await response.text()
  873. expect(body).to.equal('OK')
  874. })
  875. })
  876. })
  877. })
  878. }
  879. })