1
1
Fork 0
mirror of https://code.mensbeam.com/MensBeam/Arsse.git synced 2024-12-22 13:12:41 +00:00

Replace Resquest objects with PSR-7 request messages; improves #53

This commit is contained in:
J. King 2018-01-04 23:08:53 -05:00
parent 9ad0b47201
commit 890f9b07d4
12 changed files with 327 additions and 329 deletions

View file

@ -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 = [];

View file

@ -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;
}

View file

@ -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]);

View file

@ -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"]);
}
}
}

View file

@ -1,89 +0,0 @@
<?php
/** @license MIT
* Copyright 2017 J. King, Dustin Wilson et al.
* See LICENSE and AUTHORS files for details */
declare(strict_types=1);
namespace JKingWeb\Arsse\REST;
class Request {
public $method = "GET";
public $head = false;
public $url = "";
public $path ="";
public $paths = [];
public $query = "";
public $type ="";
public $body = "";
public function __construct(string $method = null, string $url = null, string $body = null, string $contentType = null) {
$method = $method ?? $_SERVER['REQUEST_METHOD'];
$url = $url ?? $_SERVER['REQUEST_URI'];
$body = $body ?? file_get_contents("php://input");
if (is_null($contentType)) {
if (isset($_SERVER['HTTP_CONTENT_TYPE'])) {
$contentType = $_SERVER['HTTP_CONTENT_TYPE'];
} else {
$contentType = "";
}
}
$this->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;
}
}

View file

@ -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);
}

View file

@ -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]);

View file

@ -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", '<data/>', 'application/xml')));
$this->assertResponse($exp, $this->req("PUT", "/folders/1", '<data/>', ['Content-Type' => "application/xml"]));
$exp = new EmptyResponse(400);
$this->assertResponse($exp, $this->h->dispatch(new Request("PUT", "/folders/1", '<data/>', 'application/json')));
$this->assertResponse($exp, $this->req("PUT", "/folders/1", '<data/>'));
$this->assertResponse($exp, $this->req("PUT", "/folders/1", '<data/>', ['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"));
}
}

View file

@ -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"));
}
}

View file

@ -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 {
</section>
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() {

View file

@ -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"));
}
}

View file

@ -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) {