1<?php
2
3namespace Drupal\Tests\jsonapi\Unit\Routing;
4
5use Drupal\Core\Entity\EntityInterface;
6use Drupal\jsonapi\ResourceType\ResourceType;
7use Drupal\jsonapi\ResourceType\ResourceTypeRelationship;
8use Drupal\jsonapi\ResourceType\ResourceTypeRepository;
9use Drupal\jsonapi\Routing\Routes;
10use Drupal\Tests\UnitTestCase;
11use Drupal\Core\Routing\RouteObjectInterface;
12use Symfony\Component\DependencyInjection\ContainerInterface;
13
14/**
15 * @coversDefaultClass \Drupal\jsonapi\Routing\Routes
16 * @group jsonapi
17 *
18 * @internal
19 */
20class RoutesTest extends UnitTestCase {
21
22  /**
23   * List of routes objects for the different scenarios.
24   *
25   * @var \Drupal\jsonapi\Routing\Routes[]
26   */
27  protected $routes;
28
29  /**
30   * {@inheritdoc}
31   */
32  protected function setUp(): void {
33    parent::setUp();
34    $relationship_fields = [
35      'external' => new ResourceTypeRelationship('external'),
36      'internal' => new ResourceTypeRelationship('internal'),
37      'both' => new ResourceTypeRelationship('both'),
38    ];
39    $type_1 = new ResourceType('entity_type_1', 'bundle_1_1', EntityInterface::class, FALSE, TRUE, TRUE, FALSE, $relationship_fields);
40    $type_2 = new ResourceType('entity_type_2', 'bundle_2_1', EntityInterface::class, TRUE, TRUE, TRUE, FALSE, $relationship_fields);
41    $relatable_resource_types = [
42      'external' => [$type_1],
43      'internal' => [$type_2],
44      'both' => [$type_1, $type_2],
45    ];
46    $type_1->setRelatableResourceTypes($relatable_resource_types);
47    $type_2->setRelatableResourceTypes($relatable_resource_types);
48    // This type ensures that we can create routes for bundle IDs which might be
49    // cast from strings to integers.  It should not affect related resource
50    // routing.
51    $type_3 = new ResourceType('entity_type_3', '123', EntityInterface::class, TRUE);
52    $type_3->setRelatableResourceTypes([]);
53    $resource_type_repository = $this->prophesize(ResourceTypeRepository::class);
54    $resource_type_repository->all()->willReturn([$type_1, $type_2, $type_3]);
55    $container = $this->prophesize(ContainerInterface::class);
56    $container->get('jsonapi.resource_type.repository')->willReturn($resource_type_repository->reveal());
57    $container->getParameter('jsonapi.base_path')->willReturn('/jsonapi');
58    $container->getParameter('authentication_providers')->willReturn([
59      'lorem' => [],
60      'ipsum' => [],
61    ]);
62
63    $this->routes['ok'] = Routes::create($container->reveal());
64  }
65
66  /**
67   * @covers ::routes
68   */
69  public function testRoutesCollection() {
70    // Get the route collection and start making assertions.
71    $routes = $this->routes['ok']->routes();
72
73    // - 2 collection routes; GET & POST for the non-internal resource type.
74    // - 3 individual routes; GET, PATCH & DELETE for the non-internal resource
75    //   type.
76    // - 2 related routes; GET for the non-internal resource type relationships
77    //   fields: external & both.
78    // - 12 relationship routes; 3 fields * 4 HTTP methods.
79    //   `relationship` routes are generated even for internal target resource
80    //   types (`related` routes are not).
81    // - 1 for the JSON:API entry point.
82    $this->assertEquals(20, $routes->count());
83
84    $iterator = $routes->getIterator();
85    // Check the collection route.
86    /** @var \Symfony\Component\Routing\Route $route */
87    $route = $iterator->offsetGet('jsonapi.entity_type_1--bundle_1_1.collection');
88    $this->assertSame('/jsonapi/entity_type_1/bundle_1_1', $route->getPath());
89    $this->assertSame(['lorem', 'ipsum'], $route->getOption('_auth'));
90    $this->assertSame('entity_type_1--bundle_1_1', $route->getDefault(Routes::RESOURCE_TYPE_KEY));
91    $this->assertEquals(['GET'], $route->getMethods());
92    $this->assertSame(Routes::CONTROLLER_SERVICE_NAME . ':getCollection', $route->getDefault(RouteObjectInterface::CONTROLLER_NAME));
93    // Check the collection POST route.
94    $route = $iterator->offsetGet('jsonapi.entity_type_1--bundle_1_1.collection.post');
95    $this->assertSame('/jsonapi/entity_type_1/bundle_1_1', $route->getPath());
96    $this->assertSame(['lorem', 'ipsum'], $route->getOption('_auth'));
97    $this->assertSame('entity_type_1--bundle_1_1', $route->getDefault(Routes::RESOURCE_TYPE_KEY));
98    $this->assertEquals(['POST'], $route->getMethods());
99    $this->assertSame(Routes::CONTROLLER_SERVICE_NAME . ':createIndividual', $route->getDefault(RouteObjectInterface::CONTROLLER_NAME));
100  }
101
102  /**
103   * @covers ::routes
104   */
105  public function testRoutesIndividual() {
106    // Get the route collection and start making assertions.
107    $iterator = $this->routes['ok']->routes()->getIterator();
108
109    // Check the individual route.
110    /** @var \Symfony\Component\Routing\Route $route */
111    $route = $iterator->offsetGet('jsonapi.entity_type_1--bundle_1_1.individual');
112    $this->assertSame('/jsonapi/entity_type_1/bundle_1_1/{entity}', $route->getPath());
113    $this->assertSame('entity_type_1--bundle_1_1', $route->getDefault(Routes::RESOURCE_TYPE_KEY));
114    $this->assertEquals(['GET'], $route->getMethods());
115    $this->assertSame(Routes::CONTROLLER_SERVICE_NAME . ':getIndividual', $route->getDefault(RouteObjectInterface::CONTROLLER_NAME));
116    $this->assertSame(['lorem', 'ipsum'], $route->getOption('_auth'));
117    $this->assertEquals([
118      'entity' => ['type' => 'entity:entity_type_1'],
119      'resource_type' => ['type' => 'jsonapi_resource_type'],
120    ], $route->getOption('parameters'));
121
122    $route = $iterator->offsetGet('jsonapi.entity_type_1--bundle_1_1.individual.patch');
123    $this->assertSame('/jsonapi/entity_type_1/bundle_1_1/{entity}', $route->getPath());
124    $this->assertSame('entity_type_1--bundle_1_1', $route->getDefault(Routes::RESOURCE_TYPE_KEY));
125    $this->assertEquals(['PATCH'], $route->getMethods());
126    $this->assertSame(Routes::CONTROLLER_SERVICE_NAME . ':patchIndividual', $route->getDefault(RouteObjectInterface::CONTROLLER_NAME));
127    $this->assertSame(['lorem', 'ipsum'], $route->getOption('_auth'));
128    $this->assertEquals([
129      'entity' => ['type' => 'entity:entity_type_1'],
130      'resource_type' => ['type' => 'jsonapi_resource_type'],
131    ], $route->getOption('parameters'));
132
133    $route = $iterator->offsetGet('jsonapi.entity_type_1--bundle_1_1.individual.delete');
134    $this->assertSame('/jsonapi/entity_type_1/bundle_1_1/{entity}', $route->getPath());
135    $this->assertSame('entity_type_1--bundle_1_1', $route->getDefault(Routes::RESOURCE_TYPE_KEY));
136    $this->assertEquals(['DELETE'], $route->getMethods());
137    $this->assertSame(Routes::CONTROLLER_SERVICE_NAME . ':deleteIndividual', $route->getDefault(RouteObjectInterface::CONTROLLER_NAME));
138    $this->assertSame(['lorem', 'ipsum'], $route->getOption('_auth'));
139    $this->assertEquals([
140      'entity' => ['type' => 'entity:entity_type_1'],
141      'resource_type' => ['type' => 'jsonapi_resource_type'],
142    ], $route->getOption('parameters'));
143  }
144
145  /**
146   * @covers ::routes
147   */
148  public function testRoutesRelated() {
149    // Get the route collection and start making assertions.
150    $iterator = $this->routes['ok']->routes()->getIterator();
151
152    // Check the related route.
153    /** @var \Symfony\Component\Routing\Route $route */
154    $route = $iterator->offsetGet('jsonapi.entity_type_1--bundle_1_1.external.related');
155    $this->assertSame('/jsonapi/entity_type_1/bundle_1_1/{entity}/external', $route->getPath());
156    $this->assertSame('entity_type_1--bundle_1_1', $route->getDefault(Routes::RESOURCE_TYPE_KEY));
157    $this->assertEquals(['GET'], $route->getMethods());
158    $this->assertSame(Routes::CONTROLLER_SERVICE_NAME . ':getRelated', $route->getDefault(RouteObjectInterface::CONTROLLER_NAME));
159    $this->assertSame(['lorem', 'ipsum'], $route->getOption('_auth'));
160    $this->assertEquals([
161      'entity' => ['type' => 'entity:entity_type_1'],
162      'resource_type' => ['type' => 'jsonapi_resource_type'],
163    ], $route->getOption('parameters'));
164  }
165
166  /**
167   * @covers ::routes
168   */
169  public function testRoutesRelationships() {
170    // Get the route collection and start making assertions.
171    $iterator = $this->routes['ok']->routes()->getIterator();
172
173    // Check the relationships route.
174    /** @var \Symfony\Component\Routing\Route $route */
175    $route = $iterator->offsetGet('jsonapi.entity_type_1--bundle_1_1.both.relationship.get');
176    $this->assertSame('/jsonapi/entity_type_1/bundle_1_1/{entity}/relationships/both', $route->getPath());
177    $this->assertSame('entity_type_1--bundle_1_1', $route->getDefault(Routes::RESOURCE_TYPE_KEY));
178    $this->assertEquals(['GET'], $route->getMethods());
179    $this->assertSame(Routes::CONTROLLER_SERVICE_NAME . ':getRelationship', $route->getDefault(RouteObjectInterface::CONTROLLER_NAME));
180    $this->assertSame(['lorem', 'ipsum'], $route->getOption('_auth'));
181    $this->assertEquals([
182      'entity' => ['type' => 'entity:entity_type_1'],
183      'resource_type' => ['type' => 'jsonapi_resource_type'],
184    ], $route->getOption('parameters'));
185  }
186
187  /**
188   * Ensures that the expected routes are created or not created.
189   *
190   * @dataProvider expectedRoutes
191   */
192  public function testRoutes($route) {
193    $this->assertArrayHasKey($route, $this->routes['ok']->routes()->all());
194  }
195
196  /**
197   * Lists routes which should have been created.
198   */
199  public function expectedRoutes() {
200    return [
201      ['jsonapi.entity_type_1--bundle_1_1.individual'],
202      ['jsonapi.entity_type_1--bundle_1_1.collection'],
203      ['jsonapi.entity_type_1--bundle_1_1.internal.relationship.get'],
204      ['jsonapi.entity_type_1--bundle_1_1.internal.relationship.post'],
205      ['jsonapi.entity_type_1--bundle_1_1.internal.relationship.patch'],
206      ['jsonapi.entity_type_1--bundle_1_1.internal.relationship.delete'],
207      ['jsonapi.entity_type_1--bundle_1_1.external.related'],
208      ['jsonapi.entity_type_1--bundle_1_1.external.relationship.get'],
209      ['jsonapi.entity_type_1--bundle_1_1.external.relationship.post'],
210      ['jsonapi.entity_type_1--bundle_1_1.external.relationship.patch'],
211      ['jsonapi.entity_type_1--bundle_1_1.external.relationship.delete'],
212      ['jsonapi.entity_type_1--bundle_1_1.both.related'],
213      ['jsonapi.entity_type_1--bundle_1_1.both.relationship.get'],
214      ['jsonapi.entity_type_1--bundle_1_1.both.relationship.post'],
215      ['jsonapi.entity_type_1--bundle_1_1.both.relationship.patch'],
216      ['jsonapi.entity_type_1--bundle_1_1.both.relationship.delete'],
217      ['jsonapi.resource_list'],
218    ];
219  }
220
221  /**
222   * Ensures that no routes are created for internal resources.
223   *
224   * @dataProvider notExpectedRoutes
225   */
226  public function testInternalRoutes($route) {
227    $this->assertArrayNotHasKey($route, $this->routes['ok']->routes()->all());
228  }
229
230  /**
231   * Lists routes which should have been created.
232   */
233  public function notExpectedRoutes() {
234    return [
235      ['jsonapi.entity_type_2--bundle_2_1.individual'],
236      ['jsonapi.entity_type_2--bundle_2_1.collection'],
237      ['jsonapi.entity_type_2--bundle_2_1.collection.post'],
238      ['jsonapi.entity_type_2--bundle_2_1.internal.related'],
239      ['jsonapi.entity_type_2--bundle_2_1.internal.relationship'],
240    ];
241  }
242
243}
244