From 748aff39f4ce037dc8ac0dae18340c296f8c6d4e Mon Sep 17 00:00:00 2001 From: Xenofon Spafaridis Date: Fri, 7 Oct 2016 00:55:33 +0300 Subject: [PATCH 1/4] Complete implementation of handlePost --- src/Controller/Helper/RequestBodyQueue.php | 181 +++++++++++++++++++-- src/Controller/Post.php | 123 +++++++++----- src/Relationship.php | 5 +- 3 files changed, 254 insertions(+), 55 deletions(-) diff --git a/src/Controller/Helper/RequestBodyQueue.php b/src/Controller/Helper/RequestBodyQueue.php index f2a2dfa..7f639a6 100644 --- a/src/Controller/Helper/RequestBodyQueue.php +++ b/src/Controller/Helper/RequestBodyQueue.php @@ -17,7 +17,17 @@ */ namespace Phramework\JSONAPI\Controller\Helper; +use Phramework\Exceptions\MissingParametersException; +use Phramework\Exceptions\NotFoundException; +use Phramework\Exceptions\RequestException; +use Phramework\Exceptions\Source\ISource; +use Phramework\Exceptions\Source\Pointer; +use Phramework\JSONAPI\Relationship; +use Phramework\JSONAPI\ResourceModel; use Phramework\JSONAPI\ValidationModel; +use Phramework\Validate\ArrayValidator; +use Phramework\Validate\EnumValidator; +use Phramework\Validate\ObjectValidator; /** * @license https://www.apache.org/licenses/LICENSE-2.0 Apache-2.0 @@ -27,32 +37,181 @@ trait RequestBodyQueue { /** + * What is resource ? * @todo + * @param \stdClass $resource Primary data resource */ public static function handleResource( \stdClass $resource, + ISource $source, + ResourceModel $model, ValidationModel $validationModel, array $validationCallbacks = [] - ) { - $attributes = $validationModel->getAttributes()->parse( - $resource->attributes ?? new \stdClass() + ) + { + //Fetch request attributes + $requestAttributes = $resource->attributes ?? new \stdClass(); + $requestRelationships = $resource->relationships ?? new \stdClass(); + + /* + * Validate attributes against attributes validator + */ + $parsedAttributes = $validationModel->getAttributes() + ->setSource(new Pointer($source->getPath() . '/attributes')) + ->parse( + $requestAttributes ); - //getParsedRelationshipAttributes + $parsedRelationships = new \stdClass(); + + /** + * Format, object with + * - relationshipKey1 -> id1 + * - relationshipKey2 -> [id1, id2] + */ $relationships = new \stdClass(); - //todo - //Call Validation callbacks + /** + * Foreach request relationship + * - check if relationship exists + * - if TYPE_TO_ONE check if data is object with type and id + * - if TYPE_TO_MANY check if data is an array of objects with type and id + * - check if types are correct + * - copy ids to $relationshipAttributes object + */ + foreach ($requestRelationships as $rKey => $rValue) { + if (!$model->issetRelationship($rKey)) { + throw new RequestException(sprintf( + 'Relationship "%s" is not defined', + $rKey + )); + } + + $rSource = new Pointer( + $source->getPath() . '/relationships/' . $rKey + ); + + if (!isset($rValue->data)) { + throw new MissingParametersException( + ['data'], + $rSource + ); + } + + $r = $model->getRelationship($rKey); + $resourceType = $r->getResourceModel()->getResourceType(); + + $relationshipData = $rValue->data; + + switch ($r->getType()) { + case Relationship::TYPE_TO_ONE: + (new ObjectValidator( + (object)[ + 'id' => $r->getResourceModel()->getIdAttributeValidator(), + 'type' => new EnumValidator($resourceType, true) + ], + ['id', 'type'] + ))->setSource(new Pointer( + $rSource->getPath() . '/data' + ))->parse($relationshipData); + + //Push relationship for this relationship key + $relationships->{$rKey} = $relationshipData->id; + break; + case Relationship::TYPE_TO_MANY: + $parsed = (new ArrayValidator( + 0, + null, + (new ObjectValidator( + (object)[ + 'id' => $r->getResourceModel()->getIdAttributeValidator(), + 'type' => new EnumValidator($resourceType, true) + ], + ['id', 'type'] + ))->setSource(new Pointer( + $rSource->getPath() . '/data' + )) + ))->parse($relationshipData); + + //Push relationship for this relationship key + $relationships->{$relationshipKey} = array_map( + function (\stdClass $p) { + return $p->id; + }, + $parsed + ); + break; + } + } + + /* + * Validate relationships against relationships validator + */ + if (count((array)$relationships)) { + $parsedRelationships = $validationModel->getRelationships()->parse( + $relationships + ); + } + + /* + * Foreach request relationship + * Check if requested relationship resources exist + * Copy TYPE_TO_ONE attributes to primary data's attributes + */ + foreach ($parsedRelationships as $rKey => $rValue) { + $r = $model->getRelationship($rKey); + $rResourceModel = $r->getResourceModel(); + + //Convert to array + $tempIds = ( + is_array($rValue) + ? $rValue + : [$rValue] + ); + + $data = $rResourceModel->getById( + $tempIds + ); + + /* + * Check if any of given ids is not found + */ + foreach ($data as $dId => $dValue) { + if ($dValue === null) { + throw new NotFoundException(sprintf( + 'Resource of type "%s" and id "%s" is not found', + $rResourceModel->getResourceType(), + $dId + )); + } + } + + /* + * Copy to primary attributes + * //todo make sure getRecordDataAttribute is not null + * //todo what if a TO_MANY has getRecordDataAttribute ? + */ + if ($r->getType() === Relationship::TYPE_TO_ONE) { + $parsedAttributes->{$r->getRecordDataAttribute()} = $relationshipData; + } + } + + + /* + * Call Validation callbacks + */ foreach ($validationCallbacks as $callback) { $callback( $resource, - $attributes, //parsed - $relationships //parsed + $parsedAttributes, //parsed + $parsedRelationships //parsed ); - - //todo } - return new ResourceQueueItem($attributes, $relationships); + return new ResourceQueueItem( + $parsedAttributes, + $parsedRelationships + ); } } + diff --git a/src/Controller/Post.php b/src/Controller/Post.php index 185ca8f..91091b2 100644 --- a/src/Controller/Post.php +++ b/src/Controller/Post.php @@ -20,9 +20,12 @@ use Phramework\Exceptions\ForbiddenException; use Phramework\Exceptions\IncorrectParameterException; use Phramework\Exceptions\RequestException; +use Phramework\Exceptions\ServerException; use Phramework\Exceptions\Source\Pointer; use Phramework\JSONAPI\Controller\Helper\RequestBodyQueue; +use Phramework\JSONAPI\Controller\Helper\ResourceQueueItem; use Phramework\JSONAPI\Directive\Directive; +use Phramework\JSONAPI\Relationship; use Phramework\JSONAPI\ResourceModel; use Phramework\Util\Util; use Phramework\Validate\EnumValidator; @@ -34,13 +37,26 @@ * @license https://www.apache.org/licenses/LICENSE-2.0 Apache-2.0 * @author Xenofon Spafaridis * @since 3.0.0 - * @todo modify to allow batch, remove id ? + * @todo modify to allow batch */ 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 int|null $bulkLimit + * @param array $directives + * @return ResponseInterface + * @throws ForbiddenException + * @throws RequestException + */ public static function handlePost( ServerRequestInterface $request, ResponseInterface $response, @@ -54,7 +70,7 @@ public static function handlePost( $body = json_decode(json_encode($request->getParsedBody())); //Access request body primary data - $data = $body->data ?? new \stdClass(); + $data = $body->data ?? [new \stdClass()]; /** * @var bool @@ -87,18 +103,21 @@ public static function handlePost( ); $bulkIndex = 0; - //gather data as a queue - foreach ($data as $resource) { - //Prepare exception source - $source = new Pointer( - '/data' . - ( - $isBulk - ? '/' . $bulkIndex - : '' - ) - ); + //Prepare exception source + $source = new Pointer( + '/data' . + ( + $isBulk + ? '/' . $bulkIndex + : '' + ) + ); + + /* + * gather data as a queue + */ + foreach ($data as $resource) { //Require resource type Controller::requireProperties($resource, $source, 'type'); @@ -115,32 +134,30 @@ public static function handlePost( ); } - //Fetch request attributes - $requestAttributes = $resource->attributes ?? new \stdClass(); - $requestRelationships = $resource->relationships ?? new \stdClass(); + /* + * Will call validationCallbacks + * Will call $validationModel attribute validator on attributes + * Will call $validationModel relationship validator on relationships + * Will copy TO_ONE relationship data to parsed attributes + */ + $item = static::handleResource( + $resource, + $source, + $validationModel, + $validationCallbacks + ); - //todo use helper class + /*//todo use helper class $queueItem = (object) [ 'attributes' => $requestAttributes, 'relationships' => $requestRelationships - ]; + ];*/ - $requestQueue->push($queueItem); + $requestQueue->push($item); ++$bulkIndex; } - //on each validate - //todo - foreach ($requestQueue as $i => $q) { - $validationModel->attributes - ->setSource(new Pointer('/data/' . $i . '/attributes')) - ->parse($q->attributes); - } - - //on each call validation callback - //todo - //post /** @@ -149,12 +166,17 @@ public static function handlePost( */ $ids = []; - //process queue + /* + * process queue + */ while (!$requestQueue->isEmpty()) { + /** + * @var ResourceQueueItem + */ $queueItem = $requestQueue->pop(); $id = $model->post( - $queueItem->attributes + $queueItem->getAttributes() ); Controller::assertUnknownError( @@ -162,18 +184,35 @@ public static function handlePost( 'Unknown error while posting resource' ); - //POST item's relationships - $relationships = $queueItem->relationships; + /** + * @var \stdClass + */ + $relationships = $queueItem->getRelationships(); + + /** + * POST item's relationships + * @param string[] $rValue + */ + foreach ($relationships as $rKey => $rValue) { + $r = $model->getRelationship($rKey); + + if ($r->getType() == Relationship::TYPE_TO_MANY) { + if (!isset($r->getCallbacks()->{'PATCH'})) { + throw new ServerException(sprintf( + 'POST callback is not defined for relationship "%s"', + $rKey + )); + } + } - foreach ($relationships as $key => $relationship) { - //Call post relationship method to post each of relationships pairs - //todo fix - foreach ($relationship->resources as $resourceId) { + /* + * Call post relationship callback to post each of relationships pairs + */ + foreach ($rValue as $v) { call_user_func( - $relationship->callback, - $id, - $resourceId, - null //$additionalAttributes + $r->getCallbacks()->{'PATCH'}, + $id, //Inserted resource id + $v ); } } diff --git a/src/Relationship.php b/src/Relationship.php index 769e40a..d84d3e9 100644 --- a/src/Relationship.php +++ b/src/Relationship.php @@ -22,6 +22,7 @@ * @since 0.0.0 * @license https://www.apache.org/licenses/LICENSE-2.0 Apache-2.0 * @author Xenofon Spafaridis + * @todo POST callbacks should be defined as f ($insertedId, $relationshipResourceId) */ class Relationship { @@ -115,14 +116,14 @@ public function __construct( foreach ($callbacks as $method => $callback) { if (!is_string($method)) { throw new \LogicException(sprintf( - 'callback method "%s" must be string', + 'Method "%s" for callback must be string', $method )); } if (!is_callable($callback)) { throw new \LogicException(sprintf( - 'callback for method "%s" must be a callable', + 'Callback for method "%s" must be a callable', $method )); } From aadfac4c8f1868fbca91859ca1ddab5a0173ac1d Mon Sep 17 00:00:00 2001 From: Xenofon Spafaridis Date: Sat, 8 Oct 2016 23:34:24 +0300 Subject: [PATCH 2/4] Write phpunit tests for handlePost Rewrite RequestBodyQueue as static class to fix #49 --- src/Controller/Controller.php | 7 +- src/Controller/Helper/RequestBodyQueue.php | 102 +-- src/Controller/Post.php | 87 ++- src/Directive/FilterAttribute.php | 2 +- src/Model/DataSourceTrait.php | 4 +- src/Relationship.php | 8 +- tests/APP/DataSource/MemoryDataSource.php | 2 +- tests/APP/Models/Article.php | 122 ++++ tests/APP/Models/Tag.php | 17 +- tests/bootstrap.php | 13 + .../Helper/RequestBodyQueueTest.php | 498 ++++++++++++++ tests/src/Controller/PostTest.php | 637 ++++++++++++++++++ tests/src/DataSource/DataSourceTest.php | 57 ++ .../src/DataSource/DatabaseDataSourceTest.php | 127 ++++ tests/src/Directive/FilterTest.php | 2 +- tests/src/ModelTest.php | 1 + 16 files changed, 1598 insertions(+), 88 deletions(-) create mode 100644 tests/APP/Models/Article.php create mode 100644 tests/src/Controller/Helper/RequestBodyQueueTest.php create mode 100644 tests/src/Controller/PostTest.php create mode 100644 tests/src/DataSource/DataSourceTest.php create mode 100644 tests/src/DataSource/DatabaseDataSourceTest.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..e18b763 100644 --- a/src/Controller/Helper/RequestBodyQueue.php +++ b/src/Controller/Helper/RequestBodyQueue.php @@ -28,16 +28,16 @@ use Phramework\Validate\ArrayValidator; use Phramework\Validate\EnumValidator; use Phramework\Validate\ObjectValidator; +use Phramework\Validate\StringValidator; /** * @license https://www.apache.org/licenses/LICENSE-2.0 Apache-2.0 * @author Xenofon Spafaridis * @since 3.0.0 */ -trait RequestBodyQueue +abstract class RequestBodyQueue { /** - * What is resource ? * @todo * @param \stdClass $resource Primary data resource */ @@ -59,10 +59,10 @@ public static function handleResource( $parsedAttributes = $validationModel->getAttributes() ->setSource(new Pointer($source->getPath() . '/attributes')) ->parse( - $requestAttributes - ); + $requestAttributes + ); - $parsedRelationships = new \stdClass(); + //$parsedRelationships = new \stdClass(); /** * Format, object with @@ -103,17 +103,19 @@ public static function handleResource( $relationshipData = $rValue->data; + $itemValidator = (new ObjectValidator( + (object) [ + 'id' => new StringValidator(), // $r->getResourceModel()->getIdAttributeValidator(), + 'type' => new EnumValidator([$resourceType], true) + ], + ['id', 'type'] + ))->setSource(new Pointer( + $rSource->getPath() . '/data' + )); + switch ($r->getType()) { case Relationship::TYPE_TO_ONE: - (new ObjectValidator( - (object)[ - 'id' => $r->getResourceModel()->getIdAttributeValidator(), - 'type' => new EnumValidator($resourceType, true) - ], - ['id', 'type'] - ))->setSource(new Pointer( - $rSource->getPath() . '/data' - ))->parse($relationshipData); + $itemValidator->parse($relationshipData); //Push relationship for this relationship key $relationships->{$rKey} = $relationshipData->id; @@ -122,35 +124,45 @@ public static function handleResource( $parsed = (new ArrayValidator( 0, null, - (new ObjectValidator( - (object)[ - 'id' => $r->getResourceModel()->getIdAttributeValidator(), - 'type' => new EnumValidator($resourceType, true) - ], - ['id', 'type'] - ))->setSource(new Pointer( - $rSource->getPath() . '/data' - )) + $itemValidator + ))->setSource(new Pointer( + $rSource->getPath() . '/data' ))->parse($relationshipData); - //Push relationship for this relationship key - $relationships->{$relationshipKey} = array_map( - function (\stdClass $p) { - return $p->id; - }, - $parsed - ); + if (count($parsed)) { + //Push relationship for this relationship key + $relationships->{$rKey} = array_map( + function (\stdClass $p) { + return $p->id; + }, + $parsed + ); + } break; } } /* - * Validate relationships against relationships validator + * Validate relationships against relationships validator if is set */ - if (count((array)$relationships)) { - $parsedRelationships = $validationModel->getRelationships()->parse( - $relationships - ); + if ($validationModel->getRelationships() !== null) { + $validator = $validationModel->getRelationships(); + + foreach ($validator->properties as $k => &$p) { + //force source to be ../data/id + $p->setSource(new Pointer( + $source->getPath() . '/relationships/' . $k . '/data/id' + )); + } + + $validator + //set source that will be used for missing parameters exception + ->setSource(new Pointer( + $source->getPath() . '/relationships' + )) + ->parse( + $relationships + ); } /* @@ -158,13 +170,19 @@ function (\stdClass $p) { * Check if requested relationship resources exist * Copy TYPE_TO_ONE attributes to primary data's attributes */ - foreach ($parsedRelationships as $rKey => $rValue) { + foreach ($relationships as $rKey => $rValue) { + if ($rValue === null) { //null added by relationship validator ? + //filer out null relationships, careful with TO_ONE might be needed as null + unset($relationships->{$rKey}); + continue; + } + $r = $model->getRelationship($rKey); $rResourceModel = $r->getResourceModel(); //Convert to array $tempIds = ( - is_array($rValue) + is_array($rValue) ? $rValue : [$rValue] ); @@ -192,11 +210,9 @@ function (\stdClass $p) { * //todo what if a TO_MANY has getRecordDataAttribute ? */ if ($r->getType() === Relationship::TYPE_TO_ONE) { - $parsedAttributes->{$r->getRecordDataAttribute()} = $relationshipData; + $parsedAttributes->{$r->getRecordDataAttribute()} = $rValue; } } - - /* * Call Validation callbacks */ @@ -204,14 +220,14 @@ function (\stdClass $p) { $callback( $resource, $parsedAttributes, //parsed - $parsedRelationships //parsed + $relationships, //parsed + $source ); } return new ResourceQueueItem( $parsedAttributes, - $parsedRelationships + $relationships ); } } - diff --git a/src/Controller/Post.php b/src/Controller/Post.php index 91091b2..0576c01 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; @@ -41,21 +42,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 +76,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 +99,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 )); } @@ -128,9 +139,10 @@ public static function handlePost( //Throw exception if resource id is forced if (property_exists($resource, 'id')) { - //todo include source - throw new ForbiddenException( - 'Unsupported request to create a resource with a client-generated ID' + throw new IncorrectParameterException( + 'additionalProperties', + 'Unsupported request to create a resource with a client-generated id', + new Pointer($source->getPath()) ); } @@ -140,18 +152,13 @@ public static function handlePost( * Will call $validationModel relationship validator on relationships * Will copy TO_ONE relationship data to parsed attributes */ - $item = static::handleResource( + $item = RequestBodyQueue::handleResource( $resource, $source, + $model, $validationModel, $validationCallbacks ); - - /*//todo use helper class - $queueItem = (object) [ - 'attributes' => $requestAttributes, - 'relationships' => $requestRelationships - ];*/ $requestQueue->push($item); @@ -197,23 +204,23 @@ public static function handlePost( $r = $model->getRelationship($rKey); if ($r->getType() == Relationship::TYPE_TO_MANY) { - if (!isset($r->getCallbacks()->{'PATCH'})) { + if (!isset($r->getCallbacks()->{'POST'})) { throw new ServerException(sprintf( 'POST callback is not defined for relationship "%s"', $rKey )); } - } - /* - * Call post relationship callback to post each of relationships pairs - */ - foreach ($rValue as $v) { - call_user_func( - $r->getCallbacks()->{'PATCH'}, - $id, //Inserted resource id - $v - ); + /* + * Call post relationship callback to post each of relationships pairs + */ + foreach ($rValue as $v) { + call_user_func( + $r->getCallbacks()->{'POST'}, + $id, //Inserted resource id + $v + ); + } } } @@ -233,13 +240,25 @@ public static function handlePost( ); } - /*if (count($ids) === 1) { - //Prepare response with 201 Created status code - return Response::created( + return Post::defaultPostViewCallback( + $request, + $response, + $ids + ); + } + + public static function defaultPostViewCallback( + ServerRequestInterface $request, + ResponseInterface $response, + array $ids + ) : ResponseInterface { + if (count($ids) === 1) { + //Prepare Location header + $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/src/Model/DataSourceTrait.php b/src/Model/DataSourceTrait.php index 2962569..a5281a6 100644 --- a/src/Model/DataSourceTrait.php +++ b/src/Model/DataSourceTrait.php @@ -326,8 +326,8 @@ function ($directive) { //Prepare filter $filter = new Filter( is_array($id) - ? $id - : [$id] + ? $id + : [$id] ); //Force array for primary data if (!empty($passedfilter)) { diff --git a/src/Relationship.php b/src/Relationship.php index d84d3e9..6c995e2 100644 --- a/src/Relationship.php +++ b/src/Relationship.php @@ -79,12 +79,12 @@ class Relationship protected $flags; /** - * @param ResourceModel $model Class path of relationship resource resourceModel + * @param ResourceModel $resourceModel Class path of relationship resource resourceModel * @param int $type *[Optional] Relationship type * @param string $recordDataAttribute *[Optional] Attribute name in record containing relationship data * @param \stdClass $callbacks *[Optional] Callable method can be used * to fetch relationship data, see TODO - * @param int $flags *[Optional] Relationship flags + * @param int $flags *[Optional] Relationship flags * @throws \Exception When is not null, callable or object of callbacks * @example * ```php @@ -104,7 +104,7 @@ class Relationship * ``` */ public function __construct( - ResourceModel $model, + ResourceModel $resourceModel, int $type = Relationship::TYPE_TO_ONE, string $recordDataAttribute = null, \stdClass $callbacks = null, @@ -132,7 +132,7 @@ public function __construct( $this->callbacks = $callbacks; } - $this->resourceModel = $model; + $this->resourceModel = $resourceModel; $this->type = $type; $this->recordDataAttribute = $recordDataAttribute; $this->flags = $flags; 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/Article.php b/tests/APP/Models/Article.php new file mode 100644 index 0000000..ea1ac6e --- /dev/null +++ b/tests/APP/Models/Article.php @@ -0,0 +1,122 @@ + + * @since 1.0 + */ +class Article extends Model +{ + use ModelTrait; + + protected static function defineModel() : ResourceModel + { + $r = (new ResourceModel('article', new MemoryDataSource())); + return $r + ->addVariable('table', 'article') + ->setSortableAttributes( + 'id' + )->setValidationModel( + new ValidationModel( + new ObjectValidator( + (object) [ + 'title' => new StringValidator(), + 'body' => new StringValidator(), + 'status' => (new UnsignedIntegerValidator(0, 1)) + ->setDefault(1) + ], + ['title', 'body'], + false + ), + new ObjectValidator( + (object) [ + 'author' => User::getResourceModel()->getIdAttributeValidator(), + 'tag' => new ArrayValidator( + 0, + null, + Tag::getResourceModel()->getIdAttributeValidator() + ) + ], + ['author'], + false + ) + ), + 'POST' + ) + /*->setValidationModel( + new ValidationModel( + new ObjectValidator( + (object) [ + 'title' => new StringValidator(), + 'body' => new StringValidator(), + 'status' => (new UnsignedIntegerValidator(0, 1)) + ], + [], + false + ), + new ObjectValidator( + (object) [ + 'author' => User::getResourceModel()->getIdAttributeValidator() + ], + [], + false + ) + ), + 'PATCH' + )*/ + ->setRelationships( + (object) [ + 'author' => new Relationship( + User::getResourceModel(), + Relationship::TYPE_TO_ONE, + 'creator-user_id' + ), + 'tag' => new Relationship( + Tag::getResourceModel(), + Relationship::TYPE_TO_MANY, + 'tag_id', + (object) [ + /** + * @param string $articleId + * @param string $tagId + */ + 'POST' => function (string $articleId, string $tagId) use (&$r) { + //todo use actual datasource to store connection + var_dump(sprintf('post (%s, %s', + $articleId, + $tagId + )); + } + ] + ) + ] + ); + } +} 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/bootstrap.php b/tests/bootstrap.php index add71ad..486d0d0 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -124,4 +124,17 @@ 'group_id' => '2', 'tag_id' => ['2'] ] +); + +MemoryDataSource::addTable('article'); + +MemoryDataSource::insert( + 'article', + (object) [ + 'id' => '1', + 'title' => 'Hello World', + 'body' => 'Lorem ipsum', + 'status' => 1, + 'creator-user_id' => '2' + ] ); \ No newline at end of file diff --git a/tests/src/Controller/Helper/RequestBodyQueueTest.php b/tests/src/Controller/Helper/RequestBodyQueueTest.php new file mode 100644 index 0000000..d00dc3c --- /dev/null +++ b/tests/src/Controller/Helper/RequestBodyQueueTest.php @@ -0,0 +1,498 @@ + + * Using \Phramework\JSONAPI\APP\Models\Tag model for tests + */ +class RequestBodyQueueTest extends \PHPUnit_Framework_TestCase +{ + use Post; + + /** + * Expect missing relationships author + * @covers ::handleResource + * @group relationships + * @group missing + */ + public function testMissingRelationships() + { + //omit relationships + $request = (new ServerRequest()) + ->withParsedBody((object) [ + 'data' => (object) [ + 'type' => Article::getResourceType(), + 'attributes' => (object) [ + 'title' => 'abcd', + 'body' => 'abcdef', + 'status' => 1 + ] + ] + ]); + + $this->expectMissing( + $request, + ['author'], + '/data/relationships', + Article::getResourceModel() + ); + } + + /** + * Expect missing relationships author + * @covers ::handleResource + * @group relationships + * @group missing + */ + public function testMissingRelationshipsData() + { + //omit relationships + $request = (new ServerRequest()) + ->withParsedBody((object) [ + 'data' => (object) [ + 'type' => Article::getResourceType(), + 'attributes' => (object) [ + 'title' => 'abcd', + 'body' => 'abcdef', + 'status' => 1 + ], + 'relationships' => (object) [ + 'author' => (object) [] + ] + ] + ]); + + $this->expectMissing( + $request, + ['data'], + '/data/relationships/author', + Article::getResourceModel() + ); + } + + /** + * Expect missing relationships author + * @covers ::handleResource + * @group relationships + * @group missing + */ + public function testMissingRelationshipsDataIdType() + { + //omit relationships + $request = (new ServerRequest()) + ->withParsedBody((object) [ + 'data' => (object) [ + 'type' => Article::getResourceType(), + 'attributes' => (object) [ + 'title' => 'abcd', + 'body' => 'abcdef', + 'status' => 1 + ], + 'relationships' => (object) [ + 'author' => (object) [ + 'data' => (object) [ //empty + ] + ] + ] + ] + ]); + + $this->expectMissing( + $request, + ['id', 'type'], + '/data/relationships/author/data', + Article::getResourceModel() + ); + } + + /** + * @covers ::handleResource + * @group relationships + * @group incorrect + */ + public function testRelationshipsIncorrectId() + { + $request = $this->getArticleRequest('', User::getResourceType()); + + try { + $this->handlePost( + $request, + new Response(), + Article::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(), + 'Expect minLength since empty string is given' + ); + + $this->assertEquals( + new Pointer('/data/relationships/author/data/id'), + $e->getSource() + ); + } catch (\Exception $e) { + $this->fail('Expected Exception has not been raised'); + } + } + + /** + * @covers ::handleResource + * @group relationships + * @expectedException \Phramework\Exceptions\NotFoundException + * @expectedExceptionCode 404 + * @expectedExceptionMessageRegExp /user/ + */ + public function testRelationshipsNotFound() + { + $request = $this->getArticleRequest(md5('abcd'), User::getResourceType()); + + $this->handlePost( + $request, + new Response(), + Article::getResourceModel() + ); + } + + /** + * @covers ::handleResource + * @group relationship + * @group incorrect + */ + public function testRelationshipsIncorrectType() + { + $request = $this->getArticleRequest('1', Tag::getResourceType()); + + try { + $this->handlePost( + $request, + new Response(), + Article::getResourceModel() + ); + } catch (IncorrectParametersException $e) { + $this->assertCount( + 1, + $e->getExceptions() + ); + + /** + * @var IncorrectParameterException + */ + $e = $e->getExceptions()[0]; + + $this->assertInstanceOf( + IncorrectParameterException::class, + $e + ); + + $this->assertSame( + 'enum', + $e->getFailure(), + 'Expect not expected type is given' + ); + + $this->assertEquals( + new Pointer('/data/relationships/author/data/type'), + $e->getSource() + ); + } catch (\Exception $e) { + $this->fail('Expected Exception has not been raised'); + } + } + + /** + * Expect author id to be in inserted resource + * @covers ::handleResource + * @covers \Phramework\JSONAPI\Controller\Post::handlePost + * @group relationship + */ + public function testRelationshipsToOneSuccess() + { + $user = User::get(new Page(1))[0]; + + $request = $this->getArticleRequest($user->id, User::getResourceType()); + + $unit = $this; + + $this->handlePost( + $request, + new Response(), + Article::getResourceModel(), + [ + function ( + \stdClass $resource, + \stdClass $parsedAttributes, + \stdClass $parsedRelationships, + ISource $source + ) use ($unit, $user) { + $unit->assertSame( + $user->id, + $parsedAttributes->{'creator-user_id'} + ); + + $unit->assertSame( + $user->id, + $parsedRelationships->{'author'} + ); + } + ], + function ( + ServerRequestInterface $request, + ResponseInterface $response, + array $ids + ) use ($unit, $user) : ResponseInterface { + $data = Article::getById($ids[0]); + + $this->assertSame( + $user->id, + $data->relationships->author->data->id + ); + + return Post::defaultPostViewCallback( + $request, + $response, + $ids + ); + } + ); + } + + /** + * Expect author id to be in inserted resource + * @covers ::handleResource + * @covers \Phramework\JSONAPI\Controller\Post::handlePost + * @group relationship + */ + public function testRelationshipsToManySuccess() + { + $user = User::get(new Page(1))[0]; + + $tags = Tag::get(new Page(2)); + + $request = (new ServerRequest()) + ->withParsedBody((object) [ + 'data' => (object) [ + 'type' => Article::getResourceType(), + 'attributes' => (object) [ + 'title' => 'abcd', + 'body' => 'abcdef', + 'status' => 1 + ], + 'relationships' => (object) [ + 'author' => (object) [ + 'data' => (object) [ + 'id' => $user->id, + 'type' => User::getResourceType() + ] + ], + 'tag' => (object) [ + 'data' => [ + (object) [ + 'id' => $tags[0]->id, + 'type' => Tag::getResourceType() + ], + (object) [ + 'id' => $tags[1]->id, + 'type' => Tag::getResourceType() + ] + ] + ] + ] + ] + ]); + + $unit = $this; + + $this->handlePost( + $request, + new Response(), + Article::getResourceModel() + /*[ + function ( + \stdClass $resource, + \stdClass $parsedAttributes, + \stdClass $parsedRelationships, + ISource $source + ) use ($unit, $user) { + $unit->assertSame( + $user->id, + $parsedAttributes->{'creator-user_id'} + ); + + $unit->assertSame( + $user->id, + $parsedRelationships->{'author'} + ); + } + ],*/ + /*function ( + ServerRequestInterface $request, + ResponseInterface $response, + array $ids + ) use ($unit, $user) : ResponseInterface { + $data = Article::getById($ids[0]); + + $this->assertSame( + $user->id, + $data->relationships->author->data->id + ); + + return Post::defaultPostViewCallback( + $request, + $response, + $ids + ); + }*/ + ); + + $this->markTestIncomplete('must check for execution-insertion of tag data'); + } + + /** + * @covers ::handleResource + * @group relationship + * @expectedException \Phramework\Exceptions\RequestException + * @expectedExceptionCode 400 + * @expectedExceptionMessageRegExp /Relationship/i + * @expectedExceptionMessageRegExp /abcd/i + */ + public function testRelationshipsNotDefinedException() + { + $request = (new ServerRequest()) + ->withParsedBody((object) [ + 'data' => (object) [ + 'type' => Article::getResourceType(), + 'attributes' => (object) [ + 'title' => 'abcd', + 'body' => 'abcdef', + 'status' => 1 + ], + 'relationships' => (object) [ + 'abcd' => (object) [ + ] + ] + ] + ]); + + $this->handlePost( + $request, + new Response(), + Article::getResourceModel() + ); + } + + /* + * 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, + ResourceModel $resourceModel + ) { + try { + $response = $this->handlePost( + $request, + new Response(), + $resourceModel + ); + } 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 getArticleRequest( + string $authorId, + string $authorType + ) : ServerRequestInterface { + $request = (new ServerRequest()) + ->withParsedBody((object) [ + 'data' => (object) [ + 'type' => Article::getResourceType(), + 'attributes' => (object) [ + 'title' => 'abcd', + 'body' => 'abcdef', + 'status' => 1 + ], + 'relationships' => (object) [ + 'author' => (object) [ + 'data' => (object) [ + 'id' => $authorId, + 'type' => $authorType + ] + ] + ] + ] + ]); + + return $request; + } +} diff --git a/tests/src/Controller/PostTest.php b/tests/src/Controller/PostTest.php new file mode 100644 index 0000000..5b2727b --- /dev/null +++ b/tests/src/Controller/PostTest.php @@ -0,0 +1,637 @@ + + * 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'); + } + + /** + * @covers ::defaultPostViewCallback + */ + public function testDefaultViewCallback() + { + $request = $this->getValidTagRequest('abcd'); + + $response = $this->defaultPostViewCallback( + $request, + new Response(), + ['1', '2', '3'] + ); + + $this->assertSame( + 204, + $response->getStatusCode() + ); + } + + /** + * @covers ::defaultPostViewCallback + */ + public function testDefaultViewCallbackSingle() + { + $request = $this->getValidTagRequest('abcd'); + + $response = $this->defaultPostViewCallback( + $request, + new Response(), + ['1'] + ); + + $this->assertSame( + 204, + $response->getStatusCode() + ); + + $this->assertTrue( + $response->hasHeader('Location') + ); + } + + /* + * Missing + */ + + /** + * @covers ::handlePost + * @group missing + */ + public function testMissingPrimaryData() + { + $request = (new ServerRequest()); + + $this->expectMissing( + $request, + ['data'], + '/', + Tag::getResourceModel() + ); + } + + /** + * @covers ::handlePost + * @group missing + */ + public function testMissingType() + { + $request = (new ServerRequest()) + ->withParsedBody((object) [ + 'data' => (object) [ + ] + ]); + + $this->expectMissing( + $request, + ['type'], + '/data', + Tag::getResourceModel() + ); + } + + /** + * 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', + Tag::getResourceModel() + ); + } + + /* + * 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 testUnsupportedRequestWithId() + { + $request = (new ServerRequest()) + ->withParsedBody((object) [ + 'data' => (object) [ + 'type' => Tag::getResourceType(), + 'id' => md5((string) mt_rand()), //inject unsupported id + 'attributes' => (object) [ + 'name' => 'aaaaa' + ] + ] + ]); + + try { + $this->handlePost( + $request, + new Response(), + Tag::getResourceModel() + ); + } catch (IncorrectParameterException $e) { + $this->assertSame( + 'additionalProperties', + $e->getFailure() + ); + + $this->assertSame( + '/data', + $e->getSource()->getPath() + ); + + $this->assertRegExp( + '/id/', + $e->getDetail(), + 'Expect detail message to contain "id" word' + ); + + } catch (\Exception $e) { + $this->fail('Expected Exception has not been raised'); + } + } + + /** + * @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'); + } + } + + + /* + * 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::defaultPostViewCallback( + $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 + ); + } + + /** + * Expect author id to be in inserted resource + * @covers ::handlePost + * @group relationship + */ + /*public function testRelationshipsToOneSuccess() + { + return (new RequestBodyQueueTest())->testRelationshipsToOneSuccess(); + }*/ + + /** + * Expect tag ids to be in inserted resource + * @covers ::handlePost + * @group relationship + */ + /*public function testRelationshipsToManySuccess() + { + return (new RequestBodyQueueTest())->testRelationshipsToManySuccess(); + }*/ + + + /* + * 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, + ResourceModel $resourceModel + ) { + try { + $response = $this->handlePost( + $request, + new Response(), + $resourceModel + ); + } 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) : ServerRequestInterface + { + $request = (new ServerRequest()) + ->withParsedBody((object) [ + 'data' => (object) [ + 'type' => Tag::getResourceType(), + 'attributes' => (object) [ + 'name' => $name + ] + ] + ]); + + return $request; + } +} diff --git a/tests/src/DataSource/DataSourceTest.php b/tests/src/DataSource/DataSourceTest.php new file mode 100644 index 0000000..46f0051 --- /dev/null +++ b/tests/src/DataSource/DataSourceTest.php @@ -0,0 +1,57 @@ + + * @coversDefaultClass Phramework\JSONAPI\DataSource\DataSource + */ +class DataSourceTest extends \PHPUnit_Framework_TestCase +{ + /** + * @covers ::setResourceModel + */ + public function testSetResourceModel() + { + $dataSource = new DatabaseDataSource(); + + $dataSource->setResourceModel( + Tag::getResourceModel() + ); + + return $dataSource; + } + + /** + * @covers ::getResourceModel + * @depends testSetResourceModel + */ + public function testGetResourceModel(DataSource $dataSource) + { + $resourceModel = $dataSource->getResourceModel(); + + $this->assertSame( + Tag::getResourceModel(), + $resourceModel + ); + } +} diff --git a/tests/src/DataSource/DatabaseDataSourceTest.php b/tests/src/DataSource/DatabaseDataSourceTest.php new file mode 100644 index 0000000..3587c4b --- /dev/null +++ b/tests/src/DataSource/DatabaseDataSourceTest.php @@ -0,0 +1,127 @@ + + * @coversDefaultClass Phramework\JSONAPI\DataSource\DatabaseDataSource + */ +class DatabaseDataSourceTest extends \PHPUnit_Framework_TestCase +{ + /** + * @var DatabaseDataSource + */ + protected $dataSource; + + public function setUp() + { + $this->dataSource = new DatabaseDataSource(Tag::getResourceModel()); + } + + /** + * @covers ::__construct + */ + public function testConstruct() + { + $dataSource = new DatabaseDataSource(Tag::getResourceModel()); + + $this->assertSame( + Tag::getResourceModel(), + $dataSource->getResourceModel() + ); + } + + /** + * @covers ::requireTableSetting + */ + public function testRequireTableSettingSuccess() + { + $dataSource = new DatabaseDataSource(Tag::getResourceModel()); + + $this->assertSame( + Tag::getResourceModel()->getVariable('table'), + $dataSource->requireTableSetting() + ); + } + + /** + * @covers ::requireTableSetting + * @expectedException \LogicException + */ + public function testRequireTableSettingFailure() + { + $dataSource = new DatabaseDataSource(); + + $model = (new ResourceModel('s', $dataSource)); + + $dataSource->requireTableSetting(); + } + + /** + * @covers ::handleFilter + */ + public function testHandleFilter() + { + $filter = new Filter(); + + $q = $this->dataSource->handleGet( + 'SELECT * FROM "table" {{filter}}', + false, + [$filter] + ); + + $this->assertInternalType('string', $q); + + $pattern = sprintf( + '/^SELECT \* FROM "table"\s*$/' + ); + + $this->assertRegExp($pattern, trim($q)); + } + + /** + * @covers ::handleFilter + * @covers ::handleGet + */ + public function testHandleFilter2() + { + $filter = new Filter( + ['1', '2'] + ); + + $q = $this->dataSource->handleGet( + 'SELECT * FROM "table" {{filter}}', + false, + [$filter] + ); + + $this->assertInternalType('string', $q); + + $pattern = sprintf( + '/^SELECT \* FROM "table"\s* WHERE "id"\s*IN\s*\(\'1\',\'2\'\)\s*$/' + ); + + $this->assertRegExp($pattern, trim($q)); + } +} 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 diff --git a/tests/src/ModelTest.php b/tests/src/ModelTest.php index 14c4174..1e16a53 100644 --- a/tests/src/ModelTest.php +++ b/tests/src/ModelTest.php @@ -1,4 +1,5 @@ Date: Mon, 31 Oct 2016 19:28:09 +0200 Subject: [PATCH 3/4] Introduce RequestWithBody --- src/Controller/Helper/RequestWithBody.php | 68 +++++++++++++++++++++++ src/Controller/Patch.php | 7 +-- src/Controller/Post.php | 57 ++++++------------- 3 files changed, 86 insertions(+), 46 deletions(-) create mode 100644 src/Controller/Helper/RequestWithBody.php diff --git a/src/Controller/Helper/RequestWithBody.php b/src/Controller/Helper/RequestWithBody.php new file mode 100644 index 0000000..2415d55 --- /dev/null +++ b/src/Controller/Helper/RequestWithBody.php @@ -0,0 +1,68 @@ + + * @since 3.0.0 + */ +class RequestWithBody +{ + public static function prepareData( + ServerRequestInterface $request, + int $bulkLimit = null + ) { + //todo figure out a permanent solution to have body as object instead of array (recursive), for every framework + $body = json_decode(json_encode($request->getParsedBody())); + + Controller::requireProperties($body, new Pointer('/'), 'data'); + + //Access request body primary data + $data = $body->data; + + /** + * @var bool + */ + $isBulk = true; + + //Treat all request data (bulk or not) as an array of resources + if (is_object($data) + || (is_array($data) && Util::isArrayAssoc($data)) + ) { + $isBulk = false; + $data = [$data]; + } + + //check bulk limit + if ($bulkLimit !== null && count($data) > $bulkLimit) { + throw new RequestException(sprintf( + 'Number of bulk requests is exceeding the maximum of %s', + $bulkLimit + )); + } + + return [$data, $isBulk]; + } +} diff --git a/src/Controller/Patch.php b/src/Controller/Patch.php index 950403d..dd42a24 100644 --- a/src/Controller/Patch.php +++ b/src/Controller/Patch.php @@ -31,21 +31,16 @@ */ trait Patch { - use RequestBodyQueue; - //prototype public static function handlePatch( ServerRequestInterface $request, ResponseInterface $response, ResourceModel $model, - string $id, array $validationCallbacks = [], callable $viewCallback = null, - int $bulkLimit = null, + int $bulkLimit = 1, array $directives = [] ) : ResponseInterface { - //Validate id using model's validator - $id = $model->getIdAttributeValidator()->parse($id); //gather data as a queue diff --git a/src/Controller/Post.php b/src/Controller/Post.php index 0576c01..ebfad50 100644 --- a/src/Controller/Post.php +++ b/src/Controller/Post.php @@ -24,6 +24,7 @@ use Phramework\Exceptions\ServerException; use Phramework\Exceptions\Source\Pointer; use Phramework\JSONAPI\Controller\Helper\RequestBodyQueue; +use Phramework\JSONAPI\Controller\Helper\RequestWithBody; use Phramework\JSONAPI\Controller\Helper\ResourceQueueItem; use Phramework\JSONAPI\Directive\Directive; use Phramework\JSONAPI\Relationship; @@ -59,7 +60,7 @@ trait Post * - string[] $ids * - returning ResponseInterface * @param int|null $bulkLimit - * @param array $directives + * @param Directive[] $directives * @return ResponseInterface * @throws ForbiddenException * @throws RequestException @@ -72,37 +73,13 @@ public static function handlePost( ResourceModel $model, array $validationCallbacks = [], callable $viewCallback = null, //function (request, response, $ids) : ResponseInterface - int $bulkLimit = null, //todo decide 1 or null for default + int $bulkLimit = null, 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())) ?? new \stdClass(); - - Controller::requireProperties($body, new Pointer('/'), 'data'); - - //Access request body primary data - $data = $body->data; - - /** - * @var bool - */ - $isBulk = true; - - //Treat all request data (bulk or not) as an array of resources - if (is_object($data) - || (is_array($data) && Util::isArrayAssoc($data)) - ) { - $isBulk = false; - $data = [$data]; - } - - //check bulk limit - if ($bulkLimit !== null && count($data) > $bulkLimit) { - throw new RequestException(sprintf( - 'Number of bulk requests is exceeding the maximum of %s', - $bulkLimit - )); - } + list($data, $isBulk) = RequestWithBody::prepareData( + $request, + $bulkLimit + ); $typeValidator = (new EnumValidator([$model->getResourceType()])); @@ -115,20 +92,20 @@ public static function handlePost( $bulkIndex = 0; - //Prepare exception source - $source = new Pointer( - '/data' . - ( - $isBulk - ? '/' . $bulkIndex - : '' - ) - ); - /* * gather data as a queue */ foreach ($data as $resource) { + //Prepare exception source + $source = new Pointer( + '/data' . + ( + $isBulk + ? '/' . $bulkIndex + : '' + ) + ); + //Require resource type Controller::requireProperties($resource, $source, 'type'); From 41d3d82114cb251e107e1b2416eb45eec03fb824 Mon Sep 17 00:00:00 2001 From: Xenofon Spafaridis Date: Mon, 7 Nov 2016 19:57:11 +0200 Subject: [PATCH 4/4] WIP --- src/Controller/Patch.php | 41 +++++++++++++++++++++++++++++++++------- src/Controller/Post.php | 1 - 2 files changed, 34 insertions(+), 8 deletions(-) diff --git a/src/Controller/Patch.php b/src/Controller/Patch.php index dd42a24..a3ee8df 100644 --- a/src/Controller/Patch.php +++ b/src/Controller/Patch.php @@ -18,6 +18,7 @@ namespace Phramework\JSONAPI\Controller; use Phramework\JSONAPI\Controller\Helper\RequestBodyQueue; +use Phramework\JSONAPI\Controller\Helper\RequestWithBody; use Phramework\JSONAPI\Directive\Directive; use Phramework\JSONAPI\ResourceModel; use Psr\Http\Message\ResponseInterface; @@ -32,6 +33,7 @@ trait Patch { //prototype + //@todo figure out view callback arguments public static function handlePatch( ServerRequestInterface $request, ResponseInterface $response, @@ -41,22 +43,47 @@ public static function handlePatch( int $bulkLimit = 1, array $directives = [] ) : ResponseInterface { + list($data, $isBulk) = RequestWithBody::prepareData( + $request, + $bulkLimit + ); //gather data as a queue - //check bulk limit ?? - - //on each validate - //prefer PATCH validation model $validationModel = $model->getValidationModel( 'PATCH' ); - //check if exists + //on each + + // validate + + //check if exists + + //call validation callbacks - //on each call validation callback + //gather output for view callback - //204 or view callback + //return view callback, it MUST return a ResponseInterface + if ($viewCallback !== null) { + return $viewCallback( + $request, + $response + ); + } + + return Patch::defaultPatchViewCallback( + $request, + $response + ); + } + + public static function defaultPatchViewCallback( + ServerRequestInterface $request, + ResponseInterface $response + ) : ResponseInterface { + //Return 204 No Content + return Response::noContent($response); } } diff --git a/src/Controller/Post.php b/src/Controller/Post.php index ebfad50..472d9c6 100644 --- a/src/Controller/Post.php +++ b/src/Controller/Post.php @@ -208,7 +208,6 @@ public static function handlePost( } //return view callback, it MUST return a ResponseInterface - if ($viewCallback !== null) { return $viewCallback( $request,