Skip to content

Commit ff1e3fa

Browse files
committed
PHPLIB-885: Queryable encryption spec tests
Synced with mongodb/specifications@cd34149 Change logic to use encryptedClient only when available. Support encrypted_fields in CSFLE spec tests. Updates logic for creating the collection under test and also applies a "majority" write concern for the spec's default write options. Also updates a prose test, which does not need to reference the Context option (mainly for spec tests). Add to assertion count when evaluating ErrorExpectation, which avoids a potential PHPUnit warning for a test performing no assertions when a test consisted of an operation(s) with no explicit error/result expectations.
1 parent 50fe7f3 commit ff1e3fa

22 files changed

+7332
-31
lines changed

tests/SpecTests/ClientSideEncryptionSpecTest.php

Lines changed: 40 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
use MongoDB\Driver\Exception\EncryptionException;
1616
use MongoDB\Driver\Exception\RuntimeException;
1717
use MongoDB\Driver\WriteConcern;
18-
use MongoDB\Operation\CreateCollection;
1918
use MongoDB\Tests\CommandObserver;
2019
use PHPUnit\Framework\Assert;
2120
use PHPUnit\Framework\SkippedTestError;
@@ -57,6 +56,8 @@ class ClientSideEncryptionSpecTest extends FunctionalTestCase
5756
'awsTemporary: Insert a document with auto encryption using the AWS provider with temporary credentials' => 'Not yet implemented (PHPC-1751)',
5857
'awsTemporary: Insert with invalid temporary credentials' => 'Not yet implemented (PHPC-1751)',
5958
'azureKMS: Insert a document with auto encryption using Azure KMS provider' => 'RHEL platform is missing Azure root certificate (PHPLIB-619)',
59+
'timeoutMS: timeoutMS applied to listCollections to get collection schema' => 'Not yet implemented (PHPC-1760)',
60+
'timeoutMS: remaining timeoutMS applied to find to get keyvault data' => 'Not yet implemented (PHPC-1760)',
6061
];
6162

6263
public function setUp(): void
@@ -90,7 +91,7 @@ public static function assertCommandMatches(stdClass $expected, stdClass $actual
9091
* @param string $databaseName Name of database under test
9192
* @param string $collectionName Name of collection under test
9293
*/
93-
public function testClientSideEncryption(stdClass $test, ?array $runOn, array $data, ?array $keyVaultData = null, $jsonSchema = null, ?string $databaseName = null, ?string $collectionName = null): void
94+
public function testClientSideEncryption(stdClass $test, ?array $runOn, array $data, ?stdClass $encryptedFields = null, ?array $keyVaultData = null, ?stdClass $jsonSchema = null, ?string $databaseName = null, ?string $collectionName = null): void
9495
{
9596
if (isset(self::$incompleteTests[$this->dataDescription()])) {
9697
$this->markTestIncomplete(self::$incompleteTests[$this->dataDescription()]);
@@ -107,6 +108,11 @@ public function testClientSideEncryption(stdClass $test, ?array $runOn, array $d
107108
$databaseName = $databaseName ?? $this->getDatabaseName();
108109
$collectionName = $collectionName ?? $this->getCollectionName();
109110

111+
// TODO: Remove this once SERVER-66901 is implemented (see: PHPLIB-884)
112+
if (isset($test->clientOptions->autoEncryptOpts->encryptedFieldsMap)) {
113+
$test->clientOptions->autoEncryptOpts->encryptedFieldsMap = $this->prepareEncryptedFieldsMap($test->clientOptions->autoEncryptOpts->encryptedFieldsMap);
114+
}
115+
110116
try {
111117
$context = Context::fromClientSideEncryption($test, $databaseName, $collectionName);
112118
} catch (SkippedTestError $e) {
@@ -116,15 +122,15 @@ public function testClientSideEncryption(stdClass $test, ?array $runOn, array $d
116122
$this->setContext($context);
117123

118124
self::insertKeyVaultData($context->getClient(), $keyVaultData);
119-
$this->dropTestAndOutcomeCollections();
120-
$this->createTestCollection($jsonSchema);
125+
$this->dropTestAndOutcomeCollections(empty($encryptedFields) ? [] : ['encryptedFields' => $encryptedFields]);
126+
$this->createTestCollection($encryptedFields, $jsonSchema);
121127
$this->insertDataFixtures($data);
122128

123129
if (isset($test->failPoint)) {
124130
$this->configureFailPoint($test->failPoint);
125131
}
126132

127-
$context->enableEncryption();
133+
$context->useEncryptedClientIfConfigured = true;
128134

129135
if (isset($test->expectations)) {
130136
$commandExpectations = CommandExpectations::fromClientSideEncryption($context->getClient(), $test->expectations);
@@ -140,7 +146,7 @@ public function testClientSideEncryption(stdClass $test, ?array $runOn, array $d
140146
$commandExpectations->assert($this, $context);
141147
}
142148

143-
$context->disableEncryption();
149+
$context->useEncryptedClientIfConfigured = false;
144150

145151
if (isset($test->outcome->collection->data)) {
146152
$this->assertOutcomeCollectionData($test->outcome->collection->data, ResultExpectation::ASSERT_DOCUMENTS_MATCH);
@@ -169,6 +175,7 @@ public function provideTests()
169175
$runOn = $json->runOn ?? null;
170176
$data = $json->data ?? [];
171177
// phpcs:disable Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
178+
$encryptedFields = $json->encrypted_fields ?? null;
172179
$keyVaultData = $json->key_vault_data ?? null;
173180
$jsonSchema = $json->json_schema ?? null;
174181
$databaseName = $json->database_name ?? null;
@@ -177,7 +184,7 @@ public function provideTests()
177184

178185
foreach ($json->tests as $test) {
179186
$name = $group . ': ' . $test->description;
180-
$testArgs[$name] = [$test, $runOn, $data, $keyVaultData, $jsonSchema, $databaseName, $collectionName];
187+
$testArgs[$name] = [$test, $runOn, $data, $encryptedFields, $keyVaultData, $jsonSchema, $databaseName, $collectionName];
181188
}
182189
}
183190

@@ -1331,11 +1338,20 @@ private function createInt64(string $value): Int64
13311338
return unserialize($int64);
13321339
}
13331340

1334-
private function createTestCollection($jsonSchema): void
1341+
private function createTestCollection(?stdClass $encryptedFields = null, ?stdClass $jsonSchema = null): void
13351342
{
1336-
$options = empty($jsonSchema) ? [] : ['validator' => ['$jsonSchema' => $jsonSchema]];
1337-
$operation = new CreateCollection($this->getContext()->databaseName, $this->getContext()->collectionName, $options);
1338-
$operation->execute($this->getPrimaryServer());
1343+
$context = $this->getContext();
1344+
$options = $context->defaultWriteOptions;
1345+
1346+
if (! empty($encryptedFields)) {
1347+
$options['encryptedFields'] = $this->prepareEncryptedFields($encryptedFields);
1348+
}
1349+
1350+
if (! empty($jsonSchema)) {
1351+
$options['validator'] = ['$jsonSchema' => $jsonSchema];
1352+
}
1353+
1354+
$context->getDatabase()->createCollection($context->collectionName, $options);
13391355
}
13401356

13411357
private function encryptCorpusValue(string $fieldName, stdClass $data, ClientEncryption $clientEncryption)
@@ -1466,6 +1482,19 @@ private function prepareEncryptedFields(stdClass $encryptedFields): stdClass
14661482
return $encryptedFields;
14671483
}
14681484

1485+
/**
1486+
* @todo Remove this once SERVER-66901 is implemented
1487+
* @see https://jira.mongodb.org/browse/PHPLIB-884
1488+
*/
1489+
private function prepareEncryptedFieldsMap(stdClass $encryptedFieldsMap): stdClass
1490+
{
1491+
foreach ($encryptedFieldsMap as $namespace => $encryptedFields) {
1492+
$encryptedFieldsMap->{$namespace} = $this->prepareEncryptedFields($encryptedFields);
1493+
}
1494+
1495+
return $encryptedFieldsMap;
1496+
}
1497+
14691498
private function skipIfLocalMongocryptdIsUnavailable(): void
14701499
{
14711500
$paths = explode(PATH_SEPARATOR, getenv("PATH"));

tests/SpecTests/Context.php

Lines changed: 6 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -57,15 +57,15 @@ final class Context
5757
/** @var object */
5858
public $session1Lsid;
5959

60+
/** @var bool */
61+
public $useEncryptedClientIfConfigured = false;
62+
6063
/** @var Client */
6164
private $internalClient;
6265

6366
/** @var Client|null */
6467
private $encryptedClient;
6568

66-
/** @var bool */
67-
private $useEncryptedClient = false;
68-
6969
private function __construct(string $databaseName, ?string $collectionName)
7070
{
7171
$this->databaseName = $databaseName;
@@ -74,20 +74,6 @@ private function __construct(string $databaseName, ?string $collectionName)
7474
$this->internalClient = FunctionalTestCase::createTestClient();
7575
}
7676

77-
public function disableEncryption(): void
78-
{
79-
$this->useEncryptedClient = false;
80-
}
81-
82-
public function enableEncryption(): void
83-
{
84-
if (! $this->encryptedClient instanceof Client) {
85-
throw new LogicException('Cannot enable encryption without autoEncryption options');
86-
}
87-
88-
$this->useEncryptedClient = true;
89-
}
90-
9177
public static function fromClientSideEncryption(stdClass $test, $databaseName, $collectionName)
9278
{
9379
$o = new self($databaseName, $collectionName);
@@ -127,6 +113,8 @@ public static function fromClientSideEncryption(stdClass $test, $databaseName, $
127113
$o->outcomeCollectionName = $test->outcome->collection->name;
128114
}
129115

116+
$o->defaultWriteOptions = ['writeConcern' => new WriteConcern(WriteConcern::MAJORITY)];
117+
130118
$o->client = self::createTestClient(null, $clientOptions);
131119

132120
if ($autoEncryptionOptions !== []) {
@@ -292,7 +280,7 @@ public static function getGCPCredentials(): array
292280

293281
public function getClient(): Client
294282
{
295-
return $this->useEncryptedClient && $this->encryptedClient ? $this->encryptedClient : $this->client;
283+
return $this->useEncryptedClientIfConfigured && $this->encryptedClient ? $this->encryptedClient : $this->client;
296284
}
297285

298286
public function getCollection(array $collectionOptions = [], array $databaseOptions = [])

tests/SpecTests/ErrorExpectation.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,8 @@ public function assert(TestCase $test, ?Throwable $actual = null): void
144144
$test->fail(sprintf("Operation threw unexpected %s: %s\n%s", get_class($actual), $actual->getMessage(), $actual->getTraceAsString()));
145145
}
146146

147+
$test->addToAssertionCount(1);
148+
147149
return;
148150
}
149151

tests/SpecTests/FunctionalTestCase.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,7 @@ protected function setContext(Context $context): void
197197
/**
198198
* Drop the test and outcome collections by dropping them.
199199
*/
200-
protected function dropTestAndOutcomeCollections(): void
200+
protected function dropTestAndOutcomeCollections(array $testCollectionDropOptions = []): void
201201
{
202202
$context = $this->getContext();
203203

@@ -213,7 +213,7 @@ protected function dropTestAndOutcomeCollections(): void
213213
$collection = null;
214214
if ($context->collectionName !== null) {
215215
$collection = $context->getCollection($context->defaultWriteOptions);
216-
$collection->drop();
216+
$collection->drop($testCollectionDropOptions);
217217
}
218218

219219
if ($context->outcomeCollectionName !== null) {
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
{
2+
"runOn": [
3+
{
4+
"minServerVersion": "4.1.10"
5+
}
6+
],
7+
"database_name": "default",
8+
"collection_name": "default",
9+
"data": [],
10+
"tests": [
11+
{
12+
"description": "create is OK",
13+
"clientOptions": {
14+
"autoEncryptOpts": {
15+
"kmsProviders": {
16+
"local": {
17+
"key": {
18+
"$binary": {
19+
"base64": "Mng0NCt4ZHVUYUJCa1kxNkVyNUR1QURhZ2h2UzR2d2RrZzh0cFBwM3R6NmdWMDFBMUN3YkQ5aXRRMkhGRGdQV09wOGVNYUMxT2k3NjZKelhaQmRCZGJkTXVyZG9uSjFk",
20+
"subType": "00"
21+
}
22+
}
23+
}
24+
}
25+
}
26+
},
27+
"operations": [
28+
{
29+
"name": "dropCollection",
30+
"object": "database",
31+
"arguments": {
32+
"collection": "unencryptedCollection"
33+
}
34+
},
35+
{
36+
"name": "createCollection",
37+
"object": "database",
38+
"arguments": {
39+
"collection": "unencryptedCollection",
40+
"validator": {
41+
"unencrypted_string": "foo"
42+
}
43+
}
44+
},
45+
{
46+
"name": "assertCollectionExists",
47+
"object": "testRunner",
48+
"arguments": {
49+
"database": "default",
50+
"collection": "unencryptedCollection"
51+
}
52+
}
53+
]
54+
},
55+
{
56+
"description": "createIndexes is OK",
57+
"clientOptions": {
58+
"autoEncryptOpts": {
59+
"kmsProviders": {
60+
"local": {
61+
"key": {
62+
"$binary": {
63+
"base64": "Mng0NCt4ZHVUYUJCa1kxNkVyNUR1QURhZ2h2UzR2d2RrZzh0cFBwM3R6NmdWMDFBMUN3YkQ5aXRRMkhGRGdQV09wOGVNYUMxT2k3NjZKelhaQmRCZGJkTXVyZG9uSjFk",
64+
"subType": "00"
65+
}
66+
}
67+
}
68+
}
69+
}
70+
},
71+
"operations": [
72+
{
73+
"name": "dropCollection",
74+
"object": "database",
75+
"arguments": {
76+
"collection": "unencryptedCollection"
77+
}
78+
},
79+
{
80+
"name": "createCollection",
81+
"object": "database",
82+
"arguments": {
83+
"collection": "unencryptedCollection"
84+
}
85+
},
86+
{
87+
"name": "runCommand",
88+
"object": "database",
89+
"arguments": {
90+
"command": {
91+
"createIndexes": "unencryptedCollection",
92+
"indexes": [
93+
{
94+
"name": "name",
95+
"key": {
96+
"name": 1
97+
}
98+
}
99+
]
100+
}
101+
}
102+
},
103+
{
104+
"name": "assertIndexExists",
105+
"object": "testRunner",
106+
"arguments": {
107+
"database": "default",
108+
"collection": "unencryptedCollection",
109+
"index": "name"
110+
}
111+
}
112+
]
113+
}
114+
]
115+
}

0 commit comments

Comments
 (0)