Browse Source

Multiple fixes to input sanitization

- Database functions now accept any input, but throw typeViolation exceptions where appropriate instead of idMissing or subjectMissing
- Added unit tests for the new Misc\ValueInfo static class
- Added ValueInfo::id() method to centrally validate database IDs, and made use of it consistently
- Made use of PHP's filter_var() function where appropriate when validating or sanitizing input
- Made the NCN protocol handler reject most invalid IDs before handing off to method handlers
- Made NCN's feedUpdate and subscriptionMove methods return 422 on invalid input
- Adjusted several tests to handler type violations
microsub
J. King 7 years ago
parent
commit
d365529493
  1. 95
      lib/Database.php
  2. 2
      lib/Misc/Context.php
  3. 53
      lib/Misc/ValueInfo.php
  4. 37
      lib/REST/AbstractHandler.php
  5. 53
      lib/REST/NextCloudNews/V1_2.php
  6. 4
      tests/Misc/TestContext.php
  7. 185
      tests/Misc/TestValueInfo.php
  8. 99
      tests/REST/NextCloudNews/TestNCNV1_2.php
  9. 4
      tests/lib/AbstractTest.php
  10. 5
      tests/lib/Database/SeriesFeed.php
  11. 21
      tests/lib/Database/SeriesFolder.php
  12. 17
      tests/lib/Database/SeriesSubscription.php
  13. 7
      tests/phpunit.xml

95
lib/Database.php

@ -238,17 +238,13 @@ class Database {
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 (!Arsse::$user->authorize($user, __FUNCTION__)) {
throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
}
// check to make sure the parent exists, if one is specified
if (!is_null($parent)) {
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]);
}
}
$parent = $this->folderValidateId($user, $parent)['id'];
// if we're not returning a recursive list we can use a simpler query
if (!$recursive) {
return $this->db->prepare("SELECT id,name,parent from arsse_folders where owner is ? and parent is ?", "str", "int")->run($user, $parent);
@ -260,10 +256,13 @@ class Database {
}
}
public function folderRemove(string $user, int $id): bool {
public function folderRemove(string $user, $id): bool {
if (!Arsse::$user->authorize($user, __FUNCTION__)) {
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();
if (!$changes) {
throw new Db\ExceptionInput("subjectMissing", ["action" => __FUNCTION__, "field" => "folder", 'id' => $id]);
@ -271,10 +270,13 @@ class Database {
return true;
}
public function folderPropertiesGet(string $user, int $id): array {
public function folderPropertiesGet(string $user, $id): array {
if (!Arsse::$user->authorize($user, __FUNCTION__)) {
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();
if (!$props) {
throw new Db\ExceptionInput("subjectMissing", ["action" => __FUNCTION__, "field" => "folder", 'id' => $id]);
@ -282,7 +284,7 @@ class Database {
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__)) {
throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
}
@ -294,14 +296,18 @@ class Database {
// if a new name and parent are specified, validate both together
$this->folderValidateName($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) {
// if we're trying to rename the root folder, this simply fails
if (!$id) {
return false;
}
// if a new name is specified, validate it
$this->folderValidateName($data['name'], true, $in['parent']);
$in['name'] = $data['name'];
} elseif ($parent) {
// 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 {
// if neither was specified, do nothing
return false;
@ -315,14 +321,13 @@ class Database {
}
protected function folderValidateId(string $user, $id = null, bool $subject = false): array {
$idInfo = ValueInfo::int($id);
if ($idInfo & (ValueInfo::NULL | ValueInfo::ZERO)) {
// if a null or zero ID is specified this is a no-op
return ['id' => null, 'name' => null, 'parent' => null];
// if the specified ID is not a non-negative integer (or null), this will always fail
if(!ValueInfo::id($id, true)) {
throw new Db\ExceptionInput("typeViolation", ["action" => $this->caller(), "field" => "folder", 'type' => "int >= 0"]);
}
// if a negative integer or non-integer is specified this will always fail
if (!($idInfo & ValueInfo::VALID) || (($idInfo & ValueInfo::NEG))) {
throw new Db\ExceptionInput($subject ? "subjectMissing" : "idMissing", ["action" => $this->caller(), "field" => "folder", 'id' => $id]);
// if a null or zero ID is specified this is a no-op
if (!$id) {
return ['id' => null, 'name' => null, 'parent' => null];
}
// 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();
@ -417,10 +422,12 @@ class Database {
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__)) {
throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
}
// validate inputs
$folder = $this->folderValidateId($user, $folder)['id'];
// create a complex query
$q = new Query(
"SELECT
@ -439,13 +446,11 @@ class Database {
$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
$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
// if an ID is specified, add a suitable WHERE condition and bindings
$q->setWhere("arsse_subscriptions.id is ?", "int", $id);
} 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
$q->setCTE("folders(folder)", "SELECT ? union select id from arsse_folders join folders on parent is folder", "int", $folder);
// add a suitable WHERE condition
@ -454,24 +459,30 @@ class Database {
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__)) {
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();
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;
}
public function subscriptionPropertiesGet(string $user, int $id): array {
public function subscriptionPropertiesGet(string $user, $id): array {
if (!Arsse::$user->authorize($user, __FUNCTION__)) {
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
Arsse::$user->authorizationEnabled(false);
$sub = $this->subscriptionList($user, null, $id)->getRow();
$sub = $this->subscriptionList($user, null, (int) $id)->getRow();
Arsse::$user->authorizationEnabled(true);
if (!$sub) {
throw new Db\ExceptionInput("subjectMissing", ["action" => __FUNCTION__, "field" => "feed", 'id' => $id]);
@ -479,15 +490,13 @@ class Database {
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__)) {
throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
}
$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()) {
// if the ID doesn't exist or doesn't belong to the user, throw an exception
throw new Db\ExceptionInput("subjectMissing", ["action" => __FUNCTION__, "field" => "feed", 'id' => $id]);
}
// validate the ID
$id = $this->subscriptionValidateId($user, $id, true)['id'];
if (array_key_exists("folder", $data)) {
// ensure the target folder exists and belong to the user
$data['folder'] = $this->folderValidateId($user, $data['folder'])['id'];
@ -517,10 +526,13 @@ class Database {
return $out;
}
protected function subscriptionValidateId(string $user, int $id): array {
$out = $this->db->prepare("SELECT feed from arsse_subscriptions where id is ? and owner is ?", "int", "str")->run($id, $user)->getRow();
protected function subscriptionValidateId(string $user, $id, bool $subject = false): array {
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) {
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;
}
@ -530,9 +542,12 @@ class Database {
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();
// 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();
if (!$f) {
throw new Db\ExceptionInput("subjectMissing", ["action" => __FUNCTION__, "field" => "feed", 'id' => $feedID]);
@ -543,7 +558,7 @@ class Database {
// 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
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 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);
@ -951,7 +966,10 @@ class Database {
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(
"SELECT
arsse_articles.id as article,
@ -970,6 +988,9 @@ class Database {
}
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(
"SELECT
arsse_editions.id as edition,

2
lib/Misc/Context.php

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

53
lib/Misc/ValueInfo.php

@ -15,31 +15,31 @@ class ValueInfo {
static public function int($value): int {
$out = 0;
// check if the input is null
if (is_null($value)) {
$out += self::NULL;
}
// normalize the value to an integer or float if possible
if (is_string($value)) {
if (strval(@intval($value))===$value) {
$value = (int) $value;
} elseif (strval(@floatval($value))===$value) {
$value = (float) $value;
}
// the empty string is equivalent to null when evaluating an integer
// check if the input is null
return self::NULL;
} elseif (is_string($value)) {
// normalize a string an integer or float if possible
if (!strlen((string) $value)) {
$out += self::NULL;
// 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;
} else {
return $out;
}
}
// if the value is not an integer or integral float, stop
if (!is_int($value) && (!is_float($value) || fmod($value, 1))) {
} elseif (is_float($value) && !fmod($value, 1)) {
// an integral float is acceptable
$value = (int) $value;
} elseif (!is_int($value)) {
// if the value is not an integer or integral float, stop
return $out;
}
// mark validity
$value = (int) $value;
$out += self::VALID;
// mark zeroness
if(!$value) {
if($value==0) {
$out += self::ZERO;
}
// mark negativeness
@ -55,8 +55,8 @@ class ValueInfo {
if (is_null($value)) {
$out += self::NULL;
}
// if the value is not scalar, it cannot be valid
if (!is_scalar($value)) {
// if the value is not scalar, is a boolean, or is infinity or NaN, it cannot be valid
if (!is_scalar($value) || is_bool($value) || (is_float($value) && !is_finite($value))) {
return $out;
}
// mark validity
@ -70,4 +70,19 @@ class ValueInfo {
}
return $out;
}
static public 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;
}
protected function validateInt($id): bool {
return (bool) (ValueInfo::int($id) & ValueInfo::VALID);
}
protected function NormalizeInput(array $data, array $types, string $dateFormat = null): array {
$out = [];
foreach ($data as $key => $value) {
@ -49,34 +45,29 @@ abstract class AbstractHandler implements Handler {
}
switch ($types[$key]) {
case "int":
if ($this->validateInt($value)) {
if (valueInfo::int($value) & ValueInfo::VALID) {
$out[$key] = (int) $value;
}
break;
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;
case "bool":
if (is_bool($value)) {
$out[$key] = $value;
} elseif ($this->validateInt($value)) {
$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;
}
$test = filter_var($value, \FILTER_VALIDATE_BOOLEAN, \FILTER_NULL_ON_FAILURE);
if (!is_null($test)) {
$out[$key] = $test;
}
break;
case "float":
if (is_numeric($value)) {
$out[$key] = (float) $value;
$test = filter_var($value, \FILTER_VALIDATE_FLOAT);
if ($test !== false) {
$out[$key] = $test;
}
break;
case "datetime":

53
lib/REST/NextCloudNews/V1_2.php

@ -6,6 +6,7 @@ use JKingWeb\Arsse\Arsse;
use JKingWeb\Arsse\User;
use JKingWeb\Arsse\Service;
use JKingWeb\Arsse\Misc\Context;
use JKingWeb\Arsse\Misc\ValueInfo;
use JKingWeb\Arsse\AbstractException;
use JKingWeb\Arsse\Db\ExceptionInput;
use JKingWeb\Arsse\Feed\Exception as FeedException;
@ -94,15 +95,15 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler {
'items' => [],
'folders' => [
'' => ['GET' => "folderList", 'POST' => "folderAdd"],
'0' => ['PUT' => "folderRename", 'DELETE' => "folderRemove"],
'0/read' => ['PUT' => "folderMarkRead"],
'1' => ['PUT' => "folderRename", 'DELETE' => "folderRemove"],
'1/read' => ['PUT' => "folderMarkRead"],
],
'feeds' => [
'' => ['GET' => "subscriptionList", 'POST' => "subscriptionAdd"],
'0' => ['DELETE' => "subscriptionRemove"],
'0/move' => ['PUT' => "subscriptionMove"],
'0/rename' => ['PUT' => "subscriptionRename"],
'0/read' => ['PUT' => "subscriptionMarkRead"],
'1' => ['DELETE' => "subscriptionRemove"],
'1/move' => ['PUT' => "subscriptionMove"],
'1/rename' => ['PUT' => "subscriptionRename"],
'1/read' => ['PUT' => "subscriptionMarkRead"],
'all' => ['GET' => "feedListStale"],
'update' => ['GET' => "feedUpdate"],
],
@ -110,12 +111,12 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler {
'' => ['GET' => "articleList"],
'updated' => ['GET' => "articleList"],
'read' => ['PUT' => "articleMarkReadAll"],
'0/read' => ['PUT' => "articleMarkRead"],
'0/unread' => ['PUT' => "articleMarkRead"],
'1/read' => ['PUT' => "articleMarkRead"],
'1/unread' => ['PUT' => "articleMarkRead"],
'read/multiple' => ['PUT' => "articleMarkReadMulti"],
'unread/multiple' => ['PUT' => "articleMarkReadMulti"],
'0/0/star' => ['PUT' => "articleMarkStarred"],
'0/0/unstar' => ['PUT' => "articleMarkStarred"],
'1/1/star' => ['PUT' => "articleMarkStarred"],
'1/1/unstar' => ['PUT' => "articleMarkStarred"],
'star/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
$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++) {
if ($this->validateInt($url[$a])) {
$url[$a] = "0";
if (ValueInfo::id($url[$a])) {
$url[$a] = "1";
}
}
// normalize the HTTP method to uppercase
@ -336,7 +337,14 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler {
try {
Arsse::$db->feedUpdate($data['feedId']);
} 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);
}
@ -347,8 +355,8 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler {
if (!isset($data['url'])) {
return new Response(422);
}
// normalize the folder ID, if specified; zero should be transformed to null
$folder = (isset($data['folderId']) && $data['folderId']) ? $data['folderId'] : null;
// normalize the folder ID, if specified
$folder = isset($data['folderId']) ? $data['folderId'] : null;
// try to add the feed
$tr = Arsse::$db->begin();
try {
@ -446,12 +454,13 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler {
Arsse::$db->subscriptionPropertiesSet(Arsse::$user->id, (int) $url[1], $in);
} catch (ExceptionInput $e) {
switch ($e->getCode()) {
// subscription does not exist
case 10239: return new Response(404);
// folder does not exist
case 10235: return new Response(422);
// other errors related to input
default: return new Response(400); // @codeCoverageIgnore
case 10239: // subscription does not exist
return new Response(404);
case 10235: // folder does not exist
case 10237: // folder ID is invalid
return new Response(422);
default: // other errors related to input
return new Response(400); // @codeCoverageIgnore
}
}
return new Response(204);

4
tests/Misc/TestContext.php

@ -47,9 +47,9 @@ class TestContext extends Test\AbstractTest {
$this->assertInstanceOf(Context::class, $c->$method($v[$method]));
$this->assertTrue($c->$method());
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 {
$this->assertSame($c->$method, $v[$method]);
$this->assertSame($c->$method, $v[$method], "Context method $method did not return the expected results");
}
}
}

185
tests/Misc/TestValueInfo.php

@ -0,0 +1,185 @@
<?php
declare(strict_types=1);
namespace JKingWeb\Arsse;
use JKingWeb\Arsse\Misc\ValueInfo as I;
/** @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],
];
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],
];
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],
];
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));
}
}
}

99
tests/REST/NextCloudNews/TestNCNV1_2.php

@ -46,6 +46,21 @@ class TestNCNV1_2 extends Test\AbstractTest {
'title' => 'Second example feed',
'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' => [
[
@ -76,6 +91,20 @@ class TestNCNV1_2 extends Test\AbstractTest {
'title' => 'Second example feed',
'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 = [
@ -331,7 +360,7 @@ class TestNCNV1_2 extends Test\AbstractTest {
$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);
$exp = new Response(401, "", "", ['WWW-Authenticate: Basic realm="'.REST\NextCloudNews\V1_2::REALM.'"']);
$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", '{"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
$exp = new Response(409);
$this->assertEquals($exp, $this->h->dispatch(new Request("POST", "/folders?name=Software")));
@ -457,26 +487,29 @@ class TestNCNV1_2 extends Test\AbstractTest {
$in = [
['url' => "http://example.com/news.atom", 'folderId' => 3],
['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 = [
['feeds' => [$this->feeds['rest'][0]]],
['feeds' => [$this->feeds['rest'][1]], 'newestItemId' => 4758915],
[],
[],
['feeds' => [$this->feeds['rest'][2]], 'newestItemId' => 2112],
];
// 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.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, 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(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)->subscriptionPropertiesSet(Arsse::$user->id, 42, ['folder' => 8])->thenReturn(true);
// set up a mock for a bad feed
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)->editionLatest(Arsse::$user->id, (new Context)->subscription(47))->thenReturn(2112);
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)->subscriptionPropertiesSet(Arsse::$user->id, 42, ['folder' => 8])->thenReturn(true);
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
$exp = new Response(200, $out[0]);
$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
$exp = new Response(409);
$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')));
// try to add a bad feed
$exp = new Response(422);
$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
$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() {
@ -511,11 +546,13 @@ class TestNCNV1_2 extends Test\AbstractTest {
['folderId' => 42],
['folderId' => 2112],
['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' => 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->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')));
$exp = new Response(422);
$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() {
@ -584,18 +623,20 @@ class TestNCNV1_2 extends Test\AbstractTest {
['feedId' => 42], // valid
['feedId' => 2112], // feed does not exist
['feedId' => "ook"], // invalid ID
['feedId' => -1], // invalid ID
['feed' => 42], // invalid input
];
Phake::when(Arsse::$db)->feedUpdate(42)->thenReturn(true);
Phake::when(Arsse::$db)->feedUpdate(2112)->thenThrow(new ExceptionInput("subjectMissing"));
Phake::when(Arsse::$db)->feedUpdate(-1)->thenThrow(new ExceptionInput("typeViolation"));
$exp = new Response(204);
$this->assertEquals($exp, $this->h->dispatch(new Request("GET", "/feeds/update", json_encode($in[0]), 'application/json')));
$exp = new Response(404);
$this->assertEquals($exp, $this->h->dispatch(new Request("GET", "/feeds/update", json_encode($in[1]), 'application/json')));
$exp = new Response(422);
$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[4]), 'application/json')));
// updating a feed when not an admin fails
Phake::when(Arsse::$user)->rightsGet->thenReturn(0);
$exp = new Response(403);
@ -606,13 +647,15 @@ class TestNCNV1_2 extends Test\AbstractTest {
$res = new Result($this->articles['db']);
$t = new \DateTime;
$in = [
['type' => 0, 'id' => 42],
['type' => 1, 'id' => 2112],
['type' => 2, 'id' => 0],
['type' => 3, 'id' => 0],
['type' => 0, 'id' => 42], // type=0 => subscription/feed
['type' => 1, 'id' => 2112], // type=1 => folder
['type' => 0, 'id' => -1], // type=0 => subscription/feed; invalid ID
['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' => false, 'batchSize' => 5, 'offset' => 5],
['getRead' => true],
['getRead' => true], // base context
['getRead' => false],
['lastModified' => $t->getTimestamp()],
['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, (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)->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']]);
// check the contents of the response
$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);
$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[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
$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[5]), 'application/json'));
$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[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'));
$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'));
// perform method verifications
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)->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(false)->limit(10)->oldestEdition(6));
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(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)); // 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)->modifiedSince($t));
Phake::verify(Arsse::$db)->articleList(Arsse::$user->id, (new Context)->reverse(true)->limit(5));

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");
$test = Date::transform($test, "iso8601");
$this->assertSame($exp, $test);
$this->assertSame($exp, $test, $msg);
}
public function clearData(bool $loadLang = true): bool {

5
tests/lib/Database/SeriesFeed.php

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

21
tests/lib/Database/SeriesFolder.php

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

17
tests/lib/Database/SeriesSubscription.php

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

7
tests/phpunit.xml

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

Loading…
Cancel
Save