| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371 |
- const { expect } = require('chai')
- const config = require('config')
- const fs = require('node:fs')
- const fetch = require('node-fetch')
- const HTTPStatus = require('http-status')
- const cleanup = require('../storage/support/cleanup')
- const fixtures = require('../storage/support/fixtures')
- const testFiles = require('../storage/support/test_files')
- const testServer = require('./support/test_server')
- const { expectHttpError } = require('./support/expect_response')
- const { globalBlobs } = require('../../../../storage/lib/mongodb.js')
- const {
- loadGlobalBlobs,
- } = require('../../../../storage/lib/blob_store/index.js')
- describe('Project blobs API', function () {
- const projectId = '123'
- beforeEach(cleanup.everything)
- beforeEach(fixtures.create)
- let client
- let token
- before(async function () {
- client = await testServer.createClientForProject(projectId)
- token = testServer.createTokenForProject(projectId)
- })
- it('returns 404 if the blob is not found', async function () {
- const testHash = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
- await expectHttpError(
- client.apis.Project.getProjectBlob({
- project_id: projectId,
- hash: testHash,
- }),
- HTTPStatus.NOT_FOUND
- )
- })
- it('checks if file hash matches the hash parameter', async function () {
- const testHash = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
- const response = await fetch(
- testServer.url(`/api/projects/${projectId}/blobs/${testHash}`),
- {
- method: 'PUT',
- headers: { Authorization: `Bearer ${token}` },
- body: fs.createReadStream(testFiles.path('hello.txt')),
- }
- )
- expect(response.status).to.equal(HTTPStatus.CONFLICT)
- // check that it did not store the file
- await expectHttpError(
- client.apis.Project.getProjectBlob({
- project_id: projectId,
- hash: testFiles.HELLO_TXT_HASH,
- }),
- HTTPStatus.NOT_FOUND
- )
- })
- it('rejects oversized files', async function () {
- const testHash = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
- const buffer = Buffer.alloc(
- parseInt(config.get('maxFileUploadSize'), 10) + 1
- )
- const response = await fetch(
- testServer.url(`/api/projects/${projectId}/blobs/${testHash}`),
- {
- method: 'PUT',
- headers: { Authorization: `Bearer ${token}` },
- body: buffer,
- }
- )
- expect(response.status).to.equal(HTTPStatus.REQUEST_ENTITY_TOO_LARGE)
- })
- describe('with an existing blob', async function () {
- let fileContents
- beforeEach(async function () {
- fileContents = await fs.promises.readFile(testFiles.path('hello.txt'))
- const response = await fetch(
- testServer.url(
- `/api/projects/${projectId}/blobs/${testFiles.HELLO_TXT_HASH}`
- ),
- {
- method: 'PUT',
- headers: { Authorization: `Bearer ${token}` },
- body: fileContents,
- }
- )
- expect(response.ok).to.be.true
- })
- it('fulfills a request with a JWT header', async function () {
- const response = await client.apis.Project.getProjectBlob({
- project_id: projectId,
- hash: testFiles.HELLO_TXT_HASH,
- })
- const responseText = await response.data.text()
- expect(responseText).to.equal(fileContents.toString())
- })
- it('fulfills a request with a token parameter', async function () {
- const url = new URL(
- testServer.url(
- `/api/projects/${projectId}/blobs/${testFiles.HELLO_TXT_HASH}`
- )
- )
- url.searchParams.append('token', token)
- const response = await fetch(url)
- const payload = await response.text()
- expect(payload).to.equal(fileContents.toString())
- })
- it('supports range request from beginning', async function () {
- const url = new URL(
- testServer.url(
- `/api/projects/${projectId}/blobs/${testFiles.HELLO_TXT_HASH}`
- )
- )
- url.searchParams.append('token', token)
- const response = await fetch(url, { headers: { Range: 'bytes=0-4' } })
- const payload = await response.text()
- // 0-4 is inclusive, so 5 bytes
- expect(payload).to.equal(fileContents.slice(0, 5).toString())
- expect(response.headers.get('Content-Range')).to.equal(
- `bytes 0-4/${testFiles.HELLO_TXT_BYTE_LENGTH}`
- )
- expect(response.headers.get('Content-Length')).to.equal('5')
- expect(response.status).to.equal(HTTPStatus.PARTIAL_CONTENT)
- })
- it('supports range request in middle', async function () {
- const url = new URL(
- testServer.url(
- `/api/projects/${projectId}/blobs/${testFiles.HELLO_TXT_HASH}`
- )
- )
- url.searchParams.append('token', token)
- const response = await fetch(url, { headers: { Range: 'bytes=5-9' } })
- const payload = await response.text()
- // 5-9 is inclusive, so 5 bytes
- expect(payload).to.equal(fileContents.slice(5, 10).toString())
- expect(response.headers.get('Content-Range')).to.equal(
- `bytes 5-9/${testFiles.HELLO_TXT_BYTE_LENGTH}`
- )
- expect(response.headers.get('Content-Length')).to.equal('5')
- expect(response.status).to.equal(HTTPStatus.PARTIAL_CONTENT)
- })
- it('supports range request past end', async function () {
- const url = new URL(
- testServer.url(
- `/api/projects/${projectId}/blobs/${testFiles.HELLO_TXT_HASH}`
- )
- )
- url.searchParams.append('token', token)
- const response = await fetch(url, { headers: { Range: 'bytes=5-100' } })
- const payload = await response.text()
- expect(payload).to.equal(fileContents.slice(5).toString())
- // Response range should be capped to the actual file size
- expect(response.headers.get('Content-Range')).to.equal(
- `bytes 5-10/${testFiles.HELLO_TXT_BYTE_LENGTH}`
- )
- expect(response.headers.get('Content-Length')).to.equal('6')
- expect(response.status).to.equal(HTTPStatus.PARTIAL_CONTENT)
- })
- describe('with invalid request ranges', async function () {
- async function testInvalidRange(range) {
- const url = new URL(
- testServer.url(
- `/api/projects/${projectId}/blobs/${testFiles.HELLO_TXT_HASH}`
- )
- )
- url.searchParams.append('token', token)
- const response = await fetch(url, { headers: { Range: range } })
- expect(response.headers.get('Content-Range')).to.equal(
- `bytes */${testFiles.HELLO_TXT_BYTE_LENGTH}`
- )
- expect(response.headers.get('Content-Length')).to.equal('0')
- expect(response.status).to.equal(
- HTTPStatus.REQUESTED_RANGE_NOT_SATISFIABLE
- )
- }
- it('should fail on invalid range where start > end', async function () {
- testInvalidRange('bytes=10-5')
- })
- it('should fail on invalid range where start = length', async function () {
- testInvalidRange(`bytes=${testFiles.HELLO_TXT_BYTE_LENGTH + 1}-105`)
- })
- it('should fail on invalid range where start > length', async function () {
- testInvalidRange(`bytes=${testFiles.HELLO_TXT_BYTE_LENGTH}-105`)
- })
- })
- describe('with an empty blob', async function () {
- beforeEach(async function () {
- const response = await fetch(
- testServer.url(
- `/api/projects/${projectId}/blobs/${testFiles.EMPTY_FILE_HASH}`
- ),
- {
- method: 'PUT',
- headers: { Authorization: `Bearer ${token}` },
- body: '',
- }
- )
- expect(response.ok).to.be.true
- })
- it('should not handle range requests', async function () {
- const url = new URL(
- testServer.url(
- `/api/projects/${projectId}/blobs/${testFiles.EMPTY_FILE_HASH}`
- )
- )
- url.searchParams.append('token', token)
- const response = await fetch(url, { headers: { Range: 'bytes=0-0' } })
- expect(response.headers.get('Content-Range')).to.equal(
- `bytes */${testFiles.EMPTY_FILE_BYTE_LENGTH}`
- )
- expect(response.headers.get('Content-Length')).to.equal('0')
- expect(response.status).to.equal(
- HTTPStatus.REQUESTED_RANGE_NOT_SATISFIABLE
- )
- })
- it('should handle full requests', async function () {
- const url = new URL(
- testServer.url(
- `/api/projects/${projectId}/blobs/${testFiles.EMPTY_FILE_HASH}`
- )
- )
- url.searchParams.append('token', token)
- const response = await fetch(url)
- const payload = await response.text()
- expect(payload).to.equal('')
- expect(response.status).to.equal(HTTPStatus.OK)
- })
- })
- it('supports HEAD request', async function () {
- const url = new URL(
- testServer.url(
- `/api/projects/${projectId}/blobs/${testFiles.HELLO_TXT_HASH}`
- )
- )
- url.searchParams.append('token', token)
- const response = await fetch(url, { method: 'HEAD' })
- expect(response.headers.get('Content-Length')).to.equal(
- testFiles.HELLO_TXT_BYTE_LENGTH.toString()
- )
- const payload = await response.text()
- expect(payload).to.have.length(0)
- })
- it('rejects an unautorized request', async function () {
- const response = await fetch(
- testServer.url(
- `/api/projects/${projectId}/blobs/${testFiles.HELLO_TXT_HASH}`
- )
- )
- expect(response.status).to.equal(HTTPStatus.UNAUTHORIZED)
- })
- it('copies the blob to another project', async function () {
- const targetProjectId = '456'
- const targetClient =
- await testServer.createClientForProject(targetProjectId)
- const targetToken = testServer.createTokenForProject(targetProjectId)
- const url = new URL(
- testServer.url(
- `/api/projects/${targetProjectId}/blobs/${testFiles.HELLO_TXT_HASH}`
- )
- )
- url.searchParams.append('copyFrom', projectId)
- const response = await fetch(url, {
- method: 'POST',
- headers: { Authorization: `Bearer ${targetToken}` },
- })
- expect(response.status).to.equal(HTTPStatus.CREATED)
- const newBlobResponse = await targetClient.apis.Project.getProjectBlob({
- project_id: targetProjectId,
- hash: testFiles.HELLO_TXT_HASH,
- })
- const newBlobResponseText = await newBlobResponse.data.text()
- expect(newBlobResponseText).to.equal(fileContents.toString())
- })
- it('skips copying a blob to another project if it already exists', async function () {
- const targetProjectId = '456'
- const targetClient =
- await testServer.createClientForProject(targetProjectId)
- const targetToken = testServer.createTokenForProject(targetProjectId)
- const fileContents = await fs.promises.readFile(
- testFiles.path('hello.txt')
- )
- const uploadResponse = await fetch(
- testServer.url(
- `/api/projects/${targetProjectId}/blobs/${testFiles.HELLO_TXT_HASH}`
- ),
- {
- method: 'PUT',
- headers: { Authorization: `Bearer ${targetToken}` },
- body: fileContents,
- }
- )
- expect(uploadResponse.ok).to.be.true
- const url = new URL(
- testServer.url(
- `/api/projects/${targetProjectId}/blobs/${testFiles.HELLO_TXT_HASH}`
- )
- )
- url.searchParams.append('copyFrom', projectId)
- const response = await fetch(url, {
- method: 'POST',
- headers: { Authorization: `Bearer ${targetToken}` },
- })
- expect(response.status).to.equal(HTTPStatus.NO_CONTENT)
- const newBlobResponse = await targetClient.apis.Project.getProjectBlob({
- project_id: targetProjectId,
- hash: testFiles.HELLO_TXT_HASH,
- })
- const newBlobResponseText = await newBlobResponse.data.text()
- expect(newBlobResponseText).to.equal(fileContents.toString())
- })
- })
- describe('with a global blob', async function () {
- before(async function () {
- await globalBlobs.insertOne({
- _id: testFiles.STRING_A_HASH,
- byteLength: 1,
- stringLength: 1,
- })
- await loadGlobalBlobs()
- })
- it('does not copy global blobs', async function () {
- const targetProjectId = '456'
- const targetToken = testServer.createTokenForProject(targetProjectId)
- const url = new URL(
- testServer.url(
- `/api/projects/${targetProjectId}/blobs/${testFiles.STRING_A_HASH}`
- )
- )
- url.searchParams.append('copyFrom', projectId)
- const response = await fetch(url, {
- method: 'POST',
- headers: { Authorization: `Bearer ${targetToken}` },
- })
- expect(response.status).to.equal(HTTPStatus.NO_CONTENT)
- })
- })
- })
|