diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 901ec2c6..4b59986d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,12 +15,64 @@ jobs: strategy: matrix: node-version: [14.x] + services: + mongo: + image: mongo:latest + env: + MONGO_INITDB_ROOT_USERNAME: admin + MONGO_INITDB_ROOT_PASSWORD: password + MONGO_INITDB_DATABASE: bridge-test + ports: + - 27017:27017 + bridge-redis: + image: redis:latest + ports: + - 6379:6379 + env: DATABASE_URI: ${{ secrets.DATABASE_URI }} - steps: - - name: Start MongoDB - run: docker run -p 27017:27017 -e MONGO_INITDB_ROOT_USERNAME=admin -e MONGO_INITDB_ROOT_PASSWORD=password -e MONGO_INITDB_DATABASE=bridge-test -d mongo + NODE_ENV: development + inxtbridge_storage__mongoUrl: mongodb://admin:password@localhost:27017/bridge-test + inxtbridge_redis__host: localhost + inxtbridge_storage__mongoOpts__dbName: bridge-test + inxtbridge_api_keys__segment_test: WDeJ6fM5C79J7xikZOAiQYHwmUhFGOc3 + inxtbridge_application__CLUSTER__0: 9a1c78a507689f6f54b847ad1cef1e614ee23f1e + inxtbridge_logger__level: 5 + inxtbridge_storage__mongoOpts__authSource: admin + inxtbridge_stripe__PK_LIVE: pk_live_api_key + inxtbridge_api_keys__segment: segment_key + inxtbridge_stripe__SK_LIVE: sk_live_stripe_key + inxtbridge_storage__mongoOpts__pass: password + inxtbridge_redis__port: 6379 + inxtbridge_server__ssl__redirect: 443 + inxtbridge_redis__password: + inxtbridge_complex__rpcUser: networkuser + inxtbridge_stripe__SIG_TEST: stripe_sig_test + inxtbridge_gateway__username: user + inxtbridge_mailer__auth__pass: mailerpass + inxtbridge_storage__mongoOpts__user: admin + inxtbridge_stripe__PK_TEST: pk_test_vpHlkSQ7DhmzSW4EbmfT1lIJ + #inxtbridge_storage__mongoOpts__replicaSet: internxt-net + inxtbridge_complex__rpcPassword: networkpass + inxtbridge_mailer__host: smtp.sendgrid.net + inxtbridge_server__public__host: drive-server + inxtbridge_stripe__SK_TEST: sk_test_stripe_key + inxtbridge_complex__rpcUrl: http://landlord:8080 + inxtbridge_mailer__auth__user: internxt + inxtbridge_mailer__sendgrid__api_key: APIKEY + inxtbridge_storage__mongoOpts__server__poolSize: 100 + inxtbridge_stripe__SIG: whsec_otra_clave_de_stripe + inxtbridge_drive__api: http://drive-server:8000 + inxtbridge_server__public__port: 443 + inxtbridge_mailer__port: 465 + inxtbridge_gateway__password: gatewaypass + inxtbridge_gateway__JWT_SECRET: asecret + inxtbridge_QUEUE_HOST: 100.100.100.100 + inxtbridge_QUEUE_USERNAME: username + inxtbridge_QUEUE_PASSWORD: password + + steps: - uses: actions/checkout@v2 - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v2 @@ -29,4 +81,4 @@ jobs: - run: yarn run test - run: yarn run test-mongo-init - run: yarn run test-mongo - + - run: yarn run dev & yarn run test:e2e diff --git a/lib/core/buckets/usecase.ts b/lib/core/buckets/usecase.ts index 40349c3b..3398357b 100644 --- a/lib/core/buckets/usecase.ts +++ b/lib/core/buckets/usecase.ts @@ -86,7 +86,7 @@ export class InvalidUploadIndexes extends Error { } export class InvalidMultiPartValueError extends Error { constructor() { - super('Multipart is not allowed for files smaller than 500MB'); + super('Multipart is not allowed for small files'); Object.setPrototypeOf(this, InvalidMultiPartValueError.prototype); } diff --git a/lib/server/routes/buckets.js b/lib/server/routes/buckets.js index 865a78b7..964c9923 100644 --- a/lib/server/routes/buckets.js +++ b/lib/server/routes/buckets.js @@ -1428,19 +1428,29 @@ BucketsRouter.prototype.getFileId = function (req, res, next) { BucketsRouter.prototype.getFileInfo = function (req, res, next) { + const { id: bucketId, file: fileId } = req.params; + + if (!bucketId) { + return next(new errors.BadRequestError('No bucket id')); + } + + if (!fileId) { + return next(new errors.BadRequestError('No file id')); + } + this._getBucketUnregistered(req, res, (err, bucket) => { if (err) { return next(err); } - this.usecase.getFileInfo(bucket._id, req.params.file).then((fileInfo) => { + this.usecase.getFileInfo(bucket._id, fileId).then((fileInfo) => { return res.status(200).send(fileInfo); }).catch((err) => { if (err instanceof BucketEntryNotFoundError || err instanceof BucketEntryFrameNotFoundError) { return next(new errors.NotFoundError(err.message)); } - log.error('getFileInfo: Error for file %s: %s. %s', req.params.file, err.message, err.stack); + log.error('getFileInfo: Error for file %s: %s. %s', fileId, err.message, err.stack); return next(new errors.InternalError(err.message)); }); @@ -1519,6 +1529,10 @@ BucketsRouter.prototype.startUpload = async function (req, res, next) { return next(new errors.BadRequestError('Uploads is not an array')); } + if (uploads.length === 0) { + return next(new errors.BadRequestError('Uploads is empty')); + } + for (const { index, size } of uploads) { if (typeof size !== 'number' || size < 0) { return next(new errors.BadRequestError('Invalid size')); @@ -1609,7 +1623,7 @@ BucketsRouter.prototype.finishUpload = async function (req, res, next) { return next(new errors.BadRequestError('Missing hash')); } if (UploadId && !parts) { - return next(new errors.BadRequestError('For multipart: must provide also an array of parts for this upload')); + return next(new errors.BadRequestError('For multipart: must provide also the number of parts')); } if (parts && !UploadId) { return next(new errors.BadRequestError('For multipart: must provide also the UploadId for this upload')); diff --git a/package.json b/package.json index 45dee263..ae836063 100644 --- a/package.json +++ b/package.json @@ -26,9 +26,10 @@ "make-docs": "./node_modules/.bin/jsdoc index.js lib -r -R README.md -u ./doc -c .jsdoc.json --verbose -d ./jsdoc", "publish-docs": "gh-pages -d jsdoc --repo git@github.com:internxt/bridge.git", "build": "tsc", - "test": "jest --testPathIgnorePatterns ./tests/lib/mongo", + "test": "jest --testPathIgnorePatterns ./tests/lib/mongo ./tests/lib/e2e", "test-mongo-init": "ts-node ./tests/lib/mongo/init", - "test-mongo": "jest ./tests/lib/mongo --testTimeout 30000 --runInBand" + "test-mongo": "jest ./tests/lib/mongo --testTimeout 30000 --runInBand", + "test:e2e": "NODE_ENV=test jest --config ./tests/lib/e2e/jest-e2e.json" }, "repository": { "type": "git", @@ -59,6 +60,7 @@ "@types/node": "^17.0.23", "@types/node-mongodb-fixtures": "^3.2.3", "@types/sinon": "^10.0.11", + "@types/supertest": "^2.0.12", "@types/uuid": "^8.3.4", "chai": "^4.2.0", "coveralls": "^2.11.6", @@ -83,6 +85,7 @@ "redis-mock": "^0.16.0", "rimraf": "^2.6.3", "sinon": "^13.0.1", + "supertest": "^6.2.4", "ts-jest": "^27.1.4", "ts-node": "^10.7.0" }, diff --git a/tests/lib/e2e/bucket/finishUpload.e2e-spec.ts b/tests/lib/e2e/bucket/finishUpload.e2e-spec.ts new file mode 100644 index 00000000..152dfe65 --- /dev/null +++ b/tests/lib/e2e/bucket/finishUpload.e2e-spec.ts @@ -0,0 +1,492 @@ +import { v4 as uuidv4 } from 'uuid'; +import { + startUploadEndpoint, + startsCorrectly, +} from './startUpload.e2e-spec.test'; +import crypto from 'crypto'; +import axios from 'axios'; +import { waitForBridgeToBeUp } from '../setup'; +import { + api, + AuthorizationHeader, + registerSampleUserAndGetBucketId, +} from '../setup'; + +let bucketId: string; +let FINISH_UPLOAD_PATH: string; + +export const finishUploadEndpoint = (bucketId: string) => + `/v2/buckets/${bucketId}/files/finish`; + +export const finishesCorrectly = async ( + bucketId: string, + payload: { + index: string; + shards: { + hash: string; + uuid: string; + }[]; + } +) => { + let endpoint = finishUploadEndpoint(bucketId); + return api.post(endpoint).send(payload).set(AuthorizationHeader); +}; + +export const finishesCorrectlyMultiparts = async ( + bucketId: string, + payload: { + index: string; + shards: { + UploadId: string; + parts: { ETag: string; PartNumber: number }[]; + hash: string; + uuid: string; + }[]; + } +) => { + let endpoint = finishUploadEndpoint(bucketId); + return api.post(endpoint).send(payload).set(AuthorizationHeader); +}; + +const longTimeout = 50_000; + +describe('Finish Upload v2', () => { + beforeAll(async () => { + await waitForBridgeToBeUp(); + bucketId = await registerSampleUserAndGetBucketId(); + FINISH_UPLOAD_PATH = finishUploadEndpoint(bucketId); + }, longTimeout); + + describe('Validation Finish Upload (non-multipart)', () => { + it('Mising body', async () => { + const response = await api + .post(FINISH_UPLOAD_PATH) + .send({}) + .set(AuthorizationHeader); + + expect(response.status).toBe(400); + expect(response.body.error).toBe('Missing parameters'); + }); + + it('Mising index', async () => { + const response = await api + .post(FINISH_UPLOAD_PATH) + .send({ + shards: [ + { + hash: 'ba20c3927245283f1fddaf94be044227724600df', + uuid: uuidv4(), + }, + ], + }) + .set(AuthorizationHeader); + + expect(response.status).toBe(400); + expect(response.body.error).toBe('Missing parameters'); + }); + + it('Invalid index', async () => { + const response = await api + .post(FINISH_UPLOAD_PATH) + .send({ + index: + 'c34695282e2fc4bf58833d9fc607c61da69b5b5c74e6224ec30f559c9a27043', + shards: [ + { + hash: 'ba20c3927245283f1fddaf94be044227724600df', + uuid: uuidv4(), + }, + ], + }) + .set(AuthorizationHeader); + + expect(response.status).toBe(400); + expect(response.body.error).toBe('Invalid index'); + }); + + it('Missing shards', async () => { + const response = await api + .post(FINISH_UPLOAD_PATH) + .send({ + index: + '0c34695282e2fc4bf58833d9fc607c61da69b5b5c74e6224ec30f559c9a27043', + }) + .set(AuthorizationHeader); + + expect(response.status).toBe(400); + expect(response.body.error).toBe('Missing parameters'); + }); + + it('Shards is not an array', async () => { + const response = await api + .post(FINISH_UPLOAD_PATH) + .send({ + index: + '0c34695282e2fc4bf58833d9fc607c61da69b5b5c74e6224ec30f559c9a27043', + shards: true, + }) + .set(AuthorizationHeader); + + expect(response.status).toBe(400); + expect(response.body.error).toBe('Shards is not an array'); + }); + + it('Shards Invalid uuid', async () => { + const response = await api + .post(FINISH_UPLOAD_PATH) + .send({ + index: + '0c34695282e2fc4bf58833d9fc607c61da69b5b5c74e6224ec30f559c9a27043', + shards: [ + { + hash: 'ba20c3927245283f1fddaf94be044227724600df', + uuid: 'uuid-fake', + }, + ], + }) + .set(AuthorizationHeader); + + expect(response.status).toBe(400); + expect(response.body.error).toBe('Invalid UUID'); + }); + + it('Shards Missing hash', async () => { + const response = await api + .post(FINISH_UPLOAD_PATH) + .send({ + index: + '0c34695282e2fc4bf58833d9fc607c61da69b5b5c74e6224ec30f559c9a27043', + shards: [ + { + uuid: uuidv4(), + }, + ], + }) + .set(AuthorizationHeader); + + expect(response.status).toBe(400); + expect(response.body.error).toBe('Missing hash'); + }); + + it('Shards Missing uuid', async () => { + const response = await api + .post(FINISH_UPLOAD_PATH) + .send({ + index: + '0c34695282e2fc4bf58833d9fc607c61da69b5b5c74e6224ec30f559c9a27043', + shards: [ + { + hash: 'ba20c3927245283f1fddaf94be044227724600df', + }, + ], + }) + .set(AuthorizationHeader); + + expect(response.status).toBe(400); + expect(response.body.error).toBe('Invalid UUID'); + }); + }); + + describe('Validation Finish Upload - Multipart', () => { + it('Invalid multipart value, missing parts', async () => { + const response = await api + .post(FINISH_UPLOAD_PATH) + .send({ + index: + '0c34695282e2fc4bf58833d9fc607c61da69b5b5c74e6224ec30f559c9a27043', + shards: [ + { + hash: 'ba20c3927245283f1fddaf94be044227724600df', + uuid: uuidv4(), + UploadId: 'some_id', + }, + ], + }) + .set(AuthorizationHeader); + + expect(response.status).toBe(400); + expect(response.body.error).toBe( + 'For multipart: must provide also the number of parts' + ); + }); + + it('Invalid multipart value, missing UploadId', async () => { + const response = await api + .post(FINISH_UPLOAD_PATH) + .send({ + index: + '0c34695282e2fc4bf58833d9fc607c61da69b5b5c74e6224ec30f559c9a27043', + shards: [ + { + hash: 'ba20c3927245283f1fddaf94be044227724600df', + uuid: uuidv4(), + parts: 4, + }, + ], + }) + .set(AuthorizationHeader); + + expect(response.status).toBe(400); + expect(response.body.error).toBe( + 'For multipart: must provide also the UploadId for this upload' + ); + }); + + it('Missing parts value', async () => { + const response = await api + .post(FINISH_UPLOAD_PATH) + .send({ + index: + '0c34695282e2fc4bf58833d9fc607c61da69b5b5c74e6224ec30f559c9a27043', + shards: [ + { + hash: 'ba20c3927245283f1fddaf94be044227724600df', + uuid: uuidv4(), + UploadId: 'some_id', + }, + ], + }) + .set(AuthorizationHeader); + + expect(response.status).toBe(400); + expect(response.body.error).toBe( + 'For multipart: must provide also the number of parts' + ); + }); + }); + + describe('Finish Upload functionality', () => { + describe('Normal (non-multipart)', () => { + it('Non existing bucket', async () => { + const response = await api + .post(finishUploadEndpoint('f701d5cc906a6f7e294d50f7')) + .send({ + index: + '0c34695282e2fc4bf58833d9fc607c61da69b5b5c74e6224ec30f559c9a27043', + shards: [ + { + hash: 'ba20c3927245283f1fddaf94be044227724600df', + uuid: uuidv4(), + }, + ], + }) + .set(AuthorizationHeader); + + expect(response.status).toBe(404); + expect(response.body.error).toBe('Bucket not found'); + }); + + it('Missing uploads', async () => { + const startUploadResponse = await api + .post(startUploadEndpoint(bucketId)) + .send({ + uploads: [ + { + index: 0, + size: 1000, + }, + { + index: 1, + size: 10000, + }, + ], + }) + .set(AuthorizationHeader); + + const { uploads } = startUploadResponse.body; + + const index = crypto.randomBytes(32).toString('hex'); + const response = await api + .post(FINISH_UPLOAD_PATH) + .send({ + index, + shards: [ + { + hash: 'ba20c3927245283f1fddaf94be044227724600df', + uuid: uploads[0].uuid, + }, + { + hash: 'ca20c3927245283f1fddaf94be044227724600df', + // Fake uuid: + uuid: uuidv4(), + }, + ], + }) + .set(AuthorizationHeader); + + expect(response.status).toBe(409); + expect(response.body.error).toBe( + 'Missing uploads to complete the upload' + ); + }); + }); + + it('Uploads and finishes correctly', async () => { + const startUploadResponse = await startsCorrectly(bucketId, { + uploads: [ + { + index: 0, + size: 1000, + }, + { + index: 1, + size: 10000, + }, + ], + }); + + const { uploads } = startUploadResponse.body; + + for (const upload of uploads) { + const { url, urls, index, uuid } = upload; + expect(url).toBeDefined(); + expect(url).toContain('http'); + expect(urls).toBeNull(); + expect(uuid).toBeDefined(); + const file = crypto.randomBytes(50).toString('hex'); + await axios.put(url, file, { + headers: { + 'Content-Type': 'application/octet-stream', + }, + }); + } + + const index = crypto.randomBytes(32).toString('hex'); + const responseComplete = await finishesCorrectly(bucketId, { + index, + shards: [ + { + hash: crypto.randomBytes(20).toString('hex'), + uuid: uploads[0].uuid, + }, + { + hash: crypto.randomBytes(20).toString('hex'), + uuid: uploads[1].uuid, + }, + ], + }); + + expect(responseComplete.status).toBe(200); + + const { + bucket, + created, + filename, + id, + index: indexResponse, + mimetype, + renewal, + size, + version, + } = responseComplete.body; + + expect(bucket).toEqual(bucketId); + expect(created).toBeDefined(); + expect(filename).toBeDefined(); + expect(id).toBeDefined(); + expect(indexResponse).toEqual(index); + expect(mimetype).toBeDefined(); + expect(renewal).toBeDefined(); + expect(size).toBeGreaterThan(0); + expect(typeof size).toBe('number'); + expect(version).toBe(2); + }); + + describe('Multipart', () => { + it('Uploads multipart starts and finishes correctly', async () => { + const multiparts = 3; + const startUploadResponse = await startsCorrectly( + bucketId, + { + uploads: [ + { + index: 0, + size: 100 * 1024 * 1024, + }, + { + index: 1, + size: 100 * 1024 * 1024, + }, + ], + }, + multiparts + ); + + const { uploads } = startUploadResponse.body; + const uploadParts: { ETag: string; PartNumber: number }[][] = []; + for (const upload of uploads) { + const { url, urls, index, uuid, UploadId } = upload; + expect(url).toBeNull(); + expect(urls).toBeDefined(); + expect(urls.length).toEqual(3); + expect(uuid).toBeDefined(); + expect(UploadId).toBeDefined(); + const parts: { ETag: string; PartNumber: number }[] = []; + let PartNumber = 1; + for (const urlToUpload of urls) { + expect(urlToUpload).toContain('http'); + const fileSize = 5 * 1024 * 1024; + const file = crypto.randomBytes(fileSize).toString('hex'); + const responseUpload = await axios.put(urlToUpload, file, { + headers: { + 'Content-Type': '', + }, + maxBodyLength: fileSize * 2, + }); + expect(responseUpload.status).toEqual(200); + parts.push({ + ETag: responseUpload.headers.etag, + PartNumber, + }); + PartNumber += 1; + } + uploadParts.push(parts); + } + + const index = crypto.randomBytes(32).toString('hex'); + const responseComplete = await finishesCorrectlyMultiparts(bucketId, { + index, + shards: [ + { + UploadId: uploads[0].UploadId, + parts: uploadParts[0], + hash: crypto.randomBytes(20).toString('hex'), + uuid: uploads[0].uuid, + }, + { + UploadId: uploads[1].UploadId, + parts: uploadParts[1], + hash: crypto.randomBytes(20).toString('hex'), + uuid: uploads[1].uuid, + }, + ], + }); + + expect(responseComplete.status).toBe(200); + + const { + bucket, + created, + filename, + id, + index: indexResponse, + mimetype, + renewal, + size, + version, + } = responseComplete.body; + + expect(bucket).toEqual(bucketId); + expect(created).toBeDefined(); + expect(filename).toBeDefined(); + expect(id).toBeDefined(); + expect(indexResponse).toEqual(index); + expect(mimetype).toBeDefined(); + expect(renewal).toBeDefined(); + expect(size).toBeGreaterThan(0); + expect(typeof size).toBe('number'); + expect(version).toBe(2); + }); + }); + }); +}); diff --git a/tests/lib/e2e/bucket/getDownloadLinks.e2e-spec.ts b/tests/lib/e2e/bucket/getDownloadLinks.e2e-spec.ts new file mode 100644 index 00000000..d7c29854 --- /dev/null +++ b/tests/lib/e2e/bucket/getDownloadLinks.e2e-spec.ts @@ -0,0 +1,60 @@ +import { + api, + AuthorizationHeader, + registerSampleUserAndGetBucketId, + waitForBridgeToBeUp, +} from '../setup'; + +let bucketId: string; +let GET_DOWNLOAD_LINKS_PATH: string; +let GET_DOWNLOAD_LINKS_PATH_V2: string; + +const fakeFileId = '62c2e18219e09f00511aded9'; + +const longTimeout = 50_000; + +describe('Get Download Links', () => { + beforeAll(async () => { + await waitForBridgeToBeUp(); + bucketId = await registerSampleUserAndGetBucketId(); + GET_DOWNLOAD_LINKS_PATH = `/buckets/${bucketId}/files/${fakeFileId}/info`; + GET_DOWNLOAD_LINKS_PATH_V2 = `/v2/buckets/${bucketId}/files/${fakeFileId}/mirrors`; + }, longTimeout); + + describe('Get Download links V1 - Validation', () => { + it('Malformed bucketId', async () => { + const response = await api + .get(`/buckets/a_malforned_bucket_id/files/${fakeFileId}/info`) + .set(AuthorizationHeader); + + expect(response.status).toBe(400); + expect(response.body.error).toBe('Bucket id is malformed'); + }); + + it('Malformed fileId', async () => { + const response = await api + .get(`/buckets/${bucketId}/files/a_malformed_file_id/info`) + .set(AuthorizationHeader); + + expect(response.status).toBe(400); + expect(response.body.error).toBe('File id is malformed'); + }); + }); + + describe('Get Download links V2', () => { + it('Mising bucketId', async () => { + const response = await api + .get(`/v2/buckets/a_malfprmed_bucket_id/files/${fakeFileId}/mirrors`) + .set(AuthorizationHeader); + expect(response.status).toBe(400); + expect(response.body.error).toBe('Bucket id is malformed'); + }); + it('Mising fileId', async () => { + const response = await api + .get(`/v2/buckets/${bucketId}/files/a_malformed_file_id/mirrors`) + .set(AuthorizationHeader); + expect(response.status).toBe(400); + expect(response.body.error).toBe('File id is malformed'); + }); + }); +}); diff --git a/tests/lib/e2e/bucket/getFiles.e2e-spec.ts b/tests/lib/e2e/bucket/getFiles.e2e-spec.ts new file mode 100644 index 00000000..bf3876fd --- /dev/null +++ b/tests/lib/e2e/bucket/getFiles.e2e-spec.ts @@ -0,0 +1,89 @@ +import { uploadRandomFile } from './setup'; +import { waitForBridgeToBeUp } from '../setup'; +import { + api, + AuthorizationHeader, + registerSampleUserAndGetBucketId, +} from '../setup'; + +let bucketId: string; +let GET_FILES_PATH: string; + +export const getFilesEndpoint = (bucketId: string) => + `/buckets/${bucketId}/bulk-files`; + +let fileIds: string[] = []; + +const longTimeout = 50_000; + +describe('Finish Upload v2', () => { + beforeAll(async () => { + await waitForBridgeToBeUp(); + bucketId = await registerSampleUserAndGetBucketId(); + GET_FILES_PATH = getFilesEndpoint(bucketId); + + const uploadFileResponse = await uploadRandomFile(bucketId); + fileIds.push(uploadFileResponse.body.id); + }, longTimeout); + + describe('Validation Get Files', () => { + it('No fileids', async () => { + const response = await api.get(GET_FILES_PATH).set(AuthorizationHeader); + expect(response.status).toBe(400); + }); + + it('Invalid fileid - not a mongoId', async () => { + const response = await api + .get(`${GET_FILES_PATH}?fileIds=324342jfdf2`) + .set(AuthorizationHeader); + expect(response.status).toBe(500); + }); + + it('Returns null for non existing file ids', async () => { + const response = await api + .get(`${GET_FILES_PATH}?fileIds=72b814bf3cde6dcc6f6c9a7b`) + .set(AuthorizationHeader); + expect(response.status).toBe(200); + expect(response.body).toHaveLength(1); + // Do we want to return null for non existent file ids? + expect(response.body[0]).toBeNull(); + }); + }); + + describe('Functionality', () => { + it('One valid FileId other non existing', async () => { + const response = await api + .get(`${GET_FILES_PATH}?fileIds=${fileIds[0]},72b814bf3cde6dcc6f6c9a7b`) + .set(AuthorizationHeader); + expect(response.status).toBe(200); + expect(response.body[0].fileId).toBe(fileIds[0]); + expect(response.body[1]).toBeNull(); + }); + + it('Gets the correct url', async () => { + const response = await api + .get(`${GET_FILES_PATH}?fileIds=${fileIds[0]}`) + .set(AuthorizationHeader); + expect(response.status).toBe(200); + expect(response.body).toHaveLength(1); + expect(response.body[0].link).toContain('http'); + expect(response.body[0].fileId).toBe(fileIds[0]); + }); + + it('Works with two files', async () => { + const uploadFileResponse = await uploadRandomFile(bucketId); + + fileIds.push(uploadFileResponse.body.id); + const response = await api + .get(`${GET_FILES_PATH}?fileIds=${fileIds.join(',')}`) + .set(AuthorizationHeader); + expect(response.status).toBe(200); + expect(response.body).toHaveLength(2); + for (const [i, file] of response.body.entries()) { + expect(file.link).toContain('http'); + expect(file.fileId).toBe(fileIds[i]); + expect(file.index).toBeDefined(); + } + }); + }); +}); diff --git a/tests/lib/e2e/bucket/setup.ts b/tests/lib/e2e/bucket/setup.ts new file mode 100644 index 00000000..5d1d8439 --- /dev/null +++ b/tests/lib/e2e/bucket/setup.ts @@ -0,0 +1,46 @@ +import crypto from 'crypto'; +import { finishesCorrectly } from './finishUpload.e2e-spec'; +import { startsCorrectly } from './startUpload.e2e-spec.test'; +import axios from 'axios'; + +export const uploadRandomFile = async (bucketId: string) => { + const { + body: { uploads }, + } = await startsCorrectly(bucketId, { + uploads: [ + { + index: 0, + size: 1000, + }, + { + index: 1, + size: 30000, + }, + ], + }); + + for (const upload of uploads) { + const { url } = upload; + const file = crypto.randomBytes(50).toString('hex'); + await axios.put(url, file, { + headers: { + 'Content-Type': 'application/octet-stream', + }, + }); + } + + const index = crypto.randomBytes(32).toString('hex'); + return finishesCorrectly(bucketId, { + index, + shards: [ + { + hash: crypto.randomBytes(20).toString('hex'), + uuid: uploads[0].uuid, + }, + { + hash: crypto.randomBytes(20).toString('hex'), + uuid: uploads[1].uuid, + }, + ], + }); +}; diff --git a/tests/lib/e2e/bucket/startUpload.e2e-spec.test.ts b/tests/lib/e2e/bucket/startUpload.e2e-spec.test.ts new file mode 100644 index 00000000..c4af7055 --- /dev/null +++ b/tests/lib/e2e/bucket/startUpload.e2e-spec.test.ts @@ -0,0 +1,334 @@ +import { + api, + AuthorizationHeader, + registerSampleUserAndGetBucketId, + waitForBridgeToBeUp, +} from '../setup'; + +export const startUploadEndpoint = (bucketId: string) => + `/v2/buckets/${bucketId}/files/start`; + +let bucketId: string; +let START_UPLOAD_PATH: string; + +export const startsCorrectly = async ( + bucketId: string, + payload: { uploads: { index: number; size: number }[] }, + multiparts?: number +) => { + let endpoint = startUploadEndpoint(bucketId); + if (multiparts) { + endpoint = `${endpoint}?multiparts=${multiparts}`; + } + return api.post(endpoint).send(payload).set(AuthorizationHeader); +}; + +const longTimeout = 50_000; + +describe('Start Upload v2 Validation', () => { + beforeAll(async () => { + await waitForBridgeToBeUp(); + bucketId = await registerSampleUserAndGetBucketId(); + START_UPLOAD_PATH = startUploadEndpoint(bucketId); + }, longTimeout); + + describe('Validation Start Upload (non-multipart)', () => { + it('Non existing uploads array', async () => { + const response = await api + .post(START_UPLOAD_PATH) + .send({}) + .set(AuthorizationHeader); + + expect(response.status).toBe(400); + expect(response.body.error).toBe('Missing "uploads" field'); + }); + + it('Empty uploads array', async () => { + const response = await api + .post(START_UPLOAD_PATH) + .send({ + uploads: [], + }) + .set(AuthorizationHeader); + + expect(response.status).toBe(400); + expect(response.body.error).toBe('Uploads is empty'); + }); + + it('Uploads is not an array', async () => { + const response = await api + .post(START_UPLOAD_PATH) + .send({ + uploads: 'fdsfds', + }) + .set(AuthorizationHeader); + + expect(response.status).toBe(400); + expect(response.body.error).toBe('Uploads is not an array'); + }); + + it('Invalid size', async () => { + const response = await api + .post(START_UPLOAD_PATH) + .send({ + uploads: [ + { + index: 0, + size: 'this_should_be_a_number', + }, + ], + }) + .set(AuthorizationHeader); + + expect(response.status).toBe(400); + expect(response.body.error).toBe('Invalid size'); + }); + + it('Missing index', async () => { + const response = await api + .post(START_UPLOAD_PATH) + .send({ + uploads: [ + { + size: 3234, + }, + ], + }) + .set(AuthorizationHeader); + + expect(response.status).toBe(400); + expect(response.body.error).toBe('Invalid index'); + }); + + it('Missing size', async () => { + const response = await api + .post(START_UPLOAD_PATH) + .send({ + uploads: [ + { + index: 0, + }, + ], + }) + .set(AuthorizationHeader); + + expect(response.status).toBe(400); + expect(response.body.error).toBe('Invalid size'); + }); + + it('Negative index', async () => { + const response = await api + .post(START_UPLOAD_PATH) + .send({ + uploads: [ + { + index: -1, + size: 3234, + }, + ], + }) + .set(AuthorizationHeader); + + expect(response.status).toBe(400); + expect(response.body.error).toBe('Invalid index'); + }); + + it('Negative size', async () => { + const response = await api + .post(START_UPLOAD_PATH) + .send({ + uploads: [ + { + index: 0, + size: -3234, + }, + ], + }) + .set(AuthorizationHeader); + + expect(response.status).toBe(400); + expect(response.body.error).toBe('Invalid size'); + }); + }); + + describe('Validation Start Upload - Multipart', () => { + it('Invalid multipart value', async () => { + const response = await api + .post(`${START_UPLOAD_PATH}?multiparts=true`) + .send({ + uploads: [ + { + index: 0, + size: 3234, + }, + ], + }) + .set(AuthorizationHeader); + + expect(response.status).toBe(400); + expect(response.body.error).toBe('Invalid multiparts value'); + }); + + it('Negative multipart value', async () => { + const response = await api + .post(`${START_UPLOAD_PATH}?multiparts=-1`) + .send({ + uploads: [ + { + index: 0, + size: 3234, + }, + ], + }) + .set(AuthorizationHeader); + + expect(response.status).toBe(400); + expect(response.body.error).toBe('Invalid multiparts value'); + }); + }); + + describe('Functionality', () => { + describe('Normal (non-multipart)', () => { + it('Non existing bucket', async () => { + const response = await api + .post(startUploadEndpoint('f701d5cc906a6f7e294d50f7')) + .send({ + uploads: [ + { + index: 0, + size: 3234, + }, + ], + }) + .set(AuthorizationHeader); + + expect(response.status).toBe(404); + expect(response.body.error).toBe('Bucket not found'); + }); + + it('Duplicated indexes', async () => { + const response = await api + .post(START_UPLOAD_PATH) + .send({ + uploads: [ + { + index: 0, + size: 3234, + }, + { + index: 0, + size: 32334, + }, + ], + }) + .set(AuthorizationHeader); + + expect(response.status).toBe(409); + expect(response.body.error).toBe('Invalid upload indexes'); + }); + + it('Max space reached', async () => { + const response = await api + .post(START_UPLOAD_PATH) + .send({ + uploads: [ + { + index: 0, + size: 10_000 * 1024 * 1024, + }, + ], + }) + .set(AuthorizationHeader); + + expect(response.status).toBe(420); + expect(response.body.error).toBe('Max space used'); + }); + + it('startUpload works correctly', async () => { + const startUploadResponse = await startsCorrectly(bucketId, { + uploads: [ + { + index: 0, + size: 1000, + }, + { + index: 1, + size: 10000, + }, + ], + }); + + const { uploads } = startUploadResponse.body; + + let indexCounter = 0; + for (const upload of uploads) { + const { url, urls, index, uuid } = upload; + expect(url).toBeDefined(); + expect(url).toContain('http'); + expect(urls).toBeNull(); + expect(index).toEqual(indexCounter); + indexCounter += 1; + expect(uuid).toBeDefined(); + } + }); + }); + + describe('Multipart', () => { + it('Multipart on less than 100MB', async () => { + const response = await api + .post(`${START_UPLOAD_PATH}?multiparts=4`) + .send({ + uploads: [ + { + index: 0, + size: 1000, + }, + ], + }) + .set(AuthorizationHeader); + + expect(response.status).toBe(400); + expect(response.body.error).toBe( + 'Multipart is not allowed for small files' + ); + }); + + it('Multipart startUpload works correctly', async () => { + const multiparts = 3; + const startUploadResponse = await startsCorrectly( + bucketId, + { + uploads: [ + { + index: 0, + size: 100 * 1024 * 1024, + }, + { + index: 1, + size: 100 * 1024 * 1024, + }, + ], + }, + multiparts + ); + + const { uploads } = startUploadResponse.body; + + let indexCounter = 0; + for (const upload of uploads) { + const { url, urls, index, uuid, UploadId } = upload; + expect(url).toBeNull(); + expect(urls).toBeDefined(); + expect(urls.length).toEqual(3); + expect(uuid).toBeDefined(); + expect(UploadId).toBeDefined(); + expect(index).toBe(indexCounter); + indexCounter += 1; + for (const urlToUpload of urls) { + expect(urlToUpload).toContain('http'); + } + } + }); + }); + }); +}); diff --git a/tests/lib/e2e/jest-e2e.json b/tests/lib/e2e/jest-e2e.json new file mode 100644 index 00000000..e9d912f3 --- /dev/null +++ b/tests/lib/e2e/jest-e2e.json @@ -0,0 +1,9 @@ +{ + "moduleFileExtensions": ["js", "json", "ts"], + "rootDir": ".", + "testEnvironment": "node", + "testRegex": ".e2e-spec.ts$", + "transform": { + "^.+\\.(t|j)s$": "ts-jest" + } +} diff --git a/tests/lib/e2e/setup.ts b/tests/lib/e2e/setup.ts new file mode 100644 index 00000000..b493a997 --- /dev/null +++ b/tests/lib/e2e/setup.ts @@ -0,0 +1,66 @@ +import request from 'supertest'; +import crypto from 'crypto'; + +const BRIDGE_URL = 'http://localhost:6382'; + +const email = 'test1@yahoo.com'; +const password = 'Acvx2.df28dfsZs]'; +const hex = crypto.createHash('sha256').update(password).digest('hex'); +export const AuthorizationHeader = { + Authorization: `Basic ${Buffer.from(`${email}:${hex}`).toString('base64')}`, +}; +export const api = request(BRIDGE_URL); + +const TRIES_UNTIL_BRIDGE_IS_READY = 10; +const INTERVAL_FOR_RETRY = 3000; + +export const waitForBridgeToBeUp = (): Promise => { + return new Promise((resolve, reject) => { + let tries = 0; + const interval = setInterval(async () => { + try { + console.log('Checking if bridge is up...'); + await api.get('/'); + console.log('Bridge is up!'); + clearInterval(interval); + resolve(); + } catch (err) { + tries += 1; + + if (tries > TRIES_UNTIL_BRIDGE_IS_READY) { + clearInterval(interval); + reject(new Error('Too many tries to connect to Bridge')); + } else { + console.log( + `Bridge Api not ready yet, waiting: ${ + INTERVAL_FOR_RETRY / 1000 + } more seconds` + ); + } + } + }, INTERVAL_FOR_RETRY); + }); +}; + +export async function registerSampleUserAndGetBucketId() { + const signupResponse = await api.post('/users').send({ + email, + password: hex, + }); + + if (signupResponse.body?.error === 'Email is already registered') { + const existingBuckets = await api.get('/buckets').set(AuthorizationHeader); + if (existingBuckets.body.length === 0) { + throw new Error('No buckets found'); + } + const bucketId = existingBuckets.body[0].id; + return bucketId; + } + + const createBucketResponse = await api + .post('/buckets') + .set(AuthorizationHeader); + const bucketId = createBucketResponse.body.id; + + return bucketId; +} diff --git a/yarn.lock b/yarn.lock index 0e238fda..0ce89b06 100644 --- a/yarn.lock +++ b/yarn.lock @@ -741,6 +741,11 @@ dependencies: "@types/node" "*" +"@types/cookiejar@*": + version "2.1.2" + resolved "https://registry.yarnpkg.com/@types/cookiejar/-/cookiejar-2.1.2.tgz#66ad9331f63fe8a3d3d9d8c6e3906dd10f6446e8" + integrity sha512-t73xJJrvdTjXrn4jLS9VSGRbz0nUY3cl2DMGDU48lKl+HR9dbbjW2A9r3g40VA++mQpy6uuHg33gy7du2BKpog== + "@types/express-serve-static-core@^4.17.18": version "4.17.29" resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.29.tgz#2a1795ea8e9e9c91b4a4bbe475034b20c1ec711c" @@ -908,6 +913,21 @@ resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.1.tgz#20f18294f797f2209b5f65c8e3b5c8e8261d127c" integrity sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw== +"@types/superagent@*": + version "4.1.15" + resolved "https://registry.yarnpkg.com/@types/superagent/-/superagent-4.1.15.tgz#63297de457eba5e2bc502a7609426c4cceab434a" + integrity sha512-mu/N4uvfDN2zVQQ5AYJI/g4qxn2bHB6521t1UuH09ShNWjebTqN0ZFuYK9uYjcgmI0dTQEs+Owi1EO6U0OkOZQ== + dependencies: + "@types/cookiejar" "*" + "@types/node" "*" + +"@types/supertest@^2.0.12": + version "2.0.12" + resolved "https://registry.yarnpkg.com/@types/supertest/-/supertest-2.0.12.tgz#ddb4a0568597c9aadff8dbec5b2e8fddbe8692fc" + integrity sha512-X3HPWTwXRerBZS7Mo1k6vMVR1Z6zmJcDVn5O/31whe0tnjE4te6ZJSJGq1RiqHPjzPdMTfjCFogDJmwng9xHaQ== + dependencies: + "@types/superagent" "*" + "@types/uuid@^8.3.4": version "8.3.4" resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.3.4.tgz#bd86a43617df0594787d38b735f55c805becf1bc" @@ -1212,6 +1232,11 @@ array.prototype.reduce@^1.0.4: es-array-method-boxes-properly "^1.0.0" is-string "^1.0.7" +asap@^2.0.0: + version "2.0.6" + resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46" + integrity sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA== + asn1@~0.2.3: version "0.2.6" resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.6.tgz#0d3a7bb6e64e02a90c0303b31f292868ea09a08d" @@ -2004,7 +2029,7 @@ commondir@^1.0.1: resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" integrity sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg== -component-emitter@^1.2.0: +component-emitter@^1.2.0, component-emitter@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0" integrity sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg== @@ -2087,7 +2112,7 @@ cookie@0.5.0: resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.5.0.tgz#d1f5d71adec6558c58f389987c366aa47e994f8b" integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw== -cookiejar@^2.1.0: +cookiejar@^2.1.0, cookiejar@^2.1.3: version "2.1.3" resolved "https://registry.yarnpkg.com/cookiejar/-/cookiejar-2.1.3.tgz#fc7a6216e408e74414b90230050842dacda75acc" integrity sha512-JxbCBUdrfr6AQjOXrxoTvAMJO4HBTUIlBzslcJPAz+/KT8yk53fXun51u+RenNYvad/+Vc2DIz5o9UxlCDymFQ== @@ -2254,7 +2279,7 @@ debug@3.2.6: dependencies: ms "^2.1.1" -debug@4, debug@4.x, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1: +debug@4, debug@4.x, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.3.4: version "4.3.4" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== @@ -2380,6 +2405,14 @@ detect-node@^2.0.4: resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.1.0.tgz#c9c70775a49c3d03bc2c06d9a73be550f978f8b1" integrity sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g== +dezalgo@1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/dezalgo/-/dezalgo-1.0.3.tgz#7f742de066fc748bc8db820569dddce49bf0d456" + integrity sha512-K7i4zNfT2kgQz3GylDw40ot9GAE47sFZ9EXHFSPP6zONLgH6kWXE0KWJchkbQJLBkRazq4APwZ4OwiFFlT95OQ== + dependencies: + asap "^2.0.0" + wrappy "1" + diff-sequences@^27.5.1: version "27.5.1" resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-27.5.1.tgz#eaecc0d327fd68c8d9672a1e64ab8dccb2ef5327" @@ -2996,7 +3029,7 @@ fast-levenshtein@^2.0.6, fast-levenshtein@~2.0.6: resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== -fast-safe-stringify@^2.0.6: +fast-safe-stringify@^2.0.6, fast-safe-stringify@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz#c406a83b6e70d9e35ce3b30a81141df30aeba884" integrity sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA== @@ -3174,6 +3207,15 @@ form-data@^3.0.0: combined-stream "^1.0.8" mime-types "^2.1.12" +form-data@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452" + integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + mime-types "^2.1.12" + form-data@~2.1.1: version "2.1.4" resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.1.4.tgz#33c183acf193276ecaa98143a69e94bfee1750d1" @@ -3197,6 +3239,16 @@ formidable@^1.2.0, formidable@^1.2.1: resolved "https://registry.yarnpkg.com/formidable/-/formidable-1.2.6.tgz#d2a51d60162bbc9b4a055d8457a7c75315d1a168" integrity sha512-KcpbcpuLNOwrEjnbpMC0gS+X8ciDoZE1kkqzat4a8vrprf+s9pKNQ/QIwWfbfs4ltgmFl3MD177SNTkve3BwGQ== +formidable@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/formidable/-/formidable-2.0.1.tgz#4310bc7965d185536f9565184dee74fbb75557ff" + integrity sha512-rjTMNbp2BpfQShhFbR3Ruk3qk2y9jKpvMW78nJgx8QKtxjDVrwbZG+wvDOmVbifHyOUOQJXxqEy6r0faRrPzTQ== + dependencies: + dezalgo "1.0.3" + hexoid "1.0.0" + once "1.4.0" + qs "6.9.3" + forwarded@0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" @@ -3596,6 +3648,11 @@ helmet@^3.20.0: referrer-policy "1.2.0" x-xss-protection "1.3.0" +hexoid@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/hexoid/-/hexoid-1.0.0.tgz#ad10c6573fb907de23d9ec63a711267d9dc9bc18" + integrity sha512-QFLV0taWQOZtvIRIAdBChesmogZrtuXvVWsFHZTk2SU+anspqZ2vMnoLg7IE1+Uk16N19APic1BuF8bC8c2m5g== + hide-powered-by@1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/hide-powered-by/-/hide-powered-by-1.1.0.tgz#be3ea9cab4bdb16f8744be873755ca663383fa7a" @@ -5323,7 +5380,7 @@ mime@1.6.0, mime@^1.3.4, mime@^1.4.1: resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== -mime@^2.4.0, mime@^2.4.3: +mime@2.6.0, mime@^2.4.0, mime@^2.4.3: version "2.6.0" resolved "https://registry.yarnpkg.com/mime/-/mime-2.6.0.tgz#a2a682a95cd4d0cb1d6257e28f83da7e35800367" integrity sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg== @@ -7485,6 +7542,31 @@ superagent@^3.5.0: qs "^6.5.1" readable-stream "^2.3.5" +superagent@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/superagent/-/superagent-8.0.0.tgz#2ea4587df4b81ef023ec01ebc6e1bcb9e2344cb6" + integrity sha512-iudipXEel+SzlP9y29UBWGDjB+Zzag+eeA1iLosaR2YHBRr1Q1kC29iBrF2zIVD9fqVbpZnXkN/VJmwFMVyNWg== + dependencies: + component-emitter "^1.3.0" + cookiejar "^2.1.3" + debug "^4.3.4" + fast-safe-stringify "^2.1.1" + form-data "^4.0.0" + formidable "^2.0.1" + methods "^1.1.2" + mime "2.6.0" + qs "^6.10.3" + readable-stream "^3.6.0" + semver "^7.3.7" + +supertest@^6.2.4: + version "6.2.4" + resolved "https://registry.yarnpkg.com/supertest/-/supertest-6.2.4.tgz#3dcebe42f7fd6f28dd7ac74c6cba881f7101b2f0" + integrity sha512-M8xVnCNv+q2T2WXVzxDECvL2695Uv2uUj2O0utxsld/HRyJvOU8W9f1gvsYxSNU4wmIe0/L/ItnpU4iKq0emDA== + dependencies: + methods "^1.1.2" + superagent "^8.0.0" + supports-color@6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-6.0.0.tgz#76cfe742cf1f41bb9b1c29ad03068c05b4c0e40a"