Skip to content

Commit 7f7c856

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

File tree

3 files changed

+375
-2
lines changed

3 files changed

+375
-2
lines changed
Lines changed: 373 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,373 @@
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+
114+
$cursor = $this
115+
->getEncryptedClient()
116+
->getCollection(self::getDatabaseName(), $collection)
117+
->aggregate($pipeline);
118+
119+
$cursor->rewind();
120+
$this->assertMatchesDocument(
121+
$expected,
122+
$cursor->current(),
123+
);
124+
$this->assertNull($cursor->next());
125+
}
126+
127+
public function testCase1_CsfleJoinsNoSchema(): void
128+
{
129+
$pipeline = [
130+
[
131+
'$match' => ['csfle' => 'csfle'],
132+
],
133+
[
134+
'$lookup' => [
135+
'from' => 'no_schema',
136+
'as' => 'matched',
137+
'pipeline' => [
138+
[
139+
'$match' => ['no_schema' => 'no_schema'],
140+
],
141+
[
142+
'$project' => ['_id' => 0],
143+
],
144+
],
145+
],
146+
],
147+
[
148+
'$project' => ['_id' => 0],
149+
],
150+
];
151+
$expected = [
152+
'csfle' => 'csfle',
153+
'matched' => [
154+
['no_schema' => 'no_schema'],
155+
],
156+
];
157+
158+
$this->assertPipelineReturnsSingleDocument(self::COLL_CSFLE, $pipeline, $expected);
159+
}
160+
161+
public function testCase2_QeJoinsNoSchema(): void
162+
{
163+
$pipeline = [
164+
[
165+
'$match' => ['qe' => 'qe'],
166+
],
167+
[
168+
'$lookup' => [
169+
'from' => 'no_schema',
170+
'as' => 'matched',
171+
'pipeline' => [
172+
[
173+
'$match' => ['no_schema' => 'no_schema'],
174+
],
175+
[
176+
'$project' => [
177+
'_id' => 0,
178+
'__safeContent__' => 0,
179+
],
180+
],
181+
],
182+
],
183+
],
184+
[
185+
'$project' => [
186+
'_id' => 0,
187+
'__safeContent__' => 0,
188+
],
189+
],
190+
];
191+
$expected = [
192+
'qe' => 'qe',
193+
'matched' => [
194+
['no_schema' => 'no_schema'],
195+
],
196+
];
197+
198+
$this->assertPipelineReturnsSingleDocument(self::COLL_QE, $pipeline, $expected);
199+
}
200+
201+
public function testCase3_NoSchemaJoinsCsfle(): void
202+
{
203+
$pipeline = [['$match' => ['no_schema' => 'no_schema']],
204+
[
205+
'$lookup' => [
206+
'from' => 'csfle',
207+
'as' => 'matched',
208+
'pipeline' => [
209+
[
210+
'$match' => ['csfle' => 'csfle'],
211+
],
212+
[
213+
'$project' => ['_id' => 0],
214+
],
215+
],
216+
],
217+
],
218+
['$project' => ['_id' => 0]],
219+
];
220+
$expected = ['no_schema' => 'no_schema', 'matched' => [['csfle' => 'csfle']]];
221+
222+
$this->assertPipelineReturnsSingleDocument(self::COLL_NO_SCHEMA, $pipeline, $expected);
223+
}
224+
225+
public function testCase4_NoSchemaJoinsQe(): void
226+
{
227+
$pipeline = [
228+
[
229+
'$match' => ['no_schema' => 'no_schema'],
230+
],
231+
[
232+
'$lookup' => [
233+
'from' => 'qe',
234+
'as' => 'matched',
235+
'pipeline' => [
236+
[
237+
'$match' => ['qe' => 'qe'],
238+
],
239+
[
240+
'$project' => [
241+
'_id' => 0,
242+
'__safeContent__' => 0,
243+
],
244+
],
245+
],
246+
],
247+
],
248+
[
249+
'$project' => ['_id' => 0],
250+
],
251+
];
252+
$expected = [
253+
'no_schema' => 'no_schema',
254+
'matched' => [
255+
['qe' => 'qe'],
256+
],
257+
];
258+
259+
$this->assertPipelineReturnsSingleDocument(self::COLL_NO_SCHEMA, $pipeline, $expected);
260+
}
261+
262+
public function testCase5_CsfleJoinsCsfle2(): void
263+
{
264+
$pipeline = [
265+
['$match' => ['csfle' => 'csfle']],
266+
[
267+
'$lookup' => [
268+
'from' => 'csfle2',
269+
'as' => 'matched',
270+
'pipeline' => [
271+
[
272+
'$match' => ['csfle2' => 'csfle2'],
273+
],
274+
[
275+
'$project' => ['_id' => 0],
276+
],
277+
],
278+
],
279+
],
280+
['$project' => ['_id' => 0]],
281+
];
282+
$expected = ['csfle' => 'csfle', 'matched' => [['csfle2' => 'csfle2']]];
283+
284+
$this->assertPipelineReturnsSingleDocument(self::COLL_CSFLE, $pipeline, $expected);
285+
}
286+
287+
public function testCase6_QeJoinsQe2(): void
288+
{
289+
$pipeline = [
290+
['$match' => ['qe' => 'qe']],
291+
[
292+
'$lookup' => [
293+
'from' => 'qe2',
294+
'as' => 'matched',
295+
'pipeline' => [
296+
[
297+
'$match' => ['qe2' => 'qe2'],
298+
],
299+
[
300+
'$project' => [
301+
'_id' => 0,
302+
'__safeContent__' => 0,
303+
],
304+
],
305+
],
306+
],
307+
],
308+
['$project' => ['_id' => 0, '__safeContent__' => 0]],
309+
];
310+
$expected = ['qe' => 'qe', 'matched' => [['qe2' => 'qe2']]];
311+
312+
$this->assertPipelineReturnsSingleDocument(self::COLL_QE, $pipeline, $expected);
313+
}
314+
315+
public function testCase7_NoSchemaJoinsNoSchema2(): void
316+
{
317+
$pipeline = [
318+
['$match' => ['no_schema' => 'no_schema']],
319+
[
320+
'$lookup' => [
321+
'from' => 'no_schema2',
322+
'as' => 'matched',
323+
'pipeline' => [
324+
['$match' => ['no_schema2' => 'no_schema2']],
325+
['$project' => ['_id' => 0]],
326+
],
327+
],
328+
],
329+
['$project' => ['_id' => 0]],
330+
];
331+
$expected = ['no_schema' => 'no_schema', 'matched' => [['no_schema2' => 'no_schema2']]];
332+
333+
$this->assertPipelineReturnsSingleDocument(self::COLL_NO_SCHEMA, $pipeline, $expected);
334+
}
335+
336+
public function testCase8_CsfleJoinsQeFails(): void
337+
{
338+
$this->skipIfServerVersion('<', '8.1.0', 'Lookup test case requires server version 8.1.0 or later');
339+
$this->skipIfClientSideEncryptionIsNotSupported();
340+
341+
$this->expectExceptionMessage('not supported');
342+
343+
$this->getEncryptedClient()
344+
->getCollection(self::getDatabaseName(), self::COLL_CSFLE)
345+
->aggregate([
346+
[
347+
'$match' => ['csfle' => 'qe'],
348+
],
349+
[
350+
'$lookup' => [
351+
'from' => 'qe',
352+
'as' => 'matched',
353+
'pipeline' => [
354+
[
355+
'$match' => ['qe' => 'qe'],
356+
],
357+
[
358+
'$project' => ['_id' => 0],
359+
],
360+
],
361+
],
362+
],
363+
[
364+
'$project' => ['_id' => 0],
365+
],
366+
]);
367+
}
368+
369+
public function testCase9_TestErrorWithLessThan8_1(): void
370+
{
371+
$this->markTestSkipped('Depends on PHPC-2616 to determine crypt shared version.');
372+
}
373+
}

tests/drivers-evergreen-tools

tests/specifications

Submodule specifications updated 117 files

0 commit comments

Comments
 (0)