Browse Source

Merged master; CS fixes

microsub
J. King 7 years ago
parent
commit
5488b994f7
  1. 2
      lib/Conf.php
  2. 109
      lib/Database.php
  3. 2
      lib/Db/SQLite3/Driver.php
  4. 2
      lib/Misc/Context.php
  5. 63
      lib/Misc/ValueInfo.php
  6. 37
      lib/REST/AbstractHandler.php
  7. 55
      lib/REST/NextCloudNews/V1_2.php
  8. 8
      lib/REST/TinyTinyRSS/API.php
  9. 2
      lib/REST/TinyTinyRSS/Exception.php
  10. 4
      tests/Misc/TestContext.php
  11. 204
      tests/Misc/TestValueInfo.php
  12. 97
      tests/REST/NextCloudNews/TestNCNV1_2.php
  13. 6
      tests/REST/TinyTinyRSS/TestTinyTinyAPI.php
  14. 4
      tests/lib/AbstractTest.php
  15. 1
      tests/lib/Database/SeriesCleanup.php
  16. 5
      tests/lib/Database/SeriesFeed.php
  17. 21
      tests/lib/Database/SeriesFolder.php
  18. 1
      tests/lib/Database/SeriesSession.php
  19. 17
      tests/lib/Database/SeriesSubscription.php
  20. 15
      tests/lib/Misc/StrClass.php
  21. 7
      tests/phpunit.xml

2
lib/Conf.php

@ -31,7 +31,7 @@ class Conf {
/** @var string Period of inactivity after which log-in sessions should be considered invalid, as an ISO 8601 duration (default: 1 hour) /** @var string Period of inactivity after which log-in sessions should be considered invalid, as an ISO 8601 duration (default: 1 hour)
* @see https://en.wikipedia.org/wiki/ISO_8601#Durations */ * @see https://en.wikipedia.org/wiki/ISO_8601#Durations */
public $userSessionTimeout = "PT1H"; public $userSessionTimeout = "PT1H";
/** @var string Maximum lifetime of log-in sessions regardless of activity, as an ISO 8601 duration (default: 24 hours); /** @var string Maximum lifetime of log-in sessions regardless of activity, as an ISO 8601 duration (default: 24 hours);
* @see https://en.wikipedia.org/wiki/ISO_8601#Durations */ * @see https://en.wikipedia.org/wiki/ISO_8601#Durations */
public $userSessionLifetime = "PT24H"; public $userSessionLifetime = "PT24H";

109
lib/Database.php

@ -285,23 +285,19 @@ class Database {
// normalize folder's parent, if there is one // normalize folder's parent, if there is one
$parent = array_key_exists("parent", $data) ? $this->folderValidateId($user, $data['parent'])['id'] : null; $parent = array_key_exists("parent", $data) ? $this->folderValidateId($user, $data['parent'])['id'] : null;
// validate the folder name and parent (if specified); this also checks for duplicates // validate the folder name and parent (if specified); this also checks for duplicates
$name = array_key_exists("name", $data) ? $data['name'] : ""; $name = array_key_exists("name", $data) ? $data['name'] : "";
$this->folderValidateName($name, true, $parent); $this->folderValidateName($name, true, $parent);
// actually perform the insert // actually perform the insert
return $this->db->prepare("INSERT INTO arsse_folders(owner,parent,name) values(?,?,?)", "str", "int", "str")->run($user, $parent, $name)->lastId(); return $this->db->prepare("INSERT INTO arsse_folders(owner,parent,name) values(?,?,?)", "str", "int", "str")->run($user, $parent, $name)->lastId();
} }
public function folderList(string $user, int $parent = null, bool $recursive = true): Db\Result { public function folderList(string $user, $parent = null, bool $recursive = true): Db\Result {
// if the user isn't authorized to perform this action then throw an exception. // if the user isn't authorized to perform this action then throw an exception.
if (!Arsse::$user->authorize($user, __FUNCTION__)) { if (!Arsse::$user->authorize($user, __FUNCTION__)) {
throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]); throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
} }
// check to make sure the parent exists, if one is specified // check to make sure the parent exists, if one is specified
if (!is_null($parent)) { $parent = $this->folderValidateId($user, $parent)['id'];
if (!$this->db->prepare("SELECT count(*) from arsse_folders where owner is ? and id is ?", "str", "int")->run($user, $parent)->getValue()) {
throw new Db\ExceptionInput("idMissing", ["action" => __FUNCTION__, "field" => "parent", 'id' => $parent]);
}
}
// if we're not returning a recursive list we can use a simpler query // if we're not returning a recursive list we can use a simpler query
if (!$recursive) { if (!$recursive) {
return $this->db->prepare("SELECT id,name,parent from arsse_folders where owner is ? and parent is ?", "str", "int")->run($user, $parent); return $this->db->prepare("SELECT id,name,parent from arsse_folders where owner is ? and parent is ?", "str", "int")->run($user, $parent);
@ -313,10 +309,13 @@ class Database {
} }
} }
public function folderRemove(string $user, int $id): bool { public function folderRemove(string $user, $id): bool {
if (!Arsse::$user->authorize($user, __FUNCTION__)) { if (!Arsse::$user->authorize($user, __FUNCTION__)) {
throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]); throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
} }
if (!ValueInfo::id($id)) {
throw new Db\ExceptionInput("typeViolation", ["action" => __FUNCTION__, "field" => "folder", 'id' => $id, 'type' => "int > 0"]);
}
$changes = $this->db->prepare("DELETE FROM arsse_folders where owner is ? and id is ?", "str", "int")->run($user, $id)->changes(); $changes = $this->db->prepare("DELETE FROM arsse_folders where owner is ? and id is ?", "str", "int")->run($user, $id)->changes();
if (!$changes) { if (!$changes) {
throw new Db\ExceptionInput("subjectMissing", ["action" => __FUNCTION__, "field" => "folder", 'id' => $id]); throw new Db\ExceptionInput("subjectMissing", ["action" => __FUNCTION__, "field" => "folder", 'id' => $id]);
@ -324,10 +323,13 @@ class Database {
return true; return true;
} }
public function folderPropertiesGet(string $user, int $id): array { public function folderPropertiesGet(string $user, $id): array {
if (!Arsse::$user->authorize($user, __FUNCTION__)) { if (!Arsse::$user->authorize($user, __FUNCTION__)) {
throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]); throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
} }
if (!ValueInfo::id($id)) {
throw new Db\ExceptionInput("typeViolation", ["action" => __FUNCTION__, "field" => "folder", 'id' => $id, 'type' => "int > 0"]);
}
$props = $this->db->prepare("SELECT id,name,parent from arsse_folders where owner is ? and id is ?", "str", "int")->run($user, $id)->getRow(); $props = $this->db->prepare("SELECT id,name,parent from arsse_folders where owner is ? and id is ?", "str", "int")->run($user, $id)->getRow();
if (!$props) { if (!$props) {
throw new Db\ExceptionInput("subjectMissing", ["action" => __FUNCTION__, "field" => "folder", 'id' => $id]); throw new Db\ExceptionInput("subjectMissing", ["action" => __FUNCTION__, "field" => "folder", 'id' => $id]);
@ -335,7 +337,7 @@ class Database {
return $props; return $props;
} }
public function folderPropertiesSet(string $user, int $id, array $data): bool { public function folderPropertiesSet(string $user, $id, array $data): bool {
if (!Arsse::$user->authorize($user, __FUNCTION__)) { if (!Arsse::$user->authorize($user, __FUNCTION__)) {
throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]); throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
} }
@ -347,14 +349,18 @@ class Database {
// if a new name and parent are specified, validate both together // if a new name and parent are specified, validate both together
$this->folderValidateName($data['name']); $this->folderValidateName($data['name']);
$in['name'] = $data['name']; $in['name'] = $data['name'];
$in['parent'] = $this->folderValidateMove($user, $id, $data['parent'], $data['name']); $in['parent'] = $this->folderValidateMove($user, (int) $id, $data['parent'], $data['name']);
} elseif ($name) { } elseif ($name) {
// if we're trying to rename the root folder, this simply fails
if (!$id) {
return false;
}
// if a new name is specified, validate it // if a new name is specified, validate it
$this->folderValidateName($data['name'], true, $in['parent']); $this->folderValidateName($data['name'], true, $in['parent']);
$in['name'] = $data['name']; $in['name'] = $data['name'];
} elseif ($parent) { } elseif ($parent) {
// if a new parent is specified, validate it // if a new parent is specified, validate it
$in['parent'] = $this->folderValidateMove($user, $id, $data['parent']); $in['parent'] = $this->folderValidateMove($user, (int) $id, $data['parent']);
} else { } else {
// if neither was specified, do nothing // if neither was specified, do nothing
return false; return false;
@ -368,14 +374,13 @@ class Database {
} }
protected function folderValidateId(string $user, $id = null, bool $subject = false): array { protected function folderValidateId(string $user, $id = null, bool $subject = false): array {
$idInfo = ValueInfo::int($id); // if the specified ID is not a non-negative integer (or null), this will always fail
if ($idInfo & (ValueInfo::NULL | ValueInfo::ZERO)) { if (!ValueInfo::id($id, true)) {
// if a null or zero ID is specified this is a no-op throw new Db\ExceptionInput("typeViolation", ["action" => $this->caller(), "field" => "folder", 'type' => "int >= 0"]);
return ['id' => null, 'name' => null, 'parent' => null];
} }
// if a negative integer or non-integer is specified this will always fail // if a null or zero ID is specified this is a no-op
if (!($idInfo & ValueInfo::VALID) || (($idInfo & ValueInfo::NEG))) { if (!$id) {
throw new Db\ExceptionInput($subject ? "subjectMissing" : "idMissing", ["action" => $this->caller(), "field" => "folder", 'id' => $id]); return ['id' => null, 'name' => null, 'parent' => null];
} }
// check whether the folder exists and is owned by the user // check whether the folder exists and is owned by the user
$f = $this->db->prepare("SELECT id,name,parent from arsse_folders where owner is ? and id is ?", "str", "int")->run($user, $id)->getRow(); $f = $this->db->prepare("SELECT id,name,parent from arsse_folders where owner is ? and id is ?", "str", "int")->run($user, $id)->getRow();
@ -406,7 +411,9 @@ class Database {
if ($id==$parent) { if ($id==$parent) {
throw new Db\ExceptionInput("circularDependence", $errData); throw new Db\ExceptionInput("circularDependence", $errData);
} }
// make sure both that the prospective parent exists, and that the it is not one of its children (a circular dependence) // make sure both that the prospective parent exists, and that the it is not one of its children (a circular dependence);
// also make sure that a folder with the same prospective name and parent does not already exist: if the parent is null,
// SQL will happily accept duplicates (null is not unique), so we must do this check ourselves
$p = $this->db->prepare( $p = $this->db->prepare(
"WITH RECURSIVE "WITH RECURSIVE
target as (select ? as user, ? as source, ? as dest, ? as rename), target as (select ? as user, ? as source, ? as dest, ? as rename),
@ -416,7 +423,7 @@ class Database {
((select dest from target) is null or exists(select id from arsse_folders join target on owner is user and id is dest)) as extant, ((select dest from target) is null or exists(select id from arsse_folders join target on owner is user and id is dest)) as extant,
not exists(select id from folders where id is (select dest from target)) as valid, not exists(select id from folders where id is (select dest from target)) as valid,
not exists(select id from arsse_folders join target on parent is dest and name is coalesce((select rename from target),(select name from arsse_folders join target on id is source))) as available not exists(select id from arsse_folders join target on parent is dest and name is coalesce((select rename from target),(select name from arsse_folders join target on id is source))) as available
", "str", "int", "int","str" ", "str", "int", "int", "str"
)->run($user, $id, $parent, $name)->getRow(); )->run($user, $id, $parent, $name)->getRow();
if (!$p['extant']) { if (!$p['extant']) {
// if the parent doesn't exist or doesn't below to the user, throw an exception // if the parent doesn't exist or doesn't below to the user, throw an exception
@ -425,6 +432,7 @@ class Database {
// if using the desired parent would create a circular dependence, throw a different exception // if using the desired parent would create a circular dependence, throw a different exception
throw new Db\ExceptionInput("circularDependence", $errData); throw new Db\ExceptionInput("circularDependence", $errData);
} elseif (!$p['available']) { } elseif (!$p['available']) {
// if a folder with the same parent and name already exists, throw another different exception
throw new Db\ExceptionInput("constraintViolation", ["action" => $this->caller(), "field" => (is_null($name) ? "parent" : "name")]); throw new Db\ExceptionInput("constraintViolation", ["action" => $this->caller(), "field" => (is_null($name) ? "parent" : "name")]);
} }
return $parent; return $parent;
@ -438,7 +446,10 @@ class Database {
throw new Db\ExceptionInput("whitespace", ["action" => $this->caller(), "field" => "name"]); throw new Db\ExceptionInput("whitespace", ["action" => $this->caller(), "field" => "name"]);
} elseif (!($info & ValueInfo::VALID)) { } elseif (!($info & ValueInfo::VALID)) {
throw new Db\ExceptionInput("typeViolation", ["action" => $this->caller(), "field" => "name", 'type' => "string"]); throw new Db\ExceptionInput("typeViolation", ["action" => $this->caller(), "field" => "name", 'type' => "string"]);
} elseif($checkDuplicates) { } elseif ($checkDuplicates) {
// make sure that a folder with the same prospective name and parent does not already exist: if the parent is null,
// SQL will happily accept duplicates (null is not unique), so we must do this check ourselves
$parent = $parent ? $parent : null;
if ($this->db->prepare("SELECT exists(select id from arsse_folders where parent is ? and name is ?)", "int", "str")->run($parent, $name)->getValue()) { if ($this->db->prepare("SELECT exists(select id from arsse_folders where parent is ? and name is ?)", "int", "str")->run($parent, $name)->getValue()) {
throw new Db\ExceptionInput("constraintViolation", ["action" => $this->caller(), "field" => "name"]); throw new Db\ExceptionInput("constraintViolation", ["action" => $this->caller(), "field" => "name"]);
} }
@ -470,10 +481,12 @@ class Database {
return $this->db->prepare('INSERT INTO arsse_subscriptions(owner,feed) values(?,?)', 'str', 'int')->run($user, $feedID)->lastId(); return $this->db->prepare('INSERT INTO arsse_subscriptions(owner,feed) values(?,?)', 'str', 'int')->run($user, $feedID)->lastId();
} }
public function subscriptionList(string $user, int $folder = null, int $id = null): Db\Result { public function subscriptionList(string $user, $folder = null, int $id = null): Db\Result {
if (!Arsse::$user->authorize($user, __FUNCTION__)) { if (!Arsse::$user->authorize($user, __FUNCTION__)) {
throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]); throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
} }
// validate inputs
$folder = $this->folderValidateId($user, $folder)['id'];
// create a complex query // create a complex query
$q = new Query( $q = new Query(
"SELECT "SELECT
@ -492,13 +505,11 @@ class Database {
$q->setCTE("user(user)", "SELECT ?", "str", $user); // the subject user; this way we only have to pass it to prepare() once $q->setCTE("user(user)", "SELECT ?", "str", $user); // the subject user; this way we only have to pass it to prepare() once
// topmost folders belonging to the user // topmost folders belonging to the user
$q->setCTE("topmost(f_id,top)", "SELECT id,id from arsse_folders join user on owner is user where parent is null union select id,top from arsse_folders join topmost on parent=f_id"); $q->setCTE("topmost(f_id,top)", "SELECT id,id from arsse_folders join user on owner is user where parent is null union select id,top from arsse_folders join topmost on parent=f_id");
if (!is_null($id)) { if ($id) {
// this condition facilitates the implementation of subscriptionPropertiesGet, which would otherwise have to duplicate the complex query; it takes precedence over a specified folder // this condition facilitates the implementation of subscriptionPropertiesGet, which would otherwise have to duplicate the complex query; it takes precedence over a specified folder
// if an ID is specified, add a suitable WHERE condition and bindings // if an ID is specified, add a suitable WHERE condition and bindings
$q->setWhere("arsse_subscriptions.id is ?", "int", $id); $q->setWhere("arsse_subscriptions.id is ?", "int", $id);
} elseif ($folder) { } elseif ($folder) {
// if a folder is specified, make sure it exists
$this->folderValidateId($user, $folder);
// if it does exist, add a common table expression to list it and its children so that we select from the entire subtree // if it does exist, add a common table expression to list it and its children so that we select from the entire subtree
$q->setCTE("folders(folder)", "SELECT ? union select id from arsse_folders join folders on parent is folder", "int", $folder); $q->setCTE("folders(folder)", "SELECT ? union select id from arsse_folders join folders on parent is folder", "int", $folder);
// add a suitable WHERE condition // add a suitable WHERE condition
@ -507,24 +518,30 @@ class Database {
return $this->db->prepare($q->getQuery(), $q->getTypes())->run($q->getValues()); return $this->db->prepare($q->getQuery(), $q->getTypes())->run($q->getValues());
} }
public function subscriptionRemove(string $user, int $id): bool { public function subscriptionRemove(string $user, $id): bool {
if (!Arsse::$user->authorize($user, __FUNCTION__)) { if (!Arsse::$user->authorize($user, __FUNCTION__)) {
throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]); throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
} }
if (!ValueInfo::id($id)) {
throw new Db\ExceptionInput("typeViolation", ["action" => __FUNCTION__, "field" => "feed", 'id' => $id, 'type' => "int > 0"]);
}
$changes = $this->db->prepare("DELETE from arsse_subscriptions where owner is ? and id is ?", "str", "int")->run($user, $id)->changes(); $changes = $this->db->prepare("DELETE from arsse_subscriptions where owner is ? and id is ?", "str", "int")->run($user, $id)->changes();
if (!$changes) { if (!$changes) {
throw new Db\ExceptionInput("subjectMissing", ["action" => __FUNCTION__, "field" => "folder", 'id' => $id]); throw new Db\ExceptionInput("subjectMissing", ["action" => __FUNCTION__, "field" => "feed", 'id' => $id]);
} }
return true; return true;
} }
public function subscriptionPropertiesGet(string $user, int $id): array { public function subscriptionPropertiesGet(string $user, $id): array {
if (!Arsse::$user->authorize($user, __FUNCTION__)) { if (!Arsse::$user->authorize($user, __FUNCTION__)) {
throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]); throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
} }
if (!ValueInfo::id($id)) {
throw new Db\ExceptionInput("typeViolation", ["action" => __FUNCTION__, "field" => "feed", 'id' => $id, 'type' => "int > 0"]);
}
// disable authorization checks for the list call // disable authorization checks for the list call
Arsse::$user->authorizationEnabled(false); Arsse::$user->authorizationEnabled(false);
$sub = $this->subscriptionList($user, null, $id)->getRow(); $sub = $this->subscriptionList($user, null, (int) $id)->getRow();
Arsse::$user->authorizationEnabled(true); Arsse::$user->authorizationEnabled(true);
if (!$sub) { if (!$sub) {
throw new Db\ExceptionInput("subjectMissing", ["action" => __FUNCTION__, "field" => "feed", 'id' => $id]); throw new Db\ExceptionInput("subjectMissing", ["action" => __FUNCTION__, "field" => "feed", 'id' => $id]);
@ -532,15 +549,13 @@ class Database {
return $sub; return $sub;
} }
public function subscriptionPropertiesSet(string $user, int $id, array $data): bool { public function subscriptionPropertiesSet(string $user, $id, array $data): bool {
if (!Arsse::$user->authorize($user, __FUNCTION__)) { if (!Arsse::$user->authorize($user, __FUNCTION__)) {
throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]); throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
} }
$tr = $this->db->begin(); $tr = $this->db->begin();
if (!$this->db->prepare("SELECT count(*) from arsse_subscriptions where owner is ? and id is ?", "str", "int")->run($user, $id)->getValue()) { // validate the ID
// if the ID doesn't exist or doesn't belong to the user, throw an exception $id = $this->subscriptionValidateId($user, $id, true)['id'];
throw new Db\ExceptionInput("subjectMissing", ["action" => __FUNCTION__, "field" => "feed", 'id' => $id]);
}
if (array_key_exists("folder", $data)) { if (array_key_exists("folder", $data)) {
// ensure the target folder exists and belong to the user // ensure the target folder exists and belong to the user
$data['folder'] = $this->folderValidateId($user, $data['folder'])['id']; $data['folder'] = $this->folderValidateId($user, $data['folder'])['id'];
@ -570,10 +585,13 @@ class Database {
return $out; return $out;
} }
protected function subscriptionValidateId(string $user, int $id): array { protected function subscriptionValidateId(string $user, $id, bool $subject = false): array {
$out = $this->db->prepare("SELECT feed from arsse_subscriptions where id is ? and owner is ?", "int", "str")->run($id, $user)->getRow(); if (!ValueInfo::id($id)) {
throw new Db\ExceptionInput("typeViolation", ["action" => $this->caller(), "field" => "feed", 'id' => $id, 'type' => "int > 0"]);
}
$out = $this->db->prepare("SELECT id,feed from arsse_subscriptions where id is ? and owner is ?", "int", "str")->run($id, $user)->getRow();
if (!$out) { if (!$out) {
throw new Db\ExceptionInput("idMissing", ["action" => $this->caller(), "field" => "subscription", 'id' => $id]); throw new Db\ExceptionInput($subject ? "subjectMissing" : "idMissing", ["action" => $this->caller(), "field" => "subscription", 'id' => $id]);
} }
return $out; return $out;
} }
@ -583,9 +601,12 @@ class Database {
return array_column($feeds, 'id'); return array_column($feeds, 'id');
} }
public function feedUpdate(int $feedID, bool $throwError = false): bool { public function feedUpdate($feedID, bool $throwError = false): bool {
$tr = $this->db->begin(); $tr = $this->db->begin();
// check to make sure the feed exists // check to make sure the feed exists
if (!ValueInfo::id($feedID)) {
throw new Db\ExceptionInput("typeViolation", ["action" => __FUNCTION__, "field" => "feed", 'id' => $feedID, 'type' => "int > 0"]);
}
$f = $this->db->prepare("SELECT url, username, password, modified, etag, err_count, scrape FROM arsse_feeds where id is ?", "int")->run($feedID)->getRow(); $f = $this->db->prepare("SELECT url, username, password, modified, etag, err_count, scrape FROM arsse_feeds where id is ?", "int")->run($feedID)->getRow();
if (!$f) { if (!$f) {
throw new Db\ExceptionInput("subjectMissing", ["action" => __FUNCTION__, "field" => "feed", 'id' => $feedID]); throw new Db\ExceptionInput("subjectMissing", ["action" => __FUNCTION__, "field" => "feed", 'id' => $feedID]);
@ -596,7 +617,7 @@ class Database {
// here. When an exception is thrown it should update the database with the // here. When an exception is thrown it should update the database with the
// error instead of failing; if other exceptions are thrown, we should simply roll back // error instead of failing; if other exceptions are thrown, we should simply roll back
try { try {
$feed = new Feed($feedID, $f['url'], (string) Date::transform($f['modified'], "http", "sql"), $f['etag'], $f['username'], $f['password'], $scrape); $feed = new Feed((int) $feedID, $f['url'], (string) Date::transform($f['modified'], "http", "sql"), $f['etag'], $f['username'], $f['password'], $scrape);
if (!$feed->modified) { if (!$feed->modified) {
// if the feed hasn't changed, just compute the next fetch time and record it // if the feed hasn't changed, just compute the next fetch time and record it
$this->db->prepare("UPDATE arsse_feeds SET updated = CURRENT_TIMESTAMP, next_fetch = ? WHERE id is ?", 'datetime', 'int')->run($feed->nextFetch, $feedID); $this->db->prepare("UPDATE arsse_feeds SET updated = CURRENT_TIMESTAMP, next_fetch = ? WHERE id is ?", 'datetime', 'int')->run($feed->nextFetch, $feedID);
@ -1004,7 +1025,10 @@ class Database {
return true; return true;
} }
protected function articleValidateId(string $user, int $id): array { protected function articleValidateId(string $user, $id): array {
if (!ValueInfo::id($id)) {
throw new Db\ExceptionInput("typeViolation", ["action" => $this->caller(), "field" => "article", 'id' => $id, 'type' => "int > 0"]); // @codeCoverageIgnore
}
$out = $this->db->prepare( $out = $this->db->prepare(
"SELECT "SELECT
arsse_articles.id as article, arsse_articles.id as article,
@ -1023,6 +1047,9 @@ class Database {
} }
protected function articleValidateEdition(string $user, int $id): array { protected function articleValidateEdition(string $user, int $id): array {
if (!ValueInfo::id($id)) {
throw new Db\ExceptionInput("typeViolation", ["action" => $this->caller(), "field" => "edition", 'id' => $id, 'type' => "int > 0"]); // @codeCoverageIgnore
}
$out = $this->db->prepare( $out = $this->db->prepare(
"SELECT "SELECT
arsse_editions.id as edition, arsse_editions.id as edition,

2
lib/Db/SQLite3/Driver.php

@ -20,7 +20,7 @@ class Driver extends \JKingWeb\Arsse\Db\AbstractDriver {
// check to make sure required extension is loaded // check to make sure required extension is loaded
if (!class_exists("SQLite3")) { if (!class_exists("SQLite3")) {
throw new Exception("extMissing", self::driverName()); // @codeCoverageIgnore throw new Exception("extMissing", self::driverName()); // @codeCoverageIgnore
} }
// if no database file is specified in the configuration, use a suitable default // if no database file is specified in the configuration, use a suitable default
$dbFile = Arsse::$conf->dbSQLite3File ?? \JKingWeb\Arsse\BASE."arsse.db"; $dbFile = Arsse::$conf->dbSQLite3File ?? \JKingWeb\Arsse\BASE."arsse.db";
$mode = \SQLITE3_OPEN_READWRITE | \SQLITE3_OPEN_CREATE; $mode = \SQLITE3_OPEN_READWRITE | \SQLITE3_OPEN_CREATE;

2
lib/Misc/Context.php

@ -37,7 +37,7 @@ class Context {
protected function cleanArray(array $spec): array { protected function cleanArray(array $spec): array {
$spec = array_values($spec); $spec = array_values($spec);
for ($a = 0; $a < sizeof($spec); $a++) { for ($a = 0; $a < sizeof($spec); $a++) {
if(ValueInfo::int($spec[$a])===ValueInfo::VALID) { if (ValueInfo::id($spec[$a])) {
$spec[$a] = (int) $spec[$a]; $spec[$a] = (int) $spec[$a];
} else { } else {
$spec[$a] = 0; $spec[$a] = 0;

63
lib/Misc/ValueInfo.php

@ -13,33 +13,34 @@ class ValueInfo {
const EMPTY = 1 << 2; const EMPTY = 1 << 2;
const WHITE = 1 << 3; const WHITE = 1 << 3;
static public function int($value): int { public static function int($value): int {
$out = 0; $out = 0;
// check if the input is null
if (is_null($value)) { if (is_null($value)) {
$out += self::NULL; // check if the input is null
} return self::NULL;
// normalize the value to an integer or float if possible } elseif (is_string($value) || (is_object($value) && method_exists($value, "__toString"))) {
if (is_string($value)) { $value = (string) $value;
if (strval(@intval($value))===$value) { // normalize a string an integer or float if possible
if (!strlen($value)) {
// the empty string is equivalent to null when evaluating an integer
return self::NULL;
} elseif (filter_var($value, \FILTER_VALIDATE_FLOAT) !== false && !fmod((float) $value, 1)) {
// an integral float is acceptable
$value = (int) $value; $value = (int) $value;
} elseif (strval(@floatval($value))===$value) { } else {
$value = (float) $value; return $out;
}
// the empty string is equivalent to null when evaluating an integer
if (!strlen((string) $value)) {
$out += self::NULL;
} }
} } elseif (is_float($value) && !fmod($value, 1)) {
// if the value is not an integer or integral float, stop // an integral float is acceptable
if (!is_int($value) && (!is_float($value) || fmod($value, 1))) { $value = (int) $value;
} elseif (!is_int($value)) {
// if the value is not an integer or integral float, stop
return $out; return $out;
} }
// mark validity // mark validity
$value = (int) $value;
$out += self::VALID; $out += self::VALID;
// mark zeroness // mark zeroness
if(!$value) { if ($value==0) {
$out += self::ZERO; $out += self::ZERO;
} }
// mark negativeness // mark negativeness
@ -49,14 +50,17 @@ class ValueInfo {
return $out; return $out;
} }
static public function str($value): int { public static function str($value): int {
$out = 0; $out = 0;
// check if the input is null // check if the input is null
if (is_null($value)) { if (is_null($value)) {
$out += self::NULL; $out += self::NULL;
} }
// if the value is not scalar, it cannot be valid if (is_object($value) && method_exists($value, "__toString")) {
if (!is_scalar($value)) { // if the value is an object which has a __toString method, this is acceptable
$value = (string) $value;
} elseif (!is_scalar($value) || is_bool($value) || (is_float($value) && !is_finite($value))) {
// otherwise if the value is not scalar, is a boolean, or is infinity or NaN, it cannot be valid
return $out; return $out;
} }
// mark validity // mark validity
@ -70,4 +74,19 @@ class ValueInfo {
} }
return $out; return $out;
} }
}
public static function id($value, bool $allowNull = false): bool {
$info = self::int($value);
if ($allowNull && ($info & self::NULL)) { // null (and allowed)
return true;
} elseif (!($info & self::VALID)) { // not an integer
return false;
} elseif ($info & self::NEG) { // negative integer
return false;
} elseif (!$allowNull && ($info & self::ZERO)) { // zero (and not allowed)
return false;
} else { // non-negative integer
return true;
}
}
}

37
lib/REST/AbstractHandler.php

@ -32,10 +32,6 @@ abstract class AbstractHandler implements Handler {
return $data; return $data;
} }
protected function validateInt($id): bool {
return (bool) (ValueInfo::int($id) & ValueInfo::VALID);
}
protected function NormalizeInput(array $data, array $types, string $dateFormat = null): array { protected function NormalizeInput(array $data, array $types, string $dateFormat = null): array {
$out = []; $out = [];
foreach ($data as $key => $value) { foreach ($data as $key => $value) {
@ -49,34 +45,29 @@ abstract class AbstractHandler implements Handler {
} }
switch ($types[$key]) { switch ($types[$key]) {
case "int": case "int":
if ($this->validateInt($value)) { if (valueInfo::int($value) & ValueInfo::VALID) {
$out[$key] = (int) $value; $out[$key] = (int) $value;
} }
break; break;
case "string": case "string":
$out[$key] = (string) $value; if (is_bool($value)) {
$out[$key] = var_export($value, true);
} elseif (!is_scalar($value)) {
break;
} else {
$out[$key] = (string) $value;
}
break; break;
case "bool": case "bool":
if (is_bool($value)) { $test = filter_var($value, \FILTER_VALIDATE_BOOLEAN, \FILTER_NULL_ON_FAILURE);
$out[$key] = $value; if (!is_null($test)) {
} elseif ($this->validateInt($value)) { $out[$key] = $test;
$value = (int) $value;
if ($value > -1 && $value < 2) {
$out[$key] = $value;
}
} elseif (is_string($value)) {
$value = trim(strtolower($value));
if ($value=="false") {
$out[$key] = false;
}
if ($value=="true") {
$out[$key] = true;
}
} }
break; break;
case "float": case "float":
if (is_numeric($value)) { $test = filter_var($value, \FILTER_VALIDATE_FLOAT);
$out[$key] = (float) $value; if ($test !== false) {
$out[$key] = $test;
} }
break; break;
case "datetime": case "datetime":

55
lib/REST/NextCloudNews/V1_2.php

@ -6,6 +6,7 @@ use JKingWeb\Arsse\Arsse;
use JKingWeb\Arsse\User; use JKingWeb\Arsse\User;
use JKingWeb\Arsse\Service; use JKingWeb\Arsse\Service;
use JKingWeb\Arsse\Misc\Context; use JKingWeb\Arsse\Misc\Context;
use JKingWeb\Arsse\Misc\ValueInfo;
use JKingWeb\Arsse\AbstractException; use JKingWeb\Arsse\AbstractException;
use JKingWeb\Arsse\Db\ExceptionInput; use JKingWeb\Arsse\Db\ExceptionInput;
use JKingWeb\Arsse\Feed\Exception as FeedException; use JKingWeb\Arsse\Feed\Exception as FeedException;
@ -78,7 +79,7 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler {
// dispatch // dispatch
try { try {
return $this->$func($req->paths, $data); return $this->$func($req->paths, $data);
// @codeCoverageIgnoreStart // @codeCoverageIgnoreStart
} catch (Exception $e) { } catch (Exception $e) {
// if there was a REST exception return 400 // if there was a REST exception return 400
return new Response(400); return new Response(400);
@ -94,15 +95,15 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler {
'items' => [], 'items' => [],
'folders' => [ 'folders' => [
'' => ['GET' => "folderList", 'POST' => "folderAdd"], '' => ['GET' => "folderList", 'POST' => "folderAdd"],
'0' => ['PUT' => "folderRename", 'DELETE' => "folderRemove"], '1' => ['PUT' => "folderRename", 'DELETE' => "folderRemove"],
'0/read' => ['PUT' => "folderMarkRead"], '1/read' => ['PUT' => "folderMarkRead"],
], ],
'feeds' => [ 'feeds' => [
'' => ['GET' => "subscriptionList", 'POST' => "subscriptionAdd"], '' => ['GET' => "subscriptionList", 'POST' => "subscriptionAdd"],
'0' => ['DELETE' => "subscriptionRemove"], '1' => ['DELETE' => "subscriptionRemove"],
'0/move' => ['PUT' => "subscriptionMove"], '1/move' => ['PUT' => "subscriptionMove"],
'0/rename' => ['PUT' => "subscriptionRename"], '1/rename' => ['PUT' => "subscriptionRename"],
'0/read' => ['PUT' => "subscriptionMarkRead"], '1/read' => ['PUT' => "subscriptionMarkRead"],
'all' => ['GET' => "feedListStale"], 'all' => ['GET' => "feedListStale"],
'update' => ['GET' => "feedUpdate"], 'update' => ['GET' => "feedUpdate"],
], ],
@ -110,12 +111,12 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler {
'' => ['GET' => "articleList"], '' => ['GET' => "articleList"],
'updated' => ['GET' => "articleList"], 'updated' => ['GET' => "articleList"],
'read' => ['PUT' => "articleMarkReadAll"], 'read' => ['PUT' => "articleMarkReadAll"],
'0/read' => ['PUT' => "articleMarkRead"], '1/read' => ['PUT' => "articleMarkRead"],
'0/unread' => ['PUT' => "articleMarkRead"], '1/unread' => ['PUT' => "articleMarkRead"],
'read/multiple' => ['PUT' => "articleMarkReadMulti"], 'read/multiple' => ['PUT' => "articleMarkReadMulti"],
'unread/multiple' => ['PUT' => "articleMarkReadMulti"], 'unread/multiple' => ['PUT' => "articleMarkReadMulti"],
'0/0/star' => ['PUT' => "articleMarkStarred"], '1/1/star' => ['PUT' => "articleMarkStarred"],
'0/0/unstar' => ['PUT' => "articleMarkStarred"], '1/1/unstar' => ['PUT' => "articleMarkStarred"],
'star/multiple' => ['PUT' => "articleMarkStarredMulti"], 'star/multiple' => ['PUT' => "articleMarkStarredMulti"],
'unstar/multiple' => ['PUT' => "articleMarkStarredMulti"], 'unstar/multiple' => ['PUT' => "articleMarkStarredMulti"],
], ],
@ -135,10 +136,10 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler {
]; ];
// the first path element is the overall scope of the request // the first path element is the overall scope of the request
$scope = $url[0]; $scope = $url[0];
// any URL components which are only digits should be replaced with "0", for easier comparison (integer segments are IDs, and we don't care about the specific ID) // 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++) { for ($a = 0; $a < sizeof($url); $a++) {
if ($this->validateInt($url[$a])) { if (ValueInfo::id($url[$a])) {
$url[$a] = "0"; $url[$a] = "1";
} }
} }
// normalize the HTTP method to uppercase // normalize the HTTP method to uppercase
@ -336,7 +337,14 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler {
try { try {
Arsse::$db->feedUpdate($data['feedId']); Arsse::$db->feedUpdate($data['feedId']);
} catch (ExceptionInput $e) { } catch (ExceptionInput $e) {
return new Response(404); switch ($e->getCode()) {
case 10239: // feed does not exist
return new Response(404);
case 10237: // feed ID invalid
return new Response(422);
default: // other errors related to input
return new Response(400); // @codeCoverageIgnore
}
} }
return new Response(204); return new Response(204);
} }
@ -347,8 +355,8 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler {
if (!isset($data['url'])) { if (!isset($data['url'])) {
return new Response(422); return new Response(422);
} }
// normalize the folder ID, if specified; zero should be transformed to null // normalize the folder ID, if specified
$folder = (isset($data['folderId']) && $data['folderId']) ? $data['folderId'] : null; $folder = isset($data['folderId']) ? $data['folderId'] : null;
// try to add the feed // try to add the feed
$tr = Arsse::$db->begin(); $tr = Arsse::$db->begin();
try { try {
@ -446,12 +454,13 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler {
Arsse::$db->subscriptionPropertiesSet(Arsse::$user->id, (int) $url[1], $in); Arsse::$db->subscriptionPropertiesSet(Arsse::$user->id, (int) $url[1], $in);
} catch (ExceptionInput $e) { } catch (ExceptionInput $e) {
switch ($e->getCode()) { switch ($e->getCode()) {
// subscription does not exist case 10239: // subscription does not exist
case 10239: return new Response(404); return new Response(404);
// folder does not exist case 10235: // folder does not exist
case 10235: return new Response(422); case 10237: // folder ID is invalid
// other errors related to input return new Response(422);
default: return new Response(400); // @codeCoverageIgnore default: // other errors related to input
return new Response(400); // @codeCoverageIgnore
} }
} }
return new Response(204); return new Response(204);

8
lib/REST/TinyTinyRSS/API.php

@ -58,7 +58,7 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler {
$method = strtolower($method); $method = strtolower($method);
$map = get_class_methods($this); $map = get_class_methods($this);
$map = array_combine(array_map("strtolower", $map), $map); $map = array_combine(array_map("strtolower", $map), $map);
if(!array_key_exists($method, $map)) { if (!array_key_exists($method, $map)) {
// if the method really doesn't exist, throw an exception // if the method really doesn't exist, throw an exception
throw new Exception("UNKNWON_METHOD", ['method' => $data['op']]); throw new Exception("UNKNWON_METHOD", ['method' => $data['op']]);
} }
@ -113,7 +113,7 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler {
if (isset($data['user']) && isset($data['password']) && Arsse::$user->auth($data['user'], $data['password'])) { if (isset($data['user']) && isset($data['password']) && Arsse::$user->auth($data['user'], $data['password'])) {
$id = Arsse::$db->sessionCreate($data['user']); $id = Arsse::$db->sessionCreate($data['user']);
return [ return [
'session_id' => $id, 'session_id' => $id,
'api_level' => self::LEVEL 'api_level' => self::LEVEL
]; ];
} else { } else {
@ -144,7 +144,7 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler {
} catch (ExceptionInput $e) { } catch (ExceptionInput $e) {
switch ($e->getCode()) { switch ($e->getCode()) {
// folder already exists // folder already exists
case 10236: case 10236:
// retrieve the ID of the existing folder; duplicating a category silently returns the existing one // retrieve the ID of the existing folder; duplicating a category silently returns the existing one
$folders = Arsse::$db->folderList(Arsse::$user->id, $in['parent'], false); $folders = Arsse::$db->folderList(Arsse::$user->id, $in['parent'], false);
foreach ($folders as $folder) { foreach ($folders as $folder) {
@ -159,4 +159,4 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler {
} }
} }
} }
} }

2
lib/REST/TinyTinyRSS/Exception.php

@ -14,4 +14,4 @@ class Exception extends \Exception {
$err = ['error' => $this->getMessage()]; $err = ['error' => $this->getMessage()];
return array_merge($err, $this->data, $err); return array_merge($err, $this->data, $err);
} }
} }

4
tests/Misc/TestContext.php

@ -47,9 +47,9 @@ class TestContext extends Test\AbstractTest {
$this->assertInstanceOf(Context::class, $c->$method($v[$method])); $this->assertInstanceOf(Context::class, $c->$method($v[$method]));
$this->assertTrue($c->$method()); $this->assertTrue($c->$method());
if (in_array($method, $times)) { if (in_array($method, $times)) {
$this->assertTime($c->$method, $v[$method]); $this->assertTime($c->$method, $v[$method], "Context method $method did not return the expected results");
} else { } else {
$this->assertSame($c->$method, $v[$method]); $this->assertSame($c->$method, $v[$method], "Context method $method did not return the expected results");
} }
} }
} }

204
tests/Misc/TestValueInfo.php

@ -0,0 +1,204 @@
<?php
declare(strict_types=1);
namespace JKingWeb\Arsse;
use JKingWeb\Arsse\Misc\ValueInfo as I;
use JKingWeb\Arsse\Test\Misc\StrClass;
/** @covers \JKingWeb\Arsse\Misc\ValueInfo */
class TestValueInfo extends Test\AbstractTest {
public function testGetIntegerInfo() {
$tests = [
[null, I::NULL],
["", I::NULL],
[1, I::VALID],
[PHP_INT_MAX, I::VALID],
[1.0, I::VALID],
["1.0", I::VALID],
["001.0", I::VALID],
["1.0e2", I::VALID],
["1", I::VALID],
["001", I::VALID],
["1e2", I::VALID],
["+1.0", I::VALID],
["+001.0", I::VALID],
["+1.0e2", I::VALID],
["+1", I::VALID],
["+001", I::VALID],
["+1e2", I::VALID],
[0, I::VALID | I::ZERO],
["0", I::VALID | I::ZERO],
["000", I::VALID | I::ZERO],
[0.0, I::VALID | I::ZERO],
["0.0", I::VALID | I::ZERO],
["000.000", I::VALID | I::ZERO],
["+0", I::VALID | I::ZERO],
["+000", I::VALID | I::ZERO],
["+0.0", I::VALID | I::ZERO],
["+000.000", I::VALID | I::ZERO],
[-1, I::VALID | I::NEG],
[-1.0, I::VALID | I::NEG],
["-1.0", I::VALID | I::NEG],
["-001.0", I::VALID | I::NEG],
["-1.0e2", I::VALID | I::NEG],
["-1", I::VALID | I::NEG],
["-001", I::VALID | I::NEG],
["-1e2", I::VALID | I::NEG],
[-0, I::VALID | I::ZERO],
["-0", I::VALID | I::ZERO],
["-000", I::VALID | I::ZERO],
[-0.0, I::VALID | I::ZERO],
["-0.0", I::VALID | I::ZERO],
["-000.000", I::VALID | I::ZERO],
[false, 0],
[true, 0],
[INF, 0],
[-INF, 0],
[NAN, 0],
[[], 0],
["some string", 0],
[" ", 0],
[new \StdClass, 0],
[new StrClass(""), I::NULL],
[new StrClass("1"), I::VALID],
[new StrClass("0"), I::VALID | I::ZERO],
[new StrClass("-1"), I::VALID | I::NEG],
[new StrClass("Msg"), 0],
[new StrClass(" "), 0],
];
foreach ($tests as $test) {
list($value, $exp) = $test;
$this->assertSame($exp, I::int($value), "Test returned ".decbin(I::int($value))." for value: ".var_export($value, true));
}
}
public function testGetStringInfo() {
$tests = [
[null, I::NULL],
["", I::VALID | I::EMPTY],
[1, I::VALID],
[PHP_INT_MAX, I::VALID],
[1.0, I::VALID],
["1.0", I::VALID],
["001.0", I::VALID],
["1.0e2", I::VALID],
["1", I::VALID],
["001", I::VALID],
["1e2", I::VALID],
["+1.0", I::VALID],
["+001.0", I::VALID],
["+1.0e2", I::VALID],
["+1", I::VALID],
["+001", I::VALID],
["+1e2", I::VALID],
[0, I::VALID],
["0", I::VALID],
["000", I::VALID],
[0.0, I::VALID],
["0.0", I::VALID],
["000.000", I::VALID],
["+0", I::VALID],
["+000", I::VALID],
["+0.0", I::VALID],
["+000.000", I::VALID],
[-1, I::VALID],
[-1.0, I::VALID],
["-1.0", I::VALID],
["-001.0", I::VALID],
["-1.0e2", I::VALID],
["-1", I::VALID],
["-001", I::VALID],
["-1e2", I::VALID],
[-0, I::VALID],
["-0", I::VALID],
["-000", I::VALID],
[-0.0, I::VALID],
["-0.0", I::VALID],
["-000.000", I::VALID],
[false, 0],
[true, 0],
[INF, 0],
[-INF, 0],
[NAN, 0],
[[], 0],
["some string", I::VALID],
[" ", I::VALID | I::WHITE],
[new \StdClass, 0],
[new StrClass(""), I::VALID | I::EMPTY],
[new StrClass("1"), I::VALID],
[new StrClass("0"), I::VALID],
[new StrClass("-1"), I::VALID],
[new StrClass("Msg"), I::VALID],
[new StrClass(" "), I::VALID | I::WHITE],
];
foreach ($tests as $test) {
list($value, $exp) = $test;
$this->assertSame($exp, I::str($value), "Test returned ".decbin(I::str($value))." for value: ".var_export($value, true));
}
}
public function testValidateDatabaseIdentifier() {
$tests = [
[null, false, true],
["", false, true],
[1, true, true],
[PHP_INT_MAX, true, true],
[1.0, true, true],
["1.0", true, true],
["001.0", true, true],
["1.0e2", true, true],
["1", true, true],
["001", true, true],
["1e2", true, true],
["+1.0", true, true],
["+001.0", true, true],
["+1.0e2", true, true],
["+1", true, true],
["+001", true, true],
["+1e2", true, true],
[0, false, true],
["0", false, true],
["000", false, true],
[0.0, false, true],
["0.0", false, true],
["000.000", false, true],
["+0", false, true],
["+000", false, true],
["+0.0", false, true],
["+000.000", false, true],
[-1, false, false],
[-1.0, false, false],
["-1.0", false, false],
["-001.0", false, false],
["-1.0e2", false, false],
["-1", false, false],
["-001", false, false],
["-1e2", false, false],
[-0, false, true],
["-0", false, true],
["-000", false, true],
[-0.0, false, true],
["-0.0", false, true],
["-000.000", false, true],
[false, false, false],
[true, false, false],
[INF, false, false],
[-INF, false, false],
[NAN, false, false],
[[], false, false],
["some string", false, false],
[" ", false, false],
[new \StdClass, false, false],
[new StrClass(""), false, true],
[new StrClass("1"), true, true],
[new StrClass("0"), false, true],
[new StrClass("-1"), false, false],
[new StrClass("Msg"), false, false],
[new StrClass(" "), false, false],
];
foreach ($tests as $test) {
list($value, $exp, $expNull) = $test;
$this->assertSame($exp, I::id($value), "Non-null test failed for value: ".var_export($value, true));
$this->assertSame($expNull, I::id($value, true), "Null test failed for value: ".var_export($value, true));
}
}
}

97
tests/REST/NextCloudNews/TestNCNV1_2.php

@ -46,6 +46,21 @@ class TestNCNV1_2 extends Test\AbstractTest {
'title' => 'Second example feed', 'title' => 'Second example feed',
'unread' => 23, 'unread' => 23,
], ],
[
'id' => 47,
'url' => 'http://example.net/news.atom',
'favicon' => 'http://example.net/favicon.png',
'source' => 'http://example.net/',
'folder' => null,
'top_folder' => null,
'pinned' => 0,
'err_count' => 0,
'err_msg' => '',
'order_type' => 1,
'added' => '2017-05-20 13:35:54',
'title' => 'Third example feed',
'unread' => 0,
],
], ],
'rest' => [ 'rest' => [
[ [
@ -76,6 +91,20 @@ class TestNCNV1_2 extends Test\AbstractTest {
'title' => 'Second example feed', 'title' => 'Second example feed',
'unreadCount' => 23, 'unreadCount' => 23,
], ],
[
'id' => 47,
'url' => 'http://example.net/news.atom',
'faviconLink' => 'http://example.net/favicon.png',
'link' => 'http://example.net/',
'folderId' => 0,
'pinned' => false,
'updateErrorCount' => 0,
'lastUpdateError' => '',
'ordering' => 1,
'added' => 1495287354,
'title' => 'Third example feed',
'unreadCount' => 0,
],
], ],
]; ];
protected $articles = [ protected $articles = [
@ -331,7 +360,7 @@ class TestNCNV1_2 extends Test\AbstractTest {
$this->assertEquals($exp, $this->h->dispatch(new Request("PUT", "/folders/1", '<data/>', 'application/json'))); $this->assertEquals($exp, $this->h->dispatch(new Request("PUT", "/folders/1", '<data/>', 'application/json')));
} }
public function testReceiveAuthenticationChallenge() { public function testSendAuthenticationChallenge() {
Phake::when(Arsse::$user)->authHTTP->thenReturn(false); Phake::when(Arsse::$user)->authHTTP->thenReturn(false);
$exp = new Response(401, "", "", ['WWW-Authenticate: Basic realm="'.REST\NextCloudNews\V1_2::REALM.'"']); $exp = new Response(401, "", "", ['WWW-Authenticate: Basic realm="'.REST\NextCloudNews\V1_2::REALM.'"']);
$this->assertEquals($exp, $this->h->dispatch(new Request("GET", "/"))); $this->assertEquals($exp, $this->h->dispatch(new Request("GET", "/")));
@ -381,6 +410,7 @@ class TestNCNV1_2 extends Test\AbstractTest {
$this->assertEquals($exp, $this->h->dispatch(new Request("POST", "/folders"))); $this->assertEquals($exp, $this->h->dispatch(new Request("POST", "/folders")));
$this->assertEquals($exp, $this->h->dispatch(new Request("POST", "/folders", '{"name":""}', 'application/json'))); $this->assertEquals($exp, $this->h->dispatch(new Request("POST", "/folders", '{"name":""}', 'application/json')));
$this->assertEquals($exp, $this->h->dispatch(new Request("POST", "/folders", '{"name":" "}', 'application/json'))); $this->assertEquals($exp, $this->h->dispatch(new Request("POST", "/folders", '{"name":" "}', 'application/json')));
$this->assertEquals($exp, $this->h->dispatch(new Request("POST", "/folders", '{"name":{}}', 'application/json')));
// try adding the same two folders again // try adding the same two folders again
$exp = new Response(409); $exp = new Response(409);
$this->assertEquals($exp, $this->h->dispatch(new Request("POST", "/folders?name=Software"))); $this->assertEquals($exp, $this->h->dispatch(new Request("POST", "/folders?name=Software")));
@ -457,26 +487,29 @@ class TestNCNV1_2 extends Test\AbstractTest {
$in = [ $in = [
['url' => "http://example.com/news.atom", 'folderId' => 3], ['url' => "http://example.com/news.atom", 'folderId' => 3],
['url' => "http://example.org/news.atom", 'folderId' => 8], ['url' => "http://example.org/news.atom", 'folderId' => 8],
['url' => "http://example.net/news.atom", 'folderId' => 0], ['url' => "http://example.net/news.atom", 'folderId' => 8],
['url' => "http://example.net/news.atom", 'folderId' => -1],
[], [],
]; ];
$out = [ $out = [
['feeds' => [$this->feeds['rest'][0]]], ['feeds' => [$this->feeds['rest'][0]]],
['feeds' => [$this->feeds['rest'][1]], 'newestItemId' => 4758915], ['feeds' => [$this->feeds['rest'][1]], 'newestItemId' => 4758915],
[], ['feeds' => [$this->feeds['rest'][2]], 'newestItemId' => 2112],
[],
]; ];
// set up the necessary mocks // set up the necessary mocks
Phake::when(Arsse::$db)->subscriptionAdd(Arsse::$user->id, "http://example.com/news.atom")->thenReturn(2112)->thenThrow(new ExceptionInput("constraintViolation")); // error on the second call Phake::when(Arsse::$db)->subscriptionAdd(Arsse::$user->id, "http://example.com/news.atom")->thenReturn(2112)->thenThrow(new ExceptionInput("constraintViolation")); // error on the second call
Phake::when(Arsse::$db)->subscriptionAdd(Arsse::$user->id, "http://example.org/news.atom")->thenReturn(42)->thenThrow(new ExceptionInput("constraintViolation")); // error on the second call Phake::when(Arsse::$db)->subscriptionAdd(Arsse::$user->id, "http://example.org/news.atom")->thenReturn(42)->thenThrow(new ExceptionInput("constraintViolation")); // error on the second call
Phake::when(Arsse::$db)->subscriptionPropertiesGet(Arsse::$user->id, 2112)->thenReturn($this->feeds['db'][0]); Phake::when(Arsse::$db)->subscriptionPropertiesGet(Arsse::$user->id, 2112)->thenReturn($this->feeds['db'][0]);
Phake::when(Arsse::$db)->subscriptionPropertiesGet(Arsse::$user->id, 42)->thenReturn($this->feeds['db'][1]); Phake::when(Arsse::$db)->subscriptionPropertiesGet(Arsse::$user->id, 42)->thenReturn($this->feeds['db'][1]);
Phake::when(Arsse::$db)->subscriptionPropertiesGet(Arsse::$user->id, 47)->thenReturn($this->feeds['db'][2]);
Phake::when(Arsse::$db)->editionLatest(Arsse::$user->id, (new Context)->subscription(2112))->thenReturn(0); Phake::when(Arsse::$db)->editionLatest(Arsse::$user->id, (new Context)->subscription(2112))->thenReturn(0);
Phake::when(Arsse::$db)->editionLatest(Arsse::$user->id, (new Context)->subscription(42))->thenReturn(4758915); Phake::when(Arsse::$db)->editionLatest(Arsse::$user->id, (new Context)->subscription(42))->thenReturn(4758915);
Phake::when(Arsse::$db)->subscriptionPropertiesSet(Arsse::$user->id, 2112, ['folder' => 3])->thenThrow(new ExceptionInput("idMissing")); // folder ID 3 does not exist Phake::when(Arsse::$db)->editionLatest(Arsse::$user->id, (new Context)->subscription(47))->thenReturn(2112);
Phake::when(Arsse::$db)->subscriptionPropertiesSet(Arsse::$user->id, 42, ['folder' => 8])->thenReturn(true); Phake::when(Arsse::$db)->subscriptionPropertiesSet(Arsse::$user->id, 2112, ['folder' => 3])->thenThrow(new ExceptionInput("idMissing")); // folder ID 3 does not exist
// set up a mock for a bad feed Phake::when(Arsse::$db)->subscriptionPropertiesSet(Arsse::$user->id, 42, ['folder' => 8])->thenReturn(true);
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())); Phake::when(Arsse::$db)->subscriptionPropertiesSet(Arsse::$user->id, 47, ['folder' => -1])->thenThrow(new ExceptionInput("typeViolation")); // folder ID -1 is invalid
// 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 // add the subscriptions
$exp = new Response(200, $out[0]); $exp = new Response(200, $out[0]);
$this->assertEquals($exp, $this->h->dispatch(new Request("POST", "/feeds", json_encode($in[0]), 'application/json'))); $this->assertEquals($exp, $this->h->dispatch(new Request("POST", "/feeds", json_encode($in[0]), 'application/json')));
@ -485,14 +518,16 @@ class TestNCNV1_2 extends Test\AbstractTest {
// try to add them a second time // try to add them a second time
$exp = new Response(409); $exp = new Response(409);
$this->assertEquals($exp, $this->h->dispatch(new Request("POST", "/feeds", json_encode($in[0]), 'application/json'))); $this->assertEquals($exp, $this->h->dispatch(new Request("POST", "/feeds", json_encode($in[0]), 'application/json')));
$exp = new Response(409);
$this->assertEquals($exp, $this->h->dispatch(new Request("POST", "/feeds", json_encode($in[1]), 'application/json'))); $this->assertEquals($exp, $this->h->dispatch(new Request("POST", "/feeds", json_encode($in[1]), 'application/json')));
// try to add a bad feed // try to add a bad feed
$exp = new Response(422); $exp = new Response(422);
$this->assertEquals($exp, $this->h->dispatch(new Request("POST", "/feeds", json_encode($in[2]), 'application/json'))); $this->assertEquals($exp, $this->h->dispatch(new Request("POST", "/feeds", json_encode($in[2]), 'application/json')));
// try again (this will succeed), with an invalid folder ID
$exp = new Response(200, $out[2]);
$this->assertEquals($exp, $this->h->dispatch(new Request("POST", "/feeds", json_encode($in[3]), 'application/json')));
// try to add no feed // try to add no feed
$exp = new Response(422); $exp = new Response(422);
$this->assertEquals($exp, $this->h->dispatch(new Request("POST", "/feeds", json_encode($in[3]), 'application/json'))); $this->assertEquals($exp, $this->h->dispatch(new Request("POST", "/feeds", json_encode($in[4]), 'application/json')));
} }
public function testRemoveASubscription() { public function testRemoveASubscription() {
@ -511,11 +546,13 @@ class TestNCNV1_2 extends Test\AbstractTest {
['folderId' => 42], ['folderId' => 42],
['folderId' => 2112], ['folderId' => 2112],
['folderId' => 42], ['folderId' => 42],
['folderId' => -1],
[], [],
]; ];
Phake::when(Arsse::$db)->subscriptionPropertiesSet(Arsse::$user->id, 1, ['folder' => 42])->thenReturn(true); Phake::when(Arsse::$db)->subscriptionPropertiesSet(Arsse::$user->id, 1, ['folder' => 42])->thenReturn(true);
Phake::when(Arsse::$db)->subscriptionPropertiesSet(Arsse::$user->id, 1, ['folder' => null])->thenReturn(true); Phake::when(Arsse::$db)->subscriptionPropertiesSet(Arsse::$user->id, 1, ['folder' => null])->thenReturn(true);
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' => 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 Phake::when(Arsse::$db)->subscriptionPropertiesSet(Arsse::$user->id, 42, $this->anything())->thenThrow(new ExceptionInput("subjectMissing")); // subscription does not exist
$exp = new Response(204); $exp = new Response(204);
$this->assertEquals($exp, $this->h->dispatch(new Request("PUT", "/feeds/1/move", json_encode($in[0]), 'application/json'))); $this->assertEquals($exp, $this->h->dispatch(new Request("PUT", "/feeds/1/move", json_encode($in[0]), 'application/json')));
@ -527,6 +564,8 @@ class TestNCNV1_2 extends Test\AbstractTest {
$this->assertEquals($exp, $this->h->dispatch(new Request("PUT", "/feeds/42/move", json_encode($in[3]), 'application/json'))); $this->assertEquals($exp, $this->h->dispatch(new Request("PUT", "/feeds/42/move", json_encode($in[3]), 'application/json')));
$exp = new Response(422); $exp = new Response(422);
$this->assertEquals($exp, $this->h->dispatch(new Request("PUT", "/feeds/1/move", json_encode($in[4]), 'application/json'))); $this->assertEquals($exp, $this->h->dispatch(new Request("PUT", "/feeds/1/move", json_encode($in[4]), 'application/json')));
$exp = new Response(422);
$this->assertEquals($exp, $this->h->dispatch(new Request("PUT", "/feeds/1/move", json_encode($in[5]), 'application/json')));
} }
public function testRenameASubscription() { public function testRenameASubscription() {
@ -584,18 +623,20 @@ class TestNCNV1_2 extends Test\AbstractTest {
['feedId' => 42], // valid ['feedId' => 42], // valid
['feedId' => 2112], // feed does not exist ['feedId' => 2112], // feed does not exist
['feedId' => "ook"], // invalid ID ['feedId' => "ook"], // invalid ID
['feedId' => -1], // invalid ID
['feed' => 42], // invalid input ['feed' => 42], // invalid input
]; ];
Phake::when(Arsse::$db)->feedUpdate(42)->thenReturn(true); Phake::when(Arsse::$db)->feedUpdate(42)->thenReturn(true);
Phake::when(Arsse::$db)->feedUpdate(2112)->thenThrow(new ExceptionInput("subjectMissing")); Phake::when(Arsse::$db)->feedUpdate(2112)->thenThrow(new ExceptionInput("subjectMissing"));
Phake::when(Arsse::$db)->feedUpdate(-1)->thenThrow(new ExceptionInput("typeViolation"));
$exp = new Response(204); $exp = new Response(204);
$this->assertEquals($exp, $this->h->dispatch(new Request("GET", "/feeds/update", json_encode($in[0]), 'application/json'))); $this->assertEquals($exp, $this->h->dispatch(new Request("GET", "/feeds/update", json_encode($in[0]), 'application/json')));
$exp = new Response(404); $exp = new Response(404);
$this->assertEquals($exp, $this->h->dispatch(new Request("GET", "/feeds/update", json_encode($in[1]), 'application/json'))); $this->assertEquals($exp, $this->h->dispatch(new Request("GET", "/feeds/update", json_encode($in[1]), 'application/json')));
$exp = new Response(422); $exp = new Response(422);
$this->assertEquals($exp, $this->h->dispatch(new Request("GET", "/feeds/update", json_encode($in[2]), 'application/json'))); $this->assertEquals($exp, $this->h->dispatch(new Request("GET", "/feeds/update", json_encode($in[2]), 'application/json')));
$exp = new Response(422);
$this->assertEquals($exp, $this->h->dispatch(new Request("GET", "/feeds/update", json_encode($in[3]), 'application/json'))); $this->assertEquals($exp, $this->h->dispatch(new Request("GET", "/feeds/update", json_encode($in[3]), 'application/json')));
$this->assertEquals($exp, $this->h->dispatch(new Request("GET", "/feeds/update", json_encode($in[4]), 'application/json')));
// updating a feed when not an admin fails // updating a feed when not an admin fails
Phake::when(Arsse::$user)->rightsGet->thenReturn(0); Phake::when(Arsse::$user)->rightsGet->thenReturn(0);
$exp = new Response(403); $exp = new Response(403);
@ -606,13 +647,15 @@ class TestNCNV1_2 extends Test\AbstractTest {
$res = new Result($this->articles['db']); $res = new Result($this->articles['db']);
$t = new \DateTime; $t = new \DateTime;
$in = [ $in = [
['type' => 0, 'id' => 42], ['type' => 0, 'id' => 42], // type=0 => subscription/feed
['type' => 1, 'id' => 2112], ['type' => 1, 'id' => 2112], // type=1 => folder
['type' => 2, 'id' => 0], ['type' => 0, 'id' => -1], // type=0 => subscription/feed; invalid ID
['type' => 3, 'id' => 0], ['type' => 1, 'id' => -1], // type=1 => folder; invalid ID
['type' => 2, 'id' => 0], // type=2 => starred
['type' => 3, 'id' => 0], // type=3 => all (default); base context
['oldestFirst' => true, 'batchSize' => 10, 'offset' => 5], ['oldestFirst' => true, 'batchSize' => 10, 'offset' => 5],
['oldestFirst' => false, 'batchSize' => 5, 'offset' => 5], ['oldestFirst' => false, 'batchSize' => 5, 'offset' => 5],
['getRead' => true], ['getRead' => true], // base context
['getRead' => false], ['getRead' => false],
['lastModified' => $t->getTimestamp()], ['lastModified' => $t->getTimestamp()],
['oldestFirst' => false, 'batchSize' => 5, 'offset' => 0], // offset=0 should not set the latestEdition context ['oldestFirst' => false, 'batchSize' => 5, 'offset' => 0], // offset=0 should not set the latestEdition context
@ -620,6 +663,8 @@ class TestNCNV1_2 extends Test\AbstractTest {
Phake::when(Arsse::$db)->articleList(Arsse::$user->id, $this->anything())->thenReturn($res); Phake::when(Arsse::$db)->articleList(Arsse::$user->id, $this->anything())->thenReturn($res);
Phake::when(Arsse::$db)->articleList(Arsse::$user->id, (new Context)->reverse(true)->subscription(42))->thenThrow(new ExceptionInput("idMissing")); Phake::when(Arsse::$db)->articleList(Arsse::$user->id, (new Context)->reverse(true)->subscription(42))->thenThrow(new ExceptionInput("idMissing"));
Phake::when(Arsse::$db)->articleList(Arsse::$user->id, (new Context)->reverse(true)->folder(2112))->thenThrow(new ExceptionInput("idMissing")); Phake::when(Arsse::$db)->articleList(Arsse::$user->id, (new Context)->reverse(true)->folder(2112))->thenThrow(new ExceptionInput("idMissing"));
Phake::when(Arsse::$db)->articleList(Arsse::$user->id, (new Context)->reverse(true)->subscription(-1))->thenThrow(new ExceptionInput("typeViolation"));
Phake::when(Arsse::$db)->articleList(Arsse::$user->id, (new Context)->reverse(true)->folder(-1))->thenThrow(new ExceptionInput("typeViolation"));
$exp = new Response(200, ['items' => $this->articles['rest']]); $exp = new Response(200, ['items' => $this->articles['rest']]);
// check the contents of the response // check the contents of the response
$this->assertEquals($exp, $this->h->dispatch(new Request("GET", "/items"))); // first instance of base context $this->assertEquals($exp, $this->h->dispatch(new Request("GET", "/items"))); // first instance of base context
@ -628,22 +673,26 @@ class TestNCNV1_2 extends Test\AbstractTest {
$exp = new Response(422); $exp = new Response(422);
$this->assertEquals($exp, $this->h->dispatch(new Request("GET", "/items", json_encode($in[0]), 'application/json'))); $this->assertEquals($exp, $this->h->dispatch(new Request("GET", "/items", json_encode($in[0]), 'application/json')));
$this->assertEquals($exp, $this->h->dispatch(new Request("GET", "/items", json_encode($in[1]), 'application/json'))); $this->assertEquals($exp, $this->h->dispatch(new Request("GET", "/items", json_encode($in[1]), 'application/json')));
$this->assertEquals($exp, $this->h->dispatch(new Request("GET", "/items", json_encode($in[2]), 'application/json')));
$this->assertEquals($exp, $this->h->dispatch(new Request("GET", "/items", json_encode($in[3]), 'application/json')));
// simply run through the remainder of the input for later method verification // simply run through the remainder of the input for later method verification
$this->h->dispatch(new Request("GET", "/items", json_encode($in[2]), 'application/json'));
$this->h->dispatch(new Request("GET", "/items", json_encode($in[3]), 'application/json')); // third instance of base context
$this->h->dispatch(new Request("GET", "/items", json_encode($in[4]), 'application/json')); $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')); $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')); // fourth 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[7]), 'application/json'));
$this->h->dispatch(new Request("GET", "/items", json_encode($in[8]), '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[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'));
// perform method verifications // perform method verifications
Phake::verify(Arsse::$db, Phake::times(4))->articleList(Arsse::$user->id, (new Context)->reverse(true)); Phake::verify(Arsse::$db, Phake::times(4))->articleList(Arsse::$user->id, (new Context)->reverse(true));
Phake::verify(Arsse::$db)->articleList(Arsse::$user->id, (new Context)->reverse(true)->subscription(42)); Phake::verify(Arsse::$db)->articleList(Arsse::$user->id, (new Context)->reverse(true)->subscription(42));
Phake::verify(Arsse::$db)->articleList(Arsse::$user->id, (new Context)->reverse(true)->folder(2112)); Phake::verify(Arsse::$db)->articleList(Arsse::$user->id, (new Context)->reverse(true)->folder(2112));
Phake::verify(Arsse::$db)->articleList(Arsse::$user->id, (new Context)->reverse(true)->subscription(-1));
Phake::verify(Arsse::$db)->articleList(Arsse::$user->id, (new Context)->reverse(true)->folder(-1));
Phake::verify(Arsse::$db)->articleList(Arsse::$user->id, (new Context)->reverse(true)->starred(true)); Phake::verify(Arsse::$db)->articleList(Arsse::$user->id, (new Context)->reverse(true)->starred(true));
Phake::verify(Arsse::$db)->articleList(Arsse::$user->id, (new Context)->reverse(false)->limit(10)->oldestEdition(6)); Phake::verify(Arsse::$db)->articleList(Arsse::$user->id, (new Context)->reverse(false)->limit(10)->oldestEdition(6)); // offset is one more than specified
Phake::verify(Arsse::$db)->articleList(Arsse::$user->id, (new Context)->reverse(true)->limit(5)->latestEdition(4)); Phake::verify(Arsse::$db)->articleList(Arsse::$user->id, (new Context)->reverse(true)->limit(5)->latestEdition(4)); // offset is one less than specified
Phake::verify(Arsse::$db)->articleList(Arsse::$user->id, (new Context)->reverse(true)->unread(true)); Phake::verify(Arsse::$db)->articleList(Arsse::$user->id, (new Context)->reverse(true)->unread(true));
Phake::verify(Arsse::$db)->articleList(Arsse::$user->id, (new Context)->reverse(true)->modifiedSince($t)); Phake::verify(Arsse::$db)->articleList(Arsse::$user->id, (new Context)->reverse(true)->modifiedSince($t));
Phake::verify(Arsse::$db)->articleList(Arsse::$user->id, (new Context)->reverse(true)->limit(5)); Phake::verify(Arsse::$db)->articleList(Arsse::$user->id, (new Context)->reverse(true)->limit(5));

6
tests/REST/TinyTinyRSS/TestTinyTinyAPI.php

@ -270,7 +270,7 @@ class TestTinyTinyAPI extends Test\AbstractTest {
Phake::when(Arsse::$db)->folderAdd(Arsse::$user->id, $db[0])->thenReturn(2)->thenThrow(new ExceptionInput("constraintViolation")); // error on the second call Phake::when(Arsse::$db)->folderAdd(Arsse::$user->id, $db[0])->thenReturn(2)->thenThrow(new ExceptionInput("constraintViolation")); // error on the second call
Phake::when(Arsse::$db)->folderAdd(Arsse::$user->id, $db[1])->thenReturn(3)->thenThrow(new ExceptionInput("constraintViolation")); // error on the second call Phake::when(Arsse::$db)->folderAdd(Arsse::$user->id, $db[1])->thenReturn(3)->thenThrow(new ExceptionInput("constraintViolation")); // error on the second call
Phake::when(Arsse::$db)->folderList(Arsse::$user->id, null, false)->thenReturn(new Result([$out[0], $out[2]])); Phake::when(Arsse::$db)->folderList(Arsse::$user->id, null, false)->thenReturn(new Result([$out[0], $out[2]]));
Phake::when(Arsse::$db)->folderList(Arsse::$user->id, 1, false)->thenReturn(new Result([$out[1]])); Phake::when(Arsse::$db)->folderList(Arsse::$user->id, 1, false)->thenReturn(new Result([$out[1]]));
// set up mocks that produce errors // set up mocks that produce errors
Phake::when(Arsse::$db)->folderAdd(Arsse::$user->id, [])->thenThrow(new ExceptionInput("missing")); Phake::when(Arsse::$db)->folderAdd(Arsse::$user->id, [])->thenThrow(new ExceptionInput("missing"));
Phake::when(Arsse::$db)->folderAdd(Arsse::$user->id, ['name' => "", 'parent' => null])->thenThrow(new ExceptionInput("missing")); Phake::when(Arsse::$db)->folderAdd(Arsse::$user->id, ['name' => "", 'parent' => null])->thenThrow(new ExceptionInput("missing"));
@ -286,11 +286,11 @@ class TestTinyTinyAPI extends Test\AbstractTest {
$exp = $this->respGood(3); $exp = $this->respGood(3);
$this->assertEquals($exp, $this->h->dispatch(new Request("POST", "", json_encode($in[1])))); $this->assertEquals($exp, $this->h->dispatch(new Request("POST", "", json_encode($in[1]))));
Phake::verify(Arsse::$db)->folderList(Arsse::$user->id, null, false); Phake::verify(Arsse::$db)->folderList(Arsse::$user->id, null, false);
Phake::verify(Arsse::$db)->folderList(Arsse::$user->id, 1, false); Phake::verify(Arsse::$db)->folderList(Arsse::$user->id, 1, false);
// add some invalid folders // add some invalid folders
$exp = $this->respErr("INCORRECT_USAGE"); $exp = $this->respErr("INCORRECT_USAGE");
$this->assertEquals($exp, $this->h->dispatch(new Request("POST", "", json_encode($in[2])))); $this->assertEquals($exp, $this->h->dispatch(new Request("POST", "", json_encode($in[2]))));
$this->assertEquals($exp, $this->h->dispatch(new Request("POST", "", json_encode($in[3])))); $this->assertEquals($exp, $this->h->dispatch(new Request("POST", "", json_encode($in[3]))));
$this->assertEquals($exp, $this->h->dispatch(new Request("POST", "", json_encode($in[4])))); $this->assertEquals($exp, $this->h->dispatch(new Request("POST", "", json_encode($in[4]))));
} }
} }

4
tests/lib/AbstractTest.php

@ -25,10 +25,10 @@ abstract class AbstractTest extends \PHPUnit\Framework\TestCase {
} }
} }
public function assertTime($exp, $test) { public function assertTime($exp, $test, string $msg = null) {
$exp = Date::transform($exp, "iso8601"); $exp = Date::transform($exp, "iso8601");
$test = Date::transform($test, "iso8601"); $test = Date::transform($test, "iso8601");
$this->assertSame($exp, $test); $this->assertSame($exp, $test, $msg);
} }
public function clearData(bool $loadLang = true): bool { public function clearData(bool $loadLang = true): bool {

1
tests/lib/Database/SeriesCleanup.php

@ -192,6 +192,5 @@ trait SeriesCleanup {
unset($state['arsse_sessions']['rows'][$id - 1]); unset($state['arsse_sessions']['rows'][$id - 1]);
} }
$this->compareExpectations($state); $this->compareExpectations($state);
} }
} }

5
tests/lib/Database/SeriesFeed.php

@ -221,6 +221,11 @@ trait SeriesFeed {
Arsse::$db->feedUpdate(2112); Arsse::$db->feedUpdate(2112);
} }
public function testUpdateAnInvalidFeed() {
$this->assertException("typeViolation", "Db", "ExceptionInput");
Arsse::$db->feedUpdate(-1);
}
public function testUpdateAFeedThrowingExceptions() { public function testUpdateAFeedThrowingExceptions() {
$this->assertException("invalidUrl", "Feed"); $this->assertException("invalidUrl", "Feed");
Arsse::$db->feedUpdate(3, true); Arsse::$db->feedUpdate(3, true);

21
tests/lib/Database/SeriesFolder.php

@ -77,7 +77,7 @@ trait SeriesFolder {
} }
public function testAddANestedFolderToAnInvalidParent() { public function testAddANestedFolderToAnInvalidParent() {
$this->assertException("idMissing", "Db", "ExceptionInput"); $this->assertException("typeViolation", "Db", "ExceptionInput");
Arsse::$db->folderAdd("john.doe@example.com", ['name' => "Sociology", 'parent' => "stringFolderId"]); Arsse::$db->folderAdd("john.doe@example.com", ['name' => "Sociology", 'parent' => "stringFolderId"]);
} }
@ -184,6 +184,11 @@ trait SeriesFolder {
Arsse::$db->folderRemove("john.doe@example.com", 2112); Arsse::$db->folderRemove("john.doe@example.com", 2112);
} }
public function testRemoveAnInvalidFolder() {
$this->assertException("typeViolation", "Db", "ExceptionInput");
Arsse::$db->folderRemove("john.doe@example.com", -1);
}
public function testRemoveAFolderOfTheWrongOwner() { public function testRemoveAFolderOfTheWrongOwner() {
$this->assertException("subjectMissing", "Db", "ExceptionInput"); $this->assertException("subjectMissing", "Db", "ExceptionInput");
Arsse::$db->folderRemove("john.doe@example.com", 4); // folder ID 4 belongs to Jane Arsse::$db->folderRemove("john.doe@example.com", 4); // folder ID 4 belongs to Jane
@ -210,6 +215,11 @@ trait SeriesFolder {
Arsse::$db->folderPropertiesGet("john.doe@example.com", 2112); Arsse::$db->folderPropertiesGet("john.doe@example.com", 2112);
} }
public function testGetThePropertiesOfAnInvalidFolder() {
$this->assertException("typeViolation", "Db", "ExceptionInput");
Arsse::$db->folderPropertiesGet("john.doe@example.com", -1);
}
public function testGetThePropertiesOfAFolderOfTheWrongOwner() { public function testGetThePropertiesOfAFolderOfTheWrongOwner() {
$this->assertException("subjectMissing", "Db", "ExceptionInput"); $this->assertException("subjectMissing", "Db", "ExceptionInput");
Arsse::$db->folderPropertiesGet("john.doe@example.com", 4); // folder ID 4 belongs to Jane Arsse::$db->folderPropertiesGet("john.doe@example.com", 4); // folder ID 4 belongs to Jane
@ -233,6 +243,10 @@ trait SeriesFolder {
$this->compareExpectations($state); $this->compareExpectations($state);
} }
public function testRenameTheRootFolder() {
$this->assertFalse(Arsse::$db->folderPropertiesSet("john.doe@example.com", null, ['name' => "Opinion"]));
}
public function testRenameAFolderToTheEmptyString() { public function testRenameAFolderToTheEmptyString() {
$this->assertException("missing", "Db", "ExceptionInput"); $this->assertException("missing", "Db", "ExceptionInput");
$this->assertTrue(Arsse::$db->folderPropertiesSet("john.doe@example.com", 6, ['name' => ""])); $this->assertTrue(Arsse::$db->folderPropertiesSet("john.doe@example.com", 6, ['name' => ""]));
@ -296,6 +310,11 @@ trait SeriesFolder {
Arsse::$db->folderPropertiesSet("john.doe@example.com", 2112, ['parent' => null]); Arsse::$db->folderPropertiesSet("john.doe@example.com", 2112, ['parent' => null]);
} }
public function testSetThePropertiesOfAnInvalidFolder() {
$this->assertException("typeViolation", "Db", "ExceptionInput");
Arsse::$db->folderPropertiesSet("john.doe@example.com", -1, ['parent' => null]);
}
public function testSetThePropertiesOfAFolderForTheWrongOwner() { public function testSetThePropertiesOfAFolderForTheWrongOwner() {
$this->assertException("subjectMissing", "Db", "ExceptionInput"); $this->assertException("subjectMissing", "Db", "ExceptionInput");
Arsse::$db->folderPropertiesSet("john.doe@example.com", 4, ['parent' => null]); // folder ID 4 belongs to Jane Arsse::$db->folderPropertiesSet("john.doe@example.com", 4, ['parent' => null]); // folder ID 4 belongs to Jane

1
tests/lib/Database/SeriesSession.php

@ -7,7 +7,6 @@ use JKingWeb\Arsse\Misc\Date;
use Phake; use Phake;
trait SeriesSession { trait SeriesSession {
public function setUpSeries() { public function setUpSeries() {
// set up the test data // set up the test data
$past = gmdate("Y-m-d H:i:s", strtotime("now - 1 minute")); $past = gmdate("Y-m-d H:i:s", strtotime("now - 1 minute"));

17
tests/lib/Database/SeriesSubscription.php

@ -193,6 +193,11 @@ trait SeriesSubscription {
Arsse::$db->subscriptionRemove($this->user, 2112); Arsse::$db->subscriptionRemove($this->user, 2112);
} }
public function testRemoveAnInvalidSubscription() {
$this->assertException("typeViolation", "Db", "ExceptionInput");
Arsse::$db->subscriptionRemove($this->user, -1);
}
public function testRemoveASubscriptionForTheWrongOwner() { public function testRemoveASubscriptionForTheWrongOwner() {
$this->user = "jane.doe@example.com"; $this->user = "jane.doe@example.com";
$this->assertException("subjectMissing", "Db", "ExceptionInput"); $this->assertException("subjectMissing", "Db", "ExceptionInput");
@ -264,6 +269,11 @@ trait SeriesSubscription {
Arsse::$db->subscriptionPropertiesGet($this->user, 2112); Arsse::$db->subscriptionPropertiesGet($this->user, 2112);
} }
public function testGetThePropertiesOfAnInvalidSubscription() {
$this->assertException("typeViolation", "Db", "ExceptionInput");
Arsse::$db->subscriptionPropertiesGet($this->user, -1);
}
public function testGetThePropertiesOfASubscriptionWithoutAuthority() { public function testGetThePropertiesOfASubscriptionWithoutAuthority() {
Phake::when(Arsse::$user)->authorize->thenReturn(false); Phake::when(Arsse::$user)->authorize->thenReturn(false);
$this->assertException("notAuthorized", "User", "ExceptionAuthz"); $this->assertException("notAuthorized", "User", "ExceptionAuthz");
@ -311,7 +321,7 @@ trait SeriesSubscription {
} }
public function testRenameASubscriptionToFalse() { public function testRenameASubscriptionToFalse() {
$this->assertException("missing", "Db", "ExceptionInput"); $this->assertException("typeViolation", "Db", "ExceptionInput");
Arsse::$db->subscriptionPropertiesSet($this->user, 1, ['title' => false]); Arsse::$db->subscriptionPropertiesSet($this->user, 1, ['title' => false]);
} }
@ -329,6 +339,11 @@ trait SeriesSubscription {
Arsse::$db->subscriptionPropertiesSet($this->user, 2112, ['folder' => null]); Arsse::$db->subscriptionPropertiesSet($this->user, 2112, ['folder' => null]);
} }
public function testSetThePropertiesOfAnInvalidSubscription() {
$this->assertException("typeViolation", "Db", "ExceptionInput");
Arsse::$db->subscriptionPropertiesSet($this->user, -1, ['folder' => null]);
}
public function testSetThePropertiesOfASubscriptionWithoutAuthority() { public function testSetThePropertiesOfASubscriptionWithoutAuthority() {
Phake::when(Arsse::$user)->authorize->thenReturn(false); Phake::when(Arsse::$user)->authorize->thenReturn(false);
$this->assertException("notAuthorized", "User", "ExceptionAuthz"); $this->assertException("notAuthorized", "User", "ExceptionAuthz");

15
tests/lib/Misc/StrClass.php

@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace JKingWeb\Arsse\Test\Misc;
class StrClass {
public $str = "";
public function __construct($str) {
$this->str = (string) $str;
}
public function __toString() {
return $this->str;
}
}

7
tests/phpunit.xml

@ -31,6 +31,10 @@
<testsuite name="Configuration"> <testsuite name="Configuration">
<file>Conf/TestConf.php</file> <file>Conf/TestConf.php</file>
</testsuite> </testsuite>
<testsuite name="Sundry">
<file>Misc/TestValueInfo.php</file>
<file>Misc/TestContext.php</file>
</testsuite>
<testsuite name="User management"> <testsuite name="User management">
<file>User/TestUserMockInternal.php</file> <file>User/TestUserMockInternal.php</file>
<file>User/TestUserMockExternal.php</file> <file>User/TestUserMockExternal.php</file>
@ -41,9 +45,6 @@
<file>Feed/TestFeedFetching.php</file> <file>Feed/TestFeedFetching.php</file>
<file>Feed/TestFeed.php</file> <file>Feed/TestFeed.php</file>
</testsuite> </testsuite>
<testsuite name="Sundry">
<file>Misc/TestContext.php</file>
</testsuite>
<testsuite name="Database drivers"> <testsuite name="Database drivers">
<file>Db/TestTransaction.php</file> <file>Db/TestTransaction.php</file>
<file>Db/SQLite3/TestDbResultSQLite3.php</file> <file>Db/SQLite3/TestDbResultSQLite3.php</file>

Loading…
Cancel
Save