|
| 1 | +<?php |
| 2 | + |
| 3 | +/* |
| 4 | + * This file is part of the API Platform project. |
| 5 | + * |
| 6 | + * (c) Kévin Dunglas <[email protected]> |
| 7 | + * |
| 8 | + * For the full copyright and license information, please view the LICENSE |
| 9 | + * file that was distributed with this source code. |
| 10 | + */ |
| 11 | + |
| 12 | +declare(strict_types=1); |
| 13 | + |
| 14 | +namespace ApiPlatform\Tests\Bridge\Symfony\Routing; |
| 15 | + |
| 16 | +use ApiPlatform\Core\Api\IdentifiersExtractorInterface; |
| 17 | +use ApiPlatform\Core\Bridge\Symfony\Routing\ApiLoader; |
| 18 | +use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface; |
| 19 | +use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; |
| 20 | +use ApiPlatform\Core\Metadata\Property\PropertyMetadata; |
| 21 | +use ApiPlatform\Core\Metadata\Property\PropertyNameCollection; |
| 22 | +use ApiPlatform\Core\Metadata\Resource\Factory\ResourceNameCollectionFactoryInterface; |
| 23 | +use ApiPlatform\Core\Metadata\Resource\ResourceNameCollection; |
| 24 | +use ApiPlatform\Core\Operation\UnderscorePathSegmentNameGenerator; |
| 25 | +use ApiPlatform\Core\PathResolver\CustomOperationPathResolver; |
| 26 | +use ApiPlatform\Core\PathResolver\OperationPathResolver; |
| 27 | +use ApiPlatform\Core\Tests\ProphecyTrait; |
| 28 | +use ApiPlatform\Metadata\ApiResource; |
| 29 | +use ApiPlatform\Metadata\Delete; |
| 30 | +use ApiPlatform\Metadata\Get; |
| 31 | +use ApiPlatform\Metadata\Post; |
| 32 | +use ApiPlatform\Metadata\Put; |
| 33 | +use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; |
| 34 | +use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; |
| 35 | +use ApiPlatform\Tests\Fixtures\DummyEntity; |
| 36 | +use ApiPlatform\Tests\Fixtures\RelatedDummyEntity; |
| 37 | +use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Dummy; |
| 38 | +use PHPUnit\Framework\TestCase; |
| 39 | +use Prophecy\Argument; |
| 40 | +use Symfony\Component\DependencyInjection\ContainerInterface; |
| 41 | +use Symfony\Component\HttpKernel\KernelInterface; |
| 42 | +use Symfony\Component\Routing\Route; |
| 43 | + |
| 44 | +/** |
| 45 | + * @author Antoine Bluchet <[email protected]> |
| 46 | + * @author Amrouche Hamza <[email protected]> |
| 47 | + * |
| 48 | + * TODO: in 3.0 just remove the IdentifiersExtractor |
| 49 | + * @group legacy |
| 50 | + */ |
| 51 | +class ApiLoaderTest extends TestCase |
| 52 | +{ |
| 53 | + use ProphecyTrait; |
| 54 | + |
| 55 | + public function testApiLoader() |
| 56 | + { |
| 57 | + $path = '/dummies/{id}.{_format}'; |
| 58 | + |
| 59 | + $resourceCollection = new ResourceMetadataCollection(Dummy::class, [ |
| 60 | + (new ApiResource())->withShortName('dummy')->withOperations([ |
| 61 | + // Default operations based on OperationResourceMetadataFactory |
| 62 | + 'api_dummies_get_item' => (new Get())->withIdentifiers(['id'])->withUriTemplate($path)->withDefaults(['my_default' => 'default_value', '_controller' => 'should_not_be_overriden'])->withRequirements(['id' => '\d+'])->withController('api_platform.action.get_item'), |
| 63 | + 'api_dummies_put_item' => (new Put())->withUriTemplate($path), |
| 64 | + 'api_dummies_delete_item' => (new Delete())->withUriTemplate($path), |
| 65 | + // Custom operations |
| 66 | + 'api_dummies_my_op_collection' => (new Get())->withUriTemplate('/dummies.{_format}')->withDefaults(['my_default' => 'default_value', '_format' => 'a valid format'])->withRequirements(['_format' => 'a valid format'])->withCondition("request.headers.get('User-Agent') matches '/firefox/i'")->withController('some.service.name')->withCollection(true), |
| 67 | + 'api_dummies_my_second_op_collection' => (new Post())->withUriTemplate('/dummies.{_format}')->withOptions(['option' => 'option_value'])->withHost('{subdomain}.api-platform.com')->withSchemes(['https'])->withCollection(true), |
| 68 | + //without controller, takes the default one |
| 69 | + 'api_dummies_my_path_op_collection' => (new Get())->withUriTemplate('some/custom/path')->withCollection(true), |
| 70 | + // Custom path |
| 71 | + 'api_dummies_my_stateless_op_collection' => (new Get())->withUriTemplate('/dummies.{_format}')->withStateless(true)->withCollection(true), |
| 72 | + ]), |
| 73 | + ]); |
| 74 | + |
| 75 | + $routeCollection = $this->getApiLoaderWithResourceMetadataCollection($resourceCollection)->load(null); |
| 76 | + |
| 77 | + $this->assertEquals( |
| 78 | + $this->getRoute( |
| 79 | + $path, |
| 80 | + 'api_platform.action.get_item', |
| 81 | + null, |
| 82 | + RelatedDummyEntity::class, |
| 83 | + [], |
| 84 | + 'api_dummies_get_item', |
| 85 | + ['my_default' => 'default_value', '_controller' => 'should_not_be_overriden'], |
| 86 | + ['GET'], |
| 87 | + ['id' => '\d+'] |
| 88 | + ), |
| 89 | + $routeCollection->get('api_dummies_get_item') |
| 90 | + ); |
| 91 | + |
| 92 | + $this->assertEquals( |
| 93 | + $this->getRoute( |
| 94 | + $path, |
| 95 | + 'api_platform.action.placeholder', |
| 96 | + null, |
| 97 | + RelatedDummyEntity::class, |
| 98 | + [], |
| 99 | + 'api_dummies_delete_item', |
| 100 | + [], |
| 101 | + ['DELETE'], |
| 102 | + [] |
| 103 | + ), |
| 104 | + $routeCollection->get('api_dummies_delete_item') |
| 105 | + ); |
| 106 | + |
| 107 | + $this->assertEquals( |
| 108 | + $this->getRoute( |
| 109 | + $path, |
| 110 | + 'api_platform.action.placeholder', |
| 111 | + null, |
| 112 | + RelatedDummyEntity::class, |
| 113 | + [], |
| 114 | + 'api_dummies_put_item', |
| 115 | + [], |
| 116 | + ['PUT'], |
| 117 | + [] |
| 118 | + ), |
| 119 | + $routeCollection->get('api_dummies_put_item') |
| 120 | + ); |
| 121 | + |
| 122 | + $this->assertEquals( |
| 123 | + $this->getRoute( |
| 124 | + '/dummies.{_format}', |
| 125 | + 'some.service.name', |
| 126 | + null, |
| 127 | + RelatedDummyEntity::class, |
| 128 | + [], |
| 129 | + 'api_dummies_my_op_collection', |
| 130 | + ['my_default' => 'default_value', '_format' => 'a valid format'], |
| 131 | + ['GET'], |
| 132 | + ['_format' => 'a valid format'], |
| 133 | + [], |
| 134 | + '', |
| 135 | + [], |
| 136 | + "request.headers.get('User-Agent') matches '/firefox/i'" |
| 137 | + ), |
| 138 | + $routeCollection->get('api_dummies_my_op_collection') |
| 139 | + ); |
| 140 | + |
| 141 | + $this->assertEquals( |
| 142 | + $this->getRoute( |
| 143 | + '/dummies.{_format}', |
| 144 | + 'api_platform.action.placeholder', |
| 145 | + null, |
| 146 | + RelatedDummyEntity::class, |
| 147 | + [], |
| 148 | + 'api_dummies_my_second_op_collection', |
| 149 | + [], |
| 150 | + ['POST'], |
| 151 | + [], |
| 152 | + ['option' => 'option_value'], |
| 153 | + '{subdomain}.api-platform.com', |
| 154 | + ['https'] |
| 155 | + ), |
| 156 | + $routeCollection->get('api_dummies_my_second_op_collection') |
| 157 | + ); |
| 158 | + |
| 159 | + $this->assertEquals( |
| 160 | + $this->getRoute( |
| 161 | + 'some/custom/path', |
| 162 | + 'api_platform.action.placeholder', |
| 163 | + null, |
| 164 | + RelatedDummyEntity::class, |
| 165 | + [], |
| 166 | + 'api_dummies_my_path_op_collection', |
| 167 | + [], |
| 168 | + ['GET'], |
| 169 | + [] |
| 170 | + ), |
| 171 | + $routeCollection->get('api_dummies_my_path_op_collection') |
| 172 | + ); |
| 173 | + |
| 174 | + $this->assertEquals( |
| 175 | + $this->getRoute( |
| 176 | + '/dummies.{_format}', |
| 177 | + 'api_platform.action.placeholder', |
| 178 | + true, |
| 179 | + RelatedDummyEntity::class, |
| 180 | + [], |
| 181 | + 'api_dummies_my_stateless_op_collection', |
| 182 | + [], |
| 183 | + ['GET'], |
| 184 | + [] |
| 185 | + ), |
| 186 | + $routeCollection->get('api_dummies_my_stateless_op_collection') |
| 187 | + ); |
| 188 | + } |
| 189 | + |
| 190 | + public function testApiLoaderWithPrefix() |
| 191 | + { |
| 192 | + $prefix = '/foobar-prefix'; |
| 193 | + $path = '/dummies/{id}.{_format}'; |
| 194 | + |
| 195 | + $resourceCollection = new ResourceMetadataCollection(Dummy::class, [(new ApiResource())->withShortName('dummy')->withOperations([ |
| 196 | + 'api_dummies_get_item' => (new Get())->withUriTemplate($path)->withRoutePrefix($prefix)->withDefaults(['my_default' => 'default_value', '_controller' => 'should_not_be_overriden'])->withRequirements(['id' => '\d+']), |
| 197 | + 'api_dummies_put_item' => (new Put())->withUriTemplate($path)->withRoutePrefix($prefix), |
| 198 | + 'api_dummies_delete_item' => (new Delete())->withUriTemplate($path)->withRoutePrefix($prefix), |
| 199 | + ])]); |
| 200 | + |
| 201 | + $routeCollection = $this->getApiLoaderWithResourceMetadataCollection($resourceCollection)->load(null); |
| 202 | + |
| 203 | + $prefixedPath = $prefix.$path; |
| 204 | + |
| 205 | + $this->assertEquals( |
| 206 | + $this->getRoute( |
| 207 | + $prefixedPath, |
| 208 | + 'api_platform.action.placeholder', |
| 209 | + null, |
| 210 | + RelatedDummyEntity::class, |
| 211 | + [], |
| 212 | + 'api_dummies_get_item', |
| 213 | + ['my_default' => 'default_value', '_controller' => 'should_not_be_overriden'], |
| 214 | + ['GET'], |
| 215 | + ['id' => '\d+'] |
| 216 | + ), |
| 217 | + $routeCollection->get('api_dummies_get_item') |
| 218 | + ); |
| 219 | + |
| 220 | + $this->assertEquals( |
| 221 | + $this->getRoute( |
| 222 | + $prefixedPath, |
| 223 | + 'api_platform.action.placeholder', |
| 224 | + null, |
| 225 | + RelatedDummyEntity::class, |
| 226 | + [], |
| 227 | + 'api_dummies_delete_item', |
| 228 | + [], |
| 229 | + ['DELETE'] |
| 230 | + ), |
| 231 | + $routeCollection->get('api_dummies_delete_item') |
| 232 | + ); |
| 233 | + |
| 234 | + $this->assertEquals( |
| 235 | + $this->getRoute( |
| 236 | + $prefixedPath, |
| 237 | + 'api_platform.action.placeholder', |
| 238 | + null, |
| 239 | + RelatedDummyEntity::class, |
| 240 | + [], |
| 241 | + 'api_dummies_put_item', |
| 242 | + [], |
| 243 | + ['PUT'] |
| 244 | + ), |
| 245 | + $routeCollection->get('api_dummies_put_item') |
| 246 | + ); |
| 247 | + } |
| 248 | + |
| 249 | + private function getApiLoaderWithResourceMetadataCollection(ResourceMetadataCollection $resourceCollection): ApiLoader |
| 250 | + { |
| 251 | + $routingConfig = __DIR__.'/../../../../src/Core/Bridge/Symfony/Bundle/Resources/config/routing'; |
| 252 | + |
| 253 | + $kernelProphecy = $this->prophesize(KernelInterface::class); |
| 254 | + $kernelProphecy->locateResource(Argument::any())->willReturn($routingConfig); |
| 255 | + $possibleArguments = [ |
| 256 | + 'api_platform.action.get_collection', |
| 257 | + 'api_platform.action.post_collection', |
| 258 | + 'api_platform.action.get_item', |
| 259 | + 'api_platform.action.put_item', |
| 260 | + 'api_platform.action.delete_item', |
| 261 | + ]; |
| 262 | + $containerProphecy = $this->prophesize(ContainerInterface::class); |
| 263 | + |
| 264 | + foreach ($possibleArguments as $possibleArgument) { |
| 265 | + $containerProphecy->has($possibleArgument)->willReturn(true); |
| 266 | + } |
| 267 | + |
| 268 | + $containerProphecy->has(Argument::type('string'))->willReturn(false); |
| 269 | + |
| 270 | + $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); |
| 271 | + $resourceMetadataFactoryProphecy->create(DummyEntity::class)->willReturn($resourceCollection); |
| 272 | + $resourceMetadataFactoryProphecy->create(RelatedDummyEntity::class)->willReturn($resourceCollection); |
| 273 | + |
| 274 | + $resourceNameCollectionFactoryProphecy = $this->prophesize(ResourceNameCollectionFactoryInterface::class); |
| 275 | + $resourceNameCollectionFactoryProphecy->create()->willReturn(new ResourceNameCollection([DummyEntity::class, RelatedDummyEntity::class])); |
| 276 | + |
| 277 | + $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); |
| 278 | + $propertyNameCollectionFactoryProphecy->create(DummyEntity::class)->willReturn(new PropertyNameCollection(['id'])); |
| 279 | + $propertyNameCollectionFactoryProphecy->create(RelatedDummyEntity::class)->willReturn(new PropertyNameCollection(['id'])); |
| 280 | + |
| 281 | + $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); |
| 282 | + $propertyMetadataFactoryProphecy->create(RelatedDummyEntity::class, 'id')->willReturn(new PropertyMetadata()); |
| 283 | + $propertyMetadataFactoryProphecy->create(DummyEntity::class, 'id')->willReturn(new PropertyMetadata()); |
| 284 | + |
| 285 | + $operationPathResolver = new CustomOperationPathResolver(new OperationPathResolver(new UnderscorePathSegmentNameGenerator())); |
| 286 | + |
| 287 | + $resourceMetadataFactory = $resourceMetadataFactoryProphecy->reveal(); |
| 288 | + |
| 289 | + $identifiersExtractorProphecy = $this->prophesize(IdentifiersExtractorInterface::class); |
| 290 | + $identifiersExtractorProphecy->getIdentifiersFromResourceClass(Argument::type('string'))->willReturn(['id']); |
| 291 | + $identifiersExtractor = $identifiersExtractorProphecy->reveal(); |
| 292 | + |
| 293 | + return new ApiLoader($kernelProphecy->reveal(), $resourceNameCollectionFactoryProphecy->reveal(), $resourceMetadataFactory, $operationPathResolver, $containerProphecy->reveal(), ['jsonld' => ['application/ld+json']], [], null, false, true, true, false, false, $identifiersExtractor); |
| 294 | + } |
| 295 | + |
| 296 | + private function getRoute(string $path, string $controller, ?bool $stateless, string $resourceClass, array $identifiers, string $operationName, array $extraDefaults = [], array $methods = [], array $requirements = [], array $options = [], string $host = '', array $schemes = [], string $condition = ''): Route |
| 297 | + { |
| 298 | + return new Route( |
| 299 | + $path, |
| 300 | + [ |
| 301 | + '_controller' => $controller, |
| 302 | + '_format' => null, |
| 303 | + '_stateless' => $stateless, |
| 304 | + '_api_resource_class' => $resourceClass, |
| 305 | + '_api_operation_name' => $operationName, |
| 306 | + ] + $extraDefaults, |
| 307 | + $requirements, |
| 308 | + $options, |
| 309 | + $host, |
| 310 | + $schemes, |
| 311 | + $methods, |
| 312 | + $condition |
| 313 | + ); |
| 314 | + } |
| 315 | +} |
0 commit comments