From 211cea648e18c5ecb5b4d73b2fd0269b49ce2264 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Mon, 8 Feb 2021 19:07:49 -0500 Subject: [PATCH] Implement TT-RSS API level 15 --- lib/REST/TinyTinyRSS/API.php | 16 ++++- tests/cases/REST/TinyTinyRSS/TestAPI.php | 87 +++++++++++------------- 2 files changed, 53 insertions(+), 50 deletions(-) diff --git a/lib/REST/TinyTinyRSS/API.php b/lib/REST/TinyTinyRSS/API.php index 3de4863..0d4d12a 100644 --- a/lib/REST/TinyTinyRSS/API.php +++ b/lib/REST/TinyTinyRSS/API.php @@ -24,7 +24,7 @@ use Laminas\Diactoros\Response\JsonResponse as Response; use Laminas\Diactoros\Response\EmptyResponse; class API extends \JKingWeb\Arsse\REST\AbstractHandler { - public const LEVEL = 14; // emulated API level + public const LEVEL = 15; // emulated API level public const VERSION = "17.4"; // emulated TT-RSS version protected const LABEL_OFFSET = 1024; // offset below zero at which labels begin, counting down @@ -79,7 +79,7 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler { 'include_header' => ValueInfo::T_BOOL | ValueInfo::M_DROP, // whether to attach a header to the results of `getHeadlines` 'search' => ValueInfo::T_STRING, // search string for `getHeadlines` 'field' => ValueInfo::T_INT, // which state to change in `updateArticle` - 'mode' => ValueInfo::T_INT, // whether to set, clear, or toggle the selected state in `updateArticle` + 'mode' => ValueInfo::T_MIXED, // whether to set, clear, or toggle the selected state in `updateArticle` (integer), or whether to ignore a certain recent timeframe in `catchupFeed` (string) 'data' => ValueInfo::T_STRING, // note text in `updateArticle` if setting a note ]; protected const VIEW_MODES = ["all_articles", "adaptive", "unread", "marked", "has_note", "published"]; @@ -1037,6 +1037,7 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler { public function opCatchUpFeed(array $data): array { $id = $data['feed_id'] ?? self::FEED_ARCHIVED; $cat = $data['is_cat'] ?? false; + $mode = $data['mode'] ?? "all"; $out = ['status' => "OK"]; // first prepare the context; unsupported contexts simply return early $c = (new Context)->hidden(false); @@ -1089,6 +1090,16 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler { } } } + switch ($mode) { + case "2week": + $c->notModifiedSince(Date::sub("P2W", $this->now())); + break; + case "1week": + $c->notModifiedSince(Date::sub("P1W", $this->now())); + break; + case "1day": + $c->notModifiedSince(Date::sub("PT24H", $this->now())); + } // perform the marking try { Arsse::$db->articleMark(Arsse::$user->id, ['read' => true], $c); @@ -1102,6 +1113,7 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler { public function opUpdateArticle(array $data): array { // normalize input $articles = array_filter(ValueInfo::normalize(explode(",", (string) $data['article_ids']), ValueInfo::T_INT | ValueInfo::M_ARRAY), [ValueInfo::class, "id"]); + $data['mode'] = ValueInfo::normalize($data['mode'], ValueInfo::T_INT); if (!$articles) { // if there are no valid articles this is an error throw new Exception("INCORRECT_USAGE"); diff --git a/tests/cases/REST/TinyTinyRSS/TestAPI.php b/tests/cases/REST/TinyTinyRSS/TestAPI.php index 874a103..e79a2f6 100644 --- a/tests/cases/REST/TinyTinyRSS/TestAPI.php +++ b/tests/cases/REST/TinyTinyRSS/TestAPI.php @@ -19,8 +19,8 @@ use JKingWeb\Arsse\REST\TinyTinyRSS\API; use Psr\Http\Message\ResponseInterface; use Laminas\Diactoros\Response\JsonResponse as Response; use Laminas\Diactoros\Response\EmptyResponse; - /** @covers \JKingWeb\Arsse\REST\TinyTinyRSS\API + * @covers \JKingWeb\Arsse\REST\TinyTinyRSS\Exception */ class TestAPI extends \JKingWeb\Arsse\Test\AbstractTest { protected const NOW = "2020-12-21T23:09:17.189065Z"; @@ -1309,55 +1309,46 @@ LONG_STRING; $this->assertMessage($this->respGood($exp), $this->req($in[1])); } - public function testMarkFeedsAsRead(): void { - $in1 = [ - // no-ops - ['op' => "catchupFeed", 'sid' => "PriestsOfSyrinx"], - ['op' => "catchupFeed", 'sid' => "PriestsOfSyrinx", 'feed_id' => 0], - ['op' => "catchupFeed", 'sid' => "PriestsOfSyrinx", 'feed_id' => -2], - ['op' => "catchupFeed", 'sid' => "PriestsOfSyrinx", 'feed_id' => -6], - ['op' => "catchupFeed", 'sid' => "PriestsOfSyrinx", 'feed_id' => -1, 'is_cat' => true], - ['op' => "catchupFeed", 'sid' => "PriestsOfSyrinx", 'feed_id' => -3, 'is_cat' => true], - ['op' => "catchupFeed", 'sid' => "PriestsOfSyrinx", 'feed_id' => -4, 'is_cat' => true], - ]; - $in2 = [ - // simple contexts - ['op' => "catchupFeed", 'sid' => "PriestsOfSyrinx", 'feed_id' => -1], - ['op' => "catchupFeed", 'sid' => "PriestsOfSyrinx", 'feed_id' => -4], - ['op' => "catchupFeed", 'sid' => "PriestsOfSyrinx", 'feed_id' => -2112], - ['op' => "catchupFeed", 'sid' => "PriestsOfSyrinx", 'feed_id' => 2112], - ['op' => "catchupFeed", 'sid' => "PriestsOfSyrinx", 'feed_id' => 42, 'is_cat' => true], - ['op' => "catchupFeed", 'sid' => "PriestsOfSyrinx", 'feed_id' => 0, 'is_cat' => true], - ['op' => "catchupFeed", 'sid' => "PriestsOfSyrinx", 'feed_id' => -2, 'is_cat' => true], - ]; - $in3 = [ - // this one has a tricky time-based context - ['op' => "catchupFeed", 'sid' => "PriestsOfSyrinx", 'feed_id' => -3], - ]; + /** @dataProvider provideMassMarkings */ + public function testMarkFeedsAsRead(array $in, ?Context $c): void { + $base = ['op' => "catchupFeed", 'sid' => "PriestsOfSyrinx"]; + $in = array_merge($base, $in); \Phake::when(Arsse::$db)->articleMark->thenThrow(new ExceptionInput("typeViolation")); - $exp = $this->respGood(['status' => "OK"]); - // verify the above are in fact no-ops - for ($a = 0; $a < sizeof($in1); $a++) { - $this->assertMessage($exp, $this->req($in1[$a]), "Test $a failed"); - } - \Phake::verify(Arsse::$db, \Phake::times(0))->articleMark; - // verify the simple contexts - for ($a = 0; $a < sizeof($in2); $a++) { - $this->assertMessage($exp, $this->req($in2[$a]), "Test $a failed"); - } - \Phake::verify(Arsse::$db)->articleMark($this->anything(), ['read' => true], (new Context)->hidden(false)); - \Phake::verify(Arsse::$db)->articleMark($this->anything(), ['read' => true], (new Context)->starred(true)->hidden(false)); - \Phake::verify(Arsse::$db)->articleMark($this->anything(), ['read' => true], (new Context)->label(1088)->hidden(false)); - \Phake::verify(Arsse::$db)->articleMark($this->anything(), ['read' => true], (new Context)->subscription(2112)->hidden(false)); - \Phake::verify(Arsse::$db)->articleMark($this->anything(), ['read' => true], (new Context)->folder(42)->hidden(false)); - \Phake::verify(Arsse::$db)->articleMark($this->anything(), ['read' => true], (new Context)->folderShallow(0)->hidden(false)); - \Phake::verify(Arsse::$db)->articleMark($this->anything(), ['read' => true], (new Context)->labelled(true)->hidden(false)); - // verify the time-based mock - $t = Date::sub("PT24H"); - for ($a = 0; $a < sizeof($in3); $a++) { - $this->assertMessage($exp, $this->req($in3[$a]), "Test $a failed"); + // create a mock-current time + \Phake::when(Arsse::$obj)->get(\DateTimeImmutable::class)->thenReturn(new \DateTimeImmutable(self::NOW)); + // TT-RSS always responds the same regardless of success or failure + $this->assertMessage($this->respGood(['status' => "OK"]), $this->req($in)); + if (isset($c)) { + \Phake::verify(Arsse::$db)->articleMark(Arsse::$user->id, ['read' => true], $c); + } else { + \Phake::verify(Arsse::$db, \Phake::times(0))->articleMark; } - \Phake::verify(Arsse::$db)->articleMark($this->anything(), ['read' => true], $this->equalTo((new Context)->hidden(false)->modifiedSince($t), 2)); // within two seconds + } + + public function provideMassMarkings(): iterable { + $c = (new Context)->hidden(false); + return [ + [[], null], + [['feed_id' => 0], null], + [['feed_id' => 0, 'is_cat' => true], (clone $c)->folderShallow(0)], + [['feed_id' => 0, 'is_cat' => true, 'mode' => "bogus"], (clone $c)->folderShallow(0)], + [['feed_id' => -1], (clone $c)->starred(true)], + [['feed_id' => -1, 'is_cat' => true], null], + [['feed_id' => -3], (clone $c)->modifiedSince(Date::sub("PT24H", self::NOW))], + [['feed_id' => -3, 'mode' => "1day"], (clone $c)->modifiedSince(Date::sub("PT24H", self::NOW))->notModifiedSince(Date::sub("PT24H", self::NOW))], // this is a nonsense query, but it's what TT-RSS appearsto do + [['feed_id' => -3, 'is_cat' => true], null], + [['feed_id' => -2], null], + [['feed_id' => -2, 'is_cat' => true], (clone $c)->labelled(true)], + [['feed_id' => -2, 'is_cat' => true, 'mode' => "all"], (clone $c)->labelled(true)], + [['feed_id' => -4], $c], + [['feed_id' => -4, 'is_cat' => true], null], + [['feed_id' => -6], null], + [['feed_id' => -2112], (clone $c)->label(1088)], + [['feed_id' => 42, 'is_cat' => true], (clone $c)->folder(42)], + [['feed_id' => 42, 'is_cat' => true, 'mode' => "1week"], (clone $c)->folder(42)->notModifiedSince(Date::sub("P1W", self::NOW))], + [['feed_id' => 2112], (clone $c)->subscription(2112)], + [['feed_id' => 2112, 'mode' => "2week"], (clone $c)->subscription(2112)->notModifiedSince(Date::sub("P2W", self::NOW))], + ]; } public function testRetrieveFeedList(): void {