diff --git a/lib/REST/Miniflux/V1.php b/lib/REST/Miniflux/V1.php index fdcac8a..c9a4fdd 100644 --- a/lib/REST/Miniflux/V1.php +++ b/lib/REST/Miniflux/V1.php @@ -54,17 +54,17 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { 'fetch_via_proxy' => "boolean", ]; protected const USER_META_MAP = [ - // Miniflux ID // Arsse ID Default value Extra - 'is_admin' => ["admin", false, false], - 'theme' => ["theme", "light_serif", false], - 'language' => ["lang", "en_US", false], - 'timezone' => ["tz", "UTC", false], - 'entry_sorting_direction' => ["sort_asc", false, false], - 'entries_per_page' => ["page_size", 100, false], - 'keyboard_shortcuts' => ["shortcuts", true, false], - 'show_reading_time' => ["reading_time", true, false], - 'entry_swipe' => ["swipe", true, false], - 'custom_css' => ["stylesheet", "", true], + // Miniflux ID // Arsse ID Default value + 'is_admin' => ["admin", false], + 'theme' => ["theme", "light_serif"], + 'language' => ["lang", "en_US"], + 'timezone' => ["tz", "UTC"], + 'entry_sorting_direction' => ["sort_asc", false], + 'entries_per_page' => ["page_size", 100], + 'keyboard_shortcuts' => ["shortcuts", true], + 'show_reading_time' => ["reading_time", true], + 'entry_swipe' => ["swipe", true], + 'stylesheet' => ["stylesheet", ""], ]; protected const CALLS = [ // handler method Admin Path Body Query Required fields '/categories' => [ @@ -76,13 +76,13 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { 'DELETE' => ["deleteCategory", false, true, false, false, []], ], '/categories/1/entries' => [ - 'GET' => ["getCategoryEntries", false, false, false, true, []], + 'GET' => ["getCategoryEntries", false, true, false, false, []], ], '/categories/1/entries/1' => [ - 'GET' => ["getCategoryEntry", false, false, false, true, []], + 'GET' => ["getCategoryEntry", false, true, false, false, []], ], '/categories/1/feeds' => [ - 'GET' => ["getCategoryFeeds", false, false, false, true, []], + 'GET' => ["getCategoryFeeds", false, true, false, false, []], ], '/categories/1/mark-all-as-read' => [ 'PUT' => ["markCategory", false, true, false, false, []], @@ -354,16 +354,11 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { 'id' => $info['num'], 'username' => $u, 'last_login_at' => $now, + 'google_id' => "", + 'openid_connect_id' => "", ]; - foreach (self::USER_META_MAP as $ext => [$int, $default, $extra]) { - if (!$extra) { - $entry[$ext] = $info[$int] ?? $default; - } else { - if (!isset($entry['extra'])) { - $entry['extra'] = []; - } - $entry['extra'][$ext] = $info[$int] ?? $default; - } + foreach (self::USER_META_MAP as $ext => [$int, $default]) { + $entry[$ext] = $info[$int] ?? $default; } $entry['entry_sorting_direction'] = ($entry['entry_sorting_direction']) ? "asc" : "desc"; $out[] = $entry; @@ -530,15 +525,21 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { return new EmptyResponse(204); } - protected function getCategories(): ResponseInterface { - $out = []; + protected function baseCategory(): array { + // the root folder is always a category and is always ID 1 + // the specific formulation is verbose, so a function makes sense $meta = Arsse::$user->propertiesGet(Arsse::$user->id, false); + return ['id' => 1, 'title' => $meta['root_folder_name'] ?? Arsse::$lang->msg("API.Miniflux.DefaultCategoryName"), 'user_id' => $meta['num']]; + } + + protected function getCategories(): ResponseInterface { // add the root folder as a category - $out[] = ['id' => 1, 'title' => $meta['root_folder_name'] ?? Arsse::$lang->msg("API.Miniflux.DefaultCategoryName"), 'user_id' => $meta['num']]; + $out = [$this->baseCategory()]; + $num = $out[0]['user_id']; // add other top folders as categories foreach (Arsse::$db->folderList(Arsse::$user->id, null, false) as $f) { // always add 1 to the ID since the root folder will always be 1 instead of 0. - $out[] = ['id' => $f['id'] + 1, 'title' => $f['name'], 'user_id' => $meta['num']]; + $out[] = ['id' => $f['id'] + 1, 'title' => $f['name'], 'user_id' => $num]; } return new Response($out); } @@ -622,13 +623,13 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { } protected function mapFolders(): array { - $meta = Arsse::$user->propertiesGet(Arsse::$user->id, false); - $folders = [0 => ['id' => 1, 'title' => $meta['root_folder_name'] ?? Arsse::$lang->msg("API.Miniflux.DefaultCategoryName"), 'user_id' => $meta['num']]]; + $folders = [0 => $this->baseCategory()]; + $num = $folders[0]['user_id']; foreach (Arsse::$db->folderList(Arsse::$user->id, null, false) as $r) { $folders[(int) $r['id']] = [ 'id' => ((int) $r['id']) + 1, 'title' => $r['name'], - 'user_id' => $meta['num'], + 'user_id' => $num, ]; } return $folders; @@ -676,6 +677,30 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { return new Response($out); } + protected function getCategoryFeeds(array $path): ResponseInterface { + // transform the category number into a folder number by subtracting one + $folder = ((int) $path[1]) - 1; + // unless the folder is root, list recursive + $recursive = $folder > 0; + $tr = Arsse::$db->begin(); + // get the list of subscriptions, or bail\ + try { + $subs = Arsse::$db->subscriptionList(Arsse::$user->id, $folder, $recursive)->getAll(); + } catch (ExceptionInput $e) { + // the folder does not exist + return new EmptyResponse(404); + } + // compile the list of folders; the feed list includes folder names + // NOTE: We compile the full list of folders in case someone has manually selected a non-top folder + $folders = $this->mapFolders(); + // next compile the list of feeds + $out = []; + foreach ($subs as $r) { + $out[] = $this->transformFeed($r, $folders); + } + return new Response($out); + } + protected function createFeed(array $data): ResponseInterface { $props = [ 'keep_rule' => $data['keeplist_rules'], diff --git a/tests/cases/REST/Miniflux/TestV1.php b/tests/cases/REST/Miniflux/TestV1.php index 84965ad..0bc1d50 100644 --- a/tests/cases/REST/Miniflux/TestV1.php +++ b/tests/cases/REST/Miniflux/TestV1.php @@ -15,6 +15,7 @@ use JKingWeb\Arsse\Db\ExceptionInput; use JKingWeb\Arsse\Misc\Date; use JKingWeb\Arsse\REST\Miniflux\V1; use JKingWeb\Arsse\REST\Miniflux\ErrorResponse; +use JKingWeb\Arsse\Test\FeedException; use JKingWeb\Arsse\User\ExceptionConflict; use JKingWeb\Arsse\User\ExceptionInput as UserExceptionInput; use Psr\Http\Message\ResponseInterface; @@ -34,6 +35,8 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest { 'id' => 1, 'username' => "john.doe@example.com", 'last_login_at' => self::NOW, + 'google_id' => "", + 'openid_connect_id' => "", 'is_admin' => true, 'theme' => "custom", 'language' => "fr_CA", @@ -43,14 +46,14 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest { 'keyboard_shortcuts' => false, 'show_reading_time' => false, 'entry_swipe' => false, - 'extra' => [ - 'custom_css' => "p {}", - ], + 'stylesheet' => "p {}", ], [ 'id' => 2, 'username' => "jane.doe@example.com", 'last_login_at' => self::NOW, + 'google_id' => "", + 'openid_connect_id' => "", 'is_admin' => false, 'theme' => "light_serif", 'language' => "en_US", @@ -60,11 +63,17 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest { 'keyboard_shortcuts' => true, 'show_reading_time' => true, 'entry_swipe' => true, - 'extra' => [ - 'custom_css' => "", - ], + 'stylesheet' => "", ], ]; + protected $feeds = [ + ['id' => 1, 'feed' => 12, 'url' => "http://example.com/ook", 'title' => "Ook", 'source' => "http://example.com/", 'icon_id' => 47, 'icon_url' => "http://example.com/icon", 'folder' => 2112, 'top_folder' => 5, 'pinned' => 0, 'err_count' => 1, 'err_msg' => "Oopsie", 'order_type' => 0, 'keep_rule' => "this|that", 'block_rule' => "both", 'added' => "2020-12-21 21:12:00", 'updated' => "2021-01-05 13:51:32", 'edited' => "2021-01-01 00:00:00", 'modified' => "2020-11-30 04:08:52", 'next_fetch' => "2021-01-20 00:00:00", 'etag' => "OOKEEK", 'scrape' => 0, 'unread' => 42], + ['id' => 55, 'feed' => 12, 'url' => "http://j%20k:super%20secret@example.com/eek", 'title' => "Eek", 'source' => "http://example.com/", 'icon_id' => null, 'icon_url' => null, 'folder' => null, 'top_folder' => null, 'pinned' => 0, 'err_count' => 0, 'err_msg' => null, 'order_type' => 0, 'keep_rule' => null, 'block_rule' => null, 'added' => "2020-12-21 21:12:00", 'updated' => "2021-01-05 13:51:32", 'edited' => null, 'modified' => "2020-11-30 04:08:52", 'next_fetch' => null, 'etag' => null, 'scrape' => 1, 'unread' => 0], + ]; + protected $feedsOut = [ + ['id' => 1, 'user_id' => 42, 'feed_url' => "http://example.com/ook", 'site_url' => "http://example.com/", 'title' => "Ook", 'checked_at' => "2021-01-05T13:51:32.000000Z", 'next_check_at' => "2021-01-20T00:00:00.000000Z", 'etag_header' => "OOKEEK", 'last_modified_header' => "Fri, 01 Jan 2021 00:00:00 GMT", 'parsing_error_message' => "Oopsie", 'parsing_error_count' => 1, 'scraper_rules' => "", 'rewrite_rules' => "", 'crawler' => false, 'blocklist_rules' => "both", 'keeplist_rules' => "this|that", 'user_agent' => "", 'username' => "", 'password' => "", 'disabled' => false, 'ignore_http_cache' => false, 'fetch_via_proxy' => false, 'category' => ['id' => 6, 'title' => "Cat Ook", 'user_id' => 42], 'icon' => ['feed_id' => 1,'icon_id' => 47]], + ['id' => 55, 'user_id' => 42, 'feed_url' => "http://example.com/eek", 'site_url' => "http://example.com/", 'title' => "Eek", 'checked_at' => "2021-01-05T13:51:32.000000Z", 'next_check_at' => "0001-01-01T00:00:00.000000Z", 'etag_header' => "", 'last_modified_header' => "", 'parsing_error_message' => "", 'parsing_error_count' => 0, 'scraper_rules' => "", 'rewrite_rules' => "", 'crawler' => true, 'blocklist_rules' => "", 'keeplist_rules' => "", 'user_agent' => "", 'username' => "j k", 'password' => "super secret", 'disabled' => false, 'ignore_http_cache' => false, 'fetch_via_proxy' => false, 'category' => ['id' => 1,'title' => "All", 'user_id' => 42], 'icon' => null], + ]; protected function req(string $method, string $target, $data = "", array $headers = [], ?string $user = "john.doe@example.com", bool $body = true): ResponseInterface { $prefix = "/v1"; @@ -535,82 +544,42 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest { ); } - public function testListReeds(): void { - \Phake::when(Arsse::$db)->folderList->thenReturn(new Result([ + public function testListFeeds(): void { + \Phake::when(Arsse::$db)->folderList->thenReturn(new Result($this->v([ ['id' => 5, 'name' => "Cat Ook"], - ])); - \Phake::when(Arsse::$db)->subscriptionList->thenReturn(new Result([ - ['id' => 1, 'feed' => 12, 'url' => "http://example.com/ook", 'title' => "Ook", 'source' => "http://example.com/", 'icon_id' => 47, 'icon_url' => "http://example.com/icon", 'folder' => 2112, 'top_folder' => 5, 'pinned' => 0, 'err_count' => 1, 'err_msg' => "Oopsie", 'order_type' => 0, 'keep_rule' => "this|that", 'block_rule' => "both", 'added' => "2020-12-21 21:12:00", 'updated' => "2021-01-05 13:51:32", 'edited' => "2021-01-01 00:00:00", 'modified' => "2020-11-30 04:08:52", 'next_fetch' => "2021-01-20 00:00:00", 'etag' => "OOKEEK", 'scrape' => 0, 'unread' => 42], - ['id' => 55, 'feed' => 12, 'url' => "http://j%20k:super%20secret@example.com/eek", 'title' => "Eek", 'source' => "http://example.com/", 'icon_id' => null, 'icon_url' => null, 'folder' => null, 'top_folder' => null, 'pinned' => 0, 'err_count' => 0, 'err_msg' => null, 'order_type' => 0, 'keep_rule' => null, 'block_rule' => null, 'added' => "2020-12-21 21:12:00", 'updated' => "2021-01-05 13:51:32", 'edited' => null, 'modified' => "2020-11-30 04:08:52", 'next_fetch' => null, 'etag' => null, 'scrape' => 1, 'unread' => 0], - ])); - $exp = new Response([ - [ - 'id' => 1, - 'user_id' => 42, - 'feed_url' => "http://example.com/ook", - 'site_url' => "http://example.com/", - 'title' => "Ook", - 'checked_at' => "2021-01-05T13:51:32.000000Z", - 'next_check_at' => "2021-01-20T00:00:00.000000Z", - 'etag_header' => "OOKEEK", - 'last_modified_header' => "Fri, 01 Jan 2021 00:00:00 GMT", - 'parsing_error_message' => "Oopsie", - 'parsing_error_count' => 1, - 'scraper_rules' => "", - 'rewrite_rules' => "", - 'crawler' => false, - 'blocklist_rules' => "both", - 'keeplist_rules' => "this|that", - 'user_agent' => "", - 'username' => "", - 'password' => "", - 'disabled' => false, - 'ignore_http_cache' => false, - 'fetch_via_proxy' => false, - 'category' => [ - 'id' => 6, - 'title' => "Cat Ook", - 'user_id' => 42 - ], - 'icon' => [ - 'feed_id' => 1, - 'icon_id' => 47 - ], - ], - [ - 'id' => 55, - 'user_id' => 42, - 'feed_url' => "http://example.com/eek", - 'site_url' => "http://example.com/", - 'title' => "Eek", - 'checked_at' => "2021-01-05T13:51:32.000000Z", - 'next_check_at' => "0001-01-01T00:00:00.000000Z", - 'etag_header' => "", - 'last_modified_header' => "", - 'parsing_error_message' => "", - 'parsing_error_count' => 0, - 'scraper_rules' => "", - 'rewrite_rules' => "", - 'crawler' => true, - 'blocklist_rules' => "", - 'keeplist_rules' => "", - 'user_agent' => "", - 'username' => "j k", - 'password' => "super secret", - 'disabled' => false, - 'ignore_http_cache' => false, - 'fetch_via_proxy' => false, - 'category' => [ - 'id' => 1, - 'title' => "All", - 'user_id' => 42 - ], - 'icon' => null, - ], - ]); + ]))); + \Phake::when(Arsse::$db)->subscriptionList->thenReturn(new Result($this->v($this->feeds))); + $exp = new Response($this->feedsOut); $this->assertMessage($exp, $this->req("GET", "/feeds")); } + public function testListFeedsOfACategory(): void { + \Phake::when(Arsse::$db)->folderList->thenReturn(new Result($this->v([ + ['id' => 5, 'name' => "Cat Ook"], + ]))); + \Phake::when(Arsse::$db)->subscriptionList->thenReturn(new Result($this->v($this->feeds))); + $exp = new Response($this->feedsOut); + $this->assertMessage($exp, $this->req("GET", "/categories/2112/feeds")); + \Phake::verify(Arsse::$db)->subscriptionList(Arsse::$user->id, 2111, true); + } + + public function testListFeedsOfTheRootCategory(): void { + \Phake::when(Arsse::$db)->folderList->thenReturn(new Result($this->v([ + ['id' => 5, 'name' => "Cat Ook"], + ]))); + \Phake::when(Arsse::$db)->subscriptionList->thenReturn(new Result($this->v($this->feeds))); + $exp = new Response($this->feedsOut); + $this->assertMessage($exp, $this->req("GET", "/categories/1/feeds")); + \Phake::verify(Arsse::$db)->subscriptionList(Arsse::$user->id, 0, false); + } + + public function testListFeedsOfAMissingCategory(): void { + \Phake::when(Arsse::$db)->subscriptionList->thenThrow(new ExceptionInput("idMissing")); + $exp = new EmptyResponse(404); + $this->assertMessage($exp, $this->req("GET", "/categories/2112/feeds")); + \Phake::verify(Arsse::$db)->subscriptionList(Arsse::$user->id, 2111, true); + } + /** @dataProvider provideFeedCreations */ public function testCreateAFeed(array $in, $out1, $out2, $out3, ResponseInterface $exp): void { if ($out1 instanceof \Exception) { diff --git a/tests/lib/FeedException.php b/tests/lib/FeedException.php deleted file mode 100644 index 414dbe4..0000000 --- a/tests/lib/FeedException.php +++ /dev/null @@ -1,15 +0,0 @@ -