From 12f4a94d55173d5a24fa0d42ebb515fb0b25fcac Mon Sep 17 00:00:00 2001 From: Nicolas Humbert Date: Wed, 24 Jun 2026 09:47:22 +0200 Subject: [PATCH 1/3] CLDSRV-936 Lifecycle noncurrent expiration stalls when listing truncates on a bare master ++ --- lib/api/apiUtils/object/lifecycle.js | 16 ++++- lib/api/backbeat/listLifecycleNonCurrents.js | 14 +++-- .../backbeat/listLifecycleNonCurrents.js | 59 +++++++++++++++++++ tests/unit/api/apiUtils/lifecycle.js | 28 ++++++++- 4 files changed, 110 insertions(+), 7 deletions(-) diff --git a/lib/api/apiUtils/object/lifecycle.js b/lib/api/apiUtils/object/lifecycle.js index c2e0d3de07..0fc8f830e7 100644 --- a/lib/api/apiUtils/object/lifecycle.js +++ b/lib/api/apiUtils/object/lifecycle.js @@ -1,4 +1,4 @@ -const { versioning } = require('arsenal'); +const { versioning, errorInstances } = require('arsenal'); const versionIdUtils = versioning.VersionID; const { lifecycleListing } = require('../../../../constants'); @@ -70,6 +70,19 @@ function _encodeVersionId(vid) { return versionId; } +// A 'null' or absent marker means "no marker". Any other value is decoded, +// returning InvalidArgument if it is not a valid version id. +function decodeVersionIdMarker(vid) { + if (!vid || vid === 'null') { + return undefined; + } + const decoded = versionIdUtils.decode(vid); + if (decoded instanceof Error) { + return errorInstances.InvalidArgument.customizeDescription('Invalid version id marker specified'); + } + return decoded; +} + function processNonCurrents(bucketName, listParams, list) { const nextVersionIdMarker = _encodeVersionId(list.NextVersionIdMarker); const versionIdMarker = _encodeVersionId(listParams.versionIdMarker); @@ -187,4 +200,5 @@ module.exports = { processOrphans, getLocationConstraintErrorMessage, validateMaxScannedEntries, + decodeVersionIdMarker, }; diff --git a/lib/api/backbeat/listLifecycleNonCurrents.js b/lib/api/backbeat/listLifecycleNonCurrents.js index e1808be3d8..ee9c9cfa38 100644 --- a/lib/api/backbeat/listLifecycleNonCurrents.js +++ b/lib/api/backbeat/listLifecycleNonCurrents.js @@ -1,12 +1,11 @@ -const { errors, errorInstances, versioning } = require('arsenal'); +const { errors, errorInstances } = require('arsenal'); const constants = require('../../../constants'); const services = require('../../services'); const { standardMetadataValidateBucket } = require('../../metadata/metadataUtils'); const { pushMetric } = require('../../utapi/utilities'); -const versionIdUtils = versioning.VersionID; const monitoring = require('../../utilities/monitoringHandler'); const { getLocationConstraintErrorMessage, processNonCurrents, - validateMaxScannedEntries } = require('../apiUtils/object/lifecycle'); + validateMaxScannedEntries, decodeVersionIdMarker } = require('../apiUtils/object/lifecycle'); const { config } = require('../../Config'); function handleResult(listParams, requestMaxKeys, authInfo, @@ -80,8 +79,13 @@ function listLifecycleNonCurrents(authInfo, locationConstraints, request, log, c maxScannedLifecycleListingEntries, }; - listParams.versionIdMarker = params['version-id-marker'] ? - versionIdUtils.decode(params['version-id-marker']) : undefined; + const versionIdMarker = decodeVersionIdMarker(params['version-id-marker']); + if (versionIdMarker instanceof Error) { + log.debug('invalid version id marker', { versionIdMarker: params['version-id-marker'] }); + monitoring.promMetrics('GET', bucketName, 400, 'listLifecycleNonCurrents'); + return callback(versionIdMarker); + } + listParams.versionIdMarker = versionIdMarker; return standardMetadataValidateBucket(metadataValParams, request.actionImplicitDenies, log, (err, bucket) => { if (err) { diff --git a/tests/functional/backbeat/listLifecycleNonCurrents.js b/tests/functional/backbeat/listLifecycleNonCurrents.js index f66e857f6a..d3ebc0d724 100644 --- a/tests/functional/backbeat/listLifecycleNonCurrents.js +++ b/tests/functional/backbeat/listLifecycleNonCurrents.js @@ -330,6 +330,26 @@ describe('listLifecycleNonCurrents', () => { }); }); + it('should return InvalidArgument error if version-id-marker is malformed', done => { + makeBackbeatRequest({ + method: 'GET', + bucket: testBucket, + queryObj: { + 'list-type': 'noncurrent', + 'key-marker': 'key1', + 'version-id-marker': 'notavalidversionid', + }, + authCredentials: credentials, + }, err => { + // A non-null, undecodable marker must be rejected with a 400 + // rather than reaching _encodeVersionId and crashing the worker. + assert(err); + assert.strictEqual(err.code, 'InvalidArgument'); + assert.strictEqual(err.statusCode, 400); + return done(); + }); + }); + it('should return error if bucket not versioned', done => { makeBackbeatRequest({ method: 'GET', @@ -689,4 +709,43 @@ describe('listLifecycleNonCurrents', () => { return done(); }); }); + + // Regression test: a scan-limit truncation that stops on a "bare master" + // (a pre-versioning object with no internal versionId) returns a + // version-id-marker equals to 'null'. On the following listing, + // CloudServer should treat it as "no marker" instead of decoding it. + // Decoding it was throwing and crashing the worker. + it('should treat a version-id-marker of "null" as no marker without crashing', done => { + makeBackbeatRequest({ + method: 'GET', + bucket: testBucket, + queryObj: { + 'list-type': 'noncurrent', + 'before-date': date, + 'key-marker': 'key1', + 'version-id-marker': 'null', + }, + authCredentials: credentials, + }, (err, response) => { + assert.ifError(err); + // Before the fix this request crashed the worker; assert it succeeds. + assert.strictEqual(response.statusCode, 200); + const data = JSON.parse(response.body); + + assert.strictEqual(data.KeyMarker, 'key1'); + // 'null' is echoed back unchanged and treated as no marker, so the + // listing resumes after key1 and returns key2's noncurrent versions. + assert.strictEqual(data.IsTruncated, false); + assert(!data.NextKeyMarker); + + const contents = data.Contents; + checkContents(contents); + assert.strictEqual(contents.length, expectedKey2VersionIds.length); + contents.forEach((c, i) => { + assert.strictEqual(c.Key, 'key2'); + assert.strictEqual(c.VersionId, expectedKey2VersionIds[i]); + }); + return done(); + }); + }); }); diff --git a/tests/unit/api/apiUtils/lifecycle.js b/tests/unit/api/apiUtils/lifecycle.js index 209aaf4403..f931e5ac0e 100644 --- a/tests/unit/api/apiUtils/lifecycle.js +++ b/tests/unit/api/apiUtils/lifecycle.js @@ -1,7 +1,13 @@ const assert = require('assert'); -const { validateMaxScannedEntries } = +const { versioning } = require('arsenal'); +const { validateMaxScannedEntries, decodeVersionIdMarker } = require('../../../../lib/api/apiUtils/object/lifecycle'); +const versionIdUtils = versioning.VersionID; +// A valid encoded marker that round-trips to this internal version id. +const validInternalVid = '98765432109876999999PARIS00'; +const validEncodedMarker = versionIdUtils.encode(validInternalVid); + const tests = [ { it: 'should return config value if no query params set', @@ -48,3 +54,23 @@ describe('validateMaxScannedEntries helper', () => { }); }); }); + +describe('decodeVersionIdMarker helper', () => { + ['null', '', undefined].forEach(vid => { + it(`should treat ${JSON.stringify(vid)} as no marker`, () => { + assert.strictEqual(decodeVersionIdMarker(vid), undefined); + }); + }); + + it('should decode a valid encoded marker', () => { + assert.strictEqual(decodeVersionIdMarker(validEncodedMarker), validInternalVid); + }); + + it('should return InvalidArgument when decode returns an Error value', () => { + // a malformed (non-null) marker that decode rejects by returning an Error + const result = decodeVersionIdMarker('@@@bad@@@'); + assert(result instanceof Error); + assert.strictEqual(result.message, 'InvalidArgument'); + assert.strictEqual(result.description, 'Invalid version id marker specified'); + }); +}); From b807757d28887b5a1d4f897ec38a01dda3871752 Mon Sep 17 00:00:00 2001 From: Nicolas Humbert Date: Wed, 24 Jun 2026 10:53:21 +0200 Subject: [PATCH 2/3] CLDSRV-936 prettier: format lifecycle files Apply Prettier formatting to the four files touched by this branch so the prettier:diff CI check passes. Pure formatting, no logic changes. --- lib/api/apiUtils/object/lifecycle.js | 33 +- lib/api/backbeat/listLifecycleNonCurrents.js | 42 +- .../backbeat/listLifecycleNonCurrents.js | 1317 +++++++++-------- tests/unit/api/apiUtils/lifecycle.js | 3 +- 4 files changed, 764 insertions(+), 631 deletions(-) diff --git a/lib/api/apiUtils/object/lifecycle.js b/lib/api/apiUtils/object/lifecycle.js index 0fc8f830e7..bc927898d1 100644 --- a/lib/api/apiUtils/object/lifecycle.js +++ b/lib/api/apiUtils/object/lifecycle.js @@ -7,12 +7,11 @@ const { CURRENT_TYPE, NON_CURRENT_TYPE, ORPHAN_DM_TYPE } = lifecycleListing; function _makeTags(tags) { const res = []; Object.entries(tags).forEach(([key, value]) => - res.push( - { - Key: key, - Value: value, - } - )); + res.push({ + Key: key, + Value: value, + }), + ); return res; } @@ -51,8 +50,7 @@ function processCurrents(bucketName, listParams, isBucketVersioned, list) { // NOTE: The current versions listed to be lifecycle should include version id // if the bucket is versioned. if (isBucketVersioned) { - const versionId = (v.IsNull || v.VersionId === undefined) ? - 'null' : versionIdUtils.encode(v.VersionId); + const versionId = v.IsNull || v.VersionId === undefined ? 'null' : versionIdUtils.encode(v.VersionId); content.VersionId = versionId; } @@ -103,8 +101,7 @@ function processNonCurrents(bucketName, listParams, list) { list.Contents.forEach(item => { const v = item.value; - const versionId = (v.IsNull || v.VersionId === undefined) ? - 'null' : versionIdUtils.encode(v.VersionId); + const versionId = v.IsNull || v.VersionId === undefined ? 'null' : versionIdUtils.encode(v.VersionId); const content = { Key: item.key, @@ -144,8 +141,7 @@ function processOrphans(bucketName, listParams, list) { list.Contents.forEach(item => { const v = item.value; - const versionId = (v.IsNull || v.VersionId === undefined) ? - 'null' : versionIdUtils.encode(v.VersionId); + const versionId = v.IsNull || v.VersionId === undefined ? 'null' : versionIdUtils.encode(v.VersionId); data.Contents.push({ Key: item.key, LastModified: v.LastModified, @@ -163,8 +159,10 @@ function processOrphans(bucketName, listParams, list) { } function getLocationConstraintErrorMessage(locationName) { - return 'value of the location you are attempting to set ' + - `- ${locationName} - is not listed in the locationConstraint config`; + return ( + 'value of the location you are attempting to set ' + + `- ${locationName} - is not listed in the locationConstraint config` + ); } /** @@ -183,8 +181,11 @@ function validateMaxScannedEntries(params, config, min) { if (params['max-scanned-lifecycle-listing-entries']) { const maxEntriesParams = Number.parseInt(params['max-scanned-lifecycle-listing-entries'], 10); - if (Number.isNaN(maxEntriesParams) || maxEntriesParams < min || - maxEntriesParams > maxScannedLifecycleListingEntries) { + if ( + Number.isNaN(maxEntriesParams) || + maxEntriesParams < min || + maxEntriesParams > maxScannedLifecycleListingEntries + ) { return { isValid: false }; } diff --git a/lib/api/backbeat/listLifecycleNonCurrents.js b/lib/api/backbeat/listLifecycleNonCurrents.js index ee9c9cfa38..25c8df90d5 100644 --- a/lib/api/backbeat/listLifecycleNonCurrents.js +++ b/lib/api/backbeat/listLifecycleNonCurrents.js @@ -4,12 +4,15 @@ const services = require('../../services'); const { standardMetadataValidateBucket } = require('../../metadata/metadataUtils'); const { pushMetric } = require('../../utapi/utilities'); const monitoring = require('../../utilities/monitoringHandler'); -const { getLocationConstraintErrorMessage, processNonCurrents, - validateMaxScannedEntries, decodeVersionIdMarker } = require('../apiUtils/object/lifecycle'); +const { + getLocationConstraintErrorMessage, + processNonCurrents, + validateMaxScannedEntries, + decodeVersionIdMarker, +} = require('../apiUtils/object/lifecycle'); const { config } = require('../../Config'); -function handleResult(listParams, requestMaxKeys, authInfo, - bucketName, list, log, callback) { +function handleResult(listParams, requestMaxKeys, authInfo, bucketName, list, log, callback) { // eslint-disable-next-line no-param-reassign listParams.maxKeys = requestMaxKeys; const res = processNonCurrents(bucketName, listParams, list); @@ -35,11 +38,9 @@ function listLifecycleNonCurrents(authInfo, locationConstraints, request, log, c const bucketName = request.bucketName; log.debug('processing request', { method: 'listLifecycleNonCurrents' }); - const requestMaxKeys = params['max-keys'] ? - Number.parseInt(params['max-keys'], 10) : 1000; + const requestMaxKeys = params['max-keys'] ? Number.parseInt(params['max-keys'], 10) : 1000; if (Number.isNaN(requestMaxKeys) || requestMaxKeys < 0) { - monitoring.promMetrics( - 'GET', bucketName, 400, 'listLifecycleNonCurrents'); + monitoring.promMetrics('GET', bucketName, 400, 'listLifecycleNonCurrents'); return callback(errors.InvalidArgument); } const actualMaxKeys = Math.min(constants.listingHardLimit, requestMaxKeys); @@ -47,8 +48,11 @@ function listLifecycleNonCurrents(authInfo, locationConstraints, request, log, c // 3 is required as a minimum because we must scan at least three entries to determine version eligibility. // Two entries representing the master key and the following one representing the non-current version. const minEntriesToBeScanned = 3; - const { isValid, maxScannedLifecycleListingEntries } = - validateMaxScannedEntries(params, config, minEntriesToBeScanned); + const { isValid, maxScannedLifecycleListingEntries } = validateMaxScannedEntries( + params, + config, + minEntriesToBeScanned, + ); if (!isValid) { monitoring.promMetrics('GET', bucketName, 400, 'listLifecycleNonCurrents'); return callback(errors.InvalidArgument); @@ -90,8 +94,7 @@ function listLifecycleNonCurrents(authInfo, locationConstraints, request, log, c return standardMetadataValidateBucket(metadataValParams, request.actionImplicitDenies, log, (err, bucket) => { if (err) { log.debug('error processing request', { method: 'metadataValidateBucket', error: err }); - monitoring.promMetrics( - 'GET', bucketName, err.code, 'listLifecycleNonCurrents'); + monitoring.promMetrics('GET', bucketName, err.code, 'listLifecycleNonCurrents'); return callback(err, null); } @@ -99,8 +102,7 @@ function listLifecycleNonCurrents(authInfo, locationConstraints, request, log, c const isBucketVersioned = vcfg && (vcfg.Status === 'Enabled' || vcfg.Status === 'Suspended'); if (!isBucketVersioned) { log.debug('bucket is not versioned'); - return callback(errorInstances.InvalidRequest.customizeDescription( - 'bucket is not versioned'), null); + return callback(errorInstances.InvalidRequest.customizeDescription('bucket is not versioned'), null); } if (!requestMaxKeys) { @@ -108,20 +110,16 @@ function listLifecycleNonCurrents(authInfo, locationConstraints, request, log, c Contents: [], IsTruncated: false, }; - return handleResult(listParams, requestMaxKeys, authInfo, - bucketName, emptyList, log, callback); + return handleResult(listParams, requestMaxKeys, authInfo, bucketName, emptyList, log, callback); } - return services.getLifecycleListing(bucketName, listParams, log, - (err, list) => { + return services.getLifecycleListing(bucketName, listParams, log, (err, list) => { if (err) { log.debug('error processing request', { method: 'services.getLifecycleListing', error: err }); - monitoring.promMetrics( - 'GET', bucketName, err.code, 'listLifecycleNonCurrents'); + monitoring.promMetrics('GET', bucketName, err.code, 'listLifecycleNonCurrents'); return callback(err, null); } - return handleResult(listParams, requestMaxKeys, authInfo, - bucketName, list, log, callback); + return handleResult(listParams, requestMaxKeys, authInfo, bucketName, list, log, callback); }); }); } diff --git a/tests/functional/backbeat/listLifecycleNonCurrents.js b/tests/functional/backbeat/listLifecycleNonCurrents.js index d3ebc0d724..46d69c06f6 100644 --- a/tests/functional/backbeat/listLifecycleNonCurrents.js +++ b/tests/functional/backbeat/listLifecycleNonCurrents.js @@ -1,9 +1,11 @@ const assert = require('assert'); const async = require('async'); -const { CreateBucketCommand, +const { + CreateBucketCommand, DeleteBucketCommand, PutBucketVersioningCommand, - PutObjectCommand } = require('@aws-sdk/client-s3'); + PutObjectCommand, +} = require('@aws-sdk/client-s3'); const BucketUtility = require('../aws-node-sdk/lib/utility/bucket-util'); const { removeAllVersions } = require('../aws-node-sdk/lib/utility/versioning-util'); const { makeBackbeatRequest } = require('./utils'); @@ -50,10 +52,12 @@ function checkContents(contents) { assert(d.VersionId); assert(d.staleDate); assert(!d.IsLatest); - assert.deepStrictEqual(d.TagSet, [{ - Key: 'mykey', - Value: 'myvalue', - }]); + assert.deepStrictEqual(d.TagSet, [ + { + Key: 'mykey', + Value: 'myvalue', + }, + ]); assert.strictEqual(d.DataStoreName, location); assert.strictEqual(d.ListType, 'noncurrent'); assert.strictEqual(d.Size, 3); @@ -65,105 +69,147 @@ describe('listLifecycleNonCurrents', () => { let expectedKey1VersionIds = []; let expectedKey2VersionIds = []; - before(done => async.series([ - next => { - getCredentials() - .then(creds => { - credentials = creds; - return getS3Hostname(); - }) - .then(() => next()) - .catch(next); - }, - next => { - s3.send(new CreateBucketCommand({ Bucket: testBucket })) - .then(() => next()) - .catch(next); - }, - next => { - s3.send(new CreateBucketCommand({ Bucket: emptyBucket })) - .then(() => next()) - .catch(next); - }, - next => { - s3.send(new CreateBucketCommand({ Bucket: nonVersionedBucket })) - .then(() => next()) - .catch(next); - }, - next => { - s3.send(new PutBucketVersioningCommand({ - Bucket: testBucket, - VersioningConfiguration: { Status: 'Enabled' }, - })) - .then(() => next()) - .catch(next); - }, - next => { - s3.send(new PutBucketVersioningCommand({ - Bucket: emptyBucket, - VersioningConfiguration: { Status: 'Enabled' }, - })) - .then(() => next()) - .catch(next); - }, - next => async.timesSeries(3, (n, cb) => { - s3.send(new PutObjectCommand({ - Bucket: testBucket, - Key: 'key1', - Body: '123', - Tagging: 'mykey=myvalue' - })) - .then(data => cb(null, data)) - .catch(cb); - }, (err, res) => { - // Only the two first ones are kept, since the stale date of the last one (3rd) - // Will be the last-modified of the next one (4th) that is created after the "date". - // The array is reverse since, for a specific key, we expect the listing to be ordered - // by last-modified date in descending order due to the way version id is generated. - expectedKey1VersionIds = res.map(r => r.VersionId).slice(0, 2).reverse(); - return next(err); - }), - next => async.timesSeries(3, (n, cb) => { - s3.send(new PutObjectCommand({ - Bucket: testBucket, - Key: 'key2', - Body: '123', - Tagging: 'mykey=myvalue' - })) - .then(data => cb(null, data)) - .catch(cb); - }, (err, res) => { - // Only the two first ones are kept, since the stale date of the last one (3rd) - // Will be the last-modified of the next one (4th) that is created after the "date". - // The array is reverse since, for a specific key, we expect the listing to be ordered - // by last-modified date in descending order due to the way version id is generated. - expectedKey2VersionIds = res.map(r => r.VersionId).slice(0, 2).reverse(); - return next(err); - }), - next => { - date = new Date(Date.now()).toISOString(); - return async.times(5, (n, cb) => { - s3.send(new PutObjectCommand({ - Bucket: testBucket, - Key: 'key1', - Body: '123', - Tagging: 'mykey=myvalue' - })) - .then(() => cb()) - .catch(cb); - }, next); - }, - next => async.times(5, (n, cb) => { - s3.send(new PutObjectCommand({ - Bucket: testBucket, - Key: 'key2', - Body: '123', - Tagging: 'mykey=myvalue' - })) - .then(() => cb()) - .catch(cb); - }, next), - ], done)); + before(done => + async.series( + [ + next => { + getCredentials() + .then(creds => { + credentials = creds; + return getS3Hostname(); + }) + .then(() => next()) + .catch(next); + }, + next => { + s3.send(new CreateBucketCommand({ Bucket: testBucket })) + .then(() => next()) + .catch(next); + }, + next => { + s3.send(new CreateBucketCommand({ Bucket: emptyBucket })) + .then(() => next()) + .catch(next); + }, + next => { + s3.send(new CreateBucketCommand({ Bucket: nonVersionedBucket })) + .then(() => next()) + .catch(next); + }, + next => { + s3.send( + new PutBucketVersioningCommand({ + Bucket: testBucket, + VersioningConfiguration: { Status: 'Enabled' }, + }), + ) + .then(() => next()) + .catch(next); + }, + next => { + s3.send( + new PutBucketVersioningCommand({ + Bucket: emptyBucket, + VersioningConfiguration: { Status: 'Enabled' }, + }), + ) + .then(() => next()) + .catch(next); + }, + next => + async.timesSeries( + 3, + (n, cb) => { + s3.send( + new PutObjectCommand({ + Bucket: testBucket, + Key: 'key1', + Body: '123', + Tagging: 'mykey=myvalue', + }), + ) + .then(data => cb(null, data)) + .catch(cb); + }, + (err, res) => { + // Only the two first ones are kept, since the stale date of the last one (3rd) + // Will be the last-modified of the next one (4th) that is created after the "date". + // The array is reverse since, for a specific key, we expect the listing to be ordered + // by last-modified date in descending order due to the way version id is generated. + expectedKey1VersionIds = res + .map(r => r.VersionId) + .slice(0, 2) + .reverse(); + return next(err); + }, + ), + next => + async.timesSeries( + 3, + (n, cb) => { + s3.send( + new PutObjectCommand({ + Bucket: testBucket, + Key: 'key2', + Body: '123', + Tagging: 'mykey=myvalue', + }), + ) + .then(data => cb(null, data)) + .catch(cb); + }, + (err, res) => { + // Only the two first ones are kept, since the stale date of the last one (3rd) + // Will be the last-modified of the next one (4th) that is created after the "date". + // The array is reverse since, for a specific key, we expect the listing to be ordered + // by last-modified date in descending order due to the way version id is generated. + expectedKey2VersionIds = res + .map(r => r.VersionId) + .slice(0, 2) + .reverse(); + return next(err); + }, + ), + next => { + date = new Date(Date.now()).toISOString(); + return async.times( + 5, + (n, cb) => { + s3.send( + new PutObjectCommand({ + Bucket: testBucket, + Key: 'key1', + Body: '123', + Tagging: 'mykey=myvalue', + }), + ) + .then(() => cb()) + .catch(cb); + }, + next, + ); + }, + next => + async.times( + 5, + (n, cb) => { + s3.send( + new PutObjectCommand({ + Bucket: testBucket, + Key: 'key2', + Body: '123', + Tagging: 'mykey=myvalue', + }), + ) + .then(() => cb()) + .catch(cb); + }, + next, + ), + ], + done, + ), + ); after(async () => { await removeAllVersionsPromise({ Bucket: testBucket }); @@ -173,579 +219,668 @@ describe('listLifecycleNonCurrents', () => { }); it('should return empty list of noncurrent versions if bucket is empty', done => { - makeBackbeatRequest({ - method: 'GET', - bucket: emptyBucket, - queryObj: { 'list-type': 'noncurrent' }, - authCredentials: credentials, - }, (err, response) => { - assert.ifError(err); - assert.strictEqual(response.statusCode, 200); - const data = JSON.parse(response.body); - - assert.strictEqual(data.IsTruncated, false); - assert(!data.NextKeyMarker); - assert.strictEqual(data.MaxKeys, 1000); - assert.strictEqual(data.MaxScannedLifecycleListingEntries, config.maxScannedLifecycleListingEntries); - assert.strictEqual(data.Contents.length, 0); - return done(); - }); + makeBackbeatRequest( + { + method: 'GET', + bucket: emptyBucket, + queryObj: { 'list-type': 'noncurrent' }, + authCredentials: credentials, + }, + (err, response) => { + assert.ifError(err); + assert.strictEqual(response.statusCode, 200); + const data = JSON.parse(response.body); + + assert.strictEqual(data.IsTruncated, false); + assert(!data.NextKeyMarker); + assert.strictEqual(data.MaxKeys, 1000); + assert.strictEqual(data.MaxScannedLifecycleListingEntries, config.maxScannedLifecycleListingEntries); + assert.strictEqual(data.Contents.length, 0); + return done(); + }, + ); }); it('should return empty list of noncurrent versions if prefix does not apply', done => { - makeBackbeatRequest({ - method: 'GET', - bucket: testBucket, - queryObj: { 'list-type': 'noncurrent', 'prefix': 'unknown' }, - authCredentials: credentials, - }, (err, response) => { - assert.ifError(err); - assert.strictEqual(response.statusCode, 200); - const data = JSON.parse(response.body); - - assert.strictEqual(data.IsTruncated, false); - assert(!data.NextKeyMarker); - assert.strictEqual(data.MaxKeys, 1000); - assert.strictEqual(data.MaxScannedLifecycleListingEntries, config.maxScannedLifecycleListingEntries); - assert.strictEqual(data.Contents.length, 0); - return done(); - }); + makeBackbeatRequest( + { + method: 'GET', + bucket: testBucket, + queryObj: { 'list-type': 'noncurrent', prefix: 'unknown' }, + authCredentials: credentials, + }, + (err, response) => { + assert.ifError(err); + assert.strictEqual(response.statusCode, 200); + const data = JSON.parse(response.body); + + assert.strictEqual(data.IsTruncated, false); + assert(!data.NextKeyMarker); + assert.strictEqual(data.MaxKeys, 1000); + assert.strictEqual(data.MaxScannedLifecycleListingEntries, config.maxScannedLifecycleListingEntries); + assert.strictEqual(data.Contents.length, 0); + return done(); + }, + ); }); it('should return empty list if max-keys is set to 0', done => { - makeBackbeatRequest({ - method: 'GET', - bucket: testBucket, - queryObj: { 'list-type': 'noncurrent', 'max-keys': '0' }, - authCredentials: credentials, - }, (err, response) => { - assert.ifError(err); - assert.strictEqual(response.statusCode, 200); - const data = JSON.parse(response.body); - - assert.strictEqual(data.IsTruncated, false); - assert(!data.NextMarker); - assert.strictEqual(data.MaxKeys, 0); - assert.strictEqual(data.MaxScannedLifecycleListingEntries, config.maxScannedLifecycleListingEntries); - assert.strictEqual(data.Contents.length, 0); - - return done(); - }); + makeBackbeatRequest( + { + method: 'GET', + bucket: testBucket, + queryObj: { 'list-type': 'noncurrent', 'max-keys': '0' }, + authCredentials: credentials, + }, + (err, response) => { + assert.ifError(err); + assert.strictEqual(response.statusCode, 200); + const data = JSON.parse(response.body); + + assert.strictEqual(data.IsTruncated, false); + assert(!data.NextMarker); + assert.strictEqual(data.MaxKeys, 0); + assert.strictEqual(data.MaxScannedLifecycleListingEntries, config.maxScannedLifecycleListingEntries); + assert.strictEqual(data.Contents.length, 0); + + return done(); + }, + ); }); it('should return error if bucket does not exist', done => { - makeBackbeatRequest({ - method: 'GET', - bucket: 'idonotexist', - queryObj: { 'list-type': 'noncurrent' }, - authCredentials: credentials, - }, err => { - assert.strictEqual(err.code, 'NoSuchBucket'); - return done(); - }); + makeBackbeatRequest( + { + method: 'GET', + bucket: 'idonotexist', + queryObj: { 'list-type': 'noncurrent' }, + authCredentials: credentials, + }, + err => { + assert.strictEqual(err.code, 'NoSuchBucket'); + return done(); + }, + ); }); it('should return BadRequest error if list type is empty', done => { - makeBackbeatRequest({ - method: 'GET', - bucket: testBucket, - queryObj: { 'list-type': '' }, - authCredentials: credentials, - }, err => { - assert.strictEqual(err.code, 'BadRequest'); - return done(); - }); + makeBackbeatRequest( + { + method: 'GET', + bucket: testBucket, + queryObj: { 'list-type': '' }, + authCredentials: credentials, + }, + err => { + assert.strictEqual(err.code, 'BadRequest'); + return done(); + }, + ); }); it('should return BadRequest error if list type is invalid', done => { - makeBackbeatRequest({ - method: 'GET', - bucket: testBucket, - queryObj: { 'list-type': 'invalid' }, - authCredentials: credentials, - }, err => { - assert.strictEqual(err.code, 'BadRequest'); - return done(); - }); + makeBackbeatRequest( + { + method: 'GET', + bucket: testBucket, + queryObj: { 'list-type': 'invalid' }, + authCredentials: credentials, + }, + err => { + assert.strictEqual(err.code, 'BadRequest'); + return done(); + }, + ); }); it('should return InvalidArgument error if max-keys is invalid', done => { - makeBackbeatRequest({ - method: 'GET', - bucket: testBucket, - queryObj: { 'list-type': 'noncurrent', 'max-keys': 'a' }, - authCredentials: credentials, - }, err => { - assert.strictEqual(err.code, 'InvalidArgument'); - return done(); - }); + makeBackbeatRequest( + { + method: 'GET', + bucket: testBucket, + queryObj: { 'list-type': 'noncurrent', 'max-keys': 'a' }, + authCredentials: credentials, + }, + err => { + assert.strictEqual(err.code, 'InvalidArgument'); + return done(); + }, + ); }); it('should return InvalidArgument error if max-scanned-lifecycle-listing-entries is invalid', done => { - makeBackbeatRequest({ - method: 'GET', - bucket: testBucket, - queryObj: { 'list-type': 'noncurrent', 'max-scanned-lifecycle-listing-entries': 'a' }, - authCredentials: credentials, - }, err => { - assert.strictEqual(err.code, 'InvalidArgument'); - return done(); - }); + makeBackbeatRequest( + { + method: 'GET', + bucket: testBucket, + queryObj: { 'list-type': 'noncurrent', 'max-scanned-lifecycle-listing-entries': 'a' }, + authCredentials: credentials, + }, + err => { + assert.strictEqual(err.code, 'InvalidArgument'); + return done(); + }, + ); }); it('should return InvalidArgument error if max-scanned-lifecycle-listing-entries is set to 0', done => { - makeBackbeatRequest({ - method: 'GET', - bucket: testBucket, - queryObj: { 'list-type': 'noncurrent', 'max-scanned-lifecycle-listing-entries': '0' }, - authCredentials: credentials, - }, err => { - assert.strictEqual(err.code, 'InvalidArgument'); - return done(); - }); + makeBackbeatRequest( + { + method: 'GET', + bucket: testBucket, + queryObj: { 'list-type': 'noncurrent', 'max-scanned-lifecycle-listing-entries': '0' }, + authCredentials: credentials, + }, + err => { + assert.strictEqual(err.code, 'InvalidArgument'); + return done(); + }, + ); }); it('should return InvalidArgument error if max-scanned-lifecycle-listing-entries is set to 2', done => { - makeBackbeatRequest({ - method: 'GET', - bucket: testBucket, - queryObj: { 'list-type': 'noncurrent', 'max-scanned-lifecycle-listing-entries': '2' }, - authCredentials: credentials, - }, err => { - assert.strictEqual(err.code, 'InvalidArgument'); - return done(); - }); + makeBackbeatRequest( + { + method: 'GET', + bucket: testBucket, + queryObj: { 'list-type': 'noncurrent', 'max-scanned-lifecycle-listing-entries': '2' }, + authCredentials: credentials, + }, + err => { + assert.strictEqual(err.code, 'InvalidArgument'); + return done(); + }, + ); }); it('should return InvalidArgument if max-scanned-lifecycle-listing-entries exceeds the default value', done => { - makeBackbeatRequest({ - method: 'GET', - bucket: testBucket, - queryObj: { 'list-type': 'noncurrent', 'max-scanned-lifecycle-listing-entries': - (config.maxScannedLifecycleListingEntries + 1).toString() }, - authCredentials: credentials, - }, err => { - assert.strictEqual(err.code, 'InvalidArgument'); - return done(); - }); + makeBackbeatRequest( + { + method: 'GET', + bucket: testBucket, + queryObj: { + 'list-type': 'noncurrent', + 'max-scanned-lifecycle-listing-entries': (config.maxScannedLifecycleListingEntries + 1).toString(), + }, + authCredentials: credentials, + }, + err => { + assert.strictEqual(err.code, 'InvalidArgument'); + return done(); + }, + ); }); it('should return InvalidArgument error if version-id-marker is malformed', done => { - makeBackbeatRequest({ - method: 'GET', - bucket: testBucket, - queryObj: { - 'list-type': 'noncurrent', - 'key-marker': 'key1', - 'version-id-marker': 'notavalidversionid', - }, - authCredentials: credentials, - }, err => { - // A non-null, undecodable marker must be rejected with a 400 - // rather than reaching _encodeVersionId and crashing the worker. - assert(err); - assert.strictEqual(err.code, 'InvalidArgument'); - assert.strictEqual(err.statusCode, 400); - return done(); - }); + makeBackbeatRequest( + { + method: 'GET', + bucket: testBucket, + queryObj: { + 'list-type': 'noncurrent', + 'key-marker': 'key1', + 'version-id-marker': 'notavalidversionid', + }, + authCredentials: credentials, + }, + err => { + // A non-null, undecodable marker must be rejected with a 400 + // rather than reaching _encodeVersionId and crashing the worker. + assert(err); + assert.strictEqual(err.code, 'InvalidArgument'); + assert.strictEqual(err.statusCode, 400); + return done(); + }, + ); }); it('should return error if bucket not versioned', done => { - makeBackbeatRequest({ - method: 'GET', - bucket: nonVersionedBucket, - queryObj: { 'list-type': 'noncurrent' }, - authCredentials: credentials, - }, err => { - assert.strictEqual(err.code, 'InvalidRequest'); - return done(); - }); + makeBackbeatRequest( + { + method: 'GET', + bucket: nonVersionedBucket, + queryObj: { 'list-type': 'noncurrent' }, + authCredentials: credentials, + }, + err => { + assert.strictEqual(err.code, 'InvalidRequest'); + return done(); + }, + ); }); it('should return all the noncurrent versions', done => { - makeBackbeatRequest({ - method: 'GET', - bucket: testBucket, - queryObj: { 'list-type': 'noncurrent' }, - authCredentials: credentials, - }, (err, response) => { - assert.ifError(err); - assert.strictEqual(response.statusCode, 200); - const data = JSON.parse(response.body); - - assert.strictEqual(data.IsTruncated, false); - assert(!data.NextKeyMarker); - assert.strictEqual(data.MaxKeys, 1000); - assert.strictEqual(data.MaxScannedLifecycleListingEntries, config.maxScannedLifecycleListingEntries); - - const contents = data.Contents; - assert.strictEqual(contents.length, 14); - checkContents(contents); - - return done(); - }); + makeBackbeatRequest( + { + method: 'GET', + bucket: testBucket, + queryObj: { 'list-type': 'noncurrent' }, + authCredentials: credentials, + }, + (err, response) => { + assert.ifError(err); + assert.strictEqual(response.statusCode, 200); + const data = JSON.parse(response.body); + + assert.strictEqual(data.IsTruncated, false); + assert(!data.NextKeyMarker); + assert.strictEqual(data.MaxKeys, 1000); + assert.strictEqual(data.MaxScannedLifecycleListingEntries, config.maxScannedLifecycleListingEntries); + + const contents = data.Contents; + assert.strictEqual(contents.length, 14); + checkContents(contents); + + return done(); + }, + ); }); it('should return all the noncurrent versions scanned before max scanned entries value is reached', done => { - makeBackbeatRequest({ - method: 'GET', - bucket: testBucket, - queryObj: { 'list-type': 'noncurrent', 'max-scanned-lifecycle-listing-entries': '9' }, - authCredentials: credentials, - }, (err, response) => { - assert.ifError(err); - assert.strictEqual(response.statusCode, 200); - const data = JSON.parse(response.body); - - assert.strictEqual(data.IsTruncated, true); - assert.strictEqual(data.NextKeyMarker, 'key1'); - assert.strictEqual(data.MaxKeys, 1000); - assert.strictEqual(data.MaxScannedLifecycleListingEntries, 9); - - const contents = data.Contents; - // only return the first 7 entries as the master version is denoted by 2 entries. - assert.strictEqual(contents.length, 7); - assert(contents.every(c => c.Key === 'key1')); - checkContents(contents); - - return done(); - }); + makeBackbeatRequest( + { + method: 'GET', + bucket: testBucket, + queryObj: { 'list-type': 'noncurrent', 'max-scanned-lifecycle-listing-entries': '9' }, + authCredentials: credentials, + }, + (err, response) => { + assert.ifError(err); + assert.strictEqual(response.statusCode, 200); + const data = JSON.parse(response.body); + + assert.strictEqual(data.IsTruncated, true); + assert.strictEqual(data.NextKeyMarker, 'key1'); + assert.strictEqual(data.MaxKeys, 1000); + assert.strictEqual(data.MaxScannedLifecycleListingEntries, 9); + + const contents = data.Contents; + // only return the first 7 entries as the master version is denoted by 2 entries. + assert.strictEqual(contents.length, 7); + assert(contents.every(c => c.Key === 'key1')); + checkContents(contents); + + return done(); + }, + ); }); it('should return all the noncurrent versions with prefix key1', done => { const prefix = 'key1'; - makeBackbeatRequest({ - method: 'GET', - bucket: testBucket, - queryObj: { 'list-type': 'noncurrent', prefix }, - authCredentials: credentials, - }, (err, response) => { - assert.ifError(err); - assert.strictEqual(response.statusCode, 200); - const data = JSON.parse(response.body); - - assert.strictEqual(data.IsTruncated, false); - assert(!data.NextKeyMarker); - assert.strictEqual(data.MaxKeys, 1000); - assert.strictEqual(data.MaxScannedLifecycleListingEntries, config.maxScannedLifecycleListingEntries); - assert.strictEqual(data.Prefix, prefix); - - const contents = data.Contents; - assert.strictEqual(contents.length, 7); - assert(contents.every(d => d.Key === 'key1')); - checkContents(contents); - - return done(); - }); + makeBackbeatRequest( + { + method: 'GET', + bucket: testBucket, + queryObj: { 'list-type': 'noncurrent', prefix }, + authCredentials: credentials, + }, + (err, response) => { + assert.ifError(err); + assert.strictEqual(response.statusCode, 200); + const data = JSON.parse(response.body); + + assert.strictEqual(data.IsTruncated, false); + assert(!data.NextKeyMarker); + assert.strictEqual(data.MaxKeys, 1000); + assert.strictEqual(data.MaxScannedLifecycleListingEntries, config.maxScannedLifecycleListingEntries); + assert.strictEqual(data.Prefix, prefix); + + const contents = data.Contents; + assert.strictEqual(contents.length, 7); + assert(contents.every(d => d.Key === 'key1')); + checkContents(contents); + + return done(); + }, + ); }); it('should return all the noncurrent versions with prefix key1 before a defined date', done => { const prefix = 'key1'; - makeBackbeatRequest({ - method: 'GET', - bucket: testBucket, - queryObj: { 'list-type': 'noncurrent', prefix, 'before-date': date }, - authCredentials: credentials, - }, (err, response) => { - assert.ifError(err); - assert.strictEqual(response.statusCode, 200); - const data = JSON.parse(response.body); + makeBackbeatRequest( + { + method: 'GET', + bucket: testBucket, + queryObj: { 'list-type': 'noncurrent', prefix, 'before-date': date }, + authCredentials: credentials, + }, + (err, response) => { + assert.ifError(err); + assert.strictEqual(response.statusCode, 200); + const data = JSON.parse(response.body); - assert.strictEqual(data.IsTruncated, false); - assert(!data.NextKeyMarker); - assert.strictEqual(data.MaxKeys, 1000); - assert.strictEqual(data.MaxScannedLifecycleListingEntries, config.maxScannedLifecycleListingEntries); - assert.strictEqual(data.Prefix, prefix); + assert.strictEqual(data.IsTruncated, false); + assert(!data.NextKeyMarker); + assert.strictEqual(data.MaxKeys, 1000); + assert.strictEqual(data.MaxScannedLifecycleListingEntries, config.maxScannedLifecycleListingEntries); + assert.strictEqual(data.Prefix, prefix); - const contents = data.Contents; - assert.strictEqual(contents.length, 2); - assert(contents.every(d => d.Key === 'key1')); + const contents = data.Contents; + assert.strictEqual(contents.length, 2); + assert(contents.every(d => d.Key === 'key1')); - assert.deepStrictEqual(contents.map(v => v.VersionId), expectedKey1VersionIds); + assert.deepStrictEqual( + contents.map(v => v.VersionId), + expectedKey1VersionIds, + ); - checkContents(contents); + checkContents(contents); - return done(); - }); + return done(); + }, + ); }); it('should return the noncurrent version with prefix, before a defined date and after key-marker', done => { const prefix = 'key2'; - makeBackbeatRequest({ - method: 'GET', - bucket: testBucket, - queryObj: { - 'list-type': 'noncurrent', - prefix, - 'before-date': date, - 'key-marker': 'key1', - }, - authCredentials: credentials, - }, (err, response) => { - assert.ifError(err); - assert.strictEqual(response.statusCode, 200); - const data = JSON.parse(response.body); - - assert.strictEqual(data.IsTruncated, false); - assert(!data.NextKeyMarker); - assert.strictEqual(data.MaxKeys, 1000); - assert.strictEqual(data.MaxScannedLifecycleListingEntries, config.maxScannedLifecycleListingEntries); - assert.strictEqual(data.Prefix, prefix); - - const contents = data.Contents; - assert.strictEqual(contents.length, 2); - assert(contents.every(d => d.Key === 'key2')); - - assert.deepStrictEqual(contents.map(v => v.VersionId), expectedKey2VersionIds); - - checkContents(contents); - - return done(); - }); + makeBackbeatRequest( + { + method: 'GET', + bucket: testBucket, + queryObj: { + 'list-type': 'noncurrent', + prefix, + 'before-date': date, + 'key-marker': 'key1', + }, + authCredentials: credentials, + }, + (err, response) => { + assert.ifError(err); + assert.strictEqual(response.statusCode, 200); + const data = JSON.parse(response.body); + + assert.strictEqual(data.IsTruncated, false); + assert(!data.NextKeyMarker); + assert.strictEqual(data.MaxKeys, 1000); + assert.strictEqual(data.MaxScannedLifecycleListingEntries, config.maxScannedLifecycleListingEntries); + assert.strictEqual(data.Prefix, prefix); + + const contents = data.Contents; + assert.strictEqual(contents.length, 2); + assert(contents.every(d => d.Key === 'key2')); + + assert.deepStrictEqual( + contents.map(v => v.VersionId), + expectedKey2VersionIds, + ); + + checkContents(contents); + + return done(); + }, + ); }); it('should return the noncurrent version with prefix, before a defined date, and after version-id-marker', done => { const prefix = 'key2'; - makeBackbeatRequest({ - method: 'GET', - bucket: testBucket, - queryObj: { - 'list-type': 'noncurrent', - prefix, - 'before-date': date, - 'key-marker': 'key2', - 'version-id-marker': expectedKey2VersionIds[0], - }, - authCredentials: credentials, - }, (err, response) => { - assert.ifError(err); - assert.strictEqual(response.statusCode, 200); - const data = JSON.parse(response.body); - - assert.strictEqual(data.IsTruncated, false); - assert(!data.NextKeyMarker); - assert.strictEqual(data.MaxKeys, 1000); - assert.strictEqual(data.MaxScannedLifecycleListingEntries, config.maxScannedLifecycleListingEntries); - assert.strictEqual(data.Prefix, prefix); - - const contents = data.Contents; - assert.strictEqual(contents.length, 1); - assert(contents.every(d => d.Key === 'key2')); - contents[0].Key = 'key2'; - contents[0].VersionId = expectedKey2VersionIds[1]; - - checkContents(contents); - - return done(); - }); + makeBackbeatRequest( + { + method: 'GET', + bucket: testBucket, + queryObj: { + 'list-type': 'noncurrent', + prefix, + 'before-date': date, + 'key-marker': 'key2', + 'version-id-marker': expectedKey2VersionIds[0], + }, + authCredentials: credentials, + }, + (err, response) => { + assert.ifError(err); + assert.strictEqual(response.statusCode, 200); + const data = JSON.parse(response.body); + + assert.strictEqual(data.IsTruncated, false); + assert(!data.NextKeyMarker); + assert.strictEqual(data.MaxKeys, 1000); + assert.strictEqual(data.MaxScannedLifecycleListingEntries, config.maxScannedLifecycleListingEntries); + assert.strictEqual(data.Prefix, prefix); + + const contents = data.Contents; + assert.strictEqual(contents.length, 1); + assert(contents.every(d => d.Key === 'key2')); + contents[0].Key = 'key2'; + contents[0].VersionId = expectedKey2VersionIds[1]; + + checkContents(contents); + + return done(); + }, + ); }); it('should return the non current versions before a defined date', done => { - makeBackbeatRequest({ - method: 'GET', - bucket: testBucket, - queryObj: { 'list-type': 'noncurrent', 'before-date': date }, - authCredentials: credentials, - }, (err, response) => { - assert.ifError(err); - assert.strictEqual(response.statusCode, 200); - const data = JSON.parse(response.body); - - assert.strictEqual(data.IsTruncated, false); - assert(!data.NextKeyMarker); - assert.strictEqual(data.MaxKeys, 1000); - assert.strictEqual(data.MaxScannedLifecycleListingEntries, config.maxScannedLifecycleListingEntries); - assert.strictEqual(data.BeforeDate, date); - - const contents = data.Contents; - assert.strictEqual(contents.length, 4); - checkContents(contents); - - const key1Versions = contents.filter(c => c.Key === 'key1'); - assert.strictEqual(key1Versions.length, 2); - - const key2Versions = contents.filter(c => c.Key === 'key2'); - assert.strictEqual(key2Versions.length, 2); - - assert.deepStrictEqual(key1Versions.map(v => v.VersionId), expectedKey1VersionIds); - assert.deepStrictEqual(key2Versions.map(v => v.VersionId), expectedKey2VersionIds); - - return done(); - }); + makeBackbeatRequest( + { + method: 'GET', + bucket: testBucket, + queryObj: { 'list-type': 'noncurrent', 'before-date': date }, + authCredentials: credentials, + }, + (err, response) => { + assert.ifError(err); + assert.strictEqual(response.statusCode, 200); + const data = JSON.parse(response.body); + + assert.strictEqual(data.IsTruncated, false); + assert(!data.NextKeyMarker); + assert.strictEqual(data.MaxKeys, 1000); + assert.strictEqual(data.MaxScannedLifecycleListingEntries, config.maxScannedLifecycleListingEntries); + assert.strictEqual(data.BeforeDate, date); + + const contents = data.Contents; + assert.strictEqual(contents.length, 4); + checkContents(contents); + + const key1Versions = contents.filter(c => c.Key === 'key1'); + assert.strictEqual(key1Versions.length, 2); + + const key2Versions = contents.filter(c => c.Key === 'key2'); + assert.strictEqual(key2Versions.length, 2); + + assert.deepStrictEqual( + key1Versions.map(v => v.VersionId), + expectedKey1VersionIds, + ); + assert.deepStrictEqual( + key2Versions.map(v => v.VersionId), + expectedKey2VersionIds, + ); + + return done(); + }, + ); }); it('should truncate list of non current versions before a defined date', done => { - makeBackbeatRequest({ - method: 'GET', - bucket: testBucket, - queryObj: { 'list-type': 'noncurrent', 'before-date': date, 'max-keys': '1' }, - authCredentials: credentials, - }, (err, response) => { - assert.ifError(err); - assert.strictEqual(response.statusCode, 200); - const data = JSON.parse(response.body); - - assert.strictEqual(data.IsTruncated, true); - assert.strictEqual(data.NextKeyMarker, 'key1'); - assert.strictEqual(data.NextVersionIdMarker, expectedKey1VersionIds[0]); - assert.strictEqual(data.MaxKeys, 1); - assert.strictEqual(data.MaxScannedLifecycleListingEntries, config.maxScannedLifecycleListingEntries); - assert.strictEqual(data.BeforeDate, date); - assert.strictEqual(data.Contents.length, 1); - - const contents = data.Contents; - checkContents(contents); - assert.strictEqual(contents[0].Key, 'key1'); - assert.strictEqual(contents[0].VersionId, expectedKey1VersionIds[0]); - return done(); - }); + makeBackbeatRequest( + { + method: 'GET', + bucket: testBucket, + queryObj: { 'list-type': 'noncurrent', 'before-date': date, 'max-keys': '1' }, + authCredentials: credentials, + }, + (err, response) => { + assert.ifError(err); + assert.strictEqual(response.statusCode, 200); + const data = JSON.parse(response.body); + + assert.strictEqual(data.IsTruncated, true); + assert.strictEqual(data.NextKeyMarker, 'key1'); + assert.strictEqual(data.NextVersionIdMarker, expectedKey1VersionIds[0]); + assert.strictEqual(data.MaxKeys, 1); + assert.strictEqual(data.MaxScannedLifecycleListingEntries, config.maxScannedLifecycleListingEntries); + assert.strictEqual(data.BeforeDate, date); + assert.strictEqual(data.Contents.length, 1); + + const contents = data.Contents; + checkContents(contents); + assert.strictEqual(contents[0].Key, 'key1'); + assert.strictEqual(contents[0].VersionId, expectedKey1VersionIds[0]); + return done(); + }, + ); }); it('should return the first following list of noncurrent versions before a defined date', done => { - makeBackbeatRequest({ - method: 'GET', - bucket: testBucket, - queryObj: { - 'list-type': 'noncurrent', - 'before-date': date, - 'max-keys': '1', - 'key-marker': 'key1', - 'version-id-marker': expectedKey1VersionIds[0], - }, - authCredentials: credentials, - }, (err, response) => { - assert.ifError(err); - assert.strictEqual(response.statusCode, 200); - const data = JSON.parse(response.body); - - assert.strictEqual(data.IsTruncated, true); - assert.strictEqual(data.KeyMarker, 'key1'); - assert.strictEqual(data.VersionIdMarker, expectedKey1VersionIds[0]); - assert.strictEqual(data.NextKeyMarker, 'key1'); - assert.strictEqual(data.NextVersionIdMarker, expectedKey1VersionIds[1]); - assert.strictEqual(data.MaxKeys, 1); - assert.strictEqual(data.MaxScannedLifecycleListingEntries, config.maxScannedLifecycleListingEntries); - assert.strictEqual(data.BeforeDate, date); - assert.strictEqual(data.Contents.length, 1); - - const contents = data.Contents; - checkContents(contents); - assert.strictEqual(contents[0].Key, 'key1'); - assert.strictEqual(contents[0].VersionId, expectedKey1VersionIds[1]); - return done(); - }); + makeBackbeatRequest( + { + method: 'GET', + bucket: testBucket, + queryObj: { + 'list-type': 'noncurrent', + 'before-date': date, + 'max-keys': '1', + 'key-marker': 'key1', + 'version-id-marker': expectedKey1VersionIds[0], + }, + authCredentials: credentials, + }, + (err, response) => { + assert.ifError(err); + assert.strictEqual(response.statusCode, 200); + const data = JSON.parse(response.body); + + assert.strictEqual(data.IsTruncated, true); + assert.strictEqual(data.KeyMarker, 'key1'); + assert.strictEqual(data.VersionIdMarker, expectedKey1VersionIds[0]); + assert.strictEqual(data.NextKeyMarker, 'key1'); + assert.strictEqual(data.NextVersionIdMarker, expectedKey1VersionIds[1]); + assert.strictEqual(data.MaxKeys, 1); + assert.strictEqual(data.MaxScannedLifecycleListingEntries, config.maxScannedLifecycleListingEntries); + assert.strictEqual(data.BeforeDate, date); + assert.strictEqual(data.Contents.length, 1); + + const contents = data.Contents; + checkContents(contents); + assert.strictEqual(contents[0].Key, 'key1'); + assert.strictEqual(contents[0].VersionId, expectedKey1VersionIds[1]); + return done(); + }, + ); }); it('should return the second following list of noncurrent versions before a defined date', done => { - makeBackbeatRequest({ - method: 'GET', - bucket: testBucket, - queryObj: { - 'list-type': 'noncurrent', - 'before-date': date, - 'max-keys': '1', - 'key-marker': 'key1', - 'version-id-marker': expectedKey1VersionIds[1], - }, - authCredentials: credentials, - }, (err, response) => { - assert.ifError(err); - assert.strictEqual(response.statusCode, 200); - const data = JSON.parse(response.body); - - assert.strictEqual(data.IsTruncated, true); - assert.strictEqual(data.KeyMarker, 'key1'); - assert.strictEqual(data.VersionIdMarker, expectedKey1VersionIds[1]); - assert.strictEqual(data.NextKeyMarker, 'key2'); - assert.strictEqual(data.NextVersionIdMarker, expectedKey2VersionIds[0]); - assert.strictEqual(data.MaxKeys, 1); - assert.strictEqual(data.MaxScannedLifecycleListingEntries, config.maxScannedLifecycleListingEntries); - assert.strictEqual(data.BeforeDate, date); - assert.strictEqual(data.Contents.length, 1); - - const contents = data.Contents; - checkContents(contents); - assert.strictEqual(contents[0].Key, 'key2'); - assert.strictEqual(contents[0].VersionId, expectedKey2VersionIds[0]); - return done(); - }); + makeBackbeatRequest( + { + method: 'GET', + bucket: testBucket, + queryObj: { + 'list-type': 'noncurrent', + 'before-date': date, + 'max-keys': '1', + 'key-marker': 'key1', + 'version-id-marker': expectedKey1VersionIds[1], + }, + authCredentials: credentials, + }, + (err, response) => { + assert.ifError(err); + assert.strictEqual(response.statusCode, 200); + const data = JSON.parse(response.body); + + assert.strictEqual(data.IsTruncated, true); + assert.strictEqual(data.KeyMarker, 'key1'); + assert.strictEqual(data.VersionIdMarker, expectedKey1VersionIds[1]); + assert.strictEqual(data.NextKeyMarker, 'key2'); + assert.strictEqual(data.NextVersionIdMarker, expectedKey2VersionIds[0]); + assert.strictEqual(data.MaxKeys, 1); + assert.strictEqual(data.MaxScannedLifecycleListingEntries, config.maxScannedLifecycleListingEntries); + assert.strictEqual(data.BeforeDate, date); + assert.strictEqual(data.Contents.length, 1); + + const contents = data.Contents; + checkContents(contents); + assert.strictEqual(contents[0].Key, 'key2'); + assert.strictEqual(contents[0].VersionId, expectedKey2VersionIds[0]); + return done(); + }, + ); }); it('should return the last and third following list of noncurrent versions before a defined date', done => { - makeBackbeatRequest({ - method: 'GET', - bucket: testBucket, - queryObj: { - 'list-type': 'noncurrent', - 'before-date': date, - 'max-keys': '1', - 'key-marker': 'key2', - 'version-id-marker': expectedKey2VersionIds[0], - }, - authCredentials: credentials, - }, (err, response) => { - assert.ifError(err); - assert.strictEqual(response.statusCode, 200); - const data = JSON.parse(response.body); - - assert.strictEqual(data.IsTruncated, false); - assert.strictEqual(data.KeyMarker, 'key2'); - assert.strictEqual(data.VersionIdMarker, expectedKey2VersionIds[0]); - assert(!data.NextKeyMarker); - assert(!data.NextVersionIdMarker); - assert.strictEqual(data.MaxKeys, 1); - assert.strictEqual(data.MaxScannedLifecycleListingEntries, config.maxScannedLifecycleListingEntries); - assert.strictEqual(data.BeforeDate, date); - assert.strictEqual(data.Contents.length, 1); - - const contents = data.Contents; - checkContents(contents); - assert.strictEqual(contents[0].Key, 'key2'); - assert.strictEqual(contents[0].VersionId, expectedKey2VersionIds[1]); - return done(); - }); + makeBackbeatRequest( + { + method: 'GET', + bucket: testBucket, + queryObj: { + 'list-type': 'noncurrent', + 'before-date': date, + 'max-keys': '1', + 'key-marker': 'key2', + 'version-id-marker': expectedKey2VersionIds[0], + }, + authCredentials: credentials, + }, + (err, response) => { + assert.ifError(err); + assert.strictEqual(response.statusCode, 200); + const data = JSON.parse(response.body); + + assert.strictEqual(data.IsTruncated, false); + assert.strictEqual(data.KeyMarker, 'key2'); + assert.strictEqual(data.VersionIdMarker, expectedKey2VersionIds[0]); + assert(!data.NextKeyMarker); + assert(!data.NextVersionIdMarker); + assert.strictEqual(data.MaxKeys, 1); + assert.strictEqual(data.MaxScannedLifecycleListingEntries, config.maxScannedLifecycleListingEntries); + assert.strictEqual(data.BeforeDate, date); + assert.strictEqual(data.Contents.length, 1); + + const contents = data.Contents; + checkContents(contents); + assert.strictEqual(contents[0].Key, 'key2'); + assert.strictEqual(contents[0].VersionId, expectedKey2VersionIds[1]); + return done(); + }, + ); }); // Regression test: a scan-limit truncation that stops on a "bare master" // (a pre-versioning object with no internal versionId) returns a - // version-id-marker equals to 'null'. On the following listing, - // CloudServer should treat it as "no marker" instead of decoding it. + // version-id-marker equals to 'null'. On the following listing, + // CloudServer should treat it as "no marker" instead of decoding it. // Decoding it was throwing and crashing the worker. it('should treat a version-id-marker of "null" as no marker without crashing', done => { - makeBackbeatRequest({ - method: 'GET', - bucket: testBucket, - queryObj: { - 'list-type': 'noncurrent', - 'before-date': date, - 'key-marker': 'key1', - 'version-id-marker': 'null', - }, - authCredentials: credentials, - }, (err, response) => { - assert.ifError(err); - // Before the fix this request crashed the worker; assert it succeeds. - assert.strictEqual(response.statusCode, 200); - const data = JSON.parse(response.body); - - assert.strictEqual(data.KeyMarker, 'key1'); - // 'null' is echoed back unchanged and treated as no marker, so the - // listing resumes after key1 and returns key2's noncurrent versions. - assert.strictEqual(data.IsTruncated, false); - assert(!data.NextKeyMarker); - - const contents = data.Contents; - checkContents(contents); - assert.strictEqual(contents.length, expectedKey2VersionIds.length); - contents.forEach((c, i) => { - assert.strictEqual(c.Key, 'key2'); - assert.strictEqual(c.VersionId, expectedKey2VersionIds[i]); - }); - return done(); - }); + makeBackbeatRequest( + { + method: 'GET', + bucket: testBucket, + queryObj: { + 'list-type': 'noncurrent', + 'before-date': date, + 'key-marker': 'key1', + 'version-id-marker': 'null', + }, + authCredentials: credentials, + }, + (err, response) => { + assert.ifError(err); + // Before the fix this request crashed the worker; assert it succeeds. + assert.strictEqual(response.statusCode, 200); + const data = JSON.parse(response.body); + + assert.strictEqual(data.KeyMarker, 'key1'); + // 'null' is echoed back unchanged and treated as no marker, so the + // listing resumes after key1 and returns key2's noncurrent versions. + assert.strictEqual(data.IsTruncated, false); + assert(!data.NextKeyMarker); + + const contents = data.Contents; + checkContents(contents); + assert.strictEqual(contents.length, expectedKey2VersionIds.length); + contents.forEach((c, i) => { + assert.strictEqual(c.Key, 'key2'); + assert.strictEqual(c.VersionId, expectedKey2VersionIds[i]); + }); + return done(); + }, + ); }); }); diff --git a/tests/unit/api/apiUtils/lifecycle.js b/tests/unit/api/apiUtils/lifecycle.js index f931e5ac0e..6fd043402a 100644 --- a/tests/unit/api/apiUtils/lifecycle.js +++ b/tests/unit/api/apiUtils/lifecycle.js @@ -1,7 +1,6 @@ const assert = require('assert'); const { versioning } = require('arsenal'); -const { validateMaxScannedEntries, decodeVersionIdMarker } = - require('../../../../lib/api/apiUtils/object/lifecycle'); +const { validateMaxScannedEntries, decodeVersionIdMarker } = require('../../../../lib/api/apiUtils/object/lifecycle'); const versionIdUtils = versioning.VersionID; // A valid encoded marker that round-trips to this internal version id. From 73e02ed9f384c1700da4852d5c5ed626efb6560a Mon Sep 17 00:00:00 2001 From: Nicolas Humbert Date: Wed, 24 Jun 2026 11:13:29 +0200 Subject: [PATCH 3/3] CLDSRV-936 bump package version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 8472b8763a..e417a78256 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@zenko/cloudserver", - "version": "9.3.11", + "version": "9.3.12", "description": "Zenko CloudServer, an open-source Node.js implementation of a server handling the Amazon S3 protocol", "main": "index.js", "engines": {