diff --git a/lib/api/objectGet.js b/lib/api/objectGet.js index 833dd0d7c0..319fe67db4 100644 --- a/lib/api/objectGet.js +++ b/lib/api/objectGet.js @@ -41,6 +41,12 @@ function objectGet(authInfo, request, returnTagCount, log, callback) { const bucketName = request.bucketName; const objectKey = request.objectKey; + const checksumMode = request.headers['x-amz-checksum-mode']; + if (checksumMode !== undefined && checksumMode !== 'ENABLED') { + log.debug('invalid x-amz-checksum-mode', { checksumMode }); + return callback(errors.InvalidArgument); + } + // returns name of location to get from and key if successful const locCheckResult = locationHeaderCheck(request.headers, objectKey, bucketName); @@ -145,6 +151,7 @@ function objectGet(authInfo, request, returnTagCount, log, callback) { request.serverAccessLog.objectSize = objLength; } let byteRange; + let partNumber = null; const streamingParams = {}; if (request.headers.range) { const { range, error } = parseRange(request.headers.range, @@ -211,8 +218,6 @@ function objectGet(authInfo, request, returnTagCount, log, callback) { if (dataLocator[0] && dataLocator[0].dataStoreType === 'azure') { dataLocator[0].azureStreamingOptions = streamingParams; } - - let partNumber = null; if (request.query && request.query.partNumber !== undefined) { if (byteRange) { const error = errorInstances.InvalidRequest @@ -301,6 +306,16 @@ function objectGet(authInfo, request, returnTagCount, log, callback) { dataLocator = setPartRanges(dataLocator, byteRange); } } + // Checksum is not returned for partNumber requests because per-part + // checksums are not yet stored (S3C-11073). + if (checksumMode === 'ENABLED' && !byteRange && !partNumber) { + const checksum = objMD.checksum; + if (checksum) { + responseMetaHeaders[`x-amz-checksum-${checksum.checksumAlgorithm}`] + = checksum.checksumValue; + responseMetaHeaders['x-amz-checksum-type'] = checksum.checksumType; + } + } // Check KMS Key access and usability before checking data // diff with AWS: for empty object (no dataLocator) KMS not checked return async.each(dataLocator || [], diff --git a/lib/api/objectHead.js b/lib/api/objectHead.js index 2d4c524f21..4ca8786a8c 100644 --- a/lib/api/objectHead.js +++ b/lib/api/objectHead.js @@ -32,6 +32,12 @@ function objectHead(authInfo, request, log, callback) { const bucketName = request.bucketName; const objectKey = request.objectKey; + const checksumMode = request.headers['x-amz-checksum-mode']; + if (checksumMode !== undefined && checksumMode !== 'ENABLED') { + log.debug('invalid x-amz-checksum-mode', { checksumMode }); + return callback(errors.InvalidArgument); + } + const decodedVidResult = decodeVersionId(request.query); if (decodedVidResult instanceof Error) { log.trace('invalid versionId query', { @@ -97,6 +103,18 @@ function objectHead(authInfo, request, log, callback) { const responseHeaders = collectResponseHeaders(objMD, corsHeaders, verCfg); + const partNumber = getPartNumber(request.query); + // Checksum is not returned for partNumber requests because per-part + // checksums are not yet stored (S3C-11073). + if (checksumMode === 'ENABLED' && !partNumber) { + const checksum = objMD.checksum; + if (checksum) { + responseHeaders[`x-amz-checksum-${checksum.checksumAlgorithm}`] + = checksum.checksumValue; + responseHeaders['x-amz-checksum-type'] = checksum.checksumType; + } + } + setExpirationHeaders(responseHeaders, { lifecycleConfig: bucket.getLifecycleConfiguration(), objectParams: { @@ -135,7 +153,6 @@ function objectHead(authInfo, request, log, callback) { `bytes ${range[0]}-${range[1]}/${objLength}`; } } - const partNumber = getPartNumber(request.query); if (partNumber !== undefined) { if (byteRange) { const error = errorInstances.InvalidRequest diff --git a/tests/functional/aws-node-sdk/test/object/get.js b/tests/functional/aws-node-sdk/test/object/get.js index f8182ac36a..8ca62978cb 100644 --- a/tests/functional/aws-node-sdk/test/object/get.js +++ b/tests/functional/aws-node-sdk/test/object/get.js @@ -22,6 +22,13 @@ const { const withV4 = require('../support/withV4'); const BucketUtility = require('../../lib/utility/bucket-util'); const changeObjectLock = require('../../../../utilities/objectLock-util'); +const { algorithms } = require('../../../../../lib/api/apiUtils/integrity/validateChecksums'); + +const { crc64NvmeCrtContainer } = require('@aws-sdk/middleware-flexible-checksums'); +if (crc64NvmeCrtContainer) { + const { CrtCrc64Nvme } = require('@aws-sdk/crc64-nvme-crt'); + crc64NvmeCrtContainer.CrtCrc64Nvme = CrtCrc64Nvme; +} const changeLockPromise = promisify(changeObjectLock); @@ -1121,3 +1128,102 @@ describeSkipIfCeph('GET object with object lock', () => { }); }); }); + +describe('GET object checksum mode', () => { + withV4(sigCfg => { + let bucketUtil; + let s3; + const checksumBucket = 'checksum-getobject-test'; + const checksumKey = 'checksum-test-object'; + const body = Buffer.from('checksum test body'); + + const expectedDigests = {}; + + const checksumAlgorithms = [ + { algorithm: 'SHA256', responseField: 'ChecksumSHA256', internalName: 'sha256' }, + { algorithm: 'SHA1', responseField: 'ChecksumSHA1', internalName: 'sha1' }, + { algorithm: 'CRC32', responseField: 'ChecksumCRC32', internalName: 'crc32' }, + { algorithm: 'CRC32C', responseField: 'ChecksumCRC32C', internalName: 'crc32c' }, + { algorithm: 'CRC64NVME', responseField: 'ChecksumCRC64NVME', internalName: 'crc64nvme' }, + ]; + + before(async () => { + // Disable automatic response checksum validation so the SDK does + // not silently add x-amz-checksum-mode: ENABLED to every GetObject + // request, which would interfere with the "mode not set" test. + bucketUtil = new BucketUtility('default', + { ...sigCfg, responseChecksumValidation: 'WHEN_REQUIRED' }); + s3 = bucketUtil.s3; + await s3.send(new CreateBucketCommand({ Bucket: checksumBucket })); + + for (const { internalName } of checksumAlgorithms) { + expectedDigests[internalName] = + await algorithms[internalName].digest(body); + } + }); + + after(async () => { + await bucketUtil.empty(checksumBucket); + await s3.send(new DeleteBucketCommand({ Bucket: checksumBucket })); + }); + + checksumAlgorithms.forEach(({ algorithm, responseField, internalName }) => { + it(`should return ${responseField} and ChecksumType when ChecksumMode is ENABLED`, async () => { + const putRes = await s3.send(new PutObjectCommand({ + Bucket: checksumBucket, + Key: checksumKey, + Body: body, + ChecksumAlgorithm: algorithm, + })); + const storedChecksum = putRes[responseField]; + assert(storedChecksum, `Expected ${responseField} in PutObject response`); + + const getRes = await s3.send(new GetObjectCommand({ + Bucket: checksumBucket, + Key: checksumKey, + ChecksumMode: 'ENABLED', + })); + assert.strictEqual(getRes[responseField], expectedDigests[internalName], + `${responseField} value mismatch`); + assert.strictEqual(getRes[responseField], storedChecksum); + assert.strictEqual(getRes.ChecksumType, 'FULL_OBJECT'); + }); + }); + + it('should not return checksum headers when ChecksumMode is not set', async () => { + await s3.send(new PutObjectCommand({ + Bucket: checksumBucket, + Key: checksumKey, + Body: body, + ChecksumAlgorithm: 'SHA256', + })); + + const getRes = await s3.send(new GetObjectCommand({ + Bucket: checksumBucket, + Key: checksumKey, + })); + assert.strictEqual(getRes.ChecksumSHA256, undefined); + assert.strictEqual(getRes.ChecksumType, undefined); + }); + + it('should return an error when ChecksumMode is not ENABLED', async () => { + await s3.send(new PutObjectCommand({ + Bucket: checksumBucket, + Key: checksumKey, + Body: body, + })); + + await assert.rejects( + s3.send(new GetObjectCommand({ + Bucket: checksumBucket, + Key: checksumKey, + ChecksumMode: 'DISABLED', + })), + err => { + assert.strictEqual(err.name, 'InvalidArgument'); + return true; + }, + ); + }); + }); +}); diff --git a/tests/functional/aws-node-sdk/test/object/objectHead.js b/tests/functional/aws-node-sdk/test/object/objectHead.js index a9479a89c3..a4bdfed7fa 100644 --- a/tests/functional/aws-node-sdk/test/object/objectHead.js +++ b/tests/functional/aws-node-sdk/test/object/objectHead.js @@ -14,7 +14,17 @@ const { UploadPartCommand, CompleteMultipartUploadCommand, } = require('@aws-sdk/client-s3'); +// In older versions of @aws-sdk/middleware-flexible-checksums (<3.972), the +// CRC64NVME implementation must be patched in manually from the CRT package. +// Newer versions handle this internally via @aws-sdk/crc64-nvme, so the export +// no longer exists and no registration is needed. +const { crc64NvmeCrtContainer } = require('@aws-sdk/middleware-flexible-checksums'); +if (crc64NvmeCrtContainer) { + const { CrtCrc64Nvme } = require('@aws-sdk/crc64-nvme-crt'); + crc64NvmeCrtContainer.CrtCrc64Nvme = CrtCrc64Nvme; +} +const { algorithms } = require('../../../../../lib/api/apiUtils/integrity/validateChecksums'); const changeObjectLock = require('../../../../utilities/objectLock-util'); const withV4 = require('../support/withV4'); const BucketUtility = require('../../lib/utility/bucket-util'); @@ -538,6 +548,100 @@ describe('HEAD object, conditions', () => { }); }); +describe('HEAD object checksum mode', () => { + withV4(sigCfg => { + let bucketUtil; + let s3; + const checksumBucket = 'checksum-headobject-test'; + const checksumKey = 'checksum-test-object'; + const body = Buffer.from('checksum test body'); + + const checksumAlgorithms = [ + { algorithm: 'SHA256', responseField: 'ChecksumSHA256', internalName: 'sha256' }, + { algorithm: 'SHA1', responseField: 'ChecksumSHA1', internalName: 'sha1' }, + { algorithm: 'CRC32', responseField: 'ChecksumCRC32', internalName: 'crc32' }, + { algorithm: 'CRC32C', responseField: 'ChecksumCRC32C', internalName: 'crc32c' }, + { algorithm: 'CRC64NVME', responseField: 'ChecksumCRC64NVME', internalName: 'crc64nvme' }, + ]; + + // Expected base64-encoded digests of `body` for each algorithm, + // computed once in the before hook (crc64nvme digest is async). + const expectedDigests = {}; + + before(async () => { + bucketUtil = new BucketUtility('default', sigCfg); + s3 = bucketUtil.s3; + await s3.send(new CreateBucketCommand({ Bucket: checksumBucket })); + + for (const { internalName } of checksumAlgorithms) { + // algorithms[internalName].digest() returns a base64 string + expectedDigests[internalName] = + await algorithms[internalName].digest(body); + } + }); + + after(async () => { + await bucketUtil.empty(checksumBucket); + await s3.send(new DeleteBucketCommand({ Bucket: checksumBucket })); + }); + + checksumAlgorithms.forEach(({ algorithm, responseField, internalName }) => { + it(`should return ${responseField} and ChecksumType when ChecksumMode is ENABLED`, async () => { + const putRes = await s3.send(new PutObjectCommand({ + Bucket: checksumBucket, + Key: checksumKey, + Body: body, + ChecksumAlgorithm: algorithm, + })); + const storedChecksum = putRes[responseField]; + assert(storedChecksum, `Expected ${responseField} in PutObject response`); + + const headRes = await s3.send(new HeadObjectCommand({ + Bucket: checksumBucket, + Key: checksumKey, + ChecksumMode: 'ENABLED', + })); + assert.strictEqual(headRes[responseField], expectedDigests[internalName], + `${responseField} value mismatch`); + assert.strictEqual(headRes[responseField], storedChecksum); + assert.strictEqual(headRes.ChecksumType, 'FULL_OBJECT'); + }); + }); + + it('should not return checksum headers when ChecksumMode is not set', async () => { + await s3.send(new PutObjectCommand({ + Bucket: checksumBucket, + Key: checksumKey, + Body: body, + ChecksumAlgorithm: 'SHA256', + })); + + const headRes = await s3.send(new HeadObjectCommand({ + Bucket: checksumBucket, + Key: checksumKey, + })); + assert.strictEqual(headRes.ChecksumSHA256, undefined); + assert.strictEqual(headRes.ChecksumType, undefined); + }); + + it('should return an error when ChecksumMode is not ENABLED', async () => { + await s3.send(new PutObjectCommand({ + Bucket: checksumBucket, + Key: checksumKey, + Body: body, + })); + + await assert.rejects( + s3.send(new HeadObjectCommand({ + Bucket: checksumBucket, + Key: checksumKey, + ChecksumMode: 'DISABLED', + })), + ); + }); + }); +}); + const isCEPH = process.env.CI_CEPH !== undefined; const describeSkipIfCeph = isCEPH ? describe.skip : describe; diff --git a/tests/unit/api/objectGet.js b/tests/unit/api/objectGet.js index 261bc149e0..4ed5d6afef 100644 --- a/tests/unit/api/objectGet.js +++ b/tests/unit/api/objectGet.js @@ -2,6 +2,9 @@ const assert = require('assert'); const async = require('async'); const crypto = require('crypto'); const { parseString } = require('xml2js'); +const { models } = require('arsenal'); +const { ObjectMD, ObjectMDChecksum } = models; +const { algorithms } = require('../../../lib/api/apiUtils/integrity/validateChecksums'); const { bucketPut } = require('../../../lib/api/bucketPut'); const { cleanup, DummyRequestLogger, makeAuthInfo } = require('../helpers'); @@ -518,3 +521,155 @@ describe('objectGet API', () => { }); }); }); + +describe('objectGet API - x-amz-checksum-mode', () => { + const checksumAlgorithms = [ + { name: 'sha256', header: 'x-amz-checksum-sha256' }, + { name: 'sha1', header: 'x-amz-checksum-sha1' }, + { name: 'crc32', header: 'x-amz-checksum-crc32' }, + { name: 'crc32c', header: 'x-amz-checksum-crc32c' }, + { name: 'crc64nvme', header: 'x-amz-checksum-crc64nvme' }, + ]; + + const expectedDigests = {}; + + before(done => { + Promise.all(checksumAlgorithms.map(async ({ name }) => { + expectedDigests[name] = await algorithms[name].digest(postBody); + })).then(() => done(), done); + }); + + beforeEach(() => cleanup()); + + checksumAlgorithms.forEach(({ name, header }) => { + it(`should return ${header} and x-amz-checksum-type when mode is ENABLED`, done => { + const md = new ObjectMD(mdColdHelper.baseMd) + .setChecksum(new ObjectMDChecksum(name, expectedDigests[name], 'FULL_OBJECT')); + mdColdHelper.putBucketMock(bucketName, null, () => + mdColdHelper.putObjectMock(bucketName, objectName, md, () => { + const req = { + bucketName, + namespace, + objectKey: objectName, + headers: { 'x-amz-checksum-mode': 'ENABLED' }, + url: `/${bucketName}/${objectName}`, + query: {}, + }; + objectGet(authInfo, req, false, log, (err, _locator, headers) => { + assert.ifError(err); + assert.strictEqual(headers[header], expectedDigests[name]); + assert.strictEqual(headers['x-amz-checksum-type'], 'FULL_OBJECT'); + done(); + }); + })); + }); + }); + + it('should not return checksum headers when mode is ENABLED but object has no checksum', done => { + mdColdHelper.putBucketMock(bucketName, null, () => + mdColdHelper.putObjectMock(bucketName, objectName, undefined, () => { + const req = { + bucketName, + namespace, + objectKey: objectName, + headers: { 'x-amz-checksum-mode': 'ENABLED' }, + url: `/${bucketName}/${objectName}`, + query: {}, + }; + objectGet(authInfo, req, false, log, (err, _locator, headers) => { + assert.ifError(err); + checksumAlgorithms.forEach(({ header }) => + assert.strictEqual(headers[header], undefined)); + assert.strictEqual(headers['x-amz-checksum-type'], undefined); + done(); + }); + })); + }); + + it('should not return checksum headers when x-amz-checksum-mode is not set', done => { + const md = new ObjectMD(mdColdHelper.baseMd) + .setChecksum(new ObjectMDChecksum('sha256', expectedDigests.sha256, 'FULL_OBJECT')); + mdColdHelper.putBucketMock(bucketName, null, () => + mdColdHelper.putObjectMock(bucketName, objectName, md, () => { + const req = { + bucketName, + namespace, + objectKey: objectName, + headers: {}, + url: `/${bucketName}/${objectName}`, + query: {}, + }; + objectGet(authInfo, req, false, log, (err, _locator, headers) => { + assert.ifError(err); + checksumAlgorithms.forEach(({ header }) => + assert.strictEqual(headers[header], undefined)); + assert.strictEqual(headers['x-amz-checksum-type'], undefined); + done(); + }); + })); + }); + + it('should return InvalidArgument when x-amz-checksum-mode is not ENABLED', done => { + const req = { + bucketName, + namespace, + objectKey: objectName, + headers: { 'x-amz-checksum-mode': 'DISABLED' }, + url: `/${bucketName}/${objectName}`, + query: {}, + }; + objectGet(authInfo, req, false, log, err => { + assert.strictEqual(err.is.InvalidArgument, true); + done(); + }); + }); + + it('should not return checksum headers when Range header is set', done => { + const md = new ObjectMD(mdColdHelper.baseMd) + .setChecksum(new ObjectMDChecksum('sha256', expectedDigests.sha256, 'FULL_OBJECT')); + mdColdHelper.putBucketMock(bucketName, null, () => + mdColdHelper.putObjectMock(bucketName, objectName, md, () => { + const req = { + bucketName, + namespace, + objectKey: objectName, + headers: { + 'x-amz-checksum-mode': 'ENABLED', + range: 'bytes=0-3', + }, + url: `/${bucketName}/${objectName}`, + query: {}, + }; + objectGet(authInfo, req, false, log, (err, _locator, headers) => { + assert.ifError(err); + checksumAlgorithms.forEach(({ header }) => + assert.strictEqual(headers[header], undefined)); + assert.strictEqual(headers['x-amz-checksum-type'], undefined); + done(); + }); + })); + }); + + it('should not return checksum headers when partNumber is set', done => { + const md = new ObjectMD(mdColdHelper.baseMd) + .setChecksum(new ObjectMDChecksum('sha256', expectedDigests.sha256, 'FULL_OBJECT')); + mdColdHelper.putBucketMock(bucketName, null, () => + mdColdHelper.putObjectMock(bucketName, objectName, md, () => { + const req = { + bucketName, + namespace, + objectKey: objectName, + headers: { 'x-amz-checksum-mode': 'ENABLED' }, + url: `/${bucketName}/${objectName}`, + query: { partNumber: '1' }, + }; + objectGet(authInfo, req, false, log, (err, _locator, headers) => { + assert.ifError(err); + checksumAlgorithms.forEach(({ header }) => + assert.strictEqual(headers[header], undefined)); + assert.strictEqual(headers['x-amz-checksum-type'], undefined); + done(); + }); + })); + }); +}); diff --git a/tests/unit/api/objectHead.js b/tests/unit/api/objectHead.js index 511a39037c..9508635c5a 100644 --- a/tests/unit/api/objectHead.js +++ b/tests/unit/api/objectHead.js @@ -1,4 +1,7 @@ const assert = require('assert'); +const { models } = require('arsenal'); +const { ObjectMD, ObjectMDChecksum } = models; +const { algorithms } = require('../../../lib/api/apiUtils/integrity/validateChecksums'); const { bucketPut } = require('../../../lib/api/bucketPut'); const { cleanup, DummyRequestLogger, makeAuthInfo } = require('../helpers'); @@ -565,6 +568,127 @@ describe('objectHead API', () => { }); }); + describe('x-amz-checksum-mode', () => { + const checksumAlgorithms = [ + { name: 'sha256', header: 'x-amz-checksum-sha256' }, + { name: 'sha1', header: 'x-amz-checksum-sha1' }, + { name: 'crc32', header: 'x-amz-checksum-crc32' }, + { name: 'crc32c', header: 'x-amz-checksum-crc32c' }, + { name: 'crc64nvme', header: 'x-amz-checksum-crc64nvme' }, + ]; + + // Digests of postBody ("I am a body") for each algorithm, computed once. + const expectedDigests = {}; + + before(done => { + Promise.all(checksumAlgorithms.map(async ({ name }) => { + expectedDigests[name] = await algorithms[name].digest(postBody); + })).then(() => done(), done); + }); + + checksumAlgorithms.forEach(({ name, header }) => { + it(`should return ${header} and x-amz-checksum-type when mode is ENABLED`, done => { + const md = new ObjectMD(mdColdHelper.baseMd) + .setChecksum(new ObjectMDChecksum(name, expectedDigests[name], 'FULL_OBJECT')); + mdColdHelper.putBucketMock(bucketName, null, () => + mdColdHelper.putObjectMock(bucketName, objectName, md, () => { + const req = { + bucketName, + namespace, + objectKey: objectName, + headers: { 'x-amz-checksum-mode': 'ENABLED' }, + url: `/${bucketName}/${objectName}`, + }; + objectHead(authInfo, req, log, (err, res) => { + assert.ifError(err); + assert.strictEqual(res[header], expectedDigests[name]); + assert.strictEqual(res['x-amz-checksum-type'], 'FULL_OBJECT'); + done(); + }); + })); + }); + }); + + it('should not return checksum headers when mode is ENABLED but object has no checksum', done => { + mdColdHelper.putBucketMock(bucketName, null, () => + mdColdHelper.putObjectMock(bucketName, objectName, undefined, () => { + const req = { + bucketName, + namespace, + objectKey: objectName, + headers: { 'x-amz-checksum-mode': 'ENABLED' }, + url: `/${bucketName}/${objectName}`, + }; + objectHead(authInfo, req, log, (err, res) => { + assert.ifError(err); + checksumAlgorithms.forEach(({ header }) => + assert.strictEqual(res[header], undefined)); + assert.strictEqual(res['x-amz-checksum-type'], undefined); + done(); + }); + })); + }); + + it('should not return checksum headers when x-amz-checksum-mode is not set', done => { + const md = new ObjectMD(mdColdHelper.baseMd) + .setChecksum(new ObjectMDChecksum('sha256', expectedDigests.sha256, 'FULL_OBJECT')); + mdColdHelper.putBucketMock(bucketName, null, () => + mdColdHelper.putObjectMock(bucketName, objectName, md, () => { + const req = { + bucketName, + namespace, + objectKey: objectName, + headers: {}, + url: `/${bucketName}/${objectName}`, + }; + objectHead(authInfo, req, log, (err, res) => { + assert.ifError(err); + checksumAlgorithms.forEach(({ header }) => + assert.strictEqual(res[header], undefined)); + assert.strictEqual(res['x-amz-checksum-type'], undefined); + done(); + }); + })); + }); + + it('should return InvalidArgument when x-amz-checksum-mode is not ENABLED', done => { + const req = { + bucketName, + namespace, + objectKey: objectName, + headers: { 'x-amz-checksum-mode': 'DISABLED' }, + url: `/${bucketName}/${objectName}`, + }; + objectHead(authInfo, req, log, err => { + assert.strictEqual(err.is.InvalidArgument, true); + done(); + }); + }); + + it('should not return checksum headers when partNumber is set', done => { + const md = new ObjectMD(mdColdHelper.baseMd) + .setChecksum(new ObjectMDChecksum('sha256', expectedDigests.sha256, 'FULL_OBJECT')); + mdColdHelper.putBucketMock(bucketName, null, () => + mdColdHelper.putObjectMock(bucketName, objectName, md, () => { + const req = { + bucketName, + namespace, + objectKey: objectName, + headers: { 'x-amz-checksum-mode': 'ENABLED' }, + url: `/${bucketName}/${objectName}`, + query: { partNumber: '1' }, + }; + objectHead(authInfo, req, log, (err, res) => { + assert.ifError(err); + checksumAlgorithms.forEach(({ header }) => + assert.strictEqual(res[header], undefined)); + assert.strictEqual(res['x-amz-checksum-type'], undefined); + done(); + }); + })); + }); + }); + [ { name: 'should return content-length of 0 when requesting part 1 of empty object', diff --git a/tests/unit/api/utils/metadataMockColdStorage.js b/tests/unit/api/utils/metadataMockColdStorage.js index b1f6496418..e2052e5277 100644 --- a/tests/unit/api/utils/metadataMockColdStorage.js +++ b/tests/unit/api/utils/metadataMockColdStorage.js @@ -202,6 +202,7 @@ function getDeleteMarkerObjectMD(versionId) { } module.exports = { + baseMd, putObjectMock, getArchivedObjectMD, getRestoringObjectMD,