diff --git a/lib/Database.php b/lib/Database.php index 7d745a78..71e85cbb 100644 --- a/lib/Database.php +++ b/lib/Database.php @@ -880,6 +880,15 @@ class Database { // if neither list is specified, mock an empty table $q->setCTE("requested_articles(id,edition)", "SELECT 'empty','table' where 1 is 0"); } + // filter based on label by ID or name + if ($context->label() || $context->labelName()) { + if ($context->label()) { + $id = $this->labelValidateId($user, $context->label, false)['id']; + } else { + $id = $this->labelValidateId($user, $context->labelName, true)['id']; + } + $q->setWhere("exists(select article from arsse_label_members where assigned is 1 and article is arsse_articles.id and label is ?)", "int", $id); + } // filter based on edition offset if ($context->oldestEdition()) { $q->setWhere("edition >= ?", "int", $context->oldestEdition); @@ -932,9 +941,7 @@ class Database { if (!Arsse::$user->authorize($user, __FUNCTION__)) { throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]); } - if (!$context) { - $context = new Context; - } + $context = $context ?? new Context; // sanitize input $values = [ isset($data['read']) ? $data['read'] : null, @@ -1139,11 +1146,10 @@ class Database { return $this->db->prepare( "SELECT id,name, - (select count(*) from arsse_label_members join arsse_subscriptions on arsse_subscriptions.owner is arsse_labels.owner where label is arsse_labels.id) as articles, + (select count(*) from arsse_label_members where label is id and assigned is 1) as articles, (select count(*) from arsse_label_members join arsse_marks on arsse_label_members.article is arsse_marks.article and arsse_label_members.subscription is arsse_marks.subscription - join arsse_subscriptions on arsse_subscriptions.owner is arsse_labels.owner - where label is arsse_labels.id and read is 1 + where label is id and assigned is 1 and read is 1 ) as read FROM arsse_labels where owner is ? and articles >= ? ", "str", "int" @@ -1154,13 +1160,7 @@ class Database { 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"]); - } + $this->labelValidateId($user, $id, $byName, false); $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(); @@ -1174,22 +1174,20 @@ class Database { 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"]); - } + $this->labelValidateId($user, $id, $byName, false); $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 + (select count(*) from arsse_label_members where label is id and assigned is 1) as articles, + (select count(*) from arsse_label_members + join arsse_marks on arsse_label_members.article is arsse_marks.article and arsse_label_members.subscription is arsse_marks.subscription + where label is id and assigned is 1 and read is 1 + ) as read FROM arsse_labels where $field is ? and owner is ? - ", "str", $type, "str" - )->run($user, $id, $user)->getRow(); + ", $type, "str" + )->run($id, $user)->getRow(); if (!$out) { throw new Db\ExceptionInput("subjectMissing", ["action" => __FUNCTION__, "field" => "label", 'id' => $id]); } @@ -1200,13 +1198,7 @@ class Database { 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"]); - } + $this->labelValidateId($user, $id, $byName, false); if (isset($data['name'])) { $this->labelValidateName($data['name']); } @@ -1227,6 +1219,90 @@ class Database { return $out; } + public function labelArticlesGet(string $user, $id, bool $byName = false): array { + if (!Arsse::$user->authorize($user, __FUNCTION__)) { + throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]); + } + // just do a syntactic check on the label ID + $this->labelValidateId($user, $id, $byName, false); + $field = !$byName ? "id" : "name"; + $type = !$byName ? "int" : "str"; + $out = $this->db->prepare("SELECT article from arsse_label_members join arsse_labels on label is id where assigned is 1 and $field is ? and owner is ?", $type, "str")->run($id, $user)->getAll(); + if (!$out) { + // if no results were returned, do a full validation on the label ID + $this->labelValidateId($user, $id, $byName, true, true); + // if the validation passes, return the empty result + return $out; + } else { + // flatten the result to return just the article IDs in a simple array + return array_column($out, "article"); + } + } + + public function labelArticlesSet(string $user, $id, Context $context = null, bool $remove = false, bool $byName = false): bool { + if (!Arsse::$user->authorize($user, __FUNCTION__)) { + throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]); + } + // validate the label ID, and get the numeric ID if matching by name + $id = $this->labelValidateId($user, $id, $byName, true)['id']; + $context = $context ?? new Context; + $out = 0; + // wrap this UPDATE and INSERT together into a transaction + $tr = $this->begin(); + // first update any existing entries with the removal or re-addition of their association + $q = $this->articleQuery($user, $context); + $q->setWhere("exists(select article from arsse_label_members where label is ? and article is arsse_articles.id)", "int", $id); + $q->pushCTE("target_articles"); + $q->setBody( + "UPDATE arsse_label_members set assigned = ?, modified = CURRENT_TIMESTAMP where label is ? and assigned is not ? and article in (select id from target_articles)", + ["bool","int","bool"], + [!$remove, $id, !$remove] + ); + $out += $this->db->prepare($q->getQuery(), $q->getTypes())->run($q->getValues())->changes(); + // next, if we're not removing, add any new entries that need to be added + if (!$remove) { + $q = $this->articleQuery($user, $context); + $q->setWhere("not exists(select article from arsse_label_members where label is ? and article is arsse_articles.id)", "int", $id); + $q->pushCTE("target_articles"); + $q->setBody( + "INSERT INTO + arsse_label_members(label,article,subscription) + SELECT + ?,id, + (select id from arsse_subscriptions join user on user is owner where arsse_subscriptions.feed is target_articles.feed) + FROM target_articles", + "int", $id + ); + $out += $this->db->prepare($q->getQuery(), $q->getTypes())->run($q->getValues())->changes(); + } + // commit the transaction + $tr->commit(); + return (bool) $out; + } + + protected function labelValidateId(string $user, $id, bool $byName, bool $checkDb = true, bool $subject = false): array { + 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" => $this->caller(), "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" => $this->caller(), "field" => "label", 'type' => "string"]); + } elseif ($checkDb) { + $field = !$byName ? "id" : "name"; + $type = !$byName ? "int" : "str"; + $l = $this->db->prepare("SELECT id,name from arsse_labels where $field is ? and owner is ?", $type, "str")->run($id, $user)->getRow(); + if (!$l) { + throw new Db\ExceptionInput($subject ? "subjectMissing" : "idMissing", ["action" => $this->caller(), "field" => "label", 'id' => $id]); + } else { + return $l; + } + } + return [ + 'id' => !$byName ? $id : null, + 'name' => $byName ? $id : null, + ]; + } + protected function labelValidateName($name): bool { $info = ValueInfo::str($name); if ($info & (ValueInfo::NULL | ValueInfo::EMPTY)) { diff --git a/lib/Misc/Context.php b/lib/Misc/Context.php index ca37b3de..b1864f1f 100644 --- a/lib/Misc/Context.php +++ b/lib/Misc/Context.php @@ -21,6 +21,8 @@ class Context { public $article; public $editions; public $articles; + public $label; + public $labelName; protected $props = []; @@ -113,4 +115,12 @@ class Context { } return $this->act(__FUNCTION__, func_num_args(), $spec); } + + public function label(int $spec = null) { + return $this->act(__FUNCTION__, func_num_args(), $spec); + } + + public function labelName(string $spec = null) { + return $this->act(__FUNCTION__, func_num_args(), $spec); + } } diff --git a/sql/SQLite3/1.sql b/sql/SQLite3/1.sql index 50e75678..94970ba5 100644 --- a/sql/SQLite3/1.sql +++ b/sql/SQLite3/1.sql @@ -20,6 +20,8 @@ create table arsse_label_members ( label integer not null references arsse_labels(id) on delete cascade, article integer not null references arsse_articles(id) on delete cascade, subscription integer not null references arsse_subscriptions(id) on delete cascade, -- Subscription is included so that records are deleted when a subscription is removed + assigned boolean not null default 1, + modified text not null default CURRENT_TIMESTAMP, primary key(label,article) ) without rowid; diff --git a/tests/Misc/TestContext.php b/tests/Misc/TestContext.php index 49ab9bdc..4547c581 100644 --- a/tests/Misc/TestContext.php +++ b/tests/Misc/TestContext.php @@ -35,6 +35,8 @@ class TestContext extends Test\AbstractTest { 'notModifiedSince' => new \DateTime(), 'editions' => [1,2], 'articles' => [1,2], + 'label' => 2112, + 'labelName' => "Rush", ]; $times = ['modifiedSince','notModifiedSince']; $c = new Context; diff --git a/tests/lib/Database/SeriesArticle.php b/tests/lib/Database/SeriesArticle.php index c7cdaa2e..0ecccfde 100644 --- a/tests/lib/Database/SeriesArticle.php +++ b/tests/lib/Database/SeriesArticle.php @@ -206,6 +206,35 @@ trait SeriesArticle { [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"], + [4,"john.doe@example.com","Lonely"], + ], + ], + 'arsse_label_members' => [ + 'columns' => [ + 'label' => "int", + 'article' => "int", + 'subscription' => "int", + 'assigned' => "bool", + ], + 'rows' => [ + [1, 1,1,1], + [2, 1,1,1], + [1,19,5,1], + [2,20,5,1], + [1, 5,3,0], + [2, 5,3,1], + ], + ], ]; protected $matches = [ [ @@ -355,6 +384,12 @@ trait SeriesArticle { $this->compareIds([19], (new Context)->reverse(true)->limit(1)->latestEdition(1001-1)); $this->compareIds([8], (new Context)->reverse(true)->limit(1)->latestEdition(19-1)); $this->compareIds([7,6], (new Context)->reverse(true)->limit(2)->latestEdition(8-1)); + // label by ID + $this->compareIds([1,19], (new Context)->label(1)); + $this->compareIds([1,5,20], (new Context)->label(2)); + // label by name + $this->compareIds([1,19], (new Context)->labelName("Interesting")); + $this->compareIds([1,5,20], (new Context)->labelName("Fascinating")); } public function testListArticlesOfAMissingFolder() { diff --git a/tests/lib/Database/SeriesLabel.php b/tests/lib/Database/SeriesLabel.php index f787ed6f..26c01579 100644 --- a/tests/lib/Database/SeriesLabel.php +++ b/tests/lib/Database/SeriesLabel.php @@ -216,104 +216,30 @@ trait SeriesLabel { [1,"john.doe@example.com","Interesting"], [2,"john.doe@example.com","Fascinating"], [3,"jane.doe@example.com","Boring"], + [4,"john.doe@example.com","Lonely"], ], - ] - ]; - 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", + 'arsse_label_members' => [ + 'columns' => [ + 'label' => "int", + 'article' => "int", + 'subscription' => "int", + 'assigned' => "bool", + ], + 'rows' => [ + [1, 1,1,1], + [2, 1,1,1], + [1,19,5,1], + [2,20,5,1], + [1, 5,3,0], + [2, 5,3,1], + ], ], ]; public function setUpSeries() { - $this->checkTables = ['arsse_labels' => ["id","owner","name"],]; + $this->checkLabels = ['arsse_labels' => ["id","owner","name"]]; + $this->checkMembers = ['arsse_label_members' => ["label","article","subscription","assigned"]]; $this->user = "john.doe@example.com"; } @@ -322,7 +248,7 @@ trait SeriesLabel { $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 = $this->primeExpectations($this->data, $this->checkLabels); $state['arsse_labels']['rows'][] = [$labelID, $user, "Entertaining"]; $this->compareExpectations($state); } @@ -355,8 +281,9 @@ trait SeriesLabel { public function testListLabels() { $exp = [ - ['id' => 2, 'name' => "Fascinating", 'articles' => 0], - ['id' => 1, 'name' => "Interesting", 'articles' => 0], + ['id' => 2, 'name' => "Fascinating", 'articles' => 3, 'read' => 1], + ['id' => 1, 'name' => "Interesting", 'articles' => 2, 'read' => 2], + ['id' => 4, 'name' => "Lonely", 'articles' => 0, 'read' => 0], ]; $this->assertResult($exp, Arsse::$db->labelList("john.doe@example.com")); $exp = [ @@ -364,10 +291,8 @@ trait SeriesLabel { ]; $this->assertResult($exp, Arsse::$db->labelList("jane.doe@example.com")); $exp = []; - $this->assertResult($exp, Arsse::$db->labelList("admin@example.net")); + $this->assertResult($exp, Arsse::$db->labelList("jane.doe@example.com", false)); 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() { @@ -379,7 +304,7 @@ trait SeriesLabel { 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); + $state = $this->primeExpectations($this->data, $this->checkLabels); array_shift($state['arsse_labels']['rows']); $this->compareExpectations($state); } @@ -387,7 +312,7 @@ trait SeriesLabel { 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); + $state = $this->primeExpectations($this->data, $this->checkLabels); array_shift($state['arsse_labels']['rows']); $this->compareExpectations($state); } @@ -422,7 +347,8 @@ trait SeriesLabel { $exp = [ 'id' => 2, 'name' => "Fascinating", - 'articles' => 0, + 'articles' => 3, + 'read' => 1, ]; $this->assertArraySubset($exp, Arsse::$db->labelPropertiesGet("john.doe@example.com", 2)); $this->assertArraySubset($exp, Arsse::$db->labelPropertiesGet("john.doe@example.com", "Fascinating", true)); @@ -462,7 +388,7 @@ trait SeriesLabel { 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 = $this->primeExpectations($this->data, $this->checkLabels); $state['arsse_labels']['rows'][0][2] = "Curious"; $this->compareExpectations($state); } @@ -470,7 +396,7 @@ trait SeriesLabel { 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 = $this->primeExpectations($this->data, $this->checkLabels); $state['arsse_labels']['rows'][0][2] = "Curious"; $this->compareExpectations($state); } @@ -520,4 +446,68 @@ trait SeriesLabel { $this->assertException("notAuthorized", "User", "ExceptionAuthz"); Arsse::$db->labelPropertiesSet("john.doe@example.com", 1, ['name' => "Exciting"]); } + + public function testListLabelledArticles() { + $exp = [1,19]; + $this->assertEquals($exp, Arsse::$db->labelArticlesGet("john.doe@example.com", 1)); + $this->assertEquals($exp, Arsse::$db->labelArticlesGet("john.doe@example.com", "Interesting", true)); + $exp = [1,5,20]; + $this->assertEquals($exp, Arsse::$db->labelArticlesGet("john.doe@example.com", 2)); + $this->assertEquals($exp, Arsse::$db->labelArticlesGet("john.doe@example.com", "Fascinating", true)); + $exp = []; + $this->assertEquals($exp, Arsse::$db->labelArticlesGet("john.doe@example.com", 4)); + $this->assertEquals($exp, Arsse::$db->labelArticlesGet("john.doe@example.com", "Lonely", true)); + } + + public function testListLabelledArticlesForAMissingLabel() { + $this->assertException("subjectMissing", "Db", "ExceptionInput"); + Arsse::$db->labelArticlesGet("john.doe@example.com", 3); + } + + public function testListLabelledArticlesForAnInvalidLabel() { + $this->assertException("typeViolation", "Db", "ExceptionInput"); + Arsse::$db->labelArticlesGet("john.doe@example.com", -1); + } + + public function testListLabelledArticlesWithoutAuthority() { + Phake::when(Arsse::$user)->authorize->thenReturn(false); + $this->assertException("notAuthorized", "User", "ExceptionAuthz"); + Arsse::$db->labelArticlesGet("john.doe@example.com", 1); + } + + public function testApplyALabelToArticles() { + Arsse::$db->labelArticlesSet("john.doe@example.com", 1, (new Context)->articles([2,5])); + $state = $this->primeExpectations($this->data, $this->checkMembers); + $state['arsse_label_members']['rows'][4][3] = 1; + $state['arsse_label_members']['rows'][] = [1,2,1,1]; + $this->compareExpectations($state); + } + + public function testClearALabelFromArticles() { + Arsse::$db->labelArticlesSet("john.doe@example.com", 1, (new Context)->articles([1,5]), true); + $state = $this->primeExpectations($this->data, $this->checkMembers); + $state['arsse_label_members']['rows'][0][3] = 0; + $this->compareExpectations($state); + } + + public function testApplyALabelToArticlesByName() { + Arsse::$db->labelArticlesSet("john.doe@example.com", "Interesting", (new Context)->articles([2,5]), false, true); + $state = $this->primeExpectations($this->data, $this->checkMembers); + $state['arsse_label_members']['rows'][4][3] = 1; + $state['arsse_label_members']['rows'][] = [1,2,1,1]; + $this->compareExpectations($state); + } + + public function testClearALabelFromArticlesByName() { + Arsse::$db->labelArticlesSet("john.doe@example.com", "Interesting", (new Context)->articles([1,5]), true, true); + $state = $this->primeExpectations($this->data, $this->checkMembers); + $state['arsse_label_members']['rows'][0][3] = 0; + $this->compareExpectations($state); + } + + public function testApplyALabelToArticlesWithoutAuthority() { + Phake::when(Arsse::$user)->authorize->thenReturn(false); + $this->assertException("notAuthorized", "User", "ExceptionAuthz"); + Arsse::$db->labelArticlesSet("john.doe@example.com", 1, (new Context)->articles([2,5])); + } }