Skip to content

Commit c859be7

Browse files
Add corner case test coverage, and revert unneeded format changes
Issue: CLDSRV-699
1 parent e81db96 commit c859be7

File tree

3 files changed

+120
-19
lines changed

3 files changed

+120
-19
lines changed

lib/api/apiUtils/object/abortMultipartUpload.js

Lines changed: 15 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ function abortMultipartUpload(authInfo, bucketName, objectKey, uploadId, log,
2929

3030
async.waterfall([
3131
function checkDestBucketVal(next) {
32-
// FIX: Call the function from the imported module object.
3332
metadataUtils.standardMetadataValidateBucketAndObj(metadataValParams, authzIdentityResult, log,
3433
(err, destinationBucket, objectMD) => {
3534
if (err) {
@@ -67,21 +66,21 @@ function abortMultipartUpload(authInfo, bucketName, objectKey, uploadId, log,
6766
// eslint-disable-next-line no-param-reassign
6867
delete request.actionImplicitDenies;
6968
return data.abortMPU(objectKey, uploadId, location, bucketName,
70-
request, destBucket, locationConstraintCheck, log,
71-
(err, skipDataDelete) => {
72-
// eslint-disable-next-line no-param-reassign
73-
request.actionImplicitDenies = originalIdentityAuthzResults;
74-
if (err) {
75-
log.error('error aborting MPU', { error: err });
76-
return next(err, destBucket);
77-
}
78-
// for Azure and GCP we do not need to delete data
79-
// for all other backends, skipDataDelete will be set to false
80-
return next(null, mpuBucket, destBucket, objectMD, skipDataDelete);
81-
});
69+
request, destBucket, locationConstraintCheck, log,
70+
(err, skipDataDelete) => {
71+
// eslint-disable-next-line no-param-reassign
72+
request.actionImplicitDenies = originalIdentityAuthzResults;
73+
if (err) {
74+
log.error('error aborting MPU', { error: err });
75+
return next(err, destBucket);
76+
}
77+
// for Azure and GCP we do not need to delete data
78+
// for all other backends, skipDataDelete will be set to false
79+
return next(null, mpuBucket, destBucket, objectMD, skipDataDelete);
80+
});
8281
},
8382
function getPartLocations(mpuBucket, destBucket, objectMD, skipDataDelete,
84-
next) {
83+
next) {
8584
services.getMPUparts(mpuBucket.getName(), uploadId, log,
8685
(err, result) => {
8786
if (err) {
@@ -90,7 +89,7 @@ function abortMultipartUpload(authInfo, bucketName, objectKey, uploadId, log,
9089
}
9190
const storedParts = result.Contents;
9291
return next(null, mpuBucket, storedParts, destBucket, objectMD,
93-
skipDataDelete);
92+
skipDataDelete);
9493
});
9594
},
9695
// During Abort, we dynamically detect if the previous CompleteMPU call
@@ -123,7 +122,7 @@ function abortMultipartUpload(authInfo, bucketName, objectKey, uploadId, log,
123122
// Otherwise, list all versions to find one with a matching uploadId.
124123
return services.findObjectVersionByUploadId(bucketName, objectKey, uploadId, log, (err, foundVersion) => {
125124
if (err) {
126-
log.error('error finding object version by uploadId, proceeding without cleanup', {
125+
log.warn('error finding object version by uploadId, proceeding without cleanup', {
127126
error: err,
128127
method: 'abortMultipartUpload.ensureCleanupIsRequired',
129128
});

tests/unit/api/apiUtils/object/abortMultipartUpload.spec.js

Lines changed: 75 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,21 @@ describe('abortMultipartUpload', () => {
124124
}, abortRequest);
125125
});
126126
});
127+
128+
it('should return error if data backend fails to abort', done => {
129+
const testError = new Error('Data backend abort failed');
130+
// This stub is now more explicit to avoid side-effects.
131+
data.abortMPU.callsFake((objKey, upId, loc, bucket, req, destB, locCheckFn, log, cb) => {
132+
cb(testError);
133+
});
134+
createBucketAndMPU(false, (err, uploadId) => {
135+
assert.ifError(err);
136+
abortMultipartUpload(authInfo, bucketName, objectKey, uploadId, log, err => {
137+
assert.deepStrictEqual(err, testError);
138+
done();
139+
}, { ...abortRequest, query: { uploadId } });
140+
});
141+
});
127142
});
128143

129144
describe('with multipart upload parts', () => {
@@ -182,6 +197,45 @@ describe('abortMultipartUpload', () => {
182197
});
183198
});
184199

200+
it('should NOT search for orphans on non-versioned bucket with mismatched master uploadId', done => {
201+
const standardMetadataValidateStub = sinon.stub(metadataUtils, 'standardMetadataValidateBucketAndObj');
202+
const mockBucket = {
203+
isVersioningEnabled: () => false,
204+
getOwner: () => 'testCanonicalId',
205+
getName: () => bucketName,
206+
};
207+
const mockMasterMD = { 'uploadId': 'master-id' };
208+
standardMetadataValidateStub.yields(null, mockBucket, mockMasterMD);
209+
210+
abortMultipartUpload(authInfo, bucketName, objectKey, 'abort-id', log, err => {
211+
assert.ifError(err);
212+
sinon.assert.notCalled(findObjectVersionStub);
213+
done();
214+
}, { ...abortRequest, query: { uploadId: 'abort-id' } });
215+
});
216+
217+
it('should proceed without cleanup if finding object version fails', done => {
218+
const testError = new Error('Find version failed');
219+
findObjectVersionStub.yields(testError);
220+
const deleteObjectMDStub = sinon.stub(metadata, 'deleteObjectMD').yields(null);
221+
222+
const standardMetadataValidateStub = sinon.stub(metadataUtils, 'standardMetadataValidateBucketAndObj');
223+
const mockBucket = {
224+
isVersioningEnabled: () => true,
225+
getOwner: () => 'testCanonicalId',
226+
getName: () => bucketName,
227+
};
228+
const mockMasterMD = { 'uploadId': 'master-id' };
229+
standardMetadataValidateStub.yields(null, mockBucket, mockMasterMD);
230+
231+
abortMultipartUpload(authInfo, bucketName, objectKey, 'abort-id', log, err => {
232+
assert.ifError(err);
233+
sinon.assert.calledOnce(findObjectVersionStub);
234+
sinon.assert.notCalled(deleteObjectMDStub);
235+
done();
236+
}, { ...abortRequest, query: { uploadId: 'abort-id' } });
237+
});
238+
185239
it('should delete the correct orphaned object version', done => {
186240
const deleteObjectMDStub = sinon.stub(metadata, 'deleteObjectMD').yields(null);
187241
findObjectVersionStub.yields(null, { uploadId: 'abort-id', versionId: 'orphan-vid' });
@@ -192,9 +246,7 @@ describe('abortMultipartUpload', () => {
192246
getOwner: () => 'testCanonicalId',
193247
getName: () => bucketName,
194248
};
195-
const mockMasterMD = {
196-
'uploadId': 'master-id',
197-
};
249+
const mockMasterMD = { 'uploadId': 'master-id' };
198250
standardMetadataValidateStub.yields(null, mockBucket, mockMasterMD);
199251

200252
abortMultipartUpload(authInfo, bucketName, objectKey, 'abort-id', log, err => {
@@ -204,5 +256,25 @@ describe('abortMultipartUpload', () => {
204256
done();
205257
}, { ...abortRequest, query: { uploadId: 'abort-id' } });
206258
});
259+
260+
it('should proceed if orphaned object version is already deleted (NoSuchKey)', done => {
261+
const deleteObjectMDStub = sinon.stub(metadata, 'deleteObjectMD').yields(errors.NoSuchKey);
262+
findObjectVersionStub.yields(null, { uploadId: 'abort-id', versionId: 'orphan-vid' });
263+
264+
const standardMetadataValidateStub = sinon.stub(metadataUtils, 'standardMetadataValidateBucketAndObj');
265+
const mockBucket = {
266+
isVersioningEnabled: () => true,
267+
getOwner: () => 'testCanonicalId',
268+
getName: () => bucketName,
269+
};
270+
const mockMasterMD = { 'uploadId': 'master-id' };
271+
standardMetadataValidateStub.yields(null, mockBucket, mockMasterMD);
272+
273+
abortMultipartUpload(authInfo, bucketName, objectKey, 'abort-id', log, err => {
274+
assert.ifError(err);
275+
sinon.assert.calledOnce(deleteObjectMDStub);
276+
done();
277+
}, { ...abortRequest, query: { uploadId: 'abort-id' } });
278+
});
207279
});
208280
});

tests/unit/lib/services.spec.js

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,5 +60,35 @@ describe('services', () => {
6060
done();
6161
});
6262
});
63+
64+
it('should handle error from getObjectListing', done => {
65+
const testError = new Error('listing failed');
66+
getObjectListingStub.yields(testError);
67+
68+
services.findObjectVersionByUploadId(bucketName, objectKey, 'any-upload-id', log, err => {
69+
assert.deepStrictEqual(err, testError);
70+
sinon.assert.calledOnce(getObjectListingStub);
71+
done();
72+
});
73+
});
74+
75+
it('should find a version with the matching uploadId', done => {
76+
const uploadIdToFind = 'the-correct-upload-id';
77+
const correctVersionValue = { uploadId: uploadIdToFind, data: 'this is it' };
78+
const versions = [
79+
{ key: objectKey, value: { uploadId: 'some-other-id' } },
80+
// Version with a different key but same uploadId to test the key check
81+
{ key: 'another-object-key', value: { uploadId: uploadIdToFind } },
82+
{ key: objectKey, value: correctVersionValue },
83+
];
84+
getObjectListingStub.yields(null, { Versions: versions, IsTruncated: false });
85+
86+
services.findObjectVersionByUploadId(bucketName, objectKey, uploadIdToFind, log, (err, foundVersion) => {
87+
assert.ifError(err);
88+
sinon.assert.calledOnce(getObjectListingStub);
89+
assert.deepStrictEqual(foundVersion, correctVersionValue);
90+
done();
91+
});
92+
});
6393
});
6494
});

0 commit comments

Comments
 (0)