NelmioApiDocBundle/Tests/Functional/FunctionalTest.php

695 lines
24 KiB
PHP
Raw Normal View History

2016-07-12 00:33:55 +02:00
<?php
/*
2016-12-29 12:09:26 +01:00
* This file is part of the NelmioApiDocBundle package.
2016-07-12 00:33:55 +02:00
*
2016-12-29 12:09:26 +01:00
* (c) Nelmio
2016-07-12 00:33:55 +02:00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
2016-12-29 12:09:26 +01:00
namespace Nelmio\ApiDocBundle\Tests\Functional;
2016-07-12 00:33:55 +02:00
use Nelmio\ApiDocBundle\OpenApiPhp\Util;
use Nelmio\ApiDocBundle\Tests\Helper;
use OpenApi\Annotations as OA;
use OpenApi\Generator;
use Symfony\Component\Serializer\Annotation\SerializedName;
2016-07-12 00:33:55 +02:00
class FunctionalTest extends WebTestCase
{
protected function setUp(): void
2019-12-19 15:41:14 +01:00
{
parent::setUp();
static::createClient([], ['HTTP_HOST' => 'api.example.com']);
}
public function testConfiguredDocumentation()
{
$this->assertEquals('My Default App', $this->getOpenApiDefinition()->info->title);
$this->assertEquals('My Test App', $this->getOpenApiDefinition('test')->info->title);
}
public function testUndocumentedAction()
{
$api = $this->getOpenApiDefinition();
$this->assertNotHasPath('/undocumented', $api);
$this->assertNotHasPath('/api/admin', $api);
}
/**
* @dataProvider provideArticleRoute
*/
public function testFetchArticleAction(string $articleRoute)
2017-06-13 13:34:26 +02:00
{
$operation = $this->getOperation($articleRoute, 'get');
2017-06-13 13:34:26 +02:00
$this->assertHasResponse('200', $operation);
$response = $this->getOperationResponse($operation, '200');
$this->assertEquals('#/components/schemas/Article', $response->content['application/json']->schema->ref);
2017-06-13 13:34:26 +02:00
// Ensure that groups are supported
$articleModel = $this->getModel('Article');
$this->assertCount(1, $articleModel->properties);
$this->assertHasProperty('author', $articleModel);
$this->assertSame('#/components/schemas/User2', Util::getProperty($articleModel, 'author')->ref);
$this->assertNotHasProperty('author', Util::getProperty($articleModel, 'author'));
2017-06-13 13:34:26 +02:00
}
public function provideArticleRoute(): iterable
{
yield 'Annotations' => ['/api/article/{id}'];
if (\PHP_VERSION_ID >= 80100) {
yield 'Attributes' => ['/api/article_attributes/{id}'];
}
}
2017-03-16 19:35:04 +01:00
public function testFilteredAction()
{
$openApi = $this->getOpenApiDefinition();
2017-03-16 19:35:04 +01:00
$this->assertNotHasPath('/filtered', $openApi);
2017-03-16 19:35:04 +01:00
}
/**
* Tests that the paths are automatically resolved in Swagger annotations.
*
* @dataProvider swaggerActionPathsProvider
*/
public function testSwaggerAction(string $path)
{
$operation = $this->getOperation($path, 'get');
$this->assertHasResponse('201', $operation);
$response = $this->getOperationResponse($operation, '201');
$this->assertEquals('An example resource', $response->description);
}
public function swaggerActionPathsProvider()
{
return [['/api/swagger'], ['/api/swagger2']];
}
public function testAnnotationWithManualPath()
{
$path = $this->getPath('/api/swagger2');
$this->assertSame(Generator::UNDEFINED, $path->post);
$operation = $this->getOperation('/api/swagger', 'get');
$this->assertNotHasParameter('Accept-Version', 'header', $operation);
$operation = $this->getOperation('/api/swagger2', 'get');
$this->assertHasParameter('Accept-Version', 'header', $operation);
}
/**
* @dataProvider implicitSwaggerActionMethodsProvider
*/
public function testImplicitSwaggerAction(string $method)
{
$operation = $this->getOperation('/api/swagger/implicit', $method);
$this->assertEquals(['implicit'], $operation->tags);
$this->assertHasResponse('201', $operation);
$response = $this->getOperationResponse($operation, '201');
$this->assertEquals('Operation automatically detected', $response->description);
$this->assertEquals('#/components/schemas/User', $response->content['application/json']->schema->ref);
2017-01-14 17:36:56 +01:00
$this->assertInstanceOf(OA\RequestBody::class, $operation->requestBody);
$requestBody = $operation->requestBody;
$this->assertEquals('This is a request body', $requestBody->description);
$this->assertEquals('array', $requestBody->content['application/json']->schema->type);
$this->assertEquals('#/components/schemas/User', $requestBody->content['application/json']->schema->items->ref);
}
public function implicitSwaggerActionMethodsProvider()
{
return [['get'], ['post']];
}
2016-07-13 23:05:14 +02:00
public function testUserAction()
2016-07-12 00:33:55 +02:00
{
$operation = $this->getOperation('/api/test/{user}', 'get');
2016-07-12 00:33:55 +02:00
$this->assertEquals(Generator::UNDEFINED, $operation->security);
$this->assertEquals(Generator::UNDEFINED, $operation->summary);
$this->assertEquals(Generator::UNDEFINED, $operation->description);
$this->assertEquals(Generator::UNDEFINED, $operation->deprecated);
$this->assertHasResponse(200, $operation);
$this->assertHasParameter('user', 'path', $operation);
$parameter = Util::getOperationParameter($operation, 'user', 'path');
$this->assertTrue($parameter->required);
$this->assertEquals('string', $parameter->schema->type);
$this->assertEquals('/foo/', $parameter->schema->pattern);
$this->assertEquals(Generator::UNDEFINED, $parameter->schema->format);
2016-07-12 00:33:55 +02:00
}
2016-07-13 23:05:14 +02:00
public function testDeprecatedAction()
{
$operation = $this->getOperation('/api/deprecated', 'get');
2016-07-13 23:05:14 +02:00
$this->assertEquals('This action is deprecated.', $operation->summary);
$this->assertEquals('Please do not use this action.', $operation->description);
$this->assertTrue($operation->deprecated);
2016-07-13 23:05:14 +02:00
}
2016-08-01 19:58:57 +02:00
public function testApiPlatform()
2016-07-28 10:20:59 +02:00
{
2016-08-01 19:58:57 +02:00
$operation = $this->getOperation('/api/dummies', 'get');
$operation = $this->getOperation('/api/foo', 'get');
$operation = $this->getOperation('/api/foo', 'post');
$operation = $this->getOperation('/api/dummies/{id}', 'get');
2016-07-28 10:20:59 +02:00
}
2017-01-14 17:36:56 +01:00
public function testUserModel()
{
$this->assertEquals(
[
'type' => 'object',
'properties' => [
'money' => [
'type' => 'number',
'format' => 'float',
2017-12-18 21:07:44 +01:00
'default' => 0.0,
],
'id' => [
'type' => 'integer',
2017-12-17 10:44:07 +01:00
'description' => 'User id',
'readOnly' => true,
2017-12-17 10:44:07 +01:00
'title' => 'userid',
'example' => 1,
'default' => null,
],
'email' => [
'type' => 'string',
'readOnly' => false,
],
'roles' => [
'title' => 'roles',
'type' => 'array',
'description' => 'User roles',
'example' => '["ADMIN","SUPERUSER"]',
'items' => ['type' => 'string'],
'default' => ['user'],
],
'location' => [
'title' => 'User Location.',
'type' => 'string',
],
'friendsNumber' => [
'type' => 'string',
],
2018-02-19 21:41:05 +01:00
'creationDate' => [
'type' => 'string',
'format' => 'date-time',
],
'users' => [
'items' => [
'$ref' => '#/components/schemas/User',
],
'type' => 'array',
],
2018-08-30 01:10:36 +02:00
'friend' => [
'nullable' => true,
'allOf' => [
['$ref' => '#/components/schemas/User'],
],
2018-08-30 01:10:36 +02:00
],
'friends' => [
'nullable' => true,
'items' => [
'$ref' => '#/components/schemas/User',
],
'type' => 'array',
],
'dummy' => [
'$ref' => '#/components/schemas/Dummy2',
],
'status' => [
'type' => 'string',
2017-12-18 21:07:44 +01:00
'enum' => ['disabled', 'enabled'],
],
'dateAsInterface' => [
'type' => 'string',
'format' => 'date-time',
2018-12-10 14:25:08 +01:00
],
],
'schema' => 'User',
],
json_decode($this->getModel('User')->toJson(), true)
);
2017-01-14 17:36:56 +01:00
}
2017-06-24 17:49:00 +02:00
public function testFormSupport()
{
$this->assertEquals([
'type' => 'object',
'description' => 'this is the description of an user',
'properties' => [
'strings' => [
'items' => ['type' => 'string'],
'type' => 'array',
],
'dummy' => ['$ref' => '#/components/schemas/DummyType'],
'dummies' => [
'items' => ['$ref' => '#/components/schemas/DummyType'],
'type' => 'array',
2017-12-17 10:44:07 +01:00
],
'empty_dummies' => [
'items' => ['$ref' => '#/components/schemas/DummyEmptyType'],
'type' => 'array',
],
'quz' => [
'type' => 'string',
'description' => 'User type.',
],
'entity' => [
'type' => 'string',
'format' => 'Entity id',
],
'entities' => [
'type' => 'array',
'format' => '[Entity id]',
'items' => ['type' => 'string'],
],
'document' => [
'type' => 'string',
'format' => 'Document id',
],
'documents' => [
'type' => 'array',
'format' => '[Document id]',
'items' => ['type' => 'string'],
],
'extended_builtin' => [
'type' => 'string',
'enum' => ['foo', 'bar'],
],
'save' => [
],
],
'required' => ['dummy', 'dummies', 'entity', 'entities', 'document', 'documents', 'extended_builtin'],
'schema' => 'UserType',
], json_decode($this->getModel('UserType')->toJson(), true));
2017-06-24 17:49:00 +02:00
$this->assertEquals([
'type' => 'object',
'properties' => [
'bar' => [
'type' => 'string',
],
'foo' => [
'type' => 'string',
'enum' => ['male', 'female'],
],
2018-09-24 17:35:57 +02:00
'boo' => [
'type' => 'boolean',
'enum' => [true, false],
],
'foz' => [
'type' => 'array',
'items' => [
'type' => 'string',
'enum' => ['male', 'female'],
],
],
'baz' => [
2017-12-17 10:44:07 +01:00
'type' => 'boolean',
],
'bey' => [
'type' => 'integer',
],
'password' => [
'type' => 'object',
'required' => ['first_field', 'second'],
'properties' => [
'first_field' => [
'type' => 'string',
'format' => 'password',
],
'second' => [
'type' => 'string',
'format' => 'password',
],
],
],
2017-06-24 17:49:00 +02:00
],
'required' => ['foo', 'foz', 'password'],
'schema' => 'DummyType',
], json_decode($this->getModel('DummyType')->toJson(), true));
$this->assertEquals([
'type' => 'object',
'properties' => [
'quz' => [
'$ref' => '#/components/schemas/User',
],
],
'required' => ['quz'],
'schema' => 'FormWithModel',
], json_decode($this->getModel('FormWithModel')->toJson(), true));
2017-06-24 17:49:00 +02:00
}
/**
* @dataProvider provideSecurityRoute
*/
public function testSecurityAction(string $route)
{
$operation = $this->getOperation($route, 'get');
$expected = [
['api_key' => []],
['basic' => []],
['oauth2' => ['scope_1']],
];
$this->assertEquals($expected, $operation->security);
}
public function provideSecurityRoute(): iterable
{
yield 'Annotations' => ['/api/security'];
if (\PHP_VERSION_ID >= 80100) {
yield 'Attributes' => ['/api/security_attributes'];
}
}
/**
* @dataProvider provideSecurityOverrideRoute
*/
public function testSecurityOverrideAction(string $route)
{
$operation = $this->getOperation($route, 'get');
$this->assertEquals([], $operation->security);
}
public function provideSecurityOverrideRoute(): iterable
{
yield 'Annotations' => ['/api/securityOverride'];
if (\PHP_VERSION_ID >= 80100) {
yield 'Attributes' => ['/api/security_override_attributes'];
}
}
public function testInlinePHP81Parameters()
{
if (\PHP_VERSION_ID < 80100) {
$this->markTestSkipped('Attributes require PHP 8.1');
}
$operation = $this->getOperation('/api/inline_path_parameters', 'get');
$this->assertCount(1, $operation->parameters);
$this->assertInstanceOf(OA\PathParameter::class, $operation->parameters[0]);
$this->assertSame($operation->parameters[0]->name, 'product_id');
$this->assertSame($operation->parameters[0]->schema->type, 'string');
}
public function testClassSecurityAction()
{
$operation = $this->getOperation('/api/security/class', 'get');
$expected = [
['basic' => []],
];
$this->assertEquals($expected, $operation->security);
}
public function testSymfonyConstraintDocumentation()
{
$expected = [
'required' => [
'propertyNotBlank',
'propertyNotNull',
],
'properties' => [
'propertyNotBlank' => [
'type' => 'integer',
'maxItems' => 10,
'minItems' => 0,
],
'propertyNotNull' => [
'type' => 'integer',
],
'propertyAssertLength' => [
'type' => 'integer',
'maxLength' => '50',
'minLength' => '0',
],
'propertyRegex' => [
'type' => 'integer',
'pattern' => '.*[a-z]{2}.*',
],
'propertyCount' => [
'type' => 'integer',
'maxItems' => '10',
'minItems' => '0',
],
'propertyChoice' => [
'type' => 'integer',
'enum' => ['choice1', 'choice2'],
],
'propertyChoiceWithCallback' => [
'type' => 'integer',
'enum' => ['choice1', 'choice2'],
],
'propertyChoiceWithCallbackWithoutClass' => [
'type' => 'integer',
'enum' => ['choice1', 'choice2'],
],
'propertyChoiceWithMultiple' => [
'type' => 'array',
'items' => [
'type' => 'string',
'enum' => ['choice1', 'choice2'],
],
],
'propertyExpression' => [
'type' => 'integer',
],
'propertyRange' => [
'type' => 'integer',
'maximum' => 5,
'minimum' => 1,
],
'propertyLessThan' => [
'type' => 'integer',
'exclusiveMaximum' => true,
'maximum' => 42,
],
'propertyLessThanOrEqual' => [
'type' => 'integer',
'maximum' => 23,
],
'propertyWithCompoundValidationRule' => [
'type' => 'integer',
],
],
'type' => 'object',
'schema' => 'SymfonyConstraints',
];
if (Helper::isCompoundValidatorConstraintSupported()) {
$expected['required'][] = 'propertyWithCompoundValidationRule';
$expected['properties']['propertyWithCompoundValidationRule'] = [
'type' => 'integer',
'maximum' => 5,
'exclusiveMaximum' => true,
'minimum' => 0,
'exclusiveMinimum' => true,
];
}
$this->assertEquals($expected, json_decode($this->getModel('SymfonyConstraints')->toJson(), true));
}
public function testConfigReference()
{
$operation = $this->getOperation('/api/configReference', 'get');
$this->assertEquals('#/components/schemas/Test', $this->getOperationResponse($operation, '200')->ref);
$this->assertEquals('#/components/responses/201', $this->getOperationResponse($operation, '201')->ref);
}
public function testOperationsWithOtherAnnotationsAction()
{
$getOperation = $this->getOperation('/api/multi-annotations', 'get');
$this->assertSame('This is the get operation', $getOperation->description);
$this->assertSame('Worked well!', $this->getOperationResponse($getOperation, 200)->description);
$postOperation = $this->getOperation('/api/multi-annotations', 'post');
$this->assertSame('This is post', $postOperation->description);
$this->assertSame('Worked well!', $this->getOperationResponse($postOperation, 200)->description);
}
2019-04-10 20:52:45 +02:00
public function testNoDuplicatedParameters()
{
$this->assertHasPath('/api/article/{id}', $this->getOpenApiDefinition());
$this->assertNotHasParameter('id', 'path', $this->getOperation('/api/article/{id}', 'get'));
2019-04-10 20:52:45 +02:00
}
public function testSerializedNameAction()
{
2020-08-06 10:26:59 +02:00
if (!class_exists(SerializedName::class)) {
$this->markTestSkipped('Annotation @SerializedName doesn\'t exist.');
}
2020-05-30 18:23:49 +02:00
$model = $this->getModel('SerializedNameEnt');
$this->assertCount(2, $model->properties);
2020-05-30 18:23:49 +02:00
$this->assertNotHasProperty('foo', $model);
$this->assertHasProperty('notfoo', $model);
2020-05-30 18:23:49 +02:00
$this->assertNotHasProperty('bar', $model);
$this->assertHasProperty('notwhatyouthink', $model);
}
public function testCompoundEntityAction()
{
$model = $this->getModel('CompoundEntity');
$this->assertCount(1, $model->properties);
$this->assertHasProperty('complex', $model);
$property = $model->properties[0];
$this->assertCount(2, $property->oneOf);
$this->assertSame('integer', $property->oneOf[0]->type);
$this->assertSame('array', $property->oneOf[1]->type);
$this->assertSame('#/components/schemas/CompoundEntity', $property->oneOf[1]->items->ref);
}
public function testInvokableController()
{
$operation = $this->getOperation('/api/invoke', 'get');
$this->assertSame('Invokable!', $this->getOperationResponse($operation, 200)->description);
}
public function testDefaultOperationId()
{
$operation = $this->getOperation('/api/article/{id}', 'get');
$this->assertEquals('get_api_nelmio_apidoc_tests_functional_api_fetcharticle', $operation->operationId);
}
public function testNamedRouteOperationId()
{
$operation = $this->getOperation('/api/named_route-operation-id', 'get');
$this->assertEquals('get_api_named_route_operation_id', $operation->operationId);
$operation = $this->getOperation('/api/named_route-operation-id', 'post');
$this->assertEquals('post_api_named_route_operation_id', $operation->operationId);
}
public function testCustomOperationId()
{
$operation = $this->getOperation('/api/custom-operation-id', 'get');
2022-03-21 17:03:16 +01:00
$this->assertEquals('get-custom-operation-id', $operation->operationId);
$operation = $this->getOperation('/api/custom-operation-id', 'post');
2022-03-21 17:03:16 +01:00
$this->assertEquals('post-custom-operation-id', $operation->operationId);
}
/**
* Related to https://github.com/nelmio/NelmioApiDocBundle/issues/1756
* Ensures private/protected properties are not exposed, just like the symfony serializer does.
*/
public function testPrivateProtectedExposure()
{
// Ensure that groups are supported
2020-12-17 00:06:17 +01:00
$model = $this->getModel('PrivateProtectedExposure');
$this->assertCount(1, $model->properties);
$this->assertHasProperty('publicField', $model);
$this->assertNotHasProperty('privateField', $model);
$this->assertNotHasProperty('protectedField', $model);
$this->assertNotHasProperty('protected', $model);
}
public function testModelsWithDiscriminatorMapAreLoadedWithOpenApiPolymorphism()
{
$model = $this->getModel('SymfonyDiscriminator');
$this->assertInstanceOf(OA\Discriminator::class, $model->discriminator);
$this->assertSame('type', $model->discriminator->propertyName);
$this->assertCount(2, $model->discriminator->mapping);
$this->assertArrayHasKey('one', $model->discriminator->mapping);
$this->assertArrayHasKey('two', $model->discriminator->mapping);
$this->assertNotSame(Generator::UNDEFINED, $model->oneOf);
$this->assertCount(2, $model->oneOf);
}
public function testDiscriminatorMapLoadsChildrenModels()
{
// get model does its own assertions
$this->getModel('SymfonyDiscriminatorOne');
$this->getModel('SymfonyDiscriminatorTwo');
}
2021-06-07 18:20:25 +02:00
public function testNoAdditionalPropertiesSupport()
{
$model = $this->getModel('AddProp');
$this->assertFalse($model->additionalProperties);
}
/**
* @requires PHP >= 8.1
*/
public function testEnumSupport()
{
$model = $this->getModel('ArticleType81');
$this->assertSame('string', $model->type);
$this->assertCount(2, $model->enum);
}
Stop Model Property Description When a Schema Type or Ref is Already Defined (#1978) * Return a Result Object from AnnotationsReader::updateDefinition This is so we can make a decision on whether or not a schema's type or ref has been manually defined by a user via an `@OA\Schema` annotation as something other than an object. If it has been defined, this bundle should not read model properties any further as it causes errors. I put this in AnnotationReader as it seemed the most flexible in the long run. It could have gone in `OpenApiAnnotationsReader`, but then any additional things added to `updateDefinition` could be left out of the decision down the road. This is also a convenient place to decide this once for `ObjectModelDescriber` and `JMSModelDescriber`. * Stop Model Describer if a Schema Type or Ref Has Been Defined Via the result object added in the previous commit. This lets user "short circuit" the model describers by manually defining the schema type or ref on a plain PHP object or form. For example, a collection class could be defined like this: /** * @OA\Schema(type="array", @OA\Items(ref=@Model(type=SomeEntity::class))) */ class SomeCollection implements \IteratorAggregate { } Previously the model describer would error as it tries to merge the `array` schema with the already defiend `object` schema. Now it will prefer the array schema and skip reading all the properties of the object. * Add a Documentation Bit on Stopping Property Description * Mark UpdateClassDefinitionResult as Internal
2022-04-30 13:28:05 -05:00
public function testEntitiesWithOverriddenSchemaTypeDoNotReadOtherProperties()
{
$model = $this->getModel('EntityWithAlternateType');
$this->assertSame('array', $model->type);
$this->assertSame('string', $model->items->type);
$this->assertSame(Generator::UNDEFINED, $model->properties);
}
public function testEntitiesWithRefInSchemaDoNoReadOtherProperties()
{
$model = $this->getModel('EntityWithRef');
$this->assertSame(Generator::UNDEFINED, $model->type);
$this->assertSame('#/components/schemas/Test', $model->ref);
$this->assertSame(Generator::UNDEFINED, $model->properties);
}
public function testEntitiesWithObjectTypeStillReadProperties()
{
$model = $this->getModel('EntityWithObjectType');
$this->assertSame('object', $model->type);
$this->assertCount(1, $model->properties);
$property = Util::getProperty($model, 'notIgnored');
$this->assertSame('string', $property->type);
}
public function testFormsWithOverriddenSchemaTypeDoNotReadOtherProperties()
{
$model = $this->getModel('FormWithAlternateSchemaType');
$this->assertSame('string', $model->type);
$this->assertSame(Generator::UNDEFINED, $model->properties);
}
public function testFormWithRefInSchemaDoNoReadOtherProperties()
{
$model = $this->getModel('FormWithRefType');
$this->assertSame(Generator::UNDEFINED, $model->type);
$this->assertSame('#/components/schemas/Test', $model->ref);
$this->assertSame(Generator::UNDEFINED, $model->properties);
}
2016-07-12 00:33:55 +02:00
}