FilestoreTests.js 32 KB

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