From 579551f5fd14e451aac80953aa88f5fa9bc698f0 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Mon, 30 Oct 2017 13:11:27 -0400 Subject: [PATCH] Implement TTRSS operation getFeedTree; fixes #94 --- lib/Database.php | 2 +- lib/REST/TinyTinyRSS/API.php | 177 ++++++++++++++++++++- locale/en.php | 9 +- tests/REST/TinyTinyRSS/TestTinyTinyAPI.php | 49 ++++-- 4 files changed, 218 insertions(+), 19 deletions(-) diff --git a/lib/Database.php b/lib/Database.php index 11142f6..16b2ab2 100644 --- a/lib/Database.php +++ b/lib/Database.php @@ -1165,7 +1165,7 @@ class Database { 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 owner is ? and articles >= ? + FROM arsse_labels where owner is ? and articles >= ? order by name ", "str", "int" )->run($user, !$includeEmpty); } diff --git a/lib/REST/TinyTinyRSS/API.php b/lib/REST/TinyTinyRSS/API.php index 8ea00e5..5bca289 100644 --- a/lib/REST/TinyTinyRSS/API.php +++ b/lib/REST/TinyTinyRSS/API.php @@ -21,10 +21,12 @@ Protocol difference so far: - 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?) + - Session lifetime is much shorter by default - Categories and feeds will always be sorted alphabetically (the protocol does not allow for clients to re-order) - 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) + - setArticleLabel responds with errors for invalid labels where TT-RSS simply returns a zero result + - The result of setArticleLabel counts only records which actually changed rather than all entries attempted */ @@ -283,6 +285,179 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler { return array_merge($special, $labels, $feeds, $cats); } + public function opGetFeedTree(array $data) : array { + $all = $data['include_empty'] ?? false; + $user = Arsse::$user->id; + $tSpecial = [ + 'type' => "feed", + 'auxcounter' => 0, + 'error' => "", + 'updated' => "", + ]; + $out = []; + // get the lists of categories and feeds + $cats = Arsse::$db->folderList($user, null, true)->getAll(); + $subs = Arsse::$db->subscriptionList($user)->getAll(); + // start with the special feeds + $out[] = [ + 'name' => Arsse::$lang->msg("API.TTRSS.Category.Special"), + 'id' => "CAT:-1", + 'bare_id' => -1, + 'type' => "category", + 'unread' => 0, + 'items' => [ + array_merge([ // All articles + 'name' => Arsse::$lang->msg("API.TTRSS.Feed.All"), + 'id' => "FEED:-4", + 'bare_id' => -4, + 'icon' => "images/folder.png", + 'unread' => array_reduce($subs, function($sum, $value) {return $sum + $value['unread'];}, 0), // the sum of all feeds' unread is the total unread + ], $tSpecial), + array_merge([ // Fresh articles + 'name' => Arsse::$lang->msg("API.TTRSS.Feed.Fresh"), + 'id' => "FEED:-3", + 'bare_id' => -3, + 'icon' => "images/fresh.png", + 'unread' => Arsse::$db->articleCount($user, (new Context)->unread(true)->modifiedSince(Date::sub("PT24H"))), + ], $tSpecial), + array_merge([ // Starred articles + 'name' => Arsse::$lang->msg("API.TTRSS.Feed.Starred"), + 'id' => "FEED:-1", + 'bare_id' => -1, + 'icon' => "images/star.png", + 'unread' => Arsse::$db->articleStarred($user)['unread'], + ], $tSpecial), + array_merge([ // Published articles + 'name' => Arsse::$lang->msg("API.TTRSS.Feed.Published"), + 'id' => "FEED:-2", + 'bare_id' => -2, + 'icon' => "images/feed.png", + 'unread' => 0, // TODO: unread count should be populated if the Published feed is ever implemented + ], $tSpecial), + array_merge([ // Archived articles + 'name' => Arsse::$lang->msg("API.TTRSS.Feed.Archived"), + 'id' => "FEED:0", + 'bare_id' => 0, + 'icon' => "images/archive.png", + 'unread' => 0, // Article archiving is not exposed by the API, so this is always zero + ], $tSpecial), + array_merge([ // Recently read + 'name' => Arsse::$lang->msg("API.TTRSS.Feed.Read"), + 'id' => "FEED:-6", + 'bare_id' => -6, + 'icon' => "images/time.png", + 'unread' => 0, // this is by definition zero; unread articles do not appear in this feed + ], $tSpecial), + ], + ]; + // next prepare labels + $items = []; + $unread = 0; + // add each label to a holding list (NOTE: the 'include_empty' parameter does not affect whether labels with zero total articles are shown: all labels are always shown) + foreach (Arsse::$db->labelList($user, true) as $l) { + $items[] = [ + 'name' => $l['name'], + 'id' => "FEED:".$this->labelOut($l['id']), + 'bare_id' => $this->labelOut($l['id']), + 'unread' => 0, + 'icon' => "images/label.png", + 'type' => "feed", + 'auxcounter' => 0, + 'error' => "", + 'updated' => "", + 'fg_color' => "", + 'bg_color' => "", + ]; + $unread += ($l['articles'] - $l['read']); + } + // if there are labels, all the label category, + if ($items) { + $out[] = [ + 'name' => Arsse::$lang->msg("API.TTRSS.Category.Labels"), + 'id' => "CAT:-2", + 'bare_id' => -2, + 'type' => "category", + 'unread' => $unread, + 'items' => $items, + ]; + } + // get the lists of categories and feeds + $cats = Arsse::$db->folderList($user, null, true)->getAll(); + $subs = Arsse::$db->subscriptionList($user)->getAll(); + // process all the top-level categories; their contents are gathered recursively in another function + $items = $this->enumerateCategories($cats, $subs, null, $all); + $out = array_merge($out, $items['list']); + // process uncategorized feeds; exclude the "Uncategorized" category if there are no orphan feeds and we're not displaying empties + $items = $this->enumerateFeeds($subs, null); + if ($items || !$all) { + $out[] = [ + 'name' => Arsse::$lang->msg("API.TTRSS.Category.Uncategorized"), + 'id' => "CAT:0", + 'bare_id' => 0, + 'type' => "category", + 'auxcounter' => 0, + 'unread' => 0, + 'child_unread' => 0, + 'checkbox' => false, + 'parent_id' => null, + 'param' => Arsse::$lang->msg("API.TTRSS.FeedCount", sizeof($items)), + 'items' => $items, + ]; + } + // return the result wrapped in some boilerplate + return ['categories' => ['identifier' => "id", 'label' => "name", 'items' => $out]]; + } + + protected function enumerateFeeds(array $subs, int $parent = null): array { + $out = []; + foreach ($subs as $s) { + if ($s['folder'] != $parent) { + continue; + } + $out[] = [ + 'name' => $s['title'], + 'id' => "FEED:".$s['id'], + 'bare_id' => $s['id'], + 'icon' => $s['favicon'] ? "feed-icons/".$s['id'].".ico" : false, + 'error' => (string) $s['err_msg'], + 'param' => Date::transform($s['updated'], "iso8601", "sql"), + 'unread' => 0, + 'auxcounter' => 0, + 'checkbox' => false, + ]; + } + return $out; + } + + protected function enumerateCategories(array $cats, array $subs, int $parent = null, bool $all = false): array { + $out = []; + $feedTotal = 0; + foreach ($cats as $c) { + if ($c['parent'] != $parent || (!$all && !($c['children'] + $c['feeds']))) { + // if the category is the wrong level, or if it's empty and we're not including empties, skip it + continue; + } + $children = $c['children'] ? $this->enumerateCategories($cats, $subs, $c['id'], $all) : ['list' => [], 'feeds' => 0]; + $feeds = $c['feeds'] ? $this->enumerateFeeds($subs, $c['id']) : []; + $count = sizeof($feeds) + $children['feeds']; + $out[] = [ + 'name' => $c['name'], + 'id' => "CAT:".$c['id'], + 'bare_id' => $c['id'], + 'parent_id' => $c['parent'], + 'type' => "category", + 'auxcounter' => 0, + 'unread' => 0, + 'child_unread' => 0, + 'checkbox' => false, + 'param' => Arsse::$lang->msg("API.TTRSS.FeedCount", $count), + 'items' => array_merge($children['list'], $feeds), + ]; + $feedTotal += $count; + } + return ['list' => $out, 'feeds' => $feedTotal]; + } + public function opGetCategories(array $data): array { // normalize input $all = $data['include_empty'] ?? false; diff --git a/locale/en.php b/locale/en.php index d151bee..12f3707 100644 --- a/locale/en.php +++ b/locale/en.php @@ -2,7 +2,14 @@ return [ 'API.TTRSS.Category.Uncategorized' => 'Uncategorized', 'API.TTRSS.Category.Special' => 'Special', - 'API.TTRSS.Category.Labels' => 'Labels', + 'API.TTRSS.Category.Labels' => 'Labels', + 'API.TTRSS.Feed.All' => 'All articles', + 'API.TTRSS.Feed.Fresh' => 'Fresh articles', + 'API.TTRSS.Feed.Starred' => 'Starred articles', + 'API.TTRSS.Feed.Published' => 'Published articles', + 'API.TTRSS.Feed.Archived' => 'Archived articles', + 'API.TTRSS.Feed.Read' => 'Recently read', + 'API.TTRSS.FeedCount' => '{0, select, 1 {(1 feed)} other {({0} feeds)}}', 'Driver.Db.SQLite3.Name' => 'SQLite 3', 'Driver.Service.Curl.Name' => 'HTTP (curl)', diff --git a/tests/REST/TinyTinyRSS/TestTinyTinyAPI.php b/tests/REST/TinyTinyRSS/TestTinyTinyAPI.php index e208c8c..211a71a 100644 --- a/tests/REST/TinyTinyRSS/TestTinyTinyAPI.php +++ b/tests/REST/TinyTinyRSS/TestTinyTinyAPI.php @@ -30,17 +30,17 @@ class TestTinyTinyAPI extends Test\AbstractTest { ['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'], + ['id' => 3, 'folder' => 1, 'top_folder' => 1, 'unread' => 2, 'updated' => "2016-05-23 06:40:02", 'err_msg' => 'argh', 'title' => 'Ars Technica', 'favicon' => 'http://example.com/3.png'], + ['id' => 4, 'folder' => 6, 'top_folder' => 3, 'unread' => 6, 'updated' => "2017-10-09 15:58:34", 'err_msg' => '', 'title' => 'CBC News', 'favicon' => 'http://example.com/4.png'], + ['id' => 6, 'folder' => null, 'top_folder' => null, 'unread' => 0, 'updated' => "2010-02-12 20:08:47", 'err_msg' => '', 'title' => 'Eurogamer', 'favicon' => 'http://example.com/6.png'], + ['id' => 1, 'folder' => 2, 'top_folder' => 1, 'unread' => 5, 'updated' => "2017-09-15 22:54:16", 'err_msg' => '', 'title' => 'NASA JPL', 'favicon' => null], + ['id' => 5, 'folder' => 6, 'top_folder' => 3, 'unread' => 12, 'updated' => "2017-07-07 17:07:17", 'err_msg' => '', 'title' => 'Ottawa Citizen', 'favicon' => ''], + ['id' => 2, 'folder' => 5, 'top_folder' => 3, 'unread' => 10, 'updated' => "2011-11-11 11:11:11", 'err_msg' => 'oops', 'title' => 'Toronto Star', 'favicon' => 'http://example.com/2.png'], ]; protected $labels = [ - ['id' => 5, 'articles' => 0, 'read' => 0, 'name' => "Interesting"], - ['id' => 3, 'articles' => 100, 'read' => 94, 'name' => "Fascinating"], - ['id' => 1, 'articles' => 2, 'read' => 0, 'name' => "Logical"], + ['id' => 3, 'articles' => 100, 'read' => 94, 'unread' => 6, 'name' => "Fascinating"], + ['id' => 5, 'articles' => 0, 'read' => 0, 'unread' => 0, 'name' => "Interesting"], + ['id' => 1, 'articles' => 2, 'read' => 0, 'unread' => 2, 'name' => "Logical"], ]; protected $usedLabels = [ ['id' => 3, 'articles' => 100, 'read' => 94, 'name' => "Fascinating"], @@ -766,10 +766,10 @@ class TestTinyTinyAPI extends Test\AbstractTest { ['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' => 4, 'has_img' => 1, 'counter' => 6, 'updated' => "2017-10-09T15:58:34"], ['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' => 2, 'has_img' => 1, 'counter' => 10, 'updated' => "2011-11-11T11:11:11"], ['id' => 5, 'kind' => "cat", 'counter' => 10], ['id' => 6, 'kind' => "cat", 'counter' => 18], ['id' => 3, 'kind' => "cat", 'counter' => 28], @@ -795,29 +795,29 @@ class TestTinyTinyAPI extends Test\AbstractTest { Phake::when(Arsse::$db)->articleLabelsGet($this->anything(), 4)->thenThrow(new ExceptionInput("idMissing")); $exp = [ [ - ['id' => -1025, 'caption' => "Logical", 'fg_color' => "", 'bg_color' => "", 'checked' => false], ['id' => -1027, 'caption' => "Fascinating", 'fg_color' => "", 'bg_color' => "", 'checked' => false], ['id' => -1029, 'caption' => "Interesting", 'fg_color' => "", 'bg_color' => "", 'checked' => false], + ['id' => -1025, 'caption' => "Logical", 'fg_color' => "", 'bg_color' => "", 'checked' => false], ], [ - ['id' => -1025, 'caption' => "Logical", 'fg_color' => "", 'bg_color' => "", 'checked' => true], ['id' => -1027, 'caption' => "Fascinating", 'fg_color' => "", 'bg_color' => "", 'checked' => true], ['id' => -1029, 'caption' => "Interesting", 'fg_color' => "", 'bg_color' => "", 'checked' => false], + ['id' => -1025, 'caption' => "Logical", 'fg_color' => "", 'bg_color' => "", 'checked' => true], ], [ - ['id' => -1025, 'caption' => "Logical", 'fg_color' => "", 'bg_color' => "", 'checked' => false], ['id' => -1027, 'caption' => "Fascinating", 'fg_color' => "", 'bg_color' => "", 'checked' => true], ['id' => -1029, 'caption' => "Interesting", 'fg_color' => "", 'bg_color' => "", 'checked' => false], + ['id' => -1025, 'caption' => "Logical", 'fg_color' => "", 'bg_color' => "", 'checked' => false], ], [ - ['id' => -1025, 'caption' => "Logical", 'fg_color' => "", 'bg_color' => "", 'checked' => false], ['id' => -1027, 'caption' => "Fascinating", 'fg_color' => "", 'bg_color' => "", 'checked' => false], ['id' => -1029, 'caption' => "Interesting", 'fg_color' => "", 'bg_color' => "", 'checked' => false], + ['id' => -1025, 'caption' => "Logical", 'fg_color' => "", 'bg_color' => "", 'checked' => false], ], [ - ['id' => -1025, 'caption' => "Logical", 'fg_color' => "", 'bg_color' => "", 'checked' => false], ['id' => -1027, 'caption' => "Fascinating", 'fg_color' => "", 'bg_color' => "", 'checked' => false], ['id' => -1029, 'caption' => "Interesting", 'fg_color' => "", 'bg_color' => "", 'checked' => false], + ['id' => -1025, 'caption' => "Logical", 'fg_color' => "", 'bg_color' => "", 'checked' => false], ], ]; for ($a = 0; $a < sizeof($in); $a++) { @@ -862,4 +862,21 @@ class TestTinyTinyAPI extends Test\AbstractTest { $this->assertResponse($exp, $this->h->dispatch(new Request("POST", "", json_encode($in[5])))); $this->assertResponse($exp, $this->h->dispatch(new Request("POST", "", json_encode($in[6])))); } + + public function testRetrieveFeedTree() { + $in = [ + ['op' => "getFeedTree", 'sid' => "PriestsOfSyrinx", 'include_empty' => true], + ['op' => "getFeedTree", 'sid' => "PriestsOfSyrinx"], + ]; + Phake::when(Arsse::$db)->folderList($this->anything(), null, true)->thenReturn(new Result($this->folders)); + Phake::when(Arsse::$db)->subscriptionList($this->anything())->thenReturn(new Result($this->subscriptions)); + Phake::when(Arsse::$db)->labelList($this->anything(), true)->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]); + // the expectations are packed tightly since they're very verbose; one can use var_export() (or convert to JSON) to pretty-print them + $exp = ['categories'=>['identifier'=>'id','label'=>'name','items'=>[['id'=>'CAT:-1','items'=>[['id'=>'FEED:-4','name'=>'All articles','unread'=>35,'type'=>'feed','error'=>'','updated'=>'','icon'=>'images/folder.png','bare_id'=>-4,'auxcounter'=>0,],['id'=>'FEED:-3','name'=>'Fresh articles','unread'=>7,'type'=>'feed','error'=>'','updated'=>'','icon'=>'images/fresh.png','bare_id'=>-3,'auxcounter'=>0,],['id'=>'FEED:-1','name'=>'Starred articles','unread'=>4,'type'=>'feed','error'=>'','updated'=>'','icon'=>'images/star.png','bare_id'=>-1,'auxcounter'=>0,],['id'=>'FEED:-2','name'=>'Published articles','unread'=>0,'type'=>'feed','error'=>'','updated'=>'','icon'=>'images/feed.png','bare_id'=>-2,'auxcounter'=>0,],['id'=>'FEED:0','name'=>'Archived articles','unread'=>0,'type'=>'feed','error'=>'','updated'=>'','icon'=>'images/archive.png','bare_id'=>0,'auxcounter'=>0,],['id'=>'FEED:-6','name'=>'Recently read','unread'=>0,'type'=>'feed','error'=>'','updated'=>'','icon'=>'images/time.png','bare_id'=>-6,'auxcounter'=>0,],],'name'=>'Special','type'=>'category','unread'=>0,'bare_id'=>-1,],['id'=>'CAT:-2','items'=>[['id'=>'FEED:-1027','name'=>'Fascinating','unread'=>0,'type'=>'feed','error'=>'','updated'=>'','icon'=>'images/label.png','bare_id'=>-1027,'auxcounter'=>0,'fg_color'=>'','bg_color'=>'',],['id'=>'FEED:-1029','name'=>'Interesting','unread'=>0,'type'=>'feed','error'=>'','updated'=>'','icon'=>'images/label.png','bare_id'=>-1029,'auxcounter'=>0,'fg_color'=>'','bg_color'=>'',],['id'=>'FEED:-1025','name'=>'Logical','unread'=>0,'type'=>'feed','error'=>'','updated'=>'','icon'=>'images/label.png','bare_id'=>-1025,'auxcounter'=>0,'fg_color'=>'','bg_color'=>'',],],'name'=>'Labels','type'=>'category','unread'=>8,'bare_id'=>-2,],['id'=>'CAT:4','bare_id'=>4,'auxcounter'=>0,'name'=>'Photography','items'=>[],'checkbox'=>false,'type'=>'category','unread'=>0,'child_unread'=>0,'parent_id'=>null,'param'=>'(0 feeds)',],['id'=>'CAT:3','bare_id'=>3,'auxcounter'=>0,'name'=>'Politics','items'=>[['id'=>'CAT:5','bare_id'=>5,'name'=>'Local','items'=>[['id'=>'FEED:2','bare_id'=>2,'auxcounter'=>0,'name'=>'Toronto Star','checkbox'=>false,'unread'=>0,'error'=>'oops','icon'=>'feed-icons/2.ico','param'=>'2011-11-11T11:11:11',],],'checkbox'=>false,'type'=>'category','unread'=>0,'child_unread'=>0,'auxcounter'=>0,'parent_id'=>3,'param'=>'(1 feed)',],['id'=>'CAT:6','bare_id'=>6,'name'=>'National','items'=>[['id'=>'FEED:4','bare_id'=>4,'auxcounter'=>0,'name'=>'CBC News','checkbox'=>false,'unread'=>0,'error'=>'','icon'=>'feed-icons/4.ico','param'=>'2017-10-09T15:58:34',],['id'=>'FEED:5','bare_id'=>5,'auxcounter'=>0,'name'=>'Ottawa Citizen','checkbox'=>false,'unread'=>0,'error'=>'','icon'=>false,'param'=>'2017-07-07T17:07:17',],],'checkbox'=>false,'type'=>'category','unread'=>0,'child_unread'=>0,'auxcounter'=>0,'parent_id'=>3,'param'=>'(2 feeds)',],],'checkbox'=>false,'type'=>'category','unread'=>0,'child_unread'=>0,'parent_id'=>null,'param'=>'(3 feeds)',],['id'=>'CAT:1','bare_id'=>1,'auxcounter'=>0,'name'=>'Science','items'=>[['id'=>'CAT:2','bare_id'=>2,'name'=>'Rocketry','items'=>[['id'=>'FEED:1','bare_id'=>1,'auxcounter'=>0,'name'=>'NASA JPL','checkbox'=>false,'unread'=>0,'error'=>'','icon'=>false,'param'=>'2017-09-15T22:54:16',],],'checkbox'=>false,'type'=>'category','unread'=>0,'child_unread'=>0,'auxcounter'=>0,'parent_id'=>1,'param'=>'(1 feed)',],['id'=>'FEED:3','bare_id'=>3,'auxcounter'=>0,'name'=>'Ars Technica','checkbox'=>false,'unread'=>0,'error'=>'argh','icon'=>'feed-icons/3.ico','param'=>'2016-05-23T06:40:02',],],'checkbox'=>false,'type'=>'category','unread'=>0,'child_unread'=>0,'parent_id'=>null,'param'=>'(2 feeds)',],['id'=>'CAT:0','bare_id'=>0,'auxcounter'=>0,'name'=>'Uncategorized','items'=>[['id'=>'FEED:6','bare_id'=>6,'auxcounter'=>0,'name'=>'Eurogamer','checkbox'=>false,'error'=>'','icon'=>'feed-icons/6.ico','param'=>'2010-02-12T20:08:47','unread'=>0,],],'type'=>'category','checkbox'=>false,'unread'=>0,'child_unread'=>0,'parent_id'=>null,'param'=>'(1 feed)',],],],]; + $this->assertEquals($this->respGood($exp), $this->h->dispatch(new Request("POST", "", json_encode($in[0])))); + $exp = ['categories'=>['identifier'=>'id','label'=>'name','items'=>[['id'=>'CAT:-1','items'=>[['id'=>'FEED:-4','name'=>'All articles','unread'=>35,'type'=>'feed','error'=>'','updated'=>'','icon'=>'images/folder.png','bare_id'=>-4,'auxcounter'=>0,],['id'=>'FEED:-3','name'=>'Fresh articles','unread'=>7,'type'=>'feed','error'=>'','updated'=>'','icon'=>'images/fresh.png','bare_id'=>-3,'auxcounter'=>0,],['id'=>'FEED:-1','name'=>'Starred articles','unread'=>4,'type'=>'feed','error'=>'','updated'=>'','icon'=>'images/star.png','bare_id'=>-1,'auxcounter'=>0,],['id'=>'FEED:-2','name'=>'Published articles','unread'=>0,'type'=>'feed','error'=>'','updated'=>'','icon'=>'images/feed.png','bare_id'=>-2,'auxcounter'=>0,],['id'=>'FEED:0','name'=>'Archived articles','unread'=>0,'type'=>'feed','error'=>'','updated'=>'','icon'=>'images/archive.png','bare_id'=>0,'auxcounter'=>0,],['id'=>'FEED:-6','name'=>'Recently read','unread'=>0,'type'=>'feed','error'=>'','updated'=>'','icon'=>'images/time.png','bare_id'=>-6,'auxcounter'=>0,],],'name'=>'Special','type'=>'category','unread'=>0,'bare_id'=>-1,],['id'=>'CAT:-2','items'=>[['id'=>'FEED:-1027','name'=>'Fascinating','unread'=>0,'type'=>'feed','error'=>'','updated'=>'','icon'=>'images/label.png','bare_id'=>-1027,'auxcounter'=>0,'fg_color'=>'','bg_color'=>'',],['id'=>'FEED:-1029','name'=>'Interesting','unread'=>0,'type'=>'feed','error'=>'','updated'=>'','icon'=>'images/label.png','bare_id'=>-1029,'auxcounter'=>0,'fg_color'=>'','bg_color'=>'',],['id'=>'FEED:-1025','name'=>'Logical','unread'=>0,'type'=>'feed','error'=>'','updated'=>'','icon'=>'images/label.png','bare_id'=>-1025,'auxcounter'=>0,'fg_color'=>'','bg_color'=>'',],],'name'=>'Labels','type'=>'category','unread'=>8,'bare_id'=>-2,],['id'=>'CAT:3','bare_id'=>3,'auxcounter'=>0,'name'=>'Politics','items'=>[['id'=>'CAT:5','bare_id'=>5,'name'=>'Local','items'=>[['id'=>'FEED:2','bare_id'=>2,'auxcounter'=>0,'name'=>'Toronto Star','checkbox'=>false,'unread'=>0,'error'=>'oops','icon'=>'feed-icons/2.ico','param'=>'2011-11-11T11:11:11',],],'checkbox'=>false,'type'=>'category','unread'=>0,'child_unread'=>0,'auxcounter'=>0,'parent_id'=>3,'param'=>'(1 feed)',],['id'=>'CAT:6','bare_id'=>6,'name'=>'National','items'=>[['id'=>'FEED:4','bare_id'=>4,'auxcounter'=>0,'name'=>'CBC News','checkbox'=>false,'unread'=>0,'error'=>'','icon'=>'feed-icons/4.ico','param'=>'2017-10-09T15:58:34',],['id'=>'FEED:5','bare_id'=>5,'auxcounter'=>0,'name'=>'Ottawa Citizen','checkbox'=>false,'unread'=>0,'error'=>'','icon'=>false,'param'=>'2017-07-07T17:07:17',],],'checkbox'=>false,'type'=>'category','unread'=>0,'child_unread'=>0,'auxcounter'=>0,'parent_id'=>3,'param'=>'(2 feeds)',],],'checkbox'=>false,'type'=>'category','unread'=>0,'child_unread'=>0,'parent_id'=>null,'param'=>'(3 feeds)',],['id'=>'CAT:1','bare_id'=>1,'auxcounter'=>0,'name'=>'Science','items'=>[['id'=>'CAT:2','bare_id'=>2,'name'=>'Rocketry','items'=>[['id'=>'FEED:1','bare_id'=>1,'auxcounter'=>0,'name'=>'NASA JPL','checkbox'=>false,'unread'=>0,'error'=>'','icon'=>false,'param'=>'2017-09-15T22:54:16',],],'checkbox'=>false,'type'=>'category','unread'=>0,'child_unread'=>0,'auxcounter'=>0,'parent_id'=>1,'param'=>'(1 feed)',],['id'=>'FEED:3','bare_id'=>3,'auxcounter'=>0,'name'=>'Ars Technica','checkbox'=>false,'unread'=>0,'error'=>'argh','icon'=>'feed-icons/3.ico','param'=>'2016-05-23T06:40:02',],],'checkbox'=>false,'type'=>'category','unread'=>0,'child_unread'=>0,'parent_id'=>null,'param'=>'(2 feeds)',],['id'=>'CAT:0','bare_id'=>0,'auxcounter'=>0,'name'=>'Uncategorized','items'=>[['id'=>'FEED:6','bare_id'=>6,'auxcounter'=>0,'name'=>'Eurogamer','checkbox'=>false,'error'=>'','icon'=>'feed-icons/6.ico','param'=>'2010-02-12T20:08:47','unread'=>0,],],'type'=>'category','checkbox'=>false,'unread'=>0,'child_unread'=>0,'parent_id'=>null,'param'=>'(1 feed)',],],],]; + $this->assertEquals($this->respGood($exp), $this->h->dispatch(new Request("POST", "", json_encode($in[1])))); + } }