FilestoreTests.js 54 KB

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