FilestoreTests.js 33 KB

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