From 20ff08a431d52daaa7c0c1f0b5895f6ce8085d98 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Wed, 11 Oct 2017 12:55:50 -0400 Subject: [PATCH] Implement TTTRSS getCounters operation; fixes #79 --- lib/Database.php | 19 ++- lib/Db/AbstractStatement.php | 1 - lib/REST/NextCloudNews/V1_2.php | 2 +- lib/REST/TinyTinyRSS/API.php | 138 ++++++++++++++----- tests/REST/NextCloudNews/TestNCNV1_2.php | 2 +- tests/REST/TinyTinyRSS/TestTinyTinyAPI.php | 152 ++++++++++++++------- tests/lib/Database/SeriesArticle.php | 22 ++- 7 files changed, 243 insertions(+), 93 deletions(-) diff --git a/lib/Database.php b/lib/Database.php index 9bc975e5..7d745a78 100644 --- a/lib/Database.php +++ b/lib/Database.php @@ -305,7 +305,8 @@ class Database { $q = new Query( "SELECT id,name,parent, - (select count(*) from arsse_folders as parents where parents.parent is arsse_folders.id) as children + (select count(*) from arsse_folders as parents where parents.parent is arsse_folders.id) as children, + (select count(*) from arsse_subscriptions where folder is arsse_folders.id) as feeds FROM arsse_folders" ); if (!$recursive) { @@ -508,6 +509,7 @@ class Database { "SELECT arsse_subscriptions.id as id, feed,url,favicon,source,folder,pinned,err_count,err_msg,order_type,added, + arsse_feeds.updated as updated, topmost.top as top_folder, coalesce(arsse_subscriptions.title, arsse_feeds.title) as title, (SELECT count(*) from arsse_articles where feed is arsse_subscriptions.feed) - (SELECT count(*) from arsse_marks where subscription is arsse_subscriptions.id and read is 1) as unread @@ -1002,6 +1004,21 @@ class Database { return $this->db->prepare($q->getQuery(), $q->getTypes())->run($q->getValues())->getValue(); } + public function articleStarred(string $user): array { + if (!Arsse::$user->authorize($user, __FUNCTION__)) { + throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]); + } + return $this->db->prepare( + "SELECT + count(*) as total, + coalesce(sum(not read),0) as unread, + coalesce(sum(read),0) as read + FROM ( + select read from arsse_marks where starred is 1 and subscription in (select id from arsse_subscriptions where owner is ?) + )", "str" + )->run($user)->getRow(); + } + public function articleCleanup(): bool { $query = $this->db->prepare( "WITH target_feed(id,subs) as (". diff --git a/lib/Db/AbstractStatement.php b/lib/Db/AbstractStatement.php index 3ac5052b..5c96f283 100644 --- a/lib/Db/AbstractStatement.php +++ b/lib/Db/AbstractStatement.php @@ -7,7 +7,6 @@ use JKingWeb\Arsse\Misc\Date; abstract class AbstractStatement implements Statement { protected $types = []; protected $isNullable = []; - protected $values = ['pre' => [], 'post' => []]; abstract public function runArray(array $values = []): Result; diff --git a/lib/REST/NextCloudNews/V1_2.php b/lib/REST/NextCloudNews/V1_2.php index 09f85463..46a6a1c0 100644 --- a/lib/REST/NextCloudNews/V1_2.php +++ b/lib/REST/NextCloudNews/V1_2.php @@ -395,7 +395,7 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler { $out[] = $this->feedTranslate($sub); } $out = ['feeds' => $out]; - $out['starredCount'] = Arsse::$db->articleCount(Arsse::$user->id, (new Context)->starred(true)); + $out['starredCount'] = Arsse::$db->articleStarred(Arsse::$user->id)['total']; $newest = Arsse::$db->editionLatest(Arsse::$user->id); if ($newest) { $out['newestItemId'] = $newest; diff --git a/lib/REST/TinyTinyRSS/API.php b/lib/REST/TinyTinyRSS/API.php index 3efef8d0..b6f10ffd 100644 --- a/lib/REST/TinyTinyRSS/API.php +++ b/lib/REST/TinyTinyRSS/API.php @@ -17,13 +17,13 @@ use JKingWeb\Arsse\REST\Response; /* Protocol difference so far: - - handling of incorrect Content-Type and/or HTTP method is different + - Handling of incorrect Content-Type and/or HTTP method is different - 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 - + - The "Archived" virtual feed is non-functional (the protocol does not allow archiving) + - The "Published" virtual feed is non-functional (this will not be implemented in the near term) */ @@ -31,14 +31,12 @@ Protocol difference so far: class API extends \JKingWeb\Arsse\REST\AbstractHandler { const LEVEL = 14; const VERSION = "17.4"; + const LABEL_OFFSET = 1024; const FATAL_ERR = [ 'seq' => null, 'status' => 1, 'content' => ['error' => "NOT_LOGGED_IN"], ]; - const OVERRIDE = [ - 'auth' => ["login"], - ]; public function __construct() { } @@ -65,8 +63,8 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler { 'sid' => null, ], $data); try { - if (!in_array($data['op'], self::OVERRIDE['auth'])) { - // unless otherwise specified, a session identifier is required + if (strtolower((string) $data['op']) != "login") { + // unless logging in, a session identifier is required $this->resumeSession($data['sid']); } $method = "op".ucfirst($data['op']); @@ -148,19 +146,109 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler { return ['status' => true]; } + public function opGetConfig(array $data): array { + return [ + 'icons_dir' => "feed-icons", + 'icons_url' => "feed-icons", + 'daemon_is_running' => Service::hasCheckedIn(), + 'num_feeds' => Arsse::$db->subscriptionCount(Arsse::$user->id), + ]; + } + + public function opGetUnread(array $data): array { + // simply sum the unread count of each subscription + $out = 0; + foreach (Arsse::$db->subscriptionList(Arsse::$user->id) as $sub) { + $out += $sub['unread']; + } + return ['unread' => $out]; + } + + public function opGetCounters(array $data): array { + $user = Arsse::$user->id; + $starred = Arsse::$db->articleStarred($user); + $fresh = Arsse::$db->articleCount($user, (new Context)->unread(true)->modifiedSince(Date::sub("PT24H"))); + $countAll = 0; + $countSubs = 0; + $feeds = []; + $labels = []; + // do a first pass on categories: add the ID to a lookup table and set the unread counter to zero + $categories = Arsse::$db->folderList($user)->getAll(); + $catmap = []; + for ($a = 0; $a < sizeof($categories); $a++) { + $catmap[(int) $categories[$a]['id']] = $a; + $categories[$a]['counter'] = 0; + } + // add the "Uncategorized" and "Labels" virtual categories to the list + $catmap[0] = sizeof($categories); + $categories[] = ['id' => 0, 'name' => Arsse::$lang->msg("API.TTRSS.Category.Uncategorized"), 'parent' => 0, 'children' => 0, 'counter' => 0]; + $catmap[-2] = sizeof($categories); + $categories[] = ['id' => -2, 'name' => Arsse::$lang->msg("API.TTRSS.Category.Labels"), 'parent' => 0, 'children' => 0, 'counter' => 0]; + // prepare data for each subscription; we also add unread counts for their host categories + foreach (Arsse::$db->subscriptionList($user) as $f) { + if ($f['unread']) { + // add the feed to the list of feeds + $feeds[] = ['id' => $f['id'], 'updated' => Date::transform($f['updated'], "iso8601", "sql"),'counter' => $f['unread'], 'has_img' => (int) (strlen((string) $f['favicon']) > 0)]; + // add the feed's unread count to the global unread count + $countAll += $f['unread']; + // add the feed's unread count to its category unread count + $categories[$catmap[(int) $f['folder']]]['counter'] += $f['unread']; + } + // increment the global feed count + $countSubs += 1; + } + // prepare data for each non-empty label + foreach (Arsse::$db->labelList($user, false) as $l) { + $unread = $l['articles'] - $l['read']; + $labels[] = ['id' => $this->labelOut($l['id']), 'counter' => $unread, 'auxcounter' => $l['articles']]; + $categories[$catmap[-2]]['counter'] += $unread; + } + // do a second pass on categories, summing descendant unread counts for ancestors, pruning categories with no unread, and building a final category list + $cats = []; + while ($categories) { + foreach ($categories as $c) { + if ($c['children']) { + // only act on leaf nodes + continue; + } + if ($c['parent']) { + // if the category has a parent, add its counter to the parent's counter, and decrement the parent's child count + $categories[$catmap[$c['parent']]]['counter'] += $c['counter']; + $categories[$catmap[$c['parent']]]['children'] -= 1; + } + if ($c['counter']) { + // if the category's counter is non-zero, add the category to the output list + $cats[] = ['id' => $c['id'], 'kind' => "cat", 'counter' => $c['counter']]; + } + // remove the category from the input list + unset($categories[$catmap[$c['id']]]); + } + } + // prepare data for the virtual feeds and other counters + $special = [ + ['id' => "global-unread", 'counter' => $countAll], //this should not count archived articles, but we do not have an archive + ['id' => "subscribed-feeds", 'counter' => $countSubs], + ['id' => 0, 'counter' => 0, 'auxcounter' => 0], // Archived articles + ['id' => -1, 'counter' => $starred['unread'], 'auxcounter' => $starred['total']], // Starred articles + ['id' => -2, 'counter' => 0, 'auxcounter' => 0], // Published articles + ['id' => -3, 'counter' => $fresh, 'auxcounter' => 0], // Fresh articles + ['id' => -4, 'counter' => $countAll, 'auxcounter' => 0], // All articles + ]; + return array_merge($special, $labels, $feeds, $cats); + } + public function opGetCategories(array $data): array { // normalize input $all = isset($data['include_empty']) ? ValueInfo::bool($data['include_empty'], false) : false; $read = !(isset($data['unread_only']) ? ValueInfo::bool($data['unread_only'], false) : false); $deep = !(isset($data['enable_nested']) ? ValueInfo::bool($data['enable_nested'], false) : false); $user = Arsse::$user->id; - // for each category, add the ID to a lookup table, set the number of unread and feeds to zero, and assign an increasing order index + // for each category, add the ID to a lookup table, set the number of unread to zero, and assign an increasing order index $cats = Arsse::$db->folderList($user, null, $deep)->getAll(); $map = []; for ($a = 0; $a < sizeof($cats); $a++) { $map[$cats[$a]['id']] = $a; $cats[$a]['unread'] = 0; - $cats[$a]['feeds'] = 0; $cats[$a]['order'] = $a + 1; } // add the "Uncategorized", "Special", and "Labels" virtual categories to the list @@ -176,7 +264,9 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler { // note we use top_folder if we're in "nested" mode $f = $map[(int) ($deep ? $sub['folder'] : $sub['top_folder'])]; $cats[$f]['unread'] += $sub['unread']; - $cats[$f]['feeds'] += 1; + if (!$cats[$f]['id']) { + $cats[$f]['feeds'] += 1; + } } // for each label, add the unread count to the labels category, and increment the labels category's feed count $labels = Arsse::$db->labelList($user); @@ -188,7 +278,7 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler { // get the unread counts for the special feeds // FIXME: this is pretty inefficient $f = $map[-1]; - $cats[$f]['unread'] += Arsse::$db->articleCount($user, (new Context)->unread(true)->starred(true)); // starred + $cats[$f]['unread'] += Arsse::$db->articleStarred($user)['unread']; // starred $cats[$f]['unread'] += Arsse::$db->articleCount($user, (new Context)->unread(true)->modifiedSince(Date::sub("PT24H"))); // fresh if (!$read) { // if we're only including unread entries, remove any categories with zero unread items (this will by definition also exclude empties) @@ -439,24 +529,6 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler { return null; } - public function opGetUnread(array $data): array { - // simply sum the unread count of each subscription - $out = 0; - foreach (Arsse::$db->subscriptionList(Arsse::$user->id) as $sub) { - $out += $sub['unread']; - } - return ['unread' => $out]; - } - - public function opGetConfig(array $data): array { - return [ - 'icons_dir' => "feed-icons", - 'icons_url' => "feed-icons", - 'daemon_is_running' => Service::hasCheckedIn(), - 'num_feeds' => Arsse::$db->subscriptionCount(Arsse::$user->id), - ]; - } - public function opUpdateFeed(array $data): array { if (!isset($data['feed_id']) || !ValueInfo::id($data['feed_id'])) { // if the feed is invalid, throw an error @@ -471,14 +543,14 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler { } protected function labelIn($id): int { - if (!(ValueInfo::int($id) & ValueInfo::NEG) || $id > -11) { + if (!(ValueInfo::int($id) & ValueInfo::NEG) || $id > (-1 - self::LABEL_OFFSET)) { throw new Exception("INCORRECT_USAGE"); } - return (abs($id) - 10); + return (abs($id) - self::LABEL_OFFSET); } protected function labelOut(int $id): int { - return ($id * -1 - 10); + return ($id * -1 - self::LABEL_OFFSET); } public function opAddLabel(array $data) { diff --git a/tests/REST/NextCloudNews/TestNCNV1_2.php b/tests/REST/NextCloudNews/TestNCNV1_2.php index d3fe141c..813ef9dd 100644 --- a/tests/REST/NextCloudNews/TestNCNV1_2.php +++ b/tests/REST/NextCloudNews/TestNCNV1_2.php @@ -475,7 +475,7 @@ class TestNCNV1_2 extends Test\AbstractTest { 'newestItemId' => 4758915, ]; Phake::when(Arsse::$db)->subscriptionList(Arsse::$user->id)->thenReturn(new Result([]))->thenReturn(new Result($this->feeds['db'])); - Phake::when(Arsse::$db)->articleCount(Arsse::$user->id, (new Context)->starred(true))->thenReturn(0)->thenReturn(5); + Phake::when(Arsse::$db)->articleStarred(Arsse::$user->id)->thenReturn(['total' => 0])->thenReturn(['total' => 5]); Phake::when(Arsse::$db)->editionLatest(Arsse::$user->id)->thenReturn(0)->thenReturn(4758915); $exp = new Response(200, $exp1); $this->assertEquals($exp, $this->h->dispatch(new Request("GET", "/feeds"))); diff --git a/tests/REST/TinyTinyRSS/TestTinyTinyAPI.php b/tests/REST/TinyTinyRSS/TestTinyTinyAPI.php index ad383b7a..77a98f1c 100644 --- a/tests/REST/TinyTinyRSS/TestTinyTinyAPI.php +++ b/tests/REST/TinyTinyRSS/TestTinyTinyAPI.php @@ -9,12 +9,43 @@ use JKingWeb\Arsse\Misc\Date; use JKingWeb\Arsse\Misc\Context; use JKingWeb\Arsse\Db\ExceptionInput; use JKingWeb\Arsse\Db\Transaction; +use JKingWeb\Arsse\REST\TinyTinyRSS\API; use Phake; /** @covers \JKingWeb\Arsse\REST\TinyTinyRSS\API * @covers \JKingWeb\Arsse\REST\TinyTinyRSS\Exception */ class TestTinyTinyAPI extends Test\AbstractTest { protected $h; + protected $folders = [ + ['id' => 5, 'parent' => 3, 'children' => 0, 'feeds' => 1, 'name' => "Local"], + ['id' => 6, 'parent' => 3, 'children' => 0, 'feeds' => 2, 'name' => "National"], + ['id' => 4, 'parent' => null, 'children' => 0, 'feeds' => 0, 'name' => "Photography"], + ['id' => 3, 'parent' => null, 'children' => 2, 'feeds' => 0, 'name' => "Politics"], + ['id' => 2, 'parent' => 1, 'children' => 0, 'feeds' => 1, 'name' => "Rocketry"], + ['id' => 1, 'parent' => null, 'children' => 1, 'feeds' => 1, 'name' => "Science"], + ]; + protected $topFolders = [ + ['id' => 4, 'parent' => null, 'children' => 0, 'feeds' => 0, 'name' => "Photography"], + ['id' => 3, 'parent' => null, 'children' => 2, 'feeds' => 0, 'name' => "Politics"], + ['id' => 1, 'parent' => null, 'children' => 1, 'feeds' => 1, 'name' => "Science"], + ]; + protected $subscriptions = [ + ['id' => 6, 'folder' => null, 'top_folder' => null, 'unread' => 0, 'updated' => "2010-02-12 20:08:47", 'favicon' => 'http://example.com/6.png'], + ['id' => 3, 'folder' => 1, 'top_folder' => 1, 'unread' => 2, 'updated' => "2016-05-23 06:40:02", 'favicon' => 'http://example.com/3.png'], + ['id' => 1, 'folder' => 2, 'top_folder' => 1, 'unread' => 5, 'updated' => "2017-09-15 22:54:16", 'favicon' => null], + ['id' => 2, 'folder' => 5, 'top_folder' => 3, 'unread' => 10, 'updated' => "2011-11-11 11:11:11", 'favicon' => 'http://example.com/2.png'], + ['id' => 5, 'folder' => 6, 'top_folder' => 3, 'unread' => 12, 'updated' => "2017-07-07 17:07:17", 'favicon' => ''], + ['id' => 4, 'folder' => 6, 'top_folder' => 3, 'unread' => 6, 'updated' => "2017-10-09 15:58:34", 'favicon' => 'http://example.com/4.png'], + ]; + protected $labels = [ + ['id' => 5, 'articles' => 0, 'read' => 0], + ['id' => 3, 'articles' => 100, 'read' => 94], + ['id' => 1, 'articles' => 2, 'read' => 0], + ]; + protected $usedLabels = [ + ['id' => 3, 'articles' => 100, 'read' => 94], + ['id' => 1, 'articles' => 2, 'read' => 0], + ]; protected function respGood($content = null, $seq = 0): Response { return new Response(200, [ @@ -33,6 +64,19 @@ class TestTinyTinyAPI extends Test\AbstractTest { ]); } + protected function assertResponse(Response $exp, Response $act, string $text = null) { + if ($exp->payload['status']) { + // if the expectation is an error response, do a straight object comparison + $this->assertEquals($exp, $act, $text); + } else { + // otherwise just compare their content + foreach ($act->payload['content'] as $record) { + $this->assertContains($record, $exp->payload['content'], $text); + } + $this->assertCount(sizeof($exp->payload['content']), $act->payload['content'], $text); + } + } + public function setUp() { $this->clearData(); Arsse::$conf = new Conf(); @@ -529,14 +573,14 @@ class TestTinyTinyAPI extends Test\AbstractTest { 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); + $exp = $this->respGood((-1 * API::LABEL_OFFSET) - 2); $this->assertEquals($exp, $this->h->dispatch(new Request("POST", "", json_encode($in[0])))); - $exp = $this->respGood(-13); + $exp = $this->respGood((-1 * API::LABEL_OFFSET) - 3); $this->assertEquals($exp, $this->h->dispatch(new Request("POST", "", json_encode($in[1])))); // attempt to add the two labels again - $exp = $this->respGood(-12); + $exp = $this->respGood((-1 * API::LABEL_OFFSET) - 2); $this->assertEquals($exp, $this->h->dispatch(new Request("POST", "", json_encode($in[0])))); - $exp = $this->respGood(-13); + $exp = $this->respGood((-1 * API::LABEL_OFFSET) - 3); $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); @@ -549,14 +593,14 @@ class TestTinyTinyAPI extends Test\AbstractTest { public function testRemoveALabel() { $in = [ - ['op' => "removeLabel", 'sid' => "PriestsOfSyrinx", 'label_id' => -42], + ['op' => "removeLabel", 'sid' => "PriestsOfSyrinx", 'label_id' => -1042], ['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")); + Phake::when(Arsse::$db)->labelRemove(Arsse::$user->id, 18)->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])))); @@ -571,29 +615,29 @@ 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])))); $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); + Phake::verify(Arsse::$db, Phake::times(2))->labelRemove(Arsse::$user->id, 18); + Phake::verify(Arsse::$db)->labelRemove(Arsse::$user->id, 1088); } public function testRenameALabel() { $in = [ - ['op' => "renameLabel", 'sid' => "PriestsOfSyrinx", 'label_id' => -42, 'caption' => "Ook"], + ['op' => "renameLabel", 'sid' => "PriestsOfSyrinx", 'label_id' => -1042, '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' => -1042, 'caption' => "Eek"], + ['op' => "renameLabel", 'sid' => "PriestsOfSyrinx", 'label_id' => -1042, 'caption' => ""], + ['op' => "renameLabel", 'sid' => "PriestsOfSyrinx", 'label_id' => -1042, 'caption' => " "], + ['op' => "renameLabel", 'sid' => "PriestsOfSyrinx", 'label_id' => -1042], ['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' => ""]], + [Arsse::$user->id, 18, ['name' => "Ook"]], + [Arsse::$user->id, 1088, ['name' => "Eek"]], + [Arsse::$user->id, 18, ['name' => "Eek"]], + [Arsse::$user->id, 18, ['name' => ""]], + [Arsse::$user->id, 18, ['name' => " "]], + [Arsse::$user->id, 18, ['name' => ""]], ]; Phake::when(Arsse::$db)->labelPropertiesSet(...$db[0])->thenReturn(true); Phake::when(Arsse::$db)->labelPropertiesSet(...$db[1])->thenThrow(new ExceptionInput("subjectMissing")); @@ -622,38 +666,6 @@ class TestTinyTinyAPI extends Test\AbstractTest { } public function testRetrieveCategoryLists() { - $folders = [ - ['id' => 5, 'parent' => 3, 'children' => 0, 'name' => "Local"], - ['id' => 6, 'parent' => 3, 'children' => 0, 'name' => "National"], - ['id' => 4, 'parent' => null, 'children' => 0, 'name' => "Photography"], - ['id' => 3, 'parent' => null, 'children' => 2, 'name' => "Politics"], - ['id' => 2, 'parent' => 1, 'children' => 0, 'name' => "Rocketry"], - ['id' => 1, 'parent' => null, 'children' => 1, 'name' => "Science"], - ]; - $topFolders = [ - ['id' => 4, 'parent' => null, 'children' => 0, 'name' => "Photography"], - ['id' => 3, 'parent' => null, 'children' => 2, 'name' => "Politics"], - ['id' => 1, 'parent' => null, 'children' => 1, 'name' => "Science"], - ]; - $subscriptions = [ - ['folder' => null, 'top_folder' => null, 'unread' => 0], - ['folder' => 1, 'top_folder' => 1, 'unread' => 2], - ['folder' => 2, 'top_folder' => 1, 'unread' => 5], - ['folder' => 5, 'top_folder' => 3, 'unread' => 10], - ['folder' => 6, 'top_folder' => 3, 'unread' => 12], - ['folder' => 6, 'top_folder' => 3, 'unread' => 6], - ]; - $labels = [ - ['articles' => 0, 'read' => 0], - ['articles' => 100, 'read' => 94], - ['articles' => 2, 'read' => 0], - ]; - Phake::when(Arsse::$db)->folderList($this->anything(), null, true)->thenReturn(new Result($folders)); - Phake::when(Arsse::$db)->folderList($this->anything(), null, false)->thenReturn(new Result($topFolders)); - Phake::when(Arsse::$db)->subscriptionList($this->anything())->thenReturn(new Result($subscriptions)); - Phake::when(Arsse::$db)->labelList($this->anything())->thenReturn(new Result($labels)); - Phake::when(Arsse::$db)->articleCount($this->anything(), $this->anything())->thenReturn(7); // FIXME: this should check an unread+modifiedSince context - Phake::when(Arsse::$db)->articleCount($this->anything(), (new Context)->unread(true)->starred(true))->thenReturn(4); $in = [ ['op' => "getCategories", 'sid' => "PriestsOfSyrinx", 'include_empty' => true], ['op' => "getCategories", 'sid' => "PriestsOfSyrinx"], @@ -662,6 +674,12 @@ class TestTinyTinyAPI extends Test\AbstractTest { ['op' => "getCategories", 'sid' => "PriestsOfSyrinx", 'enable_nested' => true], ['op' => "getCategories", 'sid' => "PriestsOfSyrinx", 'enable_nested' => true, 'unread_only' => true], ]; + Phake::when(Arsse::$db)->folderList($this->anything(), null, true)->thenReturn(new Result($this->folders)); + Phake::when(Arsse::$db)->folderList($this->anything(), null, false)->thenReturn(new Result($this->topFolders)); + Phake::when(Arsse::$db)->subscriptionList($this->anything())->thenReturn(new Result($this->subscriptions)); + Phake::when(Arsse::$db)->labelList($this->anything())->thenReturn(new Result($this->labels)); + Phake::when(Arsse::$db)->articleCount($this->anything(), $this->anything())->thenReturn(7); // FIXME: this should check an unread+modifiedSince context + Phake::when(Arsse::$db)->articleStarred($this->anything())->thenReturn(['total' => 10, 'unread' => 4, 'read' => 6]); $exp = [ [ ['id' => 5, 'title' => "Local", 'unread' => 10, 'order_id' => 1], @@ -718,4 +736,36 @@ class TestTinyTinyAPI extends Test\AbstractTest { $this->assertEquals($this->respGood($exp[$a]), $this->h->dispatch(new Request("POST", "", json_encode($in[$a]))), "Test $a failed"); } } + + public function testRetrieveCounterList() { + $in = ['op' => "getCounters", 'sid' => "PriestsOfSyrinx"]; + Phake::when(Arsse::$db)->folderList($this->anything())->thenReturn(new Result($this->folders)); + Phake::when(Arsse::$db)->subscriptionList($this->anything())->thenReturn(new Result($this->subscriptions)); + Phake::when(Arsse::$db)->labelList($this->anything(), false)->thenReturn(new Result($this->usedLabels)); + Phake::when(Arsse::$db)->articleCount($this->anything(), $this->anything())->thenReturn(7); // FIXME: this should check an unread+modifiedSince context + Phake::when(Arsse::$db)->articleStarred($this->anything())->thenReturn(['total' => 10, 'unread' => 4, 'read' => 6]); + $exp = [ + ['id' => "global-unread", 'counter' => 35], + ['id' => "subscribed-feeds", 'counter' => 6], + ['id' => 0, 'counter' => 0, 'auxcounter' => 0], + ['id' => -1, 'counter' => 4, 'auxcounter' => 10], + ['id' => -2, 'counter' => 0, 'auxcounter' => 0], + ['id' => -3, 'counter' => 7, 'auxcounter' => 0], + ['id' => -4, 'counter' => 35, 'auxcounter' => 0], + ['id' => -1027, 'counter' => 6, 'auxcounter' => 100], + ['id' => -1025, 'counter' => 2, 'auxcounter' => 2], + ['id' => 3, 'has_img' => 1, 'counter' => 2, 'updated' => "2016-05-23T06:40:02"], + ['id' => 1, 'has_img' => 0, 'counter' => 5, 'updated' => "2017-09-15T22:54:16"], + ['id' => 2, 'has_img' => 1, 'counter' => 10, 'updated' => "2011-11-11T11:11:11"], + ['id' => 5, 'has_img' => 0, 'counter' => 12, 'updated' => "2017-07-07T17:07:17"], + ['id' => 4, 'has_img' => 1, 'counter' => 6, 'updated' => "2017-10-09T15:58:34"], + ['id' => 5, 'kind' => "cat", 'counter' => 10], + ['id' => 6, 'kind' => "cat", 'counter' => 18], + ['id' => 3, 'kind' => "cat", 'counter' => 28], + ['id' => 2, 'kind' => "cat", 'counter' => 5], + ['id' => 1, 'kind' => "cat", 'counter' => 7], + ['id' => -2, 'kind' => "cat", 'counter' => 8], + ]; + $this->assertResponse($this->respGood($exp), $this->h->dispatch(new Request("POST", "", json_encode($in)))); + } } diff --git a/tests/lib/Database/SeriesArticle.php b/tests/lib/Database/SeriesArticle.php index 3f363fbb..c7cdaa2e 100644 --- a/tests/lib/Database/SeriesArticle.php +++ b/tests/lib/Database/SeriesArticle.php @@ -730,17 +730,29 @@ trait SeriesArticle { Arsse::$db->articleMark($this->user, ['read'=>false]); } - public function testCountStarredArticles() { + public function testCountArticles() { $this->assertSame(2, Arsse::$db->articleCount("john.doe@example.com", (new Context)->starred(true))); - $this->assertSame(2, Arsse::$db->articleCount("john.doe@example.org", (new Context)->starred(true))); - $this->assertSame(2, Arsse::$db->articleCount("john.doe@example.net", (new Context)->starred(true))); + $this->assertSame(4, Arsse::$db->articleCount("john.doe@example.com", (new Context)->folder(1))); $this->assertSame(0, Arsse::$db->articleCount("jane.doe@example.com", (new Context)->starred(true))); } - public function testCountStarredArticlesWithoutAuthority() { + public function testCountArticlesWithoutAuthority() { Phake::when(Arsse::$user)->authorize->thenReturn(false); $this->assertException("notAuthorized", "User", "ExceptionAuthz"); - Arsse::$db->articleCount($this->user, (new Context)->starred(true)); + Arsse::$db->articleCount($this->user); + } + + public function testFetchStarredCounts() { + $exp1 = ['total' => 2, 'unread' => 1, 'read' => 1]; + $exp2 = ['total' => 0, 'unread' => 0, 'read' => 0]; + $this->assertSame($exp1, Arsse::$db->articleStarred("john.doe@example.com")); + $this->assertSame($exp2, Arsse::$db->articleStarred("jane.doe@example.com")); + } + + public function testFetchStarredCountsWithoutAuthority() { + Phake::when(Arsse::$user)->authorize->thenReturn(false); + $this->assertException("notAuthorized", "User", "ExceptionAuthz"); + Arsse::$db->articleStarred($this->user); } public function testFetchLatestEdition() {