FilestoreTests.js 33 KB

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