FilestoreTests.js 54 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531
  1. import chai from 'chai'
  2. import fs from 'node:fs'
  3. import Stream from 'node:stream'
  4. import Settings from '@overleaf/settings'
  5. import Path from 'node:path'
  6. import FilestoreApp from './FilestoreApp.js'
  7. import TestHelper from './TestHelper.js'
  8. import fetch from 'node-fetch'
  9. import { promisify } from 'node:util'
  10. import { Storage } from '@google-cloud/storage'
  11. import streamifier from 'streamifier'
  12. import { ObjectId } from 'mongodb'
  13. import { ListObjectsV2Command } from '@aws-sdk/client-s3'
  14. import ChildProcess from 'node:child_process'
  15. import chaiAsPromised from 'chai-as-promised'
  16. // store settings for multiple backends, so that we can test each one.
  17. // fs will always be available - add others if they are configured
  18. import TestConfig from './TestConfig.js'
  19. import {
  20. AlreadyWrittenError,
  21. NotFoundError,
  22. NotImplementedError,
  23. NoKEKMatchedError,
  24. } from '@overleaf/object-persistor/src/Errors.js'
  25. import {
  26. PerProjectEncryptedS3Persistor,
  27. RootKeyEncryptionKey,
  28. } from '@overleaf/object-persistor/src/PerProjectEncryptedS3Persistor.js'
  29. import { S3Persistor } from '@overleaf/object-persistor/src/S3Persistor.js'
  30. import crypto from 'node:crypto'
  31. import { WritableBuffer } from '@overleaf/stream-utils'
  32. import { gzipSync } from 'node:zlib'
  33. const { expect } = chai
  34. chai.use(chaiAsPromised)
  35. const {
  36. BackendSettings,
  37. s3Config,
  38. s3SSECConfig,
  39. AWS_S3_USER_FILES_STORAGE_CLASS,
  40. } = TestConfig
  41. const fsWriteFile = promisify(fs.writeFile)
  42. const fsStat = promisify(fs.stat)
  43. const exec = promisify(ChildProcess.exec)
  44. const msleep = promisify(setTimeout)
  45. if (!process.env.AWS_ACCESS_KEY_ID) {
  46. throw new Error('please provide credentials for the AWS S3 test server')
  47. }
  48. process.on('unhandledRejection', e => {
  49. // eslint-disable-next-line no-console
  50. console.log('** Unhandled Promise Rejection **\n', e)
  51. throw e
  52. })
  53. describe('Filestore', function () {
  54. this.timeout(1000 * 10)
  55. const filestoreUrl = `http://127.0.0.1:${Settings.internal.filestore.port}`
  56. const seenSockets = []
  57. async function expectNoSockets() {
  58. try {
  59. await msleep(1000)
  60. const { stdout } = await exec('ss -tn')
  61. const lines = stdout.split('\n')
  62. const header = lines.shift()
  63. const badSockets = []
  64. for (const socket of lines) {
  65. const fields = socket.split(' ').filter(part => part !== '')
  66. if (
  67. fields.length > 2 &&
  68. parseInt(fields[1]) &&
  69. !seenSockets.includes(socket)
  70. ) {
  71. badSockets.push(socket)
  72. seenSockets.push(socket)
  73. }
  74. }
  75. if (badSockets.length) {
  76. // eslint-disable-next-line no-console
  77. console.error(
  78. 'ERR: Sockets still have receive buffer after connection closed'
  79. )
  80. console.error(header)
  81. for (const socket of badSockets) {
  82. // eslint-disable-next-line no-console
  83. console.error(socket)
  84. }
  85. throw new Error('Sockets still open after connection closed')
  86. }
  87. } catch (err) {
  88. expect(err).not.to.exist
  89. }
  90. }
  91. // redefine the test suite for every available backend
  92. for (const [backendVariantWithShardNumber, backendSettings] of Object.entries(
  93. BackendSettings
  94. )) {
  95. describe(backendVariantWithShardNumber, function () {
  96. let app,
  97. previousEgress,
  98. previousIngress,
  99. metricPrefix,
  100. templateId,
  101. otherProjectId,
  102. templateUrl,
  103. fileId,
  104. fileKey,
  105. fileUrl
  106. const dataEncryptionKeySize =
  107. backendSettings.backend === 's3SSEC' ? 32 : 0
  108. const BUCKET_NAMES = [
  109. process.env.GCS_TEMPLATE_FILES_BUCKET_NAME,
  110. `${process.env.GCS_TEMPLATE_FILES_BUCKET_NAME}-deleted`,
  111. ]
  112. before('start filestore with new settings', async function () {
  113. // create the app with the relevant filestore settings
  114. Settings.filestore = backendSettings
  115. app = new FilestoreApp()
  116. await app.runServer()
  117. })
  118. if (backendSettings.gcs) {
  119. before('create gcs buckets', async function () {
  120. // create test buckets for gcs
  121. const storage = new Storage(Settings.filestore.gcs.endpoint)
  122. for (const bucketName of BUCKET_NAMES) {
  123. await storage.createBucket(bucketName)
  124. }
  125. })
  126. after('delete gcs buckets', async function () {
  127. // tear down all the gcs buckets
  128. const storage = new Storage(Settings.filestore.gcs.endpoint)
  129. for (const bucketName of BUCKET_NAMES) {
  130. const bucket = storage.bucket(bucketName)
  131. await bucket.deleteFiles()
  132. await bucket.delete()
  133. }
  134. })
  135. }
  136. after('stop filestore app', async function () {
  137. await app.stop()
  138. })
  139. beforeEach('fetch previous egress metric', async function () {
  140. // retrieve previous metrics from the app
  141. if (['s3', 's3SSEC', 'gcs'].includes(Settings.filestore.backend)) {
  142. metricPrefix = Settings.filestore.backend.replace('SSEC', '')
  143. previousEgress = await TestHelper.getMetric(
  144. filestoreUrl,
  145. `${metricPrefix}_egress`
  146. )
  147. }
  148. templateId = new ObjectId().toString()
  149. otherProjectId = new ObjectId().toString()
  150. templateUrl = `${filestoreUrl}/template/${templateId}/v/0`
  151. fileId = new ObjectId().toString()
  152. fileUrl = `${templateUrl}/${fileId}`
  153. fileKey = `${templateId}/v/0/${fileId}`
  154. })
  155. it('should send a 200 for the status endpoint', async function () {
  156. const response = await fetch(`${filestoreUrl}/status`)
  157. expect(response.status).to.equal(200)
  158. const body = await response.text()
  159. expect(body).to.contain('filestore')
  160. expect(body).to.contain('up')
  161. })
  162. describe('with a file on the server', function () {
  163. let constantFileContent
  164. const localFileReadPath =
  165. '/tmp/filestore_acceptance_tests_file_read.txt'
  166. beforeEach('upload file', async function () {
  167. constantFileContent = [
  168. 'hello world',
  169. `line 2 goes here ${Math.random()}`,
  170. 'there are 3 lines in all',
  171. ].join('\n')
  172. await fsWriteFile(localFileReadPath, constantFileContent)
  173. const readStream = fs.createReadStream(localFileReadPath)
  174. const res = await fetch(fileUrl, { method: 'POST', body: readStream })
  175. if (!res.ok) throw new Error(res.statusText)
  176. })
  177. beforeEach('retrieve previous ingress metric', async function () {
  178. // The upload request can bump the ingress metric.
  179. // The content hash validation might require a full download
  180. // in case the ETag field of the upload response is not a md5 sum.
  181. if (['s3', 's3SSEC', 'gcs'].includes(Settings.filestore.backend)) {
  182. previousIngress = await TestHelper.getMetric(
  183. filestoreUrl,
  184. `${metricPrefix}_ingress`
  185. )
  186. }
  187. })
  188. it('should return 404 for a non-existant id', async function () {
  189. const url = fileUrl + '___this_is_clearly_wrong___'
  190. const response = await fetch(url)
  191. expect(response.status).to.equal(404)
  192. })
  193. it('should return the file size on a HEAD request', async function () {
  194. const expectedLength = Buffer.byteLength(constantFileContent)
  195. const res = await fetch(fileUrl, { method: 'HEAD' })
  196. expect(res.status).to.equal(200)
  197. expect(res.headers.get('Content-Length')).to.equal(
  198. expectedLength.toString()
  199. )
  200. })
  201. it('should be able get the file back', async function () {
  202. const res = await fetch(fileUrl)
  203. const body = await res.text()
  204. expect(body).to.equal(constantFileContent)
  205. })
  206. it('should send a 200 for the health-check endpoint using the file', async function () {
  207. const response = await fetch(`${filestoreUrl}/health_check`)
  208. expect(response.status).to.equal(200)
  209. const body = await response.text()
  210. expect(body).to.equal('OK')
  211. })
  212. it('should not leak a socket', async function () {
  213. const res = await fetch(fileUrl)
  214. if (!res.ok) throw new Error(res.statusText)
  215. await res.text()
  216. await expectNoSockets()
  217. })
  218. it('should be able to get back the first 9 bytes of the file', async function () {
  219. const res = await fetch(fileUrl, { headers: { Range: 'bytes=0-8' } })
  220. const body = await res.text()
  221. expect(body).to.equal('hello wor')
  222. })
  223. it('should be able to get back bytes 4 through 10 of the file', async function () {
  224. const res = await fetch(fileUrl, { headers: { Range: 'bytes=4-10' } })
  225. const body = await res.text()
  226. expect(body).to.equal('o world')
  227. })
  228. it('should be able to delete the file', async function () {
  229. await app.persistor.deleteObject(
  230. Settings.filestore.stores.template_files,
  231. fileKey
  232. )
  233. const response2 = await fetch(fileUrl)
  234. expect(response2.status).to.equal(404)
  235. })
  236. it('should be able to copy files', async function () {
  237. const newProjectID = new ObjectId().toString()
  238. const newFileId = new ObjectId().toString()
  239. const newFileUrl = `${filestoreUrl}/template/${newProjectID}/v/0/${newFileId}`
  240. const newFileKey = `${newProjectID}/v/0/${newFileId}`
  241. await app.persistor.copyObject(
  242. Settings.filestore.stores.template_files,
  243. fileKey,
  244. newFileKey
  245. )
  246. await app.persistor.deleteObject(
  247. Settings.filestore.stores.template_files,
  248. fileKey
  249. )
  250. const response = await fetch(newFileUrl)
  251. const body = await response.text()
  252. expect(body).to.equal(constantFileContent)
  253. })
  254. it('should be able to overwrite the file', async function () {
  255. const newContent = `here is some different content, ${Math.random()}`
  256. const readStream = streamifier.createReadStream(newContent)
  257. await fetch(fileUrl, { method: 'POST', body: readStream })
  258. const response = await fetch(fileUrl)
  259. const body = await response.text()
  260. expect(body).to.equal(newContent)
  261. })
  262. describe('IfNoneMatch', function () {
  263. if (backendSettings.backend === 'fs') {
  264. it('should refuse to handle IfNoneMatch', async function () {
  265. await expect(
  266. app.persistor.sendStream(
  267. Settings.filestore.stores.template_files,
  268. fileKey,
  269. fs.createReadStream(localFileReadPath),
  270. { ifNoneMatch: '*' }
  271. )
  272. ).to.be.rejectedWith(NotImplementedError)
  273. })
  274. } else {
  275. it('should reject sendStream on the same key with IfNoneMatch', async function () {
  276. await expect(
  277. app.persistor.sendStream(
  278. Settings.filestore.stores.template_files,
  279. fileKey,
  280. fs.createReadStream(localFileReadPath),
  281. { ifNoneMatch: '*' }
  282. )
  283. ).to.be.rejectedWith(AlreadyWrittenError)
  284. })
  285. it('should allow sendStream on a different key with IfNoneMatch', async function () {
  286. await app.persistor.sendStream(
  287. Settings.filestore.stores.template_files,
  288. `${templateId}/v/0/${fileId}-other`,
  289. fs.createReadStream(localFileReadPath),
  290. { ifNoneMatch: '*' }
  291. )
  292. })
  293. }
  294. })
  295. if (backendSettings.backend !== 'fs') {
  296. it('should record an egress metric for the upload', async function () {
  297. const metric = await TestHelper.getMetric(
  298. filestoreUrl,
  299. `${metricPrefix}_egress`
  300. )
  301. expect(metric - previousEgress).to.equal(
  302. constantFileContent.length + dataEncryptionKeySize
  303. )
  304. })
  305. it('should record an ingress metric when downloading the file', async function () {
  306. const response = await fetch(fileUrl)
  307. expect(response.ok).to.be.true
  308. await response.text()
  309. const metric = await TestHelper.getMetric(
  310. filestoreUrl,
  311. `${metricPrefix}_ingress`
  312. )
  313. expect(metric - previousIngress).to.equal(
  314. constantFileContent.length + dataEncryptionKeySize
  315. )
  316. })
  317. it('should record an ingress metric for a partial download', async function () {
  318. const response = await fetch(fileUrl, {
  319. headers: { Range: 'bytes=0-8' },
  320. })
  321. expect(response.ok).to.be.true
  322. await response.text()
  323. const metric = await TestHelper.getMetric(
  324. filestoreUrl,
  325. `${metricPrefix}_ingress`
  326. )
  327. expect(metric - previousIngress).to.equal(9 + dataEncryptionKeySize)
  328. })
  329. }
  330. })
  331. describe('with multiple files', function () {
  332. let fileIds, fileUrls, otherFileUrls, otherProjectUrl
  333. const localFileReadPaths = [
  334. '/tmp/filestore_acceptance_tests_file_read_1.txt',
  335. '/tmp/filestore_acceptance_tests_file_read_2.txt',
  336. '/tmp/filestore_acceptance_tests_file_read_3.txt',
  337. ]
  338. const constantFileContents = [
  339. [
  340. 'hello world',
  341. `line 2 goes here ${Math.random()}`,
  342. 'there are 3 lines in all',
  343. ].join('\n'),
  344. [
  345. `for reference: ${Math.random()}`,
  346. 'cats are the best animals',
  347. 'wombats are a close second',
  348. ].join('\n'),
  349. [
  350. `another file: ${Math.random()}`,
  351. 'with multiple lines',
  352. 'the end',
  353. ].join('\n'),
  354. ]
  355. before('create local files', async function () {
  356. return await Promise.all([
  357. fsWriteFile(localFileReadPaths[0], constantFileContents[0]),
  358. fsWriteFile(localFileReadPaths[1], constantFileContents[1]),
  359. fsWriteFile(localFileReadPaths[2], constantFileContents[2]),
  360. ])
  361. })
  362. beforeEach('upload two files', async function () {
  363. otherProjectUrl = `${filestoreUrl}/template/${otherProjectId}/v/0`
  364. fileIds = [
  365. new ObjectId().toString(),
  366. new ObjectId().toString(),
  367. new ObjectId().toString(),
  368. ]
  369. fileUrls = [
  370. `${templateUrl}/${fileIds[0]}`,
  371. `${templateUrl}/${fileIds[1]}`,
  372. ]
  373. otherFileUrls = [`${otherProjectUrl}/${fileIds[2]}`]
  374. await Promise.all([
  375. fetch(fileUrls[0], {
  376. method: 'POST',
  377. body: fs.createReadStream(localFileReadPaths[0]),
  378. }),
  379. fetch(fileUrls[1], {
  380. method: 'POST',
  381. body: fs.createReadStream(localFileReadPaths[1]),
  382. }),
  383. fetch(otherFileUrls[0], {
  384. method: 'POST',
  385. body: fs.createReadStream(localFileReadPaths[2]),
  386. }),
  387. ])
  388. })
  389. it('should get the directory size', async function () {
  390. expect(
  391. await app.persistor.directorySize(
  392. Settings.filestore.stores.template_files,
  393. templateId
  394. )
  395. ).to.equal(
  396. constantFileContents[0].length + constantFileContents[1].length
  397. )
  398. })
  399. it('should store the files', async function () {
  400. for (const index in fileUrls) {
  401. const response = await fetch(fileUrls[index])
  402. const body = await response.text()
  403. expect(body).to.equal(constantFileContents[index])
  404. }
  405. })
  406. it('should be able to delete a folder', async function () {
  407. await app.persistor.deleteDirectory(
  408. Settings.filestore.stores.template_files,
  409. templateId + '/'
  410. )
  411. for (const index in fileUrls) {
  412. const response = await fetch(fileUrls[index])
  413. expect(response.status).to.equal(404)
  414. }
  415. })
  416. it('should not delete files in other projects', async function () {
  417. for (const index in otherFileUrls) {
  418. const response = await fetch(otherFileUrls[index])
  419. expect(response.status).to.equal(200)
  420. }
  421. })
  422. })
  423. describe('with a large file', function () {
  424. this.timeout(1000 * 20)
  425. let largeFileContent
  426. beforeEach('upload large file', async function () {
  427. largeFileContent = '_wombat_'.repeat(1024 * 1024) // 8 megabytes
  428. largeFileContent += Math.random()
  429. const readStream = streamifier.createReadStream(largeFileContent)
  430. const res = await fetch(fileUrl, { method: 'POST', body: readStream })
  431. if (!res.ok) throw new Error(res.statusText)
  432. })
  433. it('should be able to get the file back', async function () {
  434. const response = await fetch(fileUrl)
  435. const body = await response.text()
  436. expect(body).to.equal(largeFileContent)
  437. })
  438. it('should not leak a socket', async function () {
  439. const response = await fetch(fileUrl)
  440. await response.text()
  441. await expectNoSockets()
  442. })
  443. it('should not leak a socket if the connection is aborted', async function () {
  444. const controller = new AbortController()
  445. const response = await fetch(fileUrl, { signal: controller.signal })
  446. expect(response.ok).to.be.true
  447. controller.abort()
  448. await expectNoSockets()
  449. })
  450. })
  451. if (
  452. (backendSettings.backend === 's3' && !backendSettings.fallback) ||
  453. (backendSettings.backend === 'gcs' &&
  454. backendSettings.fallback?.backend === 's3')
  455. ) {
  456. describe('with a file in a specific bucket', function () {
  457. let constantFileContent, fileId, fileUrl, bucketName
  458. beforeEach('upload file into random bucket', async function () {
  459. constantFileContent = `This is a file in a different S3 bucket ${Math.random()}`
  460. fileId = new ObjectId().toString()
  461. bucketName = `random-bucket-${new ObjectId().toString()}`
  462. fileUrl = `${filestoreUrl}/bucket/${bucketName}/key/${fileId}`
  463. const s3 = new S3Persistor({
  464. ...s3Config(),
  465. key: process.env.MINIO_ROOT_USER,
  466. secret: process.env.MINIO_ROOT_PASSWORD,
  467. })
  468. await s3._createBucket(bucketName)
  469. await s3._upload(bucketName, {
  470. Bucket: bucketName,
  471. Key: fileId,
  472. Body: constantFileContent,
  473. })
  474. })
  475. it('should get the file from the specified bucket', async function () {
  476. const response = await fetch(fileUrl)
  477. const body = await response.text()
  478. expect(body).to.equal(constantFileContent)
  479. })
  480. })
  481. }
  482. if (backendSettings.backend === 'gcs') {
  483. describe('when deleting a file in GCS', function () {
  484. let content, error, dateBefore, dateAfter
  485. beforeEach('upload and delete file', async function () {
  486. content = '_wombat_' + Math.random()
  487. const readStream = streamifier.createReadStream(content)
  488. const res = await fetch(fileUrl, {
  489. method: 'POST',
  490. body: readStream,
  491. })
  492. if (!res.ok) throw new Error(res.statusText)
  493. dateBefore = new Date()
  494. await app.persistor.deleteObject(
  495. Settings.filestore.stores.template_files,
  496. fileKey
  497. )
  498. dateAfter = new Date()
  499. })
  500. it('should not throw an error', function () {
  501. expect(error).not.to.exist
  502. })
  503. it('should copy the file to the deleted-files bucket', async function () {
  504. let date = dateBefore
  505. const keys = []
  506. while (date <= dateAfter) {
  507. keys.push(`${templateId}/v/0/${fileId}-${date.toISOString()}`)
  508. date = new Date(date.getTime() + 1)
  509. }
  510. await TestHelper.expectPersistorToHaveSomeFile(
  511. app.persistor,
  512. `${Settings.filestore.stores.template_files}-deleted`,
  513. keys,
  514. content
  515. )
  516. })
  517. it('should remove the file from the original bucket', async function () {
  518. await TestHelper.expectPersistorNotToHaveFile(
  519. app.persistor,
  520. Settings.filestore.stores.template_files,
  521. fileKey
  522. )
  523. })
  524. })
  525. }
  526. if (backendSettings.fallback) {
  527. describe('with a fallback', function () {
  528. let constantFileContent, bucket, fallbackBucket
  529. beforeEach('prepare fallback', function () {
  530. constantFileContent = `This is yet more file content ${Math.random()}`
  531. bucket = Settings.filestore.stores.template_files
  532. fallbackBucket = Settings.filestore.fallback.buckets[bucket]
  533. })
  534. describe('with a file in the fallback bucket', function () {
  535. beforeEach('upload into fallback', async function () {
  536. await TestHelper.uploadStringToPersistor(
  537. app.persistor.fallbackPersistor,
  538. fallbackBucket,
  539. fileKey,
  540. constantFileContent
  541. )
  542. })
  543. it('should not find file in the primary', async function () {
  544. await TestHelper.expectPersistorNotToHaveFile(
  545. app.persistor.primaryPersistor,
  546. bucket,
  547. fileKey
  548. )
  549. })
  550. it('should find the file in the fallback', async function () {
  551. await TestHelper.expectPersistorToHaveFile(
  552. app.persistor.fallbackPersistor,
  553. fallbackBucket,
  554. fileKey,
  555. constantFileContent
  556. )
  557. })
  558. describe('when copyOnMiss is disabled', function () {
  559. beforeEach('swap copyOnMiss=false', function () {
  560. app.persistor.settings.copyOnMiss = false
  561. })
  562. it('should fetch the file', async function () {
  563. const res = await fetch(fileUrl)
  564. const body = await res.text()
  565. expect(body).to.equal(constantFileContent)
  566. })
  567. it('should not copy the file to the primary', async function () {
  568. const response = await fetch(fileUrl)
  569. expect(response.ok).to.be.true
  570. await response.text()
  571. await TestHelper.expectPersistorNotToHaveFile(
  572. app.persistor.primaryPersistor,
  573. bucket,
  574. fileKey
  575. )
  576. })
  577. })
  578. describe('when copyOnMiss is enabled', function () {
  579. beforeEach('swap copyOnMiss=true', function () {
  580. app.persistor.settings.copyOnMiss = true
  581. })
  582. it('should fetch the file', async function () {
  583. const res = await fetch(fileUrl)
  584. const body = await res.text()
  585. expect(body).to.equal(constantFileContent)
  586. })
  587. it('copies the file to the primary', async function () {
  588. const response = await fetch(fileUrl)
  589. expect(response.ok).to.be.true
  590. await response.text()
  591. // wait for the file to copy in the background
  592. await msleep(1000)
  593. await TestHelper.expectPersistorToHaveFile(
  594. app.persistor.primaryPersistor,
  595. bucket,
  596. fileKey,
  597. constantFileContent
  598. )
  599. })
  600. })
  601. describe('when copying a file', function () {
  602. let newFileKey
  603. beforeEach('prepare to copy file', function () {
  604. const newProjectID = new ObjectId().toString()
  605. const newFileId = new ObjectId().toString()
  606. newFileKey = `${newProjectID}/v/0/${newFileId}`
  607. })
  608. describe('when copyOnMiss is false', function () {
  609. beforeEach('copy with copyOnMiss=false', async function () {
  610. app.persistor.settings.copyOnMiss = false
  611. await app.persistor.copyObject(
  612. Settings.filestore.stores.template_files,
  613. fileKey,
  614. newFileKey
  615. )
  616. })
  617. it('should leave the old file in the old bucket', async function () {
  618. await TestHelper.expectPersistorToHaveFile(
  619. app.persistor.fallbackPersistor,
  620. fallbackBucket,
  621. fileKey,
  622. constantFileContent
  623. )
  624. })
  625. it('should not create a new file in the old bucket', async function () {
  626. await TestHelper.expectPersistorNotToHaveFile(
  627. app.persistor.fallbackPersistor,
  628. fallbackBucket,
  629. newFileKey
  630. )
  631. })
  632. it('should create a new file in the new bucket', async function () {
  633. await TestHelper.expectPersistorToHaveFile(
  634. app.persistor.primaryPersistor,
  635. bucket,
  636. newFileKey,
  637. constantFileContent
  638. )
  639. })
  640. it('should not copy the old file to the primary with the old key', async function () {
  641. // wait for the file to copy in the background
  642. await msleep(1000)
  643. await TestHelper.expectPersistorNotToHaveFile(
  644. app.persistor.primaryPersistor,
  645. bucket,
  646. fileKey
  647. )
  648. })
  649. })
  650. describe('when copyOnMiss is true', function () {
  651. beforeEach('copy with copyOnMiss=false', async function () {
  652. app.persistor.settings.copyOnMiss = true
  653. await app.persistor.copyObject(
  654. Settings.filestore.stores.template_files,
  655. fileKey,
  656. newFileKey
  657. )
  658. })
  659. it('should leave the old file in the old bucket', async function () {
  660. await TestHelper.expectPersistorToHaveFile(
  661. app.persistor.fallbackPersistor,
  662. fallbackBucket,
  663. fileKey,
  664. constantFileContent
  665. )
  666. })
  667. it('should not create a new file in the old bucket', async function () {
  668. await TestHelper.expectPersistorNotToHaveFile(
  669. app.persistor.fallbackPersistor,
  670. fallbackBucket,
  671. newFileKey
  672. )
  673. })
  674. it('should create a new file in the new bucket', async function () {
  675. await TestHelper.expectPersistorToHaveFile(
  676. app.persistor.primaryPersistor,
  677. bucket,
  678. newFileKey,
  679. constantFileContent
  680. )
  681. })
  682. it('should copy the old file to the primary with the old key', async function () {
  683. // wait for the file to copy in the background
  684. await msleep(1000)
  685. await TestHelper.expectPersistorToHaveFile(
  686. app.persistor.primaryPersistor,
  687. bucket,
  688. fileKey,
  689. constantFileContent
  690. )
  691. })
  692. })
  693. })
  694. })
  695. describe('when sending a file', function () {
  696. beforeEach('upload file', async function () {
  697. const readStream =
  698. streamifier.createReadStream(constantFileContent)
  699. const res = await fetch(fileUrl, {
  700. method: 'POST',
  701. body: readStream,
  702. })
  703. if (!res.ok) throw new Error(res.statusText)
  704. })
  705. it('should store the file on the primary', async function () {
  706. await TestHelper.expectPersistorToHaveFile(
  707. app.persistor.primaryPersistor,
  708. bucket,
  709. fileKey,
  710. constantFileContent
  711. )
  712. })
  713. it('should not store the file on the fallback', async function () {
  714. await TestHelper.expectPersistorNotToHaveFile(
  715. app.persistor.fallbackPersistor,
  716. fallbackBucket,
  717. fileKey
  718. )
  719. })
  720. })
  721. describe('when deleting a file', function () {
  722. describe('when the file exists on the primary', function () {
  723. beforeEach('upload into primary', async function () {
  724. await TestHelper.uploadStringToPersistor(
  725. app.persistor.primaryPersistor,
  726. bucket,
  727. fileKey,
  728. constantFileContent
  729. )
  730. })
  731. it('should delete the file', async function () {
  732. await app.persistor.deleteObject(
  733. Settings.filestore.stores.template_files,
  734. fileKey
  735. )
  736. const response2 = await fetch(fileUrl)
  737. expect(response2.status).to.equal(404)
  738. })
  739. })
  740. describe('when the file exists on the fallback', function () {
  741. beforeEach('upload into fallback', async function () {
  742. await TestHelper.uploadStringToPersistor(
  743. app.persistor.fallbackPersistor,
  744. fallbackBucket,
  745. fileKey,
  746. constantFileContent
  747. )
  748. })
  749. it('should delete the file', async function () {
  750. await app.persistor.deleteObject(
  751. Settings.filestore.stores.template_files,
  752. fileKey
  753. )
  754. const response2 = await fetch(fileUrl)
  755. expect(response2.status).to.equal(404)
  756. })
  757. })
  758. describe('when the file exists on both the primary and the fallback', function () {
  759. beforeEach(
  760. 'upload into both primary and fallback',
  761. async function () {
  762. await TestHelper.uploadStringToPersistor(
  763. app.persistor.primaryPersistor,
  764. bucket,
  765. fileKey,
  766. constantFileContent
  767. )
  768. await TestHelper.uploadStringToPersistor(
  769. app.persistor.fallbackPersistor,
  770. fallbackBucket,
  771. fileKey,
  772. constantFileContent
  773. )
  774. }
  775. )
  776. it('should delete the files', async function () {
  777. await app.persistor.deleteObject(
  778. Settings.filestore.stores.template_files,
  779. fileKey
  780. )
  781. const response2 = await fetch(fileUrl)
  782. expect(response2.status).to.equal(404)
  783. })
  784. })
  785. describe('when the file does not exist', function () {
  786. it('should return success', async function () {
  787. // S3 doesn't give us a 404 when the object doesn't exist, so to stay
  788. // consistent we merrily return success ourselves here as well
  789. await app.persistor.deleteObject(
  790. Settings.filestore.stores.template_files,
  791. fileKey
  792. )
  793. })
  794. })
  795. })
  796. })
  797. }
  798. describe('with a pdf file', function () {
  799. let localFileSize
  800. const localFileReadPath = Path.resolve(
  801. import.meta.dirname,
  802. '../../fixtures/test.pdf'
  803. )
  804. beforeEach('upload test.pdf', async function () {
  805. const stat = await fsStat(localFileReadPath)
  806. localFileSize = stat.size
  807. const readStream = fs.createReadStream(localFileReadPath)
  808. const res = await fetch(fileUrl, { method: 'POST', body: readStream })
  809. if (!res.ok) throw new Error(res.statusText)
  810. })
  811. it('should be able get the file back', async function () {
  812. const response = await fetch(fileUrl)
  813. const body = await response.text()
  814. expect(body.substring(0, 8)).to.equal('%PDF-1.5')
  815. })
  816. if (backendSettings.backend !== 'fs') {
  817. it('should record an egress metric for the upload', async function () {
  818. const metric = await TestHelper.getMetric(
  819. filestoreUrl,
  820. `${metricPrefix}_egress`
  821. )
  822. expect(metric - previousEgress).to.equal(
  823. localFileSize + dataEncryptionKeySize
  824. )
  825. })
  826. }
  827. describe('getting the preview image', function () {
  828. this.timeout(1000 * 20)
  829. let previewFileUrl
  830. beforeEach('prepare previewFileUrl for preview', function () {
  831. previewFileUrl = `${fileUrl}?style=preview`
  832. })
  833. it('should not time out', async function () {
  834. const response = await fetch(previewFileUrl)
  835. expect(response.status).to.equal(200)
  836. await response.arrayBuffer()
  837. })
  838. it('should respond with image data', async function () {
  839. // note: this test relies of the imagemagick conversion working
  840. const response = await fetch(previewFileUrl)
  841. expect(response.status).to.equal(200)
  842. const body = await response.text()
  843. expect(body.length).to.be.greaterThan(400)
  844. expect(body.substr(1, 3)).to.equal('PNG')
  845. })
  846. })
  847. describe('warming the cache', function () {
  848. this.timeout(1000 * 20)
  849. let previewFileUrl
  850. beforeEach('prepare previewFileUrl for cacheWarn', function () {
  851. previewFileUrl = `${fileUrl}?style=preview&cacheWarm=true`
  852. })
  853. it('should not time out', async function () {
  854. const response = await fetch(previewFileUrl)
  855. expect(response.status).to.equal(200)
  856. await response.arrayBuffer()
  857. })
  858. it('should not leak sockets', async function () {
  859. const response1 = await fetch(previewFileUrl)
  860. expect(response1.status).to.equal(200)
  861. // do not read the response body, should be destroyed immediately
  862. const response2 = await fetch(previewFileUrl)
  863. expect(response2.status).to.equal(200)
  864. // do not read the response body, should be destroyed immediately
  865. await expectNoSockets()
  866. })
  867. it("should respond with only an 'OK'", async function () {
  868. // note: this test relies of the imagemagick conversion working
  869. const response = await fetch(previewFileUrl)
  870. const body = await response.text()
  871. expect(body).to.equal('OK')
  872. })
  873. })
  874. })
  875. describe('with server side encryption', function () {
  876. if (backendSettings.backend !== 's3SSEC') return
  877. before('sanity check top-level variable', function () {
  878. expect(dataEncryptionKeySize).to.equal(32)
  879. })
  880. let fileId1,
  881. fileId2,
  882. fileKey1,
  883. fileKey2,
  884. fileKeyOtherProject,
  885. fileUrl1,
  886. fileUrl2
  887. beforeEach('prepare ids', function () {
  888. fileId1 = new ObjectId().toString()
  889. fileId2 = new ObjectId().toString()
  890. fileKey1 = `${templateId}/v/0/${fileId1}`
  891. fileKey2 = `${templateId}/v/0/${fileId2}`
  892. fileKeyOtherProject = `${new ObjectId().toString()}/v/0/${new ObjectId().toString()}`
  893. fileUrl1 = `${templateUrl}/${fileId1}`
  894. fileUrl2 = `${templateUrl}/${fileId2}`
  895. })
  896. beforeEach('ensure DEK is missing', async function () {
  897. // Cannot use test helper expectPersistorNotToHaveFile here, we need to use the KEK.
  898. await expect(
  899. app.persistor.getDataEncryptionKeySize(
  900. backendSettings.stores.template_files,
  901. fileKey1
  902. )
  903. ).to.rejectedWith(NotFoundError)
  904. })
  905. async function createRandomContent(url, suffix = '') {
  906. const content = Math.random().toString() + suffix
  907. const res = await fetch(url, {
  908. method: 'POST',
  909. body: Stream.Readable.from([content]),
  910. })
  911. if (!res.ok) throw new Error(res.statusText)
  912. return async () => {
  913. const res = await fetch(url, { method: 'GET' })
  914. if (!res.ok) throw new Error(res.statusText)
  915. expect(await res.text()).to.equal(content)
  916. }
  917. }
  918. it('should create a DEK when asked explicitly', async function () {
  919. await app.persistor.generateDataEncryptionKey(
  920. backendSettings.stores.template_files,
  921. fileKey1
  922. )
  923. expect(
  924. await app.persistor.getDataEncryptionKeySize(
  925. backendSettings.stores.template_files,
  926. fileKey1
  927. )
  928. ).to.equal(32)
  929. })
  930. it('should create a DEK from writes', async function () {
  931. await createRandomContent(fileUrl1)
  932. expect(
  933. await app.persistor.getDataEncryptionKeySize(
  934. backendSettings.stores.template_files,
  935. fileKey1
  936. )
  937. ).to.equal(32)
  938. })
  939. it('should not create a DEK from reads', async function () {
  940. const res = await fetch(fileUrl1, {
  941. method: 'GET',
  942. })
  943. if (res.status !== 404) throw new Error(`${res.status} should be 404`)
  944. // Cannot use test helper expectPersistorNotToHaveFile here, we need to use the KEK.
  945. await expect(
  946. app.persistor.getDataEncryptionKeySize(
  947. backendSettings.stores.template_files,
  948. fileKey1
  949. )
  950. ).to.rejectedWith(NotFoundError)
  951. })
  952. it('should never overwrite a data encryption key', async function () {
  953. const checkGET = await createRandomContent(fileUrl1)
  954. await expect(
  955. app.persistor.generateDataEncryptionKey(
  956. backendSettings.stores.template_files,
  957. fileKey1
  958. )
  959. ).to.rejectedWith(AlreadyWrittenError)
  960. await checkGET()
  961. })
  962. it('should re-use the data encryption key after a write', async function () {
  963. const checkGET1 = await createRandomContent(fileUrl1, '1')
  964. const checkGET2 = await createRandomContent(fileUrl2, '2')
  965. await checkGET1()
  966. await checkGET2()
  967. })
  968. describe('kek rotation', function () {
  969. const newKEK = new RootKeyEncryptionKey(
  970. crypto.generateKeySync('aes', { length: 256 }).export(),
  971. Buffer.alloc(32)
  972. )
  973. const oldKEK = new RootKeyEncryptionKey(
  974. crypto.generateKeySync('aes', { length: 256 }).export(),
  975. Buffer.alloc(32)
  976. )
  977. const migrationStep0 = new PerProjectEncryptedS3Persistor({
  978. ...s3SSECConfig(),
  979. automaticallyRotateDEKEncryption: false,
  980. async getRootKeyEncryptionKeys() {
  981. return [oldKEK] // only old key
  982. },
  983. })
  984. const migrationStep1 = new PerProjectEncryptedS3Persistor({
  985. ...s3SSECConfig(),
  986. automaticallyRotateDEKEncryption: false,
  987. async getRootKeyEncryptionKeys() {
  988. return [oldKEK, newKEK] // new key as fallback
  989. },
  990. })
  991. const migrationStep2 = new PerProjectEncryptedS3Persistor({
  992. ...s3SSECConfig(),
  993. automaticallyRotateDEKEncryption: true, // <- different compared to partiallyRotated
  994. async getRootKeyEncryptionKeys() {
  995. return [newKEK, oldKEK] // old keys as fallback
  996. },
  997. })
  998. const migrationStep3 = new PerProjectEncryptedS3Persistor({
  999. ...s3SSECConfig(),
  1000. automaticallyRotateDEKEncryption: true,
  1001. async getRootKeyEncryptionKeys() {
  1002. return [newKEK] // only new key
  1003. },
  1004. })
  1005. async function checkWrites(
  1006. fileKey,
  1007. writer,
  1008. readersSuccess,
  1009. readersFailed
  1010. ) {
  1011. const content = Math.random().toString()
  1012. await writer.sendStream(
  1013. Settings.filestore.stores.template_files,
  1014. fileKey,
  1015. Stream.Readable.from([content])
  1016. )
  1017. for (const persistor of readersSuccess) {
  1018. await TestHelper.expectPersistorToHaveFile(
  1019. persistor,
  1020. backendSettings.stores.template_files,
  1021. fileKey,
  1022. content
  1023. )
  1024. }
  1025. for (const persistor of readersFailed) {
  1026. await expect(
  1027. TestHelper.expectPersistorToHaveFile(
  1028. persistor,
  1029. backendSettings.stores.template_files,
  1030. fileKey,
  1031. content
  1032. )
  1033. ).to.be.rejectedWith(NoKEKMatchedError)
  1034. }
  1035. }
  1036. const stages = [
  1037. {
  1038. name: 'stage 0 - [old]',
  1039. prev: migrationStep0,
  1040. cur: migrationStep0,
  1041. fail: [migrationStep3],
  1042. },
  1043. {
  1044. name: 'stage 1 - [old,new]',
  1045. prev: migrationStep0,
  1046. cur: migrationStep1,
  1047. fail: [],
  1048. },
  1049. {
  1050. name: 'stage 2 - [new,old]',
  1051. prev: migrationStep1,
  1052. cur: migrationStep2,
  1053. fail: [],
  1054. },
  1055. {
  1056. name: 'stage 3 - [new]',
  1057. prev: migrationStep2,
  1058. cur: migrationStep3,
  1059. fail: [migrationStep0],
  1060. },
  1061. ]
  1062. for (const { name, prev, cur, fail } of stages) {
  1063. describe(name, function () {
  1064. this.timeout(1000 * 30)
  1065. it('can read old writes', async function () {
  1066. await checkWrites(fileKey1, prev, [prev, cur], fail)
  1067. await checkWrites(fileKey2, prev, [prev, cur], fail) // check again after access
  1068. await checkWrites(fileKeyOtherProject, prev, [prev, cur], fail)
  1069. })
  1070. it('can read new writes', async function () {
  1071. await checkWrites(fileKey1, prev, [prev, cur], fail)
  1072. await checkWrites(fileKey2, cur, [prev, cur], fail) // check again after access
  1073. await checkWrites(fileKeyOtherProject, cur, [prev, cur], fail)
  1074. })
  1075. })
  1076. }
  1077. describe('full migration', function () {
  1078. it('can read old writes if rotated in sequence', async function () {
  1079. await checkWrites(
  1080. fileKey1,
  1081. migrationStep0,
  1082. [
  1083. migrationStep0,
  1084. migrationStep1,
  1085. migrationStep2, // migrates
  1086. migrationStep3,
  1087. ],
  1088. []
  1089. )
  1090. })
  1091. it('cannot read/write if not rotated', async function () {
  1092. await checkWrites(
  1093. fileKey1,
  1094. migrationStep0,
  1095. [migrationStep0],
  1096. [migrationStep3]
  1097. )
  1098. })
  1099. })
  1100. })
  1101. /** @type {import('aws-sdk/clients/s3')} */
  1102. let s3Client
  1103. before('create s3 client', function () {
  1104. s3Client = new S3Persistor(s3Config())._getClientForBucket('')
  1105. })
  1106. async function checkDEKStorage({
  1107. dekBucketKeys = [],
  1108. userFilesBucketKeys = [],
  1109. }) {
  1110. await createRandomContent(fileUrl1)
  1111. const { Contents: dekEntries } = await s3Client.send(
  1112. new ListObjectsV2Command({
  1113. Bucket: process.env.AWS_S3_USER_FILES_DEK_BUCKET_NAME,
  1114. Prefix: `${templateId}/`,
  1115. })
  1116. )
  1117. expect(dekEntries).to.have.length(dekBucketKeys.length)
  1118. // Order is not predictable, use members
  1119. expect(dekEntries.map(o => o.Key)).to.have.members(dekBucketKeys)
  1120. const { Contents: userFilesEntries } = await s3Client.send(
  1121. new ListObjectsV2Command({
  1122. Bucket: backendSettings.stores.template_files,
  1123. Prefix: `${templateId}/`,
  1124. })
  1125. )
  1126. expect(userFilesEntries).to.have.length(userFilesBucketKeys.length)
  1127. // Order is not predictable, use members
  1128. expect(userFilesEntries.map(o => o.Key)).to.have.members(
  1129. userFilesBucketKeys
  1130. )
  1131. }
  1132. it('should use a custom bucket for DEKs', async function () {
  1133. await checkDEKStorage({
  1134. dekBucketKeys: [`${templateId}/dek`],
  1135. userFilesBucketKeys: [fileKey1],
  1136. })
  1137. })
  1138. describe('deleteDirectory', function () {
  1139. let checkGET1, checkGET2
  1140. beforeEach('create files', async function () {
  1141. checkGET1 = await createRandomContent(fileUrl1, '1')
  1142. checkGET2 = await createRandomContent(fileUrl2, '2')
  1143. })
  1144. it('should refuse to delete top-level prefix', async function () {
  1145. await expect(
  1146. app.persistor.deleteDirectory(
  1147. Settings.filestore.stores.template_files,
  1148. templateId.slice(0, 3)
  1149. )
  1150. ).to.be.rejectedWith('not a project-folder')
  1151. expect(
  1152. await app.persistor.checkIfObjectExists(
  1153. Settings.filestore.stores.template_files,
  1154. fileKey1
  1155. )
  1156. ).to.equal(true)
  1157. await checkGET1()
  1158. expect(
  1159. await app.persistor.checkIfObjectExists(
  1160. Settings.filestore.stores.template_files,
  1161. fileKey2
  1162. )
  1163. ).to.equal(true)
  1164. expect(
  1165. await app.persistor.getDataEncryptionKeySize(
  1166. Settings.filestore.stores.template_files,
  1167. fileKey2
  1168. )
  1169. ).to.equal(32)
  1170. await checkGET2()
  1171. })
  1172. it('should delete sub-folder and keep DEK', async function () {
  1173. await app.persistor.deleteDirectory(
  1174. Settings.filestore.stores.template_files,
  1175. fileKey1 // not really a sub-folder, but it will do for this test.
  1176. )
  1177. expect(
  1178. await app.persistor.checkIfObjectExists(
  1179. Settings.filestore.stores.template_files,
  1180. fileKey1
  1181. )
  1182. ).to.equal(false)
  1183. expect(
  1184. await app.persistor.checkIfObjectExists(
  1185. Settings.filestore.stores.template_files,
  1186. fileKey2
  1187. )
  1188. ).to.equal(true)
  1189. expect(
  1190. await app.persistor.getDataEncryptionKeySize(
  1191. Settings.filestore.stores.template_files,
  1192. fileKey2
  1193. )
  1194. ).to.equal(32)
  1195. await checkGET2()
  1196. })
  1197. it('should delete project folder and DEK', async function () {
  1198. await app.persistor.deleteDirectory(
  1199. Settings.filestore.stores.template_files,
  1200. `${templateId}/`
  1201. )
  1202. expect(
  1203. await app.persistor.checkIfObjectExists(
  1204. Settings.filestore.stores.template_files,
  1205. fileKey1
  1206. )
  1207. ).to.equal(false)
  1208. expect(
  1209. await app.persistor.checkIfObjectExists(
  1210. Settings.filestore.stores.template_files,
  1211. fileKey2
  1212. )
  1213. ).to.equal(false)
  1214. await expect(
  1215. app.persistor.getDataEncryptionKeySize(
  1216. Settings.filestore.stores.template_files,
  1217. fileKey2
  1218. )
  1219. ).to.rejectedWith(NotFoundError)
  1220. })
  1221. })
  1222. })
  1223. describe('getObjectSize', function () {
  1224. it('should return a number', async function () {
  1225. const buf = Buffer.from('hello')
  1226. const res = await fetch(fileUrl, {
  1227. method: 'POST',
  1228. body: Stream.Readable.from([buf]),
  1229. })
  1230. if (!res.ok) throw new Error(res.statusText)
  1231. expect(
  1232. await app.persistor.getObjectSize(
  1233. Settings.filestore.stores.template_files,
  1234. fileKey
  1235. )
  1236. ).to.equal(buf.byteLength)
  1237. })
  1238. })
  1239. describe('checkIfObjectExists', function () {
  1240. it('should return false when the object does not exist', async function () {
  1241. expect(
  1242. await app.persistor.checkIfObjectExists(
  1243. Settings.filestore.stores.template_files,
  1244. fileKey
  1245. )
  1246. ).to.equal(false)
  1247. })
  1248. it('should return true when the object exists', async function () {
  1249. const res = await fetch(fileUrl, {
  1250. method: 'POST',
  1251. body: Stream.Readable.from(['hello']),
  1252. })
  1253. if (!res.ok) throw new Error(res.statusText)
  1254. expect(
  1255. await app.persistor.checkIfObjectExists(
  1256. Settings.filestore.stores.template_files,
  1257. fileKey
  1258. )
  1259. ).to.equal(true)
  1260. })
  1261. })
  1262. if (backendSettings.backend === 's3SSEC') {
  1263. describe('storageClass', function () {
  1264. it('should use the default storage class for dek', async function () {
  1265. const dekBucket = process.env.AWS_S3_USER_FILES_DEK_BUCKET_NAME
  1266. await app.persistor.sendStream(
  1267. dekBucket,
  1268. fileKey,
  1269. Stream.Readable.from(['hello'])
  1270. )
  1271. expect(
  1272. await app.persistor.getObjectStorageClass(dekBucket, fileKey)
  1273. ).to.equal(undefined)
  1274. })
  1275. it('should use the custom storage class for user files', async function () {
  1276. await app.persistor.sendStream(
  1277. Settings.filestore.stores.template_files,
  1278. fileKey,
  1279. Stream.Readable.from(['hello'])
  1280. )
  1281. const sc = AWS_S3_USER_FILES_STORAGE_CLASS
  1282. expect(sc).to.exist
  1283. expect(
  1284. await app.persistor.getObjectStorageClass(
  1285. Settings.filestore.stores.template_files,
  1286. fileKey
  1287. )
  1288. ).to.equal(sc)
  1289. })
  1290. })
  1291. }
  1292. describe('autoGunzip', function () {
  1293. this.timeout(60 * 1000)
  1294. const body = Buffer.alloc(10 * 1024 * 1024, 'hello')
  1295. const gzippedBody = gzipSync(body)
  1296. /**
  1297. * @param {string} key
  1298. * @param {Buffer} wantBody
  1299. * @param {boolean} autoGunzip
  1300. * @return {Promise<void>}
  1301. */
  1302. async function checkBodyIsTheSame(key, wantBody, autoGunzip) {
  1303. const s = await app.persistor.getObjectStream(
  1304. Settings.filestore.stores.template_files,
  1305. key,
  1306. { autoGunzip }
  1307. )
  1308. const buf = new WritableBuffer()
  1309. await Stream.promises.pipeline(s, buf)
  1310. expect(buf.getContents()).to.deep.equal(wantBody)
  1311. }
  1312. if (backendSettings.backend === 'fs') {
  1313. it('should refuse to handle autoGunzip', async function () {
  1314. await expect(
  1315. app.persistor.getObjectStream(
  1316. Settings.filestore.stores.template_files,
  1317. fileKey,
  1318. { autoGunzip: true }
  1319. )
  1320. ).to.be.rejectedWith(NotImplementedError)
  1321. })
  1322. } else {
  1323. it('should return the raw body with gzip', async function () {
  1324. await app.persistor.sendStream(
  1325. Settings.filestore.stores.template_files,
  1326. fileKey,
  1327. Stream.Readable.from([gzippedBody]),
  1328. { contentEncoding: 'gzip' }
  1329. )
  1330. expect(
  1331. await app.persistor.getObjectSize(
  1332. Settings.filestore.stores.template_files,
  1333. fileKey
  1334. )
  1335. ).to.equal(gzippedBody.byteLength)
  1336. // raw body with autoGunzip=true
  1337. await checkBodyIsTheSame(fileKey, body, true)
  1338. // gzip body without autoGunzip=false
  1339. await checkBodyIsTheSame(fileKey, gzippedBody, false)
  1340. })
  1341. it('should return the raw body without gzip compression', async function () {
  1342. await app.persistor.sendStream(
  1343. Settings.filestore.stores.template_files,
  1344. fileKey,
  1345. Stream.Readable.from([body])
  1346. )
  1347. expect(
  1348. await app.persistor.getObjectSize(
  1349. Settings.filestore.stores.template_files,
  1350. fileKey
  1351. )
  1352. ).to.equal(body.byteLength)
  1353. // raw body with both autoGunzip options
  1354. await checkBodyIsTheSame(fileKey, body, true)
  1355. await checkBodyIsTheSame(fileKey, body, false)
  1356. })
  1357. it('should return the gzip body without gzip header', async function () {
  1358. await app.persistor.sendStream(
  1359. Settings.filestore.stores.template_files,
  1360. fileKey,
  1361. Stream.Readable.from([gzippedBody])
  1362. )
  1363. expect(
  1364. await app.persistor.getObjectSize(
  1365. Settings.filestore.stores.template_files,
  1366. fileKey
  1367. )
  1368. ).to.equal(gzippedBody.byteLength)
  1369. // gzip body with both autoGunzip options
  1370. await checkBodyIsTheSame(fileKey, gzippedBody, true)
  1371. await checkBodyIsTheSame(fileKey, gzippedBody, false)
  1372. })
  1373. }
  1374. })
  1375. })
  1376. }
  1377. })