From c86bd627cf9bd6bd7ab3c0677952abaaddaae484 Mon Sep 17 00:00:00 2001 From: DjordyKoert Date: Wed, 3 Jan 2024 00:14:39 +0100 Subject: [PATCH] re-add unit tests --- Tests/RouteDescriber/Fixtures/DTO.php | 25 ++- .../SymfonyDescriberMapQueryStringClass.php | 3 + .../SymfonyMapQueryParameterDescriberTest.php | 115 +++++++++++++ .../SymfonyMapQueryStringDescriberTest.php | 161 ++++++++++++++++++ .../SymfonyMapRequestPayloadDescriberTest.php | 106 ++++++++++++ Tests/RouteDescriber/SymfonyDescriberTest.php | 146 ++++++++++++++++ 6 files changed, 551 insertions(+), 5 deletions(-) create mode 100644 Tests/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryParameterDescriberTest.php create mode 100644 Tests/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryStringDescriberTest.php create mode 100644 Tests/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapRequestPayloadDescriberTest.php create mode 100644 Tests/RouteDescriber/SymfonyDescriberTest.php diff --git a/Tests/RouteDescriber/Fixtures/DTO.php b/Tests/RouteDescriber/Fixtures/DTO.php index a37be818e..2a6d8f9e8 100644 --- a/Tests/RouteDescriber/Fixtures/DTO.php +++ b/Tests/RouteDescriber/Fixtures/DTO.php @@ -16,25 +16,40 @@ class DTO implements SelfDescribingModelInterface public const DESCRIPTION = 'some description'; public function __construct( - private int $id, - private string $name, - private ?string $nullableName, + public int $id, + public string $name, + public ?string $nullableName, #[OA\Property( example: self::EXAMPLE_NAME, )] - private string $nameWithExample, + public string $nameWithExample, #[OA\Property( description: self::DESCRIPTION, )] - private string $nameWithDescription, + public string $nameWithDescription, ) { } public static function describe(Schema $schema, Model $model): void { + $schema->type = 'object'; + $schema->required = self::getRequired(); $schema->properties = self::getProperties(); } + /** + * @return string[] + */ + public static function getRequired(): array + { + return [ + 'id', + 'name', + 'nameWithExample', + 'nameWithDescription', + ]; + } + /** * @return Property[] */ diff --git a/Tests/RouteDescriber/Fixtures/SymfonyDescriberMapQueryStringClass.php b/Tests/RouteDescriber/Fixtures/SymfonyDescriberMapQueryStringClass.php index 5482e91cd..fb659a970 100644 --- a/Tests/RouteDescriber/Fixtures/SymfonyDescriberMapQueryStringClass.php +++ b/Tests/RouteDescriber/Fixtures/SymfonyDescriberMapQueryStringClass.php @@ -22,6 +22,9 @@ public static function describe(Schema $schema, Model $model): void $schema->description = $model->getType()->getClassName(); $schema->type = self::TYPE; + $schema->required = [ + 'id', + ]; $schema->properties = self::getProperties(); } diff --git a/Tests/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryParameterDescriberTest.php b/Tests/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryParameterDescriberTest.php new file mode 100644 index 000000000..970478c73 --- /dev/null +++ b/Tests/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryParameterDescriberTest.php @@ -0,0 +1,115 @@ +argumentMetadataFactory = self::getContainer()->get('argument_metadata_factory'); + + $this->symfonyMapQueryParameterDescriber = new SymfonyMapQueryParameterDescriber(); + } + + /** + * @dataProvider provideMapQueryParameterTestData + */ + public function testMapQueryParameter(callable $function): void + { + $argumentMetaData = $this->argumentMetadataFactory->createArgumentMetadata($function)[0]; + + $this->symfonyMapQueryParameterDescriber->describe( + new OpenApi([]), + $operation = new Operation([]), + $argumentMetaData + ); + + /** @var MapQueryParameter $mapQueryParameter */ + $mapQueryParameter = $argumentMetaData->getAttributes(MapQueryParameter::class, ArgumentMetadata::IS_INSTANCEOF)[0]; + + $documentationParameter = $operation->parameters[0]; + self::assertSame($mapQueryParameter->name ?? $argumentMetaData->getName(), $documentationParameter->name); + self::assertSame('query', $documentationParameter->in); + self::assertSame(!$argumentMetaData->hasDefaultValue() && !$argumentMetaData->isNullable(), $documentationParameter->required); + + $schema = $documentationParameter->schema; + self::assertSame('integer', $schema->type); + if ($argumentMetaData->hasDefaultValue()) { + self::assertSame($argumentMetaData->getDefaultValue(), $schema->default); + } + + if (FILTER_VALIDATE_REGEXP === $mapQueryParameter->filter) { + self::assertSame($mapQueryParameter->options['regexp'], $schema->pattern); + } + } + + public static function provideMapQueryParameterTestData(): iterable + { + yield 'it documents query parameters' => [ + function ( + #[MapQueryParameter] int $parameter1, + ) { + }, + ]; + + yield 'it documents query parameters with default values' => [ + function ( + #[MapQueryParameter] int $parameter1 = 123, + ) { + }, + ]; + + yield 'it documents query parameters with nullable types' => [ + function ( + #[MapQueryParameter] ?int $parameter1, + ) { + }, + ]; + + yield 'it uses MapQueryParameter name argument as name' => [ + function ( + #[MapQueryParameter('someOtherParameter1Name')] int $parameter1, + ) { + }, + ]; + + yield 'it documents regex pattern' => [ + function ( + #[MapQueryParameter(filter: FILTER_VALIDATE_REGEXP, options: ['regexp' => '/^\d+$/'])] int $parameter1, + ) { + }, + ]; + } +} diff --git a/Tests/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryStringDescriberTest.php b/Tests/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryStringDescriberTest.php new file mode 100644 index 000000000..924689619 --- /dev/null +++ b/Tests/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapQueryStringDescriberTest.php @@ -0,0 +1,161 @@ +argumentMetadataFactory = self::getContainer()->get('argument_metadata_factory'); + + $this->openApi = new OpenApi([]); + + $this->symfonyMapQueryStringDescriber = new SymfonyMapQueryStringDescriber([new SelfDescribingModelDescriber()]); + + $registry = new ModelRegistry([], $this->openApi, []); + + $this->symfonyMapQueryStringDescriber->setModelRegistry($registry); + } + + /** + * @dataProvider provideMapQueryStringTestData + */ + public function testMapQueryString(callable $function, bool $required): void + { + $argumentMetaData = $this->argumentMetadataFactory->createArgumentMetadata($function)[0]; + + $this->symfonyMapQueryStringDescriber->describe( + $this->openApi, + $operation = new Operation([]), + $argumentMetaData + ); + + // Test it registers the model + $modelSchema = $this->openApi->components->schemas[0]; + $expectedModelProperties = SymfonyDescriberMapQueryStringClass::getProperties(); + + self::assertSame(SymfonyDescriberMapQueryStringClass::SCHEMA, $modelSchema->schema); + self::assertSame(SymfonyDescriberMapQueryStringClass::TITLE, $modelSchema->title); + self::assertSame(SymfonyDescriberMapQueryStringClass::TYPE, $modelSchema->type); + self::assertEquals($expectedModelProperties, $modelSchema->properties); + + foreach ($expectedModelProperties as $key => $expectedModelProperty) { + $queryParameter = $operation->parameters[$key]; + + self::assertSame('query', $queryParameter->in); + self::assertSame($expectedModelProperty->property, $queryParameter->name); + self::assertSame($required, $queryParameter->required); + } + } + + public static function provideMapQueryStringTestData(): iterable + { + yield 'it documents query string parameters' => [ + function ( + #[MapQueryString] SymfonyDescriberMapQueryStringClass $parameter1, + ) { + }, + true + ]; + + yield 'it documents a nullable type as optional' => [ + function ( + #[MapQueryString] ?SymfonyDescriberMapQueryStringClass $parameter1, + ) { + }, + false + ]; + + yield 'it documents a default value as optional' => [ + function ( + #[MapQueryString] ?SymfonyDescriberMapQueryStringClass $parameter1, + ) { + }, + false + ]; + } + + public function testItDescribesProperties(): void + { + $function = function ( + #[MapQueryString] DTO $DTO, + ) { + }; + + $argumentMetaData = $this->argumentMetadataFactory->createArgumentMetadata($function)[0]; + + $this->symfonyMapQueryStringDescriber->describe( + $this->openApi, + $operation = new Operation([]), + $argumentMetaData + ); + + // Test it registers the model + $modelSchema = $this->openApi->components->schemas[0]; + + self::assertEquals('object', $modelSchema->type); + self::assertEquals(DTO::getRequired(), $modelSchema->required); + self::assertEquals(DTO::getProperties(), $modelSchema->properties); + + self::assertSame('id', $operation->parameters[0]->name); + self::assertSame('int', $operation->parameters[0]->schema->type); + + self::assertSame('name', $operation->parameters[1]->name); + + self::assertSame('nullableName', $operation->parameters[2]->name); + self::assertSame('string', $operation->parameters[2]->schema->type); + self::assertSame(false, $operation->parameters[2]->required); + self::assertSame(true, $operation->parameters[2]->schema->nullable); + + self::assertSame('nameWithExample', $operation->parameters[3]->name); + self::assertSame('string', $operation->parameters[3]->schema->type); + self::assertSame(true, $operation->parameters[3]->required); + self::assertSame(DTO::EXAMPLE_NAME, $operation->parameters[3]->schema->example); + self::assertSame(DTO::EXAMPLE_NAME, $operation->parameters[3]->example); + + self::assertSame('nameWithDescription', $operation->parameters[4]->name); + self::assertSame('string', $operation->parameters[4]->schema->type); + self::assertSame(true, $operation->parameters[4]->required); + self::assertSame(DTO::DESCRIPTION, $operation->parameters[4]->schema->description); + self::assertSame(DTO::DESCRIPTION, $operation->parameters[4]->description); + } +} diff --git a/Tests/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapRequestPayloadDescriberTest.php b/Tests/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapRequestPayloadDescriberTest.php new file mode 100644 index 000000000..f5cdbaf31 --- /dev/null +++ b/Tests/RouteDescriber/SymfonyAnnotationDescriber/SymfonyMapRequestPayloadDescriberTest.php @@ -0,0 +1,106 @@ +argumentMetadataFactory = self::getContainer()->get('argument_metadata_factory'); + + $this->symfonyMapRequestPayloadDescriber = new SymfonyMapRequestPayloadDescriber(); + } + + /** + * @dataProvider provideMapRequestPayloadTestData + * + * @param string[] $expectedMediaTypes + */ + public function testMapRequestPayload(callable $function, array $expectedMediaTypes): void + { + $argumentMetaData = $this->argumentMetadataFactory->createArgumentMetadata($function)[0]; + + $this->symfonyMapRequestPayloadDescriber->describe( + new OpenApi([]), + $operation = new Operation([]), + $argumentMetaData + ); + + foreach ($expectedMediaTypes as $expectedMediaType) { + $requestBodyContent = $operation->requestBody->content[$expectedMediaType]; + + self::assertSame($expectedMediaType, $requestBodyContent->mediaType); + self::assertSame('object', $requestBodyContent->schema->type); + self::assertSame(stdClass::class, $requestBodyContent->schema->ref->type); + } + } + + public static function provideMapRequestPayloadTestData(): iterable + { + yield 'it sets default mediaType to json' => [ + function ( + #[MapRequestPayload] stdClass $payload + ) { + }, + ['application/json'], + ]; + + yield 'it sets mediaType to json' => [ + function ( + #[MapRequestPayload('json')] stdClass $payload + ) { + }, + ['application/json'], + ]; + + yield 'it sets mediaType to xml' => [ + function ( + #[MapRequestPayload('xml')] stdClass $payload + ) { + }, + ['application/xml'], + ]; + + yield 'it sets multiple mediaTypes' => [ + function ( + #[MapRequestPayload(['json', 'xml'])] stdClass $payload + ) { + }, + ['application/json', 'application/xml'], + ]; + } +} diff --git a/Tests/RouteDescriber/SymfonyDescriberTest.php b/Tests/RouteDescriber/SymfonyDescriberTest.php new file mode 100644 index 000000000..ff555ee89 --- /dev/null +++ b/Tests/RouteDescriber/SymfonyDescriberTest.php @@ -0,0 +1,146 @@ +argumentMetadataFactoryInterface = $this->createMock(ArgumentMetadataFactoryInterface::class); + $this->inlineParameterDescriberInterfaceMock = $this->createMock(InlineParameterDescriberInterface::class); + + $this->modelRegistry = new ModelRegistry( + [], + $this->openApi = new OpenApi([]), + [] + ); + + $this->inlineParameterDescriber = new InlineParameterDescriber( + $this->argumentMetadataFactoryInterface, + [$this->inlineParameterDescriberInterfaceMock] + ); + + $this->inlineParameterDescriber->setModelRegistry($this->modelRegistry); + } + + public function testDescribe(): void + { + $argumentMetaData = $this->createStub(ArgumentMetadata::class); + + $reflectionMethodStub = $this->createStub(ReflectionMethod::class); + + $this->argumentMetadataFactoryInterface + ->expects(self::once()) + ->method('createArgumentMetadata') + ->with('foo', $reflectionMethodStub) + ->willReturn([$argumentMetaData]) + ; + + $this->inlineParameterDescriberInterfaceMock + ->expects(self::exactly(count(Util::OPERATIONS))) + ->method('supports') + ->with($argumentMetaData) + ->willReturn(true) + ; + + $this->inlineParameterDescriberInterfaceMock + ->expects(self::exactly(count(Util::OPERATIONS))) + ->method('describe') + ->with( + $this->openApi, + self::isInstanceOf(Operation::class), + $argumentMetaData + ) + ; + + $this->inlineParameterDescriber->describe( + $this->openApi, + new Route('/', defaults: ['_controller' => 'foo']), + $reflectionMethodStub + ); + } + + public function testDescribeSkipsUnsupportedDescribers(): void + { + $argumentMetaData = $this->createStub(ArgumentMetadata::class); + + $reflectionMethodStub = $this->createStub(ReflectionMethod::class); + + $this->argumentMetadataFactoryInterface + ->expects(self::once()) + ->method('createArgumentMetadata') + ->with('foo', $reflectionMethodStub) + ->willReturn([$argumentMetaData]) + ; + + $this->inlineParameterDescriberInterfaceMock + ->expects(self::exactly(count(Util::OPERATIONS))) + ->method('supports') + ->with($argumentMetaData) + ->willReturn(false) + ; + + $this->inlineParameterDescriberInterfaceMock + ->expects(self::never()) + ->method('describe') + ; + + $this->inlineParameterDescriber->describe( + $this->openApi, + new Route('/', defaults: ['_controller' => 'foo']), + $reflectionMethodStub + ); + } +}