From ae4cb8e5d953000659c665ee5ab11671a106ebf9 Mon Sep 17 00:00:00 2001 From: Xenofon Spafaridis Date: Sat, 8 Oct 2016 23:34:24 +0300 Subject: [PATCH] Write phpunit tests for handlePost --- src/Controller/Controller.php | 7 +- src/Controller/Helper/RequestBodyQueue.php | 10 +- src/Controller/Post.php | 46 +- src/Directive/FilterAttribute.php | 2 +- tests/APP/DataSource/MemoryDataSource.php | 2 +- tests/APP/Models/Tag.php | 17 +- tests/src/Controller/PostTest.php | 529 +++++++++++++++++++++ tests/src/Directive/FilterTest.php | 2 +- 8 files changed, 594 insertions(+), 21 deletions(-) create mode 100644 tests/src/Controller/PostTest.php diff --git a/src/Controller/Controller.php b/src/Controller/Controller.php index 9f9bf83..e2fb87e 100644 --- a/src/Controller/Controller.php +++ b/src/Controller/Controller.php @@ -217,7 +217,6 @@ function ($d) { $model ); - if ($parsed !== null) { //overwrite if ($overwrite @@ -238,6 +237,12 @@ function ($d) { return $directives; } + /** + * @param $object + * @param ISource|null $source + * @param string[] ...$properties + * @throws MissingParametersException + */ public static function requireProperties( $object, ISource $source = null, diff --git a/src/Controller/Helper/RequestBodyQueue.php b/src/Controller/Helper/RequestBodyQueue.php index 7f639a6..d14edbd 100644 --- a/src/Controller/Helper/RequestBodyQueue.php +++ b/src/Controller/Helper/RequestBodyQueue.php @@ -37,7 +37,6 @@ trait RequestBodyQueue { /** - * What is resource ? * @todo * @param \stdClass $resource Primary data resource */ @@ -47,8 +46,7 @@ public static function handleResource( ResourceModel $model, ValidationModel $validationModel, array $validationCallbacks = [] - ) - { + ) { //Fetch request attributes $requestAttributes = $resource->attributes ?? new \stdClass(); $requestRelationships = $resource->relationships ?? new \stdClass(); @@ -164,7 +162,7 @@ function (\stdClass $p) { //Convert to array $tempIds = ( - is_array($rValue) + is_array($rValue) ? $rValue : [$rValue] ); @@ -204,7 +202,8 @@ function (\stdClass $p) { $callback( $resource, $parsedAttributes, //parsed - $parsedRelationships //parsed + $parsedRelationships, //parsed + $source ); } @@ -214,4 +213,3 @@ function (\stdClass $p) { ); } } - diff --git a/src/Controller/Post.php b/src/Controller/Post.php index 91091b2..02b4214 100644 --- a/src/Controller/Post.php +++ b/src/Controller/Post.php @@ -19,6 +19,7 @@ use Phramework\Exceptions\ForbiddenException; use Phramework\Exceptions\IncorrectParameterException; +use Phramework\Exceptions\MissingParametersException; use Phramework\Exceptions\RequestException; use Phramework\Exceptions\ServerException; use Phramework\Exceptions\Source\Pointer; @@ -43,19 +44,29 @@ trait Post { use RequestBodyQueue; - //prototype /** * Handle HTTP POST request method to create new resources * @param ServerRequestInterface $request * @param ResponseInterface $response * @param ResourceModel $model - * @param array $validationCallbacks - * @param callable|null $viewCallback + * @param array $validationCallbacks function of + * - \stdClass $resource + * - \stdClass $parsedAttributes + * - \stdClass $parsedRelationships + * - ISource $source + * returning void + * @param callable|null $viewCallback function of + * - ServerRequestInterface $request, + * - ResponseInterface $response, + * - string[] $ids + * - returning ResponseInterface * @param int|null $bulkLimit * @param array $directives * @return ResponseInterface * @throws ForbiddenException * @throws RequestException + * @throws MissingParametersException + * @throws ServerException */ public static function handlePost( ServerRequestInterface $request, @@ -67,10 +78,12 @@ public static function handlePost( array $directives = [] ) : ResponseInterface { //todo figure out a permanent solution to have body as object instead of array, for every framework - $body = json_decode(json_encode($request->getParsedBody())); + $body = json_decode(json_encode($request->getParsedBody())) ?? new \stdClass(); + + Controller::requireProperties($body, new Pointer('/'), 'data'); //Access request body primary data - $data = $body->data ?? [new \stdClass()]; + $data = $body->data; /** * @var bool @@ -88,7 +101,7 @@ public static function handlePost( //check bulk limit if ($bulkLimit !== null && count($data) > $bulkLimit) { throw new RequestException(sprintf( - 'Number of batch requests is exceeding the maximum of %s', + 'Number of bulk requests is exceeding the maximum of %s', $bulkLimit )); } @@ -143,6 +156,7 @@ public static function handlePost( $item = static::handleResource( $resource, $source, + $model, $validationModel, $validationCallbacks ); @@ -233,13 +247,25 @@ public static function handlePost( ); } - /*if (count($ids) === 1) { + return Post::defaultViewCallback( + $request, + $response, + $ids + ); + } + + public static function defaultViewCallback( + ServerRequestInterface $request, + ResponseInterface $response, + array $ids + ) : ResponseInterface { + if (count($ids) === 1) { //Prepare response with 201 Created status code - return Response::created( + $response = Response::created( $response, - 'link' . $ids[0] // location + 'link/' . $ids[0] // location //todo ); - }*/ //see https://stackoverflow.com/questions/11309444/can-the-location-header-be-used-for-multiple-resource-locations-in-a-201-created + } //see https://stackoverflow.com/questions/11309444/can-the-location-header-be-used-for-multiple-resource-locations-in-a-201-created //Return 204 No Content return Response::noContent($response); diff --git a/src/Directive/FilterAttribute.php b/src/Directive/FilterAttribute.php index de94d7a..48e849a 100644 --- a/src/Directive/FilterAttribute.php +++ b/src/Directive/FilterAttribute.php @@ -118,7 +118,7 @@ public static function parse($filterKey, $filterValue) } //@todo is this required? - $singleFilterValue = urldecode($singleFilterValue); + $singleFilterValue = urldecode((string) $singleFilterValue); list($operator, $operand) = Operator::parse($singleFilterValue); diff --git a/tests/APP/DataSource/MemoryDataSource.php b/tests/APP/DataSource/MemoryDataSource.php index 58c81cb..b2eff56 100644 --- a/tests/APP/DataSource/MemoryDataSource.php +++ b/tests/APP/DataSource/MemoryDataSource.php @@ -159,7 +159,7 @@ public function post( if (!property_exists($attributes, $idAttribute)) { //generate an id - $attributes->{$idAttribute} = md5(mt_rand()); + $attributes->{$idAttribute} = md5((string) mt_rand()); } $table = $this->resourceModel->getVariable('table'); diff --git a/tests/APP/Models/Tag.php b/tests/APP/Models/Tag.php index 7c16b01..dd9d85c 100644 --- a/tests/APP/Models/Tag.php +++ b/tests/APP/Models/Tag.php @@ -22,6 +22,9 @@ use Phramework\JSONAPI\ResourceModel; use Phramework\JSONAPI\Model; use Phramework\JSONAPI\ModelTrait; +use Phramework\JSONAPI\ValidationModel; +use Phramework\Validate\ObjectValidator; +use Phramework\Validate\StringValidator; /** * @since 3.0.0 @@ -38,7 +41,19 @@ class Tag extends Model protected static function defineModel() : ResourceModel { $model = (new ResourceModel('tag', new MemoryDataSource())) - ->addVariable('table', 'tag'); + ->addVariable('table', 'tag') + ->setValidationModel( + new ValidationModel( + new ObjectValidator( + (object) [ + 'name' => new StringValidator(2, 10) + ], + ['name'], + false + ) + ), + 'POST' + ); return $model; } diff --git a/tests/src/Controller/PostTest.php b/tests/src/Controller/PostTest.php new file mode 100644 index 0000000..e3ea517 --- /dev/null +++ b/tests/src/Controller/PostTest.php @@ -0,0 +1,529 @@ + + * Using \Phramework\JSONAPI\APP\Models\Tag model for tests + */ +class PostTest extends \PHPUnit_Framework_TestCase +{ + use Post; + + /** + * @covers ::handlePost + */ + public function testHandlePost() + { + $request = (new ServerRequest()) + ->withParsedBody((object) [ + 'data' => (object) [ + 'type' => Tag::getResourceType(), + 'attributes' => (object) [ + 'name' => 'aaaaa' + ] + ] + ]); + + $response = $this->handlePost( + $request, + new Response(), + Tag::getResourceModel() + ); + + $this->assertSame( + 204, + $response->getStatusCode() + ); + + $this->markTestIncomplete('test actual resource created'); + $this->markTestIncomplete('test headers'); + $this->markTestIncomplete('test body'); + } + + /* + * Missing + */ + + /** + * @covers ::handlePost + * @group missing + */ + public function testMissingPrimaryData() + { + $request = (new ServerRequest()); + + $this->expectMissing( + $request, + ['data'], + '/' + ); + } + + /** + * @covers ::handlePost + * @group missing + */ + public function testMissingType() + { + $request = (new ServerRequest()) + ->withParsedBody((object) [ + 'data' => (object) [ + ] + ]); + + $this->expectMissing( + $request, + ['type'], + '/data' + ); + } + + /** + * Expect exception with missing /data/attributes/name since its required + * @covers ::handlePost + * @group missing + */ + public function testMissingAttributes() + { + $request = (new ServerRequest()) + ->withParsedBody((object) [ + 'data' => (object) [ + 'type' => Tag::getResourceType() + ] + ]); + + $this->expectMissing( + $request, + ['name'], + '/data/attributes' + ); + } + + /* + * Test bulk + */ + + /** + * @covers ::handlePost + * @group bulk + */ + public function testBulk() + { + $request = (new ServerRequest()) + ->withParsedBody((object) [ + 'data' => [ + (object) [ + 'type' => Tag::getResourceType(), + 'attributes' => (object) [ + 'name' => 'abcd' + ] + ], + (object) [ + 'type' => Tag::getResourceType(), + 'attributes' => (object) [ + 'name' => 'abcdef' + ] + ], + ] + ]); + + $response = $this->handlePost( + $request, + new Response(), + Tag::getResourceModel(), + [], + null, + 2 + ); + + $this->assertSame( + 204, + $response->getStatusCode() + ); + } + + /** + * Expect exception since 2 resources are given with bulk limit of 1 + * also expect exception message to contain word bulk + * @covers ::handlePost + * @expectedException \Phramework\Exceptions\RequestException + * @expectedExceptionCode 400 + * @expectedExceptionMessageRegExp /bulk/ + * @group bulk + */ + public function testBulkMaximum() + { + $request = (new ServerRequest()) + ->withParsedBody((object) [ + 'data' => [ + (object) [ + 'type' => Tag::getResourceType() + ], + (object) [ + 'type' => Tag::getResourceType() + ], + ] + ]); + + $response = $this->handlePost( + $request, + new Response(), + Tag::getResourceModel(), + [], + null, + 1 //Set bulk limit of 1 + ); + } + + /* + * Incorrect parameters + */ + + /** + * @covers ::handlePost + * @group incorrect + */ + public function testIncorrectAttributes() + { + $request = (new ServerRequest()) + ->withParsedBody((object) [ + 'data' => (object) [ + 'type' => Tag::getResourceType(), + 'attributes' => (object) [ + 'name' => '' //since expecting 2 to 10 + ] + ] + ]); + + try { + $response = $this->handlePost( + $request, + new Response(), + Tag::getResourceModel() + ); + } catch (IncorrectParametersException $e) { + $this->assertCount( + 1, + $e->getExceptions() + ); + + /** + * @var IncorrectParameterException + */ + $e = $e->getExceptions()[0]; + + $this->assertInstanceOf( + IncorrectParameterException::class, + $e + ); + + $this->assertSame( + 'minLength', + $e->getFailure() + ); + + $this->assertEquals( + new Pointer('/data/attributes/name'), + $e->getSource() + ); + } catch (\Exception $e) { + $this->fail('Expected Exception has not been raised'); + } + } + + /** + * @covers ::handlePost + * @group incorrect + */ + public function testIncorrectRelationship() + { + $this->markTestIncomplete(); + } + + /* + * Validation callback + */ + + /** + * This test will use pass a validation callback in order to have additional checks + * @covers ::handlePost + * @group validationCallbacks + */ + public function testValidationCallbacksAdditionalException() + { + $name = 'aaaaa'; + + $request = $this->getValidTagRequest($name); + + try { + + $response = $this->handlePost( + $request, + new Response(), + Tag::getResourceModel(), + [ + function ( + \stdClass $resource, + \stdClass $parsedAttributes, + \stdClass $parsedRelationships, + ISource $source + ) use ($name) { + (new StringValidator()) + ->setNot( + (new StringValidator()) + ->setEnum([$name]) + ) + ->setSource(new Pointer( + $source->getPath() . '/attributes/name' + )) + ->parse($parsedAttributes->name); + } + ] + ); + } catch (IncorrectParameterException $e) { + $this->assertInstanceOf( + IncorrectParameterException::class, + $e + ); + + $this->assertSame( + 'not', + $e->getFailure() + ); + + $this->assertEquals( + new Pointer('/data/attributes/name'), + $e->getSource() + ); + } catch (\Exception $e) { + $this->fail('Expected Exception has not been raised'); + } + } + + /** + * This test will use pass a validation callback in order to modify attributes + * @covers ::handlePost + * @group validationCallbacks + */ + public function testValidationCallbacksModifyAttributes() + { + $name = 'aaaaa'; + $newName = str_repeat($name, 2); + + $request = $this->getValidTagRequest($name); + + $unit = $this; + + $response = $this->handlePost( + $request, + new Response(), + Tag::getResourceModel(), + [ + function ( + \stdClass $resource, + \stdClass &$parsedAttributes, + \stdClass $parsedRelationships, + ISource $source + ) use ($newName) { + $parsedAttributes->name = $newName; + } + ], + function ( + ServerRequestInterface $request, + ResponseInterface $response, + array $ids + ) use ($unit, $newName) : ResponseInterface { + $data = Tag::getById($ids[0]); + + $unit->assertSame( + $newName, + $data->attributes->name, + 'Expect inserted name to have same value with modified instead of original' + ); + + return Post::defaultViewCallback( + $request, + $response, + $ids + ); + } + ); + } + + /* + * View callback + */ + + /** + * This test will use pass a viewCallback in order to have a modified response + * It will also ensure that status, headers and body can be modified + * Additionally it will check the structure of body if it's identical to inserted resource + * @covers ::handlePost + */ + public function testViewCallback() + { + $name = 'aaaaa'; + + $request = $this->getValidTagRequest($name); + + $unit = $this; + + $response = $this->handlePost( + $request, + new Response(), + Tag::getResourceModel(), + [], + function ( + ServerRequestInterface $request, + ResponseInterface $response, + array $ids + ) use ($unit) : ResponseInterface { + $unit->assertCount( + 1, + $ids + ); + + $data = Tag::getById($ids[0]); + + $response = Controller::viewData( + $response, + $data + ); + + $response = $response + ->withStatus(203) + ->withAddedHeader( + 'x-phramework', $ids[0] + ); + + return $response; + } //set viewCallback + ); + + $this->assertSame( + 203, + $response->getStatusCode() + ); + + $this->assertTrue( + $response->hasHeader('x-phramework') + ); + + $object = json_decode( + $response->getBody()->__toString() + ); + + /* + * Test inserted resource structure + */ + + $validate = (new ObjectValidator( + (object) [ + 'data' => new ObjectValidator( + (object) [ + 'name' => (new StringValidator()) + ->setEnum([$name]) + ], + ['type', 'attributes'] + ) + ], + ['data'] + ))->validate($object); + + $this->assertTrue( + $validate->status + ); + } + + + /* + * Helper methods area + */ + + /** + * Helper method to assert missing parameters + * @param ServerRequestInterface $request + * @param array $missingParameters + * @param string $pointerPath + */ + private function expectMissing( + ServerRequestInterface $request, + array $missingParameters, + string $pointerPath + ) { + try { + $response = $this->handlePost( + $request, + new Response(), + Tag::getResourceModel() + ); + } catch (MissingParametersException $e) { + $this->assertEquals( + $missingParameters, + $e->getParameters() + ); + + $this->assertEquals( + new Pointer($pointerPath), + $e->getSource() + ); + } catch (\Exception $e) { + $this->fail('Expected Exception has not been raised'); + } + } + + private function getValidTagRequest(string $name) : RequestInterface + { + $request = (new ServerRequest()) + ->withParsedBody((object) [ + 'data' => (object) [ + 'type' => Tag::getResourceType(), + 'attributes' => (object) [ + 'name' => $name + ] + ] + ]); + + return $request; + } +} diff --git a/tests/src/Directive/FilterTest.php b/tests/src/Directive/FilterTest.php index 451bb04..6337ad3 100644 --- a/tests/src/Directive/FilterTest.php +++ b/tests/src/Directive/FilterTest.php @@ -374,7 +374,7 @@ public function testParseFromRequestFailureNotAllowedAttribute() Filter::parseFromRequest( $this->request->withQueryParams([ 'filter' => [ - 'not-found' => 1 + 'not-found' => '1' ] ]), $this->articleModel