mirror of
https://code.mensbeam.com/MensBeam/Arsse.git
synced 2024-12-22 13:12:41 +00:00
Merge CORS branch
This commit is contained in:
commit
34b508171b
32 changed files with 1746 additions and 843 deletions
|
@ -3,6 +3,12 @@ Version 0.3.0 (2018-??-??)
|
|||
|
||||
New features:
|
||||
- Support for SQLite3 via PDO
|
||||
- Support for cross-origin resource sharing in all protocols
|
||||
|
||||
Bug fixes:
|
||||
- Correctly handle %-encoded request URLs
|
||||
- Overhaul protocol detection to fix various subtle bugs
|
||||
- Overhaul HTTP response handling for more consistent results
|
||||
|
||||
Changes:
|
||||
- Make date strings in TTRSS explicitly UTC
|
||||
|
|
|
@ -86,10 +86,6 @@ The Arsse makes use of the [picoFeed] newsfeed parsing library to sanitize artic
|
|||
|
||||
As a general rule, The Arsse should yield the same output as the reference implementation for all valid inputs (otherwise you've found [a bug][newIssue]), but there are exception, either because the NextCloud News (hereafter "NCN") [protocol description][NCNv1] is at times ambiguous or incomplete, or because implementation details necessitate it differ; this section along with the General section above detail these differences.
|
||||
|
||||
#### Missing features
|
||||
|
||||
- The Arsse does not implement [Cross-Origin Resource Sharing][CORS]
|
||||
|
||||
#### Differences
|
||||
|
||||
- Article GUID hashes are not hashes like in NCN; they are integers rendered as strings
|
||||
|
|
|
@ -9,6 +9,14 @@ When upgrading between any two versions of The Arsse, the following are usually
|
|||
- If installing from source, update dependencies with `composer install -o --no-dev`
|
||||
|
||||
|
||||
Upgrading from 0.2.1 to 0.3.0
|
||||
=============================
|
||||
|
||||
- The following Composer dependencies have been added:
|
||||
- zendframework/zend-diactoros
|
||||
- psr/http-message
|
||||
|
||||
|
||||
Upgrading from 0.2.0 to 0.2.1
|
||||
=============================
|
||||
|
||||
|
|
|
@ -24,5 +24,7 @@ if (\PHP_SAPI=="cli") {
|
|||
Arsse::$conf->importFile(BASE."config.php");
|
||||
}
|
||||
// handle Web requests
|
||||
(new REST)->dispatch()->output();
|
||||
$emitter = new \Zend\Diactoros\Response\SapiEmitter();
|
||||
$response = (new REST)->dispatch();
|
||||
$emitter->emit($response);
|
||||
}
|
||||
|
|
|
@ -25,7 +25,8 @@
|
|||
"fguillot/picofeed": ">=0.1.31",
|
||||
"hosteurope/password-generator": "^1.0",
|
||||
"docopt/docopt": "^1.0",
|
||||
"jkingweb/druuid": "^3.0"
|
||||
"jkingweb/druuid": "^3.0",
|
||||
"zendframework/zend-diactoros": "^1.6"
|
||||
},
|
||||
"require-dev": {
|
||||
"bamarni/composer-bin-plugin": "*"
|
||||
|
|
104
composer.lock
generated
104
composer.lock
generated
|
@ -4,7 +4,7 @@
|
|||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "8a3c7ff23f125a5fa3dac2e6a7244a90",
|
||||
"content-hash": "7d381fa958169b7079c1d3c5b911f3bd",
|
||||
"packages": [
|
||||
{
|
||||
"name": "docopt/docopt",
|
||||
|
@ -190,6 +190,108 @@
|
|||
],
|
||||
"time": "2017-02-09T14:17:01+00:00"
|
||||
},
|
||||
{
|
||||
"name": "psr/http-message",
|
||||
"version": "1.0.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/php-fig/http-message.git",
|
||||
"reference": "f6561bf28d520154e4b0ec72be95418abe6d9363"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/php-fig/http-message/zipball/f6561bf28d520154e4b0ec72be95418abe6d9363",
|
||||
"reference": "f6561bf28d520154e4b0ec72be95418abe6d9363",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=5.3.0"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "1.0.x-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Psr\\Http\\Message\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "PHP-FIG",
|
||||
"homepage": "http://www.php-fig.org/"
|
||||
}
|
||||
],
|
||||
"description": "Common interface for HTTP messages",
|
||||
"homepage": "https://github.com/php-fig/http-message",
|
||||
"keywords": [
|
||||
"http",
|
||||
"http-message",
|
||||
"psr",
|
||||
"psr-7",
|
||||
"request",
|
||||
"response"
|
||||
],
|
||||
"time": "2016-08-06T14:39:51+00:00"
|
||||
},
|
||||
{
|
||||
"name": "zendframework/zend-diactoros",
|
||||
"version": "1.6.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/zendframework/zend-diactoros.git",
|
||||
"reference": "c8664b92a6d5bc229e48b0923486c097e45a7877"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/zendframework/zend-diactoros/zipball/c8664b92a6d5bc229e48b0923486c097e45a7877",
|
||||
"reference": "c8664b92a6d5bc229e48b0923486c097e45a7877",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "^5.6 || ^7.0",
|
||||
"psr/http-message": "^1.0"
|
||||
},
|
||||
"provide": {
|
||||
"psr/http-message-implementation": "1.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"ext-dom": "*",
|
||||
"ext-libxml": "*",
|
||||
"phpunit/phpunit": "^5.7.16 || ^6.0.8",
|
||||
"zendframework/zend-coding-standard": "~1.0"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "1.6-dev",
|
||||
"dev-develop": "1.7-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Zend\\Diactoros\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"BSD-2-Clause"
|
||||
],
|
||||
"description": "PSR HTTP Message implementations",
|
||||
"homepage": "https://github.com/zendframework/zend-diactoros",
|
||||
"keywords": [
|
||||
"http",
|
||||
"psr",
|
||||
"psr-7"
|
||||
],
|
||||
"time": "2017-10-12T15:24:51+00:00"
|
||||
},
|
||||
{
|
||||
"name": "zendframework/zendxml",
|
||||
"version": "1.0.2",
|
||||
|
|
|
@ -72,6 +72,13 @@ class Conf {
|
|||
* @see https://en.wikipedia.org/wiki/ISO_8601#Durations */
|
||||
public $purgeArticlesUnread = "P21D";
|
||||
|
||||
/** @var string Application name to present to clients during authentication */
|
||||
public $httpRealm = "The Advanced RSS Environment";
|
||||
/** @var string Space-separated list of origins from which to allow cross-origin resource sharing */
|
||||
public $httpOriginsAllowed = "*";
|
||||
/** @var string Space-separated list of origins from which to deny cross-origin resource sharing */
|
||||
public $httpOriginsDenied = "";
|
||||
|
||||
/** Creates a new configuration object
|
||||
* @param string $import_file Optional file to read configuration data from
|
||||
* @see self::importFile() */
|
||||
|
|
253
lib/REST.php
253
lib/REST.php
|
@ -6,8 +6,17 @@
|
|||
declare(strict_types=1);
|
||||
namespace JKingWeb\Arsse;
|
||||
|
||||
|
||||
use JKingWeb\Arsse\Arsse;
|
||||
use Psr\Http\Message\RequestInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Zend\Diactoros\ServerRequest;
|
||||
use Zend\Diactoros\ServerRequestFactory;
|
||||
use Zend\Diactoros\Response\EmptyResponse;
|
||||
|
||||
class REST {
|
||||
protected $apis = [
|
||||
const API_LIST = [
|
||||
// NextCloud News version enumerator
|
||||
'ncn' => [
|
||||
'match' => '/index.php/apps/news/api',
|
||||
|
@ -21,7 +30,7 @@ class REST {
|
|||
'class' => REST\NextCloudNews\V1_2::class,
|
||||
],
|
||||
'ttrss_api' => [ // Tiny Tiny RSS https://git.tt-rss.org/git/tt-rss/wiki/ApiReference
|
||||
'match' => '/tt-rss/api/',
|
||||
'match' => '/tt-rss/api',
|
||||
'strip' => '/tt-rss/api',
|
||||
'class' => REST\TinyTinyRSS\API::class,
|
||||
],
|
||||
|
@ -34,50 +43,252 @@ class REST {
|
|||
// Google Reader http://feedhq.readthedocs.io/en/latest/api/index.html
|
||||
// Fever https://feedafever.com/api
|
||||
// Feedbin v2 https://github.com/feedbin/feedbin-api
|
||||
// Feedbin v1 https://github.com/feedbin/feedbin-api/commit/86da10aac5f1a57531a6e17b08744e5f9e7db8a9
|
||||
// Miniflux https://github.com/miniflux/miniflux/blob/master/docs/json-rpc-api.markdown
|
||||
// CommaFeed https://www.commafeed.com/api/
|
||||
// Unclear if clients exist:
|
||||
// Miniflux https://github.com/miniflux/miniflux/blob/master/docs/json-rpc-api.markdown
|
||||
// NextCloud News v2 https://github.com/nextcloud/news/blob/master/docs/externalapi/External-Api.md
|
||||
// Selfoss https://github.com/SSilence/selfoss/wiki/Restful-API-for-Apps-or-any-other-external-access
|
||||
// BirdReader https://github.com/glynnbird/birdreader/blob/master/API.md
|
||||
// Feedbin v1 https://github.com/feedbin/feedbin-api/commit/86da10aac5f1a57531a6e17b08744e5f9e7db8a9
|
||||
// Proprietary (centralized) entities:
|
||||
// NewsBlur http://www.newsblur.com/api
|
||||
// Feedly https://developer.feedly.com/
|
||||
];
|
||||
const DEFAULT_PORTS = [
|
||||
'http' => 80,
|
||||
'https' => 443,
|
||||
];
|
||||
protected $apis = [];
|
||||
|
||||
public function __construct() {
|
||||
public function __construct(array $apis = null) {
|
||||
$this->apis = $apis ?? self::API_LIST;
|
||||
}
|
||||
|
||||
public function dispatch(REST\Request $req = null): REST\Response {
|
||||
if ($req===null) {
|
||||
$req = new REST\Request();
|
||||
}
|
||||
$api = $this->apiMatch($req->url, $this->apis);
|
||||
$req->url = substr($req->url, strlen($this->apis[$api]['strip']));
|
||||
$req->refreshURL();
|
||||
$class = $this->apis[$api]['class'];
|
||||
$drv = new $class();
|
||||
if ($req->head) {
|
||||
$res = $drv->dispatch($req);
|
||||
$res->head = true;
|
||||
return $res;
|
||||
public function dispatch(ServerRequestInterface $req = null): ResponseInterface {
|
||||
// create a request object if not provided
|
||||
$req = $req ?? ServerRequestFactory::fromGlobals();
|
||||
// find the API to handle
|
||||
try {
|
||||
list ($api, $target, $class) = $this->apiMatch($req->getRequestTarget(), $this->apis);
|
||||
// authenticate the request pre-emptively
|
||||
$req = $this->authenticateRequest($req);
|
||||
// modify the request to have an uppercase method and a stripped target
|
||||
$req = $req->withMethod(strtoupper($req->getMethod()))->withRequestTarget($target);
|
||||
// fetch the correct handler
|
||||
$drv = $this->getHandler($class);
|
||||
// generate a response
|
||||
if ($req->getMethod()=="HEAD") {
|
||||
// if the request is a HEAD request, we act exactly as if it were a GET request, and simply remove the response body later
|
||||
$res = $drv->dispatch($req->withMethod("GET"));
|
||||
} else {
|
||||
return $drv->dispatch($req);
|
||||
$res = $drv->dispatch($req);
|
||||
}
|
||||
} catch (REST\Exception501 $e) {
|
||||
$res = new EmptyResponse(501);
|
||||
}
|
||||
// modify the response so that it has all the required metadata
|
||||
return $this->normalizeResponse($res, $req);
|
||||
}
|
||||
|
||||
public function apiMatch(string $url, array $map): string {
|
||||
public function getHandler(string $className): REST\Handler {
|
||||
// instantiate the API handler
|
||||
return new $className();
|
||||
}
|
||||
|
||||
public function apiMatch(string $url): array {
|
||||
$map = $this->apis;
|
||||
// sort the API list so the longest URL prefixes come first
|
||||
uasort($map, function ($a, $b) {
|
||||
return (strlen($a['match']) <=> strlen($b['match'])) * -1;
|
||||
});
|
||||
// normalize the target URL
|
||||
$url = REST\Target::normalize($url);
|
||||
// find a match
|
||||
foreach ($map as $id => $api) {
|
||||
// first try a simple substring match
|
||||
if (strpos($url, $api['match'])===0) {
|
||||
return $id;
|
||||
// if it matches, perform a more rigorous match and then strip off any defined prefix
|
||||
$pattern = "<^".preg_quote($api['match'])."([/\?#]|$)>";
|
||||
if ($url==$api['match'] || in_array(substr($api['match'], -1, 1), ["/", "?", "#"]) || preg_match($pattern, $url)) {
|
||||
$target = substr($url, strlen($api['strip']));
|
||||
} else {
|
||||
// if the match fails we are not able to handle the request
|
||||
throw new REST\Exception501();
|
||||
}
|
||||
// return the API name, stripped URL, and API class name
|
||||
return [$id, $target, $api['class']];
|
||||
}
|
||||
}
|
||||
// or throw an exception otherwise
|
||||
throw new REST\Exception501();
|
||||
}
|
||||
|
||||
public function authenticateRequest(ServerRequestInterface $req): ServerRequestInterface {
|
||||
$user = "";
|
||||
$password = "";
|
||||
$env = $req->getServerParams();
|
||||
if (isset($env['PHP_AUTH_USER'])) {
|
||||
$user = $env['PHP_AUTH_USER'];
|
||||
if (isset($env['PHP_AUTH_PW'])) {
|
||||
$password = $env['PHP_AUTH_PW'];
|
||||
}
|
||||
} elseif (isset($env['REMOTE_USER'])) {
|
||||
$user = $env['REMOTE_USER'];
|
||||
}
|
||||
if (strlen($user)) {
|
||||
$valid = Arsse::$user->auth($user, $password);
|
||||
}
|
||||
if ($valid) {
|
||||
$req = $req->withAttribute("authenticated", true);
|
||||
$req = $req->withAttribute("authenticatedUser", $user);
|
||||
}
|
||||
return $req;
|
||||
}
|
||||
|
||||
public function challenge(ResponseInterface $res, string $realm = null): ResponseInterface {
|
||||
$realm = $realm ?? Arsse::$conf->httpRealm ?? "Default";
|
||||
return $res->withAddedHeader("WWW-Authenticate", 'Basic realm="'.$realm.'"');
|
||||
}
|
||||
|
||||
public function normalizeResponse(ResponseInterface $res, RequestInterface $req = null): ResponseInterface {
|
||||
// if the response code is 401, issue an HTTP authentication challenge
|
||||
if ($res->getStatusCode()==401) {
|
||||
$res = $this->challenge($res);
|
||||
}
|
||||
// set or clear the Content-Length header field
|
||||
$body = $res->getBody();
|
||||
$bodySize = $body->getSize();
|
||||
if ($bodySize || $res->getStatusCode()==200) {
|
||||
// if there is a message body or the response is 200, make sure Content-Length is included
|
||||
$res = $res->withHeader("Content-Length", (string) $bodySize);
|
||||
} else {
|
||||
// for empty responses of other statuses, omit it
|
||||
$res = $res->withoutHeader("Content-Length");
|
||||
}
|
||||
// if the response is to a HEAD request, the body should be omitted
|
||||
if ($req && $req->getMethod()=="HEAD") {
|
||||
$res = new EmptyResponse($res->getStatusCode(), $res->getHeaders());
|
||||
}
|
||||
// if an Allow header field is present, normalize it
|
||||
if ($res->hasHeader("Allow")) {
|
||||
$methods = preg_split("<\s*,\s*>", strtoupper($res->getHeaderLine("Allow")));
|
||||
// if GET is allowed, HEAD should be allowed as well
|
||||
if (in_array("GET", $methods) && !in_array("HEAD", $methods)) {
|
||||
$methods[] = "HEAD";
|
||||
}
|
||||
// OPTIONS requests are always allowed by our handlers
|
||||
if (!in_array("OPTIONS", $methods)) {
|
||||
$methods[] = "OPTIONS";
|
||||
}
|
||||
$res = $res->withHeader("Allow", implode(", ", $methods));
|
||||
}
|
||||
// add CORS header fields if the request origin is specified and allowed
|
||||
if ($req && $this->corsNegotiate($req)) {
|
||||
$res = $this->corsApply($res, $req);
|
||||
}
|
||||
return $res;
|
||||
}
|
||||
|
||||
public function corsApply(ResponseInterface $res, RequestInterface $req = null): ResponseInterface {
|
||||
if ($req && $req->getMethod()=="OPTIONS") {
|
||||
if ($res->hasHeader("Allow")) {
|
||||
$res = $res->withHeader("Access-Control-Allow-Methods", $res->getHeaderLine("Allow"));
|
||||
}
|
||||
if ($req->hasHeader("Access-Control-Request-Headers")) {
|
||||
$res = $res->withHeader("Access-Control-Allow-Headers", $req->getHeaderLine("Access-Control-Request-Headers"));
|
||||
}
|
||||
$res = $res->withHeader("Access-Control-Max-Age", (string) (60 *60 *24) ); // one day
|
||||
}
|
||||
$res = $res->withHeader("Access-Control-Allow-Origin", $req->getHeaderLine("Origin"));
|
||||
$res = $res->withHeader("Access-Control-Allow-Credentials", "true");
|
||||
return $res->withAddedHeader("Vary", "Origin");
|
||||
}
|
||||
|
||||
public function corsNegotiate(RequestInterface $req, string $allowed = null, string $denied = null): bool {
|
||||
$allowed = trim($allowed ?? Arsse::$conf->httpOriginsAllowed ?? "");
|
||||
$denied = trim($denied ?? Arsse::$conf->httpOriginsDenied ?? "");
|
||||
// continue if at least one origin is allowed
|
||||
if ($allowed) {
|
||||
// continue if the request has exactly one Origin header
|
||||
$origin = $req->getHeader("Origin");
|
||||
if (sizeof($origin)==1) {
|
||||
// continue if the origin is syntactically valid
|
||||
$origin = $this->corsNormalizeOrigin($origin[0]);
|
||||
if ($origin) {
|
||||
// the special "null" origin should not be matched by the wildcard origin
|
||||
$null = ($origin=="null");
|
||||
// pad all strings for simpler comparison
|
||||
$allowed = " ".$allowed." ";
|
||||
$denied = " ".$denied." ";
|
||||
$origin = " ".$origin." ";
|
||||
$any = " * ";
|
||||
if (strpos($denied, $origin) !== false) {
|
||||
// first check the denied list for the origin
|
||||
return false;
|
||||
} elseif (strpos($allowed, $origin) !== false) {
|
||||
// next check the allowed list for the origin
|
||||
return true;
|
||||
} elseif (!$null && strpos($denied, $any) !== false) {
|
||||
// next check the denied list for the wildcard origin
|
||||
return false;
|
||||
} elseif (!$null && strpos($allowed, $any) !== false) {
|
||||
// finally check the allowed list for the wildcard origin
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public function corsNormalizeOrigin(string $origin, array $ports = null): string {
|
||||
$origin = trim($origin);
|
||||
if ($origin=="null") {
|
||||
// if the origin is the special value "null", use it
|
||||
return "null";
|
||||
}
|
||||
if (preg_match("<^([^:]+)://(\[[^\]]+\]|[^\[\]:/\?#@]+)((?::.*)?)$>i", $origin, $match)) {
|
||||
// if the origin sort-of matches the syntax in a general sense, continue
|
||||
$scheme = $match[1];
|
||||
$host = $match[2];
|
||||
$port = $match[3];
|
||||
// decode and normalize the scheme and port (the port may be blank)
|
||||
$scheme = strtolower(rawurldecode($scheme));
|
||||
$port = rawurldecode($port);
|
||||
if (!preg_match("<^(?::[0-9]+)?$>", $port) || !preg_match("<^[a-z](?:[a-z0-9\+\-\.])*$>", $scheme)) {
|
||||
// if the normalized port contains anything but numbers, or the scheme does not follow the generic URL syntax, the origin is invalid
|
||||
return "";
|
||||
}
|
||||
if ($host[0]=="[") {
|
||||
// if the host appears to be an IPv6 address, validate it
|
||||
$host = rawurldecode(substr($host, 1, strlen($host) - 2));
|
||||
if (!filter_var($host, \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV6)) {
|
||||
return "";
|
||||
} else {
|
||||
$host = "[".inet_ntop(inet_pton($host))."]";
|
||||
}
|
||||
} else {
|
||||
// if the host is a domain name or IP address, split it along dots and just perform URL decoding
|
||||
$host = explode(".", $host);
|
||||
$host = array_map(function ($segment) {
|
||||
return str_replace(".", "%2E", rawurlencode(strtolower(rawurldecode($segment))));
|
||||
}, $host);
|
||||
$host = implode(".", $host);
|
||||
}
|
||||
// suppress default ports
|
||||
if (strlen($port)) {
|
||||
$port = (int) substr($port, 1);
|
||||
$list = array_merge($ports ?? [], self::DEFAULT_PORTS);
|
||||
if (isset($list[$scheme]) && $port==$list[$scheme]) {
|
||||
$port = "";
|
||||
} else {
|
||||
$port = ":".$port;
|
||||
}
|
||||
}
|
||||
// return the reconstructed result
|
||||
return $scheme."://".$host.$port;
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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): Response;
|
||||
abstract public function dispatch(ServerRequestInterface $req): ResponseInterface;
|
||||
|
||||
protected function fieldMapNames(array $data, array $map): array {
|
||||
$out = [];
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
* See LICENSE and AUTHORS files for details */
|
||||
|
||||
declare(strict_types=1);
|
||||
namespace JKingWeb\Arsse\REST\NextCloudNews;
|
||||
namespace JKingWeb\Arsse\REST;
|
||||
|
||||
class Exception404 extends \Exception {
|
||||
}
|
|
@ -4,7 +4,7 @@
|
|||
* See LICENSE and AUTHORS files for details */
|
||||
|
||||
declare(strict_types=1);
|
||||
namespace JKingWeb\Arsse\REST\NextCloudNews;
|
||||
namespace JKingWeb\Arsse\REST;
|
||||
|
||||
class Exception405 extends \Exception {
|
||||
}
|
10
lib/REST/Exception501.php
Normal file
10
lib/REST/Exception501.php
Normal file
|
@ -0,0 +1,10 @@
|
|||
<?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 Exception501 extends \Exception {
|
||||
}
|
|
@ -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): Response;
|
||||
public function dispatch(ServerRequestInterface $req): ResponseInterface;
|
||||
}
|
||||
|
|
|
@ -15,7 +15,13 @@ use JKingWeb\Arsse\Misc\ValueInfo;
|
|||
use JKingWeb\Arsse\AbstractException;
|
||||
use JKingWeb\Arsse\Db\ExceptionInput;
|
||||
use JKingWeb\Arsse\Feed\Exception as FeedException;
|
||||
use JKingWeb\Arsse\REST\Response;
|
||||
use JKingWeb\Arsse\REST\Target;
|
||||
use JKingWeb\Arsse\REST\Exception404;
|
||||
use JKingWeb\Arsse\REST\Exception405;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Zend\Diactoros\Response\JsonResponse as Response;
|
||||
use Zend\Diactoros\Response\EmptyResponse;
|
||||
|
||||
class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler {
|
||||
const REALM = "NextCloud News API v1-2";
|
||||
|
@ -41,100 +47,115 @@ 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): Response {
|
||||
public function dispatch(ServerRequestInterface $req): ResponseInterface {
|
||||
// try to authenticate
|
||||
if (!Arsse::$user->authHTTP()) {
|
||||
return new Response(401, "", "", ['WWW-Authenticate: Basic realm="'.self::REALM.'"']);
|
||||
if ($req->getAttribute("authenticated", false)) {
|
||||
Arsse::$user->id = $req->getAttribute("authenticatedUser");
|
||||
} else {
|
||||
return new EmptyResponse(401);
|
||||
}
|
||||
// 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) {
|
||||
// if the entity body is not JSON according to content type, return "415 Unsupported Media Type"
|
||||
if (!preg_match("<^application/json\b|^$>", $req->type)) {
|
||||
return new Response(415, "", "", ['Accept: application/json']);
|
||||
$data = (string) $req->getBody();
|
||||
$type = "";
|
||||
if ($req->hasHeader("Content-Type")) {
|
||||
$type = $req->getHeader("Content-Type");
|
||||
$type = array_pop($type);
|
||||
}
|
||||
$data = @json_decode($req->body, true);
|
||||
if ($data) {
|
||||
// if the entity body is not JSON according to content type, return "415 Unsupported Media Type"
|
||||
if (!preg_match("<^application/json\b|^$>", $type)) {
|
||||
return new EmptyResponse(415, ['Accept' => "application/json"]);
|
||||
}
|
||||
$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 Response(400);
|
||||
return new EmptyResponse(400);
|
||||
}
|
||||
} else {
|
||||
$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 Response(404);
|
||||
return new EmptyResponse(404);
|
||||
} catch (Exception405 $e) {
|
||||
return new Response(405, "", "", ["Allow: ".$e->getMessage()]);
|
||||
return new EmptyResponse(405, ['Allow' => $e->getMessage()]);
|
||||
}
|
||||
if (!method_exists($this, $func)) {
|
||||
return new Response(501); // @codeCoverageIgnore
|
||||
return new EmptyResponse(501); // @codeCoverageIgnore
|
||||
}
|
||||
// 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
|
||||
return new Response(400);
|
||||
return new EmptyResponse(400);
|
||||
} catch (AbstractException $e) {
|
||||
// if there was any other Arsse exception return 500
|
||||
return new Response(500);
|
||||
return new EmptyResponse(500);
|
||||
}
|
||||
// @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
|
||||
|
@ -242,9 +263,9 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler {
|
|||
return $article;
|
||||
}
|
||||
|
||||
protected function handleHTTPOptions(array $url): Response {
|
||||
// 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]);
|
||||
|
@ -252,81 +273,81 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler {
|
|||
if (in_array("GET", $allowed)) {
|
||||
array_unshift($allowed, "HEAD");
|
||||
}
|
||||
return new Response(204, "", "", [
|
||||
"Allow: ".implode(",", $allowed),
|
||||
"Accept: application/json",
|
||||
return new EmptyResponse(204, [
|
||||
'Allow' => implode(",", $allowed),
|
||||
'Accept' => "application/json",
|
||||
]);
|
||||
} else {
|
||||
// if the path is not supported, return 404
|
||||
return new Response(404);
|
||||
return new EmptyResponse(404);
|
||||
}
|
||||
}
|
||||
|
||||
// list folders
|
||||
protected function folderList(array $url, array $data): Response {
|
||||
protected function folderList(array $url, array $data): ResponseInterface {
|
||||
$folders = [];
|
||||
foreach (Arsse::$db->folderList(Arsse::$user->id, null, false) as $folder) {
|
||||
$folders[] = $this->folderTranslate($folder);
|
||||
}
|
||||
return new Response(200, ['folders' => $folders]);
|
||||
return new Response(['folders' => $folders]);
|
||||
}
|
||||
|
||||
// create a folder
|
||||
protected function folderAdd(array $url, array $data): Response {
|
||||
protected function folderAdd(array $url, array $data): ResponseInterface {
|
||||
try {
|
||||
$folder = Arsse::$db->folderAdd(Arsse::$user->id, ['name' => $data['name']]);
|
||||
} catch (ExceptionInput $e) {
|
||||
switch ($e->getCode()) {
|
||||
// folder already exists
|
||||
case 10236: return new Response(409);
|
||||
case 10236: return new EmptyResponse(409);
|
||||
// folder name not acceptable
|
||||
case 10231:
|
||||
case 10232: return new Response(422);
|
||||
case 10232: return new EmptyResponse(422);
|
||||
// other errors related to input
|
||||
default: return new Response(400); // @codeCoverageIgnore
|
||||
default: return new EmptyResponse(400); // @codeCoverageIgnore
|
||||
}
|
||||
}
|
||||
$folder = $this->folderTranslate(Arsse::$db->folderPropertiesGet(Arsse::$user->id, $folder));
|
||||
return new Response(200, ['folders' => [$folder]]);
|
||||
return new Response(['folders' => [$folder]]);
|
||||
}
|
||||
|
||||
// delete a folder
|
||||
protected function folderRemove(array $url, array $data): Response {
|
||||
protected function folderRemove(array $url, array $data): ResponseInterface {
|
||||
// perform the deletion
|
||||
try {
|
||||
Arsse::$db->folderRemove(Arsse::$user->id, (int) $url[1]);
|
||||
} catch (ExceptionInput $e) {
|
||||
// folder does not exist
|
||||
return new Response(404);
|
||||
return new EmptyResponse(404);
|
||||
}
|
||||
return new Response(204);
|
||||
return new EmptyResponse(204);
|
||||
}
|
||||
|
||||
// rename a folder (also supports moving nesting folders, but this is not a feature of the API)
|
||||
protected function folderRename(array $url, array $data): Response {
|
||||
protected function folderRename(array $url, array $data): ResponseInterface {
|
||||
try {
|
||||
Arsse::$db->folderPropertiesSet(Arsse::$user->id, (int) $url[1], ['name' => $data['name']]);
|
||||
} catch (ExceptionInput $e) {
|
||||
switch ($e->getCode()) {
|
||||
// folder does not exist
|
||||
case 10239: return new Response(404);
|
||||
case 10239: return new EmptyResponse(404);
|
||||
// folder already exists
|
||||
case 10236: return new Response(409);
|
||||
case 10236: return new EmptyResponse(409);
|
||||
// folder name not acceptable
|
||||
case 10231:
|
||||
case 10232: return new Response(422);
|
||||
case 10232: return new EmptyResponse(422);
|
||||
// other errors related to input
|
||||
default: return new Response(400); // @codeCoverageIgnore
|
||||
default: return new EmptyResponse(400); // @codeCoverageIgnore
|
||||
}
|
||||
}
|
||||
return new Response(204);
|
||||
return new EmptyResponse(204);
|
||||
}
|
||||
|
||||
// mark all articles associated with a folder as read
|
||||
protected function folderMarkRead(array $url, array $data): Response {
|
||||
protected function folderMarkRead(array $url, array $data): ResponseInterface {
|
||||
if (!ValueInfo::id($data['newestItemId'])) {
|
||||
// if the item ID is invalid (i.e. not a positive integer), this is an error
|
||||
return new Response(422);
|
||||
return new EmptyResponse(422);
|
||||
}
|
||||
// build the context
|
||||
$c = new Context;
|
||||
|
@ -337,16 +358,16 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler {
|
|||
Arsse::$db->articleMark(Arsse::$user->id, ['read' => true], $c);
|
||||
} catch (ExceptionInput $e) {
|
||||
// folder does not exist
|
||||
return new Response(404);
|
||||
return new EmptyResponse(404);
|
||||
}
|
||||
return new Response(204);
|
||||
return new EmptyResponse(204);
|
||||
}
|
||||
|
||||
// return list of feeds which should be refreshed
|
||||
protected function feedListStale(array $url, array $data): Response {
|
||||
protected function feedListStale(array $url, array $data): ResponseInterface {
|
||||
// function requires admin rights per spec
|
||||
if (Arsse::$user->rightsGet(Arsse::$user->id)==User::RIGHTS_NONE) {
|
||||
return new Response(403);
|
||||
return new EmptyResponse(403);
|
||||
}
|
||||
// list stale feeds which should be checked for updates
|
||||
$feeds = Arsse::$db->feedListStale();
|
||||
|
@ -355,42 +376,42 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler {
|
|||
// since in our implementation feeds don't belong the users, the 'userId' field will always be an empty string
|
||||
$out[] = ['id' => (int) $feed, 'userId' => ""];
|
||||
}
|
||||
return new Response(200, ['feeds' => $out]);
|
||||
return new Response(['feeds' => $out]);
|
||||
}
|
||||
|
||||
// refresh a feed
|
||||
protected function feedUpdate(array $url, array $data): Response {
|
||||
protected function feedUpdate(array $url, array $data): ResponseInterface {
|
||||
// function requires admin rights per spec
|
||||
if (Arsse::$user->rightsGet(Arsse::$user->id)==User::RIGHTS_NONE) {
|
||||
return new Response(403);
|
||||
return new EmptyResponse(403);
|
||||
}
|
||||
try {
|
||||
Arsse::$db->feedUpdate($data['feedId']);
|
||||
} catch (ExceptionInput $e) {
|
||||
switch ($e->getCode()) {
|
||||
case 10239: // feed does not exist
|
||||
return new Response(404);
|
||||
return new EmptyResponse(404);
|
||||
case 10237: // feed ID invalid
|
||||
return new Response(422);
|
||||
return new EmptyResponse(422);
|
||||
default: // other errors related to input
|
||||
return new Response(400); // @codeCoverageIgnore
|
||||
return new EmptyResponse(400); // @codeCoverageIgnore
|
||||
}
|
||||
}
|
||||
return new Response(204);
|
||||
return new EmptyResponse(204);
|
||||
}
|
||||
|
||||
// add a new feed
|
||||
protected function subscriptionAdd(array $url, array $data): Response {
|
||||
protected function subscriptionAdd(array $url, array $data): ResponseInterface {
|
||||
// try to add the feed
|
||||
$tr = Arsse::$db->begin();
|
||||
try {
|
||||
$id = Arsse::$db->subscriptionAdd(Arsse::$user->id, (string) $data['url']);
|
||||
} catch (ExceptionInput $e) {
|
||||
// feed already exists
|
||||
return new Response(409);
|
||||
return new EmptyResponse(409);
|
||||
} catch (FeedException $e) {
|
||||
// feed could not be retrieved
|
||||
return new Response(422);
|
||||
return new EmptyResponse(422);
|
||||
}
|
||||
// if a folder was specified, move the feed to the correct folder; silently ignore errors
|
||||
if ($data['folderId']) {
|
||||
|
@ -408,11 +429,11 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler {
|
|||
if ($newest) {
|
||||
$out['newestItemId'] = $newest;
|
||||
}
|
||||
return new Response(200, $out);
|
||||
return new Response($out);
|
||||
}
|
||||
|
||||
// return list of feeds for the logged-in user
|
||||
protected function subscriptionList(array $url, array $data): Response {
|
||||
protected function subscriptionList(array $url, array $data): ResponseInterface {
|
||||
$subs = Arsse::$db->subscriptionList(Arsse::$user->id);
|
||||
$out = [];
|
||||
foreach ($subs as $sub) {
|
||||
|
@ -424,43 +445,43 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler {
|
|||
if ($newest) {
|
||||
$out['newestItemId'] = $newest;
|
||||
}
|
||||
return new Response(200, $out);
|
||||
return new Response($out);
|
||||
}
|
||||
|
||||
// delete a feed
|
||||
protected function subscriptionRemove(array $url, array $data): Response {
|
||||
protected function subscriptionRemove(array $url, array $data): ResponseInterface {
|
||||
try {
|
||||
Arsse::$db->subscriptionRemove(Arsse::$user->id, (int) $url[1]);
|
||||
} catch (ExceptionInput $e) {
|
||||
// feed does not exist
|
||||
return new Response(404);
|
||||
return new EmptyResponse(404);
|
||||
}
|
||||
return new Response(204);
|
||||
return new EmptyResponse(204);
|
||||
}
|
||||
|
||||
// rename a feed
|
||||
protected function subscriptionRename(array $url, array $data): Response {
|
||||
protected function subscriptionRename(array $url, array $data): ResponseInterface {
|
||||
try {
|
||||
Arsse::$db->subscriptionPropertiesSet(Arsse::$user->id, (int) $url[1], ['title' => (string) $data['feedTitle']]);
|
||||
} catch (ExceptionInput $e) {
|
||||
switch ($e->getCode()) {
|
||||
// subscription does not exist
|
||||
case 10239: return new Response(404);
|
||||
case 10239: return new EmptyResponse(404);
|
||||
// name is invalid
|
||||
case 10231:
|
||||
case 10232: return new Response(422);
|
||||
case 10232: return new EmptyResponse(422);
|
||||
// other errors related to input
|
||||
default: return new Response(400); // @codeCoverageIgnore
|
||||
default: return new EmptyResponse(400); // @codeCoverageIgnore
|
||||
}
|
||||
}
|
||||
return new Response(204);
|
||||
return new EmptyResponse(204);
|
||||
}
|
||||
|
||||
// move a feed to a folder
|
||||
protected function subscriptionMove(array $url, array $data): Response {
|
||||
protected function subscriptionMove(array $url, array $data): ResponseInterface {
|
||||
// if no folder is specified this is an error
|
||||
if (!isset($data['folderId'])) {
|
||||
return new Response(422);
|
||||
return new EmptyResponse(422);
|
||||
}
|
||||
// perform the move
|
||||
try {
|
||||
|
@ -468,22 +489,22 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler {
|
|||
} catch (ExceptionInput $e) {
|
||||
switch ($e->getCode()) {
|
||||
case 10239: // subscription does not exist
|
||||
return new Response(404);
|
||||
return new EmptyResponse(404);
|
||||
case 10235: // folder does not exist
|
||||
case 10237: // folder ID is invalid
|
||||
return new Response(422);
|
||||
return new EmptyResponse(422);
|
||||
default: // other errors related to input
|
||||
return new Response(400); // @codeCoverageIgnore
|
||||
return new EmptyResponse(400); // @codeCoverageIgnore
|
||||
}
|
||||
}
|
||||
return new Response(204);
|
||||
return new EmptyResponse(204);
|
||||
}
|
||||
|
||||
// mark all articles associated with a subscription as read
|
||||
protected function subscriptionMarkRead(array $url, array $data): Response {
|
||||
protected function subscriptionMarkRead(array $url, array $data): ResponseInterface {
|
||||
if (!ValueInfo::id($data['newestItemId'])) {
|
||||
// if the item ID is invalid (i.e. not a positive integer), this is an error
|
||||
return new Response(422);
|
||||
return new EmptyResponse(422);
|
||||
}
|
||||
// build the context
|
||||
$c = new Context;
|
||||
|
@ -494,13 +515,13 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler {
|
|||
Arsse::$db->articleMark(Arsse::$user->id, ['read' => true], $c);
|
||||
} catch (ExceptionInput $e) {
|
||||
// subscription does not exist
|
||||
return new Response(404);
|
||||
return new EmptyResponse(404);
|
||||
}
|
||||
return new Response(204);
|
||||
return new EmptyResponse(204);
|
||||
}
|
||||
|
||||
// list articles and their properties
|
||||
protected function articleList(array $url, array $data): Response {
|
||||
protected function articleList(array $url, array $data): ResponseInterface {
|
||||
// set the context options supplied by the client
|
||||
$c = new Context;
|
||||
// set the batch size
|
||||
|
@ -553,32 +574,32 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler {
|
|||
$items = Arsse::$db->articleList(Arsse::$user->id, $c, Database::LIST_TYPICAL);
|
||||
} catch (ExceptionInput $e) {
|
||||
// ID of subscription or folder is not valid
|
||||
return new Response(422);
|
||||
return new EmptyResponse(422);
|
||||
}
|
||||
$out = [];
|
||||
foreach ($items as $item) {
|
||||
$out[] = $this->articleTranslate($item);
|
||||
}
|
||||
$out = ['items' => $out];
|
||||
return new Response(200, $out);
|
||||
return new Response($out);
|
||||
}
|
||||
|
||||
// mark all articles as read
|
||||
protected function articleMarkReadAll(array $url, array $data): Response {
|
||||
protected function articleMarkReadAll(array $url, array $data): ResponseInterface {
|
||||
if (!ValueInfo::id($data['newestItemId'])) {
|
||||
// if the item ID is invalid (i.e. not a positive integer), this is an error
|
||||
return new Response(422);
|
||||
return new EmptyResponse(422);
|
||||
}
|
||||
// build the context
|
||||
$c = new Context;
|
||||
$c->latestEdition((int) $data['newestItemId']);
|
||||
// perform the operation
|
||||
Arsse::$db->articleMark(Arsse::$user->id, ['read' => true], $c);
|
||||
return new Response(204);
|
||||
return new EmptyResponse(204);
|
||||
}
|
||||
|
||||
// mark a single article as read
|
||||
protected function articleMarkRead(array $url, array $data): Response {
|
||||
protected function articleMarkRead(array $url, array $data): ResponseInterface {
|
||||
// initialize the matching context
|
||||
$c = new Context;
|
||||
$c->edition((int) $url[1]);
|
||||
|
@ -588,13 +609,13 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler {
|
|||
Arsse::$db->articleMark(Arsse::$user->id, ['read' => $set], $c);
|
||||
} catch (ExceptionInput $e) {
|
||||
// ID is not valid
|
||||
return new Response(404);
|
||||
return new EmptyResponse(404);
|
||||
}
|
||||
return new Response(204);
|
||||
return new EmptyResponse(204);
|
||||
}
|
||||
|
||||
// mark a single article as read
|
||||
protected function articleMarkStarred(array $url, array $data): Response {
|
||||
protected function articleMarkStarred(array $url, array $data): ResponseInterface {
|
||||
// initialize the matching context
|
||||
$c = new Context;
|
||||
$c->article((int) $url[2]);
|
||||
|
@ -604,13 +625,13 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler {
|
|||
Arsse::$db->articleMark(Arsse::$user->id, ['starred' => $set], $c);
|
||||
} catch (ExceptionInput $e) {
|
||||
// ID is not valid
|
||||
return new Response(404);
|
||||
return new EmptyResponse(404);
|
||||
}
|
||||
return new Response(204);
|
||||
return new EmptyResponse(204);
|
||||
}
|
||||
|
||||
// mark an array of articles as read
|
||||
protected function articleMarkReadMulti(array $url, array $data): Response {
|
||||
protected function articleMarkReadMulti(array $url, array $data): ResponseInterface {
|
||||
// determine whether to mark read or unread
|
||||
$set = ($url[1]=="read");
|
||||
// initialize the matching context
|
||||
|
@ -620,11 +641,11 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler {
|
|||
Arsse::$db->articleMark(Arsse::$user->id, ['read' => $set], $c);
|
||||
} catch (ExceptionInput $e) {
|
||||
}
|
||||
return new Response(204);
|
||||
return new EmptyResponse(204);
|
||||
}
|
||||
|
||||
// mark an array of articles as starred
|
||||
protected function articleMarkStarredMulti(array $url, array $data): Response {
|
||||
protected function articleMarkStarredMulti(array $url, array $data): ResponseInterface {
|
||||
// determine whether to mark starred or unstarred
|
||||
$set = ($url[1]=="star");
|
||||
// initialize the matching context
|
||||
|
@ -634,10 +655,10 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler {
|
|||
Arsse::$db->articleMark(Arsse::$user->id, ['starred' => $set], $c);
|
||||
} catch (ExceptionInput $e) {
|
||||
}
|
||||
return new Response(204);
|
||||
return new EmptyResponse(204);
|
||||
}
|
||||
|
||||
protected function userStatus(array $url, array $data): Response {
|
||||
protected function userStatus(array $url, array $data): ResponseInterface {
|
||||
$data = Arsse::$user->propertiesGet(Arsse::$user->id, true);
|
||||
// construct the avatar structure, if an image is available
|
||||
if (isset($data['avatar'])) {
|
||||
|
@ -655,37 +676,37 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler {
|
|||
'lastLoginTimestamp' => time(),
|
||||
'avatar' => $avatar,
|
||||
];
|
||||
return new Response(200, $out);
|
||||
return new Response($out);
|
||||
}
|
||||
|
||||
protected function cleanupBefore(array $url, array $data): Response {
|
||||
protected function cleanupBefore(array $url, array $data): ResponseInterface {
|
||||
// function requires admin rights per spec
|
||||
if (Arsse::$user->rightsGet(Arsse::$user->id)==User::RIGHTS_NONE) {
|
||||
return new Response(403);
|
||||
return new EmptyResponse(403);
|
||||
}
|
||||
Service::cleanupPre();
|
||||
return new Response(204);
|
||||
return new EmptyResponse(204);
|
||||
}
|
||||
|
||||
protected function cleanupAfter(array $url, array $data): Response {
|
||||
protected function cleanupAfter(array $url, array $data): ResponseInterface {
|
||||
// function requires admin rights per spec
|
||||
if (Arsse::$user->rightsGet(Arsse::$user->id)==User::RIGHTS_NONE) {
|
||||
return new Response(403);
|
||||
return new EmptyResponse(403);
|
||||
}
|
||||
Service::cleanupPost();
|
||||
return new Response(204);
|
||||
return new EmptyResponse(204);
|
||||
}
|
||||
|
||||
// return the server version
|
||||
protected function serverVersion(array $url, array $data): Response {
|
||||
return new Response(200, [
|
||||
protected function serverVersion(array $url, array $data): ResponseInterface {
|
||||
return new Response([
|
||||
'version' => self::VERSION,
|
||||
'arsse_version' => Arsse::VERSION,
|
||||
]);
|
||||
}
|
||||
|
||||
protected function serverStatus(array $url, array $data): Response {
|
||||
return new Response(200, [
|
||||
protected function serverStatus(array $url, array $data): ResponseInterface {
|
||||
return new Response([
|
||||
'version' => self::VERSION,
|
||||
'arsse_version' => Arsse::VERSION,
|
||||
'warnings' => [
|
||||
|
|
|
@ -6,30 +6,35 @@
|
|||
declare(strict_types=1);
|
||||
namespace JKingWeb\Arsse\REST\NextCloudNews;
|
||||
|
||||
use JKingWeb\Arsse\REST\Response;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Zend\Diactoros\Response\JsonResponse as Response;
|
||||
use Zend\Diactoros\Response\EmptyResponse;
|
||||
|
||||
class Versions implements \JKingWeb\Arsse\REST\Handler {
|
||||
public function __construct() {
|
||||
}
|
||||
|
||||
public function dispatch(\JKingWeb\Arsse\REST\Request $req): Response {
|
||||
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
|
||||
return new Response(404);
|
||||
} elseif ($req->method=="OPTIONS") {
|
||||
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);
|
||||
}
|
||||
switch ($req->getMethod()) {
|
||||
case "OPTIONS":
|
||||
// if the request method is OPTIONS, respond accordingly
|
||||
return new Response(204, "", "", ["Allow: HEAD,GET"]);
|
||||
} elseif ($req->method != "GET") {
|
||||
// if a method other than GET was used, this is an error
|
||||
return new Response(405, "", "", ["Allow: HEAD,GET"]);
|
||||
} else {
|
||||
return new EmptyResponse(204, ['Allow' => "HEAD,GET"]);
|
||||
case "GET":
|
||||
// otherwise return the supported versions
|
||||
$out = [
|
||||
'apiLevels' => [
|
||||
'v1-2',
|
||||
]
|
||||
];
|
||||
return new Response(200, $out);
|
||||
return new Response($out);
|
||||
default:
|
||||
// if any other method was used, this is an error
|
||||
return new EmptyResponse(405, ['Allow' => "HEAD,GET"]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -1,65 +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;
|
||||
|
||||
use JKingWeb\Arsse\Arsse;
|
||||
|
||||
class Response {
|
||||
const T_JSON = "application/json";
|
||||
const T_XML = "application/xml";
|
||||
const T_TEXT = "text/plain";
|
||||
|
||||
public $head = false;
|
||||
public $code;
|
||||
public $payload;
|
||||
public $type;
|
||||
public $fields;
|
||||
|
||||
|
||||
public function __construct(int $code, $payload = null, string $type = self::T_JSON, array $extraFields = []) {
|
||||
$this->code = $code;
|
||||
$this->payload = $payload;
|
||||
$this->type = $type;
|
||||
$this->fields = $extraFields;
|
||||
}
|
||||
|
||||
public function output() {
|
||||
if (!headers_sent()) {
|
||||
foreach ($this->fields as $field) {
|
||||
header($field);
|
||||
}
|
||||
$body = "";
|
||||
if (!is_null($this->payload)) {
|
||||
switch ($this->type) {
|
||||
case self::T_JSON:
|
||||
$body = (string) json_encode($this->payload, \JSON_PRETTY_PRINT);
|
||||
break;
|
||||
default:
|
||||
$body = (string) $this->payload;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (strlen($body)) {
|
||||
header("Content-Type: ".$this->type);
|
||||
header("Content-Length: ".strlen($body));
|
||||
} elseif ($this->code==200) {
|
||||
$this->code = 204;
|
||||
}
|
||||
try {
|
||||
$statusText = Arsse::$lang->msg("HTTP.Status.".$this->code);
|
||||
} catch (\JKingWeb\Arsse\Lang\Exception $e) {
|
||||
$statusText = "";
|
||||
}
|
||||
header("Status: ".$this->code." ".$statusText);
|
||||
if (!$this->head) {
|
||||
echo $body;
|
||||
}
|
||||
} else {
|
||||
throw new REST\Exception("headersSent");
|
||||
}
|
||||
}
|
||||
}
|
131
lib/REST/Target.php
Normal file
131
lib/REST/Target.php
Normal file
|
@ -0,0 +1,131 @@
|
|||
<?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;
|
||||
|
||||
use JKingWeb\Arsse\Misc\ValueInfo;
|
||||
|
||||
class Target {
|
||||
public $relative = false;
|
||||
public $index = false;
|
||||
public $path = [];
|
||||
public $query = "";
|
||||
public $fragment = "";
|
||||
|
||||
public function __construct(string $target) {
|
||||
$target = $this->parseFragment($target);
|
||||
$target = $this->parseQuery($target);
|
||||
$this->path = $this->parsePath($target);
|
||||
}
|
||||
|
||||
public function __toString(): string {
|
||||
$out = "";
|
||||
$path = [];
|
||||
foreach ($this->path as $segment) {
|
||||
if (is_null($segment)) {
|
||||
if (!$path) {
|
||||
$path[] = "..";
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
} elseif ($segment==".") {
|
||||
$path[] = "%2E";
|
||||
} elseif ($segment=="..") {
|
||||
$path[] = "%2E%2E";
|
||||
} else {
|
||||
$path[] = rawurlencode(ValueInfo::normalize($segment, ValueInfo::T_STRING));
|
||||
}
|
||||
}
|
||||
$path = implode("/", $path);
|
||||
if (!$this->relative) {
|
||||
$out .= "/";
|
||||
}
|
||||
$out .= $path;
|
||||
if ($this->index && strlen($path)) {
|
||||
$out .= "/";
|
||||
}
|
||||
if (strlen($this->query)) {
|
||||
$out .= "?".$this->query;
|
||||
}
|
||||
if (strlen($this->fragment)) {
|
||||
$out .= "#".rawurlencode($this->fragment);
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
|
||||
public static function normalize(string $target): string {
|
||||
return (string) new self($target);
|
||||
}
|
||||
|
||||
protected function parseFragment(string $target): string {
|
||||
// store and strip off any fragment identifier and return the target without a fragment
|
||||
$pos = strpos($target,"#");
|
||||
if ($pos !== false) {
|
||||
$this->fragment = rawurldecode(substr($target, $pos + 1));
|
||||
$target = substr($target, 0, $pos);
|
||||
}
|
||||
return $target;
|
||||
}
|
||||
|
||||
protected function parseQuery(string $target): string {
|
||||
// store and strip off any query string and return the target without a query
|
||||
// note that the function assumes any fragment identifier has already been stripped off
|
||||
// unlike the other parts the query string is currently neither parsed nor normalized
|
||||
$pos = strpos($target,"?");
|
||||
if ($pos !== false) {
|
||||
$this->query = substr($target, $pos + 1);
|
||||
$target = substr($target, 0, $pos);
|
||||
}
|
||||
return $target;
|
||||
}
|
||||
|
||||
protected function parsePath(string $target): array {
|
||||
// note that the function assumes any fragment identifier or query has already been stripped off
|
||||
// syntax-based normalization is applied to the path segments (see RFC 3986 sec. 6.2.2)
|
||||
// duplicate slashes are NOT collapsed
|
||||
if (substr($target, 0, 1)=="/") {
|
||||
// if the path starts with a slash, strip it off
|
||||
$target = substr($target, 1);
|
||||
} else {
|
||||
// otherwise this is a relative target
|
||||
$this->relative = true;
|
||||
}
|
||||
if (!strlen($target)) {
|
||||
// if the target is an empty string, this is an index target
|
||||
$this->index = true;
|
||||
} elseif (substr($target, -1, 1)=="/") {
|
||||
// if the path ends in a slash, this is an index target and the slash should be stripped off
|
||||
$this->index = true;
|
||||
$target = substr($target, 0, strlen($target) -1);
|
||||
}
|
||||
// after stripping, explode the path parts
|
||||
if (strlen($target)) {
|
||||
$target = explode("/", $target);
|
||||
$out = [];
|
||||
// resolve relative path segments and decode each retained segment
|
||||
foreach($target as $index => $segment) {
|
||||
if ($segment==".") {
|
||||
// self-referential segments can be ignored
|
||||
continue;
|
||||
} elseif ($segment=="..") {
|
||||
if ($index==0) {
|
||||
// if the first path segment refers to its parent (which we don't know about) we cannot output a correct path, so we do the best we can
|
||||
$out[] = null;
|
||||
} else {
|
||||
// for any other segments after the first we pop off the last stored segment
|
||||
array_pop($out);
|
||||
}
|
||||
} else {
|
||||
// any other segment is decoded and retained
|
||||
$out[] = rawurldecode($segment);
|
||||
}
|
||||
}
|
||||
return $out;
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
|
@ -19,7 +19,10 @@ use JKingWeb\Arsse\ExceptionType;
|
|||
use JKingWeb\Arsse\Db\ExceptionInput;
|
||||
use JKingWeb\Arsse\Db\ResultEmpty;
|
||||
use JKingWeb\Arsse\Feed\Exception as FeedException;
|
||||
use JKingWeb\Arsse\REST\Response;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Zend\Diactoros\Response\JsonResponse as Response;
|
||||
use Zend\Diactoros\Response\EmptyResponse;
|
||||
|
||||
class API extends \JKingWeb\Arsse\REST\AbstractHandler {
|
||||
const LEVEL = 14; // emulated API level
|
||||
|
@ -88,23 +91,24 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler {
|
|||
public function __construct() {
|
||||
}
|
||||
|
||||
public function dispatch(\JKingWeb\Arsse\REST\Request $req): Response {
|
||||
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 Response(404);
|
||||
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 Response(204, "", "", [
|
||||
"Allow: POST",
|
||||
"Accept: application/json, text/json",
|
||||
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(200, self::FATAL_ERR);
|
||||
return new Response(self::FATAL_ERR);
|
||||
}
|
||||
try {
|
||||
// normalize input
|
||||
|
@ -123,23 +127,23 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler {
|
|||
// TT-RSS operations are case-insensitive by dint of PHP method names being case-insensitive; this will only trigger if the method really doesn't exist
|
||||
throw new Exception("UNKNOWN_METHOD", ['method' => $data['op']]);
|
||||
}
|
||||
return new Response(200, [
|
||||
return new Response([
|
||||
'seq' => $data['seq'],
|
||||
'status' => 0,
|
||||
'content' => $this->$method($data),
|
||||
]);
|
||||
} catch (Exception $e) {
|
||||
return new Response(200, [
|
||||
return new Response([
|
||||
'seq' => $data['seq'],
|
||||
'status' => 1,
|
||||
'content' => $e->getData(),
|
||||
]);
|
||||
} catch (AbstractException $e) {
|
||||
return new Response(500);
|
||||
return new EmptyResponse(500);
|
||||
}
|
||||
} else {
|
||||
// absence of a request body indicates an error
|
||||
return new Response(200, self::FATAL_ERR);
|
||||
return new Response(self::FATAL_ERR);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -7,17 +7,19 @@ declare(strict_types=1);
|
|||
namespace JKingWeb\Arsse\REST\TinyTinyRSS;
|
||||
|
||||
use JKingWeb\Arsse\Arsse;
|
||||
use JKingWeb\Arsse\REST\Response;
|
||||
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): Response {
|
||||
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])) {
|
||||
return new Response(405, ['Allow' => "GET"]);
|
||||
} elseif (!preg_match("<^(\d+)\.ico$>", $req->getRequestTarget(), $match) || !((int) $match[1])) {
|
||||
return new Response(404);
|
||||
}
|
||||
$url = Arsse::$db->subscriptionFavicon((int) $match[1]);
|
||||
|
@ -26,7 +28,7 @@ class Icon extends \JKingWeb\Arsse\REST\AbstractHandler {
|
|||
if (($pos = strpos($url, "\r")) !== false || ($pos = strpos($url, "\n")) !== false) {
|
||||
$url = substr($url, 0, $pos);
|
||||
}
|
||||
return new Response(301, "", "", ["Location: $url"]);
|
||||
return new Response(301, ['Location' => $url]);
|
||||
} else {
|
||||
return new Response(404);
|
||||
}
|
||||
|
|
|
@ -9,4 +9,5 @@ namespace JKingWeb\Arsse;
|
|||
const NS_BASE = __NAMESPACE__."\\";
|
||||
define(NS_BASE."BASE", dirname(__DIR__).DIRECTORY_SEPARATOR);
|
||||
ini_set("memory_limit", "-1");
|
||||
error_reporting(\E_ALL);
|
||||
require_once BASE."vendor".DIRECTORY_SEPARATOR."autoload.php";
|
||||
|
|
|
@ -11,14 +11,16 @@ use JKingWeb\Arsse\Conf;
|
|||
use JKingWeb\Arsse\User;
|
||||
use JKingWeb\Arsse\Database;
|
||||
use JKingWeb\Arsse\Service;
|
||||
use JKingWeb\Arsse\REST\Request;
|
||||
use JKingWeb\Arsse\REST\Response;
|
||||
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;
|
||||
|
||||
/** @covers \JKingWeb\Arsse\REST\NextCloudNews\V1_2<extended> */
|
||||
|
@ -299,12 +301,49 @@ 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");
|
||||
if (Arsse::$user->auth()) {
|
||||
$req = $req->withAttribute("authenticated", true)->withAttribute("authenticatedUser", "john.doe@example.com");
|
||||
}
|
||||
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();
|
||||
// create a mock user manager
|
||||
Arsse::$user = Phake::mock(User::class);
|
||||
Phake::when(Arsse::$user)->authHTTP->thenReturn(true);
|
||||
Phake::when(Arsse::$user)->auth->thenReturn(true);
|
||||
Phake::when(Arsse::$user)->rightsGet->thenReturn(100);
|
||||
Arsse::$user->id = "john.doe@example.com";
|
||||
// create a mock database interface
|
||||
|
@ -321,15 +360,10 @@ class TestV1_2 extends \JKingWeb\Arsse\Test\AbstractTest {
|
|||
return $value;
|
||||
}
|
||||
|
||||
protected function assertResponse(Response $exp, Response $act, string $text = null) {
|
||||
$this->assertEquals($exp, $act, $text);
|
||||
$this->assertSame($exp->payload, $act->payload, $text);
|
||||
}
|
||||
|
||||
public function testSendAuthenticationChallenge() {
|
||||
Phake::when(Arsse::$user)->authHTTP->thenReturn(false);
|
||||
$exp = new Response(401, "", "", ['WWW-Authenticate: Basic realm="'.V1_2::REALM.'"']);
|
||||
$this->assertResponse($exp, $this->h->dispatch(new Request("GET", "/")));
|
||||
Phake::when(Arsse::$user)->auth->thenReturn(false);
|
||||
$exp = new EmptyResponse(401);
|
||||
$this->assertMessage($exp, $this->req("GET", "/"));
|
||||
}
|
||||
|
||||
public function testRespondToInvalidPaths() {
|
||||
|
@ -365,44 +399,45 @@ class TestV1_2 extends \JKingWeb\Arsse\Test\AbstractTest {
|
|||
],
|
||||
];
|
||||
foreach ($errs[404] as $req) {
|
||||
$exp = new Response(404);
|
||||
$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->assertMessage($exp, $this->req($method, $path), "$method call to $path did not return 404.");
|
||||
}
|
||||
foreach ($errs[405] as $allow => $cases) {
|
||||
$exp = new Response(405, "", "", ['Allow: '.$allow]);
|
||||
$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->assertMessage($exp, $this->req($method, $path), "$method call to $path did not return 405.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function testRespondToInvalidInputTypes() {
|
||||
$exp = new Response(415, "", "", ['Accept: application/json']);
|
||||
$this->assertResponse($exp, $this->h->dispatch(new Request("PUT", "/folders/1", '<data/>', 'application/xml')));
|
||||
$exp = new Response(400);
|
||||
$this->assertResponse($exp, $this->h->dispatch(new Request("PUT", "/folders/1", '<data/>', 'application/json')));
|
||||
$exp = new EmptyResponse(415, ['Accept' => "application/json"]);
|
||||
$this->assertMessage($exp, $this->req("PUT", "/folders/1", '<data/>', ['Content-Type' => "application/xml"]));
|
||||
$exp = new EmptyResponse(400);
|
||||
$this->assertMessage($exp, $this->req("PUT", "/folders/1", '<data/>'));
|
||||
$this->assertMessage($exp, $this->req("PUT", "/folders/1", '<data/>', ['Content-Type' => null]));
|
||||
}
|
||||
|
||||
public function testRespondToOptionsRequests() {
|
||||
$exp = new Response(204, "", "", [
|
||||
"Allow: HEAD,GET,POST",
|
||||
"Accept: application/json",
|
||||
$exp = new EmptyResponse(204, [
|
||||
'Allow' => "HEAD,GET,POST",
|
||||
'Accept' => "application/json",
|
||||
]);
|
||||
$this->assertResponse($exp, $this->h->dispatch(new Request("OPTIONS", "/feeds")));
|
||||
$exp = new Response(204, "", "", [
|
||||
"Allow: DELETE",
|
||||
"Accept: application/json",
|
||||
$this->assertMessage($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")));
|
||||
$exp = new Response(204, "", "", [
|
||||
"Allow: HEAD,GET",
|
||||
"Accept: application/json",
|
||||
$this->assertMessage($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")));
|
||||
$exp = new Response(404);
|
||||
$this->assertResponse($exp, $this->h->dispatch(new Request("OPTIONS", "/invalid/path")));
|
||||
$this->assertMessage($exp, $this->req("OPTIONS", "/user"));
|
||||
$exp = new EmptyResponse(404);
|
||||
$this->assertMessage($exp, $this->req("OPTIONS", "/invalid/path"));
|
||||
}
|
||||
|
||||
public function testListFolders() {
|
||||
|
@ -415,10 +450,10 @@ class TestV1_2 extends \JKingWeb\Arsse\Test\AbstractTest {
|
|||
['id' => 12, 'name' => "Hardware"],
|
||||
];
|
||||
Phake::when(Arsse::$db)->folderList(Arsse::$user->id, null, false)->thenReturn(new Result([]))->thenReturn(new Result($this->v($list)));
|
||||
$exp = new Response(200, ['folders' => []]);
|
||||
$this->assertResponse($exp, $this->h->dispatch(new Request("GET", "/folders")));
|
||||
$exp = new Response(200, ['folders' => $out]);
|
||||
$this->assertResponse($exp, $this->h->dispatch(new Request("GET", "/folders")));
|
||||
$exp = new Response(['folders' => []]);
|
||||
$this->assertMessage($exp, $this->req("GET", "/folders"));
|
||||
$exp = new Response(['folders' => $out]);
|
||||
$this->assertMessage($exp, $this->req("GET", "/folders"));
|
||||
}
|
||||
|
||||
public function testAddAFolder() {
|
||||
|
@ -445,34 +480,34 @@ class TestV1_2 extends \JKingWeb\Arsse\Test\AbstractTest {
|
|||
Phake::when(Arsse::$db)->folderAdd(Arsse::$user->id, ['name' => ""])->thenThrow(new ExceptionInput("missing"));
|
||||
Phake::when(Arsse::$db)->folderAdd(Arsse::$user->id, ['name' => " "])->thenThrow(new ExceptionInput("whitespace"));
|
||||
// correctly add two folders, using different means
|
||||
$exp = new Response(200, ['folders' => [$out[0]]]);
|
||||
$this->assertResponse($exp, $this->h->dispatch(new Request("POST", "/folders", json_encode($in[0]), 'application/json')));
|
||||
$exp = new Response(200, ['folders' => [$out[1]]]);
|
||||
$this->assertResponse($exp, $this->h->dispatch(new Request("POST", "/folders?name=Hardware")));
|
||||
$exp = new Response(['folders' => [$out[0]]]);
|
||||
$this->assertMessage($exp, $this->req("POST", "/folders", json_encode($in[0])));
|
||||
$exp = new Response(['folders' => [$out[1]]]);
|
||||
$this->assertMessage($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 Response(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')));
|
||||
$exp = new EmptyResponse(422);
|
||||
$this->assertMessage($exp, $this->req("POST", "/folders"));
|
||||
$this->assertMessage($exp, $this->req("POST", "/folders", '{"name":""}'));
|
||||
$this->assertMessage($exp, $this->req("POST", "/folders", '{"name":" "}'));
|
||||
$this->assertMessage($exp, $this->req("POST", "/folders", '{"name":{}}'));
|
||||
// try adding the same two folders again
|
||||
$exp = new Response(409);
|
||||
$this->assertResponse($exp, $this->h->dispatch(new Request("POST", "/folders?name=Software")));
|
||||
$exp = new Response(409);
|
||||
$this->assertResponse($exp, $this->h->dispatch(new Request("POST", "/folders", json_encode($in[1]), 'application/json')));
|
||||
$exp = new EmptyResponse(409);
|
||||
$this->assertMessage($exp, $this->req("POST", "/folders?name=Software"));
|
||||
$exp = new EmptyResponse(409);
|
||||
$this->assertMessage($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 Response(204);
|
||||
$this->assertResponse($exp, $this->h->dispatch(new Request("DELETE", "/folders/1")));
|
||||
$exp = new EmptyResponse(204);
|
||||
$this->assertMessage($exp, $this->req("DELETE", "/folders/1"));
|
||||
// fail on the second invocation because it no longer exists
|
||||
$exp = new Response(404);
|
||||
$this->assertResponse($exp, $this->h->dispatch(new Request("DELETE", "/folders/1")));
|
||||
$exp = new EmptyResponse(404);
|
||||
$this->assertMessage($exp, $this->req("DELETE", "/folders/1"));
|
||||
Phake::verify(Arsse::$db, Phake::times(2))->folderRemove(Arsse::$user->id, 1);
|
||||
}
|
||||
|
||||
|
@ -490,26 +525,26 @@ class TestV1_2 extends \JKingWeb\Arsse\Test\AbstractTest {
|
|||
Phake::when(Arsse::$db)->folderPropertiesSet(Arsse::$user->id, 1, $in[3])->thenThrow(new ExceptionInput("whitespace"));
|
||||
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 Response(204);
|
||||
$this->assertResponse($exp, $this->h->dispatch(new Request("PUT", "/folders/1", json_encode($in[0]), 'application/json')));
|
||||
$exp = new Response(409);
|
||||
$this->assertResponse($exp, $this->h->dispatch(new Request("PUT", "/folders/2", json_encode($in[1]), 'application/json')));
|
||||
$exp = new Response(422);
|
||||
$this->assertResponse($exp, $this->h->dispatch(new Request("PUT", "/folders/1", json_encode($in[2]), 'application/json')));
|
||||
$exp = new Response(422);
|
||||
$this->assertResponse($exp, $this->h->dispatch(new Request("PUT", "/folders/1", json_encode($in[3]), 'application/json')));
|
||||
$exp = new Response(422);
|
||||
$this->assertResponse($exp, $this->h->dispatch(new Request("PUT", "/folders/1", json_encode($in[4]), 'application/json')));
|
||||
$exp = new Response(404);
|
||||
$this->assertResponse($exp, $this->h->dispatch(new Request("PUT", "/folders/3", json_encode($in[0]), 'application/json')));
|
||||
$exp = new EmptyResponse(204);
|
||||
$this->assertMessage($exp, $this->req("PUT", "/folders/1", json_encode($in[0])));
|
||||
$exp = new EmptyResponse(409);
|
||||
$this->assertMessage($exp, $this->req("PUT", "/folders/2", json_encode($in[1])));
|
||||
$exp = new EmptyResponse(422);
|
||||
$this->assertMessage($exp, $this->req("PUT", "/folders/1", json_encode($in[2])));
|
||||
$exp = new EmptyResponse(422);
|
||||
$this->assertMessage($exp, $this->req("PUT", "/folders/1", json_encode($in[3])));
|
||||
$exp = new EmptyResponse(422);
|
||||
$this->assertMessage($exp, $this->req("PUT", "/folders/1", json_encode($in[4])));
|
||||
$exp = new EmptyResponse(404);
|
||||
$this->assertMessage($exp, $this->req("PUT", "/folders/3", json_encode($in[0])));
|
||||
}
|
||||
|
||||
public function testRetrieveServerVersion() {
|
||||
$exp = new Response(200, [
|
||||
$exp = new Response([
|
||||
'version' => V1_2::VERSION,
|
||||
'arsse_version' => Arsse::VERSION,
|
||||
]);
|
||||
$this->assertResponse($exp, $this->h->dispatch(new Request("GET", "/version")));
|
||||
$this->assertMessage($exp, $this->req("GET", "/version"));
|
||||
}
|
||||
|
||||
public function testListSubscriptions() {
|
||||
|
@ -525,10 +560,10 @@ class TestV1_2 extends \JKingWeb\Arsse\Test\AbstractTest {
|
|||
Phake::when(Arsse::$db)->subscriptionList(Arsse::$user->id)->thenReturn(new Result([]))->thenReturn(new Result($this->v($this->feeds['db'])));
|
||||
Phake::when(Arsse::$db)->articleStarred(Arsse::$user->id)->thenReturn($this->v(['total' => 0]))->thenReturn($this->v(['total' => 5]));
|
||||
Phake::when(Arsse::$db)->editionLatest(Arsse::$user->id)->thenReturn(0)->thenReturn(4758915);
|
||||
$exp = new Response(200, $exp1);
|
||||
$this->assertResponse($exp, $this->h->dispatch(new Request("GET", "/feeds")));
|
||||
$exp = new Response(200, $exp2);
|
||||
$this->assertResponse($exp, $this->h->dispatch(new Request("GET", "/feeds")));
|
||||
$exp = new Response($exp1);
|
||||
$this->assertMessage($exp, $this->req("GET", "/feeds"));
|
||||
$exp = new Response($exp2);
|
||||
$this->assertMessage($exp, $this->req("GET", "/feeds"));
|
||||
}
|
||||
|
||||
public function testAddASubscription() {
|
||||
|
@ -560,32 +595,32 @@ class TestV1_2 extends \JKingWeb\Arsse\Test\AbstractTest {
|
|||
// set up a mock for a bad feed which succeeds the second time
|
||||
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(200, $out[0]);
|
||||
$this->assertResponse($exp, $this->h->dispatch(new Request("POST", "/feeds", json_encode($in[0]), 'application/json')));
|
||||
$exp = new Response(200, $out[1]);
|
||||
$this->assertResponse($exp, $this->h->dispatch(new Request("POST", "/feeds", json_encode($in[1]), 'application/json')));
|
||||
$exp = new Response($out[0]);
|
||||
$this->assertMessage($exp, $this->req("POST", "/feeds", json_encode($in[0])));
|
||||
$exp = new Response($out[1]);
|
||||
$this->assertMessage($exp, $this->req("POST", "/feeds", json_encode($in[1])));
|
||||
// try to add them a second time
|
||||
$exp = new Response(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')));
|
||||
$exp = new EmptyResponse(409);
|
||||
$this->assertMessage($exp, $this->req("POST", "/feeds", json_encode($in[0])));
|
||||
$this->assertMessage($exp, $this->req("POST", "/feeds", json_encode($in[1])));
|
||||
// try to add a bad feed
|
||||
$exp = new Response(422);
|
||||
$this->assertResponse($exp, $this->h->dispatch(new Request("POST", "/feeds", json_encode($in[2]), 'application/json')));
|
||||
$exp = new EmptyResponse(422);
|
||||
$this->assertMessage($exp, $this->req("POST", "/feeds", json_encode($in[2])));
|
||||
// try again (this will succeed), with an invalid folder ID
|
||||
$exp = new Response(200, $out[2]);
|
||||
$this->assertResponse($exp, $this->h->dispatch(new Request("POST", "/feeds", json_encode($in[3]), 'application/json')));
|
||||
$exp = new Response($out[2]);
|
||||
$this->assertMessage($exp, $this->req("POST", "/feeds", json_encode($in[3])));
|
||||
// try to add no feed
|
||||
$exp = new Response(422);
|
||||
$this->assertResponse($exp, $this->h->dispatch(new Request("POST", "/feeds", json_encode($in[4]), 'application/json')));
|
||||
$exp = new EmptyResponse(422);
|
||||
$this->assertMessage($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 Response(204);
|
||||
$this->assertResponse($exp, $this->h->dispatch(new Request("DELETE", "/feeds/1")));
|
||||
$exp = new EmptyResponse(204);
|
||||
$this->assertMessage($exp, $this->req("DELETE", "/feeds/1"));
|
||||
// fail on the second invocation because it no longer exists
|
||||
$exp = new Response(404);
|
||||
$this->assertResponse($exp, $this->h->dispatch(new Request("DELETE", "/feeds/1")));
|
||||
$exp = new EmptyResponse(404);
|
||||
$this->assertMessage($exp, $this->req("DELETE", "/feeds/1"));
|
||||
Phake::verify(Arsse::$db, Phake::times(2))->subscriptionRemove(Arsse::$user->id, 1);
|
||||
}
|
||||
|
||||
|
@ -603,18 +638,18 @@ class TestV1_2 extends \JKingWeb\Arsse\Test\AbstractTest {
|
|||
Phake::when(Arsse::$db)->subscriptionPropertiesSet(Arsse::$user->id, 1, ['folder' => 2112])->thenThrow(new ExceptionInput("idMissing")); // folder does not exist
|
||||
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 Response(204);
|
||||
$this->assertResponse($exp, $this->h->dispatch(new Request("PUT", "/feeds/1/move", json_encode($in[0]), 'application/json')));
|
||||
$exp = new Response(204);
|
||||
$this->assertResponse($exp, $this->h->dispatch(new Request("PUT", "/feeds/1/move", json_encode($in[1]), 'application/json')));
|
||||
$exp = new Response(422);
|
||||
$this->assertResponse($exp, $this->h->dispatch(new Request("PUT", "/feeds/1/move", json_encode($in[2]), 'application/json')));
|
||||
$exp = new Response(404);
|
||||
$this->assertResponse($exp, $this->h->dispatch(new Request("PUT", "/feeds/42/move", json_encode($in[3]), 'application/json')));
|
||||
$exp = new Response(422);
|
||||
$this->assertResponse($exp, $this->h->dispatch(new Request("PUT", "/feeds/1/move", json_encode($in[4]), 'application/json')));
|
||||
$exp = new Response(422);
|
||||
$this->assertResponse($exp, $this->h->dispatch(new Request("PUT", "/feeds/1/move", json_encode($in[5]), 'application/json')));
|
||||
$exp = new EmptyResponse(204);
|
||||
$this->assertMessage($exp, $this->req("PUT", "/feeds/1/move", json_encode($in[0])));
|
||||
$exp = new EmptyResponse(204);
|
||||
$this->assertMessage($exp, $this->req("PUT", "/feeds/1/move", json_encode($in[1])));
|
||||
$exp = new EmptyResponse(422);
|
||||
$this->assertMessage($exp, $this->req("PUT", "/feeds/1/move", json_encode($in[2])));
|
||||
$exp = new EmptyResponse(404);
|
||||
$this->assertMessage($exp, $this->req("PUT", "/feeds/42/move", json_encode($in[3])));
|
||||
$exp = new EmptyResponse(422);
|
||||
$this->assertMessage($exp, $this->req("PUT", "/feeds/1/move", json_encode($in[4])));
|
||||
$exp = new EmptyResponse(422);
|
||||
$this->assertMessage($exp, $this->req("PUT", "/feeds/1/move", json_encode($in[5])));
|
||||
}
|
||||
|
||||
public function testRenameASubscription() {
|
||||
|
@ -633,18 +668,18 @@ class TestV1_2 extends \JKingWeb\Arsse\Test\AbstractTest {
|
|||
Phake::when(Arsse::$db)->subscriptionPropertiesSet(Arsse::$user->id, 1, $this->identicalTo(['title' => ""]))->thenThrow(new ExceptionInput("missing"));
|
||||
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 Response(422);
|
||||
$this->assertResponse($exp, $this->h->dispatch(new Request("PUT", "/feeds/1/rename", json_encode($in[0]), 'application/json')));
|
||||
$exp = new Response(204);
|
||||
$this->assertResponse($exp, $this->h->dispatch(new Request("PUT", "/feeds/1/rename", json_encode($in[1]), 'application/json')));
|
||||
$exp = new Response(422);
|
||||
$this->assertResponse($exp, $this->h->dispatch(new Request("PUT", "/feeds/1/rename", json_encode($in[2]), 'application/json')));
|
||||
$exp = new Response(422);
|
||||
$this->assertResponse($exp, $this->h->dispatch(new Request("PUT", "/feeds/1/rename", json_encode($in[3]), 'application/json')));
|
||||
$exp = new Response(404);
|
||||
$this->assertResponse($exp, $this->h->dispatch(new Request("PUT", "/feeds/42/rename", json_encode($in[4]), 'application/json')));
|
||||
$exp = new Response(422);
|
||||
$this->assertResponse($exp, $this->h->dispatch(new Request("PUT", "/feeds/1/rename", json_encode($in[6]), 'application/json')));
|
||||
$exp = new EmptyResponse(422);
|
||||
$this->assertMessage($exp, $this->req("PUT", "/feeds/1/rename", json_encode($in[0])));
|
||||
$exp = new EmptyResponse(204);
|
||||
$this->assertMessage($exp, $this->req("PUT", "/feeds/1/rename", json_encode($in[1])));
|
||||
$exp = new EmptyResponse(422);
|
||||
$this->assertMessage($exp, $this->req("PUT", "/feeds/1/rename", json_encode($in[2])));
|
||||
$exp = new EmptyResponse(422);
|
||||
$this->assertMessage($exp, $this->req("PUT", "/feeds/1/rename", json_encode($in[3])));
|
||||
$exp = new EmptyResponse(404);
|
||||
$this->assertMessage($exp, $this->req("PUT", "/feeds/42/rename", json_encode($in[4])));
|
||||
$exp = new EmptyResponse(422);
|
||||
$this->assertMessage($exp, $this->req("PUT", "/feeds/1/rename", json_encode($in[6])));
|
||||
}
|
||||
|
||||
public function testListStaleFeeds() {
|
||||
|
@ -659,12 +694,12 @@ class TestV1_2 extends \JKingWeb\Arsse\Test\AbstractTest {
|
|||
],
|
||||
];
|
||||
Phake::when(Arsse::$db)->feedListStale->thenReturn($this->v(array_column($out, "id")));
|
||||
$exp = new Response(200, ['feeds' => $out]);
|
||||
$this->assertResponse($exp, $this->h->dispatch(new Request("GET", "/feeds/all")));
|
||||
$exp = new Response(['feeds' => $out]);
|
||||
$this->assertMessage($exp, $this->req("GET", "/feeds/all"));
|
||||
// retrieving the list when not an admin fails
|
||||
Phake::when(Arsse::$user)->rightsGet->thenReturn(0);
|
||||
$exp = new Response(403);
|
||||
$this->assertResponse($exp, $this->h->dispatch(new Request("GET", "/feeds/all")));
|
||||
$exp = new EmptyResponse(403);
|
||||
$this->assertMessage($exp, $this->req("GET", "/feeds/all"));
|
||||
}
|
||||
|
||||
public function testUpdateAFeed() {
|
||||
|
@ -678,18 +713,18 @@ class TestV1_2 extends \JKingWeb\Arsse\Test\AbstractTest {
|
|||
Phake::when(Arsse::$db)->feedUpdate(42)->thenReturn(true);
|
||||
Phake::when(Arsse::$db)->feedUpdate(2112)->thenThrow(new ExceptionInput("subjectMissing"));
|
||||
Phake::when(Arsse::$db)->feedUpdate($this->lessThan(1))->thenThrow(new ExceptionInput("typeViolation"));
|
||||
$exp = new Response(204);
|
||||
$this->assertResponse($exp, $this->h->dispatch(new Request("GET", "/feeds/update", json_encode($in[0]), 'application/json')));
|
||||
$exp = new Response(404);
|
||||
$this->assertResponse($exp, $this->h->dispatch(new Request("GET", "/feeds/update", json_encode($in[1]), 'application/json')));
|
||||
$exp = new Response(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')));
|
||||
$exp = new EmptyResponse(204);
|
||||
$this->assertMessage($exp, $this->req("GET", "/feeds/update", json_encode($in[0])));
|
||||
$exp = new EmptyResponse(404);
|
||||
$this->assertMessage($exp, $this->req("GET", "/feeds/update", json_encode($in[1])));
|
||||
$exp = new EmptyResponse(422);
|
||||
$this->assertMessage($exp, $this->req("GET", "/feeds/update", json_encode($in[2])));
|
||||
$this->assertMessage($exp, $this->req("GET", "/feeds/update", json_encode($in[3])));
|
||||
$this->assertMessage($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 Response(403);
|
||||
$this->assertResponse($exp, $this->h->dispatch(new Request("GET", "/feeds/update", json_encode($in[0]), 'application/json')));
|
||||
$exp = new EmptyResponse(403);
|
||||
$this->assertMessage($exp, $this->req("GET", "/feeds/update", json_encode($in[0])));
|
||||
}
|
||||
|
||||
public function testListArticles() {
|
||||
|
@ -713,25 +748,25 @@ class TestV1_2 extends \JKingWeb\Arsse\Test\AbstractTest {
|
|||
Phake::when(Arsse::$db)->articleList(Arsse::$user->id, (new Context)->reverse(true)->folder(2112), Database::LIST_TYPICAL)->thenThrow(new ExceptionInput("idMissing"));
|
||||
Phake::when(Arsse::$db)->articleList(Arsse::$user->id, (new Context)->reverse(true)->subscription(-1), Database::LIST_TYPICAL)->thenThrow(new ExceptionInput("typeViolation"));
|
||||
Phake::when(Arsse::$db)->articleList(Arsse::$user->id, (new Context)->reverse(true)->folder(-1), Database::LIST_TYPICAL)->thenThrow(new ExceptionInput("typeViolation"));
|
||||
$exp = new Response(200, ['items' => $this->articles['rest']]);
|
||||
$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->assertMessage($exp, $this->req("GET", "/items")); // first instance of base context
|
||||
$this->assertMessage($exp, $this->req("GET", "/items/updated")); // second instance of base context
|
||||
// check error conditions
|
||||
$exp = new Response(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')));
|
||||
$exp = new EmptyResponse(422);
|
||||
$this->assertMessage($exp, $this->req("GET", "/items", json_encode($in[0])));
|
||||
$this->assertMessage($exp, $this->req("GET", "/items", json_encode($in[1])));
|
||||
$this->assertMessage($exp, $this->req("GET", "/items", json_encode($in[2])));
|
||||
$this->assertMessage($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);
|
||||
|
@ -751,14 +786,14 @@ class TestV1_2 extends \JKingWeb\Arsse\Test\AbstractTest {
|
|||
$in = json_encode(['newestItemId' => 2112]);
|
||||
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 Response(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")));
|
||||
$exp = new Response(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")));
|
||||
$exp = new Response(404);
|
||||
$this->assertResponse($exp, $this->h->dispatch(new Request("PUT", "/folders/42/read", $in, 'application/json')));
|
||||
$exp = new EmptyResponse(204);
|
||||
$this->assertMessage($exp, $this->req("PUT", "/folders/1/read", $in));
|
||||
$this->assertMessage($exp, $this->req("PUT", "/folders/1/read?newestItemId=2112"));
|
||||
$exp = new EmptyResponse(422);
|
||||
$this->assertMessage($exp, $this->req("PUT", "/folders/1/read"));
|
||||
$this->assertMessage($exp, $this->req("PUT", "/folders/1/read?newestItemId=ook"));
|
||||
$exp = new EmptyResponse(404);
|
||||
$this->assertMessage($exp, $this->req("PUT", "/folders/42/read", $in));
|
||||
}
|
||||
|
||||
public function testMarkASubscriptionRead() {
|
||||
|
@ -766,26 +801,26 @@ class TestV1_2 extends \JKingWeb\Arsse\Test\AbstractTest {
|
|||
$in = json_encode(['newestItemId' => 2112]);
|
||||
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 Response(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")));
|
||||
$exp = new Response(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")));
|
||||
$exp = new Response(404);
|
||||
$this->assertResponse($exp, $this->h->dispatch(new Request("PUT", "/feeds/42/read", $in, 'application/json')));
|
||||
$exp = new EmptyResponse(204);
|
||||
$this->assertMessage($exp, $this->req("PUT", "/feeds/1/read", $in));
|
||||
$this->assertMessage($exp, $this->req("PUT", "/feeds/1/read?newestItemId=2112"));
|
||||
$exp = new EmptyResponse(422);
|
||||
$this->assertMessage($exp, $this->req("PUT", "/feeds/1/read"));
|
||||
$this->assertMessage($exp, $this->req("PUT", "/feeds/1/read?newestItemId=ook"));
|
||||
$exp = new EmptyResponse(404);
|
||||
$this->assertMessage($exp, $this->req("PUT", "/feeds/42/read", $in));
|
||||
}
|
||||
|
||||
public function testMarkAllItemsRead() {
|
||||
$read = ['read' => true];
|
||||
$in = json_encode(['newestItemId' => 2112]);
|
||||
Phake::when(Arsse::$db)->articleMark(Arsse::$user->id, $read, (new Context)->latestEdition(2112))->thenReturn(42);
|
||||
$exp = new Response(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")));
|
||||
$exp = new Response(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")));
|
||||
$exp = new EmptyResponse(204);
|
||||
$this->assertMessage($exp, $this->req("PUT", "/items/read", $in));
|
||||
$this->assertMessage($exp, $this->req("PUT", "/items/read?newestItemId=2112"));
|
||||
$exp = new EmptyResponse(422);
|
||||
$this->assertMessage($exp, $this->req("PUT", "/items/read"));
|
||||
$this->assertMessage($exp, $this->req("PUT", "/items/read?newestItemId=ook"));
|
||||
}
|
||||
|
||||
public function testChangeMarksOfASingleArticle() {
|
||||
|
@ -801,16 +836,16 @@ class TestV1_2 extends \JKingWeb\Arsse\Test\AbstractTest {
|
|||
Phake::when(Arsse::$db)->articleMark(Arsse::$user->id, $star, (new Context)->article(2112))->thenThrow(new ExceptionInput("subjectMissing")); // article doesn't exist doesn't exist
|
||||
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 Response(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")));
|
||||
$exp = new Response(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")));
|
||||
$exp = new EmptyResponse(204);
|
||||
$this->assertMessage($exp, $this->req("PUT", "/items/1/read"));
|
||||
$this->assertMessage($exp, $this->req("PUT", "/items/2/unread"));
|
||||
$this->assertMessage($exp, $this->req("PUT", "/items/1/3/star"));
|
||||
$this->assertMessage($exp, $this->req("PUT", "/items/4400/4/unstar"));
|
||||
$exp = new EmptyResponse(404);
|
||||
$this->assertMessage($exp, $this->req("PUT", "/items/42/read"));
|
||||
$this->assertMessage($exp, $this->req("PUT", "/items/47/unread"));
|
||||
$this->assertMessage($exp, $this->req("PUT", "/items/1/2112/star"));
|
||||
$this->assertMessage($exp, $this->req("PUT", "/items/4400/1337/unstar"));
|
||||
Phake::verify(Arsse::$db, Phake::times(8))->articleMark(Arsse::$user->id, $this->anything(), $this->anything());
|
||||
}
|
||||
|
||||
|
@ -832,27 +867,27 @@ class TestV1_2 extends \JKingWeb\Arsse\Test\AbstractTest {
|
|||
Phake::when(Arsse::$db)->articleMark(Arsse::$user->id, $this->anything(), $this->anything())->thenReturn(42);
|
||||
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 Response(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')));
|
||||
$exp = new EmptyResponse(204);
|
||||
$this->assertMessage($exp, $this->req("PUT", "/items/read/multiple"));
|
||||
$this->assertMessage($exp, $this->req("PUT", "/items/unread/multiple"));
|
||||
$this->assertMessage($exp, $this->req("PUT", "/items/star/multiple"));
|
||||
$this->assertMessage($exp, $this->req("PUT", "/items/unstar/multiple"));
|
||||
$this->assertMessage($exp, $this->req("PUT", "/items/read/multiple", json_encode(['items' => "ook"])));
|
||||
$this->assertMessage($exp, $this->req("PUT", "/items/unread/multiple", json_encode(['items' => "ook"])));
|
||||
$this->assertMessage($exp, $this->req("PUT", "/items/star/multiple", json_encode(['items' => "ook"])));
|
||||
$this->assertMessage($exp, $this->req("PUT", "/items/unstar/multiple", json_encode(['items' => "ook"])));
|
||||
$this->assertMessage($exp, $this->req("PUT", "/items/read/multiple", json_encode(['items' => []])));
|
||||
$this->assertMessage($exp, $this->req("PUT", "/items/unread/multiple", json_encode(['items' => []])));
|
||||
$this->assertMessage($exp, $this->req("PUT", "/items/read/multiple", json_encode(['items' => $in[0]])));
|
||||
$this->assertMessage($exp, $this->req("PUT", "/items/unread/multiple", json_encode(['items' => $in[0]])));
|
||||
$this->assertMessage($exp, $this->req("PUT", "/items/read/multiple", json_encode(['items' => $in[1]])));
|
||||
$this->assertMessage($exp, $this->req("PUT", "/items/unread/multiple", json_encode(['items' => $in[1]])));
|
||||
$this->assertMessage($exp, $this->req("PUT", "/items/star/multiple", json_encode(['items' => []])));
|
||||
$this->assertMessage($exp, $this->req("PUT", "/items/unstar/multiple", json_encode(['items' => []])));
|
||||
$this->assertMessage($exp, $this->req("PUT", "/items/star/multiple", json_encode(['items' => $inStar[0]])));
|
||||
$this->assertMessage($exp, $this->req("PUT", "/items/unstar/multiple", json_encode(['items' => $inStar[0]])));
|
||||
$this->assertMessage($exp, $this->req("PUT", "/items/star/multiple", json_encode(['items' => $inStar[1]])));
|
||||
$this->assertMessage($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]));
|
||||
|
@ -885,29 +920,29 @@ class TestV1_2 extends \JKingWeb\Arsse\Test\AbstractTest {
|
|||
];
|
||||
$arr2['warnings']['improperlyConfiguredCron'] = true;
|
||||
$arr2['warnings']['incorrectDbCharset'] = true;
|
||||
$exp = new Response(200, $arr1);
|
||||
$this->assertResponse($exp, $this->h->dispatch(new Request("GET", "/status")));
|
||||
$exp = new Response($arr1);
|
||||
$this->assertMessage($exp, $this->req("GET", "/status"));
|
||||
}
|
||||
|
||||
public function testCleanUpBeforeUpdate() {
|
||||
Phake::when(Arsse::$db)->feedCleanup()->thenReturn(true);
|
||||
$exp = new Response(204);
|
||||
$this->assertResponse($exp, $this->h->dispatch(new Request("GET", "/cleanup/before-update")));
|
||||
$exp = new EmptyResponse(204);
|
||||
$this->assertMessage($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 Response(403);
|
||||
$this->assertResponse($exp, $this->h->dispatch(new Request("GET", "/cleanup/before-update")));
|
||||
$exp = new EmptyResponse(403);
|
||||
$this->assertMessage($exp, $this->req("GET", "/cleanup/before-update"));
|
||||
}
|
||||
|
||||
public function testCleanUpAfterUpdate() {
|
||||
Phake::when(Arsse::$db)->articleCleanup()->thenReturn(true);
|
||||
$exp = new Response(204);
|
||||
$this->assertResponse($exp, $this->h->dispatch(new Request("GET", "/cleanup/after-update")));
|
||||
$exp = new EmptyResponse(204);
|
||||
$this->assertMessage($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 Response(403);
|
||||
$this->assertResponse($exp, $this->h->dispatch(new Request("GET", "/cleanup/after-update")));
|
||||
$exp = new EmptyResponse(403);
|
||||
$this->assertMessage($exp, $this->req("GET", "/cleanup/after-update"));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,8 +7,10 @@ declare(strict_types=1);
|
|||
namespace JKingWeb\Arsse\TestCase\REST\NextCloudNews;
|
||||
|
||||
use JKingWeb\Arsse\REST\NextCloudNews\Versions;
|
||||
use JKingWeb\Arsse\REST\Request;
|
||||
use JKingWeb\Arsse\REST\Response;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Zend\Diactoros\ServerRequest;
|
||||
use Zend\Diactoros\Response\JsonResponse as Response;
|
||||
use Zend\Diactoros\Response\EmptyResponse;
|
||||
|
||||
/** @covers \JKingWeb\Arsse\REST\NextCloudNews\Versions */
|
||||
class TestVersions extends \JKingWeb\Arsse\Test\AbstractTest {
|
||||
|
@ -16,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(200, ['apiLevels' => ['v1-2']]);
|
||||
$h = new Versions;
|
||||
$req = new Request("GET", "/");
|
||||
$res = $h->dispatch($req);
|
||||
$this->assertEquals($exp, $res);
|
||||
$req = new Request("GET", "");
|
||||
$res = $h->dispatch($req);
|
||||
$this->assertEquals($exp, $res);
|
||||
$req = new Request("GET", "/?id=1827");
|
||||
$res = $h->dispatch($req);
|
||||
$this->assertEquals($exp, $res);
|
||||
$exp = new Response(['apiLevels' => ['v1-2']]);
|
||||
$this->assertMessage($exp, $this->req("GET", "/"));
|
||||
$this->assertMessage($exp, $this->req("GET", "/"));
|
||||
$this->assertMessage($exp, $this->req("GET", "/"));
|
||||
}
|
||||
|
||||
public function testRespondToOptionsRequest() {
|
||||
$exp = new Response(204, "", "", ["Allow: HEAD,GET"]);
|
||||
$h = new Versions;
|
||||
$req = new Request("OPTIONS", "/");
|
||||
$res = $h->dispatch($req);
|
||||
$this->assertEquals($exp, $res);
|
||||
$exp = new EmptyResponse(204, ['Allow' => "HEAD,GET"]);
|
||||
$this->assertMessage($exp, $this->req("OPTIONS", "/"));
|
||||
}
|
||||
|
||||
public function testUseIncorrectMethod() {
|
||||
$exp = new Response(405, "", "", ["Allow: HEAD,GET"]);
|
||||
$h = new Versions;
|
||||
$req = new Request("POST", "/");
|
||||
$res = $h->dispatch($req);
|
||||
$this->assertEquals($exp, $res);
|
||||
$exp = new EmptyResponse(405, ['Allow' => "HEAD,GET"]);
|
||||
$this->assertMessage($exp, $this->req("POST", "/"));
|
||||
}
|
||||
|
||||
public function testUseIncorrectPath() {
|
||||
$exp = new Response(404);
|
||||
$h = new Versions;
|
||||
$req = new Request("GET", "/ook");
|
||||
$res = $h->dispatch($req);
|
||||
$this->assertEquals($exp, $res);
|
||||
$req = new Request("OPTIONS", "/ook");
|
||||
$res = $h->dispatch($req);
|
||||
$this->assertEquals($exp, $res);
|
||||
$exp = new EmptyResponse(404);
|
||||
$this->assertMessage($exp, $this->req("GET", "/ook"));
|
||||
$this->assertMessage($exp, $this->req("OPTIONS", "/ook"));
|
||||
}
|
||||
}
|
||||
|
|
334
tests/cases/REST/TestREST.php
Normal file
334
tests/cases/REST/TestREST.php
Normal file
|
@ -0,0 +1,334 @@
|
|||
<?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\TestCase\REST;
|
||||
|
||||
use JKingWeb\Arsse\Arsse;
|
||||
use JKingWeb\Arsse\User;
|
||||
use JKingWeb\Arsse\REST;
|
||||
use JKingWeb\Arsse\REST\Handler;
|
||||
use JKingWeb\Arsse\REST\Exception501;
|
||||
use JKingWeb\Arsse\REST\NextCloudNews\V1_2 as NCN;
|
||||
use JKingWeb\Arsse\REST\TinyTinyRSS\API as TTRSS;
|
||||
use Psr\Http\Message\RequestInterface;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Zend\Diactoros\Request;
|
||||
use Zend\Diactoros\Response;
|
||||
use Zend\Diactoros\ServerRequest;
|
||||
use Zend\Diactoros\Response\TextResponse;
|
||||
use Zend\Diactoros\Response\EmptyResponse;
|
||||
use Phake;
|
||||
|
||||
/** @covers \JKingWeb\Arsse\REST */
|
||||
class TestREST extends \JKingWeb\Arsse\Test\AbstractTest {
|
||||
|
||||
/** @dataProvider provideApiMatchData */
|
||||
public function testMatchAUrlToAnApi($apiList, string $input, array $exp) {
|
||||
$r = new REST($apiList);
|
||||
try {
|
||||
$out = $r->apiMatch($input);
|
||||
} catch (Exception501 $e) {
|
||||
$out = [];
|
||||
}
|
||||
$this->assertEquals($exp, $out);
|
||||
}
|
||||
|
||||
public function provideApiMatchData() {
|
||||
$real = null;
|
||||
$fake = [
|
||||
'unstripped' => ['match' => "/full/url", 'strip' => "", 'class' => "UnstrippedProtocol"],
|
||||
];
|
||||
return [
|
||||
[$real, "/index.php/apps/news/api/v1-2/feeds", ["ncn_v1-2", "/feeds", \JKingWeb\Arsse\REST\NextCloudNews\V1_2::class]],
|
||||
[$real, "/index.php/apps/news/api/v1-2", ["ncn", "/v1-2", \JKingWeb\Arsse\REST\NextCloudNews\Versions::class]],
|
||||
[$real, "/index.php/apps/news/api/", ["ncn", "/", \JKingWeb\Arsse\REST\NextCloudNews\Versions::class]],
|
||||
[$real, "/index%2Ephp/apps/news/api/", ["ncn", "/", \JKingWeb\Arsse\REST\NextCloudNews\Versions::class]],
|
||||
[$real, "/index.php/apps/news/", []],
|
||||
[$real, "/index!php/apps/news/api/", []],
|
||||
[$real, "/tt-rss/api/index.php", ["ttrss_api", "/index.php", \JKingWeb\Arsse\REST\TinyTinyRSS\API::class]],
|
||||
[$real, "/tt-rss/api", ["ttrss_api", "", \JKingWeb\Arsse\REST\TinyTinyRSS\API::class]],
|
||||
[$real, "/tt-rss/API", []],
|
||||
[$real, "/tt-rss/api-bogus", []],
|
||||
[$real, "/tt-rss/api bogus", []],
|
||||
[$real, "/tt-rss/feed-icons/", ["ttrss_icon", "", \JKingWeb\Arsse\REST\TinyTinyRSS\Icon::class]],
|
||||
[$real, "/tt-rss/feed-icons/", ["ttrss_icon", "", \JKingWeb\Arsse\REST\TinyTinyRSS\Icon::class]],
|
||||
[$real, "/tt-rss/feed-icons", []],
|
||||
[$fake, "/full/url/", ["unstripped", "/full/url/", "UnstrippedProtocol"]],
|
||||
[$fake, "/full/url-not", []],
|
||||
];
|
||||
}
|
||||
|
||||
/** @dataProvider provideAuthenticableRequests */
|
||||
public function testAuthenticateRequests(array $serverParams, array $expAttr) {
|
||||
$r = new REST();
|
||||
// create a mock user manager
|
||||
Arsse::$user = Phake::mock(User::class);
|
||||
Phake::when(Arsse::$user)->auth->thenReturn(true);
|
||||
Phake::when(Arsse::$user)->auth($this->anything(), "superman")->thenReturn(false);
|
||||
Phake::when(Arsse::$user)->auth("jane.doe@example.com", $this->anything())->thenReturn(false);
|
||||
// create an input server request
|
||||
$req = new ServerRequest($serverParams);
|
||||
// create the expected output
|
||||
$exp = $req;
|
||||
foreach ($expAttr as $key => $value) {
|
||||
$exp = $exp->withAttribute($key, $value);
|
||||
}
|
||||
$act = $r->authenticateRequest($req);
|
||||
$this->assertMessage($exp, $act);
|
||||
}
|
||||
|
||||
public function provideAuthenticableRequests() {
|
||||
return [
|
||||
[['PHP_AUTH_USER' => "john.doe@example.com", 'PHP_AUTH_PW' => "secret"], ['authenticated' => true, 'authenticatedUser' => "john.doe@example.com"]],
|
||||
[['PHP_AUTH_USER' => "john.doe@example.com", 'PHP_AUTH_PW' => "secret", 'REMOTE_USER' => "jane.doe@example.com"], ['authenticated' => true, 'authenticatedUser' => "john.doe@example.com"]],
|
||||
[['PHP_AUTH_USER' => "jane.doe@example.com", 'PHP_AUTH_PW' => "secret"], []],
|
||||
[['PHP_AUTH_USER' => "john.doe@example.com", 'PHP_AUTH_PW' => "superman"], []],
|
||||
[['REMOTE_USER' => "john.doe@example.com"], ['authenticated' => true, 'authenticatedUser' => "john.doe@example.com"]],
|
||||
[['REMOTE_USER' => "someone.else@example.com"], ['authenticated' => true, 'authenticatedUser' => "someone.else@example.com"]],
|
||||
[['REMOTE_USER' => "jane.doe@example.com"], []],
|
||||
];
|
||||
}
|
||||
|
||||
public function testSendAuthenticationChallenges() {
|
||||
$this->setConf();
|
||||
$r = new REST();
|
||||
$in = new EmptyResponse(401);
|
||||
$exp = $in->withHeader("WWW-Authenticate", 'Basic realm="OOK"');
|
||||
$act = $r->challenge($in, "OOK");
|
||||
$this->assertMessage($exp, $act);
|
||||
$exp = $in->withHeader("WWW-Authenticate", 'Basic realm="'.Arsse::$conf->httpRealm.'"');
|
||||
$act = $r->challenge($in);
|
||||
$this->assertMessage($exp, $act);
|
||||
}
|
||||
|
||||
/** @dataProvider provideUnnormalizedOrigins */
|
||||
public function testNormalizeOrigins(string $origin, string $exp, array $ports = null) {
|
||||
$r = new REST();
|
||||
$act = $r->corsNormalizeOrigin($origin, $ports);
|
||||
$this->assertSame($exp, $act);
|
||||
}
|
||||
|
||||
public function provideUnnormalizedOrigins() {
|
||||
return [
|
||||
["null", "null"],
|
||||
["http://example.com", "http://example.com"],
|
||||
["http://example.com:80", "http://example.com"],
|
||||
["http://example.com:8%30", "http://example.com"],
|
||||
["http://example.com:8080", "http://example.com:8080"],
|
||||
["http://[2001:0db8:0:0:0:0:2:1]", "http://[2001:db8::2:1]"],
|
||||
["http://example", "http://example"],
|
||||
["http://ex%41mple", "http://example"],
|
||||
["http://ex%41mple.co.uk", "http://example.co.uk"],
|
||||
["http://ex%41mple.co%2euk", "http://example.co%2Euk"],
|
||||
["http://example/", ""],
|
||||
["http://example?", ""],
|
||||
["http://example#", ""],
|
||||
["http://user@example", ""],
|
||||
["http://user:pass@example", ""],
|
||||
["http://[example", ""],
|
||||
["http://[2bef]", ""],
|
||||
["http://example%2F", "http://example%2F"],
|
||||
["HTTP://example", "http://example"],
|
||||
["HTTP://EXAMPLE", "http://example"],
|
||||
["%48%54%54%50://example", "http://example"],
|
||||
["http:%2F%2Fexample", ""],
|
||||
["https://example", "https://example"],
|
||||
["https://example:443", "https://example"],
|
||||
["https://example:80", "https://example:80"],
|
||||
["ssh://example", "ssh://example"],
|
||||
["ssh://example:22", "ssh://example:22"],
|
||||
["ssh://example:22", "ssh://example", ['ssh' => 22]],
|
||||
["SSH://example:22", "ssh://example", ['ssh' => 22]],
|
||||
["ssh://example:22", "ssh://example", ['ssh' => "22"]],
|
||||
["ssh://example:22", "ssh://example:22", ['SSH' => "22"]],
|
||||
];
|
||||
}
|
||||
|
||||
/** @dataProvider provideCorsNegotiations */
|
||||
public function testNegotiateCors($origin, bool $exp, string $allowed = null, string $denied = null) {
|
||||
$this->setConf();
|
||||
$r = Phake::partialMock(REST::class);
|
||||
Phake::when($r)->corsNormalizeOrigin->thenReturnCallback(function ($origin) {
|
||||
return $origin;
|
||||
});
|
||||
$req = new Request("", "GET", "php://memory", ['Origin' => $origin]);
|
||||
$act = $r->corsNegotiate($req, $allowed, $denied);
|
||||
$this->assertSame($exp, $act);
|
||||
}
|
||||
|
||||
public function provideCorsNegotiations() {
|
||||
return [
|
||||
["http://example", true ],
|
||||
["http://example", true, "http://example", "*" ],
|
||||
["http://example", false, "http://example", "http://example"],
|
||||
["http://example", false, "https://example", "*" ],
|
||||
["http://example", false, "*", "*" ],
|
||||
["http://example", true, "*", "" ],
|
||||
["http://example", false, "", "" ],
|
||||
["null", false ],
|
||||
["null", true, "null", "*" ],
|
||||
["null", false, "null", "null" ],
|
||||
["null", false, "*", "*" ],
|
||||
["null", false, "*", "" ],
|
||||
["null", false, "", "" ],
|
||||
["", false ],
|
||||
["", false, "", "*" ],
|
||||
["", false, "", "" ],
|
||||
["", false, "*", "*" ],
|
||||
["", false, "*", "" ],
|
||||
[["null", "http://example"], false, "*", "" ],
|
||||
[[], false, "*", "" ],
|
||||
];
|
||||
}
|
||||
|
||||
/** @dataProvider provideCorsHeaders */
|
||||
public function testAddCorsHeaders(string $reqMethod, array $reqHeaders, array $resHeaders, array $expHeaders) {
|
||||
$r = new REST();
|
||||
$req = new Request("", $reqMethod, "php://memory", $reqHeaders);
|
||||
$res = new EmptyResponse(204, $resHeaders);
|
||||
$exp = new EmptyResponse(204, $expHeaders);
|
||||
$act = $r->corsApply($res, $req);
|
||||
$this->assertMessage($exp, $act);
|
||||
}
|
||||
|
||||
public function provideCorsHeaders() {
|
||||
return [
|
||||
["GET", ['Origin' => "null"], [], [
|
||||
'Access-Control-Allow-Origin' => "null",
|
||||
'Access-Control-Allow-Credentials' => "true",
|
||||
'Vary' => "Origin",
|
||||
]],
|
||||
["GET", ['Origin' => "http://example"], [], [
|
||||
'Access-Control-Allow-Origin' => "http://example",
|
||||
'Access-Control-Allow-Credentials' => "true",
|
||||
'Vary' => "Origin",
|
||||
]],
|
||||
["GET", ['Origin' => "http://example"], ['Content-Type' => "text/plain; charset=utf-8"], [
|
||||
'Access-Control-Allow-Origin' => "http://example",
|
||||
'Access-Control-Allow-Credentials' => "true",
|
||||
'Vary' => "Origin",
|
||||
'Content-Type' => "text/plain; charset=utf-8",
|
||||
]],
|
||||
["GET", ['Origin' => "http://example"], ['Vary' => "Content-Type"], [
|
||||
'Access-Control-Allow-Origin' => "http://example",
|
||||
'Access-Control-Allow-Credentials' => "true",
|
||||
'Vary' => ["Content-Type", "Origin"],
|
||||
]],
|
||||
["OPTIONS", ['Origin' => "http://example"], [], [
|
||||
'Access-Control-Allow-Origin' => "http://example",
|
||||
'Access-Control-Allow-Credentials' => "true",
|
||||
'Access-Control-Max-Age' => (string) (60 *60 *24),
|
||||
'Vary' => "Origin",
|
||||
]],
|
||||
["OPTIONS", ['Origin' => "http://example"], ['Allow' => "GET, PUT, HEAD, OPTIONS"], [
|
||||
'Allow' => "GET, PUT, HEAD, OPTIONS",
|
||||
'Access-Control-Allow-Origin' => "http://example",
|
||||
'Access-Control-Allow-Credentials' => "true",
|
||||
'Access-Control-Allow-Methods' => "GET, PUT, HEAD, OPTIONS",
|
||||
'Access-Control-Max-Age' => (string) (60 *60 *24),
|
||||
'Vary' => "Origin",
|
||||
]],
|
||||
["OPTIONS", ['Origin' => "http://example", 'Access-Control-Request-Headers' => "Content-Type, If-None-Match"], [], [
|
||||
'Access-Control-Allow-Origin' => "http://example",
|
||||
'Access-Control-Allow-Credentials' => "true",
|
||||
'Access-Control-Allow-Headers' => "Content-Type, If-None-Match",
|
||||
'Access-Control-Max-Age' => (string) (60 *60 *24),
|
||||
'Vary' => "Origin",
|
||||
]],
|
||||
["OPTIONS", ['Origin' => "http://example", 'Access-Control-Request-Headers' => ["Content-Type", "If-None-Match"]], [], [
|
||||
'Access-Control-Allow-Origin' => "http://example",
|
||||
'Access-Control-Allow-Credentials' => "true",
|
||||
'Access-Control-Allow-Headers' => "Content-Type,If-None-Match",
|
||||
'Access-Control-Max-Age' => (string) (60 *60 *24),
|
||||
'Vary' => "Origin",
|
||||
]],
|
||||
];
|
||||
}
|
||||
|
||||
/** @dataProvider provideUnnormalizedResponses */
|
||||
public function testNormalizeHttpResponses(ResponseInterface $res, ResponseInterface $exp, RequestInterface $req = null) {
|
||||
$r = Phake::partialMock(REST::class);
|
||||
Phake::when($r)->corsNegotiate->thenReturn(true);
|
||||
Phake::when($r)->challenge->thenReturnCallback(function ($res) {
|
||||
return $res->withHeader("WWW-Authenticate", "Fake Value");
|
||||
});
|
||||
Phake::when($r)->corsApply->thenReturnCallback(function ($res) {
|
||||
return $res;
|
||||
});
|
||||
$act = $r->normalizeResponse($res, $req);
|
||||
$this->assertMessage($exp, $act);
|
||||
}
|
||||
|
||||
public function provideUnnormalizedResponses() {
|
||||
$stream = fopen("php://memory", "w+b");
|
||||
fwrite($stream,"ook");
|
||||
return [
|
||||
[new EmptyResponse(204), new EmptyResponse(204)],
|
||||
[new EmptyResponse(401), new EmptyResponse(401, ['WWW-Authenticate' => "Fake Value"])],
|
||||
[new EmptyResponse(204, ['Allow' => "PUT"]), new EmptyResponse(204, ['Allow' => "PUT, OPTIONS"])],
|
||||
[new EmptyResponse(204, ['Allow' => "PUT, OPTIONS"]), new EmptyResponse(204, ['Allow' => "PUT, OPTIONS"])],
|
||||
[new EmptyResponse(204, ['Allow' => "PUT,OPTIONS"]), new EmptyResponse(204, ['Allow' => "PUT, OPTIONS"])],
|
||||
[new EmptyResponse(204, ['Allow' => ["PUT", "OPTIONS"]]), new EmptyResponse(204, ['Allow' => "PUT, OPTIONS"])],
|
||||
[new EmptyResponse(204, ['Allow' => ["PUT, DELETE", "OPTIONS"]]), new EmptyResponse(204, ['Allow' => "PUT, DELETE, OPTIONS"])],
|
||||
[new EmptyResponse(204, ['Allow' => "HEAD,GET"]), new EmptyResponse(204, ['Allow' => "HEAD, GET, OPTIONS"])],
|
||||
[new EmptyResponse(204, ['Allow' => "GET"]), new EmptyResponse(204, ['Allow' => "GET, HEAD, OPTIONS"])],
|
||||
[new TextResponse("ook", 200), new TextResponse("ook", 200, ['Content-Length' => "3"])],
|
||||
[new TextResponse("", 200), new TextResponse("", 200, ['Content-Length' => "0"])],
|
||||
[new TextResponse("ook", 404), new TextResponse("ook", 404, ['Content-Length' => "3"])],
|
||||
[new TextResponse("", 404), new TextResponse("", 404)],
|
||||
[new Response($stream, 200), new Response($stream, 200, ['Content-Length' => "3"]), new Request("", "GET")],
|
||||
[new Response($stream, 200), new EmptyResponse(200, ['Content-Length' => "3"]), new Request("", "HEAD")],
|
||||
];
|
||||
}
|
||||
|
||||
public function testCreateHandlers() {
|
||||
$r = new REST();
|
||||
foreach (REST::API_LIST as $api) {
|
||||
$class = $api['class'];
|
||||
$this->assertInstanceOf(Handler::class, $r->getHandler($class));
|
||||
}
|
||||
}
|
||||
|
||||
/** @dataProvider provideMockRequests */
|
||||
public function testDispatchRequests(ServerRequest $req, string $method, bool $called, string $class = "", string $target ="") {
|
||||
$r = Phake::partialMock(REST::class);
|
||||
Phake::when($r)->normalizeResponse->thenReturnCallback(function ($res) {
|
||||
return $res;
|
||||
});
|
||||
Phake::when($r)->authenticateRequest->thenReturnCallback(function ($req) {
|
||||
return $req;
|
||||
});
|
||||
if ($called) {
|
||||
$h = Phake::mock($class);
|
||||
Phake::when($r)->getHandler($class)->thenReturn($h);
|
||||
Phake::when($h)->dispatch->thenReturn(new EmptyResponse(204));
|
||||
}
|
||||
$out = $r->dispatch($req);
|
||||
$this->assertInstanceOf(ResponseInterface::class, $out);
|
||||
if ($called) {
|
||||
Phake::verify($r)->authenticateRequest;
|
||||
Phake::verify($h)->dispatch(Phake::capture($in));
|
||||
$this->assertSame($method, $in->getMethod());
|
||||
$this->assertSame($target, $in->getRequestTarget());
|
||||
} else {
|
||||
$this->assertSame(501, $out->getStatusCode());
|
||||
}
|
||||
Phake::verify($r)->apiMatch;
|
||||
Phake::verify($r)->normalizeResponse;
|
||||
}
|
||||
|
||||
public function provideMockRequests() {
|
||||
return [
|
||||
[new ServerRequest([], [], "/index.php/apps/news/api/v1-2/feeds", "GET"), "GET", true, NCN::Class, "/feeds"],
|
||||
[new ServerRequest([], [], "/index.php/apps/news/api/v1-2/feeds", "HEAD"), "GET", true, NCN::Class, "/feeds"],
|
||||
[new ServerRequest([], [], "/index.php/apps/news/api/v1-2/feeds", "get"), "GET", true, NCN::Class, "/feeds"],
|
||||
[new ServerRequest([], [], "/index.php/apps/news/api/v1-2/feeds", "head"), "GET", true, NCN::Class, "/feeds"],
|
||||
[new ServerRequest([], [], "/tt-rss/api/", "POST"), "POST", true, TTRSS::Class, "/"],
|
||||
[new ServerRequest([], [], "/no/such/api/", "HEAD"), "GET", false],
|
||||
[new ServerRequest([], [], "/no/such/api/", "GET"), "GET", false],
|
||||
];
|
||||
}
|
||||
}
|
66
tests/cases/REST/TestTarget.php
Normal file
66
tests/cases/REST/TestTarget.php
Normal file
|
@ -0,0 +1,66 @@
|
|||
<?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\TestCase\REST;
|
||||
|
||||
use JKingWeb\Arsse\REST\Target;
|
||||
|
||||
/** @covers \JKingWeb\Arsse\REST\Target */
|
||||
class TestTarget extends \JKingWeb\Arsse\Test\AbstractTest {
|
||||
|
||||
/** @dataProvider provideTargetUrls */
|
||||
public function testParseTargetUrl(string $target, array $path, bool $relative, bool $index, string $query, string $fragment, string $normalized) {
|
||||
$test = new Target($target);
|
||||
$this->assertEquals($path, $test->path, "Path does not match");
|
||||
$this->assertSame($path, $test->path, "Path does not match exactly");
|
||||
$this->assertSame($relative, $test->relative, "Relative flag does not match");
|
||||
$this->assertSame($index, $test->index, "Index flag does not match");
|
||||
$this->assertSame($query, $test->query, "Query does not match");
|
||||
$this->assertSame($fragment, $test->fragment, "Fragment does not match");
|
||||
}
|
||||
|
||||
/** @dataProvider provideTargetUrls */
|
||||
public function testNormalizeTargetUrl(string $target, array $path, bool $relative, bool $index, string $query, string $fragment, string $normalized) {
|
||||
$test = new Target("");
|
||||
$test->path = $path;
|
||||
$test->relative = $relative;
|
||||
$test->index = $index;
|
||||
$test->query = $query;
|
||||
$test->fragment = $fragment;
|
||||
$this->assertSame($normalized, (string) $test);
|
||||
$this->assertSame($normalized, Target::normalize($target));
|
||||
}
|
||||
|
||||
public function provideTargetUrls() {
|
||||
return [
|
||||
["/", [], false, true, "", "", "/"],
|
||||
["", [], true, true, "", "", ""],
|
||||
["/index.php", ["index.php"], false, false, "", "", "/index.php"],
|
||||
["index.php", ["index.php"], true, false, "", "", "index.php"],
|
||||
["/ook/", ["ook"], false, true, "", "", "/ook/"],
|
||||
["ook/", ["ook"], true, true, "", "", "ook/"],
|
||||
["/eek/../ook/", ["ook"], false, true, "", "", "/ook/"],
|
||||
["eek/../ook/", ["ook"], true, true, "", "", "ook/"],
|
||||
["/./ook/", ["ook"], false, true, "", "", "/ook/"],
|
||||
["./ook/", ["ook"], true, true, "", "", "ook/"],
|
||||
["/../ook/", [null,"ook"], false, true, "", "", "/../ook/"],
|
||||
["../ook/", [null,"ook"], true, true, "", "", "../ook/"],
|
||||
["0", ["0"], true, false, "", "", "0"],
|
||||
["%6f%6F%6b", ["ook"], true, false, "", "", "ook"],
|
||||
["%2e%2E%2f%2E%2Fook%2f", [".././ook/"], true, false, "", "", "..%2F.%2Fook%2F"],
|
||||
["%2e%2E/%2E/ook%2f", ["..",".","ook/"], true, false, "", "", "%2E%2E/%2E/ook%2F"],
|
||||
["...", ["..."], true, false, "", "", "..."],
|
||||
["%2e%2e%2e", ["..."], true, false, "", "", "..."],
|
||||
["/?", [], false, true, "", "", "/"],
|
||||
["/#", [], false, true, "", "", "/"],
|
||||
["/?#", [], false, true, "", "", "/"],
|
||||
["#%2e", [], true, true, "", ".", "#."],
|
||||
["?%2e", [], true, true, "%2e", "", "?%2e"],
|
||||
["?%2e#%2f", [], true, true, "%2e", "/", "?%2e#%2F"],
|
||||
["#%2e?%2f", [], true, true, "", ".?/", "#.%3F%2F"],
|
||||
];
|
||||
}
|
||||
}
|
|
@ -12,13 +12,16 @@ use JKingWeb\Arsse\User;
|
|||
use JKingWeb\Arsse\Database;
|
||||
use JKingWeb\Arsse\Service;
|
||||
use JKingWeb\Arsse\REST\Request;
|
||||
use JKingWeb\Arsse\REST\Response;
|
||||
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\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;
|
||||
|
||||
/** @covers \JKingWeb\Arsse\REST\TinyTinyRSS\API<extended>
|
||||
|
@ -126,12 +129,26 @@ LONG_STRING;
|
|||
return $value;
|
||||
}
|
||||
|
||||
protected function req($data) : Response {
|
||||
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 {
|
||||
return new Response(200, [
|
||||
return new Response([
|
||||
'seq' => $seq,
|
||||
'status' => 0,
|
||||
'content' => $content,
|
||||
|
@ -140,18 +157,13 @@ LONG_STRING;
|
|||
|
||||
protected function respErr(string $msg, $content = [], $seq = 0): Response {
|
||||
$err = ['error' => $msg];
|
||||
return new Response(200, [
|
||||
return new Response([
|
||||
'seq' => $seq,
|
||||
'status' => 1,
|
||||
'content' => array_merge($err, $content, $err),
|
||||
]);
|
||||
}
|
||||
|
||||
protected function assertResponse(Response $exp, Response $act, string $text = null) {
|
||||
$this->assertEquals($exp, $act, $text);
|
||||
$this->assertSame($exp->payload, $act->payload, $text);
|
||||
}
|
||||
|
||||
public function setUp() {
|
||||
$this->clearData();
|
||||
Arsse::$conf = new Conf();
|
||||
|
@ -179,25 +191,25 @@ 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", "")));
|
||||
$exp = new Response(404);
|
||||
$this->assertResponse($exp, $this->h->dispatch(new Request("POST", "/bad/path", "")));
|
||||
$this->assertMessage($exp, $this->req(null, "POST", "", ""));
|
||||
$this->assertMessage($exp, $this->req(null, "POST", "/", ""));
|
||||
$this->assertMessage($exp, $this->req(null, "POST", "/index.php", ""));
|
||||
$exp = new EmptyResponse(404);
|
||||
$this->assertMessage($exp, $this->req(null, "POST", "/bad/path", ""));
|
||||
}
|
||||
|
||||
public function testHandleOptionsRequest() {
|
||||
$exp = new Response(204, "", "", [
|
||||
"Allow: POST",
|
||||
"Accept: application/json, text/json",
|
||||
$exp = new EmptyResponse(204, [
|
||||
'Allow' => "POST",
|
||||
'Accept' => "application/json, text/json",
|
||||
]);
|
||||
$this->assertResponse($exp, $this->h->dispatch(new Request("OPTIONS", "")));
|
||||
$this->assertMessage($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->assertMessage($exp, $this->req(null, "POST", "", "This is not valid JSON data"));
|
||||
$this->assertMessage($exp, $this->req(null, "POST", "", "")); // lack of data is also an error
|
||||
}
|
||||
|
||||
public function testLogIn() {
|
||||
|
@ -210,15 +222,15 @@ LONG_STRING;
|
|||
'password' => "secret",
|
||||
];
|
||||
$exp = $this->respGood(['session_id' => "PriestsOfSyrinx", 'api_level' => \JKingWeb\Arsse\REST\TinyTinyRSS\API::LEVEL]);
|
||||
$this->assertResponse($exp, $this->req($data));
|
||||
$this->assertMessage($exp, $this->req($data));
|
||||
// base64 passwords are also accepted
|
||||
$data['password'] = base64_encode($data['password']);
|
||||
$exp = $this->respGood(['session_id' => "SolarFederation", 'api_level' => \JKingWeb\Arsse\REST\TinyTinyRSS\API::LEVEL]);
|
||||
$this->assertResponse($exp, $this->req($data));
|
||||
$this->assertMessage($exp, $this->req($data));
|
||||
// test a failed log-in
|
||||
$data['password'] = "superman";
|
||||
$exp = $this->respErr("LOGIN_ERROR");
|
||||
$this->assertResponse($exp, $this->req($data));
|
||||
$this->assertMessage($exp, $this->req($data));
|
||||
// logging in should never try to resume a session
|
||||
Phake::verify(Arsse::$db, Phake::times(0))->sessionResume($this->anything());
|
||||
}
|
||||
|
@ -230,8 +242,8 @@ LONG_STRING;
|
|||
'user' => Arsse::$user->id,
|
||||
'password' => "secret",
|
||||
];
|
||||
$exp = new Response(500);
|
||||
$this->assertResponse($exp, $this->req($data));
|
||||
$exp = new EmptyResponse(500);
|
||||
$this->assertMessage($exp, $this->req($data));
|
||||
}
|
||||
|
||||
public function testLogOut() {
|
||||
|
@ -241,7 +253,7 @@ LONG_STRING;
|
|||
'sid' => "PriestsOfSyrinx",
|
||||
];
|
||||
$exp = $this->respGood(['status' => "OK"]);
|
||||
$this->assertResponse($exp, $this->req($data));
|
||||
$this->assertMessage($exp, $this->req($data));
|
||||
Phake::verify(Arsse::$db)->sessionDestroy(Arsse::$user->id, "PriestsOfSyrinx");
|
||||
}
|
||||
|
||||
|
@ -251,10 +263,10 @@ LONG_STRING;
|
|||
'sid' => "PriestsOfSyrinx",
|
||||
];
|
||||
$exp = $this->respGood(['status' => true]);
|
||||
$this->assertResponse($exp, $this->req($data));
|
||||
$this->assertMessage($exp, $this->req($data));
|
||||
$data['sid'] = "SolarFederation";
|
||||
$exp = $this->respErr("NOT_LOGGED_IN");
|
||||
$this->assertResponse($exp, $this->req($data));
|
||||
$this->assertMessage($exp, $this->req($data));
|
||||
}
|
||||
|
||||
public function testHandleUnknownMethods() {
|
||||
|
@ -263,7 +275,7 @@ LONG_STRING;
|
|||
'op' => "thisMethodDoesNotExist",
|
||||
'sid' => "PriestsOfSyrinx",
|
||||
];
|
||||
$this->assertResponse($exp, $this->req($data));
|
||||
$this->assertMessage($exp, $this->req($data));
|
||||
}
|
||||
|
||||
public function testHandleMixedCaseMethods() {
|
||||
|
@ -272,13 +284,13 @@ LONG_STRING;
|
|||
'sid' => "PriestsOfSyrinx",
|
||||
];
|
||||
$exp = $this->respGood(['status' => true]);
|
||||
$this->assertResponse($exp, $this->req($data));
|
||||
$this->assertMessage($exp, $this->req($data));
|
||||
$data['op'] = "isloggedin";
|
||||
$this->assertResponse($exp, $this->req($data));
|
||||
$this->assertMessage($exp, $this->req($data));
|
||||
$data['op'] = "ISLOGGEDIN";
|
||||
$this->assertResponse($exp, $this->req($data));
|
||||
$this->assertMessage($exp, $this->req($data));
|
||||
$data['op'] = "iSlOgGeDiN";
|
||||
$this->assertResponse($exp, $this->req($data));
|
||||
$this->assertMessage($exp, $this->req($data));
|
||||
}
|
||||
|
||||
public function testRetrieveServerVersion() {
|
||||
|
@ -290,7 +302,7 @@ LONG_STRING;
|
|||
'version' => \JKingWeb\Arsse\REST\TinyTinyRSS\API::VERSION,
|
||||
'arsse_version' => Arsse::VERSION,
|
||||
]);
|
||||
$this->assertResponse($exp, $this->req($data));
|
||||
$this->assertMessage($exp, $this->req($data));
|
||||
}
|
||||
|
||||
public function testRetrieveProtocolLevel() {
|
||||
|
@ -299,7 +311,7 @@ LONG_STRING;
|
|||
'sid' => "PriestsOfSyrinx",
|
||||
];
|
||||
$exp = $this->respGood(['level' => \JKingWeb\Arsse\REST\TinyTinyRSS\API::LEVEL]);
|
||||
$this->assertResponse($exp, $this->req($data));
|
||||
$this->assertMessage($exp, $this->req($data));
|
||||
}
|
||||
|
||||
public function testAddACategory() {
|
||||
|
@ -333,24 +345,24 @@ LONG_STRING;
|
|||
Phake::when(Arsse::$db)->folderAdd(Arsse::$user->id, ['name' => " ", 'parent' => null])->thenThrow(new ExceptionInput("whitespace"));
|
||||
// correctly add two folders
|
||||
$exp = $this->respGood("2");
|
||||
$this->assertResponse($exp, $this->req($in[0]));
|
||||
$this->assertMessage($exp, $this->req($in[0]));
|
||||
$exp = $this->respGood("3");
|
||||
$this->assertResponse($exp, $this->req($in[1]));
|
||||
$this->assertMessage($exp, $this->req($in[1]));
|
||||
// attempt to add the two folders again
|
||||
$exp = $this->respGood("2");
|
||||
$this->assertResponse($exp, $this->req($in[0]));
|
||||
$this->assertMessage($exp, $this->req($in[0]));
|
||||
$exp = $this->respGood("3");
|
||||
$this->assertResponse($exp, $this->req($in[1]));
|
||||
$this->assertMessage($exp, $this->req($in[1]));
|
||||
Phake::verify(Arsse::$db)->folderList(Arsse::$user->id, null, false);
|
||||
Phake::verify(Arsse::$db)->folderList(Arsse::$user->id, 1, false);
|
||||
// add a folder to a missing parent (silently fails)
|
||||
$exp = $this->respGood(false);
|
||||
$this->assertResponse($exp, $this->req($in[2]));
|
||||
$this->assertMessage($exp, $this->req($in[2]));
|
||||
// add some invalid folders
|
||||
$exp = $this->respErr("INCORRECT_USAGE");
|
||||
$this->assertResponse($exp, $this->req($in[3]));
|
||||
$this->assertResponse($exp, $this->req($in[4]));
|
||||
$this->assertResponse($exp, $this->req($in[5]));
|
||||
$this->assertMessage($exp, $this->req($in[3]));
|
||||
$this->assertMessage($exp, $this->req($in[4]));
|
||||
$this->assertMessage($exp, $this->req($in[5]));
|
||||
}
|
||||
|
||||
public function testRemoveACategory() {
|
||||
|
@ -363,16 +375,16 @@ LONG_STRING;
|
|||
Phake::when(Arsse::$db)->folderRemove(Arsse::$user->id, 42)->thenReturn(true)->thenThrow(new ExceptionInput("subjectMissing"));
|
||||
// succefully delete a folder
|
||||
$exp = $this->respGood();
|
||||
$this->assertResponse($exp, $this->req($in[0]));
|
||||
$this->assertMessage($exp, $this->req($in[0]));
|
||||
// try deleting it again (this should silently fail)
|
||||
$exp = $this->respGood();
|
||||
$this->assertResponse($exp, $this->req($in[0]));
|
||||
$this->assertMessage($exp, $this->req($in[0]));
|
||||
// delete a folder which does not exist (this should also silently fail)
|
||||
$exp = $this->respGood();
|
||||
$this->assertResponse($exp, $this->req($in[1]));
|
||||
$this->assertMessage($exp, $this->req($in[1]));
|
||||
// delete an invalid folder (causes an error)
|
||||
$exp = $this->respErr("INCORRECT_USAGE");
|
||||
$this->assertResponse($exp, $this->req($in[2]));
|
||||
$this->assertMessage($exp, $this->req($in[2]));
|
||||
Phake::verify(Arsse::$db, Phake::times(3))->folderRemove(Arsse::$user->id, $this->anything());
|
||||
}
|
||||
|
||||
|
@ -410,21 +422,21 @@ LONG_STRING;
|
|||
Phake::when(Arsse::$db)->folderPropertiesSet(...$db[8])->thenThrow(new ExceptionInput("typeViolation"));
|
||||
// succefully move a folder
|
||||
$exp = $this->respGood();
|
||||
$this->assertResponse($exp, $this->req($in[0]));
|
||||
$this->assertMessage($exp, $this->req($in[0]));
|
||||
// move a folder which does not exist (this should silently fail)
|
||||
$exp = $this->respGood();
|
||||
$this->assertResponse($exp, $this->req($in[1]));
|
||||
$this->assertMessage($exp, $this->req($in[1]));
|
||||
// move a folder causing a duplication (this should also silently fail)
|
||||
$exp = $this->respGood();
|
||||
$this->assertResponse($exp, $this->req($in[2]));
|
||||
$this->assertResponse($exp, $this->req($in[3]));
|
||||
$this->assertResponse($exp, $this->req($in[6]));
|
||||
$this->assertMessage($exp, $this->req($in[2]));
|
||||
$this->assertMessage($exp, $this->req($in[3]));
|
||||
$this->assertMessage($exp, $this->req($in[6]));
|
||||
// all the rest should cause errors
|
||||
$exp = $this->respErr("INCORRECT_USAGE");
|
||||
$this->assertResponse($exp, $this->req($in[4]));
|
||||
$this->assertResponse($exp, $this->req($in[5]));
|
||||
$this->assertResponse($exp, $this->req($in[7]));
|
||||
$this->assertResponse($exp, $this->req($in[8]));
|
||||
$this->assertMessage($exp, $this->req($in[4]));
|
||||
$this->assertMessage($exp, $this->req($in[5]));
|
||||
$this->assertMessage($exp, $this->req($in[7]));
|
||||
$this->assertMessage($exp, $this->req($in[8]));
|
||||
Phake::verify(Arsse::$db, Phake::times(5))->folderPropertiesSet(Arsse::$user->id, $this->anything(), $this->anything());
|
||||
}
|
||||
|
||||
|
@ -450,21 +462,21 @@ LONG_STRING;
|
|||
Phake::when(Arsse::$db)->folderPropertiesSet(...$db[2])->thenThrow(new ExceptionInput("constraintViolation"));
|
||||
// succefully rename a folder
|
||||
$exp = $this->respGood();
|
||||
$this->assertResponse($exp, $this->req($in[0]));
|
||||
$this->assertMessage($exp, $this->req($in[0]));
|
||||
// rename a folder which does not exist (this should silently fail)
|
||||
$exp = $this->respGood();
|
||||
$this->assertResponse($exp, $this->req($in[1]));
|
||||
$this->assertMessage($exp, $this->req($in[1]));
|
||||
// rename a folder causing a duplication (this should also silently fail)
|
||||
$exp = $this->respGood();
|
||||
$this->assertResponse($exp, $this->req($in[2]));
|
||||
$this->assertMessage($exp, $this->req($in[2]));
|
||||
// all the rest should cause errors
|
||||
$exp = $this->respErr("INCORRECT_USAGE");
|
||||
$this->assertResponse($exp, $this->req($in[3]));
|
||||
$this->assertResponse($exp, $this->req($in[4]));
|
||||
$this->assertResponse($exp, $this->req($in[5]));
|
||||
$this->assertResponse($exp, $this->req($in[6]));
|
||||
$this->assertResponse($exp, $this->req($in[7]));
|
||||
$this->assertResponse($exp, $this->req($in[8]));
|
||||
$this->assertMessage($exp, $this->req($in[3]));
|
||||
$this->assertMessage($exp, $this->req($in[4]));
|
||||
$this->assertMessage($exp, $this->req($in[5]));
|
||||
$this->assertMessage($exp, $this->req($in[6]));
|
||||
$this->assertMessage($exp, $this->req($in[7]));
|
||||
$this->assertMessage($exp, $this->req($in[8]));
|
||||
Phake::verify(Arsse::$db, Phake::times(3))->folderPropertiesSet(Arsse::$user->id, $this->anything(), $this->anything());
|
||||
}
|
||||
|
||||
|
@ -534,11 +546,11 @@ LONG_STRING;
|
|||
Phake::when(Arsse::$db)->subscriptionList(Arsse::$user->id)->thenReturn(new Result($this->v($list)));
|
||||
for ($a = 0; $a < (sizeof($in) - 4); $a++) {
|
||||
$exp = $this->respGood($out[$a]);
|
||||
$this->assertResponse($exp, $this->req($in[$a]), "Failed test $a");
|
||||
$this->assertMessage($exp, $this->req($in[$a]), "Failed test $a");
|
||||
}
|
||||
$exp = $this->respErr("INCORRECT_USAGE");
|
||||
for ($a = (sizeof($in) - 4); $a < sizeof($in); $a++) {
|
||||
$this->assertResponse($exp, $this->req($in[$a]), "Failed test $a");
|
||||
$this->assertMessage($exp, $this->req($in[$a]), "Failed test $a");
|
||||
}
|
||||
Phake::verify(Arsse::$db, Phake::times(0))->subscriptionPropertiesSet(Arsse::$user->id, 4, ['folder' => 1]);
|
||||
}
|
||||
|
@ -555,13 +567,13 @@ LONG_STRING;
|
|||
Phake::when(Arsse::$db)->subscriptionRemove(Arsse::$user->id, 42)->thenReturn(true)->thenThrow(new ExceptionInput("subjectMissing"));
|
||||
// succefully delete a folder
|
||||
$exp = $this->respGood(['status' => "OK"]);
|
||||
$this->assertResponse($exp, $this->req($in[0]));
|
||||
$this->assertMessage($exp, $this->req($in[0]));
|
||||
// try deleting it again (this should noisily fail, as should everything else)
|
||||
$exp = $this->respErr("FEED_NOT_FOUND");
|
||||
$this->assertResponse($exp, $this->req($in[0]));
|
||||
$this->assertResponse($exp, $this->req($in[1]));
|
||||
$this->assertResponse($exp, $this->req($in[2]));
|
||||
$this->assertResponse($exp, $this->req($in[3]));
|
||||
$this->assertMessage($exp, $this->req($in[0]));
|
||||
$this->assertMessage($exp, $this->req($in[1]));
|
||||
$this->assertMessage($exp, $this->req($in[2]));
|
||||
$this->assertMessage($exp, $this->req($in[3]));
|
||||
Phake::verify(Arsse::$db, Phake::times(5))->subscriptionRemove(Arsse::$user->id, $this->anything());
|
||||
}
|
||||
|
||||
|
@ -589,21 +601,21 @@ LONG_STRING;
|
|||
Phake::when(Arsse::$db)->subscriptionPropertiesSet(...$db[3])->thenThrow(new ExceptionInput("constraintViolation"));
|
||||
// succefully move a subscription
|
||||
$exp = $this->respGood();
|
||||
$this->assertResponse($exp, $this->req($in[0]));
|
||||
$this->assertMessage($exp, $this->req($in[0]));
|
||||
// move a subscription which does not exist (this should silently fail)
|
||||
$exp = $this->respGood();
|
||||
$this->assertResponse($exp, $this->req($in[1]));
|
||||
$this->assertMessage($exp, $this->req($in[1]));
|
||||
// move a subscription causing a duplication (this should also silently fail)
|
||||
$exp = $this->respGood();
|
||||
$this->assertResponse($exp, $this->req($in[2]));
|
||||
$this->assertResponse($exp, $this->req($in[3]));
|
||||
$this->assertMessage($exp, $this->req($in[2]));
|
||||
$this->assertMessage($exp, $this->req($in[3]));
|
||||
// all the rest should cause errors
|
||||
$exp = $this->respErr("INCORRECT_USAGE");
|
||||
$this->assertResponse($exp, $this->req($in[4]));
|
||||
$this->assertResponse($exp, $this->req($in[5]));
|
||||
$this->assertResponse($exp, $this->req($in[6]));
|
||||
$this->assertResponse($exp, $this->req($in[7]));
|
||||
$this->assertResponse($exp, $this->req($in[8]));
|
||||
$this->assertMessage($exp, $this->req($in[4]));
|
||||
$this->assertMessage($exp, $this->req($in[5]));
|
||||
$this->assertMessage($exp, $this->req($in[6]));
|
||||
$this->assertMessage($exp, $this->req($in[7]));
|
||||
$this->assertMessage($exp, $this->req($in[8]));
|
||||
Phake::verify(Arsse::$db, Phake::times(4))->subscriptionPropertiesSet(Arsse::$user->id, $this->anything(), $this->anything());
|
||||
}
|
||||
|
||||
|
@ -629,21 +641,21 @@ LONG_STRING;
|
|||
Phake::when(Arsse::$db)->subscriptionPropertiesSet(...$db[2])->thenThrow(new ExceptionInput("constraintViolation"));
|
||||
// succefully rename a subscription
|
||||
$exp = $this->respGood();
|
||||
$this->assertResponse($exp, $this->req($in[0]));
|
||||
$this->assertMessage($exp, $this->req($in[0]));
|
||||
// rename a subscription which does not exist (this should silently fail)
|
||||
$exp = $this->respGood();
|
||||
$this->assertResponse($exp, $this->req($in[1]));
|
||||
$this->assertMessage($exp, $this->req($in[1]));
|
||||
// rename a subscription causing a duplication (this should also silently fail)
|
||||
$exp = $this->respGood();
|
||||
$this->assertResponse($exp, $this->req($in[2]));
|
||||
$this->assertMessage($exp, $this->req($in[2]));
|
||||
// all the rest should cause errors
|
||||
$exp = $this->respErr("INCORRECT_USAGE");
|
||||
$this->assertResponse($exp, $this->req($in[3]));
|
||||
$this->assertResponse($exp, $this->req($in[4]));
|
||||
$this->assertResponse($exp, $this->req($in[5]));
|
||||
$this->assertResponse($exp, $this->req($in[6]));
|
||||
$this->assertResponse($exp, $this->req($in[7]));
|
||||
$this->assertResponse($exp, $this->req($in[8]));
|
||||
$this->assertMessage($exp, $this->req($in[3]));
|
||||
$this->assertMessage($exp, $this->req($in[4]));
|
||||
$this->assertMessage($exp, $this->req($in[5]));
|
||||
$this->assertMessage($exp, $this->req($in[6]));
|
||||
$this->assertMessage($exp, $this->req($in[7]));
|
||||
$this->assertMessage($exp, $this->req($in[8]));
|
||||
Phake::verify(Arsse::$db)->subscriptionPropertiesSet(...$db[0]);
|
||||
Phake::verify(Arsse::$db)->subscriptionPropertiesSet(...$db[1]);
|
||||
Phake::verify(Arsse::$db)->subscriptionPropertiesSet(...$db[2]);
|
||||
|
@ -657,7 +669,7 @@ LONG_STRING;
|
|||
['id' => 3, 'unread' => 47],
|
||||
])));
|
||||
$exp = $this->respGood(['unread' => (string) (2112 + 42 + 47)]);
|
||||
$this->assertResponse($exp, $this->req($in));
|
||||
$this->assertMessage($exp, $this->req($in));
|
||||
}
|
||||
|
||||
public function testRetrieveTheServerConfiguration() {
|
||||
|
@ -671,8 +683,8 @@ LONG_STRING;
|
|||
['icons_dir' => "feed-icons", 'icons_url' => "feed-icons", 'daemon_is_running' => true, 'num_feeds' => 12],
|
||||
['icons_dir' => "feed-icons", 'icons_url' => "feed-icons", 'daemon_is_running' => false, 'num_feeds' => 2],
|
||||
];
|
||||
$this->assertResponse($this->respGood($exp[0]), $this->req($in));
|
||||
$this->assertResponse($this->respGood($exp[1]), $this->req($in));
|
||||
$this->assertMessage($this->respGood($exp[0]), $this->req($in));
|
||||
$this->assertMessage($this->respGood($exp[1]), $this->req($in));
|
||||
}
|
||||
|
||||
public function testUpdateAFeed() {
|
||||
|
@ -686,13 +698,13 @@ LONG_STRING;
|
|||
Phake::when(Arsse::$db)->subscriptionPropertiesGet(Arsse::$user->id, 1)->thenReturn($this->v(['id' => 1, 'feed' => 11]));
|
||||
Phake::when(Arsse::$db)->subscriptionPropertiesGet(Arsse::$user->id, 2)->thenThrow(new ExceptionInput("subjectMissing"));
|
||||
$exp = $this->respGood(['status' => "OK"]);
|
||||
$this->assertResponse($exp, $this->req($in[0]));
|
||||
$this->assertMessage($exp, $this->req($in[0]));
|
||||
Phake::verify(Arsse::$db)->feedUpdate(11);
|
||||
$exp = $this->respErr("FEED_NOT_FOUND");
|
||||
$this->assertResponse($exp, $this->req($in[1]));
|
||||
$this->assertMessage($exp, $this->req($in[1]));
|
||||
$exp = $this->respErr("INCORRECT_USAGE");
|
||||
$this->assertResponse($exp, $this->req($in[2]));
|
||||
$this->assertResponse($exp, $this->req($in[3]));
|
||||
$this->assertMessage($exp, $this->req($in[2]));
|
||||
$this->assertMessage($exp, $this->req($in[3]));
|
||||
}
|
||||
|
||||
public function testAddALabel() {
|
||||
|
@ -723,21 +735,21 @@ LONG_STRING;
|
|||
Phake::when(Arsse::$db)->labelAdd(Arsse::$user->id, ['name' => " "])->thenThrow(new ExceptionInput("whitespace"));
|
||||
// correctly add two labels
|
||||
$exp = $this->respGood((-1 * API::LABEL_OFFSET) - 2);
|
||||
$this->assertResponse($exp, $this->req($in[0]));
|
||||
$this->assertMessage($exp, $this->req($in[0]));
|
||||
$exp = $this->respGood((-1 * API::LABEL_OFFSET) - 3);
|
||||
$this->assertResponse($exp, $this->req($in[1]));
|
||||
$this->assertMessage($exp, $this->req($in[1]));
|
||||
// attempt to add the two labels again
|
||||
$exp = $this->respGood((-1 * API::LABEL_OFFSET) - 2);
|
||||
$this->assertResponse($exp, $this->req($in[0]));
|
||||
$this->assertMessage($exp, $this->req($in[0]));
|
||||
$exp = $this->respGood((-1 * API::LABEL_OFFSET) - 3);
|
||||
$this->assertResponse($exp, $this->req($in[1]));
|
||||
$this->assertMessage($exp, $this->req($in[1]));
|
||||
Phake::verify(Arsse::$db)->labelPropertiesGet(Arsse::$user->id, "Software", true);
|
||||
Phake::verify(Arsse::$db)->labelPropertiesGet(Arsse::$user->id, "Hardware", true);
|
||||
// add some invalid labels
|
||||
$exp = $this->respErr("INCORRECT_USAGE");
|
||||
$this->assertResponse($exp, $this->req($in[2]));
|
||||
$this->assertResponse($exp, $this->req($in[3]));
|
||||
$this->assertResponse($exp, $this->req($in[4]));
|
||||
$this->assertMessage($exp, $this->req($in[2]));
|
||||
$this->assertMessage($exp, $this->req($in[3]));
|
||||
$this->assertMessage($exp, $this->req($in[4]));
|
||||
}
|
||||
|
||||
public function testRemoveALabel() {
|
||||
|
@ -752,18 +764,18 @@ LONG_STRING;
|
|||
Phake::when(Arsse::$db)->labelRemove(Arsse::$user->id, 18)->thenReturn(true)->thenThrow(new ExceptionInput("subjectMissing"));
|
||||
// succefully delete a label
|
||||
$exp = $this->respGood();
|
||||
$this->assertResponse($exp, $this->req($in[0]));
|
||||
$this->assertMessage($exp, $this->req($in[0]));
|
||||
// try deleting it again (this should silently fail)
|
||||
$exp = $this->respGood();
|
||||
$this->assertResponse($exp, $this->req($in[0]));
|
||||
$this->assertMessage($exp, $this->req($in[0]));
|
||||
// delete a label which does not exist (this should also silently fail)
|
||||
$exp = $this->respGood();
|
||||
$this->assertResponse($exp, $this->req($in[1]));
|
||||
$this->assertMessage($exp, $this->req($in[1]));
|
||||
// delete some invalid labels (causes an error)
|
||||
$exp = $this->respErr("INCORRECT_USAGE");
|
||||
$this->assertResponse($exp, $this->req($in[2]));
|
||||
$this->assertResponse($exp, $this->req($in[3]));
|
||||
$this->assertResponse($exp, $this->req($in[4]));
|
||||
$this->assertMessage($exp, $this->req($in[2]));
|
||||
$this->assertMessage($exp, $this->req($in[3]));
|
||||
$this->assertMessage($exp, $this->req($in[4]));
|
||||
Phake::verify(Arsse::$db, Phake::times(2))->labelRemove(Arsse::$user->id, 18);
|
||||
Phake::verify(Arsse::$db)->labelRemove(Arsse::$user->id, 1088);
|
||||
}
|
||||
|
@ -796,21 +808,21 @@ LONG_STRING;
|
|||
Phake::when(Arsse::$db)->labelPropertiesSet(...$db[5])->thenThrow(new ExceptionInput("typeViolation"));
|
||||
// succefully rename a label
|
||||
$exp = $this->respGood();
|
||||
$this->assertResponse($exp, $this->req($in[0]));
|
||||
$this->assertMessage($exp, $this->req($in[0]));
|
||||
// rename a label which does not exist (this should silently fail)
|
||||
$exp = $this->respGood();
|
||||
$this->assertResponse($exp, $this->req($in[1]));
|
||||
$this->assertMessage($exp, $this->req($in[1]));
|
||||
// rename a label causing a duplication (this should also silently fail)
|
||||
$exp = $this->respGood();
|
||||
$this->assertResponse($exp, $this->req($in[2]));
|
||||
$this->assertMessage($exp, $this->req($in[2]));
|
||||
// all the rest should cause errors
|
||||
$exp = $this->respErr("INCORRECT_USAGE");
|
||||
$this->assertResponse($exp, $this->req($in[3]));
|
||||
$this->assertResponse($exp, $this->req($in[4]));
|
||||
$this->assertResponse($exp, $this->req($in[5]));
|
||||
$this->assertResponse($exp, $this->req($in[6]));
|
||||
$this->assertResponse($exp, $this->req($in[7]));
|
||||
$this->assertResponse($exp, $this->req($in[8]));
|
||||
$this->assertMessage($exp, $this->req($in[3]));
|
||||
$this->assertMessage($exp, $this->req($in[4]));
|
||||
$this->assertMessage($exp, $this->req($in[5]));
|
||||
$this->assertMessage($exp, $this->req($in[6]));
|
||||
$this->assertMessage($exp, $this->req($in[7]));
|
||||
$this->assertMessage($exp, $this->req($in[8]));
|
||||
Phake::verify(Arsse::$db, Phake::times(6))->labelPropertiesSet(Arsse::$user->id, $this->anything(), $this->anything());
|
||||
}
|
||||
|
||||
|
@ -882,7 +894,7 @@ LONG_STRING;
|
|||
],
|
||||
];
|
||||
for ($a = 0; $a < sizeof($in); $a++) {
|
||||
$this->assertResponse($this->respGood($exp[$a]), $this->req($in[$a]), "Test $a failed");
|
||||
$this->assertMessage($this->respGood($exp[$a]), $this->req($in[$a]), "Test $a failed");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -918,7 +930,7 @@ LONG_STRING;
|
|||
['id' => 0, 'kind' => "cat", 'counter' => 0],
|
||||
['id' => -2, 'kind' => "cat", 'counter' => 6],
|
||||
];
|
||||
$this->assertResponse($this->respGood($exp), $this->req($in));
|
||||
$this->assertMessage($this->respGood($exp), $this->req($in));
|
||||
}
|
||||
|
||||
public function testRetrieveTheLabelList() {
|
||||
|
@ -962,7 +974,7 @@ LONG_STRING;
|
|||
],
|
||||
];
|
||||
for ($a = 0; $a < sizeof($in); $a++) {
|
||||
$this->assertResponse($this->respGood($exp[$a]), $this->req($in[$a]), "Test $a failed");
|
||||
$this->assertMessage($this->respGood($exp[$a]), $this->req($in[$a]), "Test $a failed");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -988,20 +1000,20 @@ LONG_STRING;
|
|||
Phake::when(Arsse::$db)->labelArticlesSet(Arsse::$user->id, 1088, (new Context)->articles($list[1]), false)->thenReturn(5);
|
||||
Phake::when(Arsse::$db)->labelArticlesSet(Arsse::$user->id, 1088, (new Context)->articles($list[2]), false)->thenReturn(2);
|
||||
$exp = $this->respGood(['status' => "OK", 'updated' => 89]);
|
||||
$this->assertResponse($exp, $this->req($in[0]));
|
||||
$this->assertMessage($exp, $this->req($in[0]));
|
||||
Phake::verify(Arsse::$db)->labelArticlesSet(Arsse::$user->id, 1088, (new Context)->articles($list[1]), true);
|
||||
Phake::verify(Arsse::$db)->labelArticlesSet(Arsse::$user->id, 1088, (new Context)->articles($list[2]), true);
|
||||
$exp = $this->respGood(['status' => "OK", 'updated' => 7]);
|
||||
$this->assertResponse($exp, $this->req($in[1]));
|
||||
$this->assertMessage($exp, $this->req($in[1]));
|
||||
Phake::verify(Arsse::$db)->labelArticlesSet(Arsse::$user->id, 1088, (new Context)->articles($list[1]), false);
|
||||
Phake::verify(Arsse::$db)->labelArticlesSet(Arsse::$user->id, 1088, (new Context)->articles($list[2]), false);
|
||||
$exp = $this->respGood(['status' => "OK", 'updated' => 0]);
|
||||
$this->assertResponse($exp, $this->req($in[2]));
|
||||
$this->assertMessage($exp, $this->req($in[2]));
|
||||
$exp = $this->respErr("INCORRECT_USAGE");
|
||||
$this->assertResponse($exp, $this->req($in[3]));
|
||||
$this->assertResponse($exp, $this->req($in[4]));
|
||||
$this->assertResponse($exp, $this->req($in[5]));
|
||||
$this->assertResponse($exp, $this->req($in[6]));
|
||||
$this->assertMessage($exp, $this->req($in[3]));
|
||||
$this->assertMessage($exp, $this->req($in[4]));
|
||||
$this->assertMessage($exp, $this->req($in[5]));
|
||||
$this->assertMessage($exp, $this->req($in[6]));
|
||||
}
|
||||
|
||||
public function testRetrieveFeedTree() {
|
||||
|
@ -1016,9 +1028,9 @@ LONG_STRING;
|
|||
Phake::when(Arsse::$db)->articleStarred($this->anything())->thenReturn($this->v($this->starred));
|
||||
// the expectations are packed tightly since they're very verbose; one can use var_export() (or convert to JSON) to pretty-print them
|
||||
$exp = ['categories'=>['identifier'=>'id','label'=>'name','items'=>[['name'=>'Special','id'=>'CAT:-1','bare_id'=>-1,'type'=>'category','unread'=>0,'items'=>[['name'=>'All articles','id'=>'FEED:-4','bare_id'=>-4,'icon'=>'images/folder.png','unread'=>35,'type'=>'feed','auxcounter'=>0,'error'=>'','updated'=>'',],['name'=>'Fresh articles','id'=>'FEED:-3','bare_id'=>-3,'icon'=>'images/fresh.png','unread'=>7,'type'=>'feed','auxcounter'=>0,'error'=>'','updated'=>'',],['name'=>'Starred articles','id'=>'FEED:-1','bare_id'=>-1,'icon'=>'images/star.png','unread'=>4,'type'=>'feed','auxcounter'=>0,'error'=>'','updated'=>'',],['name'=>'Published articles','id'=>'FEED:-2','bare_id'=>-2,'icon'=>'images/feed.png','unread'=>0,'type'=>'feed','auxcounter'=>0,'error'=>'','updated'=>'',],['name'=>'Archived articles','id'=>'FEED:0','bare_id'=>0,'icon'=>'images/archive.png','unread'=>0,'type'=>'feed','auxcounter'=>0,'error'=>'','updated'=>'',],['name'=>'Recently read','id'=>'FEED:-6','bare_id'=>-6,'icon'=>'images/time.png','unread'=>0,'type'=>'feed','auxcounter'=>0,'error'=>'','updated'=>'',],],],['name'=>'Labels','id'=>'CAT:-2','bare_id'=>-2,'type'=>'category','unread'=>6,'items'=>[['name'=>'Fascinating','id'=>'FEED:-1027','bare_id'=>-1027,'unread'=>0,'icon'=>'images/label.png','type'=>'feed','auxcounter'=>0,'error'=>'','updated'=>'','fg_color'=>'','bg_color'=>'',],['name'=>'Interesting','id'=>'FEED:-1029','bare_id'=>-1029,'unread'=>0,'icon'=>'images/label.png','type'=>'feed','auxcounter'=>0,'error'=>'','updated'=>'','fg_color'=>'','bg_color'=>'',],['name'=>'Logical','id'=>'FEED:-1025','bare_id'=>-1025,'unread'=>0,'icon'=>'images/label.png','type'=>'feed','auxcounter'=>0,'error'=>'','updated'=>'','fg_color'=>'','bg_color'=>'',],],],['name'=>'Photography','id'=>'CAT:4','bare_id'=>4,'parent_id'=>null,'type'=>'category','auxcounter'=>0,'unread'=>0,'child_unread'=>0,'checkbox'=>false,'param'=>'(0 feeds)','items'=>[],],['name'=>'Politics','id'=>'CAT:3','bare_id'=>3,'parent_id'=>null,'type'=>'category','auxcounter'=>0,'unread'=>0,'child_unread'=>0,'checkbox'=>false,'param'=>'(3 feeds)','items'=>[['name'=>'Local','id'=>'CAT:5','bare_id'=>5,'parent_id'=>3,'type'=>'category','auxcounter'=>0,'unread'=>0,'child_unread'=>0,'checkbox'=>false,'param'=>'(1 feed)','items'=>[['name'=>'Toronto Star','id'=>'FEED:2','bare_id'=>2,'icon'=>'feed-icons/2.ico','error'=>'oops','param'=>'2011-11-11T11:11:11Z','unread'=>0,'auxcounter'=>0,'checkbox'=>false,],],],['name'=>'National','id'=>'CAT:6','bare_id'=>6,'parent_id'=>3,'type'=>'category','auxcounter'=>0,'unread'=>0,'child_unread'=>0,'checkbox'=>false,'param'=>'(2 feeds)','items'=>[['name'=>'CBC News','id'=>'FEED:4','bare_id'=>4,'icon'=>'feed-icons/4.ico','error'=>'','param'=>'2017-10-09T15:58:34Z','unread'=>0,'auxcounter'=>0,'checkbox'=>false,],['name'=>'Ottawa Citizen','id'=>'FEED:5','bare_id'=>5,'icon'=>false,'error'=>'','param'=>'2017-07-07T17:07:17Z','unread'=>0,'auxcounter'=>0,'checkbox'=>false,],],],],],['name'=>'Science','id'=>'CAT:1','bare_id'=>1,'parent_id'=>null,'type'=>'category','auxcounter'=>0,'unread'=>0,'child_unread'=>0,'checkbox'=>false,'param'=>'(2 feeds)','items'=>[['name'=>'Rocketry','id'=>'CAT:2','bare_id'=>2,'parent_id'=>1,'type'=>'category','auxcounter'=>0,'unread'=>0,'child_unread'=>0,'checkbox'=>false,'param'=>'(1 feed)','items'=>[['name'=>'NASA JPL','id'=>'FEED:1','bare_id'=>1,'icon'=>false,'error'=>'','param'=>'2017-09-15T22:54:16Z','unread'=>0,'auxcounter'=>0,'checkbox'=>false,],],],['name'=>'Ars Technica','id'=>'FEED:3','bare_id'=>3,'icon'=>'feed-icons/3.ico','error'=>'argh','param'=>'2016-05-23T06:40:02Z','unread'=>0,'auxcounter'=>0,'checkbox'=>false,],],],['name'=>'Uncategorized','id'=>'CAT:0','bare_id'=>0,'type'=>'category','auxcounter'=>0,'unread'=>0,'child_unread'=>0,'checkbox'=>false,'parent_id'=>null,'param'=>'(1 feed)','items'=>[['name'=>'Eurogamer','id'=>'FEED:6','bare_id'=>6,'icon'=>'feed-icons/6.ico','error'=>'','param'=>'2010-02-12T20:08:47Z','unread'=>0,'auxcounter'=>0,'checkbox'=>false,],],],],],];
|
||||
$this->assertResponse($this->respGood($exp), $this->req($in[0]));
|
||||
$this->assertMessage($this->respGood($exp), $this->req($in[0]));
|
||||
$exp = ['categories'=>['identifier'=>'id','label'=>'name','items'=>[['name'=>'Special','id'=>'CAT:-1','bare_id'=>-1,'type'=>'category','unread'=>0,'items'=>[['name'=>'All articles','id'=>'FEED:-4','bare_id'=>-4,'icon'=>'images/folder.png','unread'=>35,'type'=>'feed','auxcounter'=>0,'error'=>'','updated'=>'',],['name'=>'Fresh articles','id'=>'FEED:-3','bare_id'=>-3,'icon'=>'images/fresh.png','unread'=>7,'type'=>'feed','auxcounter'=>0,'error'=>'','updated'=>'',],['name'=>'Starred articles','id'=>'FEED:-1','bare_id'=>-1,'icon'=>'images/star.png','unread'=>4,'type'=>'feed','auxcounter'=>0,'error'=>'','updated'=>'',],['name'=>'Published articles','id'=>'FEED:-2','bare_id'=>-2,'icon'=>'images/feed.png','unread'=>0,'type'=>'feed','auxcounter'=>0,'error'=>'','updated'=>'',],['name'=>'Archived articles','id'=>'FEED:0','bare_id'=>0,'icon'=>'images/archive.png','unread'=>0,'type'=>'feed','auxcounter'=>0,'error'=>'','updated'=>'',],['name'=>'Recently read','id'=>'FEED:-6','bare_id'=>-6,'icon'=>'images/time.png','unread'=>0,'type'=>'feed','auxcounter'=>0,'error'=>'','updated'=>'',],],],['name'=>'Labels','id'=>'CAT:-2','bare_id'=>-2,'type'=>'category','unread'=>6,'items'=>[['name'=>'Fascinating','id'=>'FEED:-1027','bare_id'=>-1027,'unread'=>0,'icon'=>'images/label.png','type'=>'feed','auxcounter'=>0,'error'=>'','updated'=>'','fg_color'=>'','bg_color'=>'',],['name'=>'Interesting','id'=>'FEED:-1029','bare_id'=>-1029,'unread'=>0,'icon'=>'images/label.png','type'=>'feed','auxcounter'=>0,'error'=>'','updated'=>'','fg_color'=>'','bg_color'=>'',],['name'=>'Logical','id'=>'FEED:-1025','bare_id'=>-1025,'unread'=>0,'icon'=>'images/label.png','type'=>'feed','auxcounter'=>0,'error'=>'','updated'=>'','fg_color'=>'','bg_color'=>'',],],],['name'=>'Politics','id'=>'CAT:3','bare_id'=>3,'parent_id'=>null,'type'=>'category','auxcounter'=>0,'unread'=>0,'child_unread'=>0,'checkbox'=>false,'param'=>'(3 feeds)','items'=>[['name'=>'Local','id'=>'CAT:5','bare_id'=>5,'parent_id'=>3,'type'=>'category','auxcounter'=>0,'unread'=>0,'child_unread'=>0,'checkbox'=>false,'param'=>'(1 feed)','items'=>[['name'=>'Toronto Star','id'=>'FEED:2','bare_id'=>2,'icon'=>'feed-icons/2.ico','error'=>'oops','param'=>'2011-11-11T11:11:11Z','unread'=>0,'auxcounter'=>0,'checkbox'=>false,],],],['name'=>'National','id'=>'CAT:6','bare_id'=>6,'parent_id'=>3,'type'=>'category','auxcounter'=>0,'unread'=>0,'child_unread'=>0,'checkbox'=>false,'param'=>'(2 feeds)','items'=>[['name'=>'CBC News','id'=>'FEED:4','bare_id'=>4,'icon'=>'feed-icons/4.ico','error'=>'','param'=>'2017-10-09T15:58:34Z','unread'=>0,'auxcounter'=>0,'checkbox'=>false,],['name'=>'Ottawa Citizen','id'=>'FEED:5','bare_id'=>5,'icon'=>false,'error'=>'','param'=>'2017-07-07T17:07:17Z','unread'=>0,'auxcounter'=>0,'checkbox'=>false,],],],],],['name'=>'Science','id'=>'CAT:1','bare_id'=>1,'parent_id'=>null,'type'=>'category','auxcounter'=>0,'unread'=>0,'child_unread'=>0,'checkbox'=>false,'param'=>'(2 feeds)','items'=>[['name'=>'Rocketry','id'=>'CAT:2','bare_id'=>2,'parent_id'=>1,'type'=>'category','auxcounter'=>0,'unread'=>0,'child_unread'=>0,'checkbox'=>false,'param'=>'(1 feed)','items'=>[['name'=>'NASA JPL','id'=>'FEED:1','bare_id'=>1,'icon'=>false,'error'=>'','param'=>'2017-09-15T22:54:16Z','unread'=>0,'auxcounter'=>0,'checkbox'=>false,],],],['name'=>'Ars Technica','id'=>'FEED:3','bare_id'=>3,'icon'=>'feed-icons/3.ico','error'=>'argh','param'=>'2016-05-23T06:40:02Z','unread'=>0,'auxcounter'=>0,'checkbox'=>false,],],],['name'=>'Uncategorized','id'=>'CAT:0','bare_id'=>0,'type'=>'category','auxcounter'=>0,'unread'=>0,'child_unread'=>0,'checkbox'=>false,'parent_id'=>null,'param'=>'(1 feed)','items'=>[['name'=>'Eurogamer','id'=>'FEED:6','bare_id'=>6,'icon'=>'feed-icons/6.ico','error'=>'','param'=>'2010-02-12T20:08:47Z','unread'=>0,'auxcounter'=>0,'checkbox'=>false,],],],],],];
|
||||
$this->assertResponse($this->respGood($exp), $this->req($in[1]));
|
||||
$this->assertMessage($this->respGood($exp), $this->req($in[1]));
|
||||
}
|
||||
|
||||
public function testMarkFeedsAsRead() {
|
||||
|
@ -1050,12 +1062,12 @@ LONG_STRING;
|
|||
$exp = $this->respGood(['status' => "OK"]);
|
||||
// verify the above are in fact no-ops
|
||||
for ($a = 0; $a < sizeof($in1); $a++) {
|
||||
$this->assertResponse($exp, $this->req($in1[$a]), "Test $a failed");
|
||||
$this->assertMessage($exp, $this->req($in1[$a]), "Test $a failed");
|
||||
}
|
||||
Phake::verify(Arsse::$db, Phake::times(0))->articleMark;
|
||||
// verify the simple contexts
|
||||
for ($a = 0; $a < sizeof($in2); $a++) {
|
||||
$this->assertResponse($exp, $this->req($in2[$a]), "Test $a failed");
|
||||
$this->assertMessage($exp, $this->req($in2[$a]), "Test $a failed");
|
||||
}
|
||||
Phake::verify(Arsse::$db)->articleMark($this->anything(), ['read' => true], new Context);
|
||||
Phake::verify(Arsse::$db)->articleMark($this->anything(), ['read' => true], (new Context)->starred(true));
|
||||
|
@ -1067,7 +1079,7 @@ LONG_STRING;
|
|||
// verify the time-based mock
|
||||
$t = Date::sub("PT24H");
|
||||
for ($a = 0; $a < sizeof($in3); $a++) {
|
||||
$this->assertResponse($exp, $this->req($in3[$a]), "Test $a failed");
|
||||
$this->assertMessage($exp, $this->req($in3[$a]), "Test $a failed");
|
||||
}
|
||||
Phake::verify(Arsse::$db)->articleMark($this->anything(), ['read' => true], (new Context)->modifiedSince($t));
|
||||
}
|
||||
|
@ -1202,10 +1214,10 @@ LONG_STRING;
|
|||
],
|
||||
];
|
||||
for ($a = 0; $a < sizeof($in1); $a++) {
|
||||
$this->assertResponse($this->respGood($exp[$a]), $this->req($in1[$a]), "Test $a failed");
|
||||
$this->assertMessage($this->respGood($exp[$a]), $this->req($in1[$a]), "Test $a failed");
|
||||
}
|
||||
for ($a = 0; $a < sizeof($in2); $a++) {
|
||||
$this->assertResponse($this->respGood([]), $this->req($in2[$a]), "Test $a failed");
|
||||
$this->assertMessage($this->respGood([]), $this->req($in2[$a]), "Test $a failed");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1315,7 +1327,7 @@ LONG_STRING;
|
|||
$this->respErr("INCORRECT_USAGE"),
|
||||
];
|
||||
for ($a = 0; $a < sizeof($in); $a++) {
|
||||
$this->assertResponse($out[$a], $this->req($in[$a]), "Test $a failed");
|
||||
$this->assertMessage($out[$a], $this->req($in[$a]), "Test $a failed");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1339,10 +1351,10 @@ LONG_STRING;
|
|||
Phake::when(Arsse::$db)->articleList($this->anything(), (new Context)->articles([101]))->thenReturn(new Result($this->v([$this->articles[0]])));
|
||||
Phake::when(Arsse::$db)->articleList($this->anything(), (new Context)->articles([102]))->thenReturn(new Result($this->v([$this->articles[1]])));
|
||||
$exp = $this->respErr("INCORRECT_USAGE");
|
||||
$this->assertResponse($exp, $this->req($in[0]));
|
||||
$this->assertResponse($exp, $this->req($in[1]));
|
||||
$this->assertResponse($exp, $this->req($in[2]));
|
||||
$this->assertResponse($exp, $this->req($in[3]));
|
||||
$this->assertMessage($exp, $this->req($in[0]));
|
||||
$this->assertMessage($exp, $this->req($in[1]));
|
||||
$this->assertMessage($exp, $this->req($in[2]));
|
||||
$this->assertMessage($exp, $this->req($in[3]));
|
||||
$exp = [
|
||||
[
|
||||
'id' => "101",
|
||||
|
@ -1399,13 +1411,13 @@ LONG_STRING;
|
|||
'content' => '<p>Article content 2</p>',
|
||||
],
|
||||
];
|
||||
$this->assertResponse($this->respGood($exp), $this->req($in[4]));
|
||||
$this->assertResponse($this->respGood([$exp[0]]), $this->req($in[5]));
|
||||
$this->assertResponse($this->respGood([$exp[1]]), $this->req($in[6]));
|
||||
$this->assertMessage($this->respGood($exp), $this->req($in[4]));
|
||||
$this->assertMessage($this->respGood([$exp[0]]), $this->req($in[5]));
|
||||
$this->assertMessage($this->respGood([$exp[1]]), $this->req($in[6]));
|
||||
// test the special case when labels are not used
|
||||
Phake::when(Arsse::$db)->labelList($this->anything())->thenReturn(new Result([]));
|
||||
Phake::when(Arsse::$db)->labelList($this->anything(), false)->thenReturn(new Result([]));
|
||||
$this->assertResponse($this->respGood([$exp[0]]), $this->req($in[5]));
|
||||
$this->assertMessage($this->respGood([$exp[0]]), $this->req($in[5]));
|
||||
}
|
||||
|
||||
public function testRetrieveCompactHeadlines() {
|
||||
|
@ -1484,13 +1496,13 @@ LONG_STRING;
|
|||
$this->respGood([['id' => 1003]]),
|
||||
];
|
||||
for ($a = 0; $a < sizeof($in1); $a++) {
|
||||
$this->assertResponse($out1[$a], $this->req($in1[$a]), "Test $a failed");
|
||||
$this->assertMessage($out1[$a], $this->req($in1[$a]), "Test $a failed");
|
||||
}
|
||||
for ($a = 0; $a < sizeof($in2); $a++) {
|
||||
Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->unread(false)->markedSince(Date::sub("PT24H")), Database::LIST_MINIMAL)->thenReturn(new Result($this->v([['id' => 1001]])));
|
||||
Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->unread(true)->modifiedSince(Date::sub("PT24H")), Database::LIST_MINIMAL)->thenReturn(new Result($this->v([['id' => 1002]])));
|
||||
Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->unread(true)->modifiedSince(Date::sub("PT24H"))->starred(true), Database::LIST_MINIMAL)->thenReturn(new Result($this->v([['id' => 1003]])));
|
||||
$this->assertResponse($out2[$a], $this->req($in2[$a]), "Test $a failed");
|
||||
$this->assertMessage($out2[$a], $this->req($in2[$a]), "Test $a failed");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1592,16 +1604,16 @@ LONG_STRING;
|
|||
$this->outputHeadlines(1003),
|
||||
];
|
||||
for ($a = 0; $a < sizeof($in1); $a++) {
|
||||
$this->assertResponse($this->respGood([]), $this->req($in1[$a]), "Test $a failed");
|
||||
$this->assertMessage($this->respGood([]), $this->req($in1[$a]), "Test $a failed");
|
||||
}
|
||||
for ($a = 0; $a < sizeof($in2); $a++) {
|
||||
$this->assertResponse($out2[$a], $this->req($in2[$a]), "Test $a failed");
|
||||
$this->assertMessage($out2[$a], $this->req($in2[$a]), "Test $a failed");
|
||||
}
|
||||
for ($a = 0; $a < sizeof($in3); $a++) {
|
||||
Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->unread(false)->markedSince(Date::sub("PT24H")), Database::LIST_FULL)->thenReturn($this->generateHeadlines(1001));
|
||||
Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->unread(true)->modifiedSince(Date::sub("PT24H")), Database::LIST_FULL)->thenReturn($this->generateHeadlines(1002));
|
||||
Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->unread(true)->modifiedSince(Date::sub("PT24H"))->starred(true), Database::LIST_FULL)->thenReturn($this->generateHeadlines(1003));
|
||||
$this->assertResponse($out3[$a], $this->req($in3[$a]), "Test $a failed");
|
||||
$this->assertMessage($out3[$a], $this->req($in3[$a]), "Test $a failed");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1631,13 +1643,13 @@ LONG_STRING;
|
|||
Phake::when(Arsse::$db)->articleCount($this->anything(), (new Context)->unread(true))->thenReturn(1);
|
||||
// sanity check; this makes sure extra fields are not included in default situations
|
||||
$test = $this->req($in[0]);
|
||||
$this->assertResponse($this->outputHeadlines(1), $test);
|
||||
$this->assertMessage($this->outputHeadlines(1), $test);
|
||||
// test 'show_content'
|
||||
$test = $this->req($in[1]);
|
||||
$this->assertArrayHasKey("content", $test->payload['content'][0]);
|
||||
$this->assertArrayHasKey("content", $test->payload['content'][1]);
|
||||
$this->assertArrayHasKey("content", $test->getPayload()['content'][0]);
|
||||
$this->assertArrayHasKey("content", $test->getPayload()['content'][1]);
|
||||
foreach ($this->generateHeadlines(1) as $key => $row) {
|
||||
$this->assertSame($row['content'], $test->payload['content'][$key]['content']);
|
||||
$this->assertSame($row['content'], $test->getPayload()['content'][$key]['content']);
|
||||
}
|
||||
// test 'include_attachments'
|
||||
$test = $this->req($in[2]);
|
||||
|
@ -1653,33 +1665,31 @@ LONG_STRING;
|
|||
'post_id' => "2112",
|
||||
],
|
||||
];
|
||||
$this->assertArrayHasKey("attachments", $test->payload['content'][0]);
|
||||
$this->assertArrayHasKey("attachments", $test->payload['content'][1]);
|
||||
$this->assertSame([], $test->payload['content'][0]['attachments']);
|
||||
$this->assertSame($exp, $test->payload['content'][1]['attachments']);
|
||||
$this->assertArrayHasKey("attachments", $test->getPayload()['content'][0]);
|
||||
$this->assertArrayHasKey("attachments", $test->getPayload()['content'][1]);
|
||||
$this->assertSame([], $test->getPayload()['content'][0]['attachments']);
|
||||
$this->assertSame($exp, $test->getPayload()['content'][1]['attachments']);
|
||||
// test 'include_header'
|
||||
$test = $this->req($in[3]);
|
||||
$exp = $this->outputHeadlines(1);
|
||||
$exp->payload['content'] = [
|
||||
$exp = $this->respGood([
|
||||
['id' => -4, 'is_cat' => false, 'first_id' => 1],
|
||||
$exp->payload['content'],
|
||||
];
|
||||
$this->assertResponse($exp, $test);
|
||||
$this->outputHeadlines(1)->getPayload()['content'],
|
||||
]);
|
||||
$this->assertMessage($exp, $test);
|
||||
// test 'include_header' with a category
|
||||
$test = $this->req($in[4]);
|
||||
$exp = $this->outputHeadlines(1);
|
||||
$exp->payload['content'] = [
|
||||
$exp = $this->respGood([
|
||||
['id' => -3, 'is_cat' => true, 'first_id' => 1],
|
||||
$exp->payload['content'],
|
||||
];
|
||||
$this->assertResponse($exp, $test);
|
||||
$this->outputHeadlines(1)->getPayload()['content'],
|
||||
]);
|
||||
$this->assertMessage($exp, $test);
|
||||
// test 'include_header' with an empty result
|
||||
$test = $this->req($in[5]);
|
||||
$exp = $this->respGood([
|
||||
['id' => -1, 'is_cat' => true, 'first_id' => 0],
|
||||
[],
|
||||
]);
|
||||
$this->assertResponse($exp, $test);
|
||||
$this->assertMessage($exp, $test);
|
||||
// test 'include_header' with an erroneous result
|
||||
Phake::when(Arsse::$db)->articleList($this->anything(), (new Context)->limit(200)->reverse(true)->subscription(2112), $this->anything())->thenThrow(new ExceptionInput("subjectMissing"));
|
||||
$test = $this->req($in[6]);
|
||||
|
@ -1687,40 +1697,37 @@ LONG_STRING;
|
|||
['id' => 2112, 'is_cat' => false, 'first_id' => 0],
|
||||
[],
|
||||
]);
|
||||
$this->assertResponse($exp, $test);
|
||||
$this->assertMessage($exp, $test);
|
||||
// test 'include_header' with ascending order
|
||||
$test = $this->req($in[7]);
|
||||
$exp = $this->outputHeadlines(1);
|
||||
$exp->payload['content'] = [
|
||||
$exp = $this->respGood([
|
||||
['id' => -4, 'is_cat' => false, 'first_id' => 0],
|
||||
$exp->payload['content'],
|
||||
];
|
||||
$this->assertResponse($exp, $test);
|
||||
$this->outputHeadlines(1)->getPayload()['content'],
|
||||
]);
|
||||
$this->assertMessage($exp, $test);
|
||||
// test 'include_header' with skip
|
||||
Phake::when(Arsse::$db)->articleList($this->anything(), (new Context)->reverse(true)->limit(1)->subscription(42), Database::LIST_MINIMAL)->thenReturn($this->generateHeadlines(1867));
|
||||
$test = $this->req($in[8]);
|
||||
$exp = $this->outputHeadlines(1);
|
||||
$exp->payload['content'] = [
|
||||
$exp = $this->respGood([
|
||||
['id' => 42, 'is_cat' => false, 'first_id' => 1867],
|
||||
$exp->payload['content'],
|
||||
];
|
||||
$this->assertResponse($exp, $test);
|
||||
$this->outputHeadlines(1)->getPayload()['content'],
|
||||
]);
|
||||
$this->assertMessage($exp, $test);
|
||||
// test 'include_header' with skip and ascending order
|
||||
$test = $this->req($in[9]);
|
||||
$exp = $this->outputHeadlines(1);
|
||||
$exp->payload['content'] = [
|
||||
$exp = $this->respGood([
|
||||
['id' => 42, 'is_cat' => false, 'first_id' => 0],
|
||||
$exp->payload['content'],
|
||||
];
|
||||
$this->assertResponse($exp, $test);
|
||||
$this->outputHeadlines(1)->getPayload()['content'],
|
||||
]);
|
||||
$this->assertMessage($exp, $test);
|
||||
// test 'show_excerpt'
|
||||
$exp1 = "“This & that, you know‽”";
|
||||
$exp2 = "Pour vous faire mieux connaitre d’ou\u{300} vient l’erreur de ceux qui bla\u{302}ment la volupte\u{301}, et qui louent en…";
|
||||
$test = $this->req($in[10]);
|
||||
$this->assertArrayHasKey("excerpt", $test->payload['content'][0]);
|
||||
$this->assertArrayHasKey("excerpt", $test->payload['content'][1]);
|
||||
$this->assertSame($exp1, $test->payload['content'][0]['excerpt']);
|
||||
$this->assertSame($exp2, $test->payload['content'][1]['excerpt']);
|
||||
$this->assertArrayHasKey("excerpt", $test->getPayload()['content'][0]);
|
||||
$this->assertArrayHasKey("excerpt", $test->getPayload()['content'][1]);
|
||||
$this->assertSame($exp1, $test->getPayload()['content'][0]['excerpt']);
|
||||
$this->assertSame($exp2, $test->getPayload()['content'][1]['excerpt']);
|
||||
}
|
||||
|
||||
protected function generateHeadlines(int $id): Result {
|
||||
|
|
|
@ -12,7 +12,9 @@ use JKingWeb\Arsse\User;
|
|||
use JKingWeb\Arsse\Database;
|
||||
use JKingWeb\Arsse\REST\TinyTinyRSS\Icon;
|
||||
use JKingWeb\Arsse\REST\Request;
|
||||
use JKingWeb\Arsse\REST\Response;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Zend\Diactoros\ServerRequest;
|
||||
use Zend\Diactoros\Response\EmptyResponse as Response;
|
||||
use Phake;
|
||||
|
||||
/** @covers \JKingWeb\Arsse\REST\TinyTinyRSS\Icon<extended> */
|
||||
|
@ -32,26 +34,37 @@ 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");
|
||||
Phake::when(Arsse::$db)->subscriptionFavicon(2112)->thenReturn("http://example.net/logo.png");
|
||||
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->assertEquals($exp, $this->h->dispatch(new Request("GET", "42.ico")));
|
||||
$exp = new Response(301, "", "", ["Location: http://example.net/logo.png"]);
|
||||
$this->assertEquals($exp, $this->h->dispatch(new Request("GET", "2112.ico")));
|
||||
$exp = new Response(301, "", "", ["Location: http://example.org/icon.gif"]);
|
||||
$this->assertEquals($exp, $this->h->dispatch(new Request("GET", "1337.ico")));
|
||||
$exp = new Response(301, ['Location' => "http://example.com/favicon.ico"]);
|
||||
$this->assertMessage($exp, $this->req("42.ico"));
|
||||
$exp = new Response(301, ['Location' => "http://example.net/logo.png"]);
|
||||
$this->assertMessage($exp, $this->req("2112.ico"));
|
||||
$exp = new Response(301, ['Location' => "http://example.org/icon.gif"]);
|
||||
$this->assertMessage($exp, $this->req("1337.ico"));
|
||||
// these requests should fail
|
||||
$exp = new Response(404);
|
||||
$this->assertEquals($exp, $this->h->dispatch(new Request("GET", "ook.ico")));
|
||||
$this->assertEquals($exp, $this->h->dispatch(new Request("GET", "ook")));
|
||||
$this->assertEquals($exp, $this->h->dispatch(new Request("GET", "47.ico")));
|
||||
$this->assertEquals($exp, $this->h->dispatch(new Request("GET", "2112.png")));
|
||||
$this->assertMessage($exp, $this->req("ook.ico"));
|
||||
$this->assertMessage($exp, $this->req("ook"));
|
||||
$this->assertMessage($exp, $this->req("47.ico"));
|
||||
$this->assertMessage($exp, $this->req("2112.png"));
|
||||
// only GET is allowed
|
||||
$exp = new Response(405, "", "", ["Allow: GET"]);
|
||||
$this->assertEquals($exp, $this->h->dispatch(new Request("PUT", "2112.ico")));
|
||||
$exp = new Response(405, ['Allow' => "GET"]);
|
||||
$this->assertMessage($exp, $this->req("2112.ico", "PUT"));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,10 +8,29 @@ namespace JKingWeb\Arsse\Test;
|
|||
|
||||
use JKingWeb\Arsse\Exception;
|
||||
use JKingWeb\Arsse\Arsse;
|
||||
use JKingWeb\Arsse\Conf;
|
||||
use JKingWeb\Arsse\Misc\Date;
|
||||
use Psr\Http\Message\MessageInterface;
|
||||
use Psr\Http\Message\RequestInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Zend\Diactoros\Response\JsonResponse;
|
||||
use Zend\Diactoros\Response\EmptyResponse;
|
||||
|
||||
/** @coversNothing */
|
||||
abstract class AbstractTest extends \PHPUnit\Framework\TestCase {
|
||||
public function setUp() {
|
||||
$this->clearData();
|
||||
}
|
||||
|
||||
public function tearDown() {
|
||||
$this->clearData();
|
||||
}
|
||||
|
||||
public function setConf(array $conf = []) {
|
||||
Arsse::$conf = (new Conf)->import($conf);
|
||||
}
|
||||
|
||||
public function assertException(string $msg = "", string $prefix = "", string $type = "Exception") {
|
||||
if (func_num_args()) {
|
||||
$class = \JKingWeb\Arsse\NS_BASE . ($prefix !== "" ? str_replace("/", "\\", $prefix) . "\\" : "") . $type;
|
||||
|
@ -29,6 +48,28 @@ abstract class AbstractTest extends \PHPUnit\Framework\TestCase {
|
|||
}
|
||||
}
|
||||
|
||||
protected function assertMessage(MessageInterface $exp, MessageInterface $act, string $text = null) {
|
||||
if ($exp instanceof ResponseInterface) {
|
||||
$this->assertInstanceOf(ResponseInterface::class, $act, $text);
|
||||
$this->assertEquals($exp->getStatusCode(), $act->getStatusCode(), $text);
|
||||
} elseif ($exp instanceof RequestInterface) {
|
||||
if ($exp instanceof ServerRequestInterface) {
|
||||
$this->assertInstanceOf(ServerRequestInterface::class, $act, $text);
|
||||
$this->assertEquals($exp->getAttributes(), $act->getAttributes(), $text);
|
||||
}
|
||||
$this->assertInstanceOf(RequestInterface::class, $act, $text);
|
||||
$this->assertSame($exp->getMethod(), $act->getMethod(), $text);
|
||||
$this->assertSame($exp->getRequestTarget(), $act->getRequestTarget(), $text);
|
||||
}
|
||||
if ($exp instanceof JsonResponse) {
|
||||
$this->assertEquals($exp->getPayload(), $act->getPayload(), $text);
|
||||
$this->assertSame($exp->getPayload(), $act->getPayload(), $text);
|
||||
} else {
|
||||
$this->assertEquals((string) $exp->getBody(), (string) $act->getBody(), $text);
|
||||
}
|
||||
$this->assertEquals($exp->getHeaders(), $act->getHeaders(), $text);
|
||||
}
|
||||
|
||||
public function approximateTime($exp, $act) {
|
||||
if (is_null($act)) {
|
||||
return null;
|
||||
|
|
|
@ -82,7 +82,10 @@
|
|||
<file>cases/Db/SQLite3PDO/Database/TestLabel.php</file>
|
||||
<file>cases/Db/SQLite3PDO/Database/TestCleanup.php</file>
|
||||
</testsuite>
|
||||
<testsuite name="Controllers">
|
||||
<testsuite name="REST">
|
||||
<file>cases/REST/TestTarget.php</file>
|
||||
<file>cases/REST/TestREST.php</file>
|
||||
</testsuite>
|
||||
<testsuite name="NCNv1">
|
||||
<file>cases/REST/NextCloudNews/TestVersions.php</file>
|
||||
<file>cases/REST/NextCloudNews/TestV1_2.php</file>
|
||||
|
@ -93,7 +96,6 @@
|
|||
<file>cases/REST/TinyTinyRSS/TestIcon.php</file>
|
||||
<file>cases/REST/TinyTinyRSS/PDO/TestAPI.php</file>
|
||||
</testsuite>
|
||||
</testsuite>
|
||||
<testsuite name="Refresh service">
|
||||
<file>cases/Service/TestService.php</file>
|
||||
</testsuite>
|
||||
|
|
20
vendor-bin/phpunit/composer.lock
generated
20
vendor-bin/phpunit/composer.lock
generated
|
@ -777,16 +777,16 @@
|
|||
},
|
||||
{
|
||||
"name": "phpunit/phpunit",
|
||||
"version": "6.5.4",
|
||||
"version": "6.5.5",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/sebastianbergmann/phpunit.git",
|
||||
"reference": "1b2f933d5775f9237369deaa2d2bfbf9d652be4c"
|
||||
"reference": "83d27937a310f2984fd575686138597147bdc7df"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/1b2f933d5775f9237369deaa2d2bfbf9d652be4c",
|
||||
"reference": "1b2f933d5775f9237369deaa2d2bfbf9d652be4c",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/83d27937a310f2984fd575686138597147bdc7df",
|
||||
"reference": "83d27937a310f2984fd575686138597147bdc7df",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
@ -857,7 +857,7 @@
|
|||
"testing",
|
||||
"xunit"
|
||||
],
|
||||
"time": "2017-12-10T08:06:19+00:00"
|
||||
"time": "2017-12-17T06:31:19+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phpunit/phpunit-mock-objects",
|
||||
|
@ -965,16 +965,16 @@
|
|||
},
|
||||
{
|
||||
"name": "sebastian/comparator",
|
||||
"version": "2.1.0",
|
||||
"version": "2.1.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/sebastianbergmann/comparator.git",
|
||||
"reference": "1174d9018191e93cb9d719edec01257fc05f8158"
|
||||
"reference": "b11c729f95109b56a0fe9650c6a63a0fcd8c439f"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/1174d9018191e93cb9d719edec01257fc05f8158",
|
||||
"reference": "1174d9018191e93cb9d719edec01257fc05f8158",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/b11c729f95109b56a0fe9650c6a63a0fcd8c439f",
|
||||
"reference": "b11c729f95109b56a0fe9650c6a63a0fcd8c439f",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
@ -1025,7 +1025,7 @@
|
|||
"compare",
|
||||
"equality"
|
||||
],
|
||||
"time": "2017-11-03T07:16:52+00:00"
|
||||
"time": "2017-12-22T14:50:35+00:00"
|
||||
},
|
||||
{
|
||||
"name": "sebastian/diff",
|
||||
|
|
76
vendor-bin/robo/composer.lock
generated
76
vendor-bin/robo/composer.lock
generated
|
@ -59,28 +59,33 @@
|
|||
},
|
||||
{
|
||||
"name": "consolidation/config",
|
||||
"version": "1.0.7",
|
||||
"version": "1.0.9",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/consolidation/config.git",
|
||||
"reference": "b59a3b9ea750c21397f26a68fd2e04d9580af42e"
|
||||
"reference": "34ca8d7c1ee60a7b591b10617114cf1210a2e92c"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/consolidation/config/zipball/b59a3b9ea750c21397f26a68fd2e04d9580af42e",
|
||||
"reference": "b59a3b9ea750c21397f26a68fd2e04d9580af42e",
|
||||
"url": "https://api.github.com/repos/consolidation/config/zipball/34ca8d7c1ee60a7b591b10617114cf1210a2e92c",
|
||||
"reference": "34ca8d7c1ee60a7b591b10617114cf1210a2e92c",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"dflydev/dot-access-data": "^1.1.0",
|
||||
"grasmash/yaml-expander": "^1.1",
|
||||
"grasmash/expander": "^1",
|
||||
"php": ">=5.4.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"greg-1-anderson/composer-test-scenarios": "^1",
|
||||
"phpunit/phpunit": "^4",
|
||||
"satooshi/php-coveralls": "^1.0",
|
||||
"squizlabs/php_codesniffer": "2.*",
|
||||
"symfony/console": "^2.5|^3"
|
||||
"symfony/console": "^2.5|^3|^4",
|
||||
"symfony/yaml": "^2.8.11|^3|^4"
|
||||
},
|
||||
"suggest": {
|
||||
"symfony/yaml": "Required to use Consolidation\\Config\\Loader\\YamlConfigLoader"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
|
@ -104,7 +109,7 @@
|
|||
}
|
||||
],
|
||||
"description": "Provide configuration services for a commandline tool.",
|
||||
"time": "2017-10-25T05:50:10+00:00"
|
||||
"time": "2017-12-22T17:28:19+00:00"
|
||||
},
|
||||
{
|
||||
"name": "consolidation/log",
|
||||
|
@ -205,16 +210,16 @@
|
|||
},
|
||||
{
|
||||
"name": "consolidation/robo",
|
||||
"version": "1.2.0",
|
||||
"version": "1.2.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/consolidation/Robo.git",
|
||||
"reference": "c46c13de3eca55e6b3635f363688ce85e845adf0"
|
||||
"reference": "b6296f1cf1088f1a11b0b819f9e42ef6f00b79a9"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/consolidation/Robo/zipball/c46c13de3eca55e6b3635f363688ce85e845adf0",
|
||||
"reference": "c46c13de3eca55e6b3635f363688ce85e845adf0",
|
||||
"url": "https://api.github.com/repos/consolidation/Robo/zipball/b6296f1cf1088f1a11b0b819f9e42ef6f00b79a9",
|
||||
"reference": "b6296f1cf1088f1a11b0b819f9e42ef6f00b79a9",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
@ -278,7 +283,7 @@
|
|||
}
|
||||
],
|
||||
"description": "Modern task runner",
|
||||
"time": "2017-12-13T02:10:49+00:00"
|
||||
"time": "2017-12-29T06:48:35+00:00"
|
||||
},
|
||||
{
|
||||
"name": "container-interop/container-interop",
|
||||
|
@ -370,6 +375,53 @@
|
|||
],
|
||||
"time": "2017-01-20T21:14:22+00:00"
|
||||
},
|
||||
{
|
||||
"name": "grasmash/expander",
|
||||
"version": "1.0.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/grasmash/expander.git",
|
||||
"reference": "95d6037344a4be1dd5f8e0b0b2571a28c397578f"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/grasmash/expander/zipball/95d6037344a4be1dd5f8e0b0b2571a28c397578f",
|
||||
"reference": "95d6037344a4be1dd5f8e0b0b2571a28c397578f",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"dflydev/dot-access-data": "^1.1.0",
|
||||
"php": ">=5.4"
|
||||
},
|
||||
"require-dev": {
|
||||
"greg-1-anderson/composer-test-scenarios": "^1",
|
||||
"phpunit/phpunit": "^4|^5.5.4",
|
||||
"satooshi/php-coveralls": "^1.0.2|dev-master",
|
||||
"squizlabs/php_codesniffer": "^2.7"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "1.x-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Grasmash\\Expander\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Matthew Grasmick"
|
||||
}
|
||||
],
|
||||
"description": "Expands internal property references in PHP arrays file.",
|
||||
"time": "2017-12-21T22:14:55+00:00"
|
||||
},
|
||||
{
|
||||
"name": "grasmash/yaml-expander",
|
||||
"version": "1.4.0",
|
||||
|
|
Loading…
Reference in a new issue