Skip to content

Commit bd4352b

Browse files
committed
Prose test 22.1
1 parent 43dcc75 commit bd4352b

File tree

2 files changed

+245
-1
lines changed

2 files changed

+245
-1
lines changed

tests/SpecTests/ClientSideEncryption/FunctionalTestCase.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ protected static function insertKeyVaultData(Client $client, ?array $keyVaultDat
6969
$collection->insertMany($keyVaultData);
7070
}
7171

72-
private function createInt64(string $value): Int64
72+
protected static function createInt64(string $value): Int64
7373
{
7474
$array = sprintf('a:1:{s:7:"integer";s:%d:"%s";}', strlen($value), $value);
7575
$int64 = sprintf('C:%d:"%s":%d:{%s}', strlen(Int64::class), Int64::class, strlen($array), $array);
Lines changed: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
1+
<?php
2+
3+
namespace MongoDB\Tests\SpecTests\ClientSideEncryption;
4+
5+
use Generator;
6+
use MongoDB\BSON\Binary;
7+
use MongoDB\BSON\Decimal128;
8+
use MongoDB\BSON\Document;
9+
use MongoDB\BSON\UTCDateTime;
10+
use MongoDB\Driver\ClientEncryption;
11+
12+
use function base64_decode;
13+
use function file_get_contents;
14+
use function get_debug_type;
15+
use function is_int;
16+
use function version_compare;
17+
18+
/**
19+
* Prose test 22: Range Explicit Encryption
20+
*
21+
* @see https://github.com/mongodb/specifications/blob/master/source/client-side-encryption/tests/README.rst#range-explicit-encryption
22+
* @group csfle
23+
* @group serverless
24+
*/
25+
class Prose22_RangeExplicitEncryptionTest extends FunctionalTestCase
26+
{
27+
private $clientEncryption;
28+
private $collection;
29+
private $encryptedClient;
30+
private $key1Id;
31+
32+
public function setUp(): void
33+
{
34+
parent::setUp();
35+
36+
if ($this->isStandalone() || ($this->isShardedCluster() && ! $this->isShardedClusterUsingReplicasets())) {
37+
$this->markTestSkipped('Range explicit encryption tests require replica sets');
38+
}
39+
40+
if (version_compare($this->getServerVersion(), '7.0.0', '<')) {
41+
$this->markTestSkipped('Range explicit encryption tests require MongoDB 7.0 or later');
42+
}
43+
44+
$client = static::createTestClient();
45+
46+
$key1Document = $this->decodeJson(file_get_contents(__DIR__ . '/../client-side-encryption/etc/data/keys/key1-document.json'));
47+
$this->key1Id = $key1Document->_id;
48+
49+
// Drop the key vault collection and insert key1Document with a majority write concern
50+
self::insertKeyVaultData($client, [$key1Document]);
51+
52+
$this->clientEncryption = $client->createClientEncryption([
53+
'keyVaultNamespace' => 'keyvault.datakeys',
54+
'kmsProviders' => ['local' => ['key' => new Binary(base64_decode(self::LOCAL_MASTERKEY), 0)]],
55+
]);
56+
57+
$autoEncryptionOpts = [
58+
'keyVaultNamespace' => 'keyvault.datakeys',
59+
'kmsProviders' => ['local' => ['key' => new Binary(base64_decode(self::LOCAL_MASTERKEY), 0)]],
60+
'bypassQueryAnalysis' => true,
61+
];
62+
63+
$this->encryptedClient = self::createTestClient(null, [], [
64+
'autoEncryption' => $autoEncryptionOpts,
65+
/* libmongocrypt caches results from listCollections. Use a new
66+
* client in each test to ensure its encryptedFields is applied. */
67+
'disableClientPersistence' => true,
68+
]);
69+
}
70+
71+
public function setUpWithTypeAndRangeOpts(string $type, array $rangeOpts): void
72+
{
73+
if ($type === 'DecimalNoPrecision' || $type === 'DecimalPrecision') {
74+
$this->markTestSkipped('Bundled libmongocrypt does not support Decimal128 (PHPC-2207)');
75+
}
76+
77+
/* Read the encryptedFields file directly into BSON to preserve typing
78+
* for 64-bit integers. This means that DropEncryptedCollection and
79+
* CreateEncryptedCollection will be unable to inspect the option for
80+
* metadata collection names, but that's not necessary for the test. */
81+
$encryptedFields = Document::fromJSON(file_get_contents(__DIR__ . '/../client-side-encryption/etc/data/range-encryptedFields-' . $type . '.json'));
82+
83+
$database = $this->encryptedClient->selectDatabase($this->getDatabaseName());
84+
$database->dropCollection('explicit_encryption', ['encryptedFields' => $encryptedFields]);
85+
$database->createCollection('explicit_encryption', ['encryptedFields' => $encryptedFields]);
86+
$this->collection = $database->selectCollection('explicit_encryption');
87+
88+
$encryptOpts = [
89+
'keyId' => $this->key1Id,
90+
'algorithm' => ClientEncryption::ALGORITHM_RANGE_PREVIEW,
91+
'contentionFactor' => 0,
92+
'rangeOpts' => $rangeOpts,
93+
];
94+
95+
$cast = self::getCastCallableForType($type);
96+
$fieldName = 'encrypted' . $type;
97+
98+
$this->collection->insertMany([
99+
['_id' => 0, $fieldName => $this->clientEncryption->encrypt($cast(0), $encryptOpts)],
100+
['_id' => 1, $fieldName => $this->clientEncryption->encrypt($cast(6), $encryptOpts)],
101+
['_id' => 2, $fieldName => $this->clientEncryption->encrypt($cast(30), $encryptOpts)],
102+
['_id' => 3, $fieldName => $this->clientEncryption->encrypt($cast(200), $encryptOpts)],
103+
]);
104+
}
105+
106+
public function tearDown(): void
107+
{
108+
$this->collection = null;
109+
$this->clientEncryption = null;
110+
$this->encryptedClient = null;
111+
$this->key1Id = null;
112+
}
113+
114+
/**
115+
* @see https://github.com/mongodb/specifications/blob/master/source/client-side-encryption/tests/README.rst#case-1-can-decrypt-a-payload
116+
* @dataProvider provideTypeAndRangeOpts
117+
*/
118+
public function testCase1_CanDecryptAPayload(string $type, array $rangeOpts): void
119+
{
120+
$this->setUpWithTypeAndRangeOpts($type, $rangeOpts);
121+
122+
$encryptOpts = [
123+
'keyId' => $this->key1Id,
124+
'algorithm' => ClientEncryption::ALGORITHM_RANGE_PREVIEW,
125+
'contentionFactor' => 0,
126+
'rangeOpts' => $rangeOpts,
127+
];
128+
129+
$cast = self::getCastCallableForType($type);
130+
$originalValue = $cast(6);
131+
132+
$insertPayload = $this->clientEncryption->encrypt($originalValue, $encryptOpts);
133+
$decryptedValue = $this->clientEncryption->decrypt($insertPayload);
134+
135+
/* Decryption of a 64-bit integer will likely result in a scalar int, so
136+
* cast it back to an Int64 before comparing to the original value. */
137+
if ($type === 'Long' && is_int($decryptedValue)) {
138+
$decryptedValue = $cast($decryptedValue);
139+
}
140+
141+
/* Use separate assertions for type and equality as assertSame isn't
142+
* suitable for comparing BSON objects and using assertEquals alone
143+
* would disregard scalar type differences. */
144+
$this->assertSame(get_debug_type($originalValue), get_debug_type($decryptedValue));
145+
$this->assertEquals($originalValue, $decryptedValue);
146+
}
147+
148+
/** @see https://github.com/mongodb/specifications/blob/master/source/client-side-encryption/tests/README.rst#test-setup-rangeopts */
149+
public static function provideTypeAndRangeOpts(): Generator
150+
{
151+
// TODO: skip DecimalNoPrecision test on mongos
152+
yield 'DecimalNoPrecision' => [
153+
'DecimalNoPrecision',
154+
['sparsity' => 1],
155+
];
156+
157+
yield 'DecimalPrecision' => [
158+
'DecimalPrecision',
159+
[
160+
'min' => new Decimal128('0'),
161+
'max' => new Decimal128('200'),
162+
'sparsity' => 1,
163+
'precision' => 2,
164+
],
165+
];
166+
167+
yield 'DoubleNoPrecision' => [
168+
'DoubleNoPrecision',
169+
['sparsity' => 1],
170+
];
171+
172+
yield 'DoublePrecision' => [
173+
'DoublePrecision',
174+
[
175+
'min' => 0.0,
176+
'max' => 200.0,
177+
'sparsity' => 1,
178+
'precision' => 2,
179+
],
180+
];
181+
182+
yield 'Date' => [
183+
'Date',
184+
[
185+
'min' => new UTCDateTime(0),
186+
'max' => new UTCDateTime(200),
187+
'sparsity' => 1,
188+
],
189+
];
190+
191+
yield 'Int' => [
192+
'Int',
193+
[
194+
'min' => 0,
195+
'max' => 200,
196+
'sparsity' => 1,
197+
],
198+
];
199+
200+
yield 'Long' => [
201+
'Long',
202+
[
203+
'min' => self::createInt64('0'),
204+
'max' => self::createInt64('200'),
205+
'sparsity' => 1,
206+
],
207+
];
208+
}
209+
210+
private static function getCastCallableForType(string $type): callable
211+
{
212+
switch ($type) {
213+
case 'DecimalNoPrecision':
214+
case 'DecimalPrecision':
215+
return function (int $value) {
216+
return new Decimal128((string) $value);
217+
};
218+
219+
case 'DoubleNoPrecision':
220+
case 'DoublePrecision':
221+
return function (int $value) {
222+
return (double) $value;
223+
};
224+
225+
case 'Date':
226+
return function (int $value) {
227+
return new UTCDateTime($value);
228+
};
229+
230+
case 'Int':
231+
return function (int $value) {
232+
return $value;
233+
};
234+
235+
case 'Long':
236+
return function (int $value) {
237+
return self::createInt64((string) $value);
238+
};
239+
240+
default:
241+
throw new LogicException('Unsupported type: ' . $type);
242+
}
243+
}
244+
}

0 commit comments

Comments
 (0)