diff --git a/lib/storage/metadata/mongoclient/MongoClientInterface.ts b/lib/storage/metadata/mongoclient/MongoClientInterface.ts index 87f180e01..dae103fa0 100644 --- a/lib/storage/metadata/mongoclient/MongoClientInterface.ts +++ b/lib/storage/metadata/mongoclient/MongoClientInterface.ts @@ -856,16 +856,9 @@ class MongoClientInterface { // filter to get master const filter = { _id: masterKey, - $or: [{ - 'value.versionId': { - $exists: false, - }, - }, - { - 'value.versionId': { - $gt: objVal.versionId, - }, - }, + $or: [ + { 'value.versionId': { $exists: false } }, + { 'value.versionId': { $gt: objVal.versionId } }, ], }; // values to update master @@ -1143,8 +1136,9 @@ class MongoClientInterface { const ops: AnyBulkWriteOperation[] = []; // filter to get master const filter = { - '_id': masterKey, - 'value.versionId': { + _id: masterKey, + $or: [ + { 'value.versionId': { $exists: false } }, // We break the semantic correctness here with // $gte instead of $gt because we do not have // a microVersionId to capture the micro @@ -1155,8 +1149,9 @@ class MongoClientInterface { // replication and ingestion can hopefully // ensure), but this would not work e.g. in // the case of an active-active replication. - $gte: mstObjVal!.versionId, - }, + + { 'value.versionId': { $gte: mstObjVal!.versionId } }, + ] }; // values to update master const update = { diff --git a/tests/unit/storage/metadata/mongoclient/MongoClientInterface.spec.js b/tests/unit/storage/metadata/mongoclient/MongoClientInterface.spec.js index b79120a4e..ae49dfc70 100644 --- a/tests/unit/storage/metadata/mongoclient/MongoClientInterface.spec.js +++ b/tests/unit/storage/metadata/mongoclient/MongoClientInterface.spec.js @@ -14,6 +14,7 @@ const MongoUtils = require('../../../../../lib/storage/metadata/mongoclient/util const ObjectMD = require('../../../../../lib/models/ObjectMD').default; const { BucketVersioningKeyFormat } = require('../../../../../lib/versioning/constants').VersioningConstants; const { formatMasterKey } = require('../../../../../lib/storage/metadata/mongoclient/utils'); +const { promisify } = require('util'); const dbName = 'metadata'; const baseBucket = BucketInfo.fromObj({ @@ -716,15 +717,27 @@ describe('MongoClientInterface, tests', () => { assert.ok(retrievedCapacityInfo, 'VeeamSOSApi.CapacityInfo should exist'); assert.strictEqual(typeof retrievedCapacityInfo.Capacity, 'bigint', 'Capacity should be a bigint'); - assert.strictEqual(retrievedCapacityInfo.Capacity.toString(), veeamCapacity.Capacity, 'Capacity value mismatch'); + assert.strictEqual( + retrievedCapacityInfo.Capacity.toString(), + veeamCapacity.Capacity, + 'Capacity value mismatch', + ); assert.strictEqual(typeof retrievedCapacityInfo.Available, 'bigint', 'Available should be a bigint'); - assert.strictEqual(retrievedCapacityInfo.Available.toString(), veeamCapacity.Available, 'Available value mismatch'); + assert.strictEqual( + retrievedCapacityInfo.Available.toString(), + veeamCapacity.Available, + 'Available value mismatch', + ); assert.strictEqual(typeof retrievedCapacityInfo.Used, 'bigint', 'Used should be a bigint'); assert.strictEqual(retrievedCapacityInfo.Used.toString(), veeamCapacity.Used, 'Used value mismatch'); - assert.strictEqual(retrievedCapacityInfo.LastModified, veeamCapacity.LastModified, 'LastModified value mismatch'); + assert.strictEqual( + retrievedCapacityInfo.LastModified, + veeamCapacity.LastModified, + 'LastModified value mismatch', + ); done(); }); }); @@ -895,6 +908,9 @@ describe('MongoClientInterface, putObjectVerCase4', () => { if (err) { return done(err); } + + client.putObjectVerCase4Promised = promisify(client.putObjectVerCase4).bind(client); + return createBucket(client, bucketName, true, err => { if (err) { return done(err); @@ -1102,6 +1118,65 @@ describe('MongoClientInterface, putObjectVerCase4', () => { }, ); }); + + it('should handle missing versionId ($exists: false) in putObjectVerCase4 to update master', async () => { + const objName = 'test-object'; + const versionId = 'test-version-id'; + const objMD = new ObjectMD() + .setKey(objName) + .setDataStoreName('us-east-1') + .setContentLength(100) + .setLastModified(new Date()); + + const updateOneStub = sandbox.stub(collection, 'updateOne').resolves({ modifiedCount: 1 }); + + sandbox.stub(client, 'getLatestVersion').callsFake((c, objName, vFormat, log, cb) => { + cb(null, objMD.getValue()); + }); + + const bulkWriteStub = sandbox.stub(collection, 'bulkWrite').resolves({ + modifiedCount: 1, + upsertedCount: 1, + matchedCount: 1, + }); + + const params = { + vFormat: BucketVersioningKeyFormat.v1, + versionId, + repairMaster: true, + versioning: true, + needOplogUpdate: false, + originOp: 'test', + conditions: {}, + }; + + const result = await client.putObjectVerCase4Promised( + collection, + bucketName, + objName, + objMD.getValue(), + params, + logger, + ); + + assert(result, 'Expected result on success'); + assert.strictEqual( + result, + `{"versionId": "undefined"}`, + 'Expected versionId in result', + ); + assert(updateOneStub.calledOnce, 'Expected updateOne to be called'); + assert(bulkWriteStub.calledOnce, 'Expected bulkWrite to be called'); + const bulkOps = bulkWriteStub.firstCall.args[0]; + const masterUpdateOp = bulkOps.find( + op => op.updateOne && op.updateOne.filter._id === formatMasterKey(objName, BucketVersioningKeyFormat.v1) + ); + assert(masterUpdateOp, 'Expected master update operation'); + assert( + masterUpdateOp.updateOne.filter.$or[0]['value.versionId'].$exists === false, + 'Expected filter to check for non-existing versionId' + ); + }); }); describe('MongoClientInterface, putObjectNoVer', () => { @@ -1470,7 +1545,7 @@ describe('MongoClientInterface, writeUUIDIfNotExists', () => { sandbox.stub(client, 'getCollection').returns(mockCollection); - client.writeUUIDIfNotExists('test-uuid', logger, (err) => { + client.writeUUIDIfNotExists('test-uuid', logger, err => { try { assert(err, 'Expected an error to be returned'); assert.strictEqual(err.code, 500, 'Expected 500 error code'); @@ -1491,7 +1566,7 @@ describe('MongoClientInterface, writeUUIDIfNotExists', () => { sandbox.stub(client, 'getCollection').returns(mockCollection); - client.writeUUIDIfNotExists('test-uuid', logger, (err) => { + client.writeUUIDIfNotExists('test-uuid', logger, err => { try { assert(err, 'Expected an error to be returned'); assert.strictEqual(err.code, 409, 'Expected KeyAlreadyExists error');