From d396ffe70f9a4129887df484e6a91ced8d481942 Mon Sep 17 00:00:00 2001 From: Leif Henriksen Date: Tue, 31 Mar 2026 16:12:53 +0200 Subject: [PATCH] CLDSRV-874: return checksum in GetObjectAttributes --- .../apiUtils/integrity/validateChecksums.js | 5 + lib/api/apiUtils/object/objectAttributes.js | 112 +++++++++------ lib/api/objectGetAttributes.js | 19 +-- .../test/object/objectGetAttributes.js | 129 +++++++++++++++-- .../api/apiUtils/object/objectAttributes.js | 78 ++++++++-- tests/unit/api/objectGetAttributes.js | 133 ++++++++++++++++-- 6 files changed, 384 insertions(+), 92 deletions(-) diff --git a/lib/api/apiUtils/integrity/validateChecksums.js b/lib/api/apiUtils/integrity/validateChecksums.js index 3a0f25e556..fe79228394 100644 --- a/lib/api/apiUtils/integrity/validateChecksums.js +++ b/lib/api/apiUtils/integrity/validateChecksums.js @@ -69,6 +69,7 @@ function uint32ToBase64(num) { const algorithms = Object.freeze({ crc64nvme: { + getObjectAttributesXMLTag: 'ChecksumCRC64NVME', digest: async data => { const input = Buffer.isBuffer(data) ? data : Buffer.from(data); const crc = new CrtCrc64Nvme(); @@ -84,6 +85,7 @@ const algorithms = Object.freeze({ createHash: () => new CrtCrc64Nvme() }, crc32: { + getObjectAttributesXMLTag: 'ChecksumCRC32', digest: data => { const input = Buffer.isBuffer(data) ? data : Buffer.from(data); return uint32ToBase64(new Crc32().update(input).digest() >>> 0); // >>> 0 coerce number to uint32 @@ -96,6 +98,7 @@ const algorithms = Object.freeze({ createHash: () => new Crc32() }, crc32c: { + getObjectAttributesXMLTag: 'ChecksumCRC32C', digest: data => { const input = Buffer.isBuffer(data) ? data : Buffer.from(data); return uint32ToBase64(new Crc32c().update(input).digest() >>> 0); // >>> 0 coerce number to uint32 @@ -105,6 +108,7 @@ const algorithms = Object.freeze({ createHash: () => new Crc32c() }, sha1: { + getObjectAttributesXMLTag: 'ChecksumSHA1', digest: data => { const input = Buffer.isBuffer(data) ? data : Buffer.from(data); return crypto.createHash('sha1').update(input).digest('base64'); @@ -114,6 +118,7 @@ const algorithms = Object.freeze({ createHash: () => crypto.createHash('sha1') }, sha256: { + getObjectAttributesXMLTag: 'ChecksumSHA256', digest: data => { const input = Buffer.isBuffer(data) ? data : Buffer.from(data); return crypto.createHash('sha256').update(input).digest('base64'); diff --git a/lib/api/apiUtils/object/objectAttributes.js b/lib/api/apiUtils/object/objectAttributes.js index 1f7ddcf44a..ad5bf2e51e 100644 --- a/lib/api/apiUtils/object/objectAttributes.js +++ b/lib/api/apiUtils/object/objectAttributes.js @@ -1,5 +1,6 @@ const { errorInstances } = require('arsenal'); const { getPartCountFromMd5 } = require('./partInfo'); +const { algorithms } = require('../integrity/validateChecksums'); /** * Parse and validate attribute headers from a request. @@ -53,59 +54,80 @@ function parseAttributesHeaders(headers, headerName, supportedAttributes) { * @param {Object.} userMetadata - Key-value pairs of user-defined metadata. * @param {string[]} requestedAttrs - A list of specific attributes to include in the output. * Supports 'ETag', 'ObjectParts', 'StorageClass', 'ObjectSize', - * 'RestoreStatus', and 'x-amz-meta-*' for all user metadata. + * 'Checksum', 'RestoreStatus', and 'x-amz-meta-*' for all user metadata. * @param {string[]} xml - The string array acting as the output buffer/collector. + * @param {object} log - Werelogs logger. * @returns {void} - this function does not return a value, it mutates the `xml` param. */ -function buildAttributesXml(objectMD, userMetadata, requestedAttrs, xml) { - const customAttributes = new Set(); - for (const attribute of requestedAttrs) { - switch (attribute) { - case 'ETag': - xml.push(`${objectMD['content-md5']}`); - break; - case 'ObjectParts': { - const partCount = getPartCountFromMd5(objectMD); - if (partCount) { - xml.push( - '', - `${partCount}`, - '', - ); - } - break; - } - case 'StorageClass': - xml.push(`${objectMD['x-amz-storage-class']}`); - break; - case 'ObjectSize': - xml.push(`${objectMD['content-length']}`); - break; - case 'RestoreStatus': - xml.push(''); - xml.push(`${!!objectMD.restoreStatus?.inProgress}`); +function buildAttributesXml(objectMD, userMetadata, requestedAttrs, xml, log) { + const customAttributes = new Set(); + for (const attribute of requestedAttrs) { + switch (attribute) { + case 'ETag': + xml.push(`${objectMD['content-md5']}`); + break; + case 'ObjectParts': { + const partCount = getPartCountFromMd5(objectMD); + if (partCount) { + xml.push( + '', + `${partCount}`, + '', + ); + } + break; + } + case 'StorageClass': + xml.push(`${objectMD['x-amz-storage-class']}`); + break; + case 'ObjectSize': + xml.push(`${objectMD['content-length']}`); + break; + case 'Checksum': { + const { checksum } = objectMD; + if (checksum) { + const algo = algorithms[checksum.checksumAlgorithm]; + if (!algo) { + log.error('unknown checksum algorithm in object metadata', { + checksumAlgorithm: checksum.checksumAlgorithm, + }); + break; + } + const tag = algo.getObjectAttributesXMLTag; + xml.push( + '', + `<${tag}>${checksum.checksumValue}`, + `${checksum.checksumType}`, + '', + ); + } + break; + } + case 'RestoreStatus': + xml.push(''); + xml.push(`${!!objectMD.restoreStatus?.inProgress}`); - if (objectMD.restoreStatus?.expiryDate) { - xml.push(`${objectMD.restoreStatus?.expiryDate}`); - } + if (objectMD.restoreStatus?.expiryDate) { + xml.push(`${objectMD.restoreStatus?.expiryDate}`); + } - xml.push(''); - break; - case 'x-amz-meta-*': - for (const key of Object.keys(userMetadata)) { - customAttributes.add(key); - } - break; - default: - if (userMetadata[attribute]) { - customAttributes.add(attribute); + xml.push(''); + break; + case 'x-amz-meta-*': + for (const key of Object.keys(userMetadata)) { + customAttributes.add(key); + } + break; + default: + if (userMetadata[attribute]) { + customAttributes.add(attribute); + } } } - } - for (const key of customAttributes) { - xml.push(`<${key}>${userMetadata[key]}`); - } + for (const key of customAttributes) { + xml.push(`<${key}>${userMetadata[key]}`); + } } module.exports = { diff --git a/lib/api/objectGetAttributes.js b/lib/api/objectGetAttributes.js index 1610d078ec..9ef71eabbb 100644 --- a/lib/api/objectGetAttributes.js +++ b/lib/api/objectGetAttributes.js @@ -17,16 +17,17 @@ const OBJECT_GET_ATTRIBUTES = 'objectGetAttributes'; * buildXmlResponse - Build XML response for GetObjectAttributes * @param {object} objMD - object metadata * @param {Set} requestedAttrs - set of requested attribute names + * @param {object} log - Werelogs logger * @returns {string} XML response */ -function buildXmlResponse(objMD, requestedAttrs) { +function buildXmlResponse(objMD, requestedAttrs, log) { const xml = [ '', '', ]; const userMetadata = getUserMetadata(objMD); - buildAttributesXml(objMD, userMetadata, requestedAttrs, xml); + buildAttributesXml(objMD, userMetadata, requestedAttrs, xml, log); xml.push(''); return xml.join(''); @@ -150,18 +151,6 @@ async function objectGetAttributes(authInfo, request, log, callback) { true, ); - if (requestedAttrs.has('Checksum')) { - log.debug('Checksum attribute requested but not implemented', { - method: OBJECT_GET_ATTRIBUTES, - bucket: bucketName, - key: objectKey, - versionId, - }); - const err = errors.NotImplemented.customizeDescription('Checksum attribute is not implemented'); - err.responseHeaders = responseHeaders; - throw err; - } - pushMetric(OBJECT_GET_ATTRIBUTES, log, { authInfo, bucket: bucketName, @@ -170,7 +159,7 @@ async function objectGetAttributes(authInfo, request, log, callback) { location: objectMD?.dataStoreName, }); - const xml = buildXmlResponse(objectMD, requestedAttrs); + const xml = buildXmlResponse(objectMD, requestedAttrs, log); return { xml, responseHeaders }; } diff --git a/tests/functional/aws-node-sdk/test/object/objectGetAttributes.js b/tests/functional/aws-node-sdk/test/object/objectGetAttributes.js index 544b21c637..4af27c76e7 100644 --- a/tests/functional/aws-node-sdk/test/object/objectGetAttributes.js +++ b/tests/functional/aws-node-sdk/test/object/objectGetAttributes.js @@ -12,6 +12,7 @@ const { const { GetObjectAttributesExtendedCommand } = require('@scality/cloudserverclient'); const withV4 = require('../support/withV4'); const BucketUtility = require('../../lib/utility/bucket-util'); +const { algorithms } = require('../../../../../lib/api/apiUtils/integrity/validateChecksums'); const bucket = 'testbucket'; const key = 'testobject'; @@ -30,7 +31,9 @@ describe('objectGetAttributes', () => { beforeEach(async () => { await s3.send(new CreateBucketCommand({ Bucket: bucket })); - await s3.send(new PutObjectCommand({ Bucket: bucket, Key: key, Body: body })); + await s3.send(new PutObjectCommand({ + Bucket: bucket, Key: key, Body: body, ChecksumAlgorithm: 'CRC64NVME', + })); }); afterEach(async () => { @@ -119,18 +122,28 @@ describe('objectGetAttributes', () => { assert.strictEqual(data.ETag, expectedMD5); }); - it('should fail with NotImplemented when Checksum is requested', async () => { - try { - await s3.send(new GetObjectAttributesCommand({ - Bucket: bucket, - Key: key, - ObjectAttributes: ['Checksum'], - })); - assert.fail('Expected NotImplemented error'); - } catch (err) { - assert.strictEqual(err.name, 'NotImplemented'); - assert.strictEqual(err.message, 'Checksum attribute is not implemented'); - } + it('should return ChecksumCRC64NVME for object', async () => { + const data = await s3.send(new GetObjectAttributesCommand({ + Bucket: bucket, + Key: key, + ObjectAttributes: ['Checksum'], + })); + + assert(data.Checksum, 'Checksum should be present'); + assert(data.Checksum.ChecksumCRC64NVME, 'ChecksumCRC64NVME should be present'); + assert.strictEqual(data.Checksum.ChecksumType, 'FULL_OBJECT'); + }); + + it('should not return Checksum when not requested', async () => { + const data = await s3.send(new GetObjectAttributesCommand({ + Bucket: bucket, + Key: key, + ObjectAttributes: ['ETag', 'ObjectSize'], + })); + + assert(data.ETag, 'ETag should be present'); + assert(data.ObjectSize, 'ObjectSize should be present'); + assert.strictEqual(data.Checksum, undefined, 'Checksum should not be present'); }); it("shouldn't return ObjectParts for non-MPU objects", async () => { @@ -480,3 +493,93 @@ describe('objectGetAttributes with user metadata', () => { }); }); }); + +describe('objectGetAttributes with checksum', () => { + withV4(sigCfg => { + let bucketUtil; + let s3; + const checksumBucket = 'checksum-getattr-test'; + const checksumKey = 'checksum-test-object'; + const checksumBody = Buffer.from('checksum test body'); + + const expectedDigests = {}; + + before(async () => { + bucketUtil = new BucketUtility('default', sigCfg); + s3 = bucketUtil.s3; + await s3.send(new CreateBucketCommand({ Bucket: checksumBucket })); + + for (const [name, algo] of Object.entries(algorithms)) { + expectedDigests[name] = await algo.digest(checksumBody); + } + }); + + after(async () => { + await bucketUtil.empty(checksumBucket); + await s3.send(new DeleteBucketCommand({ Bucket: checksumBucket })); + }); + + Object.entries(algorithms).forEach(([name, { getObjectAttributesXMLTag }]) => { + const sdkAlgorithm = name.toUpperCase(); + + it(`should return ${getObjectAttributesXMLTag} when object has ${name} checksum`, async () => { + await s3.send(new PutObjectCommand({ + Bucket: checksumBucket, + Key: checksumKey, + Body: checksumBody, + ChecksumAlgorithm: sdkAlgorithm, + })); + + const data = await s3.send(new GetObjectAttributesCommand({ + Bucket: checksumBucket, + Key: checksumKey, + ObjectAttributes: ['Checksum'], + })); + + assert(data.Checksum, 'Checksum should be present'); + assert.strictEqual(data.Checksum[getObjectAttributesXMLTag], expectedDigests[name]); + assert.strictEqual(data.Checksum.ChecksumType, 'FULL_OBJECT'); + }); + + it(`should return ${getObjectAttributesXMLTag} along with other attributes`, async () => { + await s3.send(new PutObjectCommand({ + Bucket: checksumBucket, + Key: checksumKey, + Body: checksumBody, + ChecksumAlgorithm: sdkAlgorithm, + })); + + const data = await s3.send(new GetObjectAttributesCommand({ + Bucket: checksumBucket, + Key: checksumKey, + ObjectAttributes: ['ETag', 'Checksum', 'ObjectSize'], + })); + + assert(data.ETag, 'ETag should be present'); + assert(data.ObjectSize, 'ObjectSize should be present'); + assert(data.Checksum, 'Checksum should be present'); + assert.strictEqual(data.Checksum[getObjectAttributesXMLTag], expectedDigests[name]); + assert.strictEqual(data.Checksum.ChecksumType, 'FULL_OBJECT'); + }); + }); + + it('should not return Checksum when not requested', async () => { + await s3.send(new PutObjectCommand({ + Bucket: checksumBucket, + Key: checksumKey, + Body: checksumBody, + ChecksumAlgorithm: 'CRC64NVME', + })); + + const data = await s3.send(new GetObjectAttributesCommand({ + Bucket: checksumBucket, + Key: checksumKey, + ObjectAttributes: ['ETag', 'ObjectSize'], + })); + + assert(data.ETag, 'ETag should be present'); + assert(data.ObjectSize, 'ObjectSize should be present'); + assert.strictEqual(data.Checksum, undefined, 'Checksum should not be present'); + }); + }); +}); diff --git a/tests/unit/api/apiUtils/object/objectAttributes.js b/tests/unit/api/apiUtils/object/objectAttributes.js index fe4a277e12..151d56d0aa 100644 --- a/tests/unit/api/apiUtils/object/objectAttributes.js +++ b/tests/unit/api/apiUtils/object/objectAttributes.js @@ -3,7 +3,10 @@ const { parseAttributesHeaders, buildAttributesXml } = require('../../../../../lib/api/apiUtils/object/objectAttributes'); +const { algorithms } = require('../../../../../lib/api/apiUtils/integrity/validateChecksums'); +const { DummyRequestLogger } = require('../../../helpers'); +const log = new DummyRequestLogger(); const headerName = 'x-amz-object-attributes'; const allowedAttributes = new Set(['ETag', 'StorageClass', 'ObjectSize']); @@ -68,14 +71,14 @@ describe('buildXmlAttributes', () => { describe('with object attributes', () => { it('should generate empty XML when attributes is empty', () => { const result = []; - buildAttributesXml(objectMD, userMetadata, [], result); + buildAttributesXml(objectMD, userMetadata, [], result, log); assert.strictEqual(result.length, 0); }); it('should generate ETag XML', () => { const result = []; - buildAttributesXml(objectMD, userMetadata, ['ETag'], result); + buildAttributesXml(objectMD, userMetadata, ['ETag'], result, log); assert.strictEqual(result.length, 1); assert.strictEqual(result[0], '16e37e19194511993498801d4692795f'); @@ -83,7 +86,7 @@ describe('buildXmlAttributes', () => { it('should generate StorageClass XML', () => { const result = []; - buildAttributesXml(objectMD, userMetadata, ['StorageClass'], result); + buildAttributesXml(objectMD, userMetadata, ['StorageClass'], result, log); assert.strictEqual(result.length, 1); assert.strictEqual(result[0], 'STANDARD'); @@ -91,7 +94,7 @@ describe('buildXmlAttributes', () => { it('should generate ObjectSize XML', () => { const result = []; - buildAttributesXml(objectMD, userMetadata, ['ObjectSize'], result); + buildAttributesXml(objectMD, userMetadata, ['ObjectSize'], result, log); assert.strictEqual(result.length, 1); assert.strictEqual(result[0], '5000'); @@ -100,7 +103,7 @@ describe('buildXmlAttributes', () => { it('should generate ObjectParts XML when parts exist', () => { const result = []; const objectMDWithParts = { ...objectMD, 'content-md5': `${objectMD['content-md5']}-10` }; - buildAttributesXml(objectMDWithParts, {}, ['ObjectParts'], result); + buildAttributesXml(objectMDWithParts, {}, ['ObjectParts'], result, log); assert.strictEqual(result.length, 3); assert.strictEqual(result[0], ''); @@ -110,7 +113,7 @@ describe('buildXmlAttributes', () => { it('should generate RestoreStatus XML with expiry date', () => { const result = []; - buildAttributesXml(objectMD, userMetadata, ['RestoreStatus'], result); + buildAttributesXml(objectMD, userMetadata, ['RestoreStatus'], result, log); assert.strictEqual(result.length, 4); assert.strictEqual(result[0], ''); @@ -121,7 +124,7 @@ describe('buildXmlAttributes', () => { it('should ignore unknown attributes', () => { const result = []; - buildAttributesXml(objectMD, userMetadata, ['UnknownAttribute', 'ETag'], result); + buildAttributesXml(objectMD, userMetadata, ['UnknownAttribute', 'ETag'], result, log); assert.strictEqual(result.length, 1); assert.strictEqual(result[0], '16e37e19194511993498801d4692795f'); @@ -131,7 +134,7 @@ describe('buildXmlAttributes', () => { describe('with user metadata', () => { it('should include all user metadata when x-amz-meta-* is used', () => { const result = []; - buildAttributesXml({}, userMetadata, ['x-amz-meta-*'], result); + buildAttributesXml({}, userMetadata, ['x-amz-meta-*'], result, log); assert.strictEqual(result.length, 2); assert.strictEqual(result[0], 'foo'); @@ -140,7 +143,7 @@ describe('buildXmlAttributes', () => { it('should include specific user metadata keys', () => { const result = []; - buildAttributesXml({}, userMetadata, ['x-amz-meta-foo'], result); + buildAttributesXml({}, userMetadata, ['x-amz-meta-foo'], result, log); assert.strictEqual(result.length, 1); assert.strictEqual(result[0], 'foo'); @@ -148,7 +151,7 @@ describe('buildXmlAttributes', () => { it('should de-duplicate keys when both specific key and wildcard are requested', () => { const result = []; - buildAttributesXml({}, userMetadata, ['x-amz-meta-foo', 'x-amz-meta-*'], result); + buildAttributesXml({}, userMetadata, ['x-amz-meta-foo', 'x-amz-meta-*'], result, log); assert.strictEqual(result.length, 2); assert.strictEqual(result[0], 'foo'); @@ -156,12 +159,65 @@ describe('buildXmlAttributes', () => { }); }); + describe('with Checksum attribute', () => { + const testData = Buffer.from('test data'); + const expectedDigests = {}; + + before(async () => { + await Promise.all(Object.keys(algorithms).map(async name => { + expectedDigests[name] = await algorithms[name].digest(testData); + })); + }); + + it('should not generate Checksum XML when checksumAlgorithm is unknown', () => { + const result = []; + const md = { + checksum: { + checksumAlgorithm: 'unknown', + checksumValue: 'dGVzdA==', + checksumType: 'FULL_OBJECT', + }, + }; + buildAttributesXml(md, {}, ['Checksum'], result, log); + + assert.strictEqual(result.length, 0); + }); + + it('should not generate Checksum XML when checksum is absent', () => { + const result = []; + buildAttributesXml(objectMD, {}, ['Checksum'], result, log); + + assert.strictEqual(result.length, 0); + }); + + Object.entries(algorithms).forEach(([algo, { getObjectAttributesXMLTag }]) => { + it(`should generate correct Checksum XML for ${algo}`, () => { + const digest = expectedDigests[algo]; + const result = []; + const md = { + checksum: { + checksumAlgorithm: algo, + checksumValue: digest, + checksumType: 'FULL_OBJECT', + }, + }; + buildAttributesXml(md, {}, ['Checksum'], result, log); + + assert.strictEqual(result.length, 4); + assert.strictEqual(result[0], ''); + assert.strictEqual(result[1], `<${getObjectAttributesXMLTag}>${digest}`); + assert.strictEqual(result[2], 'FULL_OBJECT'); + assert.strictEqual(result[3], ''); + }); + }); + }); + describe('with object attributes and user metadata', () => { it('should build a comprehensive XML array with all supported features', () => { const result = []; const objectMDWithParts = { ...objectMD, 'content-md5': `${objectMD['content-md5']}-10` }; const requested = ['ETag', 'ObjectSize', 'ObjectParts', 'RestoreStatus', 'x-amz-meta-*', 'x-amz-meta-foo']; - buildAttributesXml(objectMDWithParts, userMetadata, requested, result); + buildAttributesXml(objectMDWithParts, userMetadata, requested, result, log); const expected = [ '16e37e19194511993498801d4692795f-10', diff --git a/tests/unit/api/objectGetAttributes.js b/tests/unit/api/objectGetAttributes.js index 4c951fbcdd..2155731560 100644 --- a/tests/unit/api/objectGetAttributes.js +++ b/tests/unit/api/objectGetAttributes.js @@ -12,6 +12,8 @@ const objectPut = require('../../../lib/api/objectPut'); const { objectDelete } = require('../../../lib/api/objectDelete'); const objectGetAttributes = require('../../../lib/api/objectGetAttributes'); const objectPutPart = require('../../../lib/api/objectPutPart'); +const { algorithms } = require('../../../lib/api/apiUtils/integrity/validateChecksums'); +const mdColdHelper = require('./utils/metadataMockColdStorage'); const log = new DummyRequestLogger(); const authInfo = makeAuthInfo('accessKey1'); @@ -203,16 +205,16 @@ describe('objectGetAttributes API', () => { assert.strictEqual(result.GetObjectAttributesResponse.ETag[0], expectedMD5); }); - it('should fail with NotImplemented when Checksum is requested', async () => { + it('should return default crc64nvme Checksum for object put without explicit checksum', async () => { const testGetRequest = createGetAttributesRequest(['Checksum']); - try { - await objectGetAttributes(authInfo, testGetRequest, log); - assert.fail('Expected error was not thrown'); - } catch (err) { - assert.strictEqual(err.is.NotImplemented, true); - assert.strictEqual(err.description, 'Checksum attribute is not implemented'); - } + const { xml } = await objectGetAttributes(authInfo, testGetRequest, log); + const result = await parseStringPromise(xml); + const response = result.GetObjectAttributesResponse; + + assert(response.Checksum, 'Checksum should be present'); + assert(response.Checksum[0].ChecksumCRC64NVME, 'ChecksumCRC64NVME should be present'); + assert.strictEqual(response.Checksum[0].ChecksumType[0], 'FULL_OBJECT'); }); it("shouldn't return ObjectParts for non-MPU object", async () => { @@ -647,3 +649,118 @@ describe('objectGetAttributes API with versioning', () => { assert.strictEqual(responseHeaders['x-amz-version-id'], versionId); }); }); + +describe('objectGetAttributes API with checksum', () => { + const expectedDigests = {}; + + before(async () => { + await Promise.all(Object.keys(algorithms).map(async name => { + expectedDigests[name] = await algorithms[name].digest(postBody); + })); + }); + + beforeEach(async () => { + cleanup(); + await bucketPutAsync(authInfo, testPutBucketRequest, log); + }); + + Object.entries(algorithms).forEach(([name, { getObjectAttributesXMLTag }]) => { + it(`should return ${getObjectAttributesXMLTag} when object has ${name} checksum`, async () => { + const testPutObjectRequest = new DummyRequest( + { + bucketName, + namespace, + objectKey: objectName, + headers: { + 'content-length': `${postBody.length}`, + [`x-amz-checksum-${name}`]: expectedDigests[name], + }, + parsedContentLength: postBody.length, + url: `/${bucketName}/${objectName}`, + }, + postBody, + ); + await objectPutAsync(authInfo, testPutObjectRequest, undefined, log); + + const testGetRequest = createGetAttributesRequest(['Checksum']); + const { xml } = await objectGetAttributes(authInfo, testGetRequest, log); + const result = await parseStringPromise(xml); + const response = result.GetObjectAttributesResponse; + + assert(response.Checksum, 'Checksum should be present'); + assert.strictEqual(response.Checksum[0][getObjectAttributesXMLTag][0], expectedDigests[name]); + assert.strictEqual(response.Checksum[0].ChecksumType[0], 'FULL_OBJECT'); + }); + + it(`should return ${getObjectAttributesXMLTag} along with other attributes`, async () => { + const testPutObjectRequest = new DummyRequest( + { + bucketName, + namespace, + objectKey: objectName, + headers: { + 'content-length': `${postBody.length}`, + [`x-amz-checksum-${name}`]: expectedDigests[name], + }, + parsedContentLength: postBody.length, + url: `/${bucketName}/${objectName}`, + }, + postBody, + ); + await objectPutAsync(authInfo, testPutObjectRequest, undefined, log); + + const testGetRequest = createGetAttributesRequest(['ETag', 'Checksum', 'ObjectSize']); + const { xml } = await objectGetAttributes(authInfo, testGetRequest, log); + const result = await parseStringPromise(xml); + const response = result.GetObjectAttributesResponse; + + assert(response.ETag, 'ETag should be present'); + assert(response.ObjectSize, 'ObjectSize should be present'); + assert(response.Checksum, 'Checksum should be present'); + assert.strictEqual(response.Checksum[0][getObjectAttributesXMLTag][0], expectedDigests[name]); + assert.strictEqual(response.Checksum[0].ChecksumType[0], 'FULL_OBJECT'); + }); + }); + + it('should not return Checksum when requested but not present in object metadata', async () => { + await new Promise(resolve => mdColdHelper.putBucketMock(bucketName, null, resolve)); + await new Promise(resolve => mdColdHelper.putObjectMock(bucketName, objectName, undefined, resolve)); + + const testGetRequest = createGetAttributesRequest(['Checksum']); + const { xml } = await objectGetAttributes(authInfo, testGetRequest, log); + const result = await parseStringPromise(xml); + + assert.strictEqual( + result.GetObjectAttributesResponse.Checksum, + undefined, + 'Checksum should not be present', + ); + }); + + it('should not return Checksum when not requested', async () => { + const testPutObjectRequest = new DummyRequest( + { + bucketName, + namespace, + objectKey: objectName, + headers: { + 'content-length': `${postBody.length}`, + 'x-amz-checksum-sha256': expectedDigests.sha256, + }, + parsedContentLength: postBody.length, + url: `/${bucketName}/${objectName}`, + }, + postBody, + ); + await objectPutAsync(authInfo, testPutObjectRequest, undefined, log); + + const testGetRequest = createGetAttributesRequest(['ETag', 'ObjectSize']); + const { xml } = await objectGetAttributes(authInfo, testGetRequest, log); + const result = await parseStringPromise(xml); + const response = result.GetObjectAttributesResponse; + + assert(response.ETag, 'ETag should be present'); + assert(response.ObjectSize, 'ObjectSize should be present'); + assert.strictEqual(response.Checksum, undefined, 'Checksum should not be present'); + }); +});