Skip to content

Commit d6e08a4

Browse files
committed
Implement prose tests for $lookup support
As laid out in the spec: mongodb/specifications@527e22d
1 parent 68f176a commit d6e08a4

File tree

3 files changed

+374
-2
lines changed

3 files changed

+374
-2
lines changed
Lines changed: 372 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,372 @@
1+
<?php
2+
3+
namespace MongoDB\Tests\SpecTests\ClientSideEncryption;
4+
5+
use MongoDB\BSON\Binary;
6+
use MongoDB\Client;
7+
use PHPUnit\Framework\Attributes\Group;
8+
9+
use function base64_decode;
10+
use function file_get_contents;
11+
12+
/**
13+
* Prose test 25: Test lookup
14+
*
15+
* @see https://github.com/mongodb/specifications/blob/master/source/client-side-encryption/tests/README.md#25-test-lookup
16+
*/
17+
#[Group('csfle')]
18+
class Prose25_LookupTest extends FunctionalTestCase
19+
{
20+
private $key1Id;
21+
22+
private const COLL_CSFLE = 'csfle';
23+
private const COLL_CSFLE2 = 'csfle2';
24+
private const COLL_QE = 'qe';
25+
private const COLL_QE2 = 'qe2';
26+
private const COLL_NO_SCHEMA = 'no_schema';
27+
private const COLL_NO_SCHEMA2 = 'no_schema2';
28+
29+
private static string $dataDir = __DIR__ . '/../../specifications/source/client-side-encryption/etc/data/lookup';
30+
31+
public function setUp(): void
32+
{
33+
parent::setUp();
34+
35+
if ($this->isStandalone()) {
36+
$this->markTestSkipped('Lookup tests require replica sets');
37+
}
38+
39+
$this->skipIfServerVersion('<', '7.0.0', 'Lookup encryption tests require MongoDB 7.0 or later');
40+
41+
$key1Document = $this->decodeJson(file_get_contents(self::$dataDir . '/key-doc.json'));
42+
$this->key1Id = $key1Document->_id;
43+
44+
$encryptedClient = $this->getEncryptedClient();
45+
46+
// Drop the key vault collection and insert key1Document with a majority write concern
47+
self::insertKeyVaultData($encryptedClient, [$key1Document]);
48+
49+
$this->refreshCollections($encryptedClient);
50+
}
51+
52+
private function getEncryptedClient(): Client
53+
{
54+
$autoEncryptionOpts = [
55+
'keyVaultNamespace' => 'keyvault.datakeys',
56+
'kmsProviders' => ['local' => ['key' => new Binary(base64_decode(self::LOCAL_MASTERKEY))]],
57+
];
58+
59+
return self::createTestClient(null, [], [
60+
'autoEncryption' => $autoEncryptionOpts,
61+
/* libmongocrypt caches results from listCollections. Use a new
62+
* client in each test to ensure its encryptedFields is applied. */
63+
'disableClientPersistence' => true,
64+
]);
65+
}
66+
67+
private function refreshCollections(Client $client): void
68+
{
69+
$encryptedDb = $client->getDatabase(self::getDatabaseName());
70+
$unencryptedDb = self::createTestClient()->getDatabase(self::getDatabaseName());
71+
72+
$optionsMap = [
73+
self::COLL_CSFLE => [
74+
'validator' => [
75+
'$jsonSchema' => $this->decodeJson(file_get_contents(self::$dataDir . '/schema-csfle.json')),
76+
],
77+
],
78+
self::COLL_CSFLE2 => [
79+
'validator' => [
80+
'$jsonSchema' => $this->decodeJson(file_get_contents(self::$dataDir . '/schema-csfle2.json')),
81+
],
82+
],
83+
self::COLL_QE => [
84+
'encryptedFields' => $this->decodeJson(file_get_contents(self::$dataDir . '/schema-qe.json')),
85+
],
86+
self::COLL_QE2 => [
87+
'encryptedFields' => $this->decodeJson(file_get_contents(self::$dataDir . '/schema-qe2.json')),
88+
],
89+
self::COLL_NO_SCHEMA => [],
90+
self::COLL_NO_SCHEMA2 => [],
91+
];
92+
93+
foreach ($optionsMap as $collectionName => $options) {
94+
$encryptedDb->dropCollection($collectionName);
95+
$encryptedDb->createCollection($collectionName, $options);
96+
97+
$collection = $unencryptedDb->getCollection($collectionName);
98+
99+
$result = $encryptedDb->getCollection($collectionName)->insertOne([$collectionName => $collectionName]);
100+
101+
if ($options) {
102+
$document = $collection->findOne(['_id' => $result->getInsertedId()]);
103+
$this->assertInstanceOf(Binary::class, $document->{$collectionName});
104+
}
105+
}
106+
}
107+
108+
private function assertPipelineReturnsSingleDocument(string $collection, array $pipeline, array $expected): void
109+
{
110+
$this->skipIfServerVersion('<', '8.1.0', 'Lookup test case requires server version 8.1.0 or later');
111+
$this->skipIfClientSideEncryptionIsNotSupported();
112+
113+
$cursor = $this
114+
->getEncryptedClient()
115+
->getCollection(self::getDatabaseName(), $collection)
116+
->aggregate($pipeline);
117+
118+
$cursor->rewind();
119+
$this->assertMatchesDocument(
120+
$expected,
121+
$cursor->current(),
122+
);
123+
$this->assertNull($cursor->next());
124+
}
125+
126+
public function testCase1_CsfleJoinsNoSchema(): void
127+
{
128+
$pipeline = [
129+
[
130+
'$match' => ['csfle' => 'csfle'],
131+
],
132+
[
133+
'$lookup' => [
134+
'from' => 'no_schema',
135+
'as' => 'matched',
136+
'pipeline' => [
137+
[
138+
'$match' => ['no_schema' => 'no_schema'],
139+
],
140+
[
141+
'$project' => ['_id' => 0],
142+
],
143+
],
144+
],
145+
],
146+
[
147+
'$project' => ['_id' => 0],
148+
],
149+
];
150+
$expected = [
151+
'csfle' => 'csfle',
152+
'matched' => [
153+
['no_schema' => 'no_schema'],
154+
],
155+
];
156+
157+
$this->assertPipelineReturnsSingleDocument(self::COLL_CSFLE, $pipeline, $expected);
158+
}
159+
160+
public function testCase2_QeJoinsNoSchema(): void
161+
{
162+
$pipeline = [
163+
[
164+
'$match' => ['qe' => 'qe'],
165+
],
166+
[
167+
'$lookup' => [
168+
'from' => 'no_schema',
169+
'as' => 'matched',
170+
'pipeline' => [
171+
[
172+
'$match' => ['no_schema' => 'no_schema'],
173+
],
174+
[
175+
'$project' => [
176+
'_id' => 0,
177+
'__safeContent__' => 0,
178+
],
179+
],
180+
],
181+
],
182+
],
183+
[
184+
'$project' => [
185+
'_id' => 0,
186+
'__safeContent__' => 0,
187+
],
188+
],
189+
];
190+
$expected = [
191+
'qe' => 'qe',
192+
'matched' => [
193+
['no_schema' => 'no_schema'],
194+
],
195+
];
196+
197+
$this->assertPipelineReturnsSingleDocument(self::COLL_QE, $pipeline, $expected);
198+
}
199+
200+
public function testCase3_NoSchemaJoinsCsfle(): void
201+
{
202+
$pipeline = [['$match' => ['no_schema' => 'no_schema']],
203+
[
204+
'$lookup' => [
205+
'from' => 'csfle',
206+
'as' => 'matched',
207+
'pipeline' => [
208+
[
209+
'$match' => ['csfle' => 'csfle'],
210+
],
211+
[
212+
'$project' => ['_id' => 0],
213+
],
214+
],
215+
],
216+
],
217+
['$project' => ['_id' => 0]],
218+
];
219+
$expected = ['no_schema' => 'no_schema', 'matched' => [['csfle' => 'csfle']]];
220+
221+
$this->assertPipelineReturnsSingleDocument(self::COLL_NO_SCHEMA, $pipeline, $expected);
222+
}
223+
224+
public function testCase4_NoSchemaJoinsQe(): void
225+
{
226+
$pipeline = [
227+
[
228+
'$match' => ['no_schema' => 'no_schema'],
229+
],
230+
[
231+
'$lookup' => [
232+
'from' => 'qe',
233+
'as' => 'matched',
234+
'pipeline' => [
235+
[
236+
'$match' => ['qe' => 'qe'],
237+
],
238+
[
239+
'$project' => [
240+
'_id' => 0,
241+
'__safeContent__' => 0,
242+
],
243+
],
244+
],
245+
],
246+
],
247+
[
248+
'$project' => ['_id' => 0],
249+
],
250+
];
251+
$expected = [
252+
'no_schema' => 'no_schema',
253+
'matched' => [
254+
['qe' => 'qe'],
255+
],
256+
];
257+
258+
$this->assertPipelineReturnsSingleDocument(self::COLL_NO_SCHEMA, $pipeline, $expected);
259+
}
260+
261+
public function testCase5_CsfleJoinsCsfle2(): void
262+
{
263+
$pipeline = [
264+
['$match' => ['csfle' => 'csfle']],
265+
[
266+
'$lookup' => [
267+
'from' => 'csfle2',
268+
'as' => 'matched',
269+
'pipeline' => [
270+
[
271+
'$match' => ['csfle2' => 'csfle2'],
272+
],
273+
[
274+
'$project' => ['_id' => 0],
275+
],
276+
],
277+
],
278+
],
279+
['$project' => ['_id' => 0]],
280+
];
281+
$expected = ['csfle' => 'csfle', 'matched' => [['csfle2' => 'csfle2']]];
282+
283+
$this->assertPipelineReturnsSingleDocument(self::COLL_CSFLE, $pipeline, $expected);
284+
}
285+
286+
public function testCase6_QeJoinsQe2(): void
287+
{
288+
$pipeline = [
289+
['$match' => ['qe' => 'qe']],
290+
[
291+
'$lookup' => [
292+
'from' => 'qe2',
293+
'as' => 'matched',
294+
'pipeline' => [
295+
[
296+
'$match' => ['qe2' => 'qe2'],
297+
],
298+
[
299+
'$project' => [
300+
'_id' => 0,
301+
'__safeContent__' => 0,
302+
],
303+
],
304+
],
305+
],
306+
],
307+
['$project' => ['_id' => 0, '__safeContent__' => 0]],
308+
];
309+
$expected = ['qe' => 'qe', 'matched' => [['qe2' => 'qe2']]];
310+
311+
$this->assertPipelineReturnsSingleDocument(self::COLL_QE, $pipeline, $expected);
312+
}
313+
314+
public function testCase7_NoSchemaJoinsNoSchema2(): void
315+
{
316+
$pipeline = [
317+
['$match' => ['no_schema' => 'no_schema']],
318+
[
319+
'$lookup' => [
320+
'from' => 'no_schema2',
321+
'as' => 'matched',
322+
'pipeline' => [
323+
['$match' => ['no_schema2' => 'no_schema2']],
324+
['$project' => ['_id' => 0]],
325+
],
326+
],
327+
],
328+
['$project' => ['_id' => 0]],
329+
];
330+
$expected = ['no_schema' => 'no_schema', 'matched' => [['no_schema2' => 'no_schema2']]];
331+
332+
$this->assertPipelineReturnsSingleDocument(self::COLL_NO_SCHEMA, $pipeline, $expected);
333+
}
334+
335+
public function testCase8_CsfleJoinsQeFails(): void
336+
{
337+
$this->skipIfServerVersion('<', '8.1.0', 'Lookup test case requires server version 8.1.0 or later');
338+
$this->skipIfClientSideEncryptionIsNotSupported();
339+
340+
$this->expectExceptionMessage('not supported');
341+
342+
$this->getEncryptedClient()
343+
->getCollection(self::getDatabaseName(), self::COLL_CSFLE)
344+
->aggregate([
345+
[
346+
'$match' => ['csfle' => 'qe'],
347+
],
348+
[
349+
'$lookup' => [
350+
'from' => 'qe',
351+
'as' => 'matched',
352+
'pipeline' => [
353+
[
354+
'$match' => ['qe' => 'qe'],
355+
],
356+
[
357+
'$project' => ['_id' => 0],
358+
],
359+
],
360+
],
361+
],
362+
[
363+
'$project' => ['_id' => 0],
364+
],
365+
]);
366+
}
367+
368+
public function testCase9_TestErrorWithLessThan8_1(): void
369+
{
370+
$this->markTestSkipped('Depends on PHPC-2616 to determine crypt shared version.');
371+
}
372+
}

tests/drivers-evergreen-tools

tests/specifications

Submodule specifications updated 117 files

0 commit comments

Comments
 (0)