From f5eaa10eb29f51cc410661fa6530c3b22b6545b3 Mon Sep 17 00:00:00 2001 From: soyuka Date: Mon, 29 Sep 2025 11:09:54 +0200 Subject: [PATCH] test: move tests from behat to phpunit --- behat.yml.dist | 9 - features/openapi/docs.feature | 339 --------------------- features/openapi/entrypoint.feature | 19 -- tests/Behat/OpenApiContext.php | 195 ------------- tests/Functional/OpenApiTest.php | 438 +++++++++++++++++++++++++++- 5 files changed, 434 insertions(+), 566 deletions(-) delete mode 100644 features/openapi/docs.feature delete mode 100644 features/openapi/entrypoint.feature delete mode 100644 tests/Behat/OpenApiContext.php diff --git a/behat.yml.dist b/behat.yml.dist index 371bdd03fa6..5f7419cf8c0 100644 --- a/behat.yml.dist +++ b/behat.yml.dist @@ -7,7 +7,6 @@ default: - 'ApiPlatform\Tests\Behat\GraphqlContext' - 'ApiPlatform\Tests\Behat\JsonContext' - 'ApiPlatform\Tests\Behat\HydraContext' - - 'ApiPlatform\Tests\Behat\OpenApiContext' - 'ApiPlatform\Tests\Behat\HttpCacheContext' - 'ApiPlatform\Tests\Behat\JsonApiContext' - 'ApiPlatform\Tests\Behat\JsonHalContext' @@ -43,7 +42,6 @@ postgres: - 'ApiPlatform\Tests\Behat\GraphqlContext' - 'ApiPlatform\Tests\Behat\JsonContext' - 'ApiPlatform\Tests\Behat\HydraContext' - - 'ApiPlatform\Tests\Behat\OpenApiContext' - 'ApiPlatform\Tests\Behat\HttpCacheContext' - 'ApiPlatform\Tests\Behat\JsonApiContext' - 'ApiPlatform\Tests\Behat\JsonHalContext' @@ -64,7 +62,6 @@ mongodb: - 'ApiPlatform\Tests\Behat\GraphqlContext' - 'ApiPlatform\Tests\Behat\JsonContext' - 'ApiPlatform\Tests\Behat\HydraContext' - - 'ApiPlatform\Tests\Behat\OpenApiContext' - 'ApiPlatform\Tests\Behat\HttpCacheContext' - 'ApiPlatform\Tests\Behat\JsonApiContext' - 'ApiPlatform\Tests\Behat\JsonHalContext' @@ -85,7 +82,6 @@ mercure: - 'ApiPlatform\Tests\Behat\GraphqlContext' - 'ApiPlatform\Tests\Behat\JsonContext' - 'ApiPlatform\Tests\Behat\HydraContext' - - 'ApiPlatform\Tests\Behat\OpenApiContext' - 'ApiPlatform\Tests\Behat\HttpCacheContext' - 'ApiPlatform\Tests\Behat\JsonApiContext' - 'ApiPlatform\Tests\Behat\JsonHalContext' @@ -120,7 +116,6 @@ default-coverage: - 'ApiPlatform\Tests\Behat\GraphqlContext' - 'ApiPlatform\Tests\Behat\JsonContext' - 'ApiPlatform\Tests\Behat\HydraContext' - - 'ApiPlatform\Tests\Behat\OpenApiContext' - 'ApiPlatform\Tests\Behat\HttpCacheContext' - 'ApiPlatform\Tests\Behat\JsonApiContext' - 'ApiPlatform\Tests\Behat\JsonHalContext' @@ -141,7 +136,6 @@ mongodb-coverage: - 'ApiPlatform\Tests\Behat\GraphqlContext' - 'ApiPlatform\Tests\Behat\JsonContext' - 'ApiPlatform\Tests\Behat\HydraContext' - - 'ApiPlatform\Tests\Behat\OpenApiContext' - 'ApiPlatform\Tests\Behat\HttpCacheContext' - 'ApiPlatform\Tests\Behat\JsonApiContext' - 'ApiPlatform\Tests\Behat\JsonHalContext' @@ -162,7 +156,6 @@ mercure-coverage: - 'ApiPlatform\Tests\Behat\GraphqlContext' - 'ApiPlatform\Tests\Behat\JsonContext' - 'ApiPlatform\Tests\Behat\HydraContext' - - 'ApiPlatform\Tests\Behat\OpenApiContext' - 'ApiPlatform\Tests\Behat\HttpCacheContext' - 'ApiPlatform\Tests\Behat\JsonApiContext' - 'ApiPlatform\Tests\Behat\JsonHalContext' @@ -194,7 +187,6 @@ legacy: - 'ApiPlatform\Tests\Behat\GraphqlContext' - 'ApiPlatform\Tests\Behat\JsonContext' - 'ApiPlatform\Tests\Behat\HydraContext' - - 'ApiPlatform\Tests\Behat\OpenApiContext' - 'ApiPlatform\Tests\Behat\HttpCacheContext' - 'ApiPlatform\Tests\Behat\JsonApiContext' - 'ApiPlatform\Tests\Behat\JsonHalContext' @@ -229,7 +221,6 @@ symfony_listeners: - 'ApiPlatform\Tests\Behat\GraphqlContext' - 'ApiPlatform\Tests\Behat\JsonContext' - 'ApiPlatform\Tests\Behat\HydraContext' - - 'ApiPlatform\Tests\Behat\OpenApiContext' - 'ApiPlatform\Tests\Behat\HttpCacheContext' - 'ApiPlatform\Tests\Behat\JsonApiContext' - 'ApiPlatform\Tests\Behat\JsonHalContext' diff --git a/features/openapi/docs.feature b/features/openapi/docs.feature deleted file mode 100644 index d1e790d300f..00000000000 --- a/features/openapi/docs.feature +++ /dev/null @@ -1,339 +0,0 @@ -Feature: Documentation support - In order to build an auto-discoverable API - As a client software developer - I need to know OpenAPI specifications of objects I send and receive - - @createSchema - Scenario: Retrieve the OpenAPI documentation - Given I send a "GET" request to "/docs.json" - Then the response status code should be 200 - And the response should be in JSON - And the header "Content-Type" should be equal to "application/json; charset=utf-8" - # Context - And the JSON node "openapi" should be equal to "3.1.0" - # Root properties - And the JSON node "info.title" should be equal to "My Dummy API" - And the JSON node "info.description" should contain "This is a test API." - And the JSON node "info.description" should contain "Made with love" - # Security Schemes - And the JSON node "components.securitySchemes" should be equal to: - """ - { - "oauth": { - "type": "oauth2", - "description": "OAuth 2.0 implicit Grant", - "flows": { - "implicit": { - "authorizationUrl": "http://my-custom-server/openid-connect/auth", - "scopes": {} - } - } - }, - "Some_Authorization_Name": { - "type": "apiKey", - "description": "Value for the Authorization header parameter.", - "name": "Authorization", - "in": "header" - } - } - """ - # Supported classes - And the OpenAPI class "AbstractDummy" exists - And the OpenAPI class "CircularReference" exists - And the OpenAPI class "CircularReference-circular" exists - And the OpenAPI class "CompositeItem" exists - And the OpenAPI class "CompositeLabel" exists - And the OpenAPI class "ConcreteDummy" exists - And the OpenAPI class "CustomIdentifierDummy" exists - And the OpenAPI class "CustomNormalizedDummy-input" exists - And the OpenAPI class "CustomNormalizedDummy-output" exists - And the OpenAPI class "CustomWritableIdentifierDummy" exists - And the OpenAPI class "Dummy" exists - And the OpenAPI class "DummyBoolean" exists - And the OpenAPI class "RelatedDummy" exists - And the OpenAPI class "DummyTableInheritance" exists - And the OpenAPI class "DummyTableInheritanceChild" exists - And the OpenAPI class "OverriddenOperationDummy-overridden_operation_dummy_get" exists - And the OpenAPI class "OverriddenOperationDummy-overridden_operation_dummy_put" exists - And the OpenAPI class "OverriddenOperationDummy-overridden_operation_dummy_read" exists - And the OpenAPI class "OverriddenOperationDummy-overridden_operation_dummy_write" exists - And the OpenAPI class "Person" exists - And the OpenAPI class "RelatedDummy" exists - And the OpenAPI class "NoCollectionDummy" exists - And the OpenAPI class "RelatedToDummyFriend" exists - And the OpenAPI class "RelatedToDummyFriend-fakemanytomany" exists - And the OpenAPI class "DummyFriend" exists - And the OpenAPI class "RelationEmbedder-barcelona" exists - And the OpenAPI class "RelationEmbedder-chicago" exists - And the OpenAPI class "User-user_user-read" exists - And the OpenAPI class "User-user_user-write" exists - And the OpenAPI class "UuidIdentifierDummy" exists - And the OpenAPI class "ThirdLevel" exists - And the OpenAPI class "DummyCar" exists - And the OpenAPI class "DummyWebhook" exists - And the OpenAPI class "ParentDummy" doesn't exist - And the OpenAPI class "UnknownDummy" doesn't exist - And the OpenAPI path "/relation_embedders/{id}/custom" exists - And the OpenAPI path "/override/swagger" exists - And the OpenAPI path "/api/custom-call/{id}" exists - And the JSON node "paths./api/custom-call/{id}.get" should exist - And the JSON node "paths./api/custom-call/{id}.put" should exist - # Properties - And the "id" property exists for the OpenAPI class "Dummy" - And the "name" property is required for the OpenAPI class "Dummy.jsonld" - And the "genderType" property exists for the OpenAPI class "Person" - And the "genderType" property for the OpenAPI class "Person" should be equal to: - """ - { - "default": "male", - "type": ["string", "null"], - "enum": [ - "male", - "female", - null - ] - } - """ - And the "playMode" property exists for the OpenAPI class "VideoGame" - And the "playMode" property for the OpenAPI class "VideoGame" should be equal to: - """ - { - "type": "string", - "format": "iri-reference", - "example": "https://example.com/" - } - """ - # Enable these tests when SF 4.4 / PHP 7.1 support is dropped - #And the "isDummyBoolean" property exists for the OpenAPI class "DummyBoolean" - #And the "isDummyBoolean" property is not read only for the OpenAPI class "DummyBoolean" - # Filters - And the JSON node "paths./dummies.get.parameters[4].name" should be equal to "dummyBoolean" - And the JSON node "paths./dummies.get.parameters[4].in" should be equal to "query" - And the JSON node "paths./dummies.get.parameters[4].required" should be false - And the JSON node "paths./dummies.get.parameters[4].schema.type" should be equal to "boolean" - - And the JSON node "paths./dummy_cars.get.parameters[9].name" should be equal to "foobar[]" - And the JSON node "paths./dummy_cars.get.parameters[9].description" should be equal to "Allows you to reduce the response to contain only the properties you need. If your desired property is nested, you can address it using nested arrays. Example: foobar[]={propertyName}&foobar[]={anotherPropertyName}&foobar[{nestedPropertyParent}][]={nestedProperty}" - - # Webhook - And the JSON node "webhooks.a.get.description" should be equal to "Something else here for example" - And the JSON node "webhooks.b.post.description" should be equal to "Hi! it's me, I'm the problem, it's me" - - # Subcollection - check filter on subResource - And the JSON node "paths./related_dummies/{id}/related_to_dummy_friends.get.parameters[0].name" should be equal to "id" - And the JSON node "paths./related_dummies/{id}/related_to_dummy_friends.get.parameters[0].in" should be equal to "path" - And the JSON node "paths./related_dummies/{id}/related_to_dummy_friends.get.parameters[0].required" should be true - And the JSON node "paths./related_dummies/{id}/related_to_dummy_friends.get.parameters[0].schema.type" should be equal to "string" - - And the JSON node "paths./related_dummies/{id}/related_to_dummy_friends.get.parameters[1].name" should be equal to "page" - And the JSON node "paths./related_dummies/{id}/related_to_dummy_friends.get.parameters[1].in" should be equal to "query" - And the JSON node "paths./related_dummies/{id}/related_to_dummy_friends.get.parameters[1].required" should be false - And the JSON node "paths./related_dummies/{id}/related_to_dummy_friends.get.parameters[1].schema.type" should be equal to "integer" - - And the JSON node "paths./related_dummies/{id}/related_to_dummy_friends.get.parameters[2].name" should be equal to "itemsPerPage" - And the JSON node "paths./related_dummies/{id}/related_to_dummy_friends.get.parameters[2].in" should be equal to "query" - And the JSON node "paths./related_dummies/{id}/related_to_dummy_friends.get.parameters[2].required" should be false - And the JSON node "paths./related_dummies/{id}/related_to_dummy_friends.get.parameters[2].schema.type" should be equal to "integer" - - And the JSON node "paths./related_dummies/{id}/related_to_dummy_friends.get.parameters[3].name" should be equal to "pagination" - And the JSON node "paths./related_dummies/{id}/related_to_dummy_friends.get.parameters[3].in" should be equal to "query" - And the JSON node "paths./related_dummies/{id}/related_to_dummy_friends.get.parameters[3].required" should be false - And the JSON node "paths./related_dummies/{id}/related_to_dummy_friends.get.parameters[3].schema.type" should be equal to "boolean" - - And the JSON node "paths./related_dummies/{id}/related_to_dummy_friends.get.parameters[5].name" should be equal to "name" - And the JSON node "paths./related_dummies/{id}/related_to_dummy_friends.get.parameters[5].in" should be equal to "query" - And the JSON node "paths./related_dummies/{id}/related_to_dummy_friends.get.parameters[5].required" should be false - And the JSON node "paths./related_dummies/{id}/related_to_dummy_friends.get.parameters[5].schema.type" should be equal to "string" - - And the JSON node "paths./related_dummies/{id}/related_to_dummy_friends.get.parameters[6].name" should be equal to "description" - And the JSON node "paths./related_dummies/{id}/related_to_dummy_friends.get.parameters[6].in" should be equal to "query" - And the JSON node "paths./related_dummies/{id}/related_to_dummy_friends.get.parameters[6].required" should be false - - And the JSON node "paths./related_dummies/{id}/related_to_dummy_friends.get.parameters" should have 7 elements - - # Subcollection - check schema - And the JSON node "paths./related_dummies/{id}/related_to_dummy_friends.get.responses.200.content.application/ld+json.schema.allOf[1].properties.hydra:member.items.$ref" should be equal to "#/components/schemas/RelatedToDummyFriend.jsonld-fakemanytomany" - - # Deprecations - And the JSON node "paths./deprecated_resources.get.deprecated" should be true - And the JSON node "paths./deprecated_resources.post.deprecated" should be true - And the JSON node "paths./deprecated_resources/{id}.get.deprecated" should be true - And the JSON node "paths./deprecated_resources/{id}.delete.deprecated" should be true - And the JSON node "paths./deprecated_resources/{id}.put.deprecated" should be true - And the JSON node "paths./deprecated_resources/{id}.patch.deprecated" should be true - - # Formats - And the OpenAPI class "Dummy.jsonld" exists - And the JSON node "paths./override_open_api_responses.post.responses" should be equal to: - """ - { - "204": { - "description": "User activated" - } - } - """ - - Scenario: OpenAPI UI is enabled for docs endpoint - Given I add "Accept" header equal to "text/html" - And I send a "GET" request to "/docs" - Then the response status code should be 200 - And I should see text matching "My Dummy API" - And I should see text matching "openapi" - - Scenario: OpenAPI extension properties is enabled in JSON docs - Given I send a "GET" request to "/docs.json" - Then the response status code should be 200 - And the response should be in JSON - And the header "Content-Type" should be equal to "application/json; charset=utf-8" - And the JSON node "paths./dummy_addresses.get.x-visibility" should be equal to "hide" - - Scenario: OpenAPI UI is enabled for an arbitrary endpoint - Given I add "Accept" header equal to "text/html" - And I send a "GET" request to "/dummies" - Then the response status code should be 200 - And I should see text matching "openapi" - - @!mongodb - Scenario: Retrieve the OpenAPI documentation with API Gateway compatibility - Given I send a "GET" request to "/docs.json?api_gateway=true" - Then the response status code should be 200 - And the response should be in JSON - And the header "Content-Type" should be equal to "application/json; charset=utf-8" - And the JSON node "basePath" should be equal to "/" - And the JSON node "components.schemas.RamseyUuidDummy.properties.id.description" should be equal to "The dummy id." - And the JSON node "components.schemas.RelatedDummy-barcelona" should not exist - And the JSON node "components.schemas.RelatedDummybarcelona" should exist - - @!mongodb - Scenario: Retrieve the OpenAPI documentation to see if shortName property is used - Given I send a "GET" request to "/docs.json" - Then the response status code should be 200 - And the response should be in JSON - And the header "Content-Type" should be equal to "application/json; charset=utf-8" - And the OpenAPI class "Resource" exists - And the OpenAPI class "ResourceRelated" exists - And the "resourceRelated" property for the OpenAPI class "Resource" should be equal to: - """ - { - "readOnly": true, - "anyOf": [ - { - "$ref": "#/components/schemas/ResourceRelated" - }, - { - "type": "null" - } - ] - } - """ - - Scenario: Retrieve the JSON OpenAPI documentation - Given I add "Accept" header equal to "application/vnd.openapi+json" - And I send a "GET" request to "/docs" - Then the response status code should be 200 - And the response should be in JSON - And the header "Content-Type" should be equal to "application/vnd.openapi+json; charset=utf-8" - # Context - And the JSON node "openapi" should be equal to "3.1.0" - # Root properties - And the JSON node "info.title" should be equal to "My Dummy API" - And the JSON node "info.description" should contain "This is a test API." - And the JSON node "info.description" should contain "Made with love" - # Security Schemes - And the JSON node "components.securitySchemes" should be equal to: - """ - { - "oauth": { - "type": "oauth2", - "description": "OAuth 2.0 implicit Grant", - "flows": { - "implicit": { - "authorizationUrl": "http://my-custom-server/openid-connect/auth", - "scopes": {} - } - } - }, - "Some_Authorization_Name": { - "type": "apiKey", - "description": "Value for the Authorization header parameter.", - "name": "Authorization", - "in": "header" - } - } - """ - - Scenario: Retrieve the YAML OpenAPI documentation - Given I add "Accept" header equal to "application/vnd.openapi+yaml" - And I send a "GET" request to "/docs" - Then the response status code should be 200 - And the header "Content-Type" should be equal to "application/vnd.openapi+yaml; charset=utf-8" - - Scenario: Retrieve the OpenAPI documentation - Given I add "Accept" header equal to "text/html" - And I send a "GET" request to "/" - Then the response status code should be 200 - And the header "Content-Type" should be equal to "text/html; charset=utf-8" - - @!mongodb - Scenario: Retrieve the OpenAPI documentation for Entity Dto Wrappers - Given I send a "GET" request to "/docs.json" - Then the response status code should be 200 - And the response should be in JSON - And the header "Content-Type" should be equal to "application/json; charset=utf-8" - And the OpenAPI class "WrappedResponseEntity-read" exists - And the "id" property exists for the OpenAPI class "WrappedResponseEntity-read" - And the "id" property for the OpenAPI class "WrappedResponseEntity-read" should be equal to: - """ - { - "type": "string" - } - """ - And the OpenAPI class "WrappedResponseEntity.CustomOutputEntityWrapperDto-read" exists - And the "data" property exists for the OpenAPI class "WrappedResponseEntity.CustomOutputEntityWrapperDto-read" - And the "data" property for the OpenAPI class "WrappedResponseEntity.CustomOutputEntityWrapperDto-read" should be equal to: - """ - { - "$ref": "#\/components\/schemas\/WrappedResponseEntity-read" - } - """ - - Scenario: Retrieve the OpenAPI documentation with 3.0 specification - Given I send a "GET" request to "/docs.jsonopenapi?spec_version=3.0.0" - Then the response status code should be 200 - And the response should be in JSON - And the JSON node "openapi" should be equal to "3.0.0" - And the JSON node "components.schemas.DummyBoolean.properties.id.anyOf" should be equal to: - """ - [ - { - "type": "integer" - }, - { - "type": "null" - } - ] - """ - And the JSON node "components.schemas.DummyBoolean.properties.isDummyBoolean.anyOf" should be equal to: - """ - [ - { - "type": "boolean" - }, - { - "type": "null" - } - ] - """ - And the JSON node "components.schemas.DummyBoolean.properties.isDummyBoolean.owl:maxCardinality" should not exist - - Scenario: Retrieve the OpenAPI documentation in JSON - Given I add "Accept" header equal to "text/html,*/*;q=0.8" - And I send a "GET" request to "/docs.jsonopenapi" - Then the response status code should be 200 - And the response should be in JSON - - Scenario: OpenAPI UI is enabled for docs endpoint - Given there are 1 dummy objects - Given I add "Accept" header equal to "text/html" - And I send a "GET" request to "/dummies/1.html" - Then the response status code should be 200 diff --git a/features/openapi/entrypoint.feature b/features/openapi/entrypoint.feature deleted file mode 100644 index da1e1ae46ae..00000000000 --- a/features/openapi/entrypoint.feature +++ /dev/null @@ -1,19 +0,0 @@ -Feature: Entrypoint support - In order to build an auto-discoverable API - As a client software developer - I need to access to an entrypoint listing top-level resources - - Scenario: Retrieve the Entrypoint - When I add "Accept" header equal to "application/vnd.openapi+json" - When I send a "GET" request to "/" - Then the response status code should be 200 - And the response should be in JSON - And the header "Content-Type" should be equal to "application/vnd.openapi+json; charset=utf-8" - And the JSON should be sorted - - Scenario: Retrieve the Entrypoint with url format - When I send a "GET" request to "/index.jsonopenapi" - Then the response status code should be 200 - And the response should be in JSON - And the header "Content-Type" should be equal to "application/vnd.openapi+json; charset=utf-8" - And the JSON should be sorted diff --git a/tests/Behat/OpenApiContext.php b/tests/Behat/OpenApiContext.php deleted file mode 100644 index ed196f4092c..00000000000 --- a/tests/Behat/OpenApiContext.php +++ /dev/null @@ -1,195 +0,0 @@ - - * - * 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\Behat; - -use Behat\Behat\Context\Context; -use Behat\Behat\Context\Environment\InitializedContextEnvironment; -use Behat\Behat\Hook\Scope\BeforeScenarioScope; -use Behat\Gherkin\Node\PyStringNode; -use Behatch\Context\RestContext; -use Behatch\Json\Json; -use PHPUnit\Framework\Assert; -use PHPUnit\Framework\ExpectationFailedException; - -final class OpenApiContext implements Context -{ - private ?RestContext $restContext = null; - - /** - * Gives access to the Behatch context. - * - * @BeforeScenario - */ - public function gatherContexts(BeforeScenarioScope $scope): void - { - /** - * @var InitializedContextEnvironment $environment - */ - $environment = $scope->getEnvironment(); - /** - * @var RestContext $restContext - */ - $restContext = $environment->getContext(RestContext::class); - $this->restContext = $restContext; - } - - /** - * @Then the OpenAPI class :class exists - */ - public function assertTheOpenApiClassExist(string $className): void - { - try { - $this->getClassInfo($className); - } catch (\InvalidArgumentException $e) { - throw new ExpectationFailedException(\sprintf('The class "%s" doesn\'t exist.', $className), null, $e); - } - } - - /** - * @Then the OpenAPI class :class doesn't exist - */ - public function assertTheOpenAPIClassNotExist(string $className): void - { - try { - $this->getClassInfo($className); - } catch (\InvalidArgumentException) { - return; - } - - throw new ExpectationFailedException(\sprintf('The class "%s" exists.', $className)); - } - - /** - * @Then the OpenAPI path :arg1 exists - */ - public function assertThePathExist(string $path): void - { - $json = $this->getLastJsonResponse(); - - Assert::assertTrue(isset($json->paths) && isset($json->paths->{$path})); - } - - /** - * @Then the :prop property exists for the OpenAPI class :class - */ - public function assertThePropertyExistForTheOpenApiClass(string $propertyName, string $className): void - { - try { - $this->getPropertyInfo($propertyName, $className); - } catch (\InvalidArgumentException $e) { - throw new ExpectationFailedException(\sprintf('Property "%s" of class "%s" doesn\'t exist.', $propertyName, $className), null, $e); - } - } - - /** - * @Then the :prop property is required for the OpenAPI class :class - */ - public function assertThePropertyIsRequiredForTheOpenAPIClass(string $propertyName, string $className): void - { - $ok = false; - $schema = $this->getClassInfo($className); - if (isset($schema->allOf)) { - foreach ($schema->allOf as $schema) { - if (isset($schema->required) && \in_array($propertyName, $schema->required, true)) { - $ok = true; - break; - } - } - } - - if (!$ok) { - throw new ExpectationFailedException(\sprintf('Property "%s" of class "%s" should be required', $propertyName, $className)); - } - } - - /** - * @Then the :prop property is not read only for the OpenAPI class :class - */ - public function assertThePropertyIsNotReadOnlyForTheOpenAPIClass(string $propertyName, string $className): void - { - $propertyInfo = $this->getPropertyInfo($propertyName, $className); - if (property_exists($propertyInfo, 'readOnly') && $propertyInfo->readOnly) { - throw new ExpectationFailedException(\sprintf('Property "%s" of class "%s" should not be read only', $propertyName, $className)); - } - } - - /** - * @Then the :prop property for the OpenAPI class :class should be equal to: - */ - public function assertThePropertyForTheOpenAPIClassShouldBeEqualTo(string $propertyName, string $className, PyStringNode $propertyContent): void - { - $propertyInfo = $this->getPropertyInfo($propertyName, $className); - $propertyInfoJson = new Json(json_encode($propertyInfo)); - - if (new Json($propertyContent) != $propertyInfoJson) { - throw new ExpectationFailedException(\sprintf("Property \"%s\" of class \"%s\" is '%s'", $propertyName, $className, $propertyInfoJson)); - } - } - - /** - * Gets information about a property. - * - * @throws \InvalidArgumentException - */ - private function getPropertyInfo(string $propertyName, string $className): \stdClass - { - $properties = (array) $this->getProperties($className); - foreach ($properties as $classPropertyName => $property) { - if ($classPropertyName === $propertyName) { - return $property; - } - } - - throw new \InvalidArgumentException(\sprintf('The property "%s" for the class "%s" doesn\'t exist.', $propertyName, $className)); - } - - /** - * Gets all operations of a given class. - */ - private function getProperties(string $className): \stdClass - { - return $this->getClassInfo($className)->{'properties'} ?? new \stdClass(); - } - - /** - * Gets information about a class. - * - * @throws \InvalidArgumentException - */ - private function getClassInfo(string $className): \stdClass - { - $nodes = $this->getLastJsonResponse()->{'components'}->{'schemas'}; - foreach ($nodes as $classTitle => $classData) { - if ($classTitle === $className) { - return $classData; - } - } - - throw new \InvalidArgumentException(\sprintf('Class %s cannot be found in the vocabulary', $className)); - } - - /** - * Gets the last JSON response. - * - * @throws \RuntimeException - */ - private function getLastJsonResponse(): \stdClass - { - if (null === ($decoded = json_decode($this->restContext->getMink()->getSession()->getDriver()->getContent(), null, 512, \JSON_THROW_ON_ERROR))) { - throw new \RuntimeException('JSON response seems to be invalid'); - } - - return $decoded; - } -} diff --git a/tests/Functional/OpenApiTest.php b/tests/Functional/OpenApiTest.php index ca75eefb018..6912d7154d9 100644 --- a/tests/Functional/OpenApiTest.php +++ b/tests/Functional/OpenApiTest.php @@ -16,10 +16,46 @@ use ApiPlatform\Symfony\Bundle\Test\ApiTestCase; use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\Crud; use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\CrudOpenApiApiPlatformTag; +use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\DummyWebhook; +use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\Issue6151\OverrideOpenApiResponses; +use ApiPlatform\Tests\Fixtures\TestBundle\Entity\AbstractDummy; +use ApiPlatform\Tests\Fixtures\TestBundle\Entity\CircularReference; +use ApiPlatform\Tests\Fixtures\TestBundle\Entity\CompositeItem; +use ApiPlatform\Tests\Fixtures\TestBundle\Entity\CompositeLabel; +use ApiPlatform\Tests\Fixtures\TestBundle\Entity\ConcreteDummy; +use ApiPlatform\Tests\Fixtures\TestBundle\Entity\CustomIdentifierDummy; +use ApiPlatform\Tests\Fixtures\TestBundle\Entity\CustomNormalizedDummy; +use ApiPlatform\Tests\Fixtures\TestBundle\Entity\CustomWritableIdentifierDummy; +use ApiPlatform\Tests\Fixtures\TestBundle\Entity\DeprecatedResource; +use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Dummy; +use ApiPlatform\Tests\Fixtures\TestBundle\Entity\DummyBoolean; +use ApiPlatform\Tests\Fixtures\TestBundle\Entity\DummyCar; +use ApiPlatform\Tests\Fixtures\TestBundle\Entity\DummyFriend; +use ApiPlatform\Tests\Fixtures\TestBundle\Entity\DummyTableInheritance; +use ApiPlatform\Tests\Fixtures\TestBundle\Entity\DummyTableInheritanceChild; +use ApiPlatform\Tests\Fixtures\TestBundle\Entity\JsonSchemaResource; +use ApiPlatform\Tests\Fixtures\TestBundle\Entity\JsonSchemaResourceRelated; +use ApiPlatform\Tests\Fixtures\TestBundle\Entity\NoCollectionDummy; +use ApiPlatform\Tests\Fixtures\TestBundle\Entity\OverriddenOperationDummy; +use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Person; +use ApiPlatform\Tests\Fixtures\TestBundle\Entity\RamseyUuidDummy; +use ApiPlatform\Tests\Fixtures\TestBundle\Entity\RelatedDummy; +use ApiPlatform\Tests\Fixtures\TestBundle\Entity\RelatedOwnedDummy; +use ApiPlatform\Tests\Fixtures\TestBundle\Entity\RelatedOwningDummy; +use ApiPlatform\Tests\Fixtures\TestBundle\Entity\RelatedToDummyFriend; +use ApiPlatform\Tests\Fixtures\TestBundle\Entity\RelationEmbedder; +use ApiPlatform\Tests\Fixtures\TestBundle\Entity\ThirdLevel; +use ApiPlatform\Tests\Fixtures\TestBundle\Entity\User; +use ApiPlatform\Tests\Fixtures\TestBundle\Entity\UuidIdentifierDummy; +use ApiPlatform\Tests\Fixtures\TestBundle\Entity\VideoGame; +use ApiPlatform\Tests\Fixtures\TestBundle\Entity\WrappedResponseEntity; +use ApiPlatform\Tests\Fixtures\TestBundle\Model\DummyAddress; +use ApiPlatform\Tests\RecreateSchemaTrait; use ApiPlatform\Tests\SetupClassResourcesTrait; class OpenApiTest extends ApiTestCase { + use RecreateSchemaTrait; use SetupClassResourcesTrait; protected static ?bool $alwaysBootKernel = false; @@ -29,7 +65,42 @@ class OpenApiTest extends ApiTestCase */ public static function getResources(): array { - return [Crud::class, CrudOpenApiApiPlatformTag::class]; + return [ + Crud::class, + CrudOpenApiApiPlatformTag::class, + AbstractDummy::class, + CircularReference::class, + CompositeItem::class, + CompositeLabel::class, + ConcreteDummy::class, + CustomIdentifierDummy::class, + CustomNormalizedDummy::class, + CustomWritableIdentifierDummy::class, + Dummy::class, + DummyBoolean::class, + RelatedDummy::class, + DummyTableInheritance::class, + DummyTableInheritanceChild::class, + OverriddenOperationDummy::class, + Person::class, + NoCollectionDummy::class, + RelatedToDummyFriend::class, + DummyFriend::class, + RelationEmbedder::class, + User::class, + UuidIdentifierDummy::class, + ThirdLevel::class, + DummyCar::class, + DummyWebhook::class, + VideoGame::class, + DeprecatedResource::class, + OverrideOpenApiResponses::class, + DummyAddress::class, + RamseyUuidDummy::class, + JsonSchemaResource::class, + JsonSchemaResourceRelated::class, + WrappedResponseEntity::class, + ]; } public function testErrorsAreDocumented(): void @@ -173,11 +244,370 @@ public function testHasSchemasForMultipleFormats(): void [ 'type' => 'object', 'properties' => [ - 'id' => [ - 'type' => 'string', - ], + 'id' => ['type' => 'string'], ], ], ], 'description' => 'A resource used for OpenAPI tests.'], $res['components']['schemas']['Crud.jsonld']); } + + public function testRetrieveTheOpenApiDocumentation(): void + { + $response = self::createClient()->request('GET', '/docs', ['headers' => ['accept' => 'application/vnd.openapi+json']]); + $this->assertResponseIsSuccessful(); + $this->assertResponseHeaderSame('content-type', 'application/vnd.openapi+json; charset=utf-8'); + $json = $response->toArray(); + + // Context + $this->assertSame('3.1.0', $json['openapi']); + // Root properties + $this->assertSame('My Dummy API', $json['info']['title']); + $this->assertStringContainsString('This is a test API.', $json['info']['description']); + $this->assertStringContainsString('Made with love', $json['info']['description']); + // Security Schemes + $this->assertEquals([ + 'oauth' => [ + 'type' => 'oauth2', + 'description' => 'OAuth 2.0 implicit Grant', + 'flows' => [ + 'implicit' => [ + 'authorizationUrl' => 'http://my-custom-server/openid-connect/auth', + 'scopes' => [], + ], + ], + ], + 'Some_Authorization_Name' => [ + 'type' => 'apiKey', + 'description' => 'Value for the Authorization header parameter.', + 'name' => 'Authorization', + 'in' => 'header', + ], + ], $json['components']['securitySchemes']); + + // Supported classes + $this->assertArrayHasKey('AbstractDummy', $json['components']['schemas']); + $this->assertArrayHasKey('CircularReference', $json['components']['schemas']); + $this->assertArrayHasKey('CircularReference-circular', $json['components']['schemas']); + $this->assertArrayHasKey('CompositeItem', $json['components']['schemas']); + $this->assertArrayHasKey('CompositeLabel', $json['components']['schemas']); + $this->assertArrayHasKey('ConcreteDummy', $json['components']['schemas']); + $this->assertArrayHasKey('CustomIdentifierDummy', $json['components']['schemas']); + $this->assertArrayHasKey('CustomNormalizedDummy-input', $json['components']['schemas']); + $this->assertArrayHasKey('CustomNormalizedDummy-output', $json['components']['schemas']); + $this->assertArrayHasKey('CustomWritableIdentifierDummy', $json['components']['schemas']); + $this->assertArrayHasKey('Dummy', $json['components']['schemas']); + $this->assertArrayHasKey('DummyBoolean', $json['components']['schemas']); + $this->assertArrayHasKey('RelatedDummy', $json['components']['schemas']); + $this->assertArrayHasKey('DummyTableInheritance', $json['components']['schemas']); + $this->assertArrayHasKey('DummyTableInheritanceChild', $json['components']['schemas']); + $this->assertArrayHasKey('OverriddenOperationDummy-overridden_operation_dummy_get', $json['components']['schemas']); + $this->assertArrayHasKey('OverriddenOperationDummy-overridden_operation_dummy_put', $json['components']['schemas']); + $this->assertArrayHasKey('OverriddenOperationDummy-overridden_operation_dummy_read', $json['components']['schemas']); + $this->assertArrayHasKey('OverriddenOperationDummy-overridden_operation_dummy_write', $json['components']['schemas']); + $this->assertArrayHasKey('Person', $json['components']['schemas']); + $this->assertArrayHasKey('NoCollectionDummy', $json['components']['schemas']); + $this->assertArrayHasKey('RelatedToDummyFriend', $json['components']['schemas']); + $this->assertArrayHasKey('RelatedToDummyFriend-fakemanytomany', $json['components']['schemas']); + $this->assertArrayHasKey('DummyFriend', $json['components']['schemas']); + $this->assertArrayHasKey('RelationEmbedder-barcelona', $json['components']['schemas']); + $this->assertArrayHasKey('RelationEmbedder-chicago', $json['components']['schemas']); + $this->assertArrayHasKey('User-user_user-read', $json['components']['schemas']); + $this->assertArrayHasKey('User-user_user-write', $json['components']['schemas']); + $this->assertArrayHasKey('UuidIdentifierDummy', $json['components']['schemas']); + $this->assertArrayHasKey('ThirdLevel', $json['components']['schemas']); + $this->assertArrayHasKey('DummyCar', $json['components']['schemas']); + $this->assertArrayHasKey('DummyWebhook', $json['components']['schemas']); + $this->assertArrayNotHasKey('ParentDummy', $json['components']['schemas']); + $this->assertArrayNotHasKey('UnknownDummy', $json['components']['schemas']); + + $this->assertArrayHasKey('/relation_embedders/{id}/custom', $json['paths']); + $this->assertArrayHasKey('/override/swagger', $json['paths']); + $this->assertArrayHasKey('/api/custom-call/{id}', $json['paths']); + $this->assertArrayHasKey('get', $json['paths']['/api/custom-call/{id}']); + $this->assertArrayHasKey('put', $json['paths']['/api/custom-call/{id}']); + + $this->assertArrayHasKey('id', $json['components']['schemas']['Dummy']['properties']); + $this->assertSame(['name'], $json['components']['schemas']['Dummy']['required']); + $this->assertArrayHasKey('genderType', $json['components']['schemas']['Person']['properties']); + $this->assertEquals([ + 'default' => 'male', + 'type' => ['string', 'null'], + 'enum' => [ + 'male', + 'female', + null, + ], + ], $json['components']['schemas']['Person']['properties']['genderType']); + $this->assertArrayHasKey('playMode', $json['components']['schemas']['VideoGame']['properties']); + $this->assertEquals([ + 'default' => 'SinglePlayer', + 'enum' => ['CoOp', 'MultiPlayer', 'SinglePlayer'], + 'type' => 'string', + ], $json['components']['schemas']['VideoGame']['properties']['playMode']); + + // Filters + $this->assertSame('dummyBoolean', $json['paths']['/dummies']['get']['parameters'][4]['name']); + $this->assertSame('query', $json['paths']['/dummies']['get']['parameters'][4]['in']); + $this->assertFalse($json['paths']['/dummies']['get']['parameters'][4]['required']); + $this->assertSame('boolean', $json['paths']['/dummies']['get']['parameters'][4]['schema']['type']); + + $this->assertSame('foobar[]', $json['paths']['/dummy_cars']['get']['parameters'][9]['name']); + $this->assertSame('Allows you to reduce the response to contain only the properties you need. If your desired property is nested, you can address it using nested arrays. Example: foobar[]={propertyName}&foobar[]={anotherPropertyName}&foobar[{nestedPropertyParent}][]={nestedProperty}', $json['paths']['/dummy_cars']['get']['parameters'][9]['description']); + + // Webhook + $this->assertSame('Something else here for example', $json['webhooks']['a']['get']['description']); + $this->assertSame('Hi! it\'s me, I\'m the problem, it\'s me', $json['webhooks']['b']['post']['description']); + + // Subcollection - check filter on subResource + $this->assertSame('id', $json['paths']['/related_dummies/{id}/related_to_dummy_friends']['get']['parameters'][0]['name']); + $this->assertSame('path', $json['paths']['/related_dummies/{id}/related_to_dummy_friends']['get']['parameters'][0]['in']); + $this->assertTrue($json['paths']['/related_dummies/{id}/related_to_dummy_friends']['get']['parameters'][0]['required']); + $this->assertSame('string', $json['paths']['/related_dummies/{id}/related_to_dummy_friends']['get']['parameters'][0]['schema']['type']); + + $this->assertSame('page', $json['paths']['/related_dummies/{id}/related_to_dummy_friends']['get']['parameters'][1]['name']); + $this->assertSame('query', $json['paths']['/related_dummies/{id}/related_to_dummy_friends']['get']['parameters'][1]['in']); + $this->assertFalse($json['paths']['/related_dummies/{id}/related_to_dummy_friends']['get']['parameters'][1]['required']); + $this->assertSame('integer', $json['paths']['/related_dummies/{id}/related_to_dummy_friends']['get']['parameters'][1]['schema']['type']); + + $this->assertSame('itemsPerPage', $json['paths']['/related_dummies/{id}/related_to_dummy_friends']['get']['parameters'][2]['name']); + $this->assertSame('query', $json['paths']['/related_dummies/{id}/related_to_dummy_friends']['get']['parameters'][2]['in']); + $this->assertFalse($json['paths']['/related_dummies/{id}/related_to_dummy_friends']['get']['parameters'][2]['required']); + $this->assertSame('integer', $json['paths']['/related_dummies/{id}/related_to_dummy_friends']['get']['parameters'][2]['schema']['type']); + + $this->assertSame('pagination', $json['paths']['/related_dummies/{id}/related_to_dummy_friends']['get']['parameters'][3]['name']); + $this->assertSame('query', $json['paths']['/related_dummies/{id}/related_to_dummy_friends']['get']['parameters'][3]['in']); + $this->assertFalse($json['paths']['/related_dummies/{id}/related_to_dummy_friends']['get']['parameters'][3]['required']); + $this->assertSame('boolean', $json['paths']['/related_dummies/{id}/related_to_dummy_friends']['get']['parameters'][3]['schema']['type']); + + $this->assertSame('name', $json['paths']['/related_dummies/{id}/related_to_dummy_friends']['get']['parameters'][5]['name']); + $this->assertSame('query', $json['paths']['/related_dummies/{id}/related_to_dummy_friends']['get']['parameters'][5]['in']); + $this->assertFalse($json['paths']['/related_dummies/{id}/related_to_dummy_friends']['get']['parameters'][5]['required']); + $this->assertSame('string', $json['paths']['/related_dummies/{id}/related_to_dummy_friends']['get']['parameters'][5]['schema']['type']); + + $this->assertSame('description', $json['paths']['/related_dummies/{id}/related_to_dummy_friends']['get']['parameters'][6]['name']); + $this->assertSame('query', $json['paths']['/related_dummies/{id}/related_to_dummy_friends']['get']['parameters'][6]['in']); + $this->assertFalse($json['paths']['/related_dummies/{id}/related_to_dummy_friends']['get']['parameters'][6]['required']); + + $this->assertCount(7, $json['paths']['/related_dummies/{id}/related_to_dummy_friends']['get']['parameters']); + + // Subcollection - check schema + $this->assertSame('#/components/schemas/RelatedToDummyFriend.jsonld-fakemanytomany', $json['paths']['/related_dummies/{id}/related_to_dummy_friends']['get']['responses']['200']['content']['application/ld+json']['schema']['allOf'][1]['properties']['hydra:member']['items']['$ref']); + + // Deprecations + $this->assertTrue($json['paths']['/deprecated_resources']['get']['deprecated']); + $this->assertTrue($json['paths']['/deprecated_resources']['post']['deprecated']); + $this->assertTrue($json['paths']['/deprecated_resources/{id}']['get']['deprecated']); + $this->assertTrue($json['paths']['/deprecated_resources/{id}']['delete']['deprecated']); + $this->assertTrue($json['paths']['/deprecated_resources/{id}']['put']['deprecated']); + $this->assertTrue($json['paths']['/deprecated_resources/{id}']['patch']['deprecated']); + + // Formats + $this->assertArrayHasKey('Dummy.jsonld', $json['components']['schemas']); + $this->assertEquals([ + '204' => [ + 'description' => 'User activated', + ], + ], $json['paths']['/override_open_api_responses']['post']['responses']); + } + + public function testOpenApiUiIsEnabledForDocsEndpoint(): void + { + $response = self::createClient()->request('GET', '/docs', [ + 'headers' => ['Accept' => 'text/html'], + ]); + $this->assertResponseIsSuccessful(); + $this->assertStringContainsString('My Dummy API', $response->getContent()); + $this->assertStringContainsString('openapi', $response->getContent()); + } + + public function testOpenApiExtensionPropertiesIsEnabledInJsonDocs(): void + { + $response = self::createClient()->request('GET', '/docs', ['headers' => ['accept' => 'application/vnd.openapi+json']]); + $this->assertResponseIsSuccessful(); + $json = $response->toArray(); + + $this->assertSame('hide', $json['paths']['/dummy_addresses']['get']['x-visibility']); + } + + public function testOpenApiUiIsEnabledForAnArbitraryEndpoint(): void + { + $response = self::createClient()->request('GET', '/dummies', [ + 'headers' => ['Accept' => 'text/html'], + ]); + $this->assertResponseIsSuccessful(); + $this->assertStringContainsString('openapi', $response->getContent()); + } + + public function testRetrieveTheOpenApiDocumentationWithApiGatewayCompatibility(): void + { + $kernel = self::bootKernel(); + if ('mongodb' === $kernel->getEnvironment()) { + $this->markTestSkipped('Resource not loaded with MongoDB.'); + } + + $response = self::createClient()->request('GET', '/docs?api_gateway=true', ['headers' => ['accept' => 'application/vnd.openapi+json']]); + $this->assertResponseIsSuccessful(); + $this->assertResponseHeaderSame('content-type', 'application/vnd.openapi+json; charset=utf-8'); + $json = $response->toArray(); + + $this->assertSame('/', $json['basePath']); + $this->assertSame('The dummy id.', $json['components']['schemas']['RamseyUuidDummy']['properties']['id']['description']); + $this->assertArrayNotHasKey('RelatedDummy-barcelona', $json['components']['schemas']); + $this->assertArrayHasKey('RelatedDummybarcelona', $json['components']['schemas']); + } + + public function testRetrieveTheOpenApiDocumentationToSeeIfShortNamePropertyIsUsed(): void + { + $kernel = self::bootKernel(); + if ('mongodb' === $kernel->getEnvironment()) { + $this->markTestSkipped('Resource not loaded with MongoDB.'); + } + + $response = self::createClient()->request('GET', '/docs', ['headers' => ['accept' => 'application/vnd.openapi+json']]); + $this->assertResponseIsSuccessful(); + $json = $response->toArray(); + + $this->assertArrayHasKey('Resource', $json['components']['schemas']); + $this->assertArrayHasKey('ResourceRelated', $json['components']['schemas']); + $this->assertEquals([ + 'readOnly' => true, + 'anyOf' => [ + [ + '$ref' => '#/components/schemas/ResourceRelated', + ], + [ + 'type' => 'null', + ], + ], + ], $json['components']['schemas']['Resource']['properties']['resourceRelated']); + } + + public function testRetrieveTheJsonOpenApiDocumentation(): void + { + $response = self::createClient()->request('GET', '/docs', [ + 'headers' => ['Accept' => 'application/vnd.openapi+json'], + ]); + $this->assertResponseIsSuccessful(); + $this->assertResponseHeaderSame('content-type', 'application/vnd.openapi+json; charset=utf-8'); + $json = $response->toArray(); + + // Context + $this->assertSame('3.1.0', $json['openapi']); + // Root properties + $this->assertSame('My Dummy API', $json['info']['title']); + $this->assertStringContainsString('This is a test API.', $json['info']['description']); + $this->assertStringContainsString('Made with love', $json['info']['description']); + // Security Schemes + $this->assertEquals([ + 'oauth' => [ + 'type' => 'oauth2', + 'description' => 'OAuth 2.0 implicit Grant', + 'flows' => [ + 'implicit' => [ + 'authorizationUrl' => 'http://my-custom-server/openid-connect/auth', + 'scopes' => [], + ], + ], + ], + 'Some_Authorization_Name' => [ + 'type' => 'apiKey', + 'description' => 'Value for the Authorization header parameter.', + 'name' => 'Authorization', + 'in' => 'header', + ], + ], $json['components']['securitySchemes']); + } + + public function testRetrieveTheYamlOpenApiDocumentation(): void + { + $response = self::createClient()->request('GET', '/docs', [ + 'headers' => ['Accept' => 'application/vnd.openapi+yaml'], + ]); + $this->assertResponseIsSuccessful(); + $this->assertResponseHeaderSame('content-type', 'application/vnd.openapi+yaml; charset=utf-8'); + } + + public function testRetrieveTheOpenApiDocumentationHtml(): void + { + $response = self::createClient()->request('GET', '/', [ + 'headers' => ['Accept' => 'text/html'], + ]); + $this->assertResponseIsSuccessful(); + $this->assertResponseHeaderSame('content-type', 'text/html; charset=UTF-8'); + } + + public function testRetrieveTheOpenApiDocumentationForEntityDtoWrappers(): void + { + $kernel = self::bootKernel(); + if ('mongodb' === $kernel->getEnvironment()) { + $this->markTestSkipped('Resource not loaded with MongoDB.'); + } + + $response = self::createClient()->request('GET', '/docs', ['headers' => ['Accept' => 'application/vnd.openapi+json']]); + $this->assertResponseIsSuccessful(); + $json = $response->toArray(); + + $this->assertArrayHasKey('WrappedResponseEntity-read', $json['components']['schemas']); + $this->assertArrayHasKey('id', $json['components']['schemas']['WrappedResponseEntity-read']['properties']); + $this->assertEquals(['type' => 'string'], $json['components']['schemas']['WrappedResponseEntity-read']['properties']['id']); + $this->assertArrayHasKey('WrappedResponseEntity.CustomOutputEntityWrapperDto-read', $json['components']['schemas']); + $this->assertArrayHasKey('data', $json['components']['schemas']['WrappedResponseEntity.CustomOutputEntityWrapperDto-read']['properties']); + $this->assertEquals(['$ref' => '#/components/schemas/WrappedResponseEntity-read'], $json['components']['schemas']['WrappedResponseEntity.CustomOutputEntityWrapperDto-read']['properties']['data']); + } + + public function testRetrieveTheOpenApiDocumentationWith30Specification(): void + { + $response = self::createClient()->request('GET', '/docs.jsonopenapi?spec_version=3.0.0', ['headers' => ['Accept' => 'application/vnd.openapi+json']]); + $this->assertResponseIsSuccessful(); + $json = $response->toArray(); + + $this->assertSame('3.0.0', $json['openapi']); + $this->assertEquals([ + ['type' => 'integer'], + ['type' => 'null'], + ], $json['components']['schemas']['DummyBoolean']['properties']['id']['anyOf']); + $this->assertEquals([ + ['type' => 'boolean'], + ['type' => 'null'], + ], $json['components']['schemas']['DummyBoolean']['properties']['isDummyBoolean']['anyOf']); + $this->assertArrayNotHasKey('owl:maxCardinality', $json['components']['schemas']['DummyBoolean']['properties']['isDummyBoolean']); + } + + public function testRetrieveTheOpenApiDocumentationInJson(): void + { + $response = self::createClient()->request('GET', '/docs.jsonopenapi', [ + 'headers' => ['Accept' => 'text/html,*/*;q=0.8'], + ]); + $this->assertResponseIsSuccessful(); + $this->assertResponseHeaderSame('content-type', 'application/vnd.openapi+json; charset=utf-8'); + } + + public function testOpenApiUiIsEnabledForDocsEndpointWithDummyObject(): void + { + $this->recreateSchema([Dummy::class, RelatedDummy::class, RelatedOwnedDummy::class, RelatedOwningDummy::class]); + self::createClient()->request('POST', '/dummies', ['json' => ['name' => 'test']]); + $response = self::createClient()->request('GET', '/dummies/1.html', [ + 'headers' => ['Accept' => 'text/html'], + ]); + $this->assertResponseIsSuccessful(); + } + + public function testRetrieveTheEntrypoint(): void + { + $response = self::createClient()->request('GET', '/', [ + 'headers' => ['Accept' => 'application/vnd.openapi+json'], + ]); + $this->assertResponseIsSuccessful(); + $this->assertResponseHeaderSame('content-type', 'application/vnd.openapi+json; charset=utf-8'); + $this->assertJson($response->getContent()); + } + + public function testRetrieveTheEntrypointWithUrlFormat(): void + { + $response = self::createClient()->request('GET', '/index.jsonopenapi', [ + 'headers' => ['Accept' => 'application/vnd.openapi+json'], + ]); + $this->assertResponseIsSuccessful(); + $this->assertResponseHeaderSame('content-type', 'application/vnd.openapi+json; charset=utf-8'); + $this->assertJson($response->getContent()); + } }