diff --git a/lib/REST/Fever/API.php b/lib/REST/Fever/API.php index c5a93a3..47b4038 100644 --- a/lib/REST/Fever/API.php +++ b/lib/REST/Fever/API.php @@ -30,8 +30,8 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler { } public function dispatch(ServerRequestInterface $req): ResponseInterface { - $inR = $req->getQueryParams(); - $inW = $req->getParsedBody(); + $inR = $req->getQueryParams() ?? []; + $inW = $req->getParsedBody() ?? []; if (!array_key_exists("api", $inR)) { // the original would have shown the Fever UI in the absence of the "api" parameter, but we'll return 404 return new EmptyResponse(404); @@ -57,14 +57,13 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler { // otherwise if HTTP authentication failed or is required, deny access at the HTTP level return new EmptyResponse(401); } - // check that the user specified credentials + // produce a full response if authenticated or a basic response otherwise if ($this->logIn(strtolower($inW['api_key'] ?? ""))) { - $out['auth'] = 1; - $out = $this->processRequest($out, $inR, $inW); + $out = $this->processRequest($this->baseResponse(true), $inR, $inW); } else { - $out['auth'] = 0; + $out = $this->baseResponse(false); } - // return the result + // return the result, possibly formatted as XML return $this->formatResponse($out, $xml); break; default: @@ -73,9 +72,6 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler { } protected function processRequest(array $out, array $G, array $P): array { - // add base metadata - $out['last_refreshed_on_time'] = Date::transform(Arsse::$db->subscriptionRefreshed(Arsse::$user->id), "unix"); - // handle each possible parameter if (array_key_exists("feeds", $G) || array_key_exists("groups", $G)) { if (array_key_exists("groups", $G)) { $out['groups'] = $this->getGroups(); @@ -91,6 +87,18 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler { return $out; } + protected function baseResponse(bool $authenticated): array { + $out = [ + 'api_version' => self::LEVEL, + 'auth' => (int) $authenticated, + ]; + if ($authenticated) { + // authenticated requests always include the most recent feed refresh + $out['last_refreshed_on_time'] = $this->getRefreshTime(); + } + return $out; + } + protected function formatResponse(array $data, bool $xml): ResponseInterface { if ($xml) { throw \Exception("Not implemented yet"); @@ -115,17 +123,21 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler { return true; } + protected function getRefreshTime() { + return Date::transform(Arsse::$db->subscriptionRefreshed(Arsse::$user->id), "unix"); + } + protected function getFeeds(): array { $out = []; foreach (arsse::$db->subscriptionList(Arsse::$user->id) as $sub) { $out[] = [ - 'id' => (int) $sub['id'], - 'favicon_id' => (int) ($sub['favicon'] ? $sub['feed'] : 0), - 'title' => (string) $sub['title'], - 'url' => $sub['url'], - 'site_url' => $sub['source'], - 'is_spark' => 0, - 'lat_updated_on_time' => Date::transform($sub['edited'], "unix", "sql"), + 'id' => (int) $sub['id'], + 'favicon_id' => (int) ($sub['favicon'] ? $sub['feed'] : 0), + 'title' => (string) $sub['title'], + 'url' => $sub['url'], + 'site_url' => $sub['source'], + 'is_spark' => 0, + 'last_updated_on_time' => Date::transform($sub['edited'], "unix", "sql"), ]; } return $out; diff --git a/tests/cases/REST/Fever/TestAPI.php b/tests/cases/REST/Fever/TestAPI.php index be34712..272a25f 100644 --- a/tests/cases/REST/Fever/TestAPI.php +++ b/tests/cases/REST/Fever/TestAPI.php @@ -31,7 +31,7 @@ class TestAPI extends \JKingWeb\Arsse\Test\AbstractTest { return $value; } - protected function req($dataGet, $dataPost, string $method = "POST", string $type = null, string $url = "", string $user = null): ResponseInterface { + protected function req($dataGet, $dataPost = "", string $method = "POST", string $type = null, string $url = "", string $user = null): ResponseInterface { $url = "/fever/".$url; $server = [ 'REQUEST_METHOD' => $method, @@ -39,11 +39,10 @@ class TestAPI extends \JKingWeb\Arsse\Test\AbstractTest { 'HTTP_CONTENT_TYPE' => $type ?? "application/x-www-form-urlencoded", ]; $req = new ServerRequest($server, [], $url, $method, "php://memory"); - if (is_array($dataGet)) { - $req = $req->withRequestTarget($url)->withQueryParams($dataGet); - } else { - $req = $req->withRequestTarget($url."?".http_build_query((string) $dataGet, "", "&", \PHP_QUERY_RFC3986)); + if (!is_array($dataGet)) { + parse_str($dataGet, $dataGet); } + $req = $req->withRequestTarget($url)->withQueryParams($dataGet); if (is_array($dataPost)) { $req = $req->withParsedBody($dataPost); } else { @@ -72,8 +71,9 @@ class TestAPI extends \JKingWeb\Arsse\Test\AbstractTest { Arsse::$db = \Phake::mock(Database::class); \Phake::when(Arsse::$db)->begin->thenReturn(\Phake::mock(Transaction::class)); \Phake::when(Arsse::$db)->tokenLookup->thenReturn(['user' => "john.doe@example.com"]); - // instantiate the handler - $this->h = new API(); + // instantiate the handler as a partial mock to simplify testing + $this->h = \Phake::partialMock(API::class); + \Phake::when($this->h)->baseResponse->thenReturn([]); } public function tearDown() { @@ -89,8 +89,10 @@ class TestAPI extends \JKingWeb\Arsse\Test\AbstractTest { Arsse::$user->id = null; \Phake::when(Arsse::$db)->tokenLookup->thenThrow(new ExceptionInput("subjectMissing")); \Phake::when(Arsse::$db)->tokenLookup("fever.login", "validtoken")->thenReturn(['user' => "jane.doe@example.com"]); - // use a partial mock to test only the authentication process - $this->h = \Phake::partialMock(API::class); + // test only the authentication process + \Phake::when($this->h)->baseResponse->thenReturnCallback(function(bool $authenticated) { + return ['auth' => (int) $authenticated]; + }); \Phake::when($this->h)->processRequest->thenReturnCallback(function($out, $G, $P) { return $out; }); @@ -99,8 +101,8 @@ class TestAPI extends \JKingWeb\Arsse\Test\AbstractTest { } public function provideTokenAuthenticationRequests() { - $success = new JsonResponse(['api_version' => API::LEVEL, 'auth' => 1]); - $failure = new JsonResponse(['api_version' => API::LEVEL, 'auth' => 0]); + $success = new JsonResponse(['auth' => 1]); + $failure = new JsonResponse(['auth' => 0]); $denied = new EmptyResponse(401); return [ [false, true, null, [], ['api' => null], $failure], @@ -149,4 +151,58 @@ class TestAPI extends \JKingWeb\Arsse\Test\AbstractTest { [true, false, "validUser", ['api_key' => "invalidToken"], ['api' => null], $success], ]; } + + public function testListGroups() { + \Phake::when(Arsse::$db)->tagList(Arsse::$user->id)->thenReturn(new Result([ + ['id' => 1, 'name' => "Fascinating", 'subscriptions' => 2], + ['id' => 2, 'name' => "Interesting", 'subscriptions' => 2], + ['id' => 3, 'name' => "Boring", 'subscriptions' => 0], + ])); + \Phake::when(Arsse::$db)->tagSummarize(Arsse::$user->id)->thenReturn(new Result([ + ['id' => 1, 'name' => "Fascinating", 'subscription' => 1], + ['id' => 1, 'name' => "Fascinating", 'subscription' => 2], + ['id' => 2, 'name' => "Interesting", 'subscription' => 1], + ['id' => 2, 'name' => "Interesting", 'subscription' => 3], + ])); + $exp = new JsonResponse([ + 'groups' => [ + ['id' => 1, 'title' => "Fascinating"], + ['id' => 2, 'title' => "Interesting"], + ['id' => 3, 'title' => "Boring"], + ], + 'feeds_groups' => [ + ['group_id' => 1, 'feed_ids' => "1,2"], + ['group_id' => 2, 'feed_ids' => "1,3"], + ], + ]); + $act = $this->req("api&groups"); + $this->assertMessage($exp, $act); + } + + public function testListFeeds() { + \Phake::when(Arsse::$db)->subscriptionList(Arsse::$user->id)->thenReturn(new Result([ + ['id' => 1, 'feed' => 5, 'title' => "Ankh-Morpork News", 'url' => "http://example.com/feed", 'source' => "http://example.com/", 'edited' => "2019-01-01 21:12:00", 'favicon' => "http://example.com/favicon.ico"], + ['id' => 2, 'feed' => 9, 'title' => "Ook, Ook Eek Ook!", 'url' => "http://example.net/feed", 'source' => "http://example.net/", 'edited' => "1988-06-24 12:21:00", 'favicon' => ""], + ['id' => 3, 'feed' => 1, 'title' => "The Last Soul", 'url' => "http://example.org/feed", 'source' => "http://example.org/", 'edited' => "1991-08-12 03:22:00", 'favicon' => "http://example.org/favicon.ico"], + ])); + \Phake::when(Arsse::$db)->tagSummarize(Arsse::$user->id)->thenReturn(new Result([ + ['id' => 1, 'name' => "Fascinating", 'subscription' => 1], + ['id' => 1, 'name' => "Fascinating", 'subscription' => 2], + ['id' => 2, 'name' => "Interesting", 'subscription' => 1], + ['id' => 2, 'name' => "Interesting", 'subscription' => 3], + ])); + $exp = new JsonResponse([ + 'feeds' => [ + ['id' => 1, 'favicon_id' => 5, 'title' => "Ankh-Morpork News", 'url' => "http://example.com/feed", 'site_url' => "http://example.com/", 'is_spark' => 0, 'last_updated_on_time' => strtotime("2019-01-01T21:12:00Z")], + ['id' => 2, 'favicon_id' => 0, 'title' => "Ook, Ook Eek Ook!", 'url' => "http://example.net/feed", 'site_url' => "http://example.net/", 'is_spark' => 0, 'last_updated_on_time' => strtotime("1988-06-24T12:21:00Z")], + ['id' => 3, 'favicon_id' => 1, 'title' => "The Last Soul", 'url' => "http://example.org/feed", 'site_url' => "http://example.org/", 'is_spark' => 0, 'last_updated_on_time' => strtotime("1991-08-12T03:22:00Z")], + ], + 'feeds_groups' => [ + ['group_id' => 1, 'feed_ids' => "1,2"], + ['group_id' => 2, 'feed_ids' => "1,3"], + ], + ]); + $act = $this->req("api&feeds"); + $this->assertMessage($exp, $act); + } }