Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
142 changes: 142 additions & 0 deletions tests/functional/aws-node-sdk/test/object/objectOverwrite.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,49 @@
const assert = require('assert');
const {
PutObjectCommand,
PutBucketVersioningCommand,
HeadObjectCommand,
GetObjectCommand,
} = require('@aws-sdk/client-s3');

const withV4 = require('../support/withV4');
const BucketUtility = require('../../lib/utility/bucket-util');
const { fakeMetadataArchive, fakeMetadataTransition, getMetadata, initMetadata } = require('../utils/init');

const coldStateScenarios = [
{
name: 'transition in progress',
transitionInProgress: true,
},
{
name: 'archived',
archiveState: {
archiveInfo: { archiveId: 'archive-1' },
restoreRequestedAt: new Date(0).toISOString(),
restoreRequestedDays: 5,
},
},
{
name: 'restored (not expired)',
archiveState: {
archiveInfo: { archiveId: 'archive-restored' },
restoreRequestedAt: new Date(0).toISOString(),
restoreRequestedDays: 5,
restoreCompletedAt: new Date(Date.now() - 60 * 60 * 1000).toISOString(),
restoreWillExpireAt: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(),
},
},
{
name: 'restored (expired)',
archiveState: {
archiveInfo: { archiveId: 'archive-expired' },
restoreRequestedAt: new Date(0).toISOString(),
restoreRequestedDays: 5,
restoreCompletedAt: new Date(Date.now() - 48 * 60 * 60 * 1000).toISOString(),
restoreWillExpireAt: new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(),
},
},
];

const objectName = 'someObject';
const firstPutMetadata = {
Expand All @@ -30,6 +67,7 @@ describe('Put object with same key as prior object', () => {
bucketUtil = new BucketUtility('default', sigCfg);
s3 = bucketUtil.s3;
bucketName = await bucketUtil.createRandom(1);
await initMetadata();
});

beforeEach(async () => {
Expand Down Expand Up @@ -66,5 +104,109 @@ describe('Put object with same key as prior object', () => {
const bodyText = await res.Body.transformToString();
assert.deepStrictEqual(bodyText, 'Much different');
});

coldStateScenarios.forEach(({ name, transitionInProgress, archiveState }) => {
it(`should replace object with cold-state metadata (${name}) in non-versioned bucket`, async () => {
if (transitionInProgress) {
await fakeMetadataTransition(bucketName, objectName, undefined);
} else {
await fakeMetadataArchive(bucketName, objectName, undefined, archiveState);
}

await s3.send(new PutObjectCommand({
Bucket: bucketName,
Key: objectName,
Body: `overwrite cold state ${name}`,
Metadata: secondPutMetadata,
}));

const currentMD = await getMetadata(bucketName, objectName, undefined);
assert.strictEqual(currentMD.archive, undefined);
assert.strictEqual(currentMD['x-amz-scal-transition-in-progress'], undefined);
});
});

it('should create a new version when replacing archived current object in versioned bucket', async () => {
await bucketUtil.empty(bucketName);

await s3.send(new PutBucketVersioningCommand({
Bucket: bucketName,
VersioningConfiguration: { Status: 'Enabled' },
}));

const firstPutRes = await s3.send(new PutObjectCommand({
Bucket: bucketName,
Key: objectName,
Body: 'versioned first payload',
Metadata: firstPutMetadata,
}));
assert(firstPutRes.VersionId);

await fakeMetadataArchive(bucketName, objectName, undefined, {
archiveInfo: { archiveId: 'archive-versioned-current' },
restoreRequestedAt: new Date(0).toISOString(),
restoreRequestedDays: 5,
});

const secondPutRes = await s3.send(new PutObjectCommand({
Bucket: bucketName,
Key: objectName,
Body: 'versioned second payload',
Metadata: secondPutMetadata,
}));
assert(secondPutRes.VersionId);
assert.notStrictEqual(secondPutRes.VersionId, firstPutRes.VersionId);

const headRes = await s3.send(new HeadObjectCommand({
Bucket: bucketName,
Key: objectName,
}));
assert.deepStrictEqual(headRes.Metadata, secondPutMetadata);

const currentMD = await getMetadata(bucketName, objectName, undefined);
assert.strictEqual(currentMD.archive, undefined);
});

it('should replace archived current null version in version-suspended bucket', async () => {
await bucketUtil.empty(bucketName);

await s3.send(new PutBucketVersioningCommand({
Bucket: bucketName,
VersioningConfiguration: { Status: 'Enabled' },
}));
await s3.send(new PutObjectCommand({
Bucket: bucketName,
Key: objectName,
Body: 'enabled-version-payload',
}));
await s3.send(new PutBucketVersioningCommand({
Bucket: bucketName,
VersioningConfiguration: { Status: 'Suspended' },
}));

await s3.send(new PutObjectCommand({
Bucket: bucketName,
Key: objectName,
Body: 'null-current-before-archive',
}));

await fakeMetadataArchive(bucketName, objectName, undefined, {
archiveInfo: { archiveId: 'archive-null-current' },
restoreRequestedAt: new Date(0).toISOString(),
restoreRequestedDays: 5,
});

await s3.send(new PutObjectCommand({
Bucket: bucketName,
Key: objectName,
Body: 'replace archived null current',
Metadata: secondPutMetadata,
}));

const currentMD = await getMetadata(bucketName, objectName, undefined);
assert.strictEqual(currentMD.archive, undefined);
assert.deepStrictEqual(currentMD['x-amz-meta-secondput'], secondPutMetadata.secondput);
});

});
});
66 changes: 66 additions & 0 deletions tests/functional/aws-node-sdk/test/object/putVersion.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
const assert = require('assert');
const async = require('async');
const { promisify } = require('util');

const withV4 = require('../support/withV4');
const BucketUtility = require('../../lib/utility/bucket-util');
Expand Down Expand Up @@ -956,6 +957,71 @@ describe('PUT object with x-scal-s3-version-id header', () => {
},
], done);
});

it('should set restore originOp and drop restore-attempt metadata', done => {
const params = { Bucket: bucketName, Key: objectName };

async.series([
next => s3.send(new PutObjectCommand({
...params,
Metadata: {
'custom-md': 'preserved-value',
},
})).then(() => next()).catch(next),
next => fakeMetadataArchive(bucketName, objectName, undefined, archive, next),
next => getMetadata(bucketName, objectName, undefined, (err, objMD) => {
if (err) {
return next(err);
}
/* eslint-disable no-param-reassign */
objMD['x-amz-meta-scal-s3-restore-attempt'] = '3';
/* eslint-enable no-param-reassign */
return metadata.putObjectMD(bucketName, objectName, objMD, undefined, log, next);
}),
next => putObjectVersion(s3, params, '', next),
next => getMetadata(bucketName, objectName, undefined, (err, objMD) => {
if (err) {
return next(err);
}
assert.strictEqual(objMD.originOp, 's3:ObjectRestore:Completed');
assert.strictEqual(objMD['x-amz-meta-custom-md'], 'preserved-value');
assert.strictEqual(objMD['x-amz-meta-scal-s3-restore-attempt'], undefined);
return next();
}),
], done);
});

it('should keep x-amz-meta-scal-version-id when restoring on ingestion bucket', async () => {
const ingestionBucketName = `ingestion-restore-${Date.now()}`;
const params = { Bucket: ingestionBucketName, Key: objectName };
let putVersionId;
const fakeMetadataArchivePromise = promisify(fakeMetadataArchive);
const putObjectVersionPromise = promisify(putObjectVersion);
const getMetadataPromise = promisify(getMetadata);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

promisify is unnecessary here — fakeMetadataArchive, putObjectVersion, and getMetadata already return promises when called without a callback (see the dual-mode refactoring in init.js in this same PR). You can call them directly and drop the promisify wrappers and the promisify import.

— Claude Code

try {
await s3.send(new CreateBucketCommand({
Bucket: ingestionBucketName,
CreateBucketConfiguration: {
LocationConstraint: 'us-east-2:ingest',
},
}));

const putRes = await s3.send(new PutObjectCommand(params));
putVersionId = putRes.VersionId;

await fakeMetadataArchivePromise(ingestionBucketName, objectName, putVersionId, archive);

await putObjectVersionPromise(s3, params, putVersionId);

const restoredObjMD = await getMetadataPromise(
ingestionBucketName, objectName, putVersionId);

assert.strictEqual(restoredObjMD['x-amz-meta-scal-version-id'], putVersionId);
} finally {
await bucketUtil.emptyMany([ingestionBucketName]).catch(() => {});
await bucketUtil.deleteMany([ingestionBucketName]).catch(() => {});
}
});
});
});
});
77 changes: 47 additions & 30 deletions tests/functional/aws-node-sdk/test/utils/init.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,22 +38,33 @@ function decodeVersionId(versionId) {

let metadataInit = false;

function initMetadata(done) {
if (metadataInit === true) {
return done();
}
return metadata.setup(err => {
if (err) {
return done(err);
function initMetadata(cb) {
const promise = (async () => {
if (metadataInit) {
return;
}
metadataInit = true;
return done();
});
await new Promise((resolve, reject) => {
metadata.setup(err => {
if (err) {
return reject(err);
}
metadataInit = true;
return resolve();
});
});
})();
return cb ? promise.then(() => cb(), cb) : promise;
}

function getMetadata(bucketName, objectName, versionId, cb) {
return metadata.getObjectMD(bucketName, objectName, { versionId: decodeVersionId(versionId) },
log, cb);
const promise = new Promise((resolve, reject) => metadata.getObjectMD(
bucketName,
objectName,
{ versionId: decodeVersionId(versionId) },
log,
(err, data) => (err ? reject(err) : resolve(data)),
));
return cb ? promise.then(res => cb(null, res), cb) : promise;
}

/**
Expand All @@ -66,16 +77,19 @@ function getMetadata(bucketName, objectName, versionId, cb) {
* @returns {undefined}
*/
function fakeMetadataTransition(bucketName, objectName, versionId, cb) {
return getMetadata(bucketName, objectName, versionId, (err, objMD) => {
if (err) {
return cb(err);
}
/* eslint-disable no-param-reassign */
const promise = (async () => {
const objMD = await getMetadata(bucketName, objectName, versionId);
objMD['x-amz-scal-transition-in-progress'] = true;
/* eslint-enable no-param-reassign */
return metadata.putObjectMD(bucketName, objectName, objMD, { versionId: decodeVersionId(versionId) },
log, err => cb(err));
});
await new Promise((resolve, reject) => metadata.putObjectMD(
bucketName,
objectName,
objMD,
{ versionId: decodeVersionId(versionId) },
log,
err => (err ? reject(err) : resolve()),
));
})();
return cb ? promise.then(() => cb(), cb) : promise;
}

/**
Expand All @@ -89,18 +103,21 @@ function fakeMetadataTransition(bucketName, objectName, versionId, cb) {
* @returns {undefined}
*/
function fakeMetadataArchive(bucketName, objectName, versionId, archive, cb) {
return getMetadata(bucketName, objectName, versionId, (err, objMD) => {
if (err) {
return cb(err);
}
/* eslint-disable no-param-reassign */
const promise = (async () => {
const objMD = await getMetadata(bucketName, objectName, versionId);
objMD['x-amz-storage-class'] = LOCATION_NAME_DMF;
objMD.dataStoreName = LOCATION_NAME_DMF;
objMD.archive = archive;
/* eslint-enable no-param-reassign */
return metadata.putObjectMD(bucketName, objectName, objMD, { versionId: decodeVersionId(versionId) },
log, err => cb(err));
});
await new Promise((resolve, reject) => metadata.putObjectMD(
bucketName,
objectName,
objMD,
{ versionId: decodeVersionId(versionId) },
log,
err => (err ? reject(err) : resolve()),
));
})();
return cb ? promise.then(() => cb(), cb) : promise;
}

module.exports = {
Expand Down
Loading
Loading