diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c6e831f511b..2b99e071768 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -479,7 +479,7 @@ jobs: continue-on-error: true postgresql: - name: Behat (PHP ${{ matrix.php }}) (PostgreSQL) + name: PHPUnit + Behat (PHP ${{ matrix.php }}) (PostgreSQL) runs-on: ubuntu-latest timeout-minutes: 20 strategy: @@ -524,12 +524,16 @@ jobs: composer global link . - name: Clear test app cache run: tests/Fixtures/app/console cache:clear --ansi + - name: Run PHPUnit tests + run: vendor/bin/phpunit + - name: Clear test app cache + run: tests/Fixtures/app/console cache:clear --ansi - name: Run Behat tests run: | vendor/bin/behat --out=std --format=progress --profile=postgres --no-interaction -vv mysql: - name: Behat (PHP ${{ matrix.php }}) (MySQL) + name: PHPUnit + Behat (PHP ${{ matrix.php }}) (MySQL) runs-on: ubuntu-latest timeout-minutes: 20 strategy: @@ -575,6 +579,10 @@ jobs: composer global link . - name: Clear test app cache run: tests/Fixtures/app/console cache:clear --ansi + - name: Run PHPUnit tests + run: vendor/bin/phpunit + - name: Clear test app cache + run: tests/Fixtures/app/console cache:clear --ansi - name: Run Behat tests run: vendor/bin/behat --out=std --format=progress --profile=default --no-interaction --tags '~@!mysql' diff --git a/tests/Fixtures/TestBundle/Document/FilteredOrderParameter.php b/tests/Fixtures/TestBundle/Document/FilteredOrderParameter.php index ea6e5538dee..a08313f57d7 100644 --- a/tests/Fixtures/TestBundle/Document/FilteredOrderParameter.php +++ b/tests/Fixtures/TestBundle/Document/FilteredOrderParameter.php @@ -38,17 +38,17 @@ 'date_null_always_first' => new QueryParameter( filter: new OrderFilter(), property: 'createdAt', - filterContext: OrderFilterInterface::NULLS_ALWAYS_FIRST, + filterContext: ['nulls_comparison' => OrderFilterInterface::NULLS_ALWAYS_FIRST], nativeType: new BuiltinType(TypeIdentifier::STRING) ), 'date_null_always_first_old_way' => new QueryParameter( - filter: new OrderFilter(properties: ['createdAt' => OrderFilterInterface::NULLS_ALWAYS_FIRST]), + filter: new OrderFilter(properties: ['createdAt' => ['nulls_comparison' => OrderFilterInterface::NULLS_ALWAYS_FIRST]]), property: 'createdAt', nativeType: new BuiltinType(TypeIdentifier::STRING) ), 'order[:property]' => new QueryParameter( filter: new OrderFilter(), - filterContext: OrderFilterInterface::NULLS_ALWAYS_FIRST, + filterContext: ['nulls_comparison' => OrderFilterInterface::NULLS_ALWAYS_FIRST], ), ], )] diff --git a/tests/Fixtures/TestBundle/Entity/Cart.php b/tests/Fixtures/TestBundle/Entity/Cart.php index 844eb65d5d0..1adf0cad569 100644 --- a/tests/Fixtures/TestBundle/Entity/Cart.php +++ b/tests/Fixtures/TestBundle/Entity/Cart.php @@ -62,7 +62,7 @@ public static function handleLinks(QueryBuilder $queryBuilder, array $uriVariabl ->addGroupBy(\sprintf('%s.id', $rootAlias)); } - public ?int $totalQuantity; + public int|string|null $totalQuantity; #[ORM\Id] #[ORM\GeneratedValue] diff --git a/tests/Fixtures/TestBundle/Entity/FilteredOrderParameter.php b/tests/Fixtures/TestBundle/Entity/FilteredOrderParameter.php index f431cb67b87..21bf7dbaa1a 100644 --- a/tests/Fixtures/TestBundle/Entity/FilteredOrderParameter.php +++ b/tests/Fixtures/TestBundle/Entity/FilteredOrderParameter.php @@ -38,17 +38,17 @@ 'date_null_always_first' => new QueryParameter( filter: new OrderFilter(), property: 'createdAt', - filterContext: OrderFilterInterface::NULLS_ALWAYS_FIRST, + filterContext: ['nulls_comparison' => OrderFilterInterface::NULLS_ALWAYS_FIRST], nativeType: new BuiltinType(TypeIdentifier::STRING) ), 'date_null_always_first_old_way' => new QueryParameter( - filter: new OrderFilter(properties: ['createdAt' => OrderFilterInterface::NULLS_ALWAYS_FIRST]), + filter: new OrderFilter(properties: ['createdAt' => ['nulls_comparison' => OrderFilterInterface::NULLS_ALWAYS_FIRST]]), property: 'createdAt', nativeType: new BuiltinType(TypeIdentifier::STRING) ), 'order[:property]' => new QueryParameter( filter: new OrderFilter(), - filterContext: OrderFilterInterface::NULLS_ALWAYS_FIRST, + filterContext: ['nulls_comparison' => OrderFilterInterface::NULLS_ALWAYS_FIRST], ), ], )] diff --git a/tests/Fixtures/TestBundle/Entity/Issue6041/NumericValidated.php b/tests/Fixtures/TestBundle/Entity/Issue6041/NumericValidated.php index 5394a8eb43e..6954e5b3d3e 100644 --- a/tests/Fixtures/TestBundle/Entity/Issue6041/NumericValidated.php +++ b/tests/Fixtures/TestBundle/Entity/Issue6041/NumericValidated.php @@ -32,7 +32,7 @@ class NumericValidated private ?int $id = null; #[Assert\Range(min: 1, max: 10)] - #[ORM\Column] + #[ORM\Column(name: '_range')] public int $range; #[Assert\GreaterThan(value: 10)] diff --git a/tests/Functional/JsonStreamerTest.php b/tests/Functional/JsonStreamerTest.php index 7247e0e069d..a261daeab1b 100644 --- a/tests/Functional/JsonStreamerTest.php +++ b/tests/Functional/JsonStreamerTest.php @@ -229,7 +229,7 @@ public function testJsonStreamerWriteJsonLd(): void $this->assertSame(0, $res['views']); $this->assertSame(0, $res['rating']); $this->assertFalse($res['isFeatured']); - $this->assertSame('0', $res['price']); + $this->assertContains($res['price'], ['0', '0.00']); // Depends on DB $this->assertStringStartsWith('/json_stream_resources/', $res['@id']); $this->assertSame('/contexts/JsonStreamResource', $res['@context']); @@ -273,7 +273,7 @@ public function testJsonStreamerWriteJson(): void $this->assertSame(0, $res['views']); $this->assertSame(0, $res['rating']); $this->assertFalse($res['isFeatured']); - $this->assertSame('0', $res['price']); + $this->assertContains($res['price'], ['0', '0.00']); // Depends on DB $this->assertArrayNotHasKey('@id', $res); $this->assertArrayNotHasKey('@type', $res); $this->assertArrayNotHasKey('@context', $res); diff --git a/tests/Functional/Parameters/OrderFilterTest.php b/tests/Functional/Parameters/OrderFilterTest.php index c893caae55e..f4812760fd1 100644 --- a/tests/Functional/Parameters/OrderFilterTest.php +++ b/tests/Functional/Parameters/OrderFilterTest.php @@ -13,6 +13,7 @@ namespace ApiPlatform\Tests\Functional\Parameters; +use ApiPlatform\Doctrine\Odm\Filter\OrderFilter; use ApiPlatform\Symfony\Bundle\Test\ApiTestCase; use ApiPlatform\Tests\Fixtures\TestBundle\Document\FilteredOrderParameter as FilteredOrderParameterDocument; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\FilteredOrderParameter; @@ -58,6 +59,12 @@ public function testOrderFilterResponses(string $url, array $expectedOrder): voi $actualOrder = array_map(fn ($item) => $item['createdAt'] ?? null, $orderedItems); + // Default NULL order is different in PostgreSQL. + if ($this->isPostgres()) { + $actualOrder = array_values(array_filter($actualOrder)); + $expectedOrder = array_values(array_filter($expectedOrder)); + } + $this->assertSame($expectedOrder, $actualOrder, \sprintf('Expected order does not match for URL %s', $url)); } @@ -79,29 +86,51 @@ public static function orderFilterScenariosProvider(): \Generator '/filtered_order_parameters?date=desc', ['2024-12-25T00:00:00+00:00', '2024-06-15T00:00:00+00:00', '2024-01-01T00:00:00+00:00', null], ]; - yield 'date_null_always_first_alias_nulls_first' => [ + } + + #[DataProvider('orderFilterNullsComparisonScenariosProvider')] + public function testOrderFilterNullsComparisonResponses(string $url, array $expectedOrder): void + { + if ($this->isMongoDB()) { + $this->markTestSkipped(\sprintf('Not implemented in %s', OrderFilter::class)); + } + + $response = self::createClient()->request('GET', $url); + $this->assertResponseIsSuccessful(); + + $responseData = $response->toArray(); + $orderedItems = $responseData['hydra:member']; + + $actualOrder = array_map(fn ($item) => $item['createdAt'] ?? null, $orderedItems); + + $this->assertSame($expectedOrder, $actualOrder, \sprintf('Expected order does not match for URL %s', $url)); + } + + public static function orderFilterNullsComparisonScenariosProvider(): \Generator + { + yield 'date_null_always_first_alias_asc' => [ '/filtered_order_parameters?date_null_always_first=asc', [null, '2024-01-01T00:00:00+00:00', '2024-06-15T00:00:00+00:00', '2024-12-25T00:00:00+00:00'], ]; - yield 'date_null_always_first_alias_nulls_last' => [ + yield 'date_null_always_first_alias_desc' => [ '/filtered_order_parameters?date_null_always_first=desc', - ['2024-12-25T00:00:00+00:00', '2024-06-15T00:00:00+00:00', '2024-01-01T00:00:00+00:00', null], + [null, '2024-12-25T00:00:00+00:00', '2024-06-15T00:00:00+00:00', '2024-01-01T00:00:00+00:00'], ]; - yield 'date_null_always_first_old_way_alias_nulls_first' => [ + yield 'date_null_always_first_old_way_alias_asc' => [ '/filtered_order_parameters?date_null_always_first_old_way=asc', [null, '2024-01-01T00:00:00+00:00', '2024-06-15T00:00:00+00:00', '2024-12-25T00:00:00+00:00'], ]; - yield 'date_null_always_first_old_way_alias_nulls_last' => [ + yield 'date_null_always_first_old_way_alias_desc' => [ '/filtered_order_parameters?date_null_always_first_old_way=desc', - ['2024-12-25T00:00:00+00:00', '2024-06-15T00:00:00+00:00', '2024-01-01T00:00:00+00:00', null], + [null, '2024-12-25T00:00:00+00:00', '2024-06-15T00:00:00+00:00', '2024-01-01T00:00:00+00:00'], ]; - yield 'order_property_created_at_nulls_first' => [ + yield 'order_property_created_at_null_first_asc' => [ '/filtered_order_parameters?order[createdAt]=asc', [null, '2024-01-01T00:00:00+00:00', '2024-06-15T00:00:00+00:00', '2024-12-25T00:00:00+00:00'], ]; - yield 'order_property_created_at_nulls_last' => [ + yield 'order_property_created_at_null_first_desc' => [ '/filtered_order_parameters?order[createdAt]=desc', - ['2024-12-25T00:00:00+00:00', '2024-06-15T00:00:00+00:00', '2024-01-01T00:00:00+00:00', null], + [null, '2024-12-25T00:00:00+00:00', '2024-06-15T00:00:00+00:00', '2024-01-01T00:00:00+00:00'], ]; } diff --git a/tests/RecreateSchemaTrait.php b/tests/RecreateSchemaTrait.php index cc6b9dfb53a..3808d04c899 100644 --- a/tests/RecreateSchemaTrait.php +++ b/tests/RecreateSchemaTrait.php @@ -37,15 +37,40 @@ private function recreateSchema(array $classes = []): void return; } - /** @var ClassMetadata[] $cl */ - $cl = []; - foreach ($classes as $c) { - $cl[] = $manager->getMetadataFactory()->getMetadataFor($c); + /** @var ClassMetadata[] $metadataCollection */ + $metadataCollection = []; + $processedClasses = []; + + foreach ($classes as $class) { + $this->addMetadataWithDependencies($manager, $class, $metadataCollection, $processedClasses); } $schemaTool = new SchemaTool($manager); - @$schemaTool->dropSchema($cl); - @$schemaTool->createSchema($cl); + + @$schemaTool->dropDatabase(); + @$schemaTool->createSchema($metadataCollection); + } + + /** + * @param array $metadataCollection + * @param array $processedClasses + * + * @param-out array $metadataCollection + * @param-out array $processedClasses + */ + private function addMetadataWithDependencies(EntityManagerInterface $manager, string $class, array &$metadataCollection, array &$processedClasses): void + { + if (isset($processedClasses[$class])) { + return; + } + + $metadata = $manager->getMetadataFactory()->getMetadataFor($class); + $metadataCollection[] = $metadata; + $processedClasses[$class] = true; + + foreach ($metadata->getAssociationMappings() as $associationMapping) { + $this->addMetadataWithDependencies($manager, $associationMapping->targetEntity, $metadataCollection, $processedClasses); + } } private function isMongoDB(): bool @@ -53,6 +78,11 @@ private function isMongoDB(): bool return 'mongodb' === static::getContainer()->getParameter('kernel.environment'); } + private function isPostgres(): bool + { + return 'postgres' === static::getContainer()->getParameter('kernel.environment'); + } + private function getManager(): EntityManagerInterface|DocumentManager { return static::getContainer()->get($this->isMongoDB() ? 'doctrine_mongodb' : 'doctrine')->getManager();