Browse Source

Implement TT-RSS API level 15

rpm
J. King 3 years ago
parent
commit
211cea648e
  1. 16
      lib/REST/TinyTinyRSS/API.php
  2. 87
      tests/cases/REST/TinyTinyRSS/TestAPI.php

16
lib/REST/TinyTinyRSS/API.php

@ -24,7 +24,7 @@ use Laminas\Diactoros\Response\JsonResponse as Response;
use Laminas\Diactoros\Response\EmptyResponse; use Laminas\Diactoros\Response\EmptyResponse;
class API extends \JKingWeb\Arsse\REST\AbstractHandler { 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 public const VERSION = "17.4"; // emulated TT-RSS version
protected const LABEL_OFFSET = 1024; // offset below zero at which labels begin, counting down 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` '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` 'search' => ValueInfo::T_STRING, // search string for `getHeadlines`
'field' => ValueInfo::T_INT, // which state to change in `updateArticle` '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 'data' => ValueInfo::T_STRING, // note text in `updateArticle` if setting a note
]; ];
protected const VIEW_MODES = ["all_articles", "adaptive", "unread", "marked", "has_note", "published"]; 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 { public function opCatchUpFeed(array $data): array {
$id = $data['feed_id'] ?? self::FEED_ARCHIVED; $id = $data['feed_id'] ?? self::FEED_ARCHIVED;
$cat = $data['is_cat'] ?? false; $cat = $data['is_cat'] ?? false;
$mode = $data['mode'] ?? "all";
$out = ['status' => "OK"]; $out = ['status' => "OK"];
// first prepare the context; unsupported contexts simply return early // first prepare the context; unsupported contexts simply return early
$c = (new Context)->hidden(false); $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 // perform the marking
try { try {
Arsse::$db->articleMark(Arsse::$user->id, ['read' => true], $c); 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 { public function opUpdateArticle(array $data): array {
// normalize input // normalize input
$articles = array_filter(ValueInfo::normalize(explode(",", (string) $data['article_ids']), ValueInfo::T_INT | ValueInfo::M_ARRAY), [ValueInfo::class, "id"]); $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 (!$articles) {
// if there are no valid articles this is an error // if there are no valid articles this is an error
throw new Exception("INCORRECT_USAGE"); throw new Exception("INCORRECT_USAGE");

87
tests/cases/REST/TinyTinyRSS/TestAPI.php

@ -19,8 +19,8 @@ use JKingWeb\Arsse\REST\TinyTinyRSS\API;
use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ResponseInterface;
use Laminas\Diactoros\Response\JsonResponse as Response; use Laminas\Diactoros\Response\JsonResponse as Response;
use Laminas\Diactoros\Response\EmptyResponse; use Laminas\Diactoros\Response\EmptyResponse;
/** @covers \JKingWeb\Arsse\REST\TinyTinyRSS\API<extended> /** @covers \JKingWeb\Arsse\REST\TinyTinyRSS\API<extended>
* @covers \JKingWeb\Arsse\REST\TinyTinyRSS\Exception */ * @covers \JKingWeb\Arsse\REST\TinyTinyRSS\Exception */
class TestAPI extends \JKingWeb\Arsse\Test\AbstractTest { class TestAPI extends \JKingWeb\Arsse\Test\AbstractTest {
protected const NOW = "2020-12-21T23:09:17.189065Z"; protected const NOW = "2020-12-21T23:09:17.189065Z";
@ -1309,55 +1309,46 @@ LONG_STRING;
$this->assertMessage($this->respGood($exp), $this->req($in[1])); $this->assertMessage($this->respGood($exp), $this->req($in[1]));
} }
public function testMarkFeedsAsRead(): void { /** @dataProvider provideMassMarkings */
$in1 = [ public function testMarkFeedsAsRead(array $in, ?Context $c): void {
// no-ops $base = ['op' => "catchupFeed", 'sid' => "PriestsOfSyrinx"];
['op' => "catchupFeed", 'sid' => "PriestsOfSyrinx"], $in = array_merge($base, $in);
['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],
];
\Phake::when(Arsse::$db)->articleMark->thenThrow(new ExceptionInput("typeViolation")); \Phake::when(Arsse::$db)->articleMark->thenThrow(new ExceptionInput("typeViolation"));
$exp = $this->respGood(['status' => "OK"]); // create a mock-current time
// verify the above are in fact no-ops \Phake::when(Arsse::$obj)->get(\DateTimeImmutable::class)->thenReturn(new \DateTimeImmutable(self::NOW));
for ($a = 0; $a < sizeof($in1); $a++) { // TT-RSS always responds the same regardless of success or failure
$this->assertMessage($exp, $this->req($in1[$a]), "Test $a failed"); $this->assertMessage($this->respGood(['status' => "OK"]), $this->req($in));
} if (isset($c)) {
\Phake::verify(Arsse::$db, \Phake::times(0))->articleMark; \Phake::verify(Arsse::$db)->articleMark(Arsse::$user->id, ['read' => true], $c);
// verify the simple contexts } else {
for ($a = 0; $a < sizeof($in2); $a++) { \Phake::verify(Arsse::$db, \Phake::times(0))->articleMark;
$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");
} }
\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 { public function testRetrieveFeedList(): void {

Loading…
Cancel
Save