From 890f9b07d40b6a2cde26329908021d03f86d92f2 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Thu, 4 Jan 2018 23:08:53 -0500 Subject: [PATCH] Replace Resquest objects with PSR-7 request messages; improves #53 --- lib/REST/AbstractHandler.php | 4 +- lib/REST/Handler.php | 5 +- lib/REST/NextCloudNews/V1_2.php | 111 ++++--- lib/REST/NextCloudNews/Versions.php | 38 +-- lib/REST/Request.php | 89 ------ lib/REST/TinyTinyRSS/API.php | 13 +- lib/REST/TinyTinyRSS/Icon.php | 8 +- tests/cases/REST/NextCloudNews/TestV1_2.php | 278 ++++++++++-------- .../cases/REST/NextCloudNews/TestVersions.php | 46 ++- tests/cases/REST/TinyTinyRSS/TestAPI.php | 33 ++- tests/cases/REST/TinyTinyRSS/TestIcon.php | 29 +- tests/lib/AbstractTest.php | 2 +- 12 files changed, 327 insertions(+), 329 deletions(-) delete mode 100644 lib/REST/Request.php diff --git a/lib/REST/AbstractHandler.php b/lib/REST/AbstractHandler.php index 273e61a..756ebe7 100644 --- a/lib/REST/AbstractHandler.php +++ b/lib/REST/AbstractHandler.php @@ -8,10 +8,12 @@ namespace JKingWeb\Arsse\REST; use JKingWeb\Arsse\Misc\Date; use JKingWeb\Arsse\Misc\ValueInfo; +use Psr\Http\Message\ServerRequestInterface; +use Psr\Http\Message\ResponseInterface; abstract class AbstractHandler implements Handler { abstract public function __construct(); - abstract public function dispatch(Request $req): \Psr\Http\Message\ResponseInterface; + abstract public function dispatch(ServerRequestInterface $req): ResponseInterface; protected function fieldMapNames(array $data, array $map): array { $out = []; diff --git a/lib/REST/Handler.php b/lib/REST/Handler.php index 4d4904a..3b2c88e 100644 --- a/lib/REST/Handler.php +++ b/lib/REST/Handler.php @@ -6,7 +6,10 @@ declare(strict_types=1); namespace JKingWeb\Arsse\REST; +use Psr\Http\Message\ServerRequestInterface; +use Psr\Http\Message\ResponseInterface; + interface Handler { public function __construct(); - public function dispatch(Request $req): \Psr\Http\Message\ResponseInterface; + public function dispatch(ServerRequestInterface $req): ResponseInterface; } diff --git a/lib/REST/NextCloudNews/V1_2.php b/lib/REST/NextCloudNews/V1_2.php index bf39551..fe4b974 100644 --- a/lib/REST/NextCloudNews/V1_2.php +++ b/lib/REST/NextCloudNews/V1_2.php @@ -15,7 +15,9 @@ use JKingWeb\Arsse\Misc\ValueInfo; use JKingWeb\Arsse\AbstractException; use JKingWeb\Arsse\Db\ExceptionInput; use JKingWeb\Arsse\Feed\Exception as FeedException; -use \Psr\Http\Message\ResponseInterface; +use JKingWeb\Arsse\REST\Target; +use Psr\Http\Message\ServerRequestInterface; +use Psr\Http\Message\ResponseInterface; use Zend\Diactoros\Response\JsonResponse as Response; use Zend\Diactoros\Response\EmptyResponse; @@ -43,53 +45,61 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler { 'items' => ValueInfo::T_MIXED | ValueInfo::M_ARRAY, ]; protected $paths = [ - 'folders' => ['GET' => "folderList", 'POST' => "folderAdd"], - 'folders/1' => ['PUT' => "folderRename", 'DELETE' => "folderRemove"], - 'folders/1/read' => ['PUT' => "folderMarkRead"], - 'feeds' => ['GET' => "subscriptionList", 'POST' => "subscriptionAdd"], - 'feeds/1' => ['DELETE' => "subscriptionRemove"], - 'feeds/1/move' => ['PUT' => "subscriptionMove"], - 'feeds/1/rename' => ['PUT' => "subscriptionRename"], - 'feeds/1/read' => ['PUT' => "subscriptionMarkRead"], - 'feeds/all' => ['GET' => "feedListStale"], - 'feeds/update' => ['GET' => "feedUpdate"], - 'items' => ['GET' => "articleList"], - 'items/updated' => ['GET' => "articleList"], - 'items/read' => ['PUT' => "articleMarkReadAll"], - 'items/1/read' => ['PUT' => "articleMarkRead"], - 'items/1/unread' => ['PUT' => "articleMarkRead"], - 'items/read/multiple' => ['PUT' => "articleMarkReadMulti"], - 'items/unread/multiple' => ['PUT' => "articleMarkReadMulti"], - 'items/1/1/star' => ['PUT' => "articleMarkStarred"], - 'items/1/1/unstar' => ['PUT' => "articleMarkStarred"], - 'items/star/multiple' => ['PUT' => "articleMarkStarredMulti"], - 'items/unstar/multiple' => ['PUT' => "articleMarkStarredMulti"], - 'cleanup/before-update' => ['GET' => "cleanupBefore"], - 'cleanup/after-update' => ['GET' => "cleanupAfter"], - 'version' => ['GET' => "serverVersion"], - 'status' => ['GET' => "serverStatus"], - 'user' => ['GET' => "userStatus"], + '/folders' => ['GET' => "folderList", 'POST' => "folderAdd"], + '/folders/1' => ['PUT' => "folderRename", 'DELETE' => "folderRemove"], + '/folders/1/read' => ['PUT' => "folderMarkRead"], + '/feeds' => ['GET' => "subscriptionList", 'POST' => "subscriptionAdd"], + '/feeds/1' => ['DELETE' => "subscriptionRemove"], + '/feeds/1/move' => ['PUT' => "subscriptionMove"], + '/feeds/1/rename' => ['PUT' => "subscriptionRename"], + '/feeds/1/read' => ['PUT' => "subscriptionMarkRead"], + '/feeds/all' => ['GET' => "feedListStale"], + '/feeds/update' => ['GET' => "feedUpdate"], + '/items' => ['GET' => "articleList"], + '/items/updated' => ['GET' => "articleList"], + '/items/read' => ['PUT' => "articleMarkReadAll"], + '/items/1/read' => ['PUT' => "articleMarkRead"], + '/items/1/unread' => ['PUT' => "articleMarkRead"], + '/items/read/multiple' => ['PUT' => "articleMarkReadMulti"], + '/items/unread/multiple' => ['PUT' => "articleMarkReadMulti"], + '/items/1/1/star' => ['PUT' => "articleMarkStarred"], + '/items/1/1/unstar' => ['PUT' => "articleMarkStarred"], + '/items/star/multiple' => ['PUT' => "articleMarkStarredMulti"], + '/items/unstar/multiple' => ['PUT' => "articleMarkStarredMulti"], + '/cleanup/before-update' => ['GET' => "cleanupBefore"], + '/cleanup/after-update' => ['GET' => "cleanupAfter"], + '/version' => ['GET' => "serverVersion"], + '/status' => ['GET' => "serverStatus"], + '/user' => ['GET' => "userStatus"], ]; public function __construct() { } - public function dispatch(\JKingWeb\Arsse\REST\Request $req): ResponseInterface { + public function dispatch(ServerRequestInterface $req): ResponseInterface { // try to authenticate if (!Arsse::$user->authHTTP()) { return new EmptyResponse(401, ['WWW-Authenticate' => 'Basic realm="'.self::REALM.'"']); } + // explode and normalize the URL path + $target = new Target($req->getRequestTarget()); // handle HTTP OPTIONS requests - if ($req->method=="OPTIONS") { - return $this->handleHTTPOptions($req->paths); + if ($req->getMethod()=="OPTIONS") { + return $this->handleHTTPOptions((string) $target); } // normalize the input - if ($req->body) { + $data = (string) $req->getBody(); + $type = ""; + if ($req->hasHeader("Content-Type")) { + $type = $req->getHeader("Content-Type"); + $type = array_pop($type); + } + if ($data) { // if the entity body is not JSON according to content type, return "415 Unsupported Media Type" - if (!preg_match("<^application/json\b|^$>", $req->type)) { + if (!preg_match("<^application/json\b|^$>", $type)) { return new EmptyResponse(415, ['Accept' => "application/json"]); } - $data = @json_decode($req->body, true); + $data = @json_decode($data, true); if (json_last_error() != \JSON_ERROR_NONE) { // if the body could not be parsed as JSON, return "400 Bad Request" return new EmptyResponse(400); @@ -98,10 +108,10 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler { $data = []; } // FIXME: Do query parameters take precedence in NextCloud? Is there a conflict error when values differ? - $data = $this->normalizeInput(array_merge($data, $req->query), $this->validInput, "unix"); + $data = $this->normalizeInput(array_merge($data, $req->getQueryParams()), $this->validInput, "unix"); // check to make sure the requested function is implemented try { - $func = $this->chooseCall($req->paths, $req->method); + $func = $this->chooseCall((string) $target, $req->getMethod()); } catch (Exception404 $e) { return new EmptyResponse(404); } catch (Exception405 $e) { @@ -112,7 +122,7 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler { } // dispatch try { - return $this->$func($req->paths, $data); + return $this->$func($target->path, $data); // @codeCoverageIgnoreStart } catch (Exception $e) { // if there was a REST exception return 400 @@ -124,19 +134,24 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler { // @codeCoverageIgnoreEnd } - protected function normalizePath(array $url): string { - // any URL components which are database IDs (integers greater than zero) should be replaced with "1", for easier comparison (we don't care about the specific ID) - for ($a = 0; $a < sizeof($url); $a++) { - if (ValueInfo::id($url[$a])) { - $url[$a] = "1"; + protected function normalizePathIds(string $url): string { + // first parse the URL and perform syntactic normalization + $target = new Target($url); + // any path components which are database IDs (integers greater than zero) should be replaced with "1", for easier comparison (we don't care about the specific ID) + for ($a = 0; $a < sizeof($target->path); $a++) { + if (ValueInfo::id($target->path[$a])) { + $target->path[$a] = "1"; } } - return implode("/", $url); + // discard any fragment ID (there shouldn't be any) and query string (the query is available in the request itself) + $target->fragment = ""; + $target->query = ""; + return (string) $target; } - protected function chooseCall(array $url, string $method): string { - // normalize the URL path - $url = $this->normalizePath($url); + protected function chooseCall(string $url, string $method): string { + // // normalize the URL path: change any IDs to 1 for easier comparison + $url = $this->normalizePathIds($url); // normalize the HTTP method to uppercase $method = strtoupper($method); // we now evaluate the supplied URL against every supported path for the selected scope @@ -244,9 +259,9 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler { return $article; } - protected function handleHTTPOptions(array $url): ResponseInterface { - // normalize the URL path - $url = $this->normalizePath($url); + protected function handleHTTPOptions(string $url): ResponseInterface { + // normalize the URL path: change any IDs to 1 for easier comparison + $url = $this->normalizePathIDs($url); if (isset($this->paths[$url])) { // if the path is supported, respond with the allowed methods and other metadata $allowed = array_keys($this->paths[$url]); diff --git a/lib/REST/NextCloudNews/Versions.php b/lib/REST/NextCloudNews/Versions.php index 1f97e77..77924bd 100644 --- a/lib/REST/NextCloudNews/Versions.php +++ b/lib/REST/NextCloudNews/Versions.php @@ -6,6 +6,8 @@ declare(strict_types=1); namespace JKingWeb\Arsse\REST\NextCloudNews; +use Psr\Http\Message\ServerRequestInterface; +use Psr\Http\Message\ResponseInterface; use Zend\Diactoros\Response\JsonResponse as Response; use Zend\Diactoros\Response\EmptyResponse; @@ -13,24 +15,26 @@ class Versions implements \JKingWeb\Arsse\REST\Handler { public function __construct() { } - public function dispatch(\JKingWeb\Arsse\REST\Request $req): \Psr\Http\Message\ResponseInterface { - if (!preg_match("<^/?$>", $req->path)) { - // if the request path is an empty string or just a slash, the client is probably trying a version we don't support + public function dispatch(ServerRequestInterface $req): ResponseInterface { + if (!preg_match("<^/?$>", $req->getRequestTarget())) { + // if the request path is more than an empty string or a slash, the client is probably trying a version we don't support return new EmptyResponse(404); - } elseif ($req->method=="OPTIONS") { - // if the request method is OPTIONS, respond accordingly - return new EmptyResponse(204, ['Allow' => "HEAD,GET"]); - } elseif ($req->method != "GET") { - // if a method other than GET was used, this is an error - return new EmptyResponse(405, ['Allow' => "HEAD,GET"]); - } else { - // otherwise return the supported versions - $out = [ - 'apiLevels' => [ - 'v1-2', - ] - ]; - return new Response($out); + } + switch ($req->getMethod()) { + case "OPTIONS": + // if the request method is OPTIONS, respond accordingly + return new EmptyResponse(204, ['Allow' => "HEAD,GET"]); + case "GET": + // otherwise return the supported versions + $out = [ + 'apiLevels' => [ + 'v1-2', + ] + ]; + return new Response($out); + default: + // if any other method was used, this is an error + return new EmptyResponse(405, ['Allow' => "HEAD,GET"]); } } } diff --git a/lib/REST/Request.php b/lib/REST/Request.php deleted file mode 100644 index 157027a..0000000 --- a/lib/REST/Request.php +++ /dev/null @@ -1,89 +0,0 @@ -method = strtoupper($method); - $this->url = $url; - $this->body = $body; - $this->type = $contentType; - if ($this->method=="HEAD") { - $this->head = true; - $this->method = "GET"; - } - $this->refreshURL(); - } - - public function refreshURL() { - $url = $this->parseURL($this->url); - $this->path = $url['path']; - $this->paths = $url['paths']; - $this->query = $url['query']; - } - - protected function parseURL(string $url): array { - // split the query string from the path - $parts = explode("?", $url); - $out = ['path' => $parts[0], 'paths' => [''], 'query' => []]; - // if there is a query string, parse it - if (isset($parts[1])) { - // split along & to get key-value pairs - $query = explode("&", $parts[1]); - for ($a = 0; $a < sizeof($query); $a++) { - // split each pair, into no more than two parts - $data = explode("=", $query[$a], 2); - // decode the key - $key = rawurldecode($data[0]); - // decode the value if there is one - $value = ""; - if (isset($data[1])) { - $value = rawurldecode($data[1]); - } - // add the pair to the query output, overwriting earlier values for the same key, is present - $out['query'][$key] = $value; - } - } - // also include the path as a set of decoded elements - // if the path is an empty string or just / nothing needs be done - if (!in_array($out['path'], ["/",""])) { - $paths = explode("/", $out['path']); - // remove the first and last empty elements, if present (they are artefacts of the splitting; others should remain) - if (!strlen($paths[0])) { - array_shift($paths); - } - if (!strlen($paths[sizeof($paths)-1])) { - array_pop($paths); - } - // %-decode each path element - $paths = array_map(function ($v) { - return rawurldecode($v); - }, $paths); - $out['paths'] = $paths; - } - return $out; - } -} diff --git a/lib/REST/TinyTinyRSS/API.php b/lib/REST/TinyTinyRSS/API.php index 2464954..6f94bb4 100644 --- a/lib/REST/TinyTinyRSS/API.php +++ b/lib/REST/TinyTinyRSS/API.php @@ -19,6 +19,8 @@ use JKingWeb\Arsse\ExceptionType; use JKingWeb\Arsse\Db\ExceptionInput; use JKingWeb\Arsse\Db\ResultEmpty; use JKingWeb\Arsse\Feed\Exception as FeedException; +use Psr\Http\Message\ServerRequestInterface; +use Psr\Http\Message\ResponseInterface; use Zend\Diactoros\Response\JsonResponse as Response; use Zend\Diactoros\Response\EmptyResponse; @@ -89,21 +91,22 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler { public function __construct() { } - public function dispatch(\JKingWeb\Arsse\REST\Request $req): \Psr\Http\Message\ResponseInterface { - if (!preg_match("<^(?:/(?:index\.php)?)?$>", $req->path)) { + public function dispatch(ServerRequestInterface $req): ResponseInterface { + if (!preg_match("<^(?:/(?:index\.php)?)?$>", $req->getRequestTarget())) { // reject paths other than the index return new EmptyResponse(404); } - if ($req->method=="OPTIONS") { + if ($req->getMethod()=="OPTIONS") { // respond to OPTIONS rquests; the response is a fib, as we technically accept any type or method return new EmptyResponse(204, [ 'Allow' => "POST", 'Accept' => "application/json, text/json", ]); } - if ($req->body) { + $data = (string) $req->getBody(); + if ($data) { // only JSON entities are allowed, but Content-Type is ignored, as is request method - $data = @json_decode($req->body, true); + $data = @json_decode($data, true); if (json_last_error() != \JSON_ERROR_NONE || !is_array($data)) { return new Response(self::FATAL_ERR); } diff --git a/lib/REST/TinyTinyRSS/Icon.php b/lib/REST/TinyTinyRSS/Icon.php index 3641f17..ef2d0c0 100644 --- a/lib/REST/TinyTinyRSS/Icon.php +++ b/lib/REST/TinyTinyRSS/Icon.php @@ -7,17 +7,19 @@ declare(strict_types=1); namespace JKingWeb\Arsse\REST\TinyTinyRSS; use JKingWeb\Arsse\Arsse; +use Psr\Http\Message\ServerRequestInterface; +use Psr\Http\Message\ResponseInterface; use Zend\Diactoros\Response\EmptyResponse as Response; class Icon extends \JKingWeb\Arsse\REST\AbstractHandler { public function __construct() { } - public function dispatch(\JKingWeb\Arsse\REST\Request $req): \Psr\Http\Message\ResponseInterface { - if ($req->method != "GET") { + public function dispatch(ServerRequestInterface $req): ResponseInterface { + if ($req->getMethod() != "GET") { // only GET requests are allowed return new Response(405, ['Allow' => "GET"]); - } elseif (!preg_match("<^(\d+)\.ico$>", $req->url, $match) || !((int) $match[1])) { + } elseif (!preg_match("<^(\d+)\.ico$>", $req->getRequestTarget(), $match) || !((int) $match[1])) { return new Response(404); } $url = Arsse::$db->subscriptionFavicon((int) $match[1]); diff --git a/tests/cases/REST/NextCloudNews/TestV1_2.php b/tests/cases/REST/NextCloudNews/TestV1_2.php index d16e101..fd20160 100644 --- a/tests/cases/REST/NextCloudNews/TestV1_2.php +++ b/tests/cases/REST/NextCloudNews/TestV1_2.php @@ -11,13 +11,14 @@ use JKingWeb\Arsse\Conf; use JKingWeb\Arsse\User; use JKingWeb\Arsse\Database; use JKingWeb\Arsse\Service; -use JKingWeb\Arsse\REST\Request; use JKingWeb\Arsse\Test\Result; use JKingWeb\Arsse\Misc\Date; use JKingWeb\Arsse\Misc\Context; use JKingWeb\Arsse\Db\ExceptionInput; use JKingWeb\Arsse\Db\Transaction; use JKingWeb\Arsse\REST\NextCloudNews\V1_2; +use Psr\Http\Message\ResponseInterface; +use Zend\Diactoros\ServerRequest; use Zend\Diactoros\Response\JsonResponse as Response; use Zend\Diactoros\Response\EmptyResponse; use Phake; @@ -300,6 +301,40 @@ class TestV1_2 extends \JKingWeb\Arsse\Test\AbstractTest { ], ]; + protected function req(string $method, string $target, string $data = "", array $headers = []): ResponseInterface { + $url = "/index.php/apps/news/api/v1-2".$target; + $server = [ + 'REQUEST_METHOD' => $method, + 'REQUEST_URI' => $url, + 'PHP_AUTH_USER' => "john.doe@example.com", + 'PHP_AUTH_PW' => "secret", + 'REMOTE_USER' => "john.doe@example.com", + ]; + if (strlen($data)) { + $server['HTTP_CONTENT_TYPE'] = "application/json"; + } + $req = new ServerRequest($server, [], $url, $method, "php://memory"); + foreach($headers as $key => $value) { + if (!is_null($value)) { + $req = $req->withHeader($key, $value); + } else { + $req = $req->withoutHeader($key); + } + } + if (strlen($data)) { + $body = $req->getBody(); + $body->write($data); + $req = $req->withBody($body); + } + $q = $req->getUri()->getQuery(); + if (strlen($q)) { + parse_str($q, $q); + $req = $req->withQueryParams($q); + } + $req = $req->withRequestTarget($target); + return $this->h->dispatch($req); + } + public function setUp() { $this->clearData(); Arsse::$conf = new Conf(); @@ -321,7 +356,7 @@ class TestV1_2 extends \JKingWeb\Arsse\Test\AbstractTest { public function testSendAuthenticationChallenge() { Phake::when(Arsse::$user)->authHTTP->thenReturn(false); $exp = new EmptyResponse(401, ['WWW-Authenticate' => 'Basic realm="'.V1_2::REALM.'"']); - $this->assertResponse($exp, $this->h->dispatch(new Request("GET", "/"))); + $this->assertResponse($exp, $this->req("GET", "/")); } public function testRespondToInvalidPaths() { @@ -359,22 +394,23 @@ class TestV1_2 extends \JKingWeb\Arsse\Test\AbstractTest { foreach ($errs[404] as $req) { $exp = new EmptyResponse(404); list($method, $path) = $req; - $this->assertResponse($exp, $this->h->dispatch(new Request($method, $path)), "$method call to $path did not return 404."); + $this->assertResponse($exp, $this->req($method, $path), "$method call to $path did not return 404."); } foreach ($errs[405] as $allow => $cases) { $exp = new EmptyResponse(405, ['Allow' => $allow]); foreach ($cases as $req) { list($method, $path) = $req; - $this->assertResponse($exp, $this->h->dispatch(new Request($method, $path)), "$method call to $path did not return 405."); + $this->assertResponse($exp, $this->req($method, $path), "$method call to $path did not return 405."); } } } public function testRespondToInvalidInputTypes() { $exp = new EmptyResponse(415, ['Accept' => "application/json"]); - $this->assertResponse($exp, $this->h->dispatch(new Request("PUT", "/folders/1", '', 'application/xml'))); + $this->assertResponse($exp, $this->req("PUT", "/folders/1", '', ['Content-Type' => "application/xml"])); $exp = new EmptyResponse(400); - $this->assertResponse($exp, $this->h->dispatch(new Request("PUT", "/folders/1", '', 'application/json'))); + $this->assertResponse($exp, $this->req("PUT", "/folders/1", '')); + $this->assertResponse($exp, $this->req("PUT", "/folders/1", '', ['Content-Type' => null])); } public function testRespondToOptionsRequests() { @@ -382,19 +418,19 @@ class TestV1_2 extends \JKingWeb\Arsse\Test\AbstractTest { 'Allow' => "HEAD,GET,POST", 'Accept' => "application/json", ]); - $this->assertResponse($exp, $this->h->dispatch(new Request("OPTIONS", "/feeds"))); + $this->assertResponse($exp, $this->req("OPTIONS", "/feeds")); $exp = new EmptyResponse(204, [ 'Allow' => "DELETE", 'Accept' => "application/json", ]); - $this->assertResponse($exp, $this->h->dispatch(new Request("OPTIONS", "/feeds/2112"))); + $this->assertResponse($exp, $this->req("OPTIONS", "/feeds/2112")); $exp = new EmptyResponse(204, [ 'Allow' => "HEAD,GET", 'Accept' => "application/json", ]); - $this->assertResponse($exp, $this->h->dispatch(new Request("OPTIONS", "/user"))); + $this->assertResponse($exp, $this->req("OPTIONS", "/user")); $exp = new EmptyResponse(404); - $this->assertResponse($exp, $this->h->dispatch(new Request("OPTIONS", "/invalid/path"))); + $this->assertResponse($exp, $this->req("OPTIONS", "/invalid/path")); } public function testListFolders() { @@ -408,9 +444,9 @@ class TestV1_2 extends \JKingWeb\Arsse\Test\AbstractTest { ]; Phake::when(Arsse::$db)->folderList(Arsse::$user->id, null, false)->thenReturn(new Result([]))->thenReturn(new Result($list)); $exp = new Response(['folders' => []]); - $this->assertResponse($exp, $this->h->dispatch(new Request("GET", "/folders"))); + $this->assertResponse($exp, $this->req("GET", "/folders")); $exp = new Response(['folders' => $out]); - $this->assertResponse($exp, $this->h->dispatch(new Request("GET", "/folders"))); + $this->assertResponse($exp, $this->req("GET", "/folders")); } public function testAddAFolder() { @@ -438,33 +474,33 @@ class TestV1_2 extends \JKingWeb\Arsse\Test\AbstractTest { Phake::when(Arsse::$db)->folderAdd(Arsse::$user->id, ['name' => " "])->thenThrow(new ExceptionInput("whitespace")); // correctly add two folders, using different means $exp = new Response(['folders' => [$out[0]]]); - $this->assertResponse($exp, $this->h->dispatch(new Request("POST", "/folders", json_encode($in[0]), 'application/json'))); + $this->assertResponse($exp, $this->req("POST", "/folders", json_encode($in[0]))); $exp = new Response(['folders' => [$out[1]]]); - $this->assertResponse($exp, $this->h->dispatch(new Request("POST", "/folders?name=Hardware"))); + $this->assertResponse($exp, $this->req("POST", "/folders?name=Hardware")); Phake::verify(Arsse::$db)->folderAdd(Arsse::$user->id, $in[0]); Phake::verify(Arsse::$db)->folderAdd(Arsse::$user->id, $in[1]); Phake::verify(Arsse::$db)->folderPropertiesGet(Arsse::$user->id, 1); Phake::verify(Arsse::$db)->folderPropertiesGet(Arsse::$user->id, 2); // test bad folder names $exp = new EmptyResponse(422); - $this->assertResponse($exp, $this->h->dispatch(new Request("POST", "/folders"))); - $this->assertResponse($exp, $this->h->dispatch(new Request("POST", "/folders", '{"name":""}', 'application/json'))); - $this->assertResponse($exp, $this->h->dispatch(new Request("POST", "/folders", '{"name":" "}', 'application/json'))); - $this->assertResponse($exp, $this->h->dispatch(new Request("POST", "/folders", '{"name":{}}', 'application/json'))); + $this->assertResponse($exp, $this->req("POST", "/folders")); + $this->assertResponse($exp, $this->req("POST", "/folders", '{"name":""}')); + $this->assertResponse($exp, $this->req("POST", "/folders", '{"name":" "}')); + $this->assertResponse($exp, $this->req("POST", "/folders", '{"name":{}}')); // try adding the same two folders again $exp = new EmptyResponse(409); - $this->assertResponse($exp, $this->h->dispatch(new Request("POST", "/folders?name=Software"))); + $this->assertResponse($exp, $this->req("POST", "/folders?name=Software")); $exp = new EmptyResponse(409); - $this->assertResponse($exp, $this->h->dispatch(new Request("POST", "/folders", json_encode($in[1]), 'application/json'))); + $this->assertResponse($exp, $this->req("POST", "/folders", json_encode($in[1]))); } public function testRemoveAFolder() { Phake::when(Arsse::$db)->folderRemove(Arsse::$user->id, 1)->thenReturn(true)->thenThrow(new ExceptionInput("subjectMissing")); $exp = new EmptyResponse(204); - $this->assertResponse($exp, $this->h->dispatch(new Request("DELETE", "/folders/1"))); + $this->assertResponse($exp, $this->req("DELETE", "/folders/1")); // fail on the second invocation because it no longer exists $exp = new EmptyResponse(404); - $this->assertResponse($exp, $this->h->dispatch(new Request("DELETE", "/folders/1"))); + $this->assertResponse($exp, $this->req("DELETE", "/folders/1")); Phake::verify(Arsse::$db, Phake::times(2))->folderRemove(Arsse::$user->id, 1); } @@ -483,17 +519,17 @@ class TestV1_2 extends \JKingWeb\Arsse\Test\AbstractTest { Phake::when(Arsse::$db)->folderPropertiesSet(Arsse::$user->id, 1, $in[4])->thenReturn(true); // this should be stopped by the handler before the request gets to the database Phake::when(Arsse::$db)->folderPropertiesSet(Arsse::$user->id, 3, $this->anything())->thenThrow(new ExceptionInput("subjectMissing")); // folder ID 3 does not exist $exp = new EmptyResponse(204); - $this->assertResponse($exp, $this->h->dispatch(new Request("PUT", "/folders/1", json_encode($in[0]), 'application/json'))); + $this->assertResponse($exp, $this->req("PUT", "/folders/1", json_encode($in[0]))); $exp = new EmptyResponse(409); - $this->assertResponse($exp, $this->h->dispatch(new Request("PUT", "/folders/2", json_encode($in[1]), 'application/json'))); + $this->assertResponse($exp, $this->req("PUT", "/folders/2", json_encode($in[1]))); $exp = new EmptyResponse(422); - $this->assertResponse($exp, $this->h->dispatch(new Request("PUT", "/folders/1", json_encode($in[2]), 'application/json'))); + $this->assertResponse($exp, $this->req("PUT", "/folders/1", json_encode($in[2]))); $exp = new EmptyResponse(422); - $this->assertResponse($exp, $this->h->dispatch(new Request("PUT", "/folders/1", json_encode($in[3]), 'application/json'))); + $this->assertResponse($exp, $this->req("PUT", "/folders/1", json_encode($in[3]))); $exp = new EmptyResponse(422); - $this->assertResponse($exp, $this->h->dispatch(new Request("PUT", "/folders/1", json_encode($in[4]), 'application/json'))); + $this->assertResponse($exp, $this->req("PUT", "/folders/1", json_encode($in[4]))); $exp = new EmptyResponse(404); - $this->assertResponse($exp, $this->h->dispatch(new Request("PUT", "/folders/3", json_encode($in[0]), 'application/json'))); + $this->assertResponse($exp, $this->req("PUT", "/folders/3", json_encode($in[0]))); } public function testRetrieveServerVersion() { @@ -501,7 +537,7 @@ class TestV1_2 extends \JKingWeb\Arsse\Test\AbstractTest { 'version' => V1_2::VERSION, 'arsse_version' => Arsse::VERSION, ]); - $this->assertResponse($exp, $this->h->dispatch(new Request("GET", "/version"))); + $this->assertResponse($exp, $this->req("GET", "/version")); } public function testListSubscriptions() { @@ -518,9 +554,9 @@ class TestV1_2 extends \JKingWeb\Arsse\Test\AbstractTest { Phake::when(Arsse::$db)->articleStarred(Arsse::$user->id)->thenReturn(['total' => 0])->thenReturn(['total' => 5]); Phake::when(Arsse::$db)->editionLatest(Arsse::$user->id)->thenReturn(0)->thenReturn(4758915); $exp = new Response($exp1); - $this->assertResponse($exp, $this->h->dispatch(new Request("GET", "/feeds"))); + $this->assertResponse($exp, $this->req("GET", "/feeds")); $exp = new Response($exp2); - $this->assertResponse($exp, $this->h->dispatch(new Request("GET", "/feeds"))); + $this->assertResponse($exp, $this->req("GET", "/feeds")); } public function testAddASubscription() { @@ -553,31 +589,31 @@ class TestV1_2 extends \JKingWeb\Arsse\Test\AbstractTest { Phake::when(Arsse::$db)->subscriptionAdd(Arsse::$user->id, "http://example.net/news.atom")->thenThrow(new \JKingWeb\Arsse\Feed\Exception("http://example.net/news.atom", new \PicoFeed\Client\InvalidUrlException()))->thenReturn(47); // add the subscriptions $exp = new Response($out[0]); - $this->assertResponse($exp, $this->h->dispatch(new Request("POST", "/feeds", json_encode($in[0]), 'application/json'))); + $this->assertResponse($exp, $this->req("POST", "/feeds", json_encode($in[0]))); $exp = new Response($out[1]); - $this->assertResponse($exp, $this->h->dispatch(new Request("POST", "/feeds", json_encode($in[1]), 'application/json'))); + $this->assertResponse($exp, $this->req("POST", "/feeds", json_encode($in[1]))); // try to add them a second time $exp = new EmptyResponse(409); - $this->assertResponse($exp, $this->h->dispatch(new Request("POST", "/feeds", json_encode($in[0]), 'application/json'))); - $this->assertResponse($exp, $this->h->dispatch(new Request("POST", "/feeds", json_encode($in[1]), 'application/json'))); + $this->assertResponse($exp, $this->req("POST", "/feeds", json_encode($in[0]))); + $this->assertResponse($exp, $this->req("POST", "/feeds", json_encode($in[1]))); // try to add a bad feed $exp = new EmptyResponse(422); - $this->assertResponse($exp, $this->h->dispatch(new Request("POST", "/feeds", json_encode($in[2]), 'application/json'))); + $this->assertResponse($exp, $this->req("POST", "/feeds", json_encode($in[2]))); // try again (this will succeed), with an invalid folder ID $exp = new Response($out[2]); - $this->assertResponse($exp, $this->h->dispatch(new Request("POST", "/feeds", json_encode($in[3]), 'application/json'))); + $this->assertResponse($exp, $this->req("POST", "/feeds", json_encode($in[3]))); // try to add no feed $exp = new EmptyResponse(422); - $this->assertResponse($exp, $this->h->dispatch(new Request("POST", "/feeds", json_encode($in[4]), 'application/json'))); + $this->assertResponse($exp, $this->req("POST", "/feeds", json_encode($in[4]))); } public function testRemoveASubscription() { Phake::when(Arsse::$db)->subscriptionRemove(Arsse::$user->id, 1)->thenReturn(true)->thenThrow(new ExceptionInput("subjectMissing")); $exp = new EmptyResponse(204); - $this->assertResponse($exp, $this->h->dispatch(new Request("DELETE", "/feeds/1"))); + $this->assertResponse($exp, $this->req("DELETE", "/feeds/1")); // fail on the second invocation because it no longer exists $exp = new EmptyResponse(404); - $this->assertResponse($exp, $this->h->dispatch(new Request("DELETE", "/feeds/1"))); + $this->assertResponse($exp, $this->req("DELETE", "/feeds/1")); Phake::verify(Arsse::$db, Phake::times(2))->subscriptionRemove(Arsse::$user->id, 1); } @@ -596,17 +632,17 @@ class TestV1_2 extends \JKingWeb\Arsse\Test\AbstractTest { Phake::when(Arsse::$db)->subscriptionPropertiesSet(Arsse::$user->id, 1, ['folder' => -1])->thenThrow(new ExceptionInput("typeViolation")); // folder is invalid Phake::when(Arsse::$db)->subscriptionPropertiesSet(Arsse::$user->id, 42, $this->anything())->thenThrow(new ExceptionInput("subjectMissing")); // subscription does not exist $exp = new EmptyResponse(204); - $this->assertResponse($exp, $this->h->dispatch(new Request("PUT", "/feeds/1/move", json_encode($in[0]), 'application/json'))); + $this->assertResponse($exp, $this->req("PUT", "/feeds/1/move", json_encode($in[0]))); $exp = new EmptyResponse(204); - $this->assertResponse($exp, $this->h->dispatch(new Request("PUT", "/feeds/1/move", json_encode($in[1]), 'application/json'))); + $this->assertResponse($exp, $this->req("PUT", "/feeds/1/move", json_encode($in[1]))); $exp = new EmptyResponse(422); - $this->assertResponse($exp, $this->h->dispatch(new Request("PUT", "/feeds/1/move", json_encode($in[2]), 'application/json'))); + $this->assertResponse($exp, $this->req("PUT", "/feeds/1/move", json_encode($in[2]))); $exp = new EmptyResponse(404); - $this->assertResponse($exp, $this->h->dispatch(new Request("PUT", "/feeds/42/move", json_encode($in[3]), 'application/json'))); + $this->assertResponse($exp, $this->req("PUT", "/feeds/42/move", json_encode($in[3]))); $exp = new EmptyResponse(422); - $this->assertResponse($exp, $this->h->dispatch(new Request("PUT", "/feeds/1/move", json_encode($in[4]), 'application/json'))); + $this->assertResponse($exp, $this->req("PUT", "/feeds/1/move", json_encode($in[4]))); $exp = new EmptyResponse(422); - $this->assertResponse($exp, $this->h->dispatch(new Request("PUT", "/feeds/1/move", json_encode($in[5]), 'application/json'))); + $this->assertResponse($exp, $this->req("PUT", "/feeds/1/move", json_encode($in[5]))); } public function testRenameASubscription() { @@ -626,17 +662,17 @@ class TestV1_2 extends \JKingWeb\Arsse\Test\AbstractTest { Phake::when(Arsse::$db)->subscriptionPropertiesSet(Arsse::$user->id, 1, $this->identicalTo(['title' => false]))->thenThrow(new ExceptionInput("missing")); Phake::when(Arsse::$db)->subscriptionPropertiesSet(Arsse::$user->id, 42, $this->anything())->thenThrow(new ExceptionInput("subjectMissing")); $exp = new EmptyResponse(422); - $this->assertResponse($exp, $this->h->dispatch(new Request("PUT", "/feeds/1/rename", json_encode($in[0]), 'application/json'))); + $this->assertResponse($exp, $this->req("PUT", "/feeds/1/rename", json_encode($in[0]))); $exp = new EmptyResponse(204); - $this->assertResponse($exp, $this->h->dispatch(new Request("PUT", "/feeds/1/rename", json_encode($in[1]), 'application/json'))); + $this->assertResponse($exp, $this->req("PUT", "/feeds/1/rename", json_encode($in[1]))); $exp = new EmptyResponse(422); - $this->assertResponse($exp, $this->h->dispatch(new Request("PUT", "/feeds/1/rename", json_encode($in[2]), 'application/json'))); + $this->assertResponse($exp, $this->req("PUT", "/feeds/1/rename", json_encode($in[2]))); $exp = new EmptyResponse(422); - $this->assertResponse($exp, $this->h->dispatch(new Request("PUT", "/feeds/1/rename", json_encode($in[3]), 'application/json'))); + $this->assertResponse($exp, $this->req("PUT", "/feeds/1/rename", json_encode($in[3]))); $exp = new EmptyResponse(404); - $this->assertResponse($exp, $this->h->dispatch(new Request("PUT", "/feeds/42/rename", json_encode($in[4]), 'application/json'))); + $this->assertResponse($exp, $this->req("PUT", "/feeds/42/rename", json_encode($in[4]))); $exp = new EmptyResponse(422); - $this->assertResponse($exp, $this->h->dispatch(new Request("PUT", "/feeds/1/rename", json_encode($in[6]), 'application/json'))); + $this->assertResponse($exp, $this->req("PUT", "/feeds/1/rename", json_encode($in[6]))); } public function testListStaleFeeds() { @@ -652,11 +688,11 @@ class TestV1_2 extends \JKingWeb\Arsse\Test\AbstractTest { ]; Phake::when(Arsse::$db)->feedListStale->thenReturn(array_column($out, "id")); $exp = new Response(['feeds' => $out]); - $this->assertResponse($exp, $this->h->dispatch(new Request("GET", "/feeds/all"))); + $this->assertResponse($exp, $this->req("GET", "/feeds/all")); // retrieving the list when not an admin fails Phake::when(Arsse::$user)->rightsGet->thenReturn(0); $exp = new EmptyResponse(403); - $this->assertResponse($exp, $this->h->dispatch(new Request("GET", "/feeds/all"))); + $this->assertResponse($exp, $this->req("GET", "/feeds/all")); } public function testUpdateAFeed() { @@ -671,17 +707,17 @@ class TestV1_2 extends \JKingWeb\Arsse\Test\AbstractTest { Phake::when(Arsse::$db)->feedUpdate(2112)->thenThrow(new ExceptionInput("subjectMissing")); Phake::when(Arsse::$db)->feedUpdate($this->lessThan(1))->thenThrow(new ExceptionInput("typeViolation")); $exp = new EmptyResponse(204); - $this->assertResponse($exp, $this->h->dispatch(new Request("GET", "/feeds/update", json_encode($in[0]), 'application/json'))); + $this->assertResponse($exp, $this->req("GET", "/feeds/update", json_encode($in[0]))); $exp = new EmptyResponse(404); - $this->assertResponse($exp, $this->h->dispatch(new Request("GET", "/feeds/update", json_encode($in[1]), 'application/json'))); + $this->assertResponse($exp, $this->req("GET", "/feeds/update", json_encode($in[1]))); $exp = new EmptyResponse(422); - $this->assertResponse($exp, $this->h->dispatch(new Request("GET", "/feeds/update", json_encode($in[2]), 'application/json'))); - $this->assertResponse($exp, $this->h->dispatch(new Request("GET", "/feeds/update", json_encode($in[3]), 'application/json'))); - $this->assertResponse($exp, $this->h->dispatch(new Request("GET", "/feeds/update", json_encode($in[4]), 'application/json'))); + $this->assertResponse($exp, $this->req("GET", "/feeds/update", json_encode($in[2]))); + $this->assertResponse($exp, $this->req("GET", "/feeds/update", json_encode($in[3]))); + $this->assertResponse($exp, $this->req("GET", "/feeds/update", json_encode($in[4]))); // updating a feed when not an admin fails Phake::when(Arsse::$user)->rightsGet->thenReturn(0); $exp = new EmptyResponse(403); - $this->assertResponse($exp, $this->h->dispatch(new Request("GET", "/feeds/update", json_encode($in[0]), 'application/json'))); + $this->assertResponse($exp, $this->req("GET", "/feeds/update", json_encode($in[0]))); } public function testListArticles() { @@ -708,23 +744,23 @@ class TestV1_2 extends \JKingWeb\Arsse\Test\AbstractTest { Phake::when(Arsse::$db)->articleList(Arsse::$user->id, (new Context)->reverse(true)->folder(-1), Database::LIST_TYPICAL)->thenThrow(new ExceptionInput("typeViolation")); $exp = new Response(['items' => $this->articles['rest']]); // check the contents of the response - $this->assertResponse($exp, $this->h->dispatch(new Request("GET", "/items"))); // first instance of base context - $this->assertResponse($exp, $this->h->dispatch(new Request("GET", "/items/updated"))); // second instance of base context + $this->assertResponse($exp, $this->req("GET", "/items")); // first instance of base context + $this->assertResponse($exp, $this->req("GET", "/items/updated")); // second instance of base context // check error conditions $exp = new EmptyResponse(422); - $this->assertResponse($exp, $this->h->dispatch(new Request("GET", "/items", json_encode($in[0]), 'application/json'))); - $this->assertResponse($exp, $this->h->dispatch(new Request("GET", "/items", json_encode($in[1]), 'application/json'))); - $this->assertResponse($exp, $this->h->dispatch(new Request("GET", "/items", json_encode($in[2]), 'application/json'))); - $this->assertResponse($exp, $this->h->dispatch(new Request("GET", "/items", json_encode($in[3]), 'application/json'))); + $this->assertResponse($exp, $this->req("GET", "/items", json_encode($in[0]))); + $this->assertResponse($exp, $this->req("GET", "/items", json_encode($in[1]))); + $this->assertResponse($exp, $this->req("GET", "/items", json_encode($in[2]))); + $this->assertResponse($exp, $this->req("GET", "/items", json_encode($in[3]))); // simply run through the remainder of the input for later method verification - $this->h->dispatch(new Request("GET", "/items", json_encode($in[4]), 'application/json')); - $this->h->dispatch(new Request("GET", "/items", json_encode($in[5]), 'application/json')); // third instance of base context - $this->h->dispatch(new Request("GET", "/items", json_encode($in[6]), 'application/json')); - $this->h->dispatch(new Request("GET", "/items", json_encode($in[7]), 'application/json')); - $this->h->dispatch(new Request("GET", "/items", json_encode($in[8]), 'application/json')); // fourth instance of base context - $this->h->dispatch(new Request("GET", "/items", json_encode($in[9]), 'application/json')); - $this->h->dispatch(new Request("GET", "/items", json_encode($in[10]), 'application/json')); - $this->h->dispatch(new Request("GET", "/items", json_encode($in[11]), 'application/json')); + $this->req("GET", "/items", json_encode($in[4])); + $this->req("GET", "/items", json_encode($in[5])); // third instance of base context + $this->req("GET", "/items", json_encode($in[6])); + $this->req("GET", "/items", json_encode($in[7])); + $this->req("GET", "/items", json_encode($in[8])); // fourth instance of base context + $this->req("GET", "/items", json_encode($in[9])); + $this->req("GET", "/items", json_encode($in[10])); + $this->req("GET", "/items", json_encode($in[11])); // perform method verifications Phake::verify(Arsse::$db, Phake::times(4))->articleList(Arsse::$user->id, (new Context)->reverse(true), Database::LIST_TYPICAL); Phake::verify(Arsse::$db)->articleList(Arsse::$user->id, (new Context)->reverse(true)->subscription(42), Database::LIST_TYPICAL); @@ -745,13 +781,13 @@ class TestV1_2 extends \JKingWeb\Arsse\Test\AbstractTest { Phake::when(Arsse::$db)->articleMark(Arsse::$user->id, $read, (new Context)->folder(1)->latestEdition(2112))->thenReturn(42); Phake::when(Arsse::$db)->articleMark(Arsse::$user->id, $read, (new Context)->folder(42)->latestEdition(2112))->thenThrow(new ExceptionInput("idMissing")); // folder doesn't exist $exp = new EmptyResponse(204); - $this->assertResponse($exp, $this->h->dispatch(new Request("PUT", "/folders/1/read", $in, 'application/json'))); - $this->assertResponse($exp, $this->h->dispatch(new Request("PUT", "/folders/1/read?newestItemId=2112"))); + $this->assertResponse($exp, $this->req("PUT", "/folders/1/read", $in)); + $this->assertResponse($exp, $this->req("PUT", "/folders/1/read?newestItemId=2112")); $exp = new EmptyResponse(422); - $this->assertResponse($exp, $this->h->dispatch(new Request("PUT", "/folders/1/read"))); - $this->assertResponse($exp, $this->h->dispatch(new Request("PUT", "/folders/1/read?newestItemId=ook"))); + $this->assertResponse($exp, $this->req("PUT", "/folders/1/read")); + $this->assertResponse($exp, $this->req("PUT", "/folders/1/read?newestItemId=ook")); $exp = new EmptyResponse(404); - $this->assertResponse($exp, $this->h->dispatch(new Request("PUT", "/folders/42/read", $in, 'application/json'))); + $this->assertResponse($exp, $this->req("PUT", "/folders/42/read", $in)); } public function testMarkASubscriptionRead() { @@ -760,13 +796,13 @@ class TestV1_2 extends \JKingWeb\Arsse\Test\AbstractTest { Phake::when(Arsse::$db)->articleMark(Arsse::$user->id, $read, (new Context)->subscription(1)->latestEdition(2112))->thenReturn(42); Phake::when(Arsse::$db)->articleMark(Arsse::$user->id, $read, (new Context)->subscription(42)->latestEdition(2112))->thenThrow(new ExceptionInput("idMissing")); // subscription doesn't exist $exp = new EmptyResponse(204); - $this->assertResponse($exp, $this->h->dispatch(new Request("PUT", "/feeds/1/read", $in, 'application/json'))); - $this->assertResponse($exp, $this->h->dispatch(new Request("PUT", "/feeds/1/read?newestItemId=2112"))); + $this->assertResponse($exp, $this->req("PUT", "/feeds/1/read", $in)); + $this->assertResponse($exp, $this->req("PUT", "/feeds/1/read?newestItemId=2112")); $exp = new EmptyResponse(422); - $this->assertResponse($exp, $this->h->dispatch(new Request("PUT", "/feeds/1/read"))); - $this->assertResponse($exp, $this->h->dispatch(new Request("PUT", "/feeds/1/read?newestItemId=ook"))); + $this->assertResponse($exp, $this->req("PUT", "/feeds/1/read")); + $this->assertResponse($exp, $this->req("PUT", "/feeds/1/read?newestItemId=ook")); $exp = new EmptyResponse(404); - $this->assertResponse($exp, $this->h->dispatch(new Request("PUT", "/feeds/42/read", $in, 'application/json'))); + $this->assertResponse($exp, $this->req("PUT", "/feeds/42/read", $in)); } public function testMarkAllItemsRead() { @@ -774,11 +810,11 @@ class TestV1_2 extends \JKingWeb\Arsse\Test\AbstractTest { $in = json_encode(['newestItemId' => 2112]); Phake::when(Arsse::$db)->articleMark(Arsse::$user->id, $read, (new Context)->latestEdition(2112))->thenReturn(42); $exp = new EmptyResponse(204); - $this->assertResponse($exp, $this->h->dispatch(new Request("PUT", "/items/read", $in, 'application/json'))); - $this->assertResponse($exp, $this->h->dispatch(new Request("PUT", "/items/read?newestItemId=2112"))); + $this->assertResponse($exp, $this->req("PUT", "/items/read", $in)); + $this->assertResponse($exp, $this->req("PUT", "/items/read?newestItemId=2112")); $exp = new EmptyResponse(422); - $this->assertResponse($exp, $this->h->dispatch(new Request("PUT", "/items/read"))); - $this->assertResponse($exp, $this->h->dispatch(new Request("PUT", "/items/read?newestItemId=ook"))); + $this->assertResponse($exp, $this->req("PUT", "/items/read")); + $this->assertResponse($exp, $this->req("PUT", "/items/read?newestItemId=ook")); } public function testChangeMarksOfASingleArticle() { @@ -795,15 +831,15 @@ class TestV1_2 extends \JKingWeb\Arsse\Test\AbstractTest { Phake::when(Arsse::$db)->articleMark(Arsse::$user->id, $unstar, (new Context)->article(4))->thenReturn(42); Phake::when(Arsse::$db)->articleMark(Arsse::$user->id, $unstar, (new Context)->article(1337))->thenThrow(new ExceptionInput("subjectMissing")); // article doesn't exist doesn't exist $exp = new EmptyResponse(204); - $this->assertResponse($exp, $this->h->dispatch(new Request("PUT", "/items/1/read"))); - $this->assertResponse($exp, $this->h->dispatch(new Request("PUT", "/items/2/unread"))); - $this->assertResponse($exp, $this->h->dispatch(new Request("PUT", "/items/1/3/star"))); - $this->assertResponse($exp, $this->h->dispatch(new Request("PUT", "/items/4400/4/unstar"))); + $this->assertResponse($exp, $this->req("PUT", "/items/1/read")); + $this->assertResponse($exp, $this->req("PUT", "/items/2/unread")); + $this->assertResponse($exp, $this->req("PUT", "/items/1/3/star")); + $this->assertResponse($exp, $this->req("PUT", "/items/4400/4/unstar")); $exp = new EmptyResponse(404); - $this->assertResponse($exp, $this->h->dispatch(new Request("PUT", "/items/42/read"))); - $this->assertResponse($exp, $this->h->dispatch(new Request("PUT", "/items/47/unread"))); - $this->assertResponse($exp, $this->h->dispatch(new Request("PUT", "/items/1/2112/star"))); - $this->assertResponse($exp, $this->h->dispatch(new Request("PUT", "/items/4400/1337/unstar"))); + $this->assertResponse($exp, $this->req("PUT", "/items/42/read")); + $this->assertResponse($exp, $this->req("PUT", "/items/47/unread")); + $this->assertResponse($exp, $this->req("PUT", "/items/1/2112/star")); + $this->assertResponse($exp, $this->req("PUT", "/items/4400/1337/unstar")); Phake::verify(Arsse::$db, Phake::times(8))->articleMark(Arsse::$user->id, $this->anything(), $this->anything()); } @@ -826,26 +862,26 @@ class TestV1_2 extends \JKingWeb\Arsse\Test\AbstractTest { Phake::when(Arsse::$db)->articleMark(Arsse::$user->id, $this->anything(), (new Context)->editions([]))->thenThrow(new ExceptionInput("tooShort")); // data model function requires one valid integer for multiples Phake::when(Arsse::$db)->articleMark(Arsse::$user->id, $this->anything(), (new Context)->articles([]))->thenThrow(new ExceptionInput("tooShort")); // data model function requires one valid integer for multiples $exp = new EmptyResponse(204); - $this->assertResponse($exp, $this->h->dispatch(new Request("PUT", "/items/read/multiple"))); - $this->assertResponse($exp, $this->h->dispatch(new Request("PUT", "/items/unread/multiple"))); - $this->assertResponse($exp, $this->h->dispatch(new Request("PUT", "/items/star/multiple"))); - $this->assertResponse($exp, $this->h->dispatch(new Request("PUT", "/items/unstar/multiple"))); - $this->assertResponse($exp, $this->h->dispatch(new Request("PUT", "/items/read/multiple", json_encode(['items' => "ook"]), 'application/json'))); - $this->assertResponse($exp, $this->h->dispatch(new Request("PUT", "/items/unread/multiple", json_encode(['items' => "ook"]), 'application/json'))); - $this->assertResponse($exp, $this->h->dispatch(new Request("PUT", "/items/star/multiple", json_encode(['items' => "ook"]), 'application/json'))); - $this->assertResponse($exp, $this->h->dispatch(new Request("PUT", "/items/unstar/multiple", json_encode(['items' => "ook"]), 'application/json'))); - $this->assertResponse($exp, $this->h->dispatch(new Request("PUT", "/items/read/multiple", json_encode(['items' => []]), 'application/json'))); - $this->assertResponse($exp, $this->h->dispatch(new Request("PUT", "/items/unread/multiple", json_encode(['items' => []]), 'application/json'))); - $this->assertResponse($exp, $this->h->dispatch(new Request("PUT", "/items/read/multiple", json_encode(['items' => $in[0]]), 'application/json'))); - $this->assertResponse($exp, $this->h->dispatch(new Request("PUT", "/items/unread/multiple", json_encode(['items' => $in[0]]), 'application/json'))); - $this->assertResponse($exp, $this->h->dispatch(new Request("PUT", "/items/read/multiple", json_encode(['items' => $in[1]]), 'application/json'))); - $this->assertResponse($exp, $this->h->dispatch(new Request("PUT", "/items/unread/multiple", json_encode(['items' => $in[1]]), 'application/json'))); - $this->assertResponse($exp, $this->h->dispatch(new Request("PUT", "/items/star/multiple", json_encode(['items' => []]), 'application/json'))); - $this->assertResponse($exp, $this->h->dispatch(new Request("PUT", "/items/unstar/multiple", json_encode(['items' => []]), 'application/json'))); - $this->assertResponse($exp, $this->h->dispatch(new Request("PUT", "/items/star/multiple", json_encode(['items' => $inStar[0]]), 'application/json'))); - $this->assertResponse($exp, $this->h->dispatch(new Request("PUT", "/items/unstar/multiple", json_encode(['items' => $inStar[0]]), 'application/json'))); - $this->assertResponse($exp, $this->h->dispatch(new Request("PUT", "/items/star/multiple", json_encode(['items' => $inStar[1]]), 'application/json'))); - $this->assertResponse($exp, $this->h->dispatch(new Request("PUT", "/items/unstar/multiple", json_encode(['items' => $inStar[1]]), 'application/json'))); + $this->assertResponse($exp, $this->req("PUT", "/items/read/multiple")); + $this->assertResponse($exp, $this->req("PUT", "/items/unread/multiple")); + $this->assertResponse($exp, $this->req("PUT", "/items/star/multiple")); + $this->assertResponse($exp, $this->req("PUT", "/items/unstar/multiple")); + $this->assertResponse($exp, $this->req("PUT", "/items/read/multiple", json_encode(['items' => "ook"]))); + $this->assertResponse($exp, $this->req("PUT", "/items/unread/multiple", json_encode(['items' => "ook"]))); + $this->assertResponse($exp, $this->req("PUT", "/items/star/multiple", json_encode(['items' => "ook"]))); + $this->assertResponse($exp, $this->req("PUT", "/items/unstar/multiple", json_encode(['items' => "ook"]))); + $this->assertResponse($exp, $this->req("PUT", "/items/read/multiple", json_encode(['items' => []]))); + $this->assertResponse($exp, $this->req("PUT", "/items/unread/multiple", json_encode(['items' => []]))); + $this->assertResponse($exp, $this->req("PUT", "/items/read/multiple", json_encode(['items' => $in[0]]))); + $this->assertResponse($exp, $this->req("PUT", "/items/unread/multiple", json_encode(['items' => $in[0]]))); + $this->assertResponse($exp, $this->req("PUT", "/items/read/multiple", json_encode(['items' => $in[1]]))); + $this->assertResponse($exp, $this->req("PUT", "/items/unread/multiple", json_encode(['items' => $in[1]]))); + $this->assertResponse($exp, $this->req("PUT", "/items/star/multiple", json_encode(['items' => []]))); + $this->assertResponse($exp, $this->req("PUT", "/items/unstar/multiple", json_encode(['items' => []]))); + $this->assertResponse($exp, $this->req("PUT", "/items/star/multiple", json_encode(['items' => $inStar[0]]))); + $this->assertResponse($exp, $this->req("PUT", "/items/unstar/multiple", json_encode(['items' => $inStar[0]]))); + $this->assertResponse($exp, $this->req("PUT", "/items/star/multiple", json_encode(['items' => $inStar[1]]))); + $this->assertResponse($exp, $this->req("PUT", "/items/unstar/multiple", json_encode(['items' => $inStar[1]]))); // ensure the data model was queried appropriately for read/unread Phake::verify(Arsse::$db, Phake::atLeast(1))->articleMark(Arsse::$user->id, $read, (new Context)->editions([])); Phake::verify(Arsse::$db, Phake::atLeast(1))->articleMark(Arsse::$user->id, $read, (new Context)->editions($in[0])); @@ -879,28 +915,28 @@ class TestV1_2 extends \JKingWeb\Arsse\Test\AbstractTest { $arr2['warnings']['improperlyConfiguredCron'] = true; $arr2['warnings']['incorrectDbCharset'] = true; $exp = new Response($arr1); - $this->assertResponse($exp, $this->h->dispatch(new Request("GET", "/status"))); + $this->assertResponse($exp, $this->req("GET", "/status")); } public function testCleanUpBeforeUpdate() { Phake::when(Arsse::$db)->feedCleanup()->thenReturn(true); $exp = new EmptyResponse(204); - $this->assertResponse($exp, $this->h->dispatch(new Request("GET", "/cleanup/before-update"))); + $this->assertResponse($exp, $this->req("GET", "/cleanup/before-update")); Phake::verify(Arsse::$db)->feedCleanup(); // performing a cleanup when not an admin fails Phake::when(Arsse::$user)->rightsGet->thenReturn(0); $exp = new EmptyResponse(403); - $this->assertResponse($exp, $this->h->dispatch(new Request("GET", "/cleanup/before-update"))); + $this->assertResponse($exp, $this->req("GET", "/cleanup/before-update")); } public function testCleanUpAfterUpdate() { Phake::when(Arsse::$db)->articleCleanup()->thenReturn(true); $exp = new EmptyResponse(204); - $this->assertResponse($exp, $this->h->dispatch(new Request("GET", "/cleanup/after-update"))); + $this->assertResponse($exp, $this->req("GET", "/cleanup/after-update")); Phake::verify(Arsse::$db)->articleCleanup(); // performing a cleanup when not an admin fails Phake::when(Arsse::$user)->rightsGet->thenReturn(0); $exp = new EmptyResponse(403); - $this->assertResponse($exp, $this->h->dispatch(new Request("GET", "/cleanup/after-update"))); + $this->assertResponse($exp, $this->req("GET", "/cleanup/after-update")); } } diff --git a/tests/cases/REST/NextCloudNews/TestVersions.php b/tests/cases/REST/NextCloudNews/TestVersions.php index 6904d3e..3f66b42 100644 --- a/tests/cases/REST/NextCloudNews/TestVersions.php +++ b/tests/cases/REST/NextCloudNews/TestVersions.php @@ -7,7 +7,8 @@ declare(strict_types=1); namespace JKingWeb\Arsse\TestCase\REST\NextCloudNews; use JKingWeb\Arsse\REST\NextCloudNews\Versions; -use JKingWeb\Arsse\REST\Request; +use Psr\Http\Message\ResponseInterface; +use Zend\Diactoros\ServerRequest; use Zend\Diactoros\Response\JsonResponse as Response; use Zend\Diactoros\Response\EmptyResponse; @@ -17,44 +18,37 @@ class TestVersions extends \JKingWeb\Arsse\Test\AbstractTest { $this->clearData(); } + protected function req(string $method, string $target): ResponseInterface { + $url = "/index.php/apps/news/api".$target; + $server = [ + 'REQUEST_METHOD' => $method, + 'REQUEST_URI' => $url, + ]; + $req = new ServerRequest($server, [], $url, $method, "php://memory"); + $req = $req->withRequestTarget($target); + return (new Versions)->dispatch($req); + } + public function testFetchVersionList() { $exp = new Response(['apiLevels' => ['v1-2']]); - $h = new Versions; - $req = new Request("GET", "/"); - $res = $h->dispatch($req); - $this->assertResponse($exp, $res); - $req = new Request("GET", ""); - $res = $h->dispatch($req); - $this->assertResponse($exp, $res); - $req = new Request("GET", "/?id=1827"); - $res = $h->dispatch($req); - $this->assertResponse($exp, $res); + $this->assertResponse($exp, $this->req("GET", "/")); + $this->assertResponse($exp, $this->req("GET", "/")); + $this->assertResponse($exp, $this->req("GET", "/")); } public function testRespondToOptionsRequest() { $exp = new EmptyResponse(204, ['Allow' => "HEAD,GET"]); - $h = new Versions; - $req = new Request("OPTIONS", "/"); - $res = $h->dispatch($req); - $this->assertResponse($exp, $res); + $this->assertResponse($exp, $this->req("OPTIONS", "/")); } public function testUseIncorrectMethod() { $exp = new EmptyResponse(405, ['Allow' => "HEAD,GET"]); - $h = new Versions; - $req = new Request("POST", "/"); - $res = $h->dispatch($req); - $this->assertResponse($exp, $res); + $this->assertResponse($exp, $this->req("POST", "/")); } public function testUseIncorrectPath() { $exp = new EmptyResponse(404); - $h = new Versions; - $req = new Request("GET", "/ook"); - $res = $h->dispatch($req); - $this->assertResponse($exp, $res); - $req = new Request("OPTIONS", "/ook"); - $res = $h->dispatch($req); - $this->assertResponse($exp, $res); + $this->assertResponse($exp, $this->req("GET", "/ook")); + $this->assertResponse($exp, $this->req("OPTIONS", "/ook")); } } diff --git a/tests/cases/REST/TinyTinyRSS/TestAPI.php b/tests/cases/REST/TinyTinyRSS/TestAPI.php index d2f1ff3..988c2db 100644 --- a/tests/cases/REST/TinyTinyRSS/TestAPI.php +++ b/tests/cases/REST/TinyTinyRSS/TestAPI.php @@ -19,6 +19,7 @@ use JKingWeb\Arsse\Db\ExceptionInput; use JKingWeb\Arsse\Db\Transaction; use JKingWeb\Arsse\REST\TinyTinyRSS\API; use Psr\Http\Message\ResponseInterface; +use Zend\Diactoros\ServerRequest; use Zend\Diactoros\Response\JsonResponse as Response; use Zend\Diactoros\Response\EmptyResponse; use Phake; @@ -124,8 +125,22 @@ class TestAPI extends \JKingWeb\Arsse\Test\AbstractTest { LONG_STRING; - protected function req($data): ResponseInterface { - return $this->h->dispatch(new Request("POST", "", json_encode($data))); + protected function req($data, string $method = "POST", string $target = "", string $strData = null): ResponseInterface { + $url = "/tt-rss/api".$target; + $server = [ + 'REQUEST_METHOD' => $method, + 'REQUEST_URI' => $url, + 'HTTP_CONTENT_TYPE' => "application/x-www-form-urlencoded", + ]; + $req = new ServerRequest($server, [], $url, $method, "php://memory"); + $body = $req->getBody(); + if (!is_null($strData)) { + $body->write($strData); + } else { + $body->write(json_encode($data)); + } + $req = $req->withBody($body)->withRequestTarget($target); + return $this->h->dispatch($req); } protected function respGood($content = null, $seq = 0): Response { @@ -172,11 +187,11 @@ LONG_STRING; public function testHandleInvalidPaths() { $exp = $this->respErr("MALFORMED_INPUT", [], null); - $this->assertResponse($exp, $this->h->dispatch(new Request("POST", "", ""))); - $this->assertResponse($exp, $this->h->dispatch(new Request("POST", "/", ""))); - $this->assertResponse($exp, $this->h->dispatch(new Request("POST", "/index.php", ""))); + $this->assertResponse($exp, $this->req(null, "POST", "", "")); + $this->assertResponse($exp, $this->req(null, "POST", "/", "")); + $this->assertResponse($exp, $this->req(null, "POST", "/index.php", "")); $exp = new EmptyResponse(404); - $this->assertResponse($exp, $this->h->dispatch(new Request("POST", "/bad/path", ""))); + $this->assertResponse($exp, $this->req(null, "POST", "/bad/path", "")); } public function testHandleOptionsRequest() { @@ -184,13 +199,13 @@ LONG_STRING; 'Allow' => "POST", 'Accept' => "application/json, text/json", ]); - $this->assertResponse($exp, $this->h->dispatch(new Request("OPTIONS", ""))); + $this->assertResponse($exp, $this->req(null, "OPTIONS", "", "")); } public function testHandleInvalidData() { $exp = $this->respErr("MALFORMED_INPUT", [], null); - $this->assertResponse($exp, $this->h->dispatch(new Request("POST", "", "This is not valid JSON data"))); - $this->assertResponse($exp, $this->h->dispatch(new Request("POST", "", ""))); // lack of data is also an error + $this->assertResponse($exp, $this->req(null, "POST", "", "This is not valid JSON data")); + $this->assertResponse($exp, $this->req(null, "POST", "", "")); // lack of data is also an error } public function testLogIn() { diff --git a/tests/cases/REST/TinyTinyRSS/TestIcon.php b/tests/cases/REST/TinyTinyRSS/TestIcon.php index c5c67bc..548ab50 100644 --- a/tests/cases/REST/TinyTinyRSS/TestIcon.php +++ b/tests/cases/REST/TinyTinyRSS/TestIcon.php @@ -12,6 +12,8 @@ use JKingWeb\Arsse\User; use JKingWeb\Arsse\Database; use JKingWeb\Arsse\REST\TinyTinyRSS\Icon; use JKingWeb\Arsse\REST\Request; +use Psr\Http\Message\ResponseInterface; +use Zend\Diactoros\ServerRequest; use Zend\Diactoros\Response\EmptyResponse as Response; use Phake; @@ -32,6 +34,17 @@ class TestIcon extends \JKingWeb\Arsse\Test\AbstractTest { $this->clearData(); } + protected function req(string $target, $method = "GET"): ResponseInterface { + $url = "/tt-rss/feed-icons/".$target; + $server = [ + 'REQUEST_METHOD' => $method, + 'REQUEST_URI' => $url, + ]; + $req = new ServerRequest($server, [], $url, $method, "php://memory"); + $req = $req->withRequestTarget($target); + return $this->h->dispatch($req); + } + public function testRetrieveFavion() { Phake::when(Arsse::$db)->subscriptionFavicon->thenReturn(""); Phake::when(Arsse::$db)->subscriptionFavicon(42)->thenReturn("http://example.com/favicon.ico"); @@ -39,19 +52,19 @@ class TestIcon extends \JKingWeb\Arsse\Test\AbstractTest { Phake::when(Arsse::$db)->subscriptionFavicon(1337)->thenReturn("http://example.org/icon.gif\r\nLocation: http://bad.example.com/"); // these requests should succeed $exp = new Response(301, ['Location' => "http://example.com/favicon.ico"]); - $this->assertResponse($exp, $this->h->dispatch(new Request("GET", "42.ico"))); + $this->assertResponse($exp, $this->req("42.ico")); $exp = new Response(301, ['Location' => "http://example.net/logo.png"]); - $this->assertResponse($exp, $this->h->dispatch(new Request("GET", "2112.ico"))); + $this->assertResponse($exp, $this->req("2112.ico")); $exp = new Response(301, ['Location' => "http://example.org/icon.gif"]); - $this->assertResponse($exp, $this->h->dispatch(new Request("GET", "1337.ico"))); + $this->assertResponse($exp, $this->req("1337.ico")); // these requests should fail $exp = new Response(404); - $this->assertResponse($exp, $this->h->dispatch(new Request("GET", "ook.ico"))); - $this->assertResponse($exp, $this->h->dispatch(new Request("GET", "ook"))); - $this->assertResponse($exp, $this->h->dispatch(new Request("GET", "47.ico"))); - $this->assertResponse($exp, $this->h->dispatch(new Request("GET", "2112.png"))); + $this->assertResponse($exp, $this->req("ook.ico")); + $this->assertResponse($exp, $this->req("ook")); + $this->assertResponse($exp, $this->req("47.ico")); + $this->assertResponse($exp, $this->req("2112.png")); // only GET is allowed $exp = new Response(405, ['Allow' => "GET"]); - $this->assertResponse($exp, $this->h->dispatch(new Request("PUT", "2112.ico"))); + $this->assertResponse($exp, $this->req("2112.ico", "PUT")); } } diff --git a/tests/lib/AbstractTest.php b/tests/lib/AbstractTest.php index 762e991..c3188a6 100644 --- a/tests/lib/AbstractTest.php +++ b/tests/lib/AbstractTest.php @@ -33,13 +33,13 @@ abstract class AbstractTest extends \PHPUnit\Framework\TestCase { } protected function assertResponse(ResponseInterface $exp, ResponseInterface $act, string $text = null) { - $this->assertEquals($exp->getHeaders(), $act->getHeaders(), $text); $this->assertEquals($exp->getStatusCode(), $act->getStatusCode(), $text); $this->assertInstanceOf(get_class($exp), $act); if ($exp instanceof JsonResponse) { $this->assertEquals($exp->getPayload(), $act->getPayload(), $text); $this->assertSame($exp->getPayload(), $act->getPayload(), $text); } + $this->assertEquals($exp->getHeaders(), $act->getHeaders(), $text); } public function approximateTime($exp, $act) {