From 5ef8b8b800c1bd8e928ab6c4bff243903467d138 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Fri, 13 Sep 2024 14:26:38 +0200 Subject: [PATCH] Use `match` instead of `switch` when a simple value is returned (#1393) --- .../Prose22_RangeExplicitEncryptionTest.php | 463 ------------------ .../ClientSideEncryptionSpecTest.php | 7 - 2 files changed, 470 deletions(-) delete mode 100644 tests/SpecTests/ClientSideEncryption/Prose22_RangeExplicitEncryptionTest.php diff --git a/tests/SpecTests/ClientSideEncryption/Prose22_RangeExplicitEncryptionTest.php b/tests/SpecTests/ClientSideEncryption/Prose22_RangeExplicitEncryptionTest.php deleted file mode 100644 index 0c80cf2e0..000000000 --- a/tests/SpecTests/ClientSideEncryption/Prose22_RangeExplicitEncryptionTest.php +++ /dev/null @@ -1,463 +0,0 @@ -=')) { - $this->markTestIncomplete('Range protocol V1 is not supported by ext-mongodb 1.20+'); - } - - if ($this->isStandalone()) { - $this->markTestSkipped('Range explicit encryption tests require replica sets'); - } - - $this->skipIfServerVersion('<', '8.0.0', 'Range explicit encryption tests require MongoDB 8.0 or later'); - - $client = static::createTestClient(); - - $key1Document = $this->decodeJson(file_get_contents(__DIR__ . '/../client-side-encryption/etc/data/keys/key1-document.json')); - $this->key1Id = $key1Document->_id; - - // Drop the key vault collection and insert key1Document with a majority write concern - self::insertKeyVaultData($client, [$key1Document]); - - $this->clientEncryption = $client->createClientEncryption([ - 'keyVaultNamespace' => 'keyvault.datakeys', - 'kmsProviders' => ['local' => ['key' => new Binary(base64_decode(self::LOCAL_MASTERKEY))]], - ]); - - $autoEncryptionOpts = [ - 'keyVaultNamespace' => 'keyvault.datakeys', - 'kmsProviders' => ['local' => ['key' => new Binary(base64_decode(self::LOCAL_MASTERKEY))]], - 'bypassQueryAnalysis' => true, - ]; - - $this->encryptedClient = self::createTestClient(null, [], [ - 'autoEncryption' => $autoEncryptionOpts, - /* libmongocrypt caches results from listCollections. Use a new - * client in each test to ensure its encryptedFields is applied. */ - 'disableClientPersistence' => true, - ]); - } - - public function setUpWithTypeAndRangeOpts(string $type, array $rangeOpts): void - { - if ($type === 'DecimalNoPrecision' || $type === 'DecimalPrecision') { - $this->markTestSkipped('Bundled libmongocrypt does not support Decimal128 (PHPC-2207)'); - } - - /* Read the encryptedFields file directly into BSON to preserve typing - * for 64-bit integers. This means that DropEncryptedCollection and - * CreateEncryptedCollection will be unable to inspect the option for - * metadata collection names, but that's not necessary for the test. */ - $encryptedFields = Document::fromJSON(file_get_contents(__DIR__ . '/../client-side-encryption/etc/data/range-encryptedFields-' . $type . '.json')); - - $database = $this->encryptedClient->selectDatabase($this->getDatabaseName()); - $database->dropCollection('explicit_encryption', ['encryptedFields' => $encryptedFields]); - $database->createCollection('explicit_encryption', ['encryptedFields' => $encryptedFields]); - $this->collection = $database->selectCollection('explicit_encryption'); - - $encryptOpts = [ - 'keyId' => $this->key1Id, - 'algorithm' => ClientEncryption::ALGORITHM_RANGE, - 'contentionFactor' => 0, - 'rangeOpts' => $rangeOpts, - ]; - - $cast = self::getCastCallableForType($type); - $fieldName = 'encrypted' . $type; - - $this->collection->insertMany([ - ['_id' => 0, $fieldName => $this->clientEncryption->encrypt($cast(0), $encryptOpts)], - ['_id' => 1, $fieldName => $this->clientEncryption->encrypt($cast(6), $encryptOpts)], - ['_id' => 2, $fieldName => $this->clientEncryption->encrypt($cast(30), $encryptOpts)], - ['_id' => 3, $fieldName => $this->clientEncryption->encrypt($cast(200), $encryptOpts)], - ]); - } - - public function tearDown(): void - { - /* Since encryptedClient is created with disableClientPersistence=true, - * free any objects that may hold a reference to its mongoc_client_t */ - $this->collection = null; - $this->clientEncryption = null; - $this->encryptedClient = null; - } - - /** @see https://github.com/mongodb/specifications/blob/master/source/client-side-encryption/tests/README.md#test-setup-rangeopts */ - public static function provideTypeAndRangeOpts(): Generator - { - // TODO: skip DecimalNoPrecision test on mongos - yield 'DecimalNoPrecision' => [ - 'DecimalNoPrecision', - ['sparsity' => 1], - ]; - - yield 'DecimalPrecision' => [ - 'DecimalPrecision', - [ - 'min' => new Decimal128('0'), - 'max' => new Decimal128('200'), - 'sparsity' => 1, - 'precision' => 2, - ], - ]; - - yield 'DoubleNoPrecision' => [ - 'DoubleNoPrecision', - ['sparsity' => 1], - ]; - - yield 'DoublePrecision' => [ - 'DoublePrecision', - [ - 'min' => 0.0, - 'max' => 200.0, - 'sparsity' => 1, - 'precision' => 2, - ], - ]; - - yield 'Date' => [ - 'Date', - [ - 'min' => new UTCDateTime(0), - 'max' => new UTCDateTime(200), - 'sparsity' => 1, - ], - ]; - - yield 'Int' => [ - 'Int', - [ - 'min' => 0, - 'max' => 200, - 'sparsity' => 1, - ], - ]; - - yield 'Long' => [ - 'Long', - [ - 'min' => new Int64(0), - 'max' => new Int64(200), - 'sparsity' => 1, - ], - ]; - } - - /** - * @see https://github.com/mongodb/specifications/blob/master/source/client-side-encryption/tests/README.md#case-1-can-decrypt-a-payload - * @dataProvider provideTypeAndRangeOpts - */ - public function testCase1_CanDecryptAPayload(string $type, array $rangeOpts): void - { - $this->setUpWithTypeAndRangeOpts($type, $rangeOpts); - - $encryptOpts = [ - 'keyId' => $this->key1Id, - 'algorithm' => ClientEncryption::ALGORITHM_RANGE, - 'contentionFactor' => 0, - 'rangeOpts' => $rangeOpts, - ]; - - $cast = self::getCastCallableForType($type); - $originalValue = $cast(6); - - $insertPayload = $this->clientEncryption->encrypt($originalValue, $encryptOpts); - $decryptedValue = $this->clientEncryption->decrypt($insertPayload); - - /* Decryption of a 64-bit integer will likely result in a scalar int, so - * cast it back to an Int64 before comparing to the original value. */ - if ($type === 'Long' && is_int($decryptedValue)) { - $decryptedValue = $cast($decryptedValue); - } - - /* Use separate assertions for type and equality as assertSame isn't - * suitable for comparing BSON objects and using assertEquals alone - * would disregard scalar type differences. */ - $this->assertSame(get_debug_type($originalValue), get_debug_type($decryptedValue)); - $this->assertEquals($originalValue, $decryptedValue); - } - - /** - * @see https://github.com/mongodb/specifications/blob/master/source/client-side-encryption/tests/README.md#case-2-can-find-encrypted-range-and-return-the-maximum - * @dataProvider provideTypeAndRangeOpts - */ - public function testCase2_CanFindEncryptedRangeAndReturnTheMaximum(string $type, array $rangeOpts): void - { - $this->setUpWithTypeAndRangeOpts($type, $rangeOpts); - - $encryptOpts = [ - 'keyId' => $this->key1Id, - 'algorithm' => ClientEncryption::ALGORITHM_RANGE, - 'queryType' => ClientEncryption::QUERY_TYPE_RANGE, - 'contentionFactor' => 0, - 'rangeOpts' => $rangeOpts, - ]; - - $cast = self::getCastCallableForType($type); - $fieldName = 'encrypted' . $type; - - $expr = [ - '$and' => [ - [$fieldName => ['$gte' => $cast(6)]], - [$fieldName => ['$lte' => $cast(200)]], - ], - ]; - - $encryptedExpr = $this->clientEncryption->encryptExpression($expr, $encryptOpts); - $cursor = $this->collection->find($encryptedExpr, ['sort' => ['_id' => 1]]); - - $expectedDocuments = [ - ['_id' => 1, $fieldName => $cast(6)], - ['_id' => 2, $fieldName => $cast(30)], - ['_id' => 3, $fieldName => $cast(200)], - ]; - - $this->assertMultipleDocumentsMatch($expectedDocuments, $cursor); - } - - /** - * @see https://github.com/mongodb/specifications/blob/master/source/client-side-encryption/tests/README.md#case-3-can-find-encrypted-range-and-return-the-minimum - * @dataProvider provideTypeAndRangeOpts - */ - public function testCase3_CanFindEncryptedRangeAndReturnTheMinimum(string $type, array $rangeOpts): void - { - $this->setUpWithTypeAndRangeOpts($type, $rangeOpts); - - $encryptOpts = [ - 'keyId' => $this->key1Id, - 'algorithm' => ClientEncryption::ALGORITHM_RANGE, - 'queryType' => ClientEncryption::QUERY_TYPE_RANGE, - 'contentionFactor' => 0, - 'rangeOpts' => $rangeOpts, - ]; - - $cast = self::getCastCallableForType($type); - $fieldName = 'encrypted' . $type; - - $expr = [ - '$and' => [ - [$fieldName => ['$gte' => $cast(0)]], - [$fieldName => ['$lte' => $cast(6)]], - ], - ]; - - $encryptedExpr = $this->clientEncryption->encryptExpression($expr, $encryptOpts); - $cursor = $this->collection->find($encryptedExpr, ['sort' => ['_id' => 1]]); - - $expectedDocuments = [ - ['_id' => 0, $fieldName => $cast(0)], - ['_id' => 1, $fieldName => $cast(6)], - ]; - - $this->assertMultipleDocumentsMatch($expectedDocuments, $cursor); - } - - /** - * @see https://github.com/mongodb/specifications/blob/master/source/client-side-encryption/tests/README.md#case-4-can-find-encrypted-range-with-an-open-range-query - * @dataProvider provideTypeAndRangeOpts - */ - public function testCase4_CanFindEncryptedRangeWithAnOpenRangeQuery(string $type, array $rangeOpts): void - { - $this->setUpWithTypeAndRangeOpts($type, $rangeOpts); - - $encryptOpts = [ - 'keyId' => $this->key1Id, - 'algorithm' => ClientEncryption::ALGORITHM_RANGE, - 'queryType' => ClientEncryption::QUERY_TYPE_RANGE, - 'contentionFactor' => 0, - 'rangeOpts' => $rangeOpts, - ]; - - $cast = self::getCastCallableForType($type); - $fieldName = 'encrypted' . $type; - - $expr = ['$and' => [[$fieldName => ['$gt' => $cast(30)]]]]; - - $encryptedExpr = $this->clientEncryption->encryptExpression($expr, $encryptOpts); - $cursor = $this->collection->find($encryptedExpr, ['sort' => ['_id' => 1]]); - $expectedDocuments = [['_id' => 3, $fieldName => $cast(200)]]; - - $this->assertMultipleDocumentsMatch($expectedDocuments, $cursor); - } - - /** - * @see https://github.com/mongodb/specifications/blob/master/source/client-side-encryption/tests/README.md#case-5-can-run-an-aggregation-expression-inside-expr - * @dataProvider provideTypeAndRangeOpts - */ - public function testCase5_CanRunAnAggregationExpressionInsideExpr(string $type, array $rangeOpts): void - { - $this->setUpWithTypeAndRangeOpts($type, $rangeOpts); - - $encryptOpts = [ - 'keyId' => $this->key1Id, - 'algorithm' => ClientEncryption::ALGORITHM_RANGE, - 'queryType' => ClientEncryption::QUERY_TYPE_RANGE, - 'contentionFactor' => 0, - 'rangeOpts' => $rangeOpts, - ]; - - $cast = self::getCastCallableForType($type); - $fieldName = 'encrypted' . $type; - $fieldPath = '$' . $fieldName; - - $expr = ['$and' => [['$lt' => [$fieldPath, $cast(30)]]]]; - - $encryptedExpr = $this->clientEncryption->encryptExpression($expr, $encryptOpts); - $cursor = $this->collection->find(['$expr' => $encryptedExpr], ['sort' => ['_id' => 1]]); - - $expectedDocuments = [ - ['_id' => 0, $fieldName => $cast(0)], - ['_id' => 1, $fieldName => $cast(6)], - ]; - - $this->assertMultipleDocumentsMatch($expectedDocuments, $cursor); - } - - /** - * @see https://github.com/mongodb/specifications/blob/master/source/client-side-encryption/tests/README.md#case-6-encrypting-a-document-greater-than-the-maximum-errors - * @dataProvider provideTypeAndRangeOpts - */ - public function testCase6_EncryptingADocumentGreaterThanTheMaximumErrors(string $type, array $rangeOpts): void - { - if ($type === 'DecimalNoPrecision' || $type === 'DoubleNoPrecision') { - $this->markTestSkipped('Test is not applicable to "NoPrecision" types'); - } - - $this->setUpWithTypeAndRangeOpts($type, $rangeOpts); - - $encryptOpts = [ - 'keyId' => $this->key1Id, - 'algorithm' => ClientEncryption::ALGORITHM_RANGE, - 'contentionFactor' => 0, - 'rangeOpts' => $rangeOpts, - ]; - - $cast = self::getCastCallableForType($type); - - $this->expectException(EncryptionException::class); - $this->expectExceptionMessage('Value must be greater than or equal to the minimum value and less than or equal to the maximum value'); - $this->clientEncryption->encrypt($cast(201), $encryptOpts); - } - - /** - * @see https://github.com/mongodb/specifications/blob/master/source/client-side-encryption/tests/README.md#case-7-encrypting-a-value-of-a-different-type-errors - * @dataProvider provideTypeAndRangeOpts - */ - public function testCase7_EncryptingAValueOfADifferentTypeErrors(string $type, array $rangeOpts): void - { - if ($type === 'DecimalNoPrecision' || $type === 'DoubleNoPrecision') { - /* Explicit encryption relies on min/max range options to check - * types and "NoPrecision" intentionally omits those options. */ - $this->markTestSkipped('Test is not applicable to DoubleNoPrecision and DecimalNoPrecision'); - } - - $this->setUpWithTypeAndRangeOpts($type, $rangeOpts); - - $encryptOpts = [ - 'keyId' => $this->key1Id, - 'algorithm' => ClientEncryption::ALGORITHM_RANGE, - 'contentionFactor' => 0, - 'rangeOpts' => $rangeOpts, - ]; - - $value = $type === 'Int' ? 6.0 : 6; - - $this->expectException(EncryptionException::class); - $this->expectExceptionMessage('expected matching \'min\' and value type'); - $this->clientEncryption->encrypt($value, $encryptOpts); - } - - /** - * @see https://github.com/mongodb/specifications/blob/master/source/client-side-encryption/tests/README.md#case-8-setting-precision-errors-if-the-type-is-not-double-or-decimal128 - * @dataProvider provideTypeAndRangeOpts - */ - public function testCase8_SettingPrecisionErrorsIfTheTypeIsNotDoubleOrDecimal128(string $type, array $rangeOpts): void - { - if ($type === 'DecimalNoPrecision' || $type === 'DecimalPrecision' || $type === 'DoubleNoPrecision' || $type === 'DoublePrecision') { - $this->markTestSkipped('Test is not applicable to Double and Decimal types'); - } - - $this->setUpWithTypeAndRangeOpts($type, $rangeOpts); - - $encryptOpts = [ - 'keyId' => $this->key1Id, - 'algorithm' => ClientEncryption::ALGORITHM_RANGE, - 'contentionFactor' => 0, - 'rangeOpts' => $rangeOpts + ['precision' => 2], - ]; - - $cast = self::getCastCallableForType($type); - - $this->expectException(EncryptionException::class); - $this->expectExceptionMessage('expected \'precision\' to be set with double or decimal128 index'); - $this->clientEncryption->encrypt($cast(6), $encryptOpts); - } - - private function assertMultipleDocumentsMatch(array $expectedDocuments, Iterator $actualDocuments): void - { - $mi = new MultipleIterator(MultipleIterator::MIT_NEED_ANY); - $mi->attachIterator(new ArrayIterator($expectedDocuments)); - $mi->attachIterator($actualDocuments); - - foreach ($mi as $documents) { - [$expectedDocument, $actualDocument] = $documents; - $this->assertNotNull($expectedDocument); - $this->assertNotNull($actualDocument); - - $this->assertDocumentsMatch($expectedDocument, $actualDocument); - } - } - - private static function getCastCallableForType(string $type): callable - { - return match ($type) { - 'DecimalNoPrecision', 'DecimalPrecision' => fn (int $value) => new Decimal128((string) $value), - 'DoubleNoPrecision', 'DoublePrecision' => fn (int $value) => (double) $value, - 'Date' => fn (int $value) => new UTCDateTime($value), - 'Int' => fn (int $value) => $value, - 'Long' => fn (int $value) => new Int64($value), - default => throw new LogicException('Unsupported type: ' . $type), - }; - } -} diff --git a/tests/SpecTests/ClientSideEncryptionSpecTest.php b/tests/SpecTests/ClientSideEncryptionSpecTest.php index 574cf4f23..001d78a84 100644 --- a/tests/SpecTests/ClientSideEncryptionSpecTest.php +++ b/tests/SpecTests/ClientSideEncryptionSpecTest.php @@ -38,12 +38,9 @@ use function in_array; use function iterator_to_array; use function json_decode; -use function phpversion; use function sprintf; use function str_repeat; -use function str_starts_with; use function substr; -use function version_compare; use const JSON_THROW_ON_ERROR; @@ -167,10 +164,6 @@ public function testClientSideEncryption(stdClass $test, ?array $runOn, array $d $this->markTestIncomplete(self::$incompleteTests[$this->dataDescription()]); } - if (str_starts_with($this->dataDescription(), 'fle2v2-Range-') && version_compare(phpversion('mongodb'), '1.20.0dev', '>=')) { - $this->markTestIncomplete('Range protocol V1 is not supported by ext-mongodb 1.20+'); - } - if (isset($runOn)) { $this->checkServerRequirements($runOn); }