Skip to content

Commit 270b4ad

Browse files
committed
Implement prose tests for $lookup support
As laid out in the spec: mongodb/specifications@527e22d
1 parent 4aed966 commit 270b4ad

File tree

3 files changed

+381
-2
lines changed

3 files changed

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

tests/drivers-evergreen-tools

tests/specifications

Submodule specifications updated 117 files

0 commit comments

Comments
 (0)