From c56b1af0e0c3b3c10f06270a613e13bce588a88e Mon Sep 17 00:00:00 2001 From: soyuka Date: Mon, 27 Jan 2025 08:45:15 +0100 Subject: [PATCH] fix --- src/JsonApi/JsonSchema/SchemaFactory.php | 3 +- src/OpenApi/Factory/OpenApiFactory.php | 29 ++- .../Tests/Factory/OpenApiFactoryTest.php | 246 ++++++++++++------ .../Serializer/OpenApiNormalizerTest.php | 5 + .../Command/JsonSchemaGenerateCommandTest.php | 5 +- 5 files changed, 194 insertions(+), 94 deletions(-) diff --git a/src/JsonApi/JsonSchema/SchemaFactory.php b/src/JsonApi/JsonSchema/SchemaFactory.php index 99aafe2e19c..0410b4cf1c6 100644 --- a/src/JsonApi/JsonSchema/SchemaFactory.php +++ b/src/JsonApi/JsonSchema/SchemaFactory.php @@ -133,8 +133,7 @@ public function buildSchema(string $className, string $format = 'jsonapi', strin } // We don't use the serializer context here as JSON:API doesn't leverage serializer groups for related resources. // That is done by query parameter. @see https://jsonapi.org/format/#fetching-includes - $operation ??= $this->findOperation($className, $type, $operation, $serializerContext, $format); - $serializerContext ??= $this->getSerializerContext($operation, $type); + $serializerContext ??= $this->getSerializerContext($operation ?? $this->findOperation($className, $type, $operation, $serializerContext, $format), $type); $jsonApiSerializerContext = !($serializerContext[self::DISABLE_JSON_SCHEMA_SERIALIZER_GROUPS] ?? true) ? $serializerContext : []; $schema = $this->schemaFactory->buildSchema($className, $format, $type, $operation, $schema, $jsonApiSerializerContext, $forceCollection); diff --git a/src/OpenApi/Factory/OpenApiFactory.php b/src/OpenApi/Factory/OpenApiFactory.php index e95d1b417d8..f5103ebfbe0 100644 --- a/src/OpenApi/Factory/OpenApiFactory.php +++ b/src/OpenApi/Factory/OpenApiFactory.php @@ -338,7 +338,7 @@ private function collectPaths(ApiResource $resource, ResourceMetadataCollection $errorOperations[$error] = $this->getErrorResource($error); } - $openapiOperation = $this->addOperationErrors($openapiOperation, $errorOperations, $resourceMetadataCollection, $schema, $schemas); + $openapiOperation = $this->addOperationErrors($openapiOperation, $errorOperations, $resourceMetadataCollection, $schema, $schemas, $operation); } if ($overrideResponses || !$existingResponses) { @@ -355,7 +355,7 @@ private function collectPaths(ApiResource $resource, ResourceMetadataCollection $openapiOperation = $this->addOperationErrors($openapiOperation, [ ApiResourceError::class => $this->getErrorResource(ApiResourceError::class)->withStatus(400)->withDescription('Invalid input'), ValidationException::class => $this->getErrorResource(ValidationException::class, 422, 'Unprocessable entity'), // we add defaults as ValidationException can not be installed - ], $resourceMetadataCollection, $schema, $schemas); + ], $resourceMetadataCollection, $schema, $schemas, $operation); break; case 'PATCH': case 'PUT': @@ -364,7 +364,7 @@ private function collectPaths(ApiResource $resource, ResourceMetadataCollection $openapiOperation = $this->addOperationErrors($openapiOperation, [ ApiResourceError::class => $this->getErrorResource(ApiResourceError::class)->withStatus(400)->withDescription('Invalid input'), ValidationException::class => $this->getErrorResource(ValidationException::class, 422, 'Unprocessable entity'), // we add defaults as ValidationException can not be installed - ], $resourceMetadataCollection, $schema, $schemas); + ], $resourceMetadataCollection, $schema, $schemas, $operation); break; case 'DELETE': $successStatus = (string) $operation->getStatus() ?: 204; @@ -376,13 +376,13 @@ private function collectPaths(ApiResource $resource, ResourceMetadataCollection if ($overrideResponses && !isset($existingResponses[403]) && $operation->getSecurity()) { $openapiOperation = $this->addOperationErrors($openapiOperation, [ ApiResourceError::class => $this->getErrorResource(ApiResourceError::class)->withStatus(403)->withDescription('Forbidden'), - ], $resourceMetadataCollection, $schema, $schemas); + ], $resourceMetadataCollection, $schema, $schemas, $operation); } if ($overrideResponses && !$operation instanceof CollectionOperationInterface && 'POST' !== $operation->getMethod() && !isset($existingResponses[404])) { $openapiOperation = $this->addOperationErrors($openapiOperation, [ ApiResourceError::class => $this->getErrorResource(ApiResourceError::class)->withStatus(404)->withDescription('Not found'), - ], $resourceMetadataCollection, $schema, $schemas); + ], $resourceMetadataCollection, $schema, $schemas, $operation); } if (!$openapiOperation->getResponses()) { @@ -576,7 +576,7 @@ private function getLinks(ResourceMetadataCollection $resourceMetadataCollection } if ($operationUriVariables[$parameterName]->getIdentifiers() === $uriVariableDefinition->getIdentifiers() && $operationUriVariables[$parameterName]->getFromClass() === $uriVariableDefinition->getFromClass()) { - $parameters[$parameterName] = '$request.path.'.$uriVariableDefinition->getIdentifiers()[0]; + $parameters[$parameterName] = '$request.path.'.($uriVariableDefinition->getIdentifiers()[0] ?? 'id'); } } @@ -586,7 +586,7 @@ private function getLinks(ResourceMetadataCollection $resourceMetadataCollection } if ($uriVariableDefinition->getFromClass() === $currentOperation->getClass()) { - $parameters[$parameterName] = '$response.body#/'.$uriVariableDefinition->getIdentifiers()[0]; + $parameters[$parameterName] = '$response.body#/'.($uriVariableDefinition->getIdentifiers()[0] ?? 'id'); } } @@ -857,16 +857,17 @@ private function addOperationErrors( ResourceMetadataCollection $resourceMetadataCollection, Schema $schema, \ArrayObject $schemas, + HttpOperation $originalOperation, ): Operation { + $defaultFormat = ['json' => ['application/problem+json']]; foreach ($errors as $error => $errorResource) { - $responseMimeTypes = $this->flattenMimeTypes($errorResource->getOutputFormats() ?: []); - + $responseMimeTypes = $this->flattenMimeTypes($errorResource->getOutputFormats() ?: $defaultFormat); foreach ($errorResource->getOperations() as $errorOperation) { if (false === $errorOperation->getOpenApi()) { continue; } - $responseMimeTypes += $this->flattenMimeTypes($errorOperation->getOutputFormats() ?: []); + $responseMimeTypes += $this->flattenMimeTypes($errorOperation->getOutputFormats() ?: $defaultFormat); } $operationErrorSchemas = []; @@ -880,7 +881,7 @@ private function addOperationErrors( throw new RuntimeException(\sprintf('The error class %s has no status defined, please either implement ProblemExceptionInterface, or make it an ErrorResource with a status', $error)); } - $operation = $this->buildOpenApiResponse($operation->getResponses() ?: [], $status, $errorResource->getDescription() ?? '', $operation, null, $responseMimeTypes, $operationErrorSchemas, $resourceMetadataCollection); + $operation = $this->buildOpenApiResponse($operation->getResponses() ?: [], $status, $errorResource->getDescription() ?? '', $operation, $originalOperation, $responseMimeTypes, $operationErrorSchemas, $resourceMetadataCollection); } return $operation; @@ -909,7 +910,7 @@ private function getErrorResource(string $error, ?int $status = null, ?string $d try { $errorResource = $this->resourceMetadataFactory->create($error)[0] ?? new ErrorResource(status: $status, description: $description, class: ApiResourceError::class); - if (!is_a($errorResource, ErrorResource::class)) { + if (!($errorResource instanceof ErrorResource)) { throw new RuntimeException(\sprintf('The error class %s is not an ErrorResource', $error)); } @@ -925,6 +926,10 @@ private function getErrorResource(string $error, ?int $status = null, ?string $d $errorResource = new ErrorResource(status: $status, description: $description, class: ApiResourceError::class); } + if (!$errorResource->getClass()) { + $errorResource = $errorResource->withClass($error); + } + return $this->localErrorResourceCache[$error] = $errorResource; } } diff --git a/src/OpenApi/Tests/Factory/OpenApiFactoryTest.php b/src/OpenApi/Tests/Factory/OpenApiFactoryTest.php index 2a7fd031c09..7fd600de488 100644 --- a/src/OpenApi/Tests/Factory/OpenApiFactoryTest.php +++ b/src/OpenApi/Tests/Factory/OpenApiFactoryTest.php @@ -20,6 +20,7 @@ use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\Delete; use ApiPlatform\Metadata\Error as ErrorOperation; +use ApiPlatform\Metadata\ErrorResource; use ApiPlatform\Metadata\Get; use ApiPlatform\Metadata\GetCollection; use ApiPlatform\Metadata\HeaderParameter; @@ -60,8 +61,10 @@ use ApiPlatform\OpenApi\Tests\Fixtures\DummyFilter; use ApiPlatform\OpenApi\Tests\Fixtures\Issue6872\Diamond; use ApiPlatform\OpenApi\Tests\Fixtures\OutputDto; +use ApiPlatform\State\ApiResource\Error; use ApiPlatform\State\Pagination\PaginationOptions; use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\WithParameter; +use ApiPlatform\Validator\Exception\ValidationException; use PHPUnit\Framework\TestCase; use Prophecy\Argument; use Prophecy\PhpUnit\ProphecyTrait; @@ -285,14 +288,17 @@ public function testInvoke(): void $resourceCollectionMetadataFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); $resourceCollectionMetadataFactoryProphecy->create(Dummy::class)->shouldBeCalled()->willReturn(new ResourceMetadataCollection(Dummy::class, [$dummyResource, $dummyResourceWebhook])); - $resourceCollectionMetadataFactoryProphecy->create(DummyErrorResource::class)->shouldBeCalled()->willReturn(new ResourceMetadataCollection(DummyErrorResource::class, [new ApiResource(operations: [new ErrorOperation(name: 'err', description: 'nice one!')])])); + $resourceCollectionMetadataFactoryProphecy->create(DummyErrorResource::class)->shouldBeCalled()->willReturn(new ResourceMetadataCollection(DummyErrorResource::class, [new ErrorResource(operations: [new ErrorOperation(name: 'err', description: 'nice one!')])])); $resourceCollectionMetadataFactoryProphecy->create(WithParameter::class)->shouldBeCalled()->willReturn(new ResourceMetadataCollection(WithParameter::class, [$parameterResource])); $resourceCollectionMetadataFactoryProphecy->create(Diamond::class)->shouldBeCalled()->willReturn(new ResourceMetadataCollection(Diamond::class, [$diamondResource])); + $resourceCollectionMetadataFactoryProphecy->create(Error::class)->shouldBeCalled()->willReturn(new ResourceMetadataCollection(Error::class, [])); + $resourceCollectionMetadataFactoryProphecy->create(ValidationException::class)->shouldBeCalled()->willReturn(new ResourceMetadataCollection(ValidationException::class, [])); $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); $propertyNameCollectionFactoryProphecy->create(Dummy::class, Argument::any())->shouldBeCalled()->willReturn(new PropertyNameCollection(['id', 'name', 'description', 'dummyDate', 'enum'])); $propertyNameCollectionFactoryProphecy->create(DummyErrorResource::class, Argument::any())->shouldBeCalled()->willReturn(new PropertyNameCollection(['type', 'title', 'status', 'detail', 'instance'])); $propertyNameCollectionFactoryProphecy->create(OutputDto::class, Argument::any())->shouldBeCalled()->willReturn(new PropertyNameCollection(['id', 'name', 'description', 'dummyDate', 'enum'])); + $propertyNameCollectionFactoryProphecy->create(Error::class, Argument::any())->shouldBeCalled()->willReturn(new PropertyNameCollection(['type', 'title', 'status', 'detail', 'instance'])); $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); $propertyMetadataFactoryProphecy->create(Dummy::class, 'id', Argument::any())->shouldBeCalled()->willReturn( @@ -407,59 +413,62 @@ public function testInvoke(): void ->withSchema(['type' => 'string', 'description' => 'This is an enum.']) ->withOpenapiContext(['type' => 'string', 'enum' => ['one', 'two'], 'example' => 'one']) ); - $propertyMetadataFactoryProphecy->create(DummyErrorResource::class, 'type', Argument::any())->shouldBeCalled()->willReturn( - (new ApiProperty()) - ->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_STRING)]) - ->withDescription('This is an error type.') - ->withReadable(true) - ->withWritable(false) - ->withReadableLink(true) - ->withWritableLink(true) - ->withInitializable(true) - ->withSchema(['type' => 'string', 'description' => 'This is an error type.']) - ); - $propertyMetadataFactoryProphecy->create(DummyErrorResource::class, 'title', Argument::any())->shouldBeCalled()->willReturn( - (new ApiProperty()) - ->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_STRING)]) - ->withDescription('This is an error title.') - ->withReadable(true) - ->withWritable(false) - ->withReadableLink(true) - ->withWritableLink(true) - ->withInitializable(true) - ->withSchema(['type' => 'string', 'description' => 'This is an error title.']) - ); - $propertyMetadataFactoryProphecy->create(DummyErrorResource::class, 'status', Argument::any())->shouldBeCalled()->willReturn( - (new ApiProperty()) - ->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_INT)]) - ->withDescription('This is an error status.') - ->withReadable(true) - ->withWritable(false) - ->withIdentifier(true) - ->withSchema(['type' => 'integer', 'description' => 'This is an error status.', 'readOnly' => true]) - ); - $propertyMetadataFactoryProphecy->create(DummyErrorResource::class, 'detail', Argument::any())->shouldBeCalled()->willReturn( - (new ApiProperty()) - ->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_STRING)]) - ->withDescription('This is an error detail.') - ->withReadable(true) - ->withWritable(false) - ->withReadableLink(true) - ->withWritableLink(true) - ->withInitializable(true) - ->withSchema(['type' => 'string', 'description' => 'This is an error detail.']) - ); - $propertyMetadataFactoryProphecy->create(DummyErrorResource::class, 'instance', Argument::any())->shouldBeCalled()->willReturn( - (new ApiProperty()) - ->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_STRING)]) - ->withDescription('This is an error instance.') - ->withReadable(true) - ->withWritable(false) - ->withReadableLink(true) - ->withWritableLink(true) - ->withInitializable(true) - ->withSchema(['type' => 'string', 'description' => 'This is an error instance.']) - ); + + foreach ([DummyErrorResource::class, Error::class] as $cl) { + $propertyMetadataFactoryProphecy->create($cl, 'type', Argument::any())->shouldBeCalled()->willReturn( + (new ApiProperty()) + ->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_STRING)]) + ->withDescription('This is an error type.') + ->withReadable(true) + ->withWritable(false) + ->withReadableLink(true) + ->withWritableLink(true) + ->withInitializable(true) + ->withSchema(['type' => 'string', 'description' => 'This is an error type.']) + ); + $propertyMetadataFactoryProphecy->create($cl, 'title', Argument::any())->shouldBeCalled()->willReturn( + (new ApiProperty()) + ->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_STRING)]) + ->withDescription('This is an error title.') + ->withReadable(true) + ->withWritable(false) + ->withReadableLink(true) + ->withWritableLink(true) + ->withInitializable(true) + ->withSchema(['type' => 'string', 'description' => 'This is an error title.']) + ); + $propertyMetadataFactoryProphecy->create($cl, 'status', Argument::any())->shouldBeCalled()->willReturn( + (new ApiProperty()) + ->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_INT)]) + ->withDescription('This is an error status.') + ->withReadable(true) + ->withWritable(false) + ->withIdentifier(true) + ->withSchema(['type' => 'integer', 'description' => 'This is an error status.', 'readOnly' => true]) + ); + $propertyMetadataFactoryProphecy->create($cl, 'detail', Argument::any())->shouldBeCalled()->willReturn( + (new ApiProperty()) + ->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_STRING)]) + ->withDescription('This is an error detail.') + ->withReadable(true) + ->withWritable(false) + ->withReadableLink(true) + ->withWritableLink(true) + ->withInitializable(true) + ->withSchema(['type' => 'string', 'description' => 'This is an error detail.']) + ); + $propertyMetadataFactoryProphecy->create($cl, 'instance', Argument::any())->shouldBeCalled()->willReturn( + (new ApiProperty()) + ->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_STRING)]) + ->withDescription('This is an error instance.') + ->withReadable(true) + ->withWritable(false) + ->withReadableLink(true) + ->withWritableLink(true) + ->withInitializable(true) + ->withSchema(['type' => 'string', 'description' => 'This is an error instance.']) + ); + } $filterLocatorProphecy = $this->prophesize(ContainerInterface::class); $filters = [ @@ -610,6 +619,8 @@ public function testInvoke(): void ]), ], ])); + $errorSchema = clone $dummyErrorSchema->getDefinitions(); + $errorSchema['description'] = ''; $openApi = $factory(['base_url' => '/app_dev.php/']); @@ -634,8 +645,9 @@ public function testInvoke(): void $this->assertEquals($components->getSchemas(), new \ArrayObject([ 'Dummy' => $dummySchema->getDefinitions(), 'Dummy.OutputDto' => $dummySchema->getDefinitions(), - 'DummyErrorResource' => $dummyErrorSchema->getDefinitions(), 'Parameter' => $parameterSchema, + 'DummyErrorResource' => $dummyErrorSchema->getDefinitions(), + 'Error' => $errorSchema, ])); $this->assertEquals($components->getSecuritySchemes(), new \ArrayObject([ @@ -703,8 +715,16 @@ public function testInvoke(): void null, new \ArrayObject(['getDummyItem' => new Model\Link('getDummyItem', new \ArrayObject(['id' => '$response.body#/id']), null, 'This is a dummy')]) ), - '400' => new Response('Invalid input'), - '422' => new Response('Unprocessable entity'), + '400' => new Response( + 'Invalid input', + content: new \ArrayObject(['application/problem+json' => new MediaType(schema: new \ArrayObject(['$ref' => '#/components/schemas/Error']))]), + links: new \ArrayObject(['getDummyItem' => new Model\Link('getDummyItem', new \ArrayObject(['id' => '$response.body#/id']), null, 'This is a dummy')]) + ), + '422' => new Response( + 'Unprocessable entity', + content: new \ArrayObject(['application/problem+json' => new MediaType(schema: new \ArrayObject(['$ref' => '#/components/schemas/Error']))]), + links: new \ArrayObject(['getDummyItem' => new Model\Link('getDummyItem', new \ArrayObject(['id' => '$response.body#/id']), null, 'This is a dummy')]) + ), ], 'Creates a Dummy resource.', 'Creates a Dummy resource.', @@ -735,7 +755,11 @@ public function testInvoke(): void 'application/ld+json' => new MediaType(new \ArrayObject(new \ArrayObject(['$ref' => '#/components/schemas/Dummy.OutputDto']))), ]) ), - '404' => new Response('Resource not found'), + '404' => new Response( + 'Not found', + content: new \ArrayObject(['application/problem+json' => new MediaType(schema: new \ArrayObject(['$ref' => '#/components/schemas/Error']))]), + links: new \ArrayObject(['getDummyItem' => new Model\Link('getDummyItem', new \ArrayObject(['id' => '$request.path.id']), null, 'This is a dummy')]) + ), ], 'Retrieves a Dummy resource.', 'Retrieves a Dummy resource.', @@ -755,9 +779,21 @@ public function testInvoke(): void null, new \ArrayObject(['getDummyItem' => new Model\Link('getDummyItem', new \ArrayObject(['id' => '$request.path.id']), null, 'This is a dummy')]) ), - '400' => new Response('Invalid input'), - '422' => new Response('Unprocessable entity'), - '404' => new Response('Resource not found'), + '400' => new Response( + 'Invalid input', + content: new \ArrayObject(['application/problem+json' => new MediaType(schema: new \ArrayObject(['$ref' => '#/components/schemas/Error']))]), + links: new \ArrayObject(['getDummyItem' => new Model\Link('getDummyItem', new \ArrayObject(['id' => '$request.path.id']), null, 'This is a dummy')]) + ), + '422' => new Response( + 'Unprocessable entity', + content: new \ArrayObject(['application/problem+json' => new MediaType(schema: new \ArrayObject(['$ref' => '#/components/schemas/Error']))]), + links: new \ArrayObject(['getDummyItem' => new Model\Link('getDummyItem', new \ArrayObject(['id' => '$request.path.id']), null, 'This is a dummy')]) + ), + '404' => new Response( + 'Not found', + content: new \ArrayObject(['application/problem+json' => new MediaType(schema: new \ArrayObject(['$ref' => '#/components/schemas/Error']))]), + links: new \ArrayObject(['getDummyItem' => new Model\Link('getDummyItem', new \ArrayObject(['id' => '$request.path.id']), null, 'This is a dummy')]) + ), ], 'Replaces the Dummy resource.', 'Replaces the Dummy resource.', @@ -777,7 +813,11 @@ public function testInvoke(): void ['Dummy'], [ '204' => new Response('Dummy resource deleted'), - '404' => new Response('Resource not found'), + '404' => new Response( + 'Not found', + content: new \ArrayObject(['application/problem+json' => new MediaType(schema: new \ArrayObject(['$ref' => '#/components/schemas/Error']))]), + links: new \ArrayObject(['getDummyItem' => new Model\Link('getDummyItem', new \ArrayObject(['id' => '$request.path.id']), null, 'This is a dummy')]) + ), ], 'Removes the Dummy resource.', 'Removes the Dummy resource.', @@ -800,7 +840,11 @@ public function testInvoke(): void 'Foo' => ['$ref' => '#/components/schemas/Dummy'], ])), '205' => new Response(), - '404' => new Response('Resource not found'), + '404' => new Response( + 'Not found', + content: new \ArrayObject(['application/problem+json' => new MediaType(schema: new \ArrayObject(['$ref' => '#/components/schemas/Error']))]), + links: new \ArrayObject(['getDummyItem' => new Model\Link('getDummyItem', new \ArrayObject(['id' => '$request.path.id']), null, 'This is a dummy')]) + ), ], 'Dummy', 'Custom description', @@ -843,9 +887,21 @@ public function testInvoke(): void null, new \ArrayObject(['getDummyItem' => new Model\Link('getDummyItem', new \ArrayObject(['id' => '$request.path.id']), null, 'This is a dummy')]) ), - '400' => new Response('Invalid input'), - '422' => new Response('Unprocessable entity'), - '404' => new Response('Resource not found'), + '400' => new Response( + 'Invalid input', + content: new \ArrayObject(['application/problem+json' => new MediaType(schema: new \ArrayObject(['$ref' => '#/components/schemas/Error']))]), + links: new \ArrayObject(['getDummyItem' => new Model\Link('getDummyItem', new \ArrayObject(['id' => '$request.path.id']), null, 'This is a dummy')]) + ), + '422' => new Response( + 'Unprocessable entity', + content: new \ArrayObject(['application/problem+json' => new MediaType(schema: new \ArrayObject(['$ref' => '#/components/schemas/Error']))]), + links: new \ArrayObject(['getDummyItem' => new Model\Link('getDummyItem', new \ArrayObject(['id' => '$request.path.id']), null, 'This is a dummy')]) + ), + '404' => new Response( + 'Not found', + content: new \ArrayObject(['application/problem+json' => new MediaType(schema: new \ArrayObject(['$ref' => '#/components/schemas/Error']))]), + links: new \ArrayObject(['getDummyItem' => new Model\Link('getDummyItem', new \ArrayObject(['id' => '$request.path.id']), null, 'This is a dummy')]) + ), ], 'Replaces the Dummy resource.', 'Replaces the Dummy resource.', @@ -952,8 +1008,16 @@ public function testInvoke(): void null, new \ArrayObject(['getDummyItem' => new Model\Link('getDummyItem', new \ArrayObject(['id' => '$response.body#/id']), null, 'This is a dummy')]) ), - '400' => new Response('Invalid input'), - '422' => new Response('Unprocessable entity'), + '400' => new Response( + 'Invalid input', + content: new \ArrayObject(['application/problem+json' => new MediaType(schema: new \ArrayObject(['$ref' => '#/components/schemas/Error']))]), + links: new \ArrayObject(['getDummyItem' => new Model\Link('getDummyItem', new \ArrayObject(['id' => '$response.body#/id']), null, 'This is a dummy')]) + ), + '422' => new Response( + 'Unprocessable entity', + content: new \ArrayObject(['application/problem+json' => new MediaType(schema: new \ArrayObject(['$ref' => '#/components/schemas/Error']))]), + links: new \ArrayObject(['getDummyItem' => new Model\Link('getDummyItem', new \ArrayObject(['id' => '$response.body#/id']), null, 'This is a dummy')]) + ), ], 'Creates a Dummy resource.', 'Creates a Dummy resource.', @@ -996,8 +1060,16 @@ public function testInvoke(): void null, new \ArrayObject(['getDummyItem' => new Model\Link('getDummyItem', new \ArrayObject(['id' => '$response.body#/id']), null, 'This is a dummy')]) ), - '400' => new Response('Invalid input'), - '422' => new Response('Unprocessable entity'), + '400' => new Response( + 'Invalid input', + content: new \ArrayObject(['application/problem+json' => new MediaType(schema: new \ArrayObject(['$ref' => '#/components/schemas/Error']))]), + links: new \ArrayObject(['getDummyItem' => new Model\Link('getDummyItem', new \ArrayObject(['id' => '$response.body#/id']), null, 'This is a dummy')]) + ), + '422' => new Response( + 'Unprocessable entity', + content: new \ArrayObject(['application/problem+json' => new MediaType(schema: new \ArrayObject(['$ref' => '#/components/schemas/Error']))]), + links: new \ArrayObject(['getDummyItem' => new Model\Link('getDummyItem', new \ArrayObject(['id' => '$response.body#/id']), null, 'This is a dummy')]) + ), ], 'Creates a Dummy resource.', 'Creates a Dummy resource.', @@ -1033,8 +1105,16 @@ public function testInvoke(): void ]) ), '400' => new Response('Error'), - '422' => new Response('Unprocessable entity'), - '404' => new Response('Resource not found'), + '422' => new Response( + 'Unprocessable entity', + content: new \ArrayObject(['application/problem+json' => new MediaType(schema: new \ArrayObject(['$ref' => '#/components/schemas/Error']))]), + links: new \ArrayObject(['getDummyItem' => new Model\Link('getDummyItem', new \ArrayObject(['id' => '$response.body#/id']), null, 'This is a dummy')]) + ), + '404' => new Response( + 'Not found', + content: new \ArrayObject(['application/problem+json' => new MediaType(schema: new \ArrayObject(['$ref' => '#/components/schemas/Error']))]), + links: new \ArrayObject(['getDummyItem' => new Model\Link('getDummyItem', new \ArrayObject(['id' => '$response.body#/id']), null, 'This is a dummy')]) + ), ], 'Replaces the Dummy resource.', 'Replaces the Dummy resource.', @@ -1070,7 +1150,11 @@ public function testInvoke(): void ]) ), '400' => new Response('Error'), - '422' => new Response('Unprocessable entity'), + '422' => new Response( + 'Unprocessable entity', + content: new \ArrayObject(['application/problem+json' => new MediaType(schema: new \ArrayObject(['$ref' => '#/components/schemas/Error']))]), + links: new \ArrayObject(['getDummyItem' => new Model\Link('getDummyItem', new \ArrayObject(['id' => '$response.body#/id']), null, 'This is a dummy')]) + ), ], 'Creates a Dummy resource.', 'Creates a Dummy resource.', @@ -1128,8 +1212,16 @@ public function testInvoke(): void null, new \ArrayObject(['getDummyItem' => new Model\Link('getDummyItem', new \ArrayObject(['id' => '$response.body#/id']), null, 'This is a dummy')]) ), - '400' => new Response('Invalid input'), - '422' => new Response('Unprocessable entity'), + '400' => new Response( + 'Invalid input', + content: new \ArrayObject(['application/problem+json' => new MediaType(schema: new \ArrayObject(['$ref' => '#/components/schemas/Error']))]), + links: new \ArrayObject(['getDummyItem' => new Model\Link('getDummyItem', new \ArrayObject(['id' => '$response.body#/id']), null, 'This is a dummy')]) + ), + '422' => new Response( + 'Unprocessable entity', + content: new \ArrayObject(['application/problem+json' => new MediaType(schema: new \ArrayObject(['$ref' => '#/components/schemas/Error']))]), + links: new \ArrayObject(['getDummyItem' => new Model\Link('getDummyItem', new \ArrayObject(['id' => '$response.body#/id']), null, 'This is a dummy')]) + ), ], 'Creates a Dummy resource.', 'Creates a Dummy resource.', @@ -1160,11 +1252,11 @@ public function testInvoke(): void '418' => new Response( 'A Teapot Exception', new \ArrayObject([ - 'application/ld+json' => new MediaType(new \ArrayObject(new \ArrayObject([ + 'application/problem+json' => new MediaType(new \ArrayObject(new \ArrayObject([ '$ref' => '#/components/schemas/DummyErrorResource', ]))), ]), - links: new \ArrayObject(['getDummyItem' => new Model\Link('getDummyItem', new \ArrayObject(), null, 'This is a dummy')]) + links: new \ArrayObject(['getDummyItem' => new Model\Link('getDummyItem', new \ArrayObject(['id' => '$response.body#/id']), null, 'This is a dummy')]) ), ], 'Retrieves the collection of Dummy resources.', diff --git a/src/OpenApi/Tests/Serializer/OpenApiNormalizerTest.php b/src/OpenApi/Tests/Serializer/OpenApiNormalizerTest.php index defc4ad12f7..23b7d7af780 100644 --- a/src/OpenApi/Tests/Serializer/OpenApiNormalizerTest.php +++ b/src/OpenApi/Tests/Serializer/OpenApiNormalizerTest.php @@ -43,7 +43,9 @@ use ApiPlatform\OpenApi\Options; use ApiPlatform\OpenApi\Serializer\OpenApiNormalizer; use ApiPlatform\OpenApi\Tests\Fixtures\Dummy; +use ApiPlatform\State\ApiResource\Error; use ApiPlatform\State\Pagination\PaginationOptions; +use ApiPlatform\Validator\Exception\ValidationException; use PHPUnit\Framework\TestCase; use Prophecy\Argument; use Prophecy\PhpUnit\ProphecyTrait; @@ -100,6 +102,7 @@ public function testNormalize(): void $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); $propertyNameCollectionFactoryProphecy->create(Dummy::class, Argument::any())->shouldBeCalled()->willReturn(new PropertyNameCollection(['id', 'name', 'description', 'dummyDate'])); $propertyNameCollectionFactoryProphecy->create('Zorro', Argument::any())->shouldBeCalled()->willReturn(new PropertyNameCollection(['id'])); + $propertyNameCollectionFactoryProphecy->create(Error::class, Argument::any())->shouldBeCalled()->willReturn(new PropertyNameCollection([])); $baseOperation = (new HttpOperation())->withTypes(['http://schema.example.com/Dummy']) ->withInputFormats(self::OPERATION_FORMATS['input_formats'])->withOutputFormats(self::OPERATION_FORMATS['output_formats']) @@ -141,6 +144,8 @@ public function testNormalize(): void $resourceCollectionMetadataFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); $resourceCollectionMetadataFactoryProphecy->create(Dummy::class)->shouldBeCalled()->willReturn($dummyMetadata); $resourceCollectionMetadataFactoryProphecy->create('Zorro')->shouldBeCalled()->willReturn($zorroMetadata); + $resourceCollectionMetadataFactoryProphecy->create(Error::class)->shouldBeCalled()->willReturn(new ResourceMetadataCollection(Error::class, [])); + $resourceCollectionMetadataFactoryProphecy->create(ValidationException::class)->shouldBeCalled()->willReturn(new ResourceMetadataCollection(ValidationException::class, [])); $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); $propertyMetadataFactoryProphecy->create(Dummy::class, 'id', Argument::any())->shouldBeCalled()->willReturn( diff --git a/tests/JsonSchema/Command/JsonSchemaGenerateCommandTest.php b/tests/JsonSchema/Command/JsonSchemaGenerateCommandTest.php index cd34b6bfb11..260124a1e3a 100644 --- a/tests/JsonSchema/Command/JsonSchemaGenerateCommandTest.php +++ b/tests/JsonSchema/Command/JsonSchemaGenerateCommandTest.php @@ -34,6 +34,7 @@ use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Question; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\RelatedDummy; use ApiPlatform\Tests\SetupClassResourcesTrait; +use PHPUnit\Framework\Attributes\DataProvider; use Symfony\Bundle\FrameworkBundle\Console\Application; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; use Symfony\Component\Console\Tester\ApplicationTester; @@ -378,9 +379,7 @@ public function testGenId(): void $this->assertArrayNotHasKey('@id', $json['definitions']['DisableIdGenerationItem.jsonld']['properties']); } - /** - * @dataProvider arrayPropertyTypeSyntaxProvider - */ + #[DataProvider('arrayPropertyTypeSyntaxProvider')] public function testOpenApiSchemaGenerationForArrayProperty(string $propertyName, array $expectedProperties): void { $this->tester->run([