From 654cf17531fea5614da53b032594ce4acf9a030e Mon Sep 17 00:00:00 2001 From: Andres Pinto <143480783+apsantiso@users.noreply.github.com> Date: Tue, 28 Apr 2026 08:15:14 +0200 Subject: [PATCH 1/2] refactor: remove parts from finish endpoint --- lib/core/shards/Shard.ts | 1 - lib/server/routes/buckets.js | 10 ++++----- tests/lib/e2e/buckets/files.e2e-spec.ts | 29 +++++++++++++++++++++++++ 3 files changed, 33 insertions(+), 7 deletions(-) diff --git a/lib/core/shards/Shard.ts b/lib/core/shards/Shard.ts index 46f1d9f96..056b8487f 100644 --- a/lib/core/shards/Shard.ts +++ b/lib/core/shards/Shard.ts @@ -25,7 +25,6 @@ export type ShardWithPossibleMultiUpload = Pick< 'hash' | 'uuid' > & { UploadId?: string; - parts?: { PartNumber: number; ETag: string }[]; }; export type ShardWithMultiUpload = Required; diff --git a/lib/server/routes/buckets.js b/lib/server/routes/buckets.js index 0611895d2..e2c64e3a4 100644 --- a/lib/server/routes/buckets.js +++ b/lib/server/routes/buckets.js @@ -1275,18 +1275,16 @@ BucketsRouter.prototype.finishUpload = async function (req, res, next) { return next(new errors.BadRequestError('Shards is not an array')); } - for (const { uuid, hash, UploadId, parts } of shards) { + for (const { uuid, hash, UploadId } of shards) { if (!uuidValidate(uuid)) { return next(new errors.BadRequestError('Invalid UUID')); } if (!hash) { 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')); - } - if (parts && !UploadId) { - return next(new errors.BadRequestError('For multipart: must provide also the UploadId for this upload')); + + if (UploadId !== undefined && (typeof UploadId !== 'string' || UploadId.length === 0)) { + return next(new errors.BadRequestError('Invalid UploadId')); } } diff --git a/tests/lib/e2e/buckets/files.e2e-spec.ts b/tests/lib/e2e/buckets/files.e2e-spec.ts index 5532ac127..91064f14f 100644 --- a/tests/lib/e2e/buckets/files.e2e-spec.ts +++ b/tests/lib/e2e/buckets/files.e2e-spec.ts @@ -245,6 +245,35 @@ describe('Bridge E2E Tests', () => { }); + it('When a user finishes a multipart upload without providing UploadId, then it should fail', async () => { + const { body: { id: bucketId } } = await testServer + .post('/buckets') + .set('Authorization', getAuth(testUser)) + .expect(201) + + const MB100 = 100 * 1024 * 1024 + const { body: { uploads } } = await testServer + .post(`/v2/buckets/${bucketId}/files/start?multiparts=2`) + .set('Authorization', getAuth(testUser)) + .send({ uploads: [{ index: 0, size: MB100 / 2 }, { index: 1, size: MB100 / 2 }] }) + + const index = crypto.randomBytes(32).toString('hex'); + + // finish without UploadId + const response = await testServer + .post(`/v2/buckets/${bucketId}/files/finish`) + .set('Authorization', getAuth(testUser)) + .send({ + index, + shards: (uploads as any[]).map((upload) => ({ + hash: crypto.randomBytes(20).toString('hex'), + uuid: upload.uuid, + })), + }) + + expect(response.status).toBe(400); + }); + it('When an user finished to upload a file, then file size should be added to the used space', async () => { // Arrange: Create a user with some used space const originalUser = await createTestUser(); From 77e76d1a54e1ee993dd303d9bbe2ff340a54bb42 Mon Sep 17 00:00:00 2001 From: Andres Pinto <143480783+apsantiso@users.noreply.github.com> Date: Mon, 4 May 2026 11:07:30 +0200 Subject: [PATCH 2/2] chore: added e2e to check upload is not completed when uploadId is not provided --- tests/lib/e2e/buckets/files.e2e-spec.ts | 44 +++++++++++++++++++++++-- 1 file changed, 41 insertions(+), 3 deletions(-) diff --git a/tests/lib/e2e/buckets/files.e2e-spec.ts b/tests/lib/e2e/buckets/files.e2e-spec.ts index 91064f14f..733656fe1 100644 --- a/tests/lib/e2e/buckets/files.e2e-spec.ts +++ b/tests/lib/e2e/buckets/files.e2e-spec.ts @@ -15,6 +15,7 @@ describe('Bridge E2E Tests', () => { let testUser: User let axiosGetStub: sinon.SinonStub + let axiosPostStub: sinon.SinonStub const databaseConnection = new StorageDbManager(); beforeAll(async () => { @@ -47,10 +48,12 @@ describe('Bridge E2E Tests', () => { if (url.includes('/v2/download/link')) return { data: { result: FAKE_DOWNLOAD_URL } } if (url.includes('exists')) return { status: 200 } }) + axiosPostStub = sinon.stub(axios, 'post').resolves() }) afterEach(() => { axiosGetStub.restore() + axiosPostStub.restore() }) describe('File Management v2', () => { @@ -221,7 +224,7 @@ describe('Bridge E2E Tests', () => { getAuth(testUser), bucketId, index, - (uploads as any[]).map((upload) => ({ hash: crypto.randomBytes(20).toString('hex'), uuid: upload.uuid })), + (uploads as any[]).map((upload) => ({ UploadId: crypto.randomBytes(20).toString('hex'), hash: crypto.randomBytes(20).toString('hex'), uuid: upload.uuid })), { shardSize: MB100 / 2 }, ) @@ -230,6 +233,9 @@ describe('Bridge E2E Tests', () => { const body = responseComplete.body; + expect( + axiosPostStub.calledWithMatch(sinon.match((url: string) => url.includes('/v2/upload-multipart-complete/link/'))) + ).toBe(true); expect(body).toMatchObject({ bucket: bucketId, created: expect.any(String), @@ -245,7 +251,7 @@ describe('Bridge E2E Tests', () => { }); - it('When a user finishes a multipart upload without providing UploadId, then it should fail', async () => { + it('When a user finishes a multipart and provides an invalid uploadId, then it should fail', async () => { const { body: { id: bucketId } } = await testServer .post('/buckets') .set('Authorization', getAuth(testUser)) @@ -259,13 +265,13 @@ describe('Bridge E2E Tests', () => { const index = crypto.randomBytes(32).toString('hex'); - // finish without UploadId const response = await testServer .post(`/v2/buckets/${bucketId}/files/finish`) .set('Authorization', getAuth(testUser)) .send({ index, shards: (uploads as any[]).map((upload) => ({ + UploadId: '', hash: crypto.randomBytes(20).toString('hex'), uuid: upload.uuid, })), @@ -274,6 +280,38 @@ describe('Bridge E2E Tests', () => { expect(response.status).toBe(400); }); + it('When a user finishes to upload a file with multipart upload and uploadId is not provided, then upload is not completed and fails', async () => { + // Arrange: Create a bucket + const { body: { id: bucketId } } = await testServer + .post('/buckets') + .set('Authorization', getAuth(testUser)) + + // Arrange: start the upload + const MB100 = 100 * 1024 * 1024 + const response = await testServer.post(`/v2/buckets/${bucketId}/files/start?multiparts=2`) + .set('Authorization', getAuth(testUser)) + .send({ uploads: [{ index: 0, size: MB100 / 2, }, { index: 1, size: MB100 / 2, }] }) + + const { uploads } = response.body; + + // Act: finish the upload without UploadId in shards + const index = crypto.randomBytes(32).toString('hex'); + const responseComplete = await finishUpload( + getAuth(testUser), + bucketId, + index, + (uploads as any[]).map((upload) => ({ hash: crypto.randomBytes(20).toString('hex'), uuid: upload.uuid })), + // Upload is not completed, so meta is not found in the farmer so we return null to simulate that scenario + { shardSize: MB100 / 2, farmerReportedSize: null }, + ) + + // Assert: complete multipart was not called on the farmer and upload failed + expect( + axiosPostStub.calledWithMatch(sinon.match((url: string) => url.includes('/v2/upload-multipart-complete/link/'))) + ).toBe(false); + expect(responseComplete.status).toBe(404); + }); + it('When an user finished to upload a file, then file size should be added to the used space', async () => { // Arrange: Create a user with some used space const originalUser = await createTestUser();