diff --git a/tests/UnifiedSpecTests/Constraint/Matches.php b/tests/UnifiedSpecTests/Constraint/Matches.php index e1b659a3e..b793614a4 100644 --- a/tests/UnifiedSpecTests/Constraint/Matches.php +++ b/tests/UnifiedSpecTests/Constraint/Matches.php @@ -3,6 +3,7 @@ namespace MongoDB\Tests\UnifiedSpecTests\Constraint; use LogicException; +use MongoDB\BSON\Document; use MongoDB\BSON\Serializable; use MongoDB\BSON\Type; use MongoDB\Model\BSONArray; @@ -25,10 +26,13 @@ use function is_int; use function is_object; use function ltrim; +use function PHPUnit\Framework\assertInstanceOf; use function PHPUnit\Framework\assertIsBool; use function PHPUnit\Framework\assertIsString; +use function PHPUnit\Framework\assertJson; use function PHPUnit\Framework\assertMatchesRegularExpression; use function PHPUnit\Framework\assertNotNull; +use function PHPUnit\Framework\assertStringStartsWith; use function PHPUnit\Framework\assertThat; use function PHPUnit\Framework\containsOnly; use function PHPUnit\Framework\isInstanceOf; @@ -39,6 +43,7 @@ use function sprintf; use function str_starts_with; use function strrchr; +use function trim; /** * Constraint that checks if one value matches another. @@ -263,6 +268,35 @@ private function assertMatchesOperator(BSONDocument $operator, $actual, string $ return; } + if ($name === '$$matchAsDocument') { + assertInstanceOf(BSONDocument::class, $operator['$$matchAsDocument'], '$$matchAsDocument requires a BSON document'); + assertIsString($actual, '$$matchAsDocument requires actual value to be a JSON string'); + assertJson($actual, '$$matchAsDocument requires actual value to be a JSON string'); + + /* Note: assertJson() accepts array and scalar values, but the spec + * assumes that the JSON string will yield a document. */ + assertStringStartsWith('{', trim($actual), '$$matchAsDocument requires actual value to be a JSON string denoting an object'); + + $actualDocument = Document::fromJSON($actual)->toPHP(); + $constraint = new Matches($operator['$$matchAsDocument'], $this->entityMap, allowExtraRootKeys: false); + + if (! $constraint->evaluate($actualDocument, '', true)) { + self::failAt(sprintf('%s did not match: %s', (new Exporter())->shortenedExport($actual), $constraint->additionalFailureDescription(null)), $keyPath); + } + + return; + } + + if ($name === '$$matchAsRoot') { + $constraint = new Matches($operator['$$matchAsRoot'], $this->entityMap, allowExtraRootKeys: true); + + if (! $constraint->evaluate($actual, '', true)) { + self::failAt(sprintf('$actual did not match as root-level document: %s', $constraint->additionalFailureDescription(null)), $keyPath); + } + + return; + } + if ($name === '$$matchesEntity') { assertNotNull($this->entityMap, '$$matchesEntity requires EntityMap'); assertIsString($operator['$$matchesEntity'], '$$matchesEntity requires string'); diff --git a/tests/UnifiedSpecTests/Constraint/MatchesTest.php b/tests/UnifiedSpecTests/Constraint/MatchesTest.php index 0de7aa9cb..41b57fad1 100644 --- a/tests/UnifiedSpecTests/Constraint/MatchesTest.php +++ b/tests/UnifiedSpecTests/Constraint/MatchesTest.php @@ -30,6 +30,12 @@ public function testFlexibleNumericComparison(): void $this->assertResult(true, $c, ['x' => 1.0, 'y' => 1.0], 'Float instead of expected int matches'); $this->assertResult(true, $c, ['x' => 1, 'y' => 1], 'Int instead of expected float matches'); $this->assertResult(false, $c, ['x' => 'foo', 'y' => 1.0], 'Different type does not match'); + + /* Matches uses PHPUnit's comparators, which follow PHP behavior. This + * is more liberal than the comparison logic called for by the unified + * test format. This test can be removed when PHPLIB-1577 is addressed. + */ + $this->assertResult(true, $c, ['x' => '1.0', 'y' => '1'], 'Numeric strings may match ints and floats'); } public function testDoNotAllowExtraRootKeys(): void @@ -171,6 +177,37 @@ public function testOperatorSessionLsid(): void $this->assertResult(false, $c, ['x' => 1], 'session LSID does not match (embedded)'); } + public function testOperatorMatchAsDocument(): void + { + $c = new Matches(['json' => ['$$matchAsDocument' => ['x' => 1]]]); + $this->assertResult(true, $c, ['json' => '{"x": 1}'], 'JSON document matches'); + $this->assertResult(false, $c, ['json' => '{"x": 2}'], 'JSON document does not match'); + $this->assertResult(false, $c, ['json' => '{"x": 1, "y": 2}'], 'JSON document cannot contain extra fields'); + + $c = new Matches(['json' => ['$$matchAsDocument' => ['x' => 1.0]]]); + $this->assertResult(true, $c, ['json' => '{"x": 1}'], 'JSON document matches (flexible numeric comparison)'); + + $c = new Matches(['json' => ['$$matchAsDocument' => ['x' => ['$$exists' => true]]]]); + $this->assertResult(true, $c, ['json' => '{"x": 1}'], 'JSON document matches (special operators)'); + $this->assertResult(false, $c, ['json' => '{"y": 1}'], 'JSON document does not match (special operators)'); + + $c = new Matches(['json' => ['$$matchAsDocument' => ['x' => ['$$type' => 'objectId']]]]); + $this->assertResult(true, $c, ['json' => '{"x": {"$oid": "57e193d7a9cc81b4027498b5"}}'], 'JSON document matches (extended JSON)'); + $this->assertResult(false, $c, ['json' => '{"x": {"$numberDecimal": "1234.5"}}'], 'JSON document does not match (extended JSON)'); + } + + public function testOperatorMatchAsRoot(): void + { + $c = new Matches(['x' => ['$$matchAsRoot' => ['y' => 2]]]); + $this->assertResult(true, $c, ['x' => ['y' => 2, 'z' => 3]], 'Nested document matches (allow extra fields)'); + $this->assertResult(true, $c, ['x' => ['y' => 2.0, 'z' => 3.0]], 'Nested document matches (flexible numeric comparison)'); + $this->assertResult(false, $c, ['x' => ['y' => 3, 'z' => 3]], 'Nested document does not match'); + + $c = new Matches(['x' => ['$$matchAsRoot' => ['y' => ['$$exists' => true]]]]); + $this->assertResult(true, $c, ['x' => ['y' => 2, 'z' => 3]], 'Nested document matches (special operators)'); + $this->assertResult(false, $c, ['x' => ['z' => 3]], 'Nested document matches (special operators)'); + } + #[DataProvider('errorMessageProvider')] public function testErrorMessages($expectedMessageRegex, Matches $constraint, $actualValue): void { @@ -302,6 +339,10 @@ public static function operatorErrorMessageProvider() '$$sessionLsid requires string', new Matches(['x' => ['$$sessionLsid' => 1]], new EntityMap()), ], + '$$matchAsDocument type' => [ + '$$matchAsDocument requires a BSON document', + new Matches(['x' => ['$$matchAsDocument' => 'foo']]), + ], ]; } diff --git a/tests/UnifiedSpecTests/UnifiedSpecTest.php b/tests/UnifiedSpecTests/UnifiedSpecTest.php index 9f47f745b..1c4df0478 100644 --- a/tests/UnifiedSpecTests/UnifiedSpecTest.php +++ b/tests/UnifiedSpecTests/UnifiedSpecTest.php @@ -43,6 +43,7 @@ class UnifiedSpecTest extends FunctionalTestCase 'crud/replaceOne-sort' => 'Sort for replace operations is not supported (PHPLIB-1492)', 'crud/updateOne-sort' => 'Sort for update operations is not supported (PHPLIB-1492)', 'crud/bypassDocumentValidation' => 'bypassDocumentValidation is handled by libmongoc (PHPLIB-1576)', + 'crud/distinct-hint' => 'Hint for distinct operations is not supported (PHPLIB-1582)', ]; /** @var array */ @@ -60,7 +61,7 @@ class UnifiedSpecTest extends FunctionalTestCase 'valid-pass/expectedEventsForClient-eventType: eventType defaults to command if unset' => 'PHPC does not implement CMAP', // CSOT is not yet implemented (PHPC-1760) 'valid-pass/collectionData-createOptions: collection is created with the correct options' => 'CSOT is not yet implemented (PHPC-1760)', - 'valid-pass/matches-lte-operator: special lte matching operator' => 'CSOT is not yet implemented (PHPC-1760)', + 'valid-pass/operator-lte: special lte matching operator' => 'CSOT is not yet implemented (PHPC-1760)', // libmongoc always adds readConcern to aggregate command 'index-management/search index operations ignore read and write concern: listSearchIndexes ignores read and write concern' => 'libmongoc appends readConcern to aggregate command', // Uses an invalid object name diff --git a/tests/UnifiedSpecTests/UnifiedTestRunner.php b/tests/UnifiedSpecTests/UnifiedTestRunner.php index 93d3a3421..92dd0f61a 100644 --- a/tests/UnifiedSpecTests/UnifiedTestRunner.php +++ b/tests/UnifiedSpecTests/UnifiedTestRunner.php @@ -61,7 +61,7 @@ final class UnifiedTestRunner * - 1.9: Only createEntities operation is implemented * - 1.10: Not implemented * - 1.11: Not implemented, but CMAP is not applicable - * - 1.13: Not implemented + * - 1.13: Only $$matchAsDocument and $$matchAsRoot is implemented * - 1.14: Not implemented */ public const MAX_SCHEMA_VERSION = '1.15'; diff --git a/tests/specifications b/tests/specifications index b1b5a7f42..daf9e0744 160000 --- a/tests/specifications +++ b/tests/specifications @@ -1 +1 @@ -Subproject commit b1b5a7f4209ba4a0fd3dca31a7b34378ac5ce6c1 +Subproject commit daf9e0744692d1e48a8a2d90d585602ea965e520