From e74a3ae3cb199bc140cffc8d5699f43454889730 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Tue, 26 Sep 2017 16:45:41 -0400 Subject: [PATCH] Fix numerous bugs when adding or changing folders - Specifying a non-integer parent no longer silently casts to 0 or 1 - Specifying a folder ID of 0 now always converts to null automatically - Performing both a rename and move to root in the same operation no longer results in potential duplicates - Calling folderSetProperties with an empty data array no peforms an update; it now returns false before the update call - Modification timestamps are now actually updated when a folder is modified - Constraint violation exceptions triggered by code (rather than the database) now print a message - Renaming a folder or subscription to a non-string value (e.g. an array) throws an exception rather than silently casting - Added tests to better cover all the above - Centralized the normalization of integers and title strings into a new ValueInfo static class --- lib/AbstractException.php | 128 +++++------ lib/Database.php | 178 ++++++++------- lib/Db/SQLite3/ExceptionBuilder.php | 4 +- lib/Misc/Context.php | 18 +- lib/Misc/ValueInfo.php | 73 +++++++ lib/REST/AbstractHandler.php | 5 +- locale/en.php | 252 +++++++++++----------- tests/lib/Database/SeriesFolder.php | 29 +++ tests/lib/Database/SeriesSubscription.php | 5 + 9 files changed, 408 insertions(+), 284 deletions(-) create mode 100644 lib/Misc/ValueInfo.php diff --git a/lib/AbstractException.php b/lib/AbstractException.php index 034254c..4d630c7 100644 --- a/lib/AbstractException.php +++ b/lib/AbstractException.php @@ -4,69 +4,71 @@ namespace JKingWeb\Arsse; abstract class AbstractException extends \Exception { const CODES = [ - "Exception.uncoded" => -1, - "Exception.unknown" => 10000, - "Lang/Exception.defaultFileMissing" => 10101, - "Lang/Exception.fileMissing" => 10102, - "Lang/Exception.fileUnreadable" => 10103, - "Lang/Exception.fileCorrupt" => 10104, - "Lang/Exception.stringMissing" => 10105, - "Lang/Exception.stringInvalid" => 10106, - "Db/Exception.extMissing" => 10201, - "Db/Exception.fileMissing" => 10202, - "Db/Exception.fileUnusable" => 10203, - "Db/Exception.fileUnreadable" => 10204, - "Db/Exception.fileUnwritable" => 10205, - "Db/Exception.fileUncreatable" => 10206, - "Db/Exception.fileCorrupt" => 10207, - "Db/Exception.updateTooNew" => 10211, - "Db/Exception.updateManual" => 10212, - "Db/Exception.updateManualOnly" => 10213, - "Db/Exception.updateFileMissing" => 10214, - "Db/Exception.updateFileUnusable" => 10215, - "Db/Exception.updateFileUnreadable" => 10216, - "Db/Exception.updateFileError" => 10217, - "Db/Exception.updateFileIncomplete" => 10218, - "Db/Exception.paramTypeInvalid" => 10221, - "Db/Exception.paramTypeUnknown" => 10222, - "Db/Exception.paramTypeMissing" => 10223, - "Db/Exception.engineErrorGeneral" => 10224, // this symbol may have engine-specific duplicates to accomodate engine-specific error string construction - "Db/Exception.unknownSavepointStatus" => 10225, - "Db/ExceptionSavepoint.invalid" => 10226, - "Db/ExceptionSavepoint.stale" => 10227, - "Db/ExceptionInput.missing" => 10231, - "Db/ExceptionInput.whitespace" => 10232, - "Db/ExceptionInput.tooLong" => 10233, - "Db/ExceptionInput.tooShort" => 10234, - "Db/ExceptionInput.idMissing" => 10235, - "Db/ExceptionInput.constraintViolation" => 10236, - "Db/ExceptionInput.typeViolation" => 10237, - "Db/ExceptionInput.circularDependence" => 10238, - "Db/ExceptionInput.subjectMissing" => 10239, - "Db/ExceptionTimeout.general" => 10241, - "Conf/Exception.fileMissing" => 10301, - "Conf/Exception.fileUnusable" => 10302, - "Conf/Exception.fileUnreadable" => 10303, - "Conf/Exception.fileUnwritable" => 10304, - "Conf/Exception.fileUncreatable" => 10305, - "Conf/Exception.fileCorrupt" => 10306, - "User/Exception.functionNotImplemented" => 10401, - "User/Exception.doesNotExist" => 10402, - "User/Exception.alreadyExists" => 10403, - "User/Exception.authMissing" => 10411, - "User/Exception.authFailed" => 10412, - "User/ExceptionAuthz.notAuthorized" => 10421, - "Feed/Exception.invalidCertificate" => 10501, - "Feed/Exception.invalidUrl" => 10502, - "Feed/Exception.maxRedirect" => 10503, - "Feed/Exception.maxSize" => 10504, - "Feed/Exception.timeout" => 10505, - "Feed/Exception.forbidden" => 10506, - "Feed/Exception.unauthorized" => 10507, - "Feed/Exception.malformedXml" => 10511, - "Feed/Exception.xmlEntity" => 10512, - "Feed/Exception.subscriptionNotFound" => 10521, - "Feed/Exception.unsupportedFeedFormat" => 10522, + "Exception.uncoded" => -1, + "Exception.unknown" => 10000, + "Lang/Exception.defaultFileMissing" => 10101, + "Lang/Exception.fileMissing" => 10102, + "Lang/Exception.fileUnreadable" => 10103, + "Lang/Exception.fileCorrupt" => 10104, + "Lang/Exception.stringMissing" => 10105, + "Lang/Exception.stringInvalid" => 10106, + "Db/Exception.extMissing" => 10201, + "Db/Exception.fileMissing" => 10202, + "Db/Exception.fileUnusable" => 10203, + "Db/Exception.fileUnreadable" => 10204, + "Db/Exception.fileUnwritable" => 10205, + "Db/Exception.fileUncreatable" => 10206, + "Db/Exception.fileCorrupt" => 10207, + "Db/Exception.updateTooNew" => 10211, + "Db/Exception.updateManual" => 10212, + "Db/Exception.updateManualOnly" => 10213, + "Db/Exception.updateFileMissing" => 10214, + "Db/Exception.updateFileUnusable" => 10215, + "Db/Exception.updateFileUnreadable" => 10216, + "Db/Exception.updateFileError" => 10217, + "Db/Exception.updateFileIncomplete" => 10218, + "Db/Exception.paramTypeInvalid" => 10221, + "Db/Exception.paramTypeUnknown" => 10222, + "Db/Exception.paramTypeMissing" => 10223, + "Db/Exception.engineErrorGeneral" => 10224, // this symbol may have engine-specific duplicates to accomodate engine-specific error string construction + "Db/Exception.unknownSavepointStatus" => 10225, + "Db/ExceptionSavepoint.invalid" => 10226, + "Db/ExceptionSavepoint.stale" => 10227, + "Db/ExceptionInput.missing" => 10231, + "Db/ExceptionInput.whitespace" => 10232, + "Db/ExceptionInput.tooLong" => 10233, + "Db/ExceptionInput.tooShort" => 10234, + "Db/ExceptionInput.idMissing" => 10235, + "Db/ExceptionInput.constraintViolation" => 10236, + "Db/ExceptionInput.engineConstraintViolation" => 10236, + "Db/ExceptionInput.typeViolation" => 10237, + "Db/ExceptionInput.engineTypeViolation" => 10237, + "Db/ExceptionInput.circularDependence" => 10238, + "Db/ExceptionInput.subjectMissing" => 10239, + "Db/ExceptionTimeout.general" => 10241, + "Conf/Exception.fileMissing" => 10301, + "Conf/Exception.fileUnusable" => 10302, + "Conf/Exception.fileUnreadable" => 10303, + "Conf/Exception.fileUnwritable" => 10304, + "Conf/Exception.fileUncreatable" => 10305, + "Conf/Exception.fileCorrupt" => 10306, + "User/Exception.functionNotImplemented" => 10401, + "User/Exception.doesNotExist" => 10402, + "User/Exception.alreadyExists" => 10403, + "User/Exception.authMissing" => 10411, + "User/Exception.authFailed" => 10412, + "User/ExceptionAuthz.notAuthorized" => 10421, + "Feed/Exception.invalidCertificate" => 10501, + "Feed/Exception.invalidUrl" => 10502, + "Feed/Exception.maxRedirect" => 10503, + "Feed/Exception.maxSize" => 10504, + "Feed/Exception.timeout" => 10505, + "Feed/Exception.forbidden" => 10506, + "Feed/Exception.unauthorized" => 10507, + "Feed/Exception.malformedXml" => 10511, + "Feed/Exception.xmlEntity" => 10512, + "Feed/Exception.subscriptionNotFound" => 10521, + "Feed/Exception.unsupportedFeedFormat" => 10522, ]; public function __construct(string $msgID = "", $vars = null, \Throwable $e = null) { diff --git a/lib/Database.php b/lib/Database.php index 4983da3..9accd6e 100644 --- a/lib/Database.php +++ b/lib/Database.php @@ -6,6 +6,7 @@ use PasswordGenerator\Generator as PassGen; use JKingWeb\Arsse\Misc\Query; use JKingWeb\Arsse\Misc\Context; use JKingWeb\Arsse\Misc\Date; +use JKingWeb\Arsse\Misc\ValueInfo; class Database { const SCHEMA_VERSION = 1; @@ -228,31 +229,13 @@ class Database { if (!Arsse::$user->authorize($user, __FUNCTION__)) { throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]); } - // if the desired folder name is missing or invalid, throw an exception - if (!array_key_exists("name", $data) || $data['name']=="") { - throw new Db\ExceptionInput("missing", ["action" => __FUNCTION__, "field" => "name"]); - } elseif (!strlen(trim($data['name']))) { - throw new Db\ExceptionInput("whitespace", ["action" => __FUNCTION__, "field" => "name"]); - } // normalize folder's parent, if there is one - $parent = array_key_exists("parent", $data) ? (int) $data['parent'] : 0; - if ($parent===0) { - // if no parent is specified, do nothing - $parent = null; - } else { - // if a parent is specified, make sure it exists and belongs to the user; get its root (first-level) folder if it's a nested folder - $p = $this->db->prepare("SELECT id from arsse_folders where owner is ? and id is ?", "str", "int")->run($user, $parent)->getValue(); - if (!$p) { - throw new Db\ExceptionInput("idMissing", ["action" => __FUNCTION__, "field" => "parent", 'id' => $parent]); - } - } - // check if a folder by the same name already exists, because nulls are wonky in SQL - // FIXME: How should folder name be compared? Should a Unicode normalization be applied before comparison and insertion? - if ($this->db->prepare("SELECT count(*) from arsse_folders where owner is ? and parent is ? and name is ?", "str", "int", "str")->run($user, $parent, $data['name'])->getValue() > 0) { - throw new Db\ExceptionInput("constraintViolation"); // FIXME: There needs to be a practical message here - } - // actually perform the insert (!) - return $this->db->prepare("INSERT INTO arsse_folders(owner,parent,name) values(?,?,?)", "str", "int", "str")->run($user, $parent, $data['name'])->lastId(); + $parent = array_key_exists("parent", $data) ? $this->folderValidateId($user, $data['parent'])['id'] : null; + // validate the folder name and parent (if specified); this also checks for duplicates + $name = array_key_exists("name", $data) ? $data['name'] : ""; + $this->folderValidateName($name, true, $parent); + // actually perform the insert + 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 { @@ -303,70 +286,110 @@ class Database { if (!Arsse::$user->authorize($user, __FUNCTION__)) { throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]); } - // validate the folder ID and, if specified, the parent to move it to - $parent = null; - if (array_key_exists("parent", $data)) { - $parent = $data['parent']; - } - $f = $this->folderValidateId($user, $id, $parent, true); - // if a new name is specified, validate it - if (array_key_exists("name", $data)) { + // verify the folder belongs to the user + $in = $this->folderValidateId($user, $id, true); + $name = array_key_exists("name", $data); + $parent = array_key_exists("parent", $data); + if ($name && $parent) { + // if a new name and parent are specified, validate both together $this->folderValidateName($data['name']); - } - $data = array_merge($f, $data); - // check to make sure the target folder name/location would not create a duplicate (we must do this check because null is not distinct in SQL) - $existing = $this->db->prepare("SELECT id from arsse_folders where owner is ? and parent is ? and name is ?", "str", "int", "str")->run($user, $data['parent'], $data['name'])->getValue(); - if (!is_null($existing) && $existing != $id) { - throw new Db\ExceptionInput("constraintViolation"); // FIXME: There needs to be a practical message here + $in['name'] = $data['name']; + $in['parent'] = $this->folderValidateMove($user, $id, $data['parent'], $data['name']); + } elseif ($name) { + // 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']); + } else { + // if neither was specified, do nothing + return false; } $valid = [ 'name' => "str", 'parent' => "int", ]; - list($setClause, $setTypes, $setValues) = $this->generateSet($data, $valid); - return (bool) $this->db->prepare("UPDATE arsse_folders set $setClause where owner is ? and id is ?", $setTypes, "str", "int")->run($setValues, $user, $id)->changes(); + list($setClause, $setTypes, $setValues) = $this->generateSet($in, $valid); + return (bool) $this->db->prepare("UPDATE arsse_folders set $setClause, modified = CURRENT_TIMESTAMP where owner is ? and id is ?", $setTypes, "str", "int")->run($setValues, $user, $id)->changes(); } - protected function folderValidateId(string $user, int $id = null, int $parent = null, bool $subject = false): array { - if (is_null($id)) { - // if no ID is specified this is a no-op, unless a parent is specified, which is always a circular dependence (the root cannot be moved) - if (!is_null($parent)) { - throw new Db\ExceptionInput("circularDependence", ["action" => $this->caller(), "field" => "parent", 'id' => $parent]); // @codeCoverageIgnore - } - return ['name' => null, 'parent' => null]; + 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 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]); } // check whether the folder exists and is owned by the user - $f = $this->db->prepare("SELECT name,parent from arsse_folders where owner is ? and id is ?", "str", "int")->run($user, $id)->getRow(); + $f = $this->db->prepare("SELECT id,name,parent from arsse_folders where owner is ? and id is ?", "str", "int")->run($user, $id)->getRow(); if (!$f) { - throw new Db\ExceptionInput($subject ? "subjectMissing" : "idMissing", ["action" => $this->caller(), "field" => "folder", 'id' => $parent]); - } - // if we're moving a folder to a new parent, check that the parent is valid - if (!is_null($parent)) { - // make sure both that the parent exists, and that the parent is not either the folder itself or one of its children (a circular dependence) - $p = $this->db->prepare( - "WITH RECURSIVE folders(id) as (SELECT id from arsse_folders where owner is ? and id is ? union select arsse_folders.id from arsse_folders join folders on arsse_folders.parent=folders.id) ". - "SELECT id,(id not in (select id from folders)) as valid from arsse_folders where owner is ? and id is ?", - "str", "int", "str", "int" - )->run($user, $id, $user, $parent)->getRow(); - if (!$p) { - // if the parent doesn't exist or doesn't below to the user, throw an exception - throw new Db\ExceptionInput("idMissing", ["action" => $this->caller(), "field" => "parent", 'id' => $parent]); - } else { - // if using the desired parent would create a circular dependence, throw a different exception - if (!$p['valid']) { - throw new Db\ExceptionInput("circularDependence", ["action" => $this->caller(), "field" => "parent", 'id' => $parent]); - } - } + throw new Db\ExceptionInput($subject ? "subjectMissing" : "idMissing", ["action" => $this->caller(), "field" => "folder", 'id' => $id]); } return $f; } - protected function folderValidateName($name): bool { - $name = (string) $name; - if (!strlen($name)) { + protected function folderValidateMove(string $user, int $id = null, $parent = null, string $name = null) { + $errData = ["action" => $this->caller(), "field" => "parent", 'id' => $parent]; + if (!$id) { + // the root cannot be moved + throw new Db\ExceptionInput("circularDependence", $errData); + } + $info = ValueInfo::int($parent); + // the root is always a valid parent + if ($info & (ValueInfo::NULL | ValueInfo::ZERO)) { + $parent = null; + } else { + // if a negative integer or non-integer is specified this will always fail + if (!($info & ValueInfo::VALID) || (($info & ValueInfo::NEG))) { + throw new Db\ExceptionInput("idMissing", $errData); + } + $parent = (int) $parent; + } + // if the target parent is the folder itself, this is a circular dependence + if ($id==$parent) { + throw new Db\ExceptionInput("circularDependence", $errData); + } + // make sure both that the prospective parent exists, and that the it is not one of its children (a circular dependence) + $p = $this->db->prepare( + "WITH RECURSIVE + target as (select ? as user, ? as source, ? as dest, ? as rename), + folders as (SELECT id from arsse_folders join target on owner is user and parent is source union select arsse_folders.id as id from arsse_folders join folders on arsse_folders.parent=folders.id) + ". + "SELECT + ((select dest from target) is null or exists(select id from arsse_folders join target on owner is user and id is dest)) as extant, + not exists(select id from folders where id is (select dest from target)) as valid, + not exists(select id from arsse_folders join target on parent is dest and name is coalesce((select rename from target),(select name from arsse_folders join target on id is source))) as available + ", "str", "int", "int","str" + )->run($user, $id, $parent, $name)->getRow(); + if (!$p['extant']) { + // if the parent doesn't exist or doesn't below to the user, throw an exception + throw new Db\ExceptionInput("idMissing", $errData); + } elseif (!$p['valid']) { + // if using the desired parent would create a circular dependence, throw a different exception + throw new Db\ExceptionInput("circularDependence", $errData); + } elseif (!$p['available']) { + throw new Db\ExceptionInput("constraintViolation", ["action" => $this->caller(), "field" => (is_null($name) ? "parent" : "name")]); + } + return $parent; + } + + protected function folderValidateName($name, bool $checkDuplicates = false, int $parent = null): bool { + $info = ValueInfo::str($name); + if ($info & (ValueInfo::NULL | ValueInfo::EMPTY)) { throw new Db\ExceptionInput("missing", ["action" => $this->caller(), "field" => "name"]); - } elseif (!strlen(trim($name))) { + } elseif ($info & ValueInfo::WHITE) { throw new Db\ExceptionInput("whitespace", ["action" => $this->caller(), "field" => "name"]); + } elseif (!($info & ValueInfo::VALID)) { + throw new Db\ExceptionInput("typeViolation", ["action" => $this->caller(), "field" => "name", 'type' => "string"]); + } elseif($checkDuplicates) { + if ($this->db->prepare("SELECT exists(select id from arsse_folders where parent is ? and name is ?)", "int", "str")->run($parent, $name)->getValue()) { + throw new Db\ExceptionInput("constraintViolation", ["action" => $this->caller(), "field" => "name"]); + } + return true; } else { return true; } @@ -420,7 +443,7 @@ class Database { // 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 (!is_null($folder)) { + } elseif ($folder) { // if a folder is specified, make sure it exists $this->folderValidateId($user, $folder); // if it does exist, add a common table expression to list it and its children so that we select from the entire subtree @@ -467,18 +490,19 @@ class Database { } if (array_key_exists("folder", $data)) { // ensure the target folder exists and belong to the user - $this->folderValidateId($user, $data['folder']); + $data['folder'] = $this->folderValidateId($user, $data['folder'])['id']; } if (array_key_exists("title", $data)) { // if the title is null, this signals intended use of the default title; otherwise make sure it's not effectively an empty string if (!is_null($data['title'])) { - $title = (string) $data['title']; - if (!strlen($title)) { + $info = ValueInfo::str($data['title']); + if ($info & ValueInfo::EMPTY) { throw new Db\ExceptionInput("missing", ["action" => __FUNCTION__, "field" => "title"]); - } elseif (!strlen(trim($title))) { + } elseif ($info & ValueInfo::WHITE) { throw new Db\ExceptionInput("whitespace", ["action" => __FUNCTION__, "field" => "title"]); + } elseif (!($info & ValueInfo::VALID)) { + throw new Db\ExceptionInput("typeViolation", ["action" => __FUNCTION__, "field" => "title", 'type' => "string"]); } - $data['title'] = $title; } } $valid = [ diff --git a/lib/Db/SQLite3/ExceptionBuilder.php b/lib/Db/SQLite3/ExceptionBuilder.php index e9f13a5..035ab02 100644 --- a/lib/Db/SQLite3/ExceptionBuilder.php +++ b/lib/Db/SQLite3/ExceptionBuilder.php @@ -12,9 +12,9 @@ trait ExceptionBuilder { case self::SQLITE_BUSY: return [ExceptionTimeout::class, 'general', $this->db->lastErrorMsg()]; case self::SQLITE_CONSTRAINT: - return [ExceptionInput::class, 'constraintViolation', $this->db->lastErrorMsg()]; + return [ExceptionInput::class, 'engineConstraintViolation', $this->db->lastErrorMsg()]; case self::SQLITE_MISMATCH: - return [ExceptionInput::class, 'typeViolation', $this->db->lastErrorMsg()]; + return [ExceptionInput::class, 'engineTypeViolation', $this->db->lastErrorMsg()]; default: return [Exception::class, 'engineErrorGeneral', $this->db->lastErrorMsg()]; } diff --git a/lib/Misc/Context.php b/lib/Misc/Context.php index d64a18f..566d928 100644 --- a/lib/Misc/Context.php +++ b/lib/Misc/Context.php @@ -3,6 +3,7 @@ declare(strict_types=1); namespace JKingWeb\Arsse\Misc; use JKingWeb\Arsse\Misc\Date; +use JKingWeb\Arsse\Misc\ValueInfo; class Context { public $reverse = false; @@ -36,22 +37,11 @@ class Context { protected function cleanArray(array $spec): array { $spec = array_values($spec); for ($a = 0; $a < sizeof($spec); $a++) { - $id = $spec[$a]; - if (is_int($id) && $id > -1) { - continue; - } elseif (is_float($id) && !fmod($id, 1) && $id >= 0) { - $spec[$a] = (int) $id; - continue; - } elseif (is_string($id)) { - $ch1 = strval(@intval($id)); - $ch2 = strval($id); - if ($ch1 !== $ch2 || $id < 1) { - $id = 0; - } + if(ValueInfo::int($spec[$a])===ValueInfo::VALID) { + $spec[$a] = (int) $spec[$a]; } else { - $id = 0; + $spec[$a] = 0; } - $spec[$a] = (int) $id; } return array_values(array_filter($spec)); } diff --git a/lib/Misc/ValueInfo.php b/lib/Misc/ValueInfo.php new file mode 100644 index 0000000..f6f85e9 --- /dev/null +++ b/lib/Misc/ValueInfo.php @@ -0,0 +1,73 @@ + 'SQLite 3', - 'Driver.Service.Curl.Name' => 'HTTP (curl)', - 'Driver.Service.Internal.Name' => 'Internal', - 'Driver.User.Internal.Name' => 'Internal', + 'Driver.Db.SQLite3.Name' => 'SQLite 3', + 'Driver.Service.Curl.Name' => 'HTTP (curl)', + 'Driver.Service.Internal.Name' => 'Internal', + 'Driver.User.Internal.Name' => 'Internal', - 'HTTP.Status.100' => 'Continue', - 'HTTP.Status.101' => 'Switching Protocols', - 'HTTP.Status.102' => 'Processing', - 'HTTP.Status.200' => 'OK', - 'HTTP.Status.201' => 'Created', - 'HTTP.Status.202' => 'Accepted', - 'HTTP.Status.203' => 'Non-Authoritative Information', - 'HTTP.Status.204' => 'No Content', - 'HTTP.Status.205' => 'Reset Content', - 'HTTP.Status.206' => 'Partial Content', - 'HTTP.Status.207' => 'Multi-Status', - 'HTTP.Status.208' => 'Already Reported', - 'HTTP.Status.226' => 'IM Used', - 'HTTP.Status.300' => 'Multiple Choice', - 'HTTP.Status.301' => 'Moved Permanently', - 'HTTP.Status.302' => 'Found', - 'HTTP.Status.303' => 'See Other', - 'HTTP.Status.304' => 'Not Modified', - 'HTTP.Status.305' => 'Use Proxy', - 'HTTP.Status.306' => 'Switch Proxy', - 'HTTP.Status.307' => 'Temporary Redirect', - 'HTTP.Status.308' => 'Permanent Redirect', - 'HTTP.Status.400' => 'Bad Request', - 'HTTP.Status.401' => 'Unauthorized', - 'HTTP.Status.402' => 'Payment Required', - 'HTTP.Status.403' => 'Forbidden', - 'HTTP.Status.404' => 'Not Found', - 'HTTP.Status.405' => 'Method Not Allowed', - 'HTTP.Status.406' => 'Not Acceptable', - 'HTTP.Status.407' => 'Proxy Authentication Required', - 'HTTP.Status.408' => 'Request Timeout', - 'HTTP.Status.409' => 'Conflict', - 'HTTP.Status.410' => 'Gone', - 'HTTP.Status.411' => 'Length Required', - 'HTTP.Status.412' => 'Precondition Failed', - 'HTTP.Status.413' => 'Payload Too Large', - 'HTTP.Status.414' => 'URL Too Long', - 'HTTP.Status.415' => 'Unsupported Media Type', - 'HTTP.Status.416' => 'Range Not Satisfiable', - 'HTTP.Status.417' => 'Expectation Failed', - 'HTTP.Status.421' => 'Misdirected Request', - 'HTTP.Status.422' => 'Unprocessable Entity', - 'HTTP.Status.423' => 'Locked', - 'HTTP.Status.424' => 'Failed Depedency', - 'HTTP.Status.426' => 'Upgrade Required', - 'HTTP.Status.428' => 'Precondition Failed', - 'HTTP.Status.429' => 'Too Many Requests', - 'HTTP.Status.431' => 'Request Header Fields Too Large', - 'HTTP.Status.451' => 'Unavailable For Legal Reasons', - 'HTTP.Status.500' => 'Internal Server Error', - 'HTTP.Status.501' => 'Not Implemented', - 'HTTP.Status.502' => 'Bad Gateway', - 'HTTP.Status.503' => 'Service Unavailable', - 'HTTP.Status.504' => 'Gateway Timeout', - 'HTTP.Status.505' => 'HTTP Version Not Supported', - 'HTTP.Status.506' => 'Variant Also Negotiates', - 'HTTP.Status.507' => 'Insufficient Storage', - 'HTTP.Status.508' => 'Loop Detected', - 'HTTP.Status.510' => 'Not Extended', - 'HTTP.Status.511' => 'Network Authentication Required', + 'HTTP.Status.100' => 'Continue', + 'HTTP.Status.101' => 'Switching Protocols', + 'HTTP.Status.102' => 'Processing', + 'HTTP.Status.200' => 'OK', + 'HTTP.Status.201' => 'Created', + 'HTTP.Status.202' => 'Accepted', + 'HTTP.Status.203' => 'Non-Authoritative Information', + 'HTTP.Status.204' => 'No Content', + 'HTTP.Status.205' => 'Reset Content', + 'HTTP.Status.206' => 'Partial Content', + 'HTTP.Status.207' => 'Multi-Status', + 'HTTP.Status.208' => 'Already Reported', + 'HTTP.Status.226' => 'IM Used', + 'HTTP.Status.300' => 'Multiple Choice', + 'HTTP.Status.301' => 'Moved Permanently', + 'HTTP.Status.302' => 'Found', + 'HTTP.Status.303' => 'See Other', + 'HTTP.Status.304' => 'Not Modified', + 'HTTP.Status.305' => 'Use Proxy', + 'HTTP.Status.306' => 'Switch Proxy', + 'HTTP.Status.307' => 'Temporary Redirect', + 'HTTP.Status.308' => 'Permanent Redirect', + 'HTTP.Status.400' => 'Bad Request', + 'HTTP.Status.401' => 'Unauthorized', + 'HTTP.Status.402' => 'Payment Required', + 'HTTP.Status.403' => 'Forbidden', + 'HTTP.Status.404' => 'Not Found', + 'HTTP.Status.405' => 'Method Not Allowed', + 'HTTP.Status.406' => 'Not Acceptable', + 'HTTP.Status.407' => 'Proxy Authentication Required', + 'HTTP.Status.408' => 'Request Timeout', + 'HTTP.Status.409' => 'Conflict', + 'HTTP.Status.410' => 'Gone', + 'HTTP.Status.411' => 'Length Required', + 'HTTP.Status.412' => 'Precondition Failed', + 'HTTP.Status.413' => 'Payload Too Large', + 'HTTP.Status.414' => 'URL Too Long', + 'HTTP.Status.415' => 'Unsupported Media Type', + 'HTTP.Status.416' => 'Range Not Satisfiable', + 'HTTP.Status.417' => 'Expectation Failed', + 'HTTP.Status.421' => 'Misdirected Request', + 'HTTP.Status.422' => 'Unprocessable Entity', + 'HTTP.Status.423' => 'Locked', + 'HTTP.Status.424' => 'Failed Depedency', + 'HTTP.Status.426' => 'Upgrade Required', + 'HTTP.Status.428' => 'Precondition Failed', + 'HTTP.Status.429' => 'Too Many Requests', + 'HTTP.Status.431' => 'Request Header Fields Too Large', + 'HTTP.Status.451' => 'Unavailable For Legal Reasons', + 'HTTP.Status.500' => 'Internal Server Error', + 'HTTP.Status.501' => 'Not Implemented', + 'HTTP.Status.502' => 'Bad Gateway', + 'HTTP.Status.503' => 'Service Unavailable', + 'HTTP.Status.504' => 'Gateway Timeout', + 'HTTP.Status.505' => 'HTTP Version Not Supported', + 'HTTP.Status.506' => 'Variant Also Negotiates', + 'HTTP.Status.507' => 'Insufficient Storage', + 'HTTP.Status.508' => 'Loop Detected', + 'HTTP.Status.510' => 'Not Extended', + 'HTTP.Status.511' => 'Network Authentication Required', // this should only be encountered in testing (because tests should cover all exceptions!) - 'Exception.JKingWeb/Arsse/Exception.uncoded' => 'The specified exception symbol {0} has no code specified in AbstractException.php', + 'Exception.JKingWeb/Arsse/Exception.uncoded' => 'The specified exception symbol {0} has no code specified in AbstractException.php', // this should not usually be encountered - 'Exception.JKingWeb/Arsse/Exception.unknown' => 'An unknown error has occurred', - 'Exception.JKingWeb/Arsse/Lang/Exception.defaultFileMissing' => 'Default language file "{0}" missing', - 'Exception.JKingWeb/Arsse/Lang/Exception.fileMissing' => 'Language file "{0}" is not available', - 'Exception.JKingWeb/Arsse/Lang/Exception.fileUnreadable' => 'Insufficient permissions to read language file "{0}"', - 'Exception.JKingWeb/Arsse/Lang/Exception.fileCorrupt' => 'Language file "{0}" is corrupt or does not conform to expected format', - 'Exception.JKingWeb/Arsse/Lang/Exception.stringMissing' => 'Message string "{msgID}" missing from all loaded language files ({fileList})', - 'Exception.JKingWeb/Arsse/Lang/Exception.stringInvalid' => 'Message string "{msgID}" is not a valid ICU message string (language files loaded: {fileList})', - 'Exception.JKingWeb/Arsse/Conf/Exception.fileMissing' => 'Configuration file "{0}" does not exist', - 'Exception.JKingWeb/Arsse/Conf/Exception.fileUnreadable' => 'Insufficient permissions to read configuration file "{0}"', - 'Exception.JKingWeb/Arsse/Conf/Exception.fileUncreatable' => 'Insufficient permissions to write new configuration file "{0}"', - 'Exception.JKingWeb/Arsse/Conf/Exception.fileUnwritable' => 'Insufficient permissions to overwrite configuration file "{0}"', - 'Exception.JKingWeb/Arsse/Conf/Exception.fileCorrupt' => 'Configuration file "{0}" is corrupt or does not conform to expected format', - 'Exception.JKingWeb/Arsse/Db/Exception.extMissing' => 'Required PHP extension for driver "{0}" not installed', - 'Exception.JKingWeb/Arsse/Db/Exception.fileMissing' => 'Database file "{0}" does not exist', - 'Exception.JKingWeb/Arsse/Db/Exception.fileUnreadable' => 'Insufficient permissions to open database file "{0}" for reading', - 'Exception.JKingWeb/Arsse/Db/Exception.fileUnwritable' => 'Insufficient permissions to open database file "{0}" for writing', - 'Exception.JKingWeb/Arsse/Db/Exception.fileUnusable' => 'Insufficient permissions to open database file "{0}" for reading or writing', - 'Exception.JKingWeb/Arsse/Db/Exception.fileUncreatable' => 'Insufficient permissions to create new database file "{0}"', - 'Exception.JKingWeb/Arsse/Db/Exception.fileCorrupt' => 'Database file "{0}" is corrupt or not a valid database', - 'Exception.JKingWeb/Arsse/Db/Exception.paramTypeInvalid' => 'Prepared statement parameter type "{0}" is invalid', - 'Exception.JKingWeb/Arsse/Db/Exception.paramTypeUnknown' => 'Prepared statement parameter type "{0}" is valid, but not implemented', - 'Exception.JKingWeb/Arsse/Db/Exception.paramTypeMissing' => 'Prepared statement parameter type for parameter #{0} was not specified', - 'Exception.JKingWeb/Arsse/Db/Exception.updateManual' => + 'Exception.JKingWeb/Arsse/Exception.unknown' => 'An unknown error has occurred', + 'Exception.JKingWeb/Arsse/Lang/Exception.defaultFileMissing' => 'Default language file "{0}" missing', + 'Exception.JKingWeb/Arsse/Lang/Exception.fileMissing' => 'Language file "{0}" is not available', + 'Exception.JKingWeb/Arsse/Lang/Exception.fileUnreadable' => 'Insufficient permissions to read language file "{0}"', + 'Exception.JKingWeb/Arsse/Lang/Exception.fileCorrupt' => 'Language file "{0}" is corrupt or does not conform to expected format', + 'Exception.JKingWeb/Arsse/Lang/Exception.stringMissing' => 'Message string "{msgID}" missing from all loaded language files ({fileList})', + 'Exception.JKingWeb/Arsse/Lang/Exception.stringInvalid' => 'Message string "{msgID}" is not a valid ICU message string (language files loaded: {fileList})', + 'Exception.JKingWeb/Arsse/Conf/Exception.fileMissing' => 'Configuration file "{0}" does not exist', + 'Exception.JKingWeb/Arsse/Conf/Exception.fileUnreadable' => 'Insufficient permissions to read configuration file "{0}"', + 'Exception.JKingWeb/Arsse/Conf/Exception.fileUncreatable' => 'Insufficient permissions to write new configuration file "{0}"', + 'Exception.JKingWeb/Arsse/Conf/Exception.fileUnwritable' => 'Insufficient permissions to overwrite configuration file "{0}"', + 'Exception.JKingWeb/Arsse/Conf/Exception.fileCorrupt' => 'Configuration file "{0}" is corrupt or does not conform to expected format', + 'Exception.JKingWeb/Arsse/Db/Exception.extMissing' => 'Required PHP extension for driver "{0}" not installed', + 'Exception.JKingWeb/Arsse/Db/Exception.fileMissing' => 'Database file "{0}" does not exist', + 'Exception.JKingWeb/Arsse/Db/Exception.fileUnreadable' => 'Insufficient permissions to open database file "{0}" for reading', + 'Exception.JKingWeb/Arsse/Db/Exception.fileUnwritable' => 'Insufficient permissions to open database file "{0}" for writing', + 'Exception.JKingWeb/Arsse/Db/Exception.fileUnusable' => 'Insufficient permissions to open database file "{0}" for reading or writing', + 'Exception.JKingWeb/Arsse/Db/Exception.fileUncreatable' => 'Insufficient permissions to create new database file "{0}"', + 'Exception.JKingWeb/Arsse/Db/Exception.fileCorrupt' => 'Database file "{0}" is corrupt or not a valid database', + 'Exception.JKingWeb/Arsse/Db/Exception.paramTypeInvalid' => 'Prepared statement parameter type "{0}" is invalid', + 'Exception.JKingWeb/Arsse/Db/Exception.paramTypeUnknown' => 'Prepared statement parameter type "{0}" is valid, but not implemented', + 'Exception.JKingWeb/Arsse/Db/Exception.paramTypeMissing' => 'Prepared statement parameter type for parameter #{0} was not specified', + 'Exception.JKingWeb/Arsse/Db/Exception.updateManual' => '{from_version, select, 0 {{driver_name} database is configured for manual updates and is not initialized; please populate the database with the base schema} other {{driver_name} database is configured for manual updates; please update from schema version {current} to version {target}} }', - 'Exception.JKingWeb/Arsse/Db/Exception.updateManualOnly' => + 'Exception.JKingWeb/Arsse/Db/Exception.updateManualOnly' => '{from_version, select, 0 {{driver_name} database must be updated manually and is not initialized; please populate the database with the base schema} other {{driver_name} database must be updated manually; please update from schema version {current} to version {target}} }', - 'Exception.JKingWeb/Arsse/Db/Exception.updateFileMissing' => 'Automatic updating of the {driver_name} database failed due to instructions for updating from version {current} not being available', - 'Exception.JKingWeb/Arsse/Db/Exception.updateFileUnreadable' => 'Automatic updating of the {driver_name} database failed due to insufficient permissions to read instructions for updating from version {current}', - 'Exception.JKingWeb/Arsse/Db/Exception.updateFileUnusable' => 'Automatic updating of the {driver_name} database failed due to an error reading instructions for updating from version {current}', - 'Exception.JKingWeb/Arsse/Db/Exception.updateFileError' => 'Automatic updating of the {driver_name} database failed updating from version {current} with the following error: "{message}"', - 'Exception.JKingWeb/Arsse/Db/Exception.updateFileIncomplete' => 'Automatic updating of the {driver_name} database failed due to instructions for updating from version {current} being incomplete', - 'Exception.JKingWeb/Arsse/Db/Exception.updateTooNew' => + 'Exception.JKingWeb/Arsse/Db/Exception.updateFileMissing' => 'Automatic updating of the {driver_name} database failed due to instructions for updating from version {current} not being available', + 'Exception.JKingWeb/Arsse/Db/Exception.updateFileUnreadable' => 'Automatic updating of the {driver_name} database failed due to insufficient permissions to read instructions for updating from version {current}', + 'Exception.JKingWeb/Arsse/Db/Exception.updateFileUnusable' => 'Automatic updating of the {driver_name} database failed due to an error reading instructions for updating from version {current}', + 'Exception.JKingWeb/Arsse/Db/Exception.updateFileError' => 'Automatic updating of the {driver_name} database failed updating from version {current} with the following error: "{message}"', + 'Exception.JKingWeb/Arsse/Db/Exception.updateFileIncomplete' => 'Automatic updating of the {driver_name} database failed due to instructions for updating from version {current} being incomplete', + 'Exception.JKingWeb/Arsse/Db/Exception.updateTooNew' => '{difference, select, 0 {Automatic updating of the {driver_name} database failed because it is already up to date with the requested version, {target}} other {Automatic updating of the {driver_name} database failed because its version, {current}, is newer than the requested version, {target}} }', - 'Exception.JKingWeb/Arsse/Db/Exception.engineErrorGeneral' => '{0}', - 'Exception.JKingWeb/Arsse/Db/Exception.unknownSavepointStatus' => 'Savepoint status code {0} not implemented', - 'Exception.JKingWeb/Arsse/Db/ExceptionInput.missing' => 'Required field "{field}" missing while performing action "{action}"', - 'Exception.JKingWeb/Arsse/Db/ExceptionInput.whitespace' => 'Field "{field}" of action "{action}" may not contain only whitespace', - 'Exception.JKingWeb/Arsse/Db/ExceptionInput.tooLong' => 'Field "{field}" of action "{action}" has a maximum length of {max}', - 'Exception.JKingWeb/Arsse/Db/ExceptionInput.tooShort' => 'Field "{field}" of action "{action}" has a minimum length of {min}', - 'Exception.JKingWeb/Arsse/Db/ExceptionInput.subjectMissing' => 'Referenced ID ({id}) in field "{field}" does not exist', - 'Exception.JKingWeb/Arsse/Db/ExceptionInput.idMissing' => 'Referenced ID ({id}) in field "{field}" does not exist', - 'Exception.JKingWeb/Arsse/Db/ExceptionInput.circularDependence' => 'Referenced ID ({id}) in field "{field}" creates a circular dependence', - 'Exception.JKingWeb/Arsse/Db/ExceptionInput.constraintViolation' => '{0}', - 'Exception.JKingWeb/Arsse/Db/ExceptionInput.typeViolation' => '{0}', - 'Exception.JKingWeb/Arsse/Db/ExceptionTimeout.general' => '{0}', - 'Exception.JKingWeb/Arsse/Db/ExceptionSavepoint.invalid' => 'Tried to {action} invalid savepoint {index}', - 'Exception.JKingWeb/Arsse/Db/ExceptionSavepoint.stale' => 'Tried to {action} stale savepoint {index}', - 'Exception.JKingWeb/Arsse/User/Exception.alreadyExists' => 'Could not perform action "{action}" because the user {user} already exists', - 'Exception.JKingWeb/Arsse/User/Exception.doesNotExist' => 'Could not perform action "{action}" because the user {user} does not exist', - 'Exception.JKingWeb/Arsse/User/Exception.authMissing' => 'Please log in to proceed', - 'Exception.JKingWeb/Arsse/User/Exception.authFailed' => 'Authentication failed', - 'Exception.JKingWeb/Arsse/User/ExceptionAuthz.notAuthorized' => + 'Exception.JKingWeb/Arsse/Db/Exception.engineErrorGeneral' => '{0}', + 'Exception.JKingWeb/Arsse/Db/Exception.unknownSavepointStatus' => 'Savepoint status code {0} not implemented', + 'Exception.JKingWeb/Arsse/Db/ExceptionInput.missing' => 'Required field "{field}" missing while performing action "{action}"', + 'Exception.JKingWeb/Arsse/Db/ExceptionInput.whitespace' => 'Field "{field}" of action "{action}" may not contain only whitespace', + 'Exception.JKingWeb/Arsse/Db/ExceptionInput.tooLong' => 'Field "{field}" of action "{action}" has a maximum length of {max}', + 'Exception.JKingWeb/Arsse/Db/ExceptionInput.tooShort' => 'Field "{field}" of action "{action}" has a minimum length of {min}', + 'Exception.JKingWeb/Arsse/Db/ExceptionInput.typeViolation' => 'Field "{field}" of action "{action}" expects a value of type "{type}"', + 'Exception.JKingWeb/Arsse/Db/ExceptionInput.subjectMissing' => 'Referenced ID ({id}) in field "{field}" does not exist', + 'Exception.JKingWeb/Arsse/Db/ExceptionInput.idMissing' => 'Referenced ID ({id}) in field "{field}" does not exist', + 'Exception.JKingWeb/Arsse/Db/ExceptionInput.circularDependence' => 'Referenced ID ({id}) in field "{field}" creates a circular dependence', + 'Exception.JKingWeb/Arsse/Db/ExceptionInput.constraintViolation' => 'Specified value in field "{0}" already exists', + 'Exception.JKingWeb/Arsse/Db/ExceptionInput.engineConstraintViolation' => '{0}', + 'Exception.JKingWeb/Arsse/Db/ExceptionInput.engineTypeViolation' => '{0}', + 'Exception.JKingWeb/Arsse/Db/ExceptionTimeout.general' => '{0}', + 'Exception.JKingWeb/Arsse/Db/ExceptionSavepoint.invalid' => 'Tried to {action} invalid savepoint {index}', + 'Exception.JKingWeb/Arsse/Db/ExceptionSavepoint.stale' => 'Tried to {action} stale savepoint {index}', + 'Exception.JKingWeb/Arsse/User/Exception.alreadyExists' => 'Could not perform action "{action}" because the user {user} already exists', + 'Exception.JKingWeb/Arsse/User/Exception.doesNotExist' => 'Could not perform action "{action}" because the user {user} does not exist', + 'Exception.JKingWeb/Arsse/User/Exception.authMissing' => 'Please log in to proceed', + 'Exception.JKingWeb/Arsse/User/Exception.authFailed' => 'Authentication failed', + 'Exception.JKingWeb/Arsse/User/ExceptionAuthz.notAuthorized' => '{action, select, userList {{user, select, global {Authenticated user is not authorized to view the global user list} @@ -137,15 +139,15 @@ return [ }} other {Authenticated user is not authorized to perform the action "{action}" on behalf of {user}} }', - 'Exception.JKingWeb/Arsse/Feed/Exception.invalidCertificate' => 'Could not download feed "{url}" because its server is serving an invalid SSL certificate', - 'Exception.JKingWeb/Arsse/Feed/Exception.invalidUrl' => 'Feed URL "{url}" is invalid', - 'Exception.JKingWeb/Arsse/Feed/Exception.maxRedirect' => 'Could not download feed "{url}" because its server reached its maximum number of HTTP redirections', - 'Exception.JKingWeb/Arsse/Feed/Exception.maxSize' => 'Could not download feed "{url}" because its size exceeds the maximum allowed on its server', - 'Exception.JKingWeb/Arsse/Feed/Exception.timeout' => 'Could not download feed "{url}" because its server timed out', - 'Exception.JKingWeb/Arsse/Feed/Exception.forbidden' => 'Could not download feed "{url}" because you do not have permission to access it', - 'Exception.JKingWeb/Arsse/Feed/Exception.unauthorized' => 'Could not download feed "{url}" because you provided insufficient or invalid credentials', - 'Exception.JKingWeb/Arsse/Feed/Exception.malformedXml' => 'Could not parse feed "{url}" because it is malformed', - 'Exception.JKingWeb/Arsse/Feed/Exception.xmlEntity' => 'Refused to parse feed "{url}" because it contains an XXE attack', - 'Exception.JKingWeb/Arsse/Feed/Exception.subscriptionNotFound' => 'Unable to find a feed at location "{url}"', - 'Exception.JKingWeb/Arsse/Feed/Exception.unsupportedFeedFormat' => 'Feed "{url}" is of an unsupported format', + 'Exception.JKingWeb/Arsse/Feed/Exception.invalidCertificate' => 'Could not download feed "{url}" because its server is serving an invalid SSL certificate', + 'Exception.JKingWeb/Arsse/Feed/Exception.invalidUrl' => 'Feed URL "{url}" is invalid', + 'Exception.JKingWeb/Arsse/Feed/Exception.maxRedirect' => 'Could not download feed "{url}" because its server reached its maximum number of HTTP redirections', + 'Exception.JKingWeb/Arsse/Feed/Exception.maxSize' => 'Could not download feed "{url}" because its size exceeds the maximum allowed on its server', + 'Exception.JKingWeb/Arsse/Feed/Exception.timeout' => 'Could not download feed "{url}" because its server timed out', + 'Exception.JKingWeb/Arsse/Feed/Exception.forbidden' => 'Could not download feed "{url}" because you do not have permission to access it', + 'Exception.JKingWeb/Arsse/Feed/Exception.unauthorized' => 'Could not download feed "{url}" because you provided insufficient or invalid credentials', + 'Exception.JKingWeb/Arsse/Feed/Exception.malformedXml' => 'Could not parse feed "{url}" because it is malformed', + 'Exception.JKingWeb/Arsse/Feed/Exception.xmlEntity' => 'Refused to parse feed "{url}" because it contains an XXE attack', + 'Exception.JKingWeb/Arsse/Feed/Exception.subscriptionNotFound' => 'Unable to find a feed at location "{url}"', + 'Exception.JKingWeb/Arsse/Feed/Exception.unsupportedFeedFormat' => 'Feed "{url}" is of an unsupported format', ]; \ No newline at end of file diff --git a/tests/lib/Database/SeriesFolder.php b/tests/lib/Database/SeriesFolder.php index 4fe7ea2..16312f2 100644 --- a/tests/lib/Database/SeriesFolder.php +++ b/tests/lib/Database/SeriesFolder.php @@ -76,6 +76,11 @@ trait SeriesFolder { Arsse::$db->folderAdd("john.doe@example.com", ['name' => "Sociology", 'parent' => 2112]); } + public function testAddANestedFolderToAnInvalidParent() { + $this->assertException("idMissing", "Db", "ExceptionInput"); + Arsse::$db->folderAdd("john.doe@example.com", ['name' => "Sociology", 'parent' => "stringFolderId"]); + } + public function testAddANestedFolderForTheWrongOwner() { $this->assertException("idMissing", "Db", "ExceptionInput"); Arsse::$db->folderAdd("john.doe@example.com", ['name' => "Sociology", 'parent' => 4]); // folder ID 4 belongs to Jane @@ -216,6 +221,10 @@ trait SeriesFolder { Arsse::$db->folderPropertiesGet("john.doe@example.com", 1); } + public function testMakeNoChangesToAFolder() { + $this->assertFalse(Arsse::$db->folderPropertiesSet("john.doe@example.com", 6, [])); + } + public function testRenameAFolder() { $this->assertTrue(Arsse::$db->folderPropertiesSet("john.doe@example.com", 6, ['name' => "Opinion"])); Phake::verify(Arsse::$user)->authorize("john.doe@example.com", "folderPropertiesSet"); @@ -234,6 +243,11 @@ trait SeriesFolder { $this->assertTrue(Arsse::$db->folderPropertiesSet("john.doe@example.com", 6, ['name' => " "])); } + public function testRenameAFolderToAnInvalidValue() { + $this->assertException("typeViolation", "Db", "ExceptionInput"); + $this->assertTrue(Arsse::$db->folderPropertiesSet("john.doe@example.com", 6, ['name' => []])); + } + public function testMoveAFolder() { $this->assertTrue(Arsse::$db->folderPropertiesSet("john.doe@example.com", 6, ['parent' => 5])); Phake::verify(Arsse::$user)->authorize("john.doe@example.com", "folderPropertiesSet"); @@ -242,6 +256,11 @@ trait SeriesFolder { $this->compareExpectations($state); } + public function testMoveTheRootFolder() { + $this->assertException("circularDependence", "Db", "ExceptionInput"); + Arsse::$db->folderPropertiesSet("john.doe@example.com", 0, ['parent' => 1]); + } + public function testMoveAFolderToItsDescendant() { $this->assertException("circularDependence", "Db", "ExceptionInput"); Arsse::$db->folderPropertiesSet("john.doe@example.com", 1, ['parent' => 3]); @@ -257,11 +276,21 @@ trait SeriesFolder { Arsse::$db->folderPropertiesSet("john.doe@example.com", 1, ['parent' => 2112]); } + public function testMoveAFolderToAnInvalidParent() { + $this->assertException("idMissing", "Db", "ExceptionInput"); + Arsse::$db->folderPropertiesSet("john.doe@example.com", 1, ['parent' => "ThisFolderDoesNotExist"]); + } + public function testCauseAFolderCollision() { $this->assertException("constraintViolation", "Db", "ExceptionInput"); Arsse::$db->folderPropertiesSet("john.doe@example.com", 6, ['parent' => null]); } + public function testCauseACompoundFolderCollision() { + $this->assertException("constraintViolation", "Db", "ExceptionInput"); + Arsse::$db->folderPropertiesSet("john.doe@example.com", 3, ['parent' => null, 'name' => "Technology"]); + } + public function testSetThePropertiesOfAMissingFolder() { $this->assertException("subjectMissing", "Db", "ExceptionInput"); Arsse::$db->folderPropertiesSet("john.doe@example.com", 2112, ['parent' => null]); diff --git a/tests/lib/Database/SeriesSubscription.php b/tests/lib/Database/SeriesSubscription.php index 5ca9643..12f5760 100644 --- a/tests/lib/Database/SeriesSubscription.php +++ b/tests/lib/Database/SeriesSubscription.php @@ -319,6 +319,11 @@ trait SeriesSubscription { $this->assertTrue(Arsse::$db->subscriptionPropertiesSet($this->user, 1, ['title' => 0])); } + public function testRenameASubscriptionToAnArray() { + $this->assertException("typeViolation", "Db", "ExceptionInput"); + Arsse::$db->subscriptionPropertiesSet($this->user, 1, ['title' => []]); + } + public function testSetThePropertiesOfAMissingSubscription() { $this->assertException("subjectMissing", "Db", "ExceptionInput"); Arsse::$db->subscriptionPropertiesSet($this->user, 2112, ['folder' => null]);