FilestoreTests.js 32 KB

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