diff --git a/features/bootstrap/FeatureContext.php b/features/bootstrap/FeatureContext.php index 40b9e8be797..54bb7015b03 100644 --- a/features/bootstrap/FeatureContext.php +++ b/features/bootstrap/FeatureContext.php @@ -21,8 +21,10 @@ use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\DummyCar; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\DummyCarColor; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\DummyFriend; +use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\DummyGroup; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\DummyOffer; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\DummyProduct; +use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\DummyProperty; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\FileConfigDummy; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Node; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Question; @@ -35,6 +37,7 @@ use Behat\Behat\Context\SnippetAcceptingContext; use Behatch\HttpCall\Request; use Doctrine\Common\Persistence\ManagerRegistry; +use Doctrine\Common\Persistence\ObjectManager; use Doctrine\ORM\Tools\SchemaTool; /** @@ -45,7 +48,7 @@ class FeatureContext implements Context, SnippetAcceptingContext private $doctrine; /** - * @var \Doctrine\Common\Persistence\ObjectManager + * @var ObjectManager */ private $manager; @@ -127,6 +130,46 @@ public function thereIsDummyObjects($nb) $this->manager->flush(); } + /** + * @Given there is :nb dummy group objects + */ + public function thereIsDummyGroupObjects($nb) + { + for ($i = 1; $i <= $nb; ++$i) { + $dummyGroup = new DummyGroup(); + + foreach (['foo', 'bar', 'baz', 'qux'] as $property) { + $dummyGroup->$property = ucfirst($property).' #'.$i; + } + + $this->manager->persist($dummyGroup); + } + + $this->manager->flush(); + } + + /** + * @Given there is :nb dummy property objects + */ + public function thereIsDummyPropertyObjects($nb) + { + for ($i = 1; $i <= $nb; ++$i) { + $dummyProperty = new DummyProperty(); + $dummyGroup = new DummyGroup(); + + foreach (['foo', 'bar', 'baz'] as $property) { + $dummyProperty->$property = $dummyGroup->$property = ucfirst($property).' #'.$i; + } + + $dummyProperty->group = $dummyGroup; + + $this->manager->persist($dummyGroup); + $this->manager->persist($dummyProperty); + } + + $this->manager->flush(); + } + /** * @Given there is :nb dummy objects with relatedDummy */ diff --git a/features/serializer/group_filter.feature b/features/serializer/group_filter.feature new file mode 100644 index 00000000000..23e024e08df --- /dev/null +++ b/features/serializer/group_filter.feature @@ -0,0 +1,750 @@ +Feature: Filter with serialization groups on items and collections + In order to retrieve, create and update resources or large collections of resources + As a client software developer + I need to retrieve, create and update resources or collections of resources with serialization groups + + @createSchema + Scenario: Get a collection of resources by group dummy_foo without overriding + Given there is "10" dummy group objects + When I send a "GET" request to "/dummy_groups?groups[]=dummy_foo" + Then the response status code should be 200 + And the response should be in JSON + And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" + And the JSON should be valid according to this schema: + """ + { + "type": "object", + "properties": { + "@context": {"pattern": "^/contexts/DummyGroup$"}, + "@id": {"pattern": "^/dummy_groups$"}, + "@type": {"pattern": "^hydra:Collection$"}, + "hydra:member": { + "type": "array", + "items": [ + { + "type": "object", + "properties": { + "@id": {}, + "@type": {}, + "id": {}, + "foo": {}, + "bar": {}, + "baz": {} + }, + "additionalProperties": false, + "required": ["@id", "@type", "id", "foo", "bar", "baz"] + }, + { + "type": "object", + "properties": { + "@id": {}, + "@type": {}, + "id": {}, + "foo": {}, + "bar": {}, + "baz": {} + }, + "additionalProperties": false, + "required": ["@id", "@type", "id", "foo", "bar", "baz"] + }, + { + "type": "object", + "properties": { + "@id": {}, + "@type": {}, + "id": {}, + "foo": {}, + "bar": {}, + "baz": {} + }, + "additionalProperties": false, + "required": ["@id", "@type", "id", "foo", "bar", "baz"] + } + ], + "additionalItems": false, + "maxItems": 3, + "minItems": 3 + }, + "hydra:view": { + "type": "object", + "properties": { + "@id": {"pattern": "^/dummy_groups\\?groups%5B%5D=dummy_foo&page=1$"}, + "@type": {"pattern": "^hydra:PartialCollectionView$"} + } + } + } + } + """ + + Scenario: Get a collection of resources by group dummy_foo with overriding + When I send a "GET" request to "/dummy_groups?override_groups[]=dummy_foo" + Then the response status code should be 200 + And the response should be in JSON + And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" + And the JSON should be valid according to this schema: + """ + { + "type": "object", + "properties": { + "@context": {"pattern": "^/contexts/DummyGroup$"}, + "@id": {"pattern": "^/dummy_groups$"}, + "@type": {"pattern": "^hydra:Collection$"}, + "hydra:member": { + "type": "array", + "items": [ + { + "type": "object", + "properties": { + "@id": {}, + "@type": {}, + "foo": {} + }, + "additionalProperties": false, + "required": ["@id", "@type", "foo"] + }, + { + "type": "object", + "properties": { + "@id": {}, + "@type": {}, + "foo": {} + }, + "additionalProperties": false, + "required": ["@id", "@type", "foo"] + }, + { + "type": "object", + "properties": { + "@id": {}, + "@type": {}, + "foo": {} + }, + "additionalProperties": false, + "required": ["@id", "@type", "foo"] + } + ], + "additionalItems": false, + "maxItems": 3, + "minItems": 3 + }, + "hydra:view": { + "type": "object", + "properties": { + "@id": {"pattern": "^/dummy_groups\\?override_groups%5B%5D=dummy_foo&page=1$"}, + "@type": {"pattern": "^hydra:PartialCollectionView$"} + } + } + } + } + """ + + Scenario: Get a collection of resources by groups dummy_foo, dummy_qux and without overriding + When I send a "GET" request to "/dummy_groups?groups[]=dummy_foo&groups[]=dummy_qux" + Then the response status code should be 200 + And the response should be in JSON + And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" + And the JSON should be valid according to this schema: + """ + { + "type": "object", + "properties": { + "@context": {"pattern": "^/contexts/DummyGroup$"}, + "@id": {"pattern": "^/dummy_groups$"}, + "@type": {"pattern": "^hydra:Collection$"}, + "hydra:member": { + "type": "array", + "items": [ + { + "type": "object", + "properties": { + "@id": {}, + "@type": {}, + "id": {}, + "foo": {}, + "bar": {}, + "baz": {}, + "qux": {} + }, + "additionalProperties": false, + "required": ["@id", "@type", "id", "foo", "bar", "baz", "qux"] + }, + { + "type": "object", + "properties": { + "@id": {}, + "@type": {}, + "id": {}, + "foo": {}, + "bar": {}, + "baz": {}, + "qux": {} + }, + "additionalProperties": false, + "required": ["@id", "@type", "id", "foo", "bar", "baz", "qux"] + }, + { + "type": "object", + "properties": { + "@id": {}, + "@type": {}, + "id": {}, + "foo": {}, + "bar": {}, + "baz": {}, + "qux": {} + }, + "additionalProperties": false, + "required": ["@id", "@type", "id", "foo", "bar", "baz", "qux"] + } + ], + "additionalItems": false, + "maxItems": 3, + "minItems": 3 + }, + "hydra:view": { + "type": "object", + "properties": { + "@id": {"pattern": "^/dummy_groups\\?groups%5B%5D=dummy_foo&groups%5B%5D=dummy_qux&page=1$"}, + "@type": {"pattern": "^hydra:PartialCollectionView$"} + } + } + } + } + """ + + Scenario: Get a collection of resources by groups dummy_foo, dummy_qux and with overriding + When I send a "GET" request to "/dummy_groups?override_groups[]=dummy_foo&override_groups[]=dummy_qux" + Then the response status code should be 200 + And the response should be in JSON + And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" + And the JSON should be valid according to this schema: + """ + { + "type": "object", + "properties": { + "@context": {"pattern": "^/contexts/DummyGroup$"}, + "@id": {"pattern": "^/dummy_groups$"}, + "@type": {"pattern": "^hydra:Collection$"}, + "hydra:member": { + "type": "array", + "items": [ + { + "type": "object", + "properties": { + "@id": {}, + "@type": {}, + "foo": {}, + "qux": {} + }, + "additionalProperties": false, + "required": ["@id", "@type", "foo", "qux"] + }, + { + "type": "object", + "properties": { + "@id": {}, + "@type": {}, + "foo": {}, + "qux": {} + }, + "additionalProperties": false, + "required": ["@id", "@type", "foo", "qux"] + }, + { + "type": "object", + "properties": { + "@id": {}, + "@type": {}, + "foo": {}, + "qux": {} + }, + "additionalProperties": false, + "required": ["@id", "@type", "foo", "qux"] + } + ], + "additionalItems": false, + "maxItems": 3, + "minItems": 3 + }, + "hydra:view": { + "type": "object", + "properties": { + "@id": {"pattern": "^/dummy_groups\\?override_groups%5B%5D=dummy_foo&override_groups%5B%5D=dummy_qux&page=1$"}, + "@type": {"pattern": "^hydra:PartialCollectionView$"} + } + } + } + } + """ + + Scenario: Get a collection of resources by group empty and without overriding + When I send a "GET" request to "/dummy_groups?groups[]=" + Then the response status code should be 200 + And the response should be in JSON + And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" + And the JSON should be valid according to this schema: + """ + { + "type": "object", + "properties": { + "@context": {"pattern": "^/contexts/DummyGroup$"}, + "@id": {"pattern": "^/dummy_groups$"}, + "@type": {"pattern": "^hydra:Collection$"}, + "hydra:member": { + "type": "array", + "items": [ + { + "type": "object", + "properties": { + "@id": {}, + "@type": {}, + "id": {}, + "foo": {}, + "bar": {}, + "baz": {} + }, + "additionalProperties": false, + "required": ["@id", "@type", "id", "foo", "bar", "baz"] + }, + { + "type": "object", + "properties": { + "@id": {}, + "@type": {}, + "id": {}, + "foo": {}, + "bar": {}, + "baz": {} + }, + "additionalProperties": false, + "required": ["@id", "@type", "id", "foo", "bar", "baz"] + }, + { + "type": "object", + "properties": { + "@id": {}, + "@type": {}, + "id": {}, + "foo": {}, + "bar": {}, + "baz": {} + }, + "additionalProperties": false, + "required": ["@id", "@type", "id", "foo", "bar", "baz"] + } + ], + "additionalItems": false, + "maxItems": 3, + "minItems": 3 + }, + "hydra:view": { + "type": "object", + "properties": { + "@id": {"pattern": "^/dummy_groups\\?groups%5B%5D=&page=1$"}, + "@type": {"pattern": "^hydra:PartialCollectionView$"} + } + } + } + } + """ + + Scenario: Get a collection of resources by group empty and with overriding + When I send a "GET" request to "/dummy_groups?override_groups[]=" + Then the response status code should be 200 + And the response should be in JSON + And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" + And the JSON should be valid according to this schema: + """ + { + "type": "object", + "properties": { + "@context": {"pattern": "^/contexts/DummyGroup$"}, + "@id": {"pattern": "^/dummy_groups$"}, + "@type": {"pattern": "^hydra:Collection$"}, + "hydra:member": { + "type": "array", + "items": [ + { + "type": "object", + "properties": { + "@id": {}, + "@type": {} + }, + "additionalProperties": false, + "required": ["@id", "@type"] + }, + { + "type": "object", + "properties": { + "@id": {}, + "@type": {} + }, + "additionalProperties": false, + "required": ["@id", "@type"] + }, + { + "type": "object", + "properties": { + "@id": {}, + "@type": {} + }, + "additionalProperties": false, + "required": ["@id", "@type"] + } + ], + "additionalItems": false, + "maxItems": 3, + "minItems": 3 + }, + "hydra:view": { + "type": "object", + "properties": { + "@id": {"pattern": "^/dummy_groups\\?override_groups%5B%5D=&page=1$"}, + "@type": {"pattern": "^hydra:PartialCollectionView$"} + } + } + } + } + """ + + Scenario: Get a resource by group dummy_foo without overriding + When I send a "GET" request to "/dummy_groups/1?groups[]=dummy_foo" + Then the response status code should be 200 + And the response should be in JSON + And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" + And the JSON should be valid according to this schema: + """ + { + "type": "object", + "properties": { + "@context": {"pattern": "^/contexts/DummyGroup$"}, + "@id": {"pattern": "^/dummy_groups/1$"}, + "@type": {"pattern": "^DummyGroup$"}, + "id": {}, + "foo": {}, + "bar": {}, + "baz": {} + }, + "additionalProperties": false, + "required": ["@context", "@id", "@type", "id", "foo", "bar", "baz"] + } + """ + + Scenario: Get a resource by group dummy_foo with overriding + When I send a "GET" request to "/dummy_groups/1?override_groups[]=dummy_foo" + Then the response status code should be 200 + And the response should be in JSON + And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" + And the JSON should be valid according to this schema: + """ + { + "type": "object", + "properties": { + "@context": {"pattern": "^/contexts/DummyGroup$"}, + "@id": {"pattern": "^/dummy_groups/1$"}, + "@type": {"pattern": "^DummyGroup$"}, + "foo": {} + }, + "additionalProperties": false, + "required": ["@context", "@id", "@type", "foo"] + } + """ + + Scenario: Get a resource by groups dummy_foo, dummy_qux and without overriding + When I send a "GET" request to "/dummy_groups/1?groups[]=dummy_foo&groups[]=dummy_qux" + Then the response status code should be 200 + And the response should be in JSON + And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" + And the JSON should be valid according to this schema: + """ + { + "type": "object", + "properties": { + "@context": {"pattern": "^/contexts/DummyGroup$"}, + "@id": {"pattern": "^/dummy_groups/1$"}, + "@type": {"pattern": "^DummyGroup$"}, + "id": {}, + "foo": {}, + "bar": {}, + "baz": {}, + "qux": {} + }, + "additionalProperties": false, + "required": ["@context", "@id", "@type", "id", "foo", "bar", "baz", "qux"] + } + """ + + Scenario: Get a resource by groups dummy_foo, dummy_qux and with overriding + When I send a "GET" request to "/dummy_groups/1?override_groups[]=dummy_foo&override_groups[]=dummy_qux" + Then the response status code should be 200 + And the response should be in JSON + And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" + And the JSON should be valid according to this schema: + """ + { + "type": "object", + "properties": { + "@context": {"pattern": "^/contexts/DummyGroup$"}, + "@id": {"pattern": "^/dummy_groups/1$"}, + "@type": {"pattern": "^DummyGroup$"}, + "foo": {}, + "qux": {} + }, + "additionalProperties": false, + "required": ["@context", "@id", "@type", "foo", "qux"] + } + """ + + Scenario: Get a resource by group empty and without overriding + When I send a "GET" request to "/dummy_groups/1?groups[]=" + Then the response status code should be 200 + And the response should be in JSON + And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" + And the JSON should be valid according to this schema: + """ + { + "type": "object", + "properties": { + "@context": {"pattern": "^/contexts/DummyGroup$"}, + "@id": {"pattern": "^/dummy_groups/1$"}, + "@type": {"pattern": "^DummyGroup$"}, + "id": {}, + "foo": {}, + "bar": {}, + "baz": {} + }, + "additionalProperties": false, + "required": ["@context", "@id", "@type", "id", "foo", "bar", "baz"] + } + """ + + Scenario: Get a resource by group empty and with overriding + When I send a "GET" request to "/dummy_groups/1?override_groups[]=" + Then the response status code should be 200 + And the response should be in JSON + And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" + And the JSON should be valid according to this schema: + """ + { + "type": "object", + "properties": { + "@context": {"pattern": "^/contexts/DummyGroup$"}, + "@id": {"pattern": "^/dummy_groups/1$"}, + "@type": {"pattern": "^DummyGroup$"} + }, + "additionalProperties": false, + "required": ["@context", "@id", "@type"] + } + """ + + Scenario: Create a resource by group dummy_foo and without overriding + When I add "Content-Type" header equal to "application/ld+json" + And I send a "POST" request to "/dummy_groups?groups[]=dummy_foo" with body: + """ + { + "foo": "Foo", + "bar": "Bar", + "baz": "Baz", + "qux": "Qux" + } + """ + Then the response status code should be 201 + And the response should be in JSON + And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" + And the JSON should be equal to: + """ + { + "@context": "\/contexts\/DummyGroup", + "@id": "\/dummy_groups\/11", + "@type": "DummyGroup", + "id": 11, + "foo": "Foo", + "bar": "Bar", + "baz": null + } + """ + + Scenario: Create a resource by group dummy_foo and with overriding + When I add "Content-Type" header equal to "application/ld+json" + And I send a "POST" request to "/dummy_groups?override_groups[]=dummy_foo" with body: + """ + { + "foo": "Foo", + "bar": "Bar", + "baz": "Baz", + "qux": "Qux" + } + """ + Then the response status code should be 201 + And the response should be in JSON + And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" + And the JSON should be equal to: + """ + { + "@context": "\/contexts\/DummyGroup", + "@id": "\/dummy_groups\/12", + "@type": "DummyGroup", + "foo": "Foo" + } + """ + + Scenario: Create a resource by groups dummy_foo, dummy_baz, dummy_qux and without overriding + When I add "Content-Type" header equal to "application/ld+json" + And I send a "POST" request to "/dummy_groups?groups[]=dummy_foo&groups[]=dummy_baz&groups[]=dummy_qux" with body: + """ + { + "foo": "Foo", + "bar": "Bar", + "baz": "Baz", + "qux": "Qux" + } + """ + Then the response status code should be 201 + And the response should be in JSON + And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" + And the JSON should be equal to: + """ + { + "@context": "\/contexts\/DummyGroup", + "@id": "\/dummy_groups\/13", + "@type": "DummyGroup", + "id": 13, + "foo": "Foo", + "bar": "Bar", + "baz": "Baz", + "qux": "Qux" + } + """ + + Scenario: Create a resource by groups dummy_foo, dummy_baz, dummy_qux and with overriding + When I add "Content-Type" header equal to "application/ld+json" + And I send a "POST" request to "/dummy_groups?override_groups[]=dummy_foo&override_groups[]=dummy_baz&override_groups[]=dummy_qux" with body: + """ + { + "foo": "Foo", + "bar": "Bar", + "baz": "Baz", + "qux": "Qux" + } + """ + Then the response status code should be 201 + And the response should be in JSON + And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" + And the JSON should be equal to: + """ + { + "@context": "\/contexts\/DummyGroup", + "@id": "\/dummy_groups\/14", + "@type": "DummyGroup", + "foo": "Foo", + "baz": "Baz", + "qux": "Qux" + } + """ + + Scenario: Create a resource by groups dummy, dummy_baz, without overriding + When I add "Content-Type" header equal to "application/ld+json" + And I send a "POST" request to "/dummy_groups?groups[]=dummy&groups[]=dummy_baz" with body: + """ + { + "foo": "Foo", + "bar": "Bar", + "baz": "Baz", + "qux": "Qux" + } + """ + Then the response status code should be 201 + And the response should be in JSON + And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" + And the JSON should be equal to: + """ + { + "@context": "\/contexts\/DummyGroup", + "@id": "\/dummy_groups\/15", + "@type": "DummyGroup", + "id": 15, + "foo": "Foo", + "bar": "Bar", + "baz": "Baz", + "qux": "Qux" + } + """ + + Scenario: Create a resource by groups dummy, dummy_baz and with overriding + When I add "Content-Type" header equal to "application/ld+json" + And I send a "POST" request to "/dummy_groups?override_groups[]=dummy&override_groups[]=dummy_baz" with body: + """ + { + "foo": "Foo", + "bar": "Bar", + "baz": "Baz", + "qux": "Qux" + } + """ + Then the response status code should be 201 + And the response should be in JSON + And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" + And the JSON should be equal to: + """ + { + "@context": "\/contexts\/DummyGroup", + "@id": "\/dummy_groups\/16", + "@type": "DummyGroup", + "id": 16, + "foo": "Foo", + "bar": "Bar", + "baz": "Baz", + "qux": "Qux" + } + """ + + Scenario: Create a resource by group empty and without overriding + When I add "Content-Type" header equal to "application/ld+json" + And I send a "POST" request to "/dummy_groups?groups[]=" with body: + """ + { + "foo": "Foo", + "bar": "Bar", + "baz": "Baz", + "qux": "Qux" + } + """ + Then the response status code should be 201 + And the response should be in JSON + And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" + And the JSON should be equal to: + """ + { + "@context": "\/contexts\/DummyGroup", + "@id": "\/dummy_groups\/17", + "@type": "DummyGroup", + "id": 17, + "foo": "Foo", + "bar": "Bar", + "baz": null + } + """ + + @dropSchema + Scenario: Create a resource by group empty and with overriding + When I add "Content-Type" header equal to "application/ld+json" + And I send a "POST" request to "/dummy_groups?override_groups[]=" with body: + """ + { + "foo": "Foo", + "bar": "Bar", + "baz": "Baz", + "qux": "Qux" + } + """ + Then the response status code should be 201 + And the response should be in JSON + And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" + And the JSON should be equal to: + """ + { + "@context": "\/contexts\/DummyGroup", + "@id": "\/dummy_groups\/18", + "@type": "DummyGroup" + } + """ diff --git a/features/serializer/property_filter.feature b/features/serializer/property_filter.feature new file mode 100644 index 00000000000..9602ea4b002 --- /dev/null +++ b/features/serializer/property_filter.feature @@ -0,0 +1,394 @@ +Feature: Filter with serialization attributes on items and collections + In order to retrieve, create and update resources or large collection of resources + As a client software developer + I need to retrieve, create and update resources or collections of resources with serialization attributes + + @createSchema + Scenario: Get a collection of resources by attributes id, foo and bar + Given there is "10" dummy property objects + When I send a "GET" request to "/dummy_properties?properties[]=id&properties[]=foo&properties[]=bar" + Then the response status code should be 200 + And the response should be in JSON + And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" + And the JSON should be valid according to this schema: + """ + { + "type": "object", + "properties": { + "@context": {"pattern": "^/contexts/DummyProperty$"}, + "@id": {"pattern": "^/dummy_properties$"}, + "@type": {"pattern": "^hydra:Collection$"}, + "hydra:member": { + "type": "array", + "items": [ + { + "type": "object", + "properties": { + "@id": {}, + "@type": {}, + "id": {}, + "foo": {}, + "bar": {} + }, + "additionalProperties": false, + "required": ["@id", "@type", "id", "foo", "bar"] + }, + { + "type": "object", + "properties": { + "@id": {}, + "@type": {}, + "id": {}, + "foo": {}, + "bar": {} + }, + "additionalProperties": false, + "required": ["@id", "@type", "id", "foo", "bar"] + }, + { + "type": "object", + "properties": { + "@id": {}, + "@type": {}, + "id": {}, + "foo": {}, + "bar": {} + }, + "additionalProperties": false, + "required": ["@id", "@type", "id", "foo", "bar"] + } + ], + "additionalItems": false, + "maxItems": 3, + "minItems": 3 + }, + "hydra:view": { + "type": "object", + "properties": { + "@id": {"pattern": "^/dummy_properties\\?properties%5B%5D=id&properties%5B%5D=foo&properties%5B%5D=bar&page=1$"}, + "@type": {"pattern": "^hydra:PartialCollectionView$"} + } + } + } + } + """ + + Scenario: Get a collection of resources by attributes foo, bar, group.baz and group.qux + When I send a "GET" request to "/dummy_properties?properties[]=foo&properties[]=bar&properties[group][]=baz&properties[group][]=qux" + Then the response status code should be 200 + And the response should be in JSON + And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" + And the JSON should be valid according to this schema: + """ + { + "type": "object", + "properties": { + "@context": {"pattern": "^/contexts/DummyProperty$"}, + "@id": {"pattern": "^/dummy_properties$"}, + "@type": {"pattern": "^hydra:Collection$"}, + "hydra:member": { + "type": "array", + "items": [ + { + "type": "object", + "properties": { + "@id": {}, + "@type": {}, + "foo": {}, + "bar": {}, + "group": { + "type": "object", + "properties": { + "@id": {}, + "@type": {}, + "baz": {} + }, + "additionalProperties": false, + "required": ["@id", "@type", "baz"] + } + }, + "additionalProperties": false, + "required": ["@id", "@type", "foo", "bar"] + }, + { + "type": "object", + "properties": { + "@id": {}, + "@type": {}, + "foo": {}, + "bar": {}, + "group": { + "type": "object", + "properties": { + "@id": {}, + "@type": {}, + "baz": {} + }, + "additionalProperties": false, + "required": ["@id", "@type", "baz"] + } + }, + "additionalProperties": false, + "required": ["@id", "@type", "foo", "bar"] + }, + { + "type": "object", + "properties": { + "@id": {}, + "@type": {}, + "foo": {}, + "bar": {}, + "group": { + "type": "object", + "properties": { + "@id": {}, + "@type": {}, + "baz": {} + }, + "additionalProperties": false, + "required": ["@id", "@type", "baz"] + } + }, + "additionalProperties": false, + "required": ["@id", "@type", "foo", "bar"] + } + ], + "additionalItems": false, + "maxItems": 3, + "minItems": 3 + }, + "hydra:view": { + "type": "object", + "properties": { + "@id": {"pattern": "^/dummy_properties\\?properties%5B%5D=foo&properties%5B%5D=bar&properties%5Bgroup%5D%5B%5D=baz&properties%5Bgroup%5D%5B%5D=qux&page=1$"}, + "@type": {"pattern": "^hydra:PartialCollectionView$"} + } + } + } + } + """ + + Scenario: Get a collection of resources by attributes empty + When I send a "GET" request to "/dummy_properties?properties[]=&properties[group][]=" + Then the response status code should be 200 + And the response should be in JSON + And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" + And the JSON should be valid according to this schema: + """ + { + "type": "object", + "properties": { + "@context": {"pattern": "^/contexts/DummyProperty$"}, + "@id": {"pattern": "^/dummy_properties$"}, + "@type": {"pattern": "^hydra:Collection$"}, + "hydra:member": { + "type": "array", + "items": [ + { + "type": "object", + "properties": { + "@id": {}, + "@type": {}, + "group": { + "type": "object", + "properties": { + "@id": {}, + "@type": {} + }, + "additionalProperties": false, + "required": ["@id", "@type"] + } + }, + "additionalProperties": false, + "required": ["@id", "@type", "group"] + }, + { + "type": "object", + "properties": { + "@id": {}, + "@type": {}, + "group": { + "type": "object", + "properties": { + "@id": {}, + "@type": {} + }, + "additionalProperties": false, + "required": ["@id", "@type"] + } + }, + "additionalProperties": false, + "required": ["@id", "@type", "group"] + }, + { + "type": "object", + "properties": { + "@id": {}, + "@type": {}, + "group": { + "type": "object", + "properties": { + "@id": {}, + "@type": {} + }, + "additionalProperties": false, + "required": ["@id", "@type"] + } + }, + "additionalProperties": false, + "required": ["@id", "@type", "group"] + } + ], + "additionalItems": false, + "maxItems": 3, + "minItems": 3 + }, + "hydra:view": { + "type": "object", + "properties": { + "@id": {"pattern": "^/dummy_properties\\?properties%5B%5D=&properties%5Bgroup%5D%5B%5D=&page=1$"}, + "@type": {"pattern": "^hydra:PartialCollectionView$"} + } + } + } + } + """ + + Scenario: Get a resource by attributes id, foo and bar + When I send a "GET" request to "/dummy_properties/1?properties[]=id&properties[]=foo&properties[]=bar" + Then the response status code should be 200 + And the response should be in JSON + And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" + And the JSON should be valid according to this schema: + """ + { + "type": "object", + "properties": { + "@context": {"pattern": "^/contexts/DummyProperty$"}, + "@id": {"pattern": "^/dummy_properties/1$"}, + "@type": {"pattern": "^DummyProperty$"}, + "id": {}, + "foo": {}, + "bar": {} + }, + "additionalProperties": false, + "required": ["@context", "@id", "@type", "id", "foo", "bar"] + } + """ + + Scenario: Get a resource by attributes foo, bar, group.baz and group.qux + When I send a "GET" request to "/dummy_properties/1?properties[]=foo&properties[]=bar&properties[group][]=baz&properties[group][]=qux" + Then the response status code should be 200 + And the response should be in JSON + And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" + And the JSON should be valid according to this schema: + """ + { + "type": "object", + "properties": { + "@context": {"pattern": "^/contexts/DummyProperty$"}, + "@id": {"pattern": "^/dummy_properties/1$"}, + "@type": {"pattern": "^DummyProperty$"}, + "foo": {}, + "bar": {}, + "group": { + "type": "object", + "properties": { + "@id": {}, + "@type": {}, + "baz": {} + }, + "additionalProperties": false, + "required": ["@id", "@type", "baz"] + } + }, + "additionalProperties": false, + "required": ["@context", "@id", "@type", "foo", "bar", "group"] + } + """ + + Scenario: Get a resource by attributes empty + When I send a "GET" request to "/dummy_properties/1?properties[]=&properties[group][]=" + Then the response status code should be 200 + And the response should be in JSON + And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" + And the JSON should be valid according to this schema: + """ + { + "type": "object", + "properties": { + "@context": {"pattern": "^/contexts/DummyProperty$"}, + "@id": {"pattern": "^/dummy_properties/1$"}, + "@type": {"pattern": "^DummyProperty$"}, + "group": { + "type": "object", + "properties": { + "@id": {}, + "@type": {} + }, + "additionalProperties": false, + "required": ["@id", "@type"] + } + }, + "additionalProperties": false, + "required": ["@context", "@id", "@type", "group"] + } + """ + + Scenario: Create a resource by attributes foo and bar + When I add "Content-Type" header equal to "application/ld+json" + And I send a "POST" request to "/dummy_properties?properties[]=foo&properties[]=bar" with body: + """ + { + "foo": "Foo", + "bar": "Bar" + } + """ + Then the response status code should be 201 + And the response should be in JSON + And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" + And the JSON should be equal to: + """ + { + "@context": "\/contexts\/DummyProperty", + "@id": "\/dummy_properties\/11", + "@type": "DummyProperty", + "foo": "Foo", + "bar": "Bar" + } + """ + + @dropSchema + Scenario: Create a resource by attributes foo, bar, group.foo, group.baz and group.qux + When I add "Content-Type" header equal to "application/ld+json" + And I send a "POST" request to "/dummy_properties?properties[]=foo&properties[]=bar&properties[group][]=foo&properties[group][]=baz&properties[group][]=qux" with body: + """ + { + "foo": "Foo", + "bar": "Bar", + "group": { + "foo": "Foo", + "baz": "Baz", + "qux": "Qux" + } + } + """ + Then the response status code should be 201 + And the response should be in JSON + And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" + And the JSON should be equal to: + """ + { + "@context": "\/contexts\/DummyProperty", + "@id": "\/dummy_properties\/12", + "@type": "DummyProperty", + "foo": "Foo", + "bar": "Bar", + "group": { + "@id": "\/dummy_groups\/11", + "@type": "DummyGroup", + "foo": "Foo", + "baz": null + } + } + """ diff --git a/src/Bridge/Symfony/Bundle/Resources/config/api.xml b/src/Bridge/Symfony/Bundle/Resources/config/api.xml index 938ee90901b..43fde27e12d 100644 --- a/src/Bridge/Symfony/Bundle/Resources/config/api.xml +++ b/src/Bridge/Symfony/Bundle/Resources/config/api.xml @@ -62,6 +62,16 @@ + + + + + + + + + + diff --git a/src/Serializer/AbstractItemNormalizer.php b/src/Serializer/AbstractItemNormalizer.php index 1d9a58bb327..bdf9e49bbfd 100644 --- a/src/Serializer/AbstractItemNormalizer.php +++ b/src/Serializer/AbstractItemNormalizer.php @@ -183,7 +183,7 @@ protected function setAttributeValue($object, $attribute, $value, $format = null $this->setValue( $object, $attribute, - $this->denormalizeRelation($attribute, $propertyMetadata, $className, $value, $format, $context) + $this->denormalizeRelation($attribute, $propertyMetadata, $className, $value, $format, $this->createChildContext($context, $attribute)) ); return; @@ -254,7 +254,7 @@ private function denormalizeCollection(string $attribute, PropertyMetadata $prop ); } - $values[$index] = $this->denormalizeRelation($attribute, $propertyMetadata, $className, $obj, $format, $context); + $values[$index] = $this->denormalizeRelation($attribute, $propertyMetadata, $className, $obj, $format, $this->createChildContext($context, $attribute)); } return $values; @@ -390,7 +390,7 @@ protected function getAttributeValue($object, $attribute, $format = null, array ) { $value = []; foreach ($attributeValue as $index => $obj) { - $value[$index] = $this->normalizeRelation($propertyMetadata, $obj, $className, $format, $context); + $value[$index] = $this->normalizeRelation($propertyMetadata, $obj, $className, $format, $this->createChildContext($context, $attribute)); } return $value; @@ -408,7 +408,7 @@ protected function getAttributeValue($object, $attribute, $format = null, array if (null === $attributeValue && $context['operation_type'] === OperationType::SUBRESOURCE && isset($context['subresource_resources'][$className])) { return $this->iriConverter->getItemIriFromResourceClass($className, $context['subresource_resources'][$className]); } elseif ($attributeValue) { - return $this->normalizeRelation($propertyMetadata, $attributeValue, $className, $format, $context); + return $this->normalizeRelation($propertyMetadata, $attributeValue, $className, $format, $this->createChildContext($context, $attribute)); } } diff --git a/src/Serializer/Filter/FilterInterface.php b/src/Serializer/Filter/FilterInterface.php new file mode 100644 index 00000000000..7bd5a56d94f --- /dev/null +++ b/src/Serializer/Filter/FilterInterface.php @@ -0,0 +1,35 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Core\Serializer\Filter; + +use ApiPlatform\Core\Api\FilterInterface as BaseFilterInterface; +use Symfony\Component\HttpFoundation\Request; + +/** + * Symfony serializer context builder filter interface. + * + * @author Baptiste Meyer + */ +interface FilterInterface extends BaseFilterInterface +{ + /** + * Apply a filter to the serializer context. + * + * @param Request $request + * @param bool $normalization + * @param array $attributes + * @param array $context + */ + public function apply(Request $request, bool $normalization, array $attributes, array &$context); +} diff --git a/src/Serializer/Filter/GroupFilter.php b/src/Serializer/Filter/GroupFilter.php new file mode 100644 index 00000000000..2063b0dd75e --- /dev/null +++ b/src/Serializer/Filter/GroupFilter.php @@ -0,0 +1,63 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Core\Serializer\Filter; + +use Symfony\Component\HttpFoundation\Request; + +/** + * Group filter. + * + * @author Baptiste Meyer + */ +final class GroupFilter implements FilterInterface +{ + private $overrideDefaultGroups; + private $parameterName; + + public function __construct(string $parameterName = 'groups', bool $overrideDefaultGroups = false) + { + $this->overrideDefaultGroups = $overrideDefaultGroups; + $this->parameterName = $parameterName; + } + + /** + * {@inheritdoc} + */ + public function apply(Request $request, bool $normalization, array $attributes, array &$context) + { + if (!is_array($groups = $request->query->get($this->parameterName))) { + return; + } + + if (!$this->overrideDefaultGroups && isset($context['groups'])) { + $groups = array_merge((array) $context['groups'], $groups); + } + + $context['groups'] = $groups; + } + + /** + * {@inheritdoc} + */ + public function getDescription(string $resourceClass): array + { + return [ + $this->parameterName.'[]' => [ + 'property' => null, + 'type' => 'string', + 'required' => false, + ], + ]; + } +} diff --git a/src/Serializer/Filter/PropertyFilter.php b/src/Serializer/Filter/PropertyFilter.php new file mode 100644 index 00000000000..282f815822c --- /dev/null +++ b/src/Serializer/Filter/PropertyFilter.php @@ -0,0 +1,63 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Core\Serializer\Filter; + +use Symfony\Component\HttpFoundation\Request; + +/** + * Property filter. + * + * @author Baptiste Meyer + */ +final class PropertyFilter implements FilterInterface +{ + private $overrideDefaultProperties; + private $parameterName; + + public function __construct(string $parameterName = 'properties', bool $overrideDefaultProperties = false) + { + $this->overrideDefaultProperties = $overrideDefaultProperties; + $this->parameterName = $parameterName; + } + + /** + * {@inheritdoc} + */ + public function apply(Request $request, bool $normalization, array $attributes, array &$context) + { + if (!is_array($properties = $request->query->get($this->parameterName))) { + return; + } + + if (!$this->overrideDefaultProperties && isset($context['attributes'])) { + $properties = array_merge_recursive((array) $context['attributes'], $properties); + } + + $context['attributes'] = $properties; + } + + /** + * {@inheritdoc} + */ + public function getDescription(string $resourceClass): array + { + return [ + $this->parameterName.'[]' => [ + 'property' => null, + 'type' => 'string', + 'required' => false, + ], + ]; + } +} diff --git a/src/Serializer/SerializerFilterContextBuilder.php b/src/Serializer/SerializerFilterContextBuilder.php new file mode 100644 index 00000000000..c74d8ff1ced --- /dev/null +++ b/src/Serializer/SerializerFilterContextBuilder.php @@ -0,0 +1,76 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Core\Serializer; + +use ApiPlatform\Core\Exception\RuntimeException; +use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface; +use ApiPlatform\Core\Serializer\Filter\FilterInterface; +use ApiPlatform\Core\Util\RequestAttributesExtractor; +use Psr\Container\ContainerInterface; +use Symfony\Component\HttpFoundation\Request; + +/** + * {@inheritdoc} + * + * @author Baptiste Meyer + */ +final class SerializerFilterContextBuilder implements SerializerContextBuilderInterface +{ + private $decorated; + private $filterLocator; + private $resourceMetadataFactory; + + public function __construct(ResourceMetadataFactoryInterface $resourceMetadataFactory, ContainerInterface $filterLocator, SerializerContextBuilderInterface $decorated) + { + $this->decorated = $decorated; + $this->filterLocator = $filterLocator; + $this->resourceMetadataFactory = $resourceMetadataFactory; + } + + /** + * {@inheritdoc} + */ + public function createFromRequest(Request $request, bool $normalization, array $attributes = null): array + { + if (null === $attributes && !$attributes = RequestAttributesExtractor::extractAttributes($request)) { + throw new RuntimeException('Request attributes are not valid.'); + } + + $context = $this->decorated->createFromRequest($request, $normalization, $attributes); + $resourceMetadata = $this->resourceMetadataFactory->create($attributes['resource_class']); + + if (isset($attributes['collection_operation_name']) || isset($attributes['subresource_operation_name'])) { + $resourceFilters = $resourceMetadata->getCollectionOperationAttribute( + $attributes['collection_operation_name'] ?? $attributes['subresource_operation_name'], + 'filters', + [], + true + ); + } else { + $resourceFilters = $resourceMetadata->getItemOperationAttribute($attributes['item_operation_name'], 'filters', [], true); + } + + if (!$resourceFilters) { + return $context; + } + + foreach ($resourceFilters as $filterId) { + if ($this->filterLocator->has($filterId) && ($filter = $this->filterLocator->get($filterId)) instanceof FilterInterface) { + $filter->apply($request, $normalization, $attributes, $context); + } + } + + return $context; + } +} diff --git a/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php b/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php index b753881fc3f..e86616a8845 100644 --- a/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php +++ b/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php @@ -385,6 +385,9 @@ private function getContainerBuilderProphecy() 'api_platform.route_name_resolver.cached', 'api_platform.router', 'api_platform.serializer.context_builder', + 'api_platform.serializer.context_builder.filter', + 'api_platform.serializer.property_filter', + 'api_platform.serializer.group_filter', 'api_platform.serializer.normalizer.item', 'api_platform.subresource_data_provider', 'api_platform.swagger.action.ui', diff --git a/tests/Fixtures/TestBundle/Entity/DummyGroup.php b/tests/Fixtures/TestBundle/Entity/DummyGroup.php new file mode 100644 index 00000000000..ce195da656a --- /dev/null +++ b/tests/Fixtures/TestBundle/Entity/DummyGroup.php @@ -0,0 +1,92 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity; + +use ApiPlatform\Core\Annotation\ApiResource; +use Doctrine\ORM\Mapping as ORM; +use Symfony\Component\Serializer\Annotation\Groups; + +/** + * DummyGroup. + * + * @author Baptiste Meyer + * + * @ORM\Entity + * + * @ApiResource(attributes={ + * "normalization_context"={"groups"={"dummy_read"}}, + * "denormalization_context"={"groups"={"dummy_write"}}, + * "filters"={ + * "dummy_group.group", + * "dummy_group.override_group" + * } + * }) + */ +class DummyGroup +{ + /** + * @var int + * + * @ORM\Id + * @ORM\Column(type="integer") + * @ORM\GeneratedValue(strategy="AUTO") + * + * @Groups({"dummy", "dummy_read", "dummy_id"}) + */ + private $id; + + /** + * @var string + * + * @ORM\Column(nullable=true) + * + * @Groups({"dummy", "dummy_read", "dummy_write", "dummy_foo"}) + */ + public $foo; + + /** + * @var string + * + * @ORM\Column(nullable=true) + * + * @Groups({"dummy", "dummy_read", "dummy_write", "dummy_bar"}) + */ + public $bar; + + /** + * @var string + * + * @ORM\Column(nullable=true) + * + * @Groups({"dummy", "dummy_read", "dummy_baz"}) + */ + public $baz; + + /** + * @var string + * + * @ORM\Column(nullable=true) + * + * @Groups({"dummy", "dummy_write", "dummy_qux"}) + */ + public $qux; + + /** + * @return int + */ + public function getId() + { + return $this->id; + } +} diff --git a/tests/Fixtures/TestBundle/Entity/DummyProperty.php b/tests/Fixtures/TestBundle/Entity/DummyProperty.php new file mode 100644 index 00000000000..09edcf79cd7 --- /dev/null +++ b/tests/Fixtures/TestBundle/Entity/DummyProperty.php @@ -0,0 +1,91 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity; + +use ApiPlatform\Core\Annotation\ApiResource; +use Doctrine\ORM\Mapping as ORM; +use Symfony\Component\Serializer\Annotation\Groups; + +/** + * DummyProperty. + * + * @author Baptiste Meyer + * + * @ORM\Entity + * + * @ApiResource(attributes={ + * "normalization_context"={"groups"={"dummy_read"}}, + * "denormalization_context"={"groups"={"dummy_write"}}, + * "filters"={ + * "dummy_property.property" + * } + * }) + */ +class DummyProperty +{ + /** + * @var int + * + * @ORM\Id + * @ORM\Column(type="integer") + * @ORM\GeneratedValue(strategy="AUTO") + * + * @Groups("dummy_read") + */ + private $id; + + /** + * @var string + * + * @ORM\Column(nullable=true) + * + * @Groups({"dummy_read", "dummy_write"}) + */ + public $foo; + + /** + * @var string + * + * @ORM\Column(nullable=true) + * + * @Groups({"dummy_read", "dummy_write"}) + */ + public $bar; + + /** + * @var string + * + * @ORM\Column(nullable=true) + * + * @Groups({"dummy_read", "dummy_write"}) + */ + public $baz; + + /** + * @var DummyGroup + * + * @ORM\ManyToOne(targetEntity=DummyGroup::class, cascade={"persist"}) + * + * @Groups({"dummy_read", "dummy_write"}) + */ + public $group; + + /** + * @return int + */ + public function getId() + { + return $this->id; + } +} diff --git a/tests/Fixtures/app/config/config.yml b/tests/Fixtures/app/config/config.yml index 6bfe622d14e..af4cabe0174 100644 --- a/tests/Fixtures/app/config/config.yml +++ b/tests/Fixtures/app/config/config.yml @@ -136,6 +136,19 @@ services: arguments: [ { 'colors.prop': 'ipartial' } ] tags: [ { name: 'api_platform.filter', id: 'dummy_car_colors.search_filter' } ] + app.entity.filter.dummy_property.property: + parent: 'api_platform.serializer.property_filter' + tags: [ { name: 'api_platform.filter', id: 'dummy_property.property' } ] + + app.entity.filter.dummy_group.group: + parent: 'api_platform.serializer.group_filter' + tags: [ { name: 'api_platform.filter', id: 'dummy_group.group' } ] + + app.entity.filter.dummy_group.override_group: + parent: 'api_platform.serializer.group_filter' + arguments: [ 'override_groups', true ] + tags: [ { name: 'api_platform.filter', id: 'dummy_group.override_group' } ] + app.related_dummy_resource.search_filter: parent: 'api_platform.doctrine.orm.search_filter' arguments: [ { 'relatedToDummyFriend.dummyFriend': 'exact', 'name': 'partial' } ] diff --git a/tests/Serializer/Filter/GroupFilterTest.php b/tests/Serializer/Filter/GroupFilterTest.php new file mode 100644 index 00000000000..6855123a2cf --- /dev/null +++ b/tests/Serializer/Filter/GroupFilterTest.php @@ -0,0 +1,92 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Core\Tests\Serializer\Filter; + +use ApiPlatform\Core\Serializer\Filter\GroupFilter; +use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\DummyGroup; +use Symfony\Component\HttpFoundation\Request; + +/** + * @author Baptiste Meyer + */ +class GroupFilterTest extends \PHPUnit_Framework_TestCase +{ + public function testApply() + { + $request = new Request(['groups' => ['foo', 'bar', 'baz']]); + $context = ['groups' => ['foo', 'qux']]; + + $groupFilter = new GroupFilter(); + $groupFilter->apply($request, true, [], $context); + + $this->assertEquals(['groups' => ['foo', 'qux', 'foo', 'bar', 'baz']], $context); + } + + public function testApplyWithOverriding() + { + $request = new Request(['custom_groups' => ['foo', 'bar', 'baz']]); + $context = ['groups' => ['foo', 'qux']]; + + $groupFilter = new GroupFilter('custom_groups', true); + $groupFilter->apply($request, false, [], $context); + + $this->assertEquals(['groups' => ['foo', 'bar', 'baz']], $context); + } + + public function testApplyWithoutGroupsInRequest() + { + $context = ['groups' => ['foo', 'bar']]; + + $groupFilter = new GroupFilter(); + $groupFilter->apply(new Request(), false, [], $context); + + $this->assertEquals(['groups' => ['foo', 'bar']], $context); + } + + public function testApplyWithInvalidGroupsInRequest() + { + $request = new Request(['groups' => 'foo,bar,baz']); + $context = ['groups' => ['foo', 'bar']]; + + $groupFilter = new GroupFilter(); + $groupFilter->apply($request, true, [], $context); + + $this->assertEquals(['groups' => ['foo', 'bar']], $context); + } + + public function testApplyWithInvalidGroupsInContext() + { + $request = new Request(['custom_groups' => ['foo', 'bar', 'baz']]); + $context = ['groups' => 'qux']; + + $groupFilter = new GroupFilter('custom_groups'); + $groupFilter->apply($request, true, [], $context); + + $this->assertEquals(['groups' => ['qux', 'foo', 'bar', 'baz']], $context); + } + + public function testGetDescription() + { + $groupFilter = new GroupFilter('custom_groups'); + $expectedDescription = [ + 'custom_groups[]' => [ + 'property' => null, + 'type' => 'string', + 'required' => false, + ], + ]; + + $this->assertEquals($expectedDescription, $groupFilter->getDescription(DummyGroup::class)); + } +} diff --git a/tests/Serializer/Filter/PropertyFilterTest.php b/tests/Serializer/Filter/PropertyFilterTest.php new file mode 100644 index 00000000000..abcc6875590 --- /dev/null +++ b/tests/Serializer/Filter/PropertyFilterTest.php @@ -0,0 +1,81 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Core\Tests\Serializer\Filter; + +use ApiPlatform\Core\Serializer\Filter\PropertyFilter; +use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\DummyProperty; +use Symfony\Component\HttpFoundation\Request; + +/** + * @author Baptiste Meyer + */ +class PropertyFilterTest extends \PHPUnit_Framework_TestCase +{ + public function testApply() + { + $request = new Request(['properties' => ['foo', 'bar', 'baz']]); + $context = ['attributes' => ['foo', 'qux']]; + + $propertyFilter = new PropertyFilter(); + $propertyFilter->apply($request, true, [], $context); + + $this->assertEquals(['attributes' => ['foo', 'qux', 'foo', 'bar', 'baz']], $context); + } + + public function testApplyWithOverriding() + { + $request = new Request(['custom_properties' => ['foo', 'bar', 'baz']]); + $context = ['attributes' => ['foo', 'qux']]; + + $propertyFilter = new PropertyFilter('custom_properties', true); + $propertyFilter->apply($request, false, [], $context); + + $this->assertEquals(['attributes' => ['foo', 'bar', 'baz']], $context); + } + + public function testApplyWithoutPropertiesInRequest() + { + $context = ['attributes' => ['foo', 'bar']]; + + $propertyFilter = new PropertyFilter(); + $propertyFilter->apply(new Request(), false, [], $context); + + $this->assertEquals(['attributes' => ['foo', 'bar']], $context); + } + + public function testApplyWithInvalidPropertiesInRequest() + { + $request = new Request(['properties' => 'foo,bar,baz']); + $context = ['attributes' => ['foo', 'bar']]; + + $propertyFilter = new PropertyFilter(); + $propertyFilter->apply($request, true, [], $context); + + $this->assertEquals(['attributes' => ['foo', 'bar']], $context); + } + + public function testGetDescription() + { + $propertyFilter = new PropertyFilter('custom_properties'); + $expectedDescription = [ + 'custom_properties[]' => [ + 'property' => null, + 'type' => 'string', + 'required' => false, + ], + ]; + + $this->assertEquals($expectedDescription, $propertyFilter->getDescription(DummyProperty::class)); + } +} diff --git a/tests/Serializer/SerializerFilterContextBuilderTest.php b/tests/Serializer/SerializerFilterContextBuilderTest.php new file mode 100644 index 00000000000..0fd6efd9252 --- /dev/null +++ b/tests/Serializer/SerializerFilterContextBuilderTest.php @@ -0,0 +1,204 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Core\Tests\Serializer; + +use ApiPlatform\Core\Api\FilterInterface; +use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface; +use ApiPlatform\Core\Metadata\Resource\ResourceMetadata; +use ApiPlatform\Core\Serializer\Filter\FilterInterface as SerializerFilterInterface; +use ApiPlatform\Core\Serializer\SerializerContextBuilderInterface; +use ApiPlatform\Core\Serializer\SerializerFilterContextBuilder; +use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\DummyGroup; +use Psr\Container\ContainerInterface; +use Symfony\Component\HttpFoundation\Request; + +/** + * @author Baptiste Meyer + */ +class SerializerFilterContextBuilderTest extends \PHPUnit_Framework_TestCase +{ + public function testCreateFromRequestWithCollectionOperation() + { + $request = new Request(); + + $attributes = [ + 'resource_class' => DummyGroup::class, + 'collection_operation_name' => 'get', + ]; + + $resourceMetadata = new ResourceMetadata( + null, + null, + null, + null, + ['get' => ['filters' => ['dummy_group.group', 'dummy_group.search', 'dummy_group.nonexistent']]] + ); + + $decoratedProphecy = $this->prophesize(SerializerContextBuilderInterface::class); + $decoratedProphecy->createFromRequest($request, true, $attributes)->willReturn([])->shouldBeCalled(); + + $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class); + $resourceMetadataFactoryProphecy->create(DummyGroup::class)->willReturn($resourceMetadata)->shouldBeCalled(); + + $dummyGroupGroupFilterProphecy = $this->prophesize(SerializerFilterInterface::class); + $dummyGroupGroupFilterProphecy->apply($request, true, $attributes, [])->shouldBeCalled(); + + $dummyGroupSearchFilterProphecy = $this->prophesize(FilterInterface::class); + + $filterLocatorProphecy = $this->prophesize(ContainerInterface::class); + $filterLocatorProphecy->has('dummy_group.group')->willReturn(true)->shouldBeCalled(); + $filterLocatorProphecy->get('dummy_group.group')->willReturn($dummyGroupGroupFilterProphecy->reveal())->shouldBeCalled(); + $filterLocatorProphecy->has('dummy_group.search')->willReturn(true)->shouldBeCalled(); + $filterLocatorProphecy->get('dummy_group.search')->willReturn($dummyGroupSearchFilterProphecy->reveal())->shouldBeCalled(); + $filterLocatorProphecy->has('dummy_group.nonexistent')->willReturn(false)->shouldBeCalled(); + + $serializerContextBuilderFilter = new SerializerFilterContextBuilder($resourceMetadataFactoryProphecy->reveal(), $filterLocatorProphecy->reveal(), $decoratedProphecy->reveal()); + $context = $serializerContextBuilderFilter->createFromRequest($request, true, $attributes); + + $this->assertInternalType('array', $context); + } + + public function testCreateFromRequestWithItemOperation() + { + $request = new Request(); + + $attributes = [ + 'resource_class' => DummyGroup::class, + 'item_operation_name' => 'put', + ]; + + $resourceMetadata = new ResourceMetadata( + null, + null, + null, + ['put' => ['filters' => ['dummy_group.group', 'dummy_group.search', 'dummy_group.nonexistent']]], + null + ); + + $decoratedProphecy = $this->prophesize(SerializerContextBuilderInterface::class); + $decoratedProphecy->createFromRequest($request, true, $attributes)->willReturn([])->shouldBeCalled(); + + $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class); + $resourceMetadataFactoryProphecy->create(DummyGroup::class)->willReturn($resourceMetadata)->shouldBeCalled(); + + $dummyGroupGroupFilterProphecy = $this->prophesize(SerializerFilterInterface::class); + $dummyGroupGroupFilterProphecy->apply($request, true, $attributes, [])->shouldBeCalled(); + + $dummyGroupSearchFilterProphecy = $this->prophesize(FilterInterface::class); + + $filterLocatorProphecy = $this->prophesize(ContainerInterface::class); + $filterLocatorProphecy->has('dummy_group.group')->willReturn(true)->shouldBeCalled(); + $filterLocatorProphecy->get('dummy_group.group')->willReturn($dummyGroupGroupFilterProphecy->reveal())->shouldBeCalled(); + $filterLocatorProphecy->has('dummy_group.search')->willReturn(true)->shouldBeCalled(); + $filterLocatorProphecy->get('dummy_group.search')->willReturn($dummyGroupSearchFilterProphecy->reveal())->shouldBeCalled(); + $filterLocatorProphecy->has('dummy_group.nonexistent')->willReturn(false)->shouldBeCalled(); + + $serializerContextBuilderFilter = new SerializerFilterContextBuilder($resourceMetadataFactoryProphecy->reveal(), $filterLocatorProphecy->reveal(), $decoratedProphecy->reveal()); + $context = $serializerContextBuilderFilter->createFromRequest($request, true, $attributes); + + $this->assertInternalType('array', $context); + } + + public function testCreateFromRequestWithoutFilters() + { + $request = new Request(); + + $attributes = [ + 'resource_class' => DummyGroup::class, + 'collection_operation_name' => 'get', + ]; + + $resourceMetadata = new ResourceMetadata( + null, + null, + null, + null, + ['get' => []] + ); + + $decoratedProphecy = $this->prophesize(SerializerContextBuilderInterface::class); + $decoratedProphecy->createFromRequest($request, false, $attributes)->willReturn([])->shouldBeCalled(); + + $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class); + $resourceMetadataFactoryProphecy->create(DummyGroup::class)->willReturn($resourceMetadata)->shouldBeCalled(); + + $filterLocatorProphecy = $this->prophesize(ContainerInterface::class); + + $serializerContextBuilderFilter = new SerializerFilterContextBuilder($resourceMetadataFactoryProphecy->reveal(), $filterLocatorProphecy->reveal(), $decoratedProphecy->reveal()); + $context = $serializerContextBuilderFilter->createFromRequest($request, false, $attributes); + + $this->assertInternalType('array', $context); + } + + public function testCreateFromRequestWithoutAttributes() + { + $request = new Request([], [], [ + '_api_resource_class' => DummyGroup::class, + '_api_collection_operation_name' => 'get', + ]); + + $attributes = [ + 'resource_class' => DummyGroup::class, + 'collection_operation_name' => 'get', + 'receive' => true, + ]; + + $resourceMetadata = new ResourceMetadata( + null, + null, + null, + null, + ['get' => ['filters' => ['dummy_group.group', 'dummy_group.search', 'dummy_group.nonexistent']]] + ); + + $decoratedProphecy = $this->prophesize(SerializerContextBuilderInterface::class); + $decoratedProphecy->createFromRequest($request, true, $attributes)->willReturn([])->shouldBeCalled(); + + $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class); + $resourceMetadataFactoryProphecy->create(DummyGroup::class)->willReturn($resourceMetadata)->shouldBeCalled(); + + $dummyGroupGroupFilterProphecy = $this->prophesize(SerializerFilterInterface::class); + $dummyGroupGroupFilterProphecy->apply($request, true, $attributes, [])->shouldBeCalled(); + + $dummyGroupSearchFilterProphecy = $this->prophesize(FilterInterface::class); + + $filterLocatorProphecy = $this->prophesize(ContainerInterface::class); + $filterLocatorProphecy->has('dummy_group.group')->willReturn(true)->shouldBeCalled(); + $filterLocatorProphecy->get('dummy_group.group')->willReturn($dummyGroupGroupFilterProphecy->reveal())->shouldBeCalled(); + $filterLocatorProphecy->has('dummy_group.search')->willReturn(true)->shouldBeCalled(); + $filterLocatorProphecy->get('dummy_group.search')->willReturn($dummyGroupSearchFilterProphecy->reveal())->shouldBeCalled(); + $filterLocatorProphecy->has('dummy_group.nonexistent')->willReturn(false)->shouldBeCalled(); + + $serializerContextBuilderFilter = new SerializerFilterContextBuilder($resourceMetadataFactoryProphecy->reveal(), $filterLocatorProphecy->reveal(), $decoratedProphecy->reveal()); + $context = $serializerContextBuilderFilter->createFromRequest($request, true, null); + + $this->assertInternalType('array', $context); + } + + /** + * @expectedException \ApiPlatform\Core\Exception\RuntimeException + * @expectedExceptionMessage Request attributes are not valid. + */ + public function testCreateFromRequestThrowsExceptionWithoutAttributesAndRequestAttributes() + { + $request = new Request(); + + $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class); + $filterLocatorProphecy = $this->prophesize(ContainerInterface::class); + $decoratedProphecy = $this->prophesize(SerializerContextBuilderInterface::class); + + $serializerContextBuilderFilter = new SerializerFilterContextBuilder($resourceMetadataFactoryProphecy->reveal(), $filterLocatorProphecy->reveal(), $decoratedProphecy->reveal()); + $serializerContextBuilderFilter->createFromRequest($request, true, null); + } +}