diff --git a/lib/Database.php b/lib/Database.php index 9accd6ef..3a97177a 100644 --- a/lib/Database.php +++ b/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, diff --git a/lib/Misc/Context.php b/lib/Misc/Context.php index 566d9285..01e0528f 100644 --- a/lib/Misc/Context.php +++ b/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; diff --git a/lib/Misc/ValueInfo.php b/lib/Misc/ValueInfo.php index f6f85e9b..bc7e9d38 100644 --- a/lib/Misc/ValueInfo.php +++ b/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; + } + } } \ No newline at end of file diff --git a/lib/REST/AbstractHandler.php b/lib/REST/AbstractHandler.php index 281c7c54..fbc42d41 100644 --- a/lib/REST/AbstractHandler.php +++ b/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": diff --git a/lib/REST/NextCloudNews/V1_2.php b/lib/REST/NextCloudNews/V1_2.php index f17a298d..caf3d404 100644 --- a/lib/REST/NextCloudNews/V1_2.php +++ b/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); diff --git a/tests/Misc/TestContext.php b/tests/Misc/TestContext.php index f03ca41d..49ab9bdc 100644 --- a/tests/Misc/TestContext.php +++ b/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"); } } } diff --git a/tests/Misc/TestValueInfo.php b/tests/Misc/TestValueInfo.php new file mode 100644 index 00000000..8a590552 --- /dev/null +++ b/tests/Misc/TestValueInfo.php @@ -0,0 +1,185 @@ +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)); + } + } +} diff --git a/tests/REST/NextCloudNews/TestNCNV1_2.php b/tests/REST/NextCloudNews/TestNCNV1_2.php index a137df97..24665991 100644 --- a/tests/REST/NextCloudNews/TestNCNV1_2.php +++ b/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", '', '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)); diff --git a/tests/lib/AbstractTest.php b/tests/lib/AbstractTest.php index 391ae7aa..a310cda0 100644 --- a/tests/lib/AbstractTest.php +++ b/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 { diff --git a/tests/lib/Database/SeriesFeed.php b/tests/lib/Database/SeriesFeed.php index d2cd7ed1..09312e52 100644 --- a/tests/lib/Database/SeriesFeed.php +++ b/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); diff --git a/tests/lib/Database/SeriesFolder.php b/tests/lib/Database/SeriesFolder.php index 16312f2e..5c0d15b4 100644 --- a/tests/lib/Database/SeriesFolder.php +++ b/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 diff --git a/tests/lib/Database/SeriesSubscription.php b/tests/lib/Database/SeriesSubscription.php index 12f57600..b68ecb47 100644 --- a/tests/lib/Database/SeriesSubscription.php +++ b/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"); diff --git a/tests/phpunit.xml b/tests/phpunit.xml index 2a2b4434..1e30465b 100644 --- a/tests/phpunit.xml +++ b/tests/phpunit.xml @@ -31,6 +31,10 @@ Conf/TestConf.php + + Misc/TestValueInfo.php + Misc/TestContext.php + User/TestUserMockInternal.php User/TestUserMockExternal.php @@ -41,9 +45,6 @@ Feed/TestFeedFetching.php Feed/TestFeed.php - - Misc/TestContext.php - Db/TestTransaction.php Db/SQLite3/TestDbResultSQLite3.php