From 26f6922b25cc22391daaaa2691a3c6c7aaeeb200 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Thu, 5 Oct 2017 17:42:12 -0400 Subject: [PATCH] Partially implement labels - Backend functions for adding, listing, removing, and editing (renaming) labels currently implemented - TTRSS functions for adding (fixes #96), removing (fixes #97), and renaming (fixes #98) labels currently implemented --- lib/Database.php | 142 ++++- lib/REST.php | 7 +- lib/REST/TinyTinyRSS/API.php | 58 ++ sql/SQLite3/1.sql | 7 +- .../Database/TestDatabaseLabelSQLite3.php | 10 + tests/REST/TinyTinyRSS/TestTinyTinyAPI.php | 119 ++++ tests/lib/Database/SeriesLabel.php | 523 ++++++++++++++++++ tests/lib/Database/SeriesSubscription.php | 3 + tests/lib/Database/SeriesUser.php | 3 + tests/phpunit.xml | 1 + 10 files changed, 859 insertions(+), 14 deletions(-) create mode 100644 tests/Db/SQLite3/Database/TestDatabaseLabelSQLite3.php create mode 100644 tests/lib/Database/SeriesLabel.php diff --git a/lib/Database.php b/lib/Database.php index 47c74deb..e50312cf 100644 --- a/lib/Database.php +++ b/lib/Database.php @@ -204,6 +204,10 @@ class Database { "name" => "str", ]; list($setClause, $setTypes, $setValues) = $this->generateSet($properties, $valid); + if (!$setClause) { + // if no changes would actually be applied, just return + return $this->userPropertiesGet($user); + } $this->db->prepare("UPDATE arsse_users set $setClause where id is ?", $setTypes, "str")->run($setValues, $user); return $this->userPropertiesGet($user); } @@ -314,7 +318,7 @@ class Database { 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"]); + throw new Db\ExceptionInput("typeViolation", ["action" => __FUNCTION__, "field" => "folder", '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) { @@ -328,7 +332,7 @@ class Database { 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"]); + throw new Db\ExceptionInput("typeViolation", ["action" => __FUNCTION__, "field" => "folder", '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) { @@ -362,7 +366,7 @@ class Database { // if a new parent is specified, validate it $in['parent'] = $this->folderValidateMove($user, (int) $id, $data['parent']); } else { - // if neither was specified, do nothing + // if no changes would actually be applied, just return return false; } $valid = [ @@ -547,7 +551,7 @@ class Database { 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"]); + throw new Db\ExceptionInput("typeViolation", ["action" => __FUNCTION__, "field" => "feed", '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) { @@ -561,7 +565,7 @@ class Database { 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"]); + throw new Db\ExceptionInput("typeViolation", ["action" => __FUNCTION__, "field" => "feed", 'type' => "int > 0"]); } // disable authorization checks for the list call Arsse::$user->authorizationEnabled(false); @@ -604,14 +608,18 @@ class Database { 'pinned' => "strict bool", ]; list($setClause, $setTypes, $setValues) = $this->generateSet($data, $valid); - $out = (bool) $this->db->prepare("UPDATE arsse_subscriptions set $setClause where owner is ? and id is ?", $setTypes, "str", "int")->run($setValues, $user, $id)->changes(); + if (!$setClause) { + // if no changes would actually be applied, just return + return false; + } + $out = (bool) $this->db->prepare("UPDATE arsse_subscriptions set $setClause, modified = CURRENT_TIMESTAMP where owner is ? and id is ?", $setTypes, "str", "int")->run($setValues, $user, $id)->changes(); $tr->commit(); return $out; } 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"]); + throw new Db\ExceptionInput("typeViolation", ["action" => $this->caller(), "field" => "feed", '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) { @@ -1051,7 +1059,7 @@ class Database { 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 + throw new Db\ExceptionInput("typeViolation", ["action" => $this->caller(), "field" => "article", 'type' => "int > 0"]); // @codeCoverageIgnore } $out = $this->db->prepare( "SELECT @@ -1072,7 +1080,7 @@ 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 + throw new Db\ExceptionInput("typeViolation", ["action" => $this->caller(), "field" => "edition", 'type' => "int > 0"]); // @codeCoverageIgnore } $out = $this->db->prepare( "SELECT @@ -1112,4 +1120,120 @@ class Database { } return (int) $this->db->prepare($q->getQuery(), $q->getTypes())->run($q->getValues())->getValue(); } + + public function labelAdd(string $user, array $data): int { + // 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]); + } + // validate the label name + $name = array_key_exists("name", $data) ? $data['name'] : ""; + $this->labelValidateName($name, true); + // perform the insert + return $this->db->prepare("INSERT INTO arsse_labels(owner,name) values(?,?)", "str", "str")->run($user, $name)->lastId(); + } + + public function labelList(string $user, bool $includeEmpty = 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]); + } + return $this->db->prepare( + "SELECT + id,name, + (select count(*) from arsse_label_members where owner is ? and label is arsse_labels.id) as articles + FROM arsse_labels where owner is ? and articles >= ? + ", "str", "str", "int" + )->run($user, $user, !$includeEmpty); + } + + public function labelRemove(string $user, $id, bool $byName = false): bool { + if (!Arsse::$user->authorize($user, __FUNCTION__)) { + throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]); + } + if (!$byName && !ValueInfo::id($id)) { + // if we're not referring to a label by name and the ID is invalid, throw an exception + throw new Db\ExceptionInput("typeViolation", ["action" => __FUNCTION__, "field" => "label", 'type' => "int > 0"]); + } elseif ($byName && !(ValueInfo::str($id) & ValueInfo::VALID)) { + // otherwise if we are referring to a label by name but the ID is not a string, also throw an exception + throw new Db\ExceptionInput("typeViolation", ["action" => __FUNCTION__, "field" => "label", 'type' => "string"]); + } + $field = $byName ? "name" : "id"; + $type = $byName ? "str" : "int"; + $changes = $this->db->prepare("DELETE FROM arsse_labels where owner is ? and $field is ?", "str", $type)->run($user, $id)->changes(); + if (!$changes) { + throw new Db\ExceptionInput("subjectMissing", ["action" => __FUNCTION__, "field" => "label", 'id' => $id]); + } + return true; + } + + public function labelPropertiesGet(string $user, $id, bool $byName = false): array { + if (!Arsse::$user->authorize($user, __FUNCTION__)) { + throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]); + } + if (!$byName && !ValueInfo::id($id)) { + // if we're not referring to a label by name and the ID is invalid, throw an exception + throw new Db\ExceptionInput("typeViolation", ["action" => __FUNCTION__, "field" => "label", 'type' => "int > 0"]); + } elseif ($byName && !(ValueInfo::str($id) & ValueInfo::VALID)) { + // otherwise if we are referring to a label by name but the ID is not a string, also throw an exception + throw new Db\ExceptionInput("typeViolation", ["action" => __FUNCTION__, "field" => "label", 'type' => "string"]); + } + $field = $byName ? "name" : "id"; + $type = $byName ? "str" : "int"; + $out = $this->db->prepare( + "SELECT + id,name, + (select count(*) from arsse_label_members where owner is ? and label is arsse_labels.id) as articles + FROM arsse_labels where $field is ? and owner is ? + ", "str", $type, "str" + )->run($user, $id, $user)->getRow(); + if (!$out) { + throw new Db\ExceptionInput("subjectMissing", ["action" => __FUNCTION__, "field" => "label", 'id' => $id]); + } + return $out; + } + + public function labelPropertiesSet(string $user, $id, array $data, bool $byName = false): bool { + if (!Arsse::$user->authorize($user, __FUNCTION__)) { + throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]); + } + if (!$byName && !ValueInfo::id($id)) { + // if we're not referring to a label by name and the ID is invalid, throw an exception + throw new Db\ExceptionInput("typeViolation", ["action" => __FUNCTION__, "field" => "label", 'type' => "int > 0"]); + } elseif ($byName && !(ValueInfo::str($id) & ValueInfo::VALID)) { + // otherwise if we are referring to a label by name but the ID is not a string, also throw an exception + throw new Db\ExceptionInput("typeViolation", ["action" => __FUNCTION__, "field" => "label", 'type' => "string"]); + } + if (isset($data['name'])) { + $this->labelValidateName($data['name']); + } + $field = $byName ? "name" : "id"; + $type = $byName ? "str" : "int"; + $valid = [ + 'name' => "str", + ]; + list($setClause, $setTypes, $setValues) = $this->generateSet($data, $valid); + if (!$setClause) { + // if no changes would actually be applied, just return + return false; + } + $out = (bool) $this->db->prepare("UPDATE arsse_labels set $setClause, modified = CURRENT_TIMESTAMP where owner is ? and $field is ?", $setTypes, "str", $type)->run($setValues, $user, $id)->changes(); + if (!$out) { + throw new Db\ExceptionInput("subjectMissing", ["action" => __FUNCTION__, "field" => "label", 'id' => $id]); + } + return $out; + } + + protected function labelValidateName($name): bool { + $info = ValueInfo::str($name); + if ($info & (ValueInfo::NULL | ValueInfo::EMPTY)) { + throw new Db\ExceptionInput("missing", ["action" => $this->caller(), "field" => "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"]); + } else { + return true; + } + } } diff --git a/lib/REST.php b/lib/REST.php index 15bfac73..c340e37f 100644 --- a/lib/REST.php +++ b/lib/REST.php @@ -26,9 +26,14 @@ class REST { // Feedbin v1 https://github.com/feedbin/feedbin-api/commit/86da10aac5f1a57531a6e17b08744e5f9e7db8a9 // Feedbin v2 https://github.com/feedbin/feedbin-api // Fever https://feedafever.com/api - // NewsBlur http://www.newsblur.com/api + // Google Reader http://feedhq.readthedocs.io/en/latest/api/index.html // Miniflux https://github.com/miniflux/miniflux/blob/master/docs/json-rpc-api.markdown // CommaFeed https://www.commafeed.com/api/ + // Selfoss https://github.com/SSilence/selfoss/wiki/Restful-API-for-Apps-or-any-other-external-access + // BirdReader https://github.com/glynnbird/birdreader/blob/master/API.md + // Proprietary (centralized) entities: + // NewsBlur http://www.newsblur.com/api + // Feedly https://developer.feedly.com/ ]; public function __construct() { diff --git a/lib/REST/TinyTinyRSS/API.php b/lib/REST/TinyTinyRSS/API.php index c50703e8..2fed94a6 100644 --- a/lib/REST/TinyTinyRSS/API.php +++ b/lib/REST/TinyTinyRSS/API.php @@ -20,6 +20,8 @@ Protocol difference so far: - TT-RSS accepts whitespace-only names; we do not - TT-RSS allows two folders to share the same name under the same parent; we do not - Session lifetime is much shorter by default (does TT-RSS even expire sessions?) + - Categories and feeds will always be sorted alphabetically (the protocol does not allow for clients to re-order) + - Label IDs decrease from -11 instead of from -1025 */ @@ -392,4 +394,60 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler { } return ['status' => "OK"]; } + + protected function labelIn($id): int { + if (!(ValueInfo::int($id) & ValueInfo::NEG) || $id > -11) { + throw new Exception("INCORRECT_USAGE"); + } + return (abs($id) - 10); + } + + protected function labelOut(int $id): int { + return ($id * -1 - 10); + } + + public function opAddLabel(array $data) { + $in = [ + 'name' => isset($data['caption']) ? $data['caption'] : "", + ]; + try { + return $this->labelOut(Arsse::$db->labelAdd(Arsse::$user->id, $in)); + } catch (ExceptionInput $e) { + switch ($e->getCode()) { + case 10236: // label already exists + // retrieve the ID of the existing label; duplicating a label silently returns the existing one + return $this->labelOut(Arsse::$db->labelPropertiesGet(Arsse::$user->id, $in['name'], true)['id']); + default: // other errors related to input + throw new Exception("INCORRECT_USAGE"); + } + } + } + + public function opRemoveLabel(array $data) { + // normalize the label ID; missing or invalid IDs are rejected + $id = $this->labelIn(isset($data['label_id']) ? $data['label_id'] : 0); + try { + // attempt to remove the label + Arsse::$db->labelRemove(Arsse::$user->id, $id); + } catch(ExceptionInput $e) { + // ignore all errors + } + return null; + } + + public function opRenameLabel(array $data) { + // normalize input; missing or invalid IDs are rejected + $id = $this->labelIn(isset($data['label_id']) ? $data['label_id'] : 0); + $name = isset($data['caption']) ? $data['caption'] : ""; + try { + // try to rename the folder + Arsse::$db->labelPropertiesSet(Arsse::$user->id, $id, ['name' => $name]); + } catch(ExceptionInput $e) { + if ($e->getCode()==10237) { + // if the supplied ID was invalid, report an error; other errors are to be ignored + throw new Exception("INCORRECT_USAGE"); + } + } + return null; + } } diff --git a/sql/SQLite3/1.sql b/sql/SQLite3/1.sql index 8a50ed1d..50e75678 100644 --- a/sql/SQLite3/1.sql +++ b/sql/SQLite3/1.sql @@ -1,8 +1,8 @@ -- Sessions for Tiny Tiny RSS (and possibly others) create table arsse_sessions ( id text primary key, -- UUID of session - created datetime not null default CURRENT_TIMESTAMP, -- Session start timestamp - expires datetime not null, -- Time at which session is no longer valid + created text not null default CURRENT_TIMESTAMP, -- Session start timestamp + expires text not null, -- Time at which session is no longer valid user text not null references arsse_users(id) on delete cascade on update cascade -- user associated with the session ) without rowid; @@ -11,8 +11,7 @@ create table arsse_labels ( id integer primary key, -- numeric ID owner text not null references arsse_users(id) on delete cascade on update cascade, -- owning user name text not null, -- label text - foreground text, -- foreground (text) colour in hexdecimal RGB - background text, -- background colour in hexadecimal RGB + modified text not null default CURRENT_TIMESTAMP, -- time at which the label was last modified unique(owner,name) ); diff --git a/tests/Db/SQLite3/Database/TestDatabaseLabelSQLite3.php b/tests/Db/SQLite3/Database/TestDatabaseLabelSQLite3.php new file mode 100644 index 00000000..815bd490 --- /dev/null +++ b/tests/Db/SQLite3/Database/TestDatabaseLabelSQLite3.php @@ -0,0 +1,10 @@ + */ +class TestDatabaseLabelSQLite3 extends Test\AbstractTest { + use Test\Database\Setup; + use Test\Database\DriverSQLite3; + use Test\Database\SeriesLabel; +} diff --git a/tests/REST/TinyTinyRSS/TestTinyTinyAPI.php b/tests/REST/TinyTinyRSS/TestTinyTinyAPI.php index b96bc010..0294b1df 100644 --- a/tests/REST/TinyTinyRSS/TestTinyTinyAPI.php +++ b/tests/REST/TinyTinyRSS/TestTinyTinyAPI.php @@ -629,4 +629,123 @@ class TestTinyTinyAPI extends Test\AbstractTest { $this->assertEquals($exp, $this->h->dispatch(new Request("POST", "", json_encode($in[2])))); $this->assertEquals($exp, $this->h->dispatch(new Request("POST", "", json_encode($in[3])))); } + + public function testAddALabel() { + $in = [ + ['op' => "addLabel", 'sid' => "PriestsOfSyrinx", 'caption' => "Software"], + ['op' => "addLabel", 'sid' => "PriestsOfSyrinx", 'caption' => "Hardware",], + ['op' => "addLabel", 'sid' => "PriestsOfSyrinx"], + ['op' => "addLabel", 'sid' => "PriestsOfSyrinx", 'caption' => ""], + ['op' => "addLabel", 'sid' => "PriestsOfSyrinx", 'caption' => " "], + ]; + $db = [ + ['name' => "Software"], + ['name' => "Hardware"], + ]; + $out = [ + ['id' => 2, 'name' => "Software"], + ['id' => 3, 'name' => "Hardware"], + ['id' => 1, 'name' => "Politics"], + ]; + // set of various mocks for testing + Phake::when(Arsse::$db)->labelAdd(Arsse::$user->id, $db[0])->thenReturn(2)->thenThrow(new ExceptionInput("constraintViolation")); // error on the second call + Phake::when(Arsse::$db)->labelAdd(Arsse::$user->id, $db[1])->thenReturn(3)->thenThrow(new ExceptionInput("constraintViolation")); // error on the second call + Phake::when(Arsse::$db)->labelPropertiesGet(Arsse::$user->id, "Software", true)->thenReturn($out[0]); + Phake::when(Arsse::$db)->labelPropertiesGet(Arsse::$user->id, "Hardware", true)->thenReturn($out[1]); + // set up mocks that produce errors + Phake::when(Arsse::$db)->labelAdd(Arsse::$user->id, [])->thenThrow(new ExceptionInput("missing")); + Phake::when(Arsse::$db)->labelAdd(Arsse::$user->id, ['name' => ""])->thenThrow(new ExceptionInput("missing")); + Phake::when(Arsse::$db)->labelAdd(Arsse::$user->id, ['name' => " "])->thenThrow(new ExceptionInput("whitespace")); + // correctly add two labels + $exp = $this->respGood(-12); + $this->assertEquals($exp, $this->h->dispatch(new Request("POST", "", json_encode($in[0])))); + $exp = $this->respGood(-13); + $this->assertEquals($exp, $this->h->dispatch(new Request("POST", "", json_encode($in[1])))); + // attempt to add the two labels again + $exp = $this->respGood(-12); + $this->assertEquals($exp, $this->h->dispatch(new Request("POST", "", json_encode($in[0])))); + $exp = $this->respGood(-13); + $this->assertEquals($exp, $this->h->dispatch(new Request("POST", "", json_encode($in[1])))); + Phake::verify(Arsse::$db)->labelPropertiesGet(Arsse::$user->id, "Software", true); + Phake::verify(Arsse::$db)->labelPropertiesGet(Arsse::$user->id, "Hardware", true); + // add some invalid labels + $exp = $this->respErr("INCORRECT_USAGE"); + $this->assertEquals($exp, $this->h->dispatch(new Request("POST", "", json_encode($in[2])))); + $this->assertEquals($exp, $this->h->dispatch(new Request("POST", "", json_encode($in[3])))); + $this->assertEquals($exp, $this->h->dispatch(new Request("POST", "", json_encode($in[4])))); + } + + public function testRemoveALabel() { + $in = [ + ['op' => "removeLabel", 'sid' => "PriestsOfSyrinx", 'label_id' => -42], + ['op' => "removeLabel", 'sid' => "PriestsOfSyrinx", 'label_id' => -2112], + ['op' => "removeLabel", 'sid' => "PriestsOfSyrinx", 'label_id' => 1], + ['op' => "removeLabel", 'sid' => "PriestsOfSyrinx", 'label_id' => 0], + ['op' => "removeLabel", 'sid' => "PriestsOfSyrinx", 'label_id' => -10], + ]; + Phake::when(Arsse::$db)->labelRemove(Arsse::$user->id, $this->anything())->thenThrow(new ExceptionInput("subjectMissing")); + Phake::when(Arsse::$db)->labelRemove(Arsse::$user->id, 32)->thenReturn(true)->thenThrow(new ExceptionInput("subjectMissing")); + // succefully delete a label + $exp = $this->respGood(); + $this->assertEquals($exp, $this->h->dispatch(new Request("POST", "", json_encode($in[0])))); + // try deleting it again (this should silently fail) + $exp = $this->respGood(); + $this->assertEquals($exp, $this->h->dispatch(new Request("POST", "", json_encode($in[0])))); + // delete a label which does not exist (this should also silently fail) + $exp = $this->respGood(); + $this->assertEquals($exp, $this->h->dispatch(new Request("POST", "", json_encode($in[1])))); + // delete some invalid labels (causes an error) + $exp = $this->respErr("INCORRECT_USAGE"); + $this->assertEquals($exp, $this->h->dispatch(new Request("POST", "", json_encode($in[2])))); + $this->assertEquals($exp, $this->h->dispatch(new Request("POST", "", json_encode($in[3])))); + $this->assertEquals($exp, $this->h->dispatch(new Request("POST", "", json_encode($in[4])))); + Phake::verify(Arsse::$db, Phake::times(2))->labelRemove(Arsse::$user->id, 32); + Phake::verify(Arsse::$db)->labelRemove(Arsse::$user->id, 2102); + } + + public function testRenameALabel() { + $in = [ + ['op' => "renameLabel", 'sid' => "PriestsOfSyrinx", 'label_id' => -42, 'caption' => "Ook"], + ['op' => "renameLabel", 'sid' => "PriestsOfSyrinx", 'label_id' => -2112, 'caption' => "Eek"], + ['op' => "renameLabel", 'sid' => "PriestsOfSyrinx", 'label_id' => -42, 'caption' => "Eek"], + ['op' => "renameLabel", 'sid' => "PriestsOfSyrinx", 'label_id' => -42, 'caption' => ""], + ['op' => "renameLabel", 'sid' => "PriestsOfSyrinx", 'label_id' => -42, 'caption' => " "], + ['op' => "renameLabel", 'sid' => "PriestsOfSyrinx", 'label_id' => -42], + ['op' => "renameLabel", 'sid' => "PriestsOfSyrinx", 'label_id' => -1, 'caption' => "Ook"], + ['op' => "renameLabel", 'sid' => "PriestsOfSyrinx", 'caption' => "Ook"], + ['op' => "renameLabel", 'sid' => "PriestsOfSyrinx"], + ]; + $db = [ + [Arsse::$user->id, 32, ['name' => "Ook"]], + [Arsse::$user->id, 2102, ['name' => "Eek"]], + [Arsse::$user->id, 32, ['name' => "Eek"]], + [Arsse::$user->id, 32, ['name' => ""]], + [Arsse::$user->id, 32, ['name' => " "]], + [Arsse::$user->id, 32, ['name' => ""]], + ]; + Phake::when(Arsse::$db)->labelPropertiesSet(...$db[0])->thenReturn(true); + Phake::when(Arsse::$db)->labelPropertiesSet(...$db[1])->thenThrow(new ExceptionInput("subjectMissing")); + Phake::when(Arsse::$db)->labelPropertiesSet(...$db[2])->thenThrow(new ExceptionInput("constraintViolation")); + Phake::when(Arsse::$db)->labelPropertiesSet(...$db[3])->thenThrow(new ExceptionInput("typeViolation")); + Phake::when(Arsse::$db)->labelPropertiesSet(...$db[4])->thenThrow(new ExceptionInput("typeViolation")); + Phake::when(Arsse::$db)->labelPropertiesSet(...$db[5])->thenThrow(new ExceptionInput("typeViolation")); + // succefully rename a label + $exp = $this->respGood(); + $this->assertEquals($exp, $this->h->dispatch(new Request("POST", "", json_encode($in[0])))); + // rename a label which does not exist (this should silently fail) + $exp = $this->respGood(); + $this->assertEquals($exp, $this->h->dispatch(new Request("POST", "", json_encode($in[1])))); + // rename a label causing a duplication (this should also silently fail) + $exp = $this->respGood(); + $this->assertEquals($exp, $this->h->dispatch(new Request("POST", "", json_encode($in[2])))); + // all the rest should cause errors + $exp = $this->respErr("INCORRECT_USAGE"); + $this->assertEquals($exp, $this->h->dispatch(new Request("POST", "", json_encode($in[3])))); + $this->assertEquals($exp, $this->h->dispatch(new Request("POST", "", json_encode($in[4])))); + $this->assertEquals($exp, $this->h->dispatch(new Request("POST", "", json_encode($in[5])))); + $this->assertEquals($exp, $this->h->dispatch(new Request("POST", "", json_encode($in[6])))); + $this->assertEquals($exp, $this->h->dispatch(new Request("POST", "", json_encode($in[7])))); + $this->assertEquals($exp, $this->h->dispatch(new Request("POST", "", json_encode($in[8])))); + Phake::verify(Arsse::$db, Phake::times(6))->labelPropertiesSet(Arsse::$user->id, $this->anything(), $this->anything()); + } } diff --git a/tests/lib/Database/SeriesLabel.php b/tests/lib/Database/SeriesLabel.php new file mode 100644 index 00000000..9e4f0a4f --- /dev/null +++ b/tests/lib/Database/SeriesLabel.php @@ -0,0 +1,523 @@ + [ + 'columns' => [ + 'id' => 'str', + 'password' => 'str', + 'name' => 'str', + ], + 'rows' => [ + ["jane.doe@example.com", "", "Jane Doe"], + ["john.doe@example.com", "", "John Doe"], + ["john.doe@example.org", "", "John Doe"], + ["john.doe@example.net", "", "John Doe"], + ], + ], + 'arsse_folders' => [ + 'columns' => [ + 'id' => "int", + 'owner' => "str", + 'parent' => "int", + 'name' => "str", + ], + 'rows' => [ + [1, "john.doe@example.com", null, "Technology"], + [2, "john.doe@example.com", 1, "Software"], + [3, "john.doe@example.com", 1, "Rocketry"], + [4, "jane.doe@example.com", null, "Politics"], + [5, "john.doe@example.com", null, "Politics"], + [6, "john.doe@example.com", 2, "Politics"], + [7, "john.doe@example.net", null, "Technology"], + [8, "john.doe@example.net", 7, "Software"], + [9, "john.doe@example.net", null, "Politics"], + ] + ], + 'arsse_feeds' => [ + 'columns' => [ + 'id' => "int", + 'url' => "str", + ], + 'rows' => [ + [1,"http://example.com/1"], + [2,"http://example.com/2"], + [3,"http://example.com/3"], + [4,"http://example.com/4"], + [5,"http://example.com/5"], + [6,"http://example.com/6"], + [7,"http://example.com/7"], + [8,"http://example.com/8"], + [9,"http://example.com/9"], + [10,"http://example.com/10"], + [11,"http://example.com/11"], + [12,"http://example.com/12"], + [13,"http://example.com/13"], + ] + ], + 'arsse_subscriptions' => [ + 'columns' => [ + 'id' => "int", + 'owner' => "str", + 'feed' => "int", + 'folder' => "int", + ], + 'rows' => [ + [1,"john.doe@example.com",1,null], + [2,"john.doe@example.com",2,null], + [3,"john.doe@example.com",3,1], + [4,"john.doe@example.com",4,6], + [5,"john.doe@example.com",10,5], + [6,"jane.doe@example.com",1,null], + [7,"jane.doe@example.com",10,null], + [8,"john.doe@example.org",11,null], + [9,"john.doe@example.org",12,null], + [10,"john.doe@example.org",13,null], + [11,"john.doe@example.net",10,null], + [12,"john.doe@example.net",2,9], + [13,"john.doe@example.net",3,8], + [14,"john.doe@example.net",4,7], + ] + ], + 'arsse_articles' => [ + 'columns' => [ + 'id' => "int", + 'feed' => "int", + 'url' => "str", + 'title' => "str", + 'author' => "str", + 'published' => "datetime", + 'edited' => "datetime", + 'content' => "str", + 'guid' => "str", + 'url_title_hash' => "str", + 'url_content_hash' => "str", + 'title_content_hash' => "str", + 'modified' => "datetime", + ], + 'rows' => [ + [1,1,null,null,null,null,null,null,null,"","","","2000-01-01T00:00:00Z"], + [2,1,null,null,null,null,null,null,null,"","","","2010-01-01T00:00:00Z"], + [3,2,null,null,null,null,null,null,null,"","","","2000-01-01T00:00:00Z"], + [4,2,null,null,null,null,null,null,null,"","","","2010-01-01T00:00:00Z"], + [5,3,null,null,null,null,null,null,null,"","","","2000-01-01T00:00:00Z"], + [6,3,null,null,null,null,null,null,null,"","","","2010-01-01T00:00:00Z"], + [7,4,null,null,null,null,null,null,null,"","","","2000-01-01T00:00:00Z"], + [8,4,null,null,null,null,null,null,null,"","","","2010-01-01T00:00:00Z"], + [9,5,null,null,null,null,null,null,null,"","","","2000-01-01T00:00:00Z"], + [10,5,null,null,null,null,null,null,null,"","","","2010-01-01T00:00:00Z"], + [11,6,null,null,null,null,null,null,null,"","","","2000-01-01T00:00:00Z"], + [12,6,null,null,null,null,null,null,null,"","","","2010-01-01T00:00:00Z"], + [13,7,null,null,null,null,null,null,null,"","","","2000-01-01T00:00:00Z"], + [14,7,null,null,null,null,null,null,null,"","","","2010-01-01T00:00:00Z"], + [15,8,null,null,null,null,null,null,null,"","","","2000-01-01T00:00:00Z"], + [16,8,null,null,null,null,null,null,null,"","","","2010-01-01T00:00:00Z"], + [17,9,null,null,null,null,null,null,null,"","","","2000-01-01T00:00:00Z"], + [18,9,null,null,null,null,null,null,null,"","","","2010-01-01T00:00:00Z"], + [19,10,null,null,null,null,null,null,null,"","","","2000-01-01T00:00:00Z"], + [20,10,null,null,null,null,null,null,null,"","","","2010-01-01T00:00:00Z"], + [101,11,'http://example.com/1','Article title 1','','2000-01-01 00:00:00','2000-01-01 00:00:01','

Article content 1

','e433653cef2e572eee4215fa299a4a5af9137b2cefd6283c85bd69a32915beda','f5cb8bfc1c7396dc9816af212a3e2ac5221585c2a00bf7ccb6aabd95dcfcd6a6','fb0bc8f8cb08913dc5a497db700e327f1d34e4987402687d494a5891f24714d4','18fdd4fa93d693128c43b004399e5c9cea6c261ddfa002518d3669f55d8c2207','2000-01-01 01:00:00'], + [102,11,'http://example.com/2','Article title 2','','2000-01-02 00:00:00','2000-01-02 00:00:02','

Article content 2

','5be8a5a46ecd52ed132191c8d27fb1af6b3d4edc00234c5d9f8f0e10562ed3b7','0e86d2de822a174fe3c44a466953e63ca1f1a58a19cbf475fce0855d4e3d5153','13075894189c47ffcfafd1dfe7fbb539f7c74a69d35a399b3abf8518952714f9','2abd0a8cba83b8214a66c8f0293ba63e467d720540e29ff8ddcdab069d4f1c9e','2000-01-02 02:00:00'], + [103,12,'http://example.com/3','Article title 3','','2000-01-03 00:00:00','2000-01-03 00:00:03','

Article content 3

','31a6594500a48b59fcc8a075ce82b946c9c3c782460d088bd7b8ef3ede97ad92','f74b06b240bd08abf4d3fdfc20dba6a6f6eb8b4f1a00e9a617efd63a87180a4b','b278380e984cefe63f0e412b88ffc9cb0befdfa06fdc00bace1da99a8daff406','ad622b31e739cd3a3f3c788991082cf4d2f7a8773773008e75f0572e58cd373b','2000-01-03 03:00:00'], + [104,12,'http://example.com/4','Article title 4','','2000-01-04 00:00:00','2000-01-04 00:00:04','

Article content 4

','804e517d623390e71497982c77cf6823180342ebcd2e7d5e32da1e55b09dd180','f3615c7f16336d3ea242d35cf3fc17dbc4ee3afb78376bf49da2dd7a5a25dec8','f11c2b4046f207579aeb9c69a8c20ca5461cef49756ccfa5ba5e2344266da3b3','ab2da63276acce431250b18d3d49b988b226a99c7faadf275c90b751aee05be9','2000-01-04 04:00:00'], + [105,13,'http://example.com/5','Article title 5','','2000-01-05 00:00:00','2000-01-05 00:00:05','

Article content 5

','db3e736c2c492f5def5c5da33ddcbea1824040e9ced2142069276b0a6e291a41','d40da96e39eea6c55948ccbe9b3d275b5f931298288dbe953990c5f496097022','834240f84501b5341d375414718204ec421561f3825d34c22bf9182203e42900','43b970ac6ec5f8a9647b2c7e4eed8b1d7f62e154a95eed748b0294c1256764ba','2000-01-05 05:00:00'], + ] + ], + 'arsse_enclosures' => [ + 'columns' => [ + 'article' => "int", + 'url' => "str", + 'type' => "str", + ], + 'rows' => [ + [102,"http://example.com/text","text/plain"], + [103,"http://example.com/video","video/webm"], + [104,"http://example.com/image","image/svg+xml"], + [105,"http://example.com/audio","audio/ogg"], + + ] + ], + 'arsse_editions' => [ + 'columns' => [ + 'id' => "int", + 'article' => "int", + ], + 'rows' => [ + [1,1], + [2,2], + [3,3], + [4,4], + [5,5], + [6,6], + [7,7], + [8,8], + [9,9], + [10,10], + [11,11], + [12,12], + [13,13], + [14,14], + [15,15], + [16,16], + [17,17], + [18,18], + [19,19], + [20,20], + [101,101], + [102,102], + [103,103], + [104,104], + [105,105], + [202,102], + [203,103], + [204,104], + [205,105], + [305,105], + [1001,20], + ] + ], + 'arsse_marks' => [ + 'columns' => [ + 'subscription' => "int", + 'article' => "int", + 'read' => "bool", + 'starred' => "bool", + 'modified' => "datetime" + ], + 'rows' => [ + [1, 1,1,1,'2000-01-01 00:00:00'], + [5, 19,1,0,'2000-01-01 00:00:00'], + [5, 20,0,1,'2010-01-01 00:00:00'], + [7, 20,1,0,'2010-01-01 00:00:00'], + [8, 102,1,0,'2000-01-02 02:00:00'], + [9, 103,0,1,'2000-01-03 03:00:00'], + [9, 104,1,1,'2000-01-04 04:00:00'], + [10,105,0,0,'2000-01-05 05:00:00'], + [11, 19,0,0,'2017-01-01 00:00:00'], + [11, 20,1,0,'2017-01-01 00:00:00'], + [12, 3,0,1,'2017-01-01 00:00:00'], + [12, 4,1,1,'2017-01-01 00:00:00'], + ] + ], + 'arsse_labels' => [ + 'columns' => [ + 'id' => "int", + 'owner' => "str", + 'name' => "str", + ], + 'rows' => [ + [1,"john.doe@example.com","Interesting"], + [2,"john.doe@example.com","Fascinating"], + [3,"jane.doe@example.com","Boring"], + ], + ] + ]; + protected $matches = [ + [ + 'id' => 101, + 'url' => 'http://example.com/1', + 'title' => 'Article title 1', + 'author' => '', + 'content' => '

Article content 1

', + 'guid' => 'e433653cef2e572eee4215fa299a4a5af9137b2cefd6283c85bd69a32915beda', + 'published_date' => '2000-01-01 00:00:00', + 'edited_date' => '2000-01-01 00:00:01', + 'modified_date' => '2000-01-01 01:00:00', + 'unread' => 1, + 'starred' => 0, + 'edition' => 101, + 'subscription' => 8, + 'fingerprint' => 'f5cb8bfc1c7396dc9816af212a3e2ac5221585c2a00bf7ccb6aabd95dcfcd6a6:fb0bc8f8cb08913dc5a497db700e327f1d34e4987402687d494a5891f24714d4:18fdd4fa93d693128c43b004399e5c9cea6c261ddfa002518d3669f55d8c2207', + 'media_url' => null, + 'media_type' => null, + ], + [ + 'id' => 102, + 'url' => 'http://example.com/2', + 'title' => 'Article title 2', + 'author' => '', + 'content' => '

Article content 2

', + 'guid' => '5be8a5a46ecd52ed132191c8d27fb1af6b3d4edc00234c5d9f8f0e10562ed3b7', + 'published_date' => '2000-01-02 00:00:00', + 'edited_date' => '2000-01-02 00:00:02', + 'modified_date' => '2000-01-02 02:00:00', + 'unread' => 0, + 'starred' => 0, + 'edition' => 202, + 'subscription' => 8, + 'fingerprint' => '0e86d2de822a174fe3c44a466953e63ca1f1a58a19cbf475fce0855d4e3d5153:13075894189c47ffcfafd1dfe7fbb539f7c74a69d35a399b3abf8518952714f9:2abd0a8cba83b8214a66c8f0293ba63e467d720540e29ff8ddcdab069d4f1c9e', + 'media_url' => "http://example.com/text", + 'media_type' => "text/plain", + ], + [ + 'id' => 103, + 'url' => 'http://example.com/3', + 'title' => 'Article title 3', + 'author' => '', + 'content' => '

Article content 3

', + 'guid' => '31a6594500a48b59fcc8a075ce82b946c9c3c782460d088bd7b8ef3ede97ad92', + 'published_date' => '2000-01-03 00:00:00', + 'edited_date' => '2000-01-03 00:00:03', + 'modified_date' => '2000-01-03 03:00:00', + 'unread' => 1, + 'starred' => 1, + 'edition' => 203, + 'subscription' => 9, + 'fingerprint' => 'f74b06b240bd08abf4d3fdfc20dba6a6f6eb8b4f1a00e9a617efd63a87180a4b:b278380e984cefe63f0e412b88ffc9cb0befdfa06fdc00bace1da99a8daff406:ad622b31e739cd3a3f3c788991082cf4d2f7a8773773008e75f0572e58cd373b', + 'media_url' => "http://example.com/video", + 'media_type' => "video/webm", + ], + [ + 'id' => 104, + 'url' => 'http://example.com/4', + 'title' => 'Article title 4', + 'author' => '', + 'content' => '

Article content 4

', + 'guid' => '804e517d623390e71497982c77cf6823180342ebcd2e7d5e32da1e55b09dd180', + 'published_date' => '2000-01-04 00:00:00', + 'edited_date' => '2000-01-04 00:00:04', + 'modified_date' => '2000-01-04 04:00:00', + 'unread' => 0, + 'starred' => 1, + 'edition' => 204, + 'subscription' => 9, + 'fingerprint' => 'f3615c7f16336d3ea242d35cf3fc17dbc4ee3afb78376bf49da2dd7a5a25dec8:f11c2b4046f207579aeb9c69a8c20ca5461cef49756ccfa5ba5e2344266da3b3:ab2da63276acce431250b18d3d49b988b226a99c7faadf275c90b751aee05be9', + 'media_url' => "http://example.com/image", + 'media_type' => "image/svg+xml", + ], + [ + 'id' => 105, + 'url' => 'http://example.com/5', + 'title' => 'Article title 5', + 'author' => '', + 'content' => '

Article content 5

', + 'guid' => 'db3e736c2c492f5def5c5da33ddcbea1824040e9ced2142069276b0a6e291a41', + 'published_date' => '2000-01-05 00:00:00', + 'edited_date' => '2000-01-05 00:00:05', + 'modified_date' => '2000-01-05 05:00:00', + 'unread' => 1, + 'starred' => 0, + 'edition' => 305, + 'subscription' => 10, + 'fingerprint' => 'd40da96e39eea6c55948ccbe9b3d275b5f931298288dbe953990c5f496097022:834240f84501b5341d375414718204ec421561f3825d34c22bf9182203e42900:43b970ac6ec5f8a9647b2c7e4eed8b1d7f62e154a95eed748b0294c1256764ba', + 'media_url' => "http://example.com/audio", + 'media_type' => "audio/ogg", + ], + ]; + + public function setUpSeries() { + $this->checkTables = ['arsse_labels' => ["id","owner","name"],]; + $this->user = "john.doe@example.com"; + } + + public function testAddALabel() { + $user = "john.doe@example.com"; + $labelID = $this->nextID("arsse_labels"); + $this->assertSame($labelID, Arsse::$db->labelAdd($user, ['name' => "Entertaining"])); + Phake::verify(Arsse::$user)->authorize($user, "labelAdd"); + $state = $this->primeExpectations($this->data, $this->checkTables); + $state['arsse_labels']['rows'][] = [$labelID, $user, "Entertaining"]; + $this->compareExpectations($state); + } + + public function testAddADuplicateLabel() { + $this->assertException("constraintViolation", "Db", "ExceptionInput"); + Arsse::$db->labelAdd("john.doe@example.com", ['name' => "Interesting"]); + } + + public function testAddALabelWithAMissingName() { + $this->assertException("missing", "Db", "ExceptionInput"); + Arsse::$db->labelAdd("john.doe@example.com", []); + } + + public function testAddALabelWithABlankName() { + $this->assertException("missing", "Db", "ExceptionInput"); + Arsse::$db->labelAdd("john.doe@example.com", ['name' => ""]); + } + + public function testAddALabelWithAWhitespaceName() { + $this->assertException("whitespace", "Db", "ExceptionInput"); + Arsse::$db->labelAdd("john.doe@example.com", ['name' => " "]); + } + + public function testAddALabelWithoutAuthority() { + Phake::when(Arsse::$user)->authorize->thenReturn(false); + $this->assertException("notAuthorized", "User", "ExceptionAuthz"); + Arsse::$db->labelAdd("john.doe@example.com", ['name' => "Boring"]); + } + + public function testListLabels() { + $exp = [ + ['id' => 2, 'name' => "Fascinating", 'articles' => 0], + ['id' => 1, 'name' => "Interesting", 'articles' => 0], + ]; + $this->assertSame($exp, Arsse::$db->labelList("john.doe@example.com")->getAll()); + $exp = [ + ['id' => 3, 'name' => "Boring", 'articles' => 0], + ]; + $this->assertSame($exp, Arsse::$db->labelList("jane.doe@example.com")->getAll()); + $exp = []; + $this->assertSame($exp, Arsse::$db->labelList("admin@example.net")->getAll()); + Phake::verify(Arsse::$user)->authorize("john.doe@example.com", "labelList"); + Phake::verify(Arsse::$user)->authorize("jane.doe@example.com", "labelList"); + Phake::verify(Arsse::$user)->authorize("admin@example.net", "labelList"); + } + + public function testListLabelsWithoutAuthority() { + Phake::when(Arsse::$user)->authorize->thenReturn(false); + $this->assertException("notAuthorized", "User", "ExceptionAuthz"); + Arsse::$db->labelList("john.doe@example.com"); + } + + public function testRemoveALabel() { + $this->assertTrue(Arsse::$db->labelRemove("john.doe@example.com", 1)); + Phake::verify(Arsse::$user)->authorize("john.doe@example.com", "labelRemove"); + $state = $this->primeExpectations($this->data, $this->checkTables); + array_shift($state['arsse_labels']['rows']); + $this->compareExpectations($state); + } + + public function testRemoveALabelByName() { + $this->assertTrue(Arsse::$db->labelRemove("john.doe@example.com", "Interesting", true)); + Phake::verify(Arsse::$user)->authorize("john.doe@example.com", "labelRemove"); + $state = $this->primeExpectations($this->data, $this->checkTables); + array_shift($state['arsse_labels']['rows']); + $this->compareExpectations($state); + } + + public function testRemoveAMissingLabel() { + $this->assertException("subjectMissing", "Db", "ExceptionInput"); + Arsse::$db->labelRemove("john.doe@example.com", 2112); + } + + public function testRemoveAnInvalidLabel() { + $this->assertException("typeViolation", "Db", "ExceptionInput"); + Arsse::$db->labelRemove("john.doe@example.com", -1); + } + + public function testRemoveAnInvalidLabelByName() { + $this->assertException("typeViolation", "Db", "ExceptionInput"); + Arsse::$db->labelRemove("john.doe@example.com", [], true); + } + + public function testRemoveALabelOfTheWrongOwner() { + $this->assertException("subjectMissing", "Db", "ExceptionInput"); + Arsse::$db->labelRemove("john.doe@example.com", 3); // label ID 3 belongs to Jane + } + + public function testRemoveALabelWithoutAuthority() { + Phake::when(Arsse::$user)->authorize->thenReturn(false); + $this->assertException("notAuthorized", "User", "ExceptionAuthz"); + Arsse::$db->labelRemove("john.doe@example.com", 1); + } + + public function testGetThePropertiesOfALabel() { + $exp = [ + 'id' => 2, + 'name' => "Fascinating", + 'articles' => 0, + ]; + $this->assertArraySubset($exp, Arsse::$db->labelPropertiesGet("john.doe@example.com", 2)); + $this->assertArraySubset($exp, Arsse::$db->labelPropertiesGet("john.doe@example.com", "Fascinating", true)); + Phake::verify(Arsse::$user, Phake::times(2))->authorize("john.doe@example.com", "labelPropertiesGet"); + } + + public function testGetThePropertiesOfAMissingLabel() { + $this->assertException("subjectMissing", "Db", "ExceptionInput"); + Arsse::$db->labelPropertiesGet("john.doe@example.com", 2112); + } + + public function testGetThePropertiesOfAnInvalidLabel() { + $this->assertException("typeViolation", "Db", "ExceptionInput"); + Arsse::$db->labelPropertiesGet("john.doe@example.com", -1); + } + + public function testGetThePropertiesOfAnInvalidLabelByName() { + $this->assertException("typeViolation", "Db", "ExceptionInput"); + Arsse::$db->labelPropertiesGet("john.doe@example.com", [], true); + } + + public function testGetThePropertiesOfALabelOfTheWrongOwner() { + $this->assertException("subjectMissing", "Db", "ExceptionInput"); + Arsse::$db->labelPropertiesGet("john.doe@example.com", 3); // label ID 3 belongs to Jane + } + + public function testGetThePropertiesOfALabelWithoutAuthority() { + Phake::when(Arsse::$user)->authorize->thenReturn(false); + $this->assertException("notAuthorized", "User", "ExceptionAuthz"); + Arsse::$db->labelPropertiesGet("john.doe@example.com", 1); + } + + public function testMakeNoChangesToALabel() { + $this->assertFalse(Arsse::$db->labelPropertiesSet("john.doe@example.com", 1, [])); + } + + public function testRenameALabel() { + $this->assertTrue(Arsse::$db->labelPropertiesSet("john.doe@example.com", 1, ['name' => "Curious"])); + Phake::verify(Arsse::$user)->authorize("john.doe@example.com", "labelPropertiesSet"); + $state = $this->primeExpectations($this->data, $this->checkTables); + $state['arsse_labels']['rows'][0][2] = "Curious"; + $this->compareExpectations($state); + } + + public function testRenameALabelByName() { + $this->assertTrue(Arsse::$db->labelPropertiesSet("john.doe@example.com", "Interesting", ['name' => "Curious"], true)); + Phake::verify(Arsse::$user)->authorize("john.doe@example.com", "labelPropertiesSet"); + $state = $this->primeExpectations($this->data, $this->checkTables); + $state['arsse_labels']['rows'][0][2] = "Curious"; + $this->compareExpectations($state); + } + + public function testRenameALabelToTheEmptyString() { + $this->assertException("missing", "Db", "ExceptionInput"); + $this->assertTrue(Arsse::$db->labelPropertiesSet("john.doe@example.com", 1, ['name' => ""])); + } + + public function testRenameALabelToWhitespaceOnly() { + $this->assertException("whitespace", "Db", "ExceptionInput"); + $this->assertTrue(Arsse::$db->labelPropertiesSet("john.doe@example.com", 1, ['name' => " "])); + } + + public function testRenameALabelToAnInvalidValue() { + $this->assertException("typeViolation", "Db", "ExceptionInput"); + $this->assertTrue(Arsse::$db->labelPropertiesSet("john.doe@example.com", 1, ['name' => []])); + } + + public function testCauseALabelCollision() { + $this->assertException("constraintViolation", "Db", "ExceptionInput"); + Arsse::$db->labelPropertiesSet("john.doe@example.com", 1, ['name' => "Fascinating"]); + } + + public function testSetThePropertiesOfAMissingLabel() { + $this->assertException("subjectMissing", "Db", "ExceptionInput"); + Arsse::$db->labelPropertiesSet("john.doe@example.com", 2112, ['name' => "Exciting"]); + } + + public function testSetThePropertiesOfAnInvalidLabel() { + $this->assertException("typeViolation", "Db", "ExceptionInput"); + Arsse::$db->labelPropertiesSet("john.doe@example.com", -1, ['name' => "Exciting"]); + } + + public function testSetThePropertiesOfAnInvalidLabelByName() { + $this->assertException("typeViolation", "Db", "ExceptionInput"); + Arsse::$db->labelPropertiesSet("john.doe@example.com", [], ['name' => "Exciting"], true); + } + + public function testSetThePropertiesOfALabelForTheWrongOwner() { + $this->assertException("subjectMissing", "Db", "ExceptionInput"); + Arsse::$db->labelPropertiesSet("john.doe@example.com", 3, ['name' => "Exciting"]); // label ID 3 belongs to Jane + } + + public function testSetThePropertiesOfALabelWithoutAuthority() { + Phake::when(Arsse::$user)->authorize->thenReturn(false); + $this->assertException("notAuthorized", "User", "ExceptionAuthz"); + Arsse::$db->labelPropertiesSet("john.doe@example.com", 1, ['name' => "Exciting"]); + } +} diff --git a/tests/lib/Database/SeriesSubscription.php b/tests/lib/Database/SeriesSubscription.php index 4a1c841b..3a5a1592 100644 --- a/tests/lib/Database/SeriesSubscription.php +++ b/tests/lib/Database/SeriesSubscription.php @@ -333,6 +333,9 @@ trait SeriesSubscription { ]); $state['arsse_subscriptions']['rows'][0] = [1,"john.doe@example.com",2,null,3,0,0]; $this->compareExpectations($state); + // making no changes is a valid result + Arsse::$db->subscriptionPropertiesSet($this->user, 1, ['unhinged' => true]); + $this->compareExpectations($state); } public function testMoveASubscriptionToAMissingFolder() { diff --git a/tests/lib/Database/SeriesUser.php b/tests/lib/Database/SeriesUser.php index e4a72c59..e0f02232 100644 --- a/tests/lib/Database/SeriesUser.php +++ b/tests/lib/Database/SeriesUser.php @@ -209,6 +209,9 @@ trait SeriesUser { $state = $this->primeExpectations($this->data, ['arsse_users' => ['id','password','name','rights']]); $state['arsse_users']['rows'][0][2] = "James Kirk"; $this->compareExpectations($state); + // making now changes should make no changes :) + Arsse::$db->userPropertiesSet("admin@example.net", ['lifeform' => "tribble"]); + $this->compareExpectations($state); } public function testSetThePropertiesOfAMissingUser() { diff --git a/tests/phpunit.xml b/tests/phpunit.xml index 6d486bcf..f12fe403 100644 --- a/tests/phpunit.xml +++ b/tests/phpunit.xml @@ -62,6 +62,7 @@ Db/SQLite3/Database/TestDatabaseFeedSQLite3.php Db/SQLite3/Database/TestDatabaseSubscriptionSQLite3.php Db/SQLite3/Database/TestDatabaseArticleSQLite3.php + Db/SQLite3/Database/TestDatabaseLabelSQLite3.php Db/SQLite3/Database/TestDatabaseCleanupSQLite3.php