FilestoreTests.js 31 KB

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