diff --git a/src/Symfony/Routing/ApiLoader.php b/src/Symfony/Routing/ApiLoader.php index 67a4fcbf5e..715687d656 100644 --- a/src/Symfony/Routing/ApiLoader.php +++ b/src/Symfony/Routing/ApiLoader.php @@ -62,6 +62,12 @@ public function load(mixed $data, ?string $type = null): RouteCollection if ($notExposedOnly) { $routeCollection->addCollection($this->fileLoader->load('genid.php')); + + // JSON-LD responses, errors included, link to the API documentation and to the context. + if (isset($this->formats['jsonld'])) { + $routeCollection->addCollection($this->fileLoader->load('docs.php')); + $routeCollection->addCollection($this->fileLoader->load('jsonld.php')); + } } else { $this->loadExternalFiles($routeCollection); } diff --git a/tests/Fixtures/TestBundle/ApiResource/Issue7915/IrisDummy.php b/tests/Fixtures/TestBundle/ApiResource/Issue7915/IrisDummy.php new file mode 100644 index 0000000000..a3d318e9fb --- /dev/null +++ b/tests/Fixtures/TestBundle/ApiResource/Issue7915/IrisDummy.php @@ -0,0 +1,35 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\Issue7915; + +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\Get; +use ApiPlatform\Metadata\GetCollection; +use ApiPlatform\Metadata\Operation; + +#[ApiResource( + operations: [ + new Get(uriTemplate: '/iris_dummies/{id}', provider: [self::class, 'provide']), + new GetCollection(uriTemplate: '/iris_dummies', provider: [self::class, 'provide']), + ], +)] +class IrisDummy +{ + public int $id = 1; + + public static function provide(Operation $operation, array $uriVariables = [], array $context = []): self|array + { + return $operation instanceof GetCollection ? [new self()] : new self(); + } +} diff --git a/tests/Fixtures/app/config/config_iris.yml b/tests/Fixtures/app/config/config_iris.yml new file mode 100644 index 0000000000..3cc6d1aa08 --- /dev/null +++ b/tests/Fixtures/app/config/config_iris.yml @@ -0,0 +1,2 @@ +imports: + - { resource: config_test.yml } diff --git a/tests/Fixtures/app/config/routing_iris.yml b/tests/Fixtures/app/config/routing_iris.yml new file mode 100644 index 0000000000..efdec79737 --- /dev/null +++ b/tests/Fixtures/app/config/routing_iris.yml @@ -0,0 +1,3 @@ +api_platform_iris: + resource: . + type: api_platform_iris diff --git a/tests/Functional/IrisRoutesTest.php b/tests/Functional/IrisRoutesTest.php new file mode 100644 index 0000000000..0d37951d6f --- /dev/null +++ b/tests/Functional/IrisRoutesTest.php @@ -0,0 +1,65 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Tests\Functional; + +use ApiPlatform\Symfony\Bundle\Test\ApiTestCase; +use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\Issue7915\IrisDummy; +use ApiPlatform\Tests\SetupClassResourcesTrait; + +/** + * The "api_platform_iris" route loader registers item routes answered by the not exposed action. + */ +final class IrisRoutesTest extends ApiTestCase +{ + use SetupClassResourcesTrait; + + protected static ?bool $alwaysBootKernel = false; + + /** + * @return class-string[] + */ + public static function getResources(): array + { + return [IrisDummy::class]; + } + + public function testNotExposedItemRouteIsAJsonLdNotFound(): void + { + $this->bootIrisKernel(); + self::createClient()->request('GET', '/iris_dummies/1', ['headers' => ['accept' => 'application/ld+json']]); + + $this->assertResponseStatusCodeSame(404); + $this->assertJsonContains([ + '@context' => '/contexts/Error', + 'status' => 404, + ]); + } + + private function bootIrisKernel(): void + { + $baseEnv = $_SERVER['APP_ENV'] ?? 'test'; + if ('mongodb' === $baseEnv) { + $this->markTestSkipped('The iris environment is not configured for MongoDB.'); + } + + // AppKernel overrides environment with $_SERVER['APP_ENV'] (behat compat). + $_SERVER['APP_ENV'] = 'iris'; + + try { + self::bootKernel(['environment' => 'iris']); + } finally { + $_SERVER['APP_ENV'] = $baseEnv; + } + } +} diff --git a/tests/Symfony/Routing/ApiLoaderTest.php b/tests/Symfony/Routing/ApiLoaderTest.php index 4f029a9d02..430f016d91 100644 --- a/tests/Symfony/Routing/ApiLoaderTest.php +++ b/tests/Symfony/Routing/ApiLoaderTest.php @@ -278,8 +278,26 @@ public function testApiLoaderIrisTypeRegistersItemRoutesWithNotExposedController $this->assertNotNull($routeCollection->get('api_dummies_not_exposed_item')); $this->assertNotNull($routeCollection->get('api_dummies_not_exposed_by_uuid_item')); $this->assertNull($routeCollection->get('api_dummies_get_collection')); - $this->assertNull($routeCollection->get('api_jsonld_context')); + $this->assertNotNull($routeCollection->get('api_jsonld_context')); + $this->assertNotNull($routeCollection->get('api_doc')); $this->assertNull($routeCollection->get('api_entrypoint')); + $this->assertNull($routeCollection->get('api_validation_errors')); + $this->assertNotNull($routeCollection->get('api_genid')); + } + + public function testApiLoaderIrisTypeWithoutJsonLdRegistersNeitherDocsNorContextRoutes(): void + { + $resourceCollection = new ResourceMetadataCollection(Dummy::class, [ + (new ApiResource())->withShortName('dummy')->withOperations(new Operations([ + 'api_dummies_get_item' => (new Get())->withUriTemplate('/dummies/{id}{._format}')->withController('api_platform.action.get_item'), + ])), + ]); + + $routeCollection = $this->getApiLoaderWithResourceMetadataCollection($resourceCollection, formats: ['json' => ['application/json']])->load(null, ApiLoader::TYPE_IRIS); + + $this->assertNotNull($routeCollection->get('api_dummies_get_item')); + $this->assertNull($routeCollection->get('api_jsonld_context')); + $this->assertNull($routeCollection->get('api_doc')); $this->assertNotNull($routeCollection->get('api_genid')); } @@ -355,7 +373,7 @@ public function testApiLoaderLetsAnExposedOperationWinOverANotExposedPlaceholder $this->assertSame(RelatedDummyEntity::class, $route->getDefault('_api_resource_class')); } - private function getApiLoaderWithResourceMetadataCollection(ResourceMetadataCollection $resourceCollection, ?ResourceMetadataCollection $relatedResourceCollection = null): ApiLoader + private function getApiLoaderWithResourceMetadataCollection(ResourceMetadataCollection $resourceCollection, ?ResourceMetadataCollection $relatedResourceCollection = null, array $formats = ['jsonld' => ['application/ld+json']]): ApiLoader { $routingConfig = __DIR__.'/../../../src/Symfony/Bundle/Resources/config/routing'; @@ -397,7 +415,7 @@ private function getApiLoaderWithResourceMetadataCollection(ResourceMetadataColl $resourceMetadataFactory = $resourceMetadataFactoryProphecy->reveal(); - return new ApiLoader($kernelProphecy->reveal(), $resourceNameCollectionFactoryProphecy->reveal(), $resourceMetadataFactory, $containerProphecy->reveal(), ['jsonld' => ['application/ld+json']], [], false, true, true, false); + return new ApiLoader($kernelProphecy->reveal(), $resourceNameCollectionFactoryProphecy->reveal(), $resourceMetadataFactory, $containerProphecy->reveal(), $formats, [], false, true, true, false); } private function getRoute(string $path, string $controller, ?bool $stateless, string $resourceClass, array $identifiers, string $operationName, array $extraDefaults = [], array $methods = [], array $requirements = [], array $options = [], string $host = '', array $schemes = [], string $condition = ''): Route