From 4d38e8e3385074e73f98f0dd4c65bc05089edad0 Mon Sep 17 00:00:00 2001 From: Dmitry Rybakov Date: Tue, 21 Jul 2026 09:31:37 +0200 Subject: [PATCH 1/2] RUBY-3625 Support auto encryption in unified tests Sync the poc-queryable-encryption fixture from the specifications repo into the valid-pass unified suite. It exercises autoEncryptOpts on a client entity (local KMS + queryable encryption), demonstrating the runner support landed under RUBY-3773 and RUBY-3363. --- .../valid-pass/poc-queryable-encryption.yml | 86 +++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 spec/spec_tests/data/unified/valid-pass/poc-queryable-encryption.yml diff --git a/spec/spec_tests/data/unified/valid-pass/poc-queryable-encryption.yml b/spec/spec_tests/data/unified/valid-pass/poc-queryable-encryption.yml new file mode 100644 index 0000000000..797904ee95 --- /dev/null +++ b/spec/spec_tests/data/unified/valid-pass/poc-queryable-encryption.yml @@ -0,0 +1,86 @@ +description: poc-queryable-encryption + +schemaVersion: "1.23" + +runOnRequirements: + - minServerVersion: "7.0" + csfle: true + # QE is not supported on standalone servers + topologies: [ replicaset, load-balanced, sharded ] + +createEntities: + - client: + id: &client0 client0 + autoEncryptOpts: + keyVaultNamespace: keyvault.datakeys + kmsProviders: + local: + key: Mng0NCt4ZHVUYUJCa1kxNkVyNUR1QURhZ2h2UzR2d2RrZzh0cFBwM3R6NmdWMDFBMUN3YkQ5aXRRMkhGRGdQV09wOGVNYUMxT2k3NjZKelhaQmRCZGJkTXVyZG9uSjFk + - database: + id: &encryptedDB encryptedDB + client: *client0 + databaseName: &encryptedDBName poc-queryable-encryption + - collection: + id: &encryptedColl encryptedColl + database: *encryptedDB + collectionName: &encryptedCollName encrypted + - client: + id: &client1 client1 + - database: + id: &unencryptedDB unencryptedDB + client: *client1 + databaseName: *encryptedDBName + - collection: + id: &unencryptedColl unencryptedColl + database: *unencryptedDB + collectionName: *encryptedCollName + +initialData: + - databaseName: keyvault + collectionName: datakeys + documents: + - _id: &keyid { $binary: { base64: EjRWeBI0mHYSNBI0VniQEg==, subType: "04" } } + keyMaterial: { $binary: { base64: sHe0kz57YW7v8g9VP9sf/+K1ex4JqKc5rf/URX3n3p8XdZ6+15uXPaSayC6adWbNxkFskuMCOifDoTT+rkqMtFkDclOy884RuGGtUysq3X7zkAWYTKi8QAfKkajvVbZl2y23UqgVasdQu3OVBQCrH/xY00nNAs/52e958nVjBuzQkSb1T8pKJAyjZsHJ60+FtnfafDZSTAIBJYn7UWBCwQ==, subType: "00" } } + creationDate: { $date: { $numberLong: "1641024000000" } } + updateDate: { $date: { $numberLong: "1641024000000" } } + status: 1 + masterKey: + provider: local + - databaseName: *encryptedDBName + collectionName: *encryptedCollName + documents: [] + createOptions: + encryptedFields: + fields: + - keyId: *keyid + path: 'encryptedInt' + bsonType: 'int' + queries: {'queryType': 'equality', 'contention': {'$numberLong': '0'}} + +tests: + - description: insert, replace, and find with queryable encryption + operations: + - object: *encryptedColl + name: insertOne + arguments: + document: + _id: 1 + encryptedInt: 11 + - object: *encryptedColl + name: replaceOne + arguments: + filter: { encryptedInt: 11 } + replacement: { encryptedInt: 22 } + - object: *encryptedColl + name: find + arguments: + filter: { encryptedInt: 22 } + expectResult: + - _id: 1 + encryptedInt: 22 + - object: *unencryptedColl + name: find + arguments: + filter: {} + expectResult: + - { _id: 1, encryptedInt: { $$type: binData }, __safeContent__: [ { "$binary" : { "base64" : "rhS16TJojgDDBtbluxBokvcotP1mQTGeYpNt8xd3MJQ=", "subType" : "00" } } ] } \ No newline at end of file From 138f1cde48d9bf2e8d26321d9ea4bc375c0659e3 Mon Sep 17 00:00:00 2001 From: Dmitry Rybakov Date: Thu, 30 Jul 2026 10:30:53 +0200 Subject: [PATCH 2/2] RUBY-3625 Honour empty FLE in csfle test requirement The poc-queryable-encryption fixture added to the valid-pass unified suite is gated only by `csfle: true`. Mongo::CRUD::Requirement#satisfied? checked that gate with `!!(ENV['LIBMONGOCRYPT_PATH'] || ENV['FLE'])`, and Evergreen exports FLE="" on every host. An empty string is truthy in Ruby, so the gate always passed and the fixture ran on variants that have no libmongocrypt. Use the same emptiness check that Mrss::LiteConstraints#require_libmongocrypt uses, so the two gates cannot disagree. Also close the cluster when building the encrypter fails with a LoadError. Mongo::Client#initialize only rescued StandardError there, but Mongo::Crypt::Binding raises LoadError, which is a ScriptError. The cluster was therefore left open and its monitoring and connection pool threads outlived the failed constructor, which in turn failed every example in the next spec file that calls clean_slate_for_all_if_possible. --- lib/mongo/client.rb | 6 +++++- spec/mongo/client_construction_spec.rb | 26 ++++++++++++++++++++++++++ spec/runners/crud/requirement.rb | 6 +++++- 3 files changed, 36 insertions(+), 2 deletions(-) diff --git a/lib/mongo/client.rb b/lib/mongo/client.rb index 4fc3250a83..77d9767a99 100644 --- a/lib/mongo/client.rb +++ b/lib/mongo/client.rb @@ -625,7 +625,11 @@ def initialize(addresses_or_uri, options = nil) build_encrypter end end - rescue StandardError + # ScriptError is rescued in addition to StandardError because + # Mongo::Crypt::Binding raises a LoadError when libmongocrypt cannot be + # found. Without it the cluster built above would be left open, leaking + # its monitoring and connection pool threads. + rescue StandardError, ScriptError begin @cluster.close rescue StandardError => e diff --git a/spec/mongo/client_construction_spec.rb b/spec/mongo/client_construction_spec.rb index fc88be7fcb..582eef5dbe 100644 --- a/spec/mongo/client_construction_spec.rb +++ b/spec/mongo/client_construction_spec.rb @@ -393,6 +393,32 @@ end end + # Mongo::Crypt::Binding raises a LoadError, which is a ScriptError and + # not a StandardError, when libmongocrypt is not available. The cluster + # is built before the encrypter, so it must be closed before the error + # propagates out of the constructor. This context deliberately sits + # outside the require_libmongocrypt guard above: the scenario it covers + # only happens when libmongocrypt is missing. + context 'when building the encrypter raises a LoadError' do + before do + allow(Mongo::Crypt::AutoEncrypter).to receive(:new).and_raise(LoadError, 'no libmongocrypt') + end + + it 'closes the cluster and propagates the error' do + expect_any_instance_of(Mongo::Cluster).to receive(:close).and_call_original + + expect do + new_local_client_nmio( + SpecConfig.instance.addresses, + auto_encryption_options: { + key_vault_namespace: 'keyvault.datakeys', + kms_providers: { local: { key: 'x' * 96 } }, + } + ) + end.to raise_error(LoadError, /no libmongocrypt/) + end + end + context 'timeout options' do let(:client) do new_local_client( diff --git a/spec/runners/crud/requirement.rb b/spec/runners/crud/requirement.rb index cb3ee755cb..4099312f38 100644 --- a/spec/runners/crud/requirement.rb +++ b/spec/runners/crud/requirement.rb @@ -114,7 +114,11 @@ def satisfied? ok &&= !SpecConfig.instance.auth? end if @csfle - ok &&= !!(ENV['LIBMONGOCRYPT_PATH'] || ENV['FLE']) + # Evergreen exports FLE="" on variants that do not exercise FLE, and + # an empty string is truthy in Ruby. Treat an empty value as + # "libmongocrypt is not available", the same way + # Mrss::LiteConstraints#require_libmongocrypt does. + ok &&= !(ENV['LIBMONGOCRYPT_PATH'] || '').empty? || !(ENV['FLE'] || '').empty? if ok && @csfle.is_a?(Hash) && (min_version = @csfle['minLibmongocryptVersion']) actual_version = Mongo::Crypt::Binding.mongocrypt_version(nil) ok &&= Mongo::Crypt::Binding.parse_version(actual_version) >= Gem::Version.new(min_version)