mirror of
https://code.mensbeam.com/MensBeam/Arsse.git
synced 2024-12-22 21:22:40 +00:00
Implement TTRSS operation getHeadlines; fixe #82
This commit is contained in:
parent
faf00d63ba
commit
c669273792
3 changed files with 495 additions and 40 deletions
|
@ -41,14 +41,19 @@ Protocol difference so far:
|
|||
- IDs for enclosures are ommitted as we don't give them IDs
|
||||
- Searching in getHeadlines is not yet implemented
|
||||
- Category -3 (all non-special feeds) is handled correctly in getHeadlines; TT-RSS returns results for feed -3 (Fresh)
|
||||
- Sorting of headlines does not match TT-RSS: special feeds are not sorted specially like they should be
|
||||
- The 'sanitize', 'force_update', and 'has_sandbox' parameters of getHeadlines are ignored
|
||||
- The 'always_display_attachments' key of articles in getHeadlines is omitted, as the user cannot express a preference
|
||||
*/
|
||||
|
||||
|
||||
|
||||
class API extends \JKingWeb\Arsse\REST\AbstractHandler {
|
||||
const LEVEL = 14;
|
||||
const VERSION = "17.4";
|
||||
const LABEL_OFFSET = 1024;
|
||||
const LEVEL = 14; // emulated API level
|
||||
const VERSION = "17.4"; // emulated TT-RSS version
|
||||
const LABEL_OFFSET = 1024; // offset below zero at which labels begin, counting down
|
||||
const LIMIT_ARTICLES = 200; // maximum number of articles returned by getHeadlines
|
||||
const LIMIT_EXCERPT = 100; // maximum length of excerpts in getHeadlines, counted in grapheme units
|
||||
// special feeds
|
||||
const FEED_ARCHIVED = 0;
|
||||
const FEED_STARRED = -1;
|
||||
|
@ -91,18 +96,14 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler {
|
|||
'show_excerpt' => ValueInfo::T_BOOL | ValueInfo::M_DROP, // whether to include article excerpts in `getHeadlines`
|
||||
'show_content' => ValueInfo::T_BOOL | ValueInfo::M_DROP, // whether to include article content in `getHeadlines`
|
||||
'include_attachments' => ValueInfo::T_BOOL | ValueInfo::M_DROP, // whether to include article enclosures in `getHeadlines`
|
||||
'view_mode' => ValueInfo::T_STRING,
|
||||
'since_id' => ValueInfo::T_INT,
|
||||
'order_by' => ValueInfo::T_STRING,
|
||||
'sanitize' => ValueInfo::T_BOOL | ValueInfo::M_DROP,
|
||||
'force_update' => ValueInfo::T_BOOL | ValueInfo::M_DROP,
|
||||
'has_sandbox' => ValueInfo::T_BOOL | ValueInfo::M_DROP,
|
||||
'include_header' => ValueInfo::T_BOOL | ValueInfo::M_DROP,
|
||||
'search' => ValueInfo::T_STRING,
|
||||
'view_mode' => ValueInfo::T_STRING, // various filters for `getHeadlines`
|
||||
'since_id' => ValueInfo::T_INT, // cut-off article ID for `getHeadlines` and `getCompactHeadlines; returns only higher article IDs when specified
|
||||
'order_by' => ValueInfo::T_STRING, // sort order for `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` (not yet implemented)
|
||||
'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`
|
||||
'data' => ValueInfo::T_STRING, // note text in `updateArticle` if setting a note
|
||||
'pref_name' => ValueInfo::T_STRING, // preference identifier in `getPref`
|
||||
];
|
||||
// generic error construct
|
||||
const FATAL_ERR = [
|
||||
|
@ -1232,8 +1233,101 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler {
|
|||
$data = $this->normalizeInput($data, self::VALID_INPUT, "unix");
|
||||
// fetch the list of IDs
|
||||
$out = [];
|
||||
foreach ($this->fetchArticles($data, Database::LIST_MINIMAL) as $row) {
|
||||
$out[] = ['id' => $row['id']];
|
||||
try {
|
||||
foreach ($this->fetchArticles($data, Database::LIST_MINIMAL) as $row) {
|
||||
$out[] = ['id' => $row['id']];
|
||||
}
|
||||
} catch (ExceptionInput $e) {
|
||||
// ignore database errors (feeds/categories that don't exist)
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
|
||||
public function opGetHeadlines(array $data): array {
|
||||
// normalize input
|
||||
$data['limit'] = max(min(!$data['limit'] ? 200 : $data['limit'], 200), 0); // at most 200; not specified/zero yields 200; negative values yield no limit
|
||||
$tr = Arsse::$db->begin();
|
||||
// retrieve the list of label names for the user
|
||||
$labels = [];
|
||||
foreach (Arsse::$db->labelList(Arsse::$user->id, false) as $label) {
|
||||
$labels[$label['id']] = $label['name'];
|
||||
}
|
||||
// retrieve the requested articles
|
||||
$out = [];
|
||||
try {
|
||||
foreach ($this->fetchArticles($data, Database::LIST_FULL) as $article) {
|
||||
$row = [
|
||||
'id' => $article['id'],
|
||||
'guid' => $article['guid'] ? "SHA256:".$article['guid'] : null,
|
||||
'title' => $article['title'],
|
||||
'link' => $article['url'],
|
||||
'labels' => $this->articleLabelList($labels, $article['id']),
|
||||
'unread' => (bool) $article['unread'],
|
||||
'marked' => (bool) $article['starred'],
|
||||
'published' => false, // TODO: if the Published feed is implemented, the getHeadlines operation should be amended accordingly
|
||||
'author' => $article['author'],
|
||||
'updated' => Date::transform($article['edited_date'], "unix", "sql"),
|
||||
'is_updated' => ($article['published_date'] < $article['edited_date']),
|
||||
'feed_id' => $article['subscription'],
|
||||
'feed_title' => $article['subscription_title'],
|
||||
'score' => 0, // score is not implemented as it is not modifiable from the TTRSS API
|
||||
'note' => strlen($article['note']) ? $article['note'] : null,
|
||||
'lang' => "", // FIXME: picoFeed should be able to retrieve this information
|
||||
'tags' => Arsse::$db->articleCategoriesGet(Arsse::$user->id, $article['id']),
|
||||
'comments_count' => 0,
|
||||
'comments_link' => "",
|
||||
];
|
||||
if ($data['show_content']) {
|
||||
$row['content'] = $article['content'];
|
||||
}
|
||||
if ($data['show_excerpt']) {
|
||||
// prepare an excerpt from the content
|
||||
$text = strip_tags($article['content']); // get rid of all tags; elements with problematic content (e.g. script, style) should already be gone thanks to sanitization
|
||||
$text = html_entity_decode($text, \ENT_QUOTES | \ENT_HTML5, "UTF-8");
|
||||
$text = trim($text); // trim whitespace at ends
|
||||
$text = preg_replace("<\s+>s", " ", $text); // replace runs of whitespace with a single space
|
||||
$row['excerpt'] = grapheme_substr($text, 0, self::LIMIT_EXCERPT).(grapheme_strlen($text) > self::LIMIT_EXCERPT ? "…" : ""); // add an ellipsis if the string is longer than N characters
|
||||
}
|
||||
if ($data['include_attachments']) {
|
||||
$row['attachments'] = $article['media_url'] ? [[
|
||||
'content_url' => $article['media_url'],
|
||||
'content_type' => $article['media_type'],
|
||||
'title' => "",
|
||||
'duration' => "",
|
||||
'width' => "",
|
||||
'height' => "",
|
||||
'post_id' => $article['id'],
|
||||
]] : []; // TODO: We need to support multiple enclosures
|
||||
}
|
||||
$out[] = $row;
|
||||
}
|
||||
} catch (ExceptionInput $e) {
|
||||
// ignore database errors (feeds/categories that don't exist)
|
||||
// ensure that if using a header the database is not needlessly queried again
|
||||
$data['skip'] = null;
|
||||
}
|
||||
if ($data['include_header']) {
|
||||
if ($data['skip'] > 0 && $data['order_by'] != "date_reverse") {
|
||||
// when paginating the header returns the latest ("first") item ID in the full list; we get this ID here
|
||||
$data['skip'] = 0;
|
||||
$data['limit'] = 1;
|
||||
$firstID = ($this->fetchArticles($data, Database::LIST_MINIMAL)->getRow() ?? ['id' => 0])['id'];
|
||||
} elseif ($data['order_by']=="date_reverse") {
|
||||
// the "date_reverse" sort order doesn't get a first ID because it's meaningless for ascending-order pagination (pages doesn't go stale)
|
||||
$firstID = 0;
|
||||
} else {
|
||||
// otherwise just use the ID of the first item in the list we've already computed
|
||||
$firstID = ($out) ? $out[0]['id'] : 0;
|
||||
}
|
||||
// wrap the output with (but after) the header
|
||||
$out = [
|
||||
[
|
||||
'id' => $data['feed_id'],
|
||||
'is_cat' => $data['is_cat'] ?? false,
|
||||
'first_id' => $firstID,
|
||||
],
|
||||
$out,
|
||||
];
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
|
@ -1340,6 +1434,21 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler {
|
|||
throw new \JKingWeb\Arsse\Exception("constantUnknown", $viewMode); // @codeCoverageIgnore
|
||||
}
|
||||
// TODO: implement searching
|
||||
// handle sorting
|
||||
switch ($data['order_by']) {
|
||||
case "date_reverse":
|
||||
// sort oldest first
|
||||
$c->reverse(false);
|
||||
break;
|
||||
case "feed_dates":
|
||||
// sort newest first
|
||||
$c->reverse(true);
|
||||
break;
|
||||
default:
|
||||
// in TT-RSS the default sort order is unusual for some of the special feeds; we do not implement this
|
||||
$c->reverse(true);
|
||||
break;
|
||||
}
|
||||
// set the limit and offset
|
||||
if ($data['limit'] > 0) {
|
||||
$c->limit($data['limit']);
|
||||
|
@ -1352,11 +1461,6 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler {
|
|||
$c->oldestArticle($data['since_id'] + 1);
|
||||
}
|
||||
// return results
|
||||
try {
|
||||
return Arsse::$db->articleList(Arsse::$user->id, $c, $fields);
|
||||
} catch (ExceptionInput $e) {
|
||||
// if a category/feed does not exist
|
||||
return new ResultEmpty;
|
||||
}
|
||||
return Arsse::$db->articleList(Arsse::$user->id, $c, $fields);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -93,6 +93,29 @@ class TestTinyTinyAPI extends Test\AbstractTest {
|
|||
'note' => "Note 2",
|
||||
],
|
||||
];
|
||||
// text from https://corrigeur.fr/lorem-ipsum-traduction-origine.php
|
||||
protected $richContent = <<<LONG_STRING
|
||||
<section>
|
||||
<p>
|
||||
<b>Pour</b> vous faire mieux
|
||||
connaitre d’ou\u{300} vient
|
||||
l’erreur de ceux qui
|
||||
bla\u{302}ment la
|
||||
volupte\u{301}, et qui louent
|
||||
en quelque sorte la douleur,
|
||||
je vais entrer dans une
|
||||
explication plus
|
||||
e\u{301}tendue, et vous faire
|
||||
voir tout ce qui a
|
||||
e\u{301}te\u{301} dit
|
||||
la\u{300}-dessus par
|
||||
l’inventeur de la
|
||||
ve\u{301}rite\u{301}, et, pour
|
||||
ainsi dire, par l’architecte
|
||||
de la vie heureuse.
|
||||
</p>
|
||||
</section>
|
||||
LONG_STRING;
|
||||
|
||||
protected function respGood($content = null, $seq = 0): Response {
|
||||
return new Response(200, [
|
||||
|
@ -1301,7 +1324,7 @@ class TestTinyTinyAPI extends Test\AbstractTest {
|
|||
$this->assertEquals($this->respGood([$exp[0]]), $this->h->dispatch(new Request("POST", "", json_encode($in[5]))));
|
||||
}
|
||||
|
||||
public function testGetCompactHeadlines() {
|
||||
public function testRetrieveCompactHeadlines() {
|
||||
$in1 = [
|
||||
// erroneous input
|
||||
['op' => "getCompactHeadlines", 'sid' => "PriestsOfSyrinx"],
|
||||
|
@ -1336,18 +1359,19 @@ class TestTinyTinyAPI extends Test\AbstractTest {
|
|||
Phake::when(Arsse::$db)->articleList->thenReturn(new Result([['id' => 0]]));
|
||||
Phake::when(Arsse::$db)->articleCount->thenReturn(0);
|
||||
Phake::when(Arsse::$db)->articleCount($this->anything(), (new Context)->unread(true))->thenReturn(1);
|
||||
Phake::when(Arsse::$db)->articleList($this->anything(), (new Context)->subscription(2112), Database::LIST_MINIMAL)->thenThrow(new ExceptionInput("subjectMissing"));
|
||||
Phake::when(Arsse::$db)->articleList($this->anything(), new Context, Database::LIST_MINIMAL)->thenReturn(new Result($this->articles));
|
||||
Phake::when(Arsse::$db)->articleList($this->anything(), (new Context)->starred(true), Database::LIST_MINIMAL)->thenReturn(new Result([['id' => 1]]));
|
||||
Phake::when(Arsse::$db)->articleList($this->anything(), (new Context)->label(1088), Database::LIST_MINIMAL)->thenReturn(new Result([['id' => 2]]));
|
||||
Phake::when(Arsse::$db)->articleList($this->anything(), (new Context)->unread(true), Database::LIST_MINIMAL)->thenReturn(new Result([['id' => 3]]));
|
||||
Phake::when(Arsse::$db)->articleList($this->anything(), (new Context)->label(1088)->unread(true), Database::LIST_MINIMAL)->thenReturn(new Result([['id' => 4]]));
|
||||
Phake::when(Arsse::$db)->articleList($this->anything(), (new Context)->subscription(42)->starred(true), Database::LIST_MINIMAL)->thenReturn(new Result([['id' => 5]]));
|
||||
Phake::when(Arsse::$db)->articleList($this->anything(), (new Context)->subscription(42)->annotated(true), Database::LIST_MINIMAL)->thenReturn(new Result([['id' => 6]]));
|
||||
Phake::when(Arsse::$db)->articleList($this->anything(), (new Context)->limit(5), Database::LIST_MINIMAL)->thenReturn(new Result([['id' => 7]]));
|
||||
Phake::when(Arsse::$db)->articleList($this->anything(), (new Context)->offset(2), Database::LIST_MINIMAL)->thenReturn(new Result([['id' => 8]]));
|
||||
Phake::when(Arsse::$db)->articleList($this->anything(), (new Context)->limit(5)->offset(2), Database::LIST_MINIMAL)->thenReturn(new Result([['id' => 9]]));
|
||||
Phake::when(Arsse::$db)->articleList($this->anything(), (new Context)->oldestArticle(48), Database::LIST_MINIMAL)->thenReturn(new Result([['id' => 10]]));
|
||||
$c = (new Context)->reverse(true);
|
||||
Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->subscription(2112), Database::LIST_MINIMAL)->thenThrow(new ExceptionInput("subjectMissing"));
|
||||
Phake::when(Arsse::$db)->articleList($this->anything(), $c, Database::LIST_MINIMAL)->thenReturn(new Result($this->articles));
|
||||
Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->starred(true), Database::LIST_MINIMAL)->thenReturn(new Result([['id' => 1]]));
|
||||
Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->label(1088), Database::LIST_MINIMAL)->thenReturn(new Result([['id' => 2]]));
|
||||
Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->unread(true), Database::LIST_MINIMAL)->thenReturn(new Result([['id' => 3]]));
|
||||
Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->label(1088)->unread(true), Database::LIST_MINIMAL)->thenReturn(new Result([['id' => 4]]));
|
||||
Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->subscription(42)->starred(true), Database::LIST_MINIMAL)->thenReturn(new Result([['id' => 5]]));
|
||||
Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->subscription(42)->annotated(true), Database::LIST_MINIMAL)->thenReturn(new Result([['id' => 6]]));
|
||||
Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->limit(5), Database::LIST_MINIMAL)->thenReturn(new Result([['id' => 7]]));
|
||||
Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->offset(2), Database::LIST_MINIMAL)->thenReturn(new Result([['id' => 8]]));
|
||||
Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->limit(5)->offset(2), Database::LIST_MINIMAL)->thenReturn(new Result([['id' => 9]]));
|
||||
Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->oldestArticle(48), Database::LIST_MINIMAL)->thenReturn(new Result([['id' => 10]]));
|
||||
$out1 = [
|
||||
$this->respErr("INCORRECT_USAGE"),
|
||||
$this->respGood([]),
|
||||
|
@ -1379,10 +1403,333 @@ class TestTinyTinyAPI extends Test\AbstractTest {
|
|||
$this->assertEquals($out1[$a], $this->h->dispatch(new Request("POST", "", json_encode($in1[$a]))), "Test $a failed");
|
||||
}
|
||||
for ($a = 0; $a < sizeof($in2); $a++) {
|
||||
Phake::when(Arsse::$db)->articleList($this->anything(), (new Context)->unread(false)->markedSince(Date::sub("PT24H")), Database::LIST_MINIMAL)->thenReturn(new Result([['id' => 1001]]));
|
||||
Phake::when(Arsse::$db)->articleList($this->anything(), (new Context)->unread(true)->modifiedSince(Date::sub("PT24H")), Database::LIST_MINIMAL)->thenReturn(new Result([['id' => 1002]]));
|
||||
Phake::when(Arsse::$db)->articleList($this->anything(), (new Context)->unread(true)->modifiedSince(Date::sub("PT24H"))->starred(true), Database::LIST_MINIMAL)->thenReturn(new Result([['id' => 1003]]));
|
||||
Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->unread(false)->markedSince(Date::sub("PT24H")), Database::LIST_MINIMAL)->thenReturn(new Result([['id' => 1001]]));
|
||||
Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->unread(true)->modifiedSince(Date::sub("PT24H")), Database::LIST_MINIMAL)->thenReturn(new Result([['id' => 1002]]));
|
||||
Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->unread(true)->modifiedSince(Date::sub("PT24H"))->starred(true), Database::LIST_MINIMAL)->thenReturn(new Result([['id' => 1003]]));
|
||||
$this->assertEquals($out2[$a], $this->h->dispatch(new Request("POST", "", json_encode($in2[$a]))), "Test $a failed");
|
||||
}
|
||||
}
|
||||
|
||||
public function testRetrieveFullHeadlines() {
|
||||
$in1 = [
|
||||
// empty results
|
||||
['op' => "getHeadlines", 'sid' => "PriestsOfSyrinx", 'feed_id' => 0],
|
||||
['op' => "getHeadlines", 'sid' => "PriestsOfSyrinx", 'feed_id' => -2],
|
||||
['op' => "getHeadlines", 'sid' => "PriestsOfSyrinx", 'feed_id' => -1, 'is_cat' => true],
|
||||
['op' => "getHeadlines", 'sid' => "PriestsOfSyrinx", 'feed_id' => -4, 'view_mode' => "published"],
|
||||
['op' => "getHeadlines", 'sid' => "PriestsOfSyrinx", 'feed_id' => -6, 'view_mode' => "unread"],
|
||||
['op' => "getHeadlines", 'sid' => "PriestsOfSyrinx", 'feed_id' => 2112],
|
||||
];
|
||||
$in2 = [
|
||||
// simple context tests
|
||||
['op' => "getHeadlines", 'sid' => "PriestsOfSyrinx"],
|
||||
['op' => "getHeadlines", 'sid' => "PriestsOfSyrinx", 'feed_id' => -4],
|
||||
['op' => "getHeadlines", 'sid' => "PriestsOfSyrinx", 'feed_id' => -1],
|
||||
['op' => "getHeadlines", 'sid' => "PriestsOfSyrinx", 'feed_id' => -2112],
|
||||
['op' => "getHeadlines", 'sid' => "PriestsOfSyrinx", 'feed_id' => -4, 'view_mode' => "adaptive"],
|
||||
['op' => "getHeadlines", 'sid' => "PriestsOfSyrinx", 'feed_id' => -2112, 'view_mode' => "adaptive"],
|
||||
['op' => "getHeadlines", 'sid' => "PriestsOfSyrinx", 'feed_id' => -2112, 'view_mode' => "unread"],
|
||||
['op' => "getHeadlines", 'sid' => "PriestsOfSyrinx", 'feed_id' => 42, 'view_mode' => "marked"],
|
||||
['op' => "getHeadlines", 'sid' => "PriestsOfSyrinx", 'feed_id' => 42, 'view_mode' => "has_note"],
|
||||
['op' => "getHeadlines", 'sid' => "PriestsOfSyrinx", 'feed_id' => -4, 'limit' => 5],
|
||||
['op' => "getHeadlines", 'sid' => "PriestsOfSyrinx", 'feed_id' => -4, 'skip' => 2],
|
||||
['op' => "getHeadlines", 'sid' => "PriestsOfSyrinx", 'feed_id' => -4, 'limit' => 5, 'skip' => 2],
|
||||
['op' => "getHeadlines", 'sid' => "PriestsOfSyrinx", 'feed_id' => -4, 'since_id' => 47],
|
||||
['op' => "getHeadlines", 'sid' => "PriestsOfSyrinx", 'feed_id' => -3, 'is_cat' => true],
|
||||
['op' => "getHeadlines", 'sid' => "PriestsOfSyrinx", 'feed_id' => -4, 'is_cat' => true],
|
||||
['op' => "getHeadlines", 'sid' => "PriestsOfSyrinx", 'feed_id' => -2, 'is_cat' => true],
|
||||
['op' => "getHeadlines", 'sid' => "PriestsOfSyrinx", 'feed_id' => 0, 'is_cat' => true],
|
||||
['op' => "getHeadlines", 'sid' => "PriestsOfSyrinx", 'feed_id' => 42, 'is_cat' => true],
|
||||
['op' => "getHeadlines", 'sid' => "PriestsOfSyrinx", 'feed_id' => 42, 'is_cat' => true, 'include_nested' => true],
|
||||
['op' => "getHeadlines", 'sid' => "PriestsOfSyrinx", 'feed_id' => -4, 'order_by' => "feed_dates"],
|
||||
['op' => "getHeadlines", 'sid' => "PriestsOfSyrinx", 'feed_id' => -4, 'order_by' => "date_reverse"],
|
||||
];
|
||||
$in3 = [
|
||||
// time-based context tests
|
||||
['op' => "getHeadlines", 'sid' => "PriestsOfSyrinx", 'feed_id' => -6],
|
||||
['op' => "getHeadlines", 'sid' => "PriestsOfSyrinx", 'feed_id' => -6, 'view_mode' => "adaptive"],
|
||||
['op' => "getHeadlines", 'sid' => "PriestsOfSyrinx", 'feed_id' => -3],
|
||||
['op' => "getHeadlines", 'sid' => "PriestsOfSyrinx", 'feed_id' => -3, 'view_mode' => "marked"],
|
||||
];
|
||||
Phake::when(Arsse::$db)->labelList($this->anything())->thenReturn(new Result($this->labels));
|
||||
Phake::when(Arsse::$db)->labelList($this->anything(), false)->thenReturn(new Result($this->usedLabels));
|
||||
Phake::when(Arsse::$db)->articleLabelsGet->thenReturn([]);
|
||||
Phake::when(Arsse::$db)->articleLabelsGet($this->anything(), 2112)->thenReturn([1,3]);
|
||||
Phake::when(Arsse::$db)->articleCategoriesGet->thenReturn([]);
|
||||
Phake::when(Arsse::$db)->articleCategoriesGet($this->anything(), 2112)->thenReturn(["Boring","Illogical"]);
|
||||
Phake::when(Arsse::$db)->articleList->thenReturn($this->generateHeadlines(0));
|
||||
Phake::when(Arsse::$db)->articleCount->thenReturn(0);
|
||||
Phake::when(Arsse::$db)->articleCount($this->anything(), (new Context)->unread(true))->thenReturn(1);
|
||||
$c = (new Context)->limit(200)->reverse(true);
|
||||
Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->subscription(2112), Database::LIST_FULL)->thenThrow(new ExceptionInput("subjectMissing"));
|
||||
Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->starred(true), Database::LIST_FULL)->thenReturn($this->generateHeadlines(1));
|
||||
Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->label(1088), Database::LIST_FULL)->thenReturn($this->generateHeadlines(2));
|
||||
Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->unread(true), Database::LIST_FULL)->thenReturn($this->generateHeadlines(3));
|
||||
Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->label(1088)->unread(true), Database::LIST_FULL)->thenReturn($this->generateHeadlines(4));
|
||||
Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->subscription(42)->starred(true), Database::LIST_FULL)->thenReturn($this->generateHeadlines(5));
|
||||
Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->subscription(42)->annotated(true), Database::LIST_FULL)->thenReturn($this->generateHeadlines(6));
|
||||
Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->limit(5), Database::LIST_FULL)->thenReturn($this->generateHeadlines(7));
|
||||
Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->offset(2), Database::LIST_FULL)->thenReturn($this->generateHeadlines(8));
|
||||
Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->limit(5)->offset(2), Database::LIST_FULL)->thenReturn($this->generateHeadlines(9));
|
||||
Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->oldestArticle(48), Database::LIST_FULL)->thenReturn($this->generateHeadlines(10));
|
||||
Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c), Database::LIST_FULL)->thenReturn($this->generateHeadlines(11));
|
||||
Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->labelled(true), Database::LIST_FULL)->thenReturn($this->generateHeadlines(12));
|
||||
Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->folderShallow(0), Database::LIST_FULL)->thenReturn($this->generateHeadlines(13));
|
||||
Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->folderShallow(42), Database::LIST_FULL)->thenReturn($this->generateHeadlines(14));
|
||||
Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->folder(42), Database::LIST_FULL)->thenReturn($this->generateHeadlines(15));
|
||||
Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->reverse(false), Database::LIST_FULL)->thenReturn($this->generateHeadlines(16));
|
||||
$out2 = [
|
||||
$this->respErr("INCORRECT_USAGE"),
|
||||
$this->outputHeadlines(11),
|
||||
$this->outputHeadlines(1),
|
||||
$this->outputHeadlines(2),
|
||||
$this->outputHeadlines(3),
|
||||
$this->outputHeadlines(2), // the result is 2 rather than 4 because there are no unread, so the unread context is not used
|
||||
$this->outputHeadlines(4),
|
||||
$this->outputHeadlines(5),
|
||||
$this->outputHeadlines(6),
|
||||
$this->outputHeadlines(7),
|
||||
$this->outputHeadlines(8),
|
||||
$this->outputHeadlines(9),
|
||||
$this->outputHeadlines(10),
|
||||
$this->outputHeadlines(11),
|
||||
$this->outputHeadlines(11),
|
||||
$this->outputHeadlines(12),
|
||||
$this->outputHeadlines(13),
|
||||
$this->outputHeadlines(14),
|
||||
$this->outputHeadlines(15),
|
||||
$this->outputHeadlines(11), // defaulting sorting is not fully implemented
|
||||
$this->outputHeadlines(16),
|
||||
];
|
||||
$out3 = [
|
||||
$this->outputHeadlines(1001),
|
||||
$this->outputHeadlines(1001),
|
||||
$this->outputHeadlines(1002),
|
||||
$this->outputHeadlines(1003),
|
||||
];
|
||||
for ($a = 0; $a < sizeof($in1); $a++) {
|
||||
$this->assertResponse($this->respGood([]), $this->h->dispatch(new Request("POST", "", json_encode($in1[$a]))), "Test $a failed");
|
||||
}
|
||||
for ($a = 0; $a < sizeof($in2); $a++) {
|
||||
$this->assertEquals($out2[$a], $this->h->dispatch(new Request("POST", "", json_encode($in2[$a]))), "Test $a failed");
|
||||
}
|
||||
for ($a = 0; $a < sizeof($in3); $a++) {
|
||||
Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->unread(false)->markedSince(Date::sub("PT24H")), Database::LIST_FULL)->thenReturn($this->generateHeadlines(1001));
|
||||
Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->unread(true)->modifiedSince(Date::sub("PT24H")), Database::LIST_FULL)->thenReturn($this->generateHeadlines(1002));
|
||||
Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->unread(true)->modifiedSince(Date::sub("PT24H"))->starred(true), Database::LIST_FULL)->thenReturn($this->generateHeadlines(1003));
|
||||
$this->assertEquals($out3[$a], $this->h->dispatch(new Request("POST", "", json_encode($in3[$a]))), "Test $a failed");
|
||||
}
|
||||
}
|
||||
|
||||
public function testRetrieveFullHeadlinesCheckingExtraFields() {
|
||||
$in = [
|
||||
// empty results
|
||||
['op' => "getHeadlines", 'sid' => "PriestsOfSyrinx", 'feed_id' => -4],
|
||||
['op' => "getHeadlines", 'sid' => "PriestsOfSyrinx", 'feed_id' => -4, 'show_content' => true],
|
||||
['op' => "getHeadlines", 'sid' => "PriestsOfSyrinx", 'feed_id' => -4, 'include_attachments' => true],
|
||||
['op' => "getHeadlines", 'sid' => "PriestsOfSyrinx", 'feed_id' => -4, 'include_header' => true],
|
||||
['op' => "getHeadlines", 'sid' => "PriestsOfSyrinx", 'feed_id' => -3, 'is_cat' => true, 'include_header' => true],
|
||||
['op' => "getHeadlines", 'sid' => "PriestsOfSyrinx", 'feed_id' => -1, 'is_cat' => true, 'include_header' => true],
|
||||
['op' => "getHeadlines", 'sid' => "PriestsOfSyrinx", 'feed_id' => 2112, 'include_header' => true],
|
||||
['op' => "getHeadlines", 'sid' => "PriestsOfSyrinx", 'feed_id' => -4, 'include_header' => true, 'order_by' => "date_reverse"],
|
||||
['op' => "getHeadlines", 'sid' => "PriestsOfSyrinx", 'feed_id' => 42, 'skip' => 47, 'include_header' => true],
|
||||
['op' => "getHeadlines", 'sid' => "PriestsOfSyrinx", 'feed_id' => 42, 'skip' => 47, 'include_header' => true, 'order_by' => "date_reverse"],
|
||||
['op' => "getHeadlines", 'sid' => "PriestsOfSyrinx", 'feed_id' => -4, 'show_excerpt' => true],
|
||||
];
|
||||
Phake::when(Arsse::$db)->labelList($this->anything())->thenReturn(new Result($this->labels));
|
||||
Phake::when(Arsse::$db)->labelList($this->anything(), false)->thenReturn(new Result($this->usedLabels));
|
||||
Phake::when(Arsse::$db)->articleLabelsGet->thenReturn([]);
|
||||
Phake::when(Arsse::$db)->articleLabelsGet($this->anything(), 2112)->thenReturn([1,3]);
|
||||
Phake::when(Arsse::$db)->articleCategoriesGet->thenReturn([]);
|
||||
Phake::when(Arsse::$db)->articleCategoriesGet($this->anything(), 2112)->thenReturn(["Boring","Illogical"]);
|
||||
Phake::when(Arsse::$db)->articleList->thenReturn($this->generateHeadlines(1));
|
||||
Phake::when(Arsse::$db)->articleCount->thenReturn(0);
|
||||
Phake::when(Arsse::$db)->articleCount($this->anything(), (new Context)->unread(true))->thenReturn(1);
|
||||
// sanity check; this makes sure extra fields are not included in default situations
|
||||
$test = $this->h->dispatch(new Request("POST", "", json_encode($in[0])));
|
||||
$this->assertEquals($this->outputHeadlines(1), $test);
|
||||
// test 'show_content'
|
||||
$test = $this->h->dispatch(new Request("POST", "", json_encode($in[1])));
|
||||
$this->assertArrayHasKey("content", $test->payload['content'][0]);
|
||||
$this->assertArrayHasKey("content", $test->payload['content'][1]);
|
||||
foreach ($this->generateHeadlines(1) as $key => $row) {
|
||||
$this->assertSame($row['content'], $test->payload['content'][$key]['content']);
|
||||
}
|
||||
// test 'include_attachments'
|
||||
$test = $this->h->dispatch(new Request("POST", "", json_encode($in[2])));
|
||||
$exp = [
|
||||
[
|
||||
'content_url' => "http://example.com/text",
|
||||
'content_type' => "text/plain",
|
||||
'title' => "",
|
||||
'duration' => "",
|
||||
'width' => "",
|
||||
'height' => "",
|
||||
'post_id' => 2112,
|
||||
],
|
||||
];
|
||||
$this->assertArrayHasKey("attachments", $test->payload['content'][0]);
|
||||
$this->assertArrayHasKey("attachments", $test->payload['content'][1]);
|
||||
$this->assertSame([], $test->payload['content'][0]['attachments']);
|
||||
$this->assertSame($exp, $test->payload['content'][1]['attachments']);
|
||||
// test 'include_header'
|
||||
$test = $this->h->dispatch(new Request("POST", "", json_encode($in[3])));
|
||||
$exp = $this->outputHeadlines(1);
|
||||
$exp->payload['content'] = [
|
||||
['id' => -4, 'is_cat' => false, 'first_id' => 1],
|
||||
$exp->payload['content'],
|
||||
];
|
||||
$this->assertEquals($exp, $test);
|
||||
// test 'include_header' with a category
|
||||
$test = $this->h->dispatch(new Request("POST", "", json_encode($in[4])));
|
||||
$exp = $this->outputHeadlines(1);
|
||||
$exp->payload['content'] = [
|
||||
['id' => -3, 'is_cat' => true, 'first_id' => 1],
|
||||
$exp->payload['content'],
|
||||
];
|
||||
$this->assertEquals($exp, $test);
|
||||
// test 'include_header' with an empty result
|
||||
$test = $this->h->dispatch(new Request("POST", "", json_encode($in[5])));
|
||||
$exp = $this->respGood([
|
||||
['id' => -1, 'is_cat' => true, 'first_id' => 0],
|
||||
[],
|
||||
]);
|
||||
$this->assertEquals($exp, $test);
|
||||
// test 'include_header' with an erroneous result
|
||||
Phake::when(Arsse::$db)->articleList($this->anything(), (new Context)->limit(200)->reverse(true)->subscription(2112), $this->anything())->thenThrow(new ExceptionInput("subjectMissing"));
|
||||
$test = $this->h->dispatch(new Request("POST", "", json_encode($in[6])));
|
||||
$exp = $this->respGood([
|
||||
['id' => 2112, 'is_cat' => false, 'first_id' => 0],
|
||||
[],
|
||||
]);
|
||||
$this->assertEquals($exp, $test);
|
||||
// test 'include_header' with ascending order
|
||||
$test = $this->h->dispatch(new Request("POST", "", json_encode($in[7])));
|
||||
$exp = $this->outputHeadlines(1);
|
||||
$exp->payload['content'] = [
|
||||
['id' => -4, 'is_cat' => false, 'first_id' => 0],
|
||||
$exp->payload['content'],
|
||||
];
|
||||
$this->assertEquals($exp, $test);
|
||||
// test 'include_header' with skip
|
||||
Phake::when(Arsse::$db)->articleList($this->anything(), (new Context)->reverse(true)->limit(1)->subscription(42), Database::LIST_MINIMAL)->thenReturn($this->generateHeadlines(1867));
|
||||
$test = $this->h->dispatch(new Request("POST", "", json_encode($in[8])));
|
||||
$exp = $this->outputHeadlines(1);
|
||||
$exp->payload['content'] = [
|
||||
['id' => 42, 'is_cat' => false, 'first_id' => 1867],
|
||||
$exp->payload['content'],
|
||||
];
|
||||
$this->assertEquals($exp, $test);
|
||||
// test 'include_header' with skip and ascending order
|
||||
$test = $this->h->dispatch(new Request("POST", "", json_encode($in[9])));
|
||||
$exp = $this->outputHeadlines(1);
|
||||
$exp->payload['content'] = [
|
||||
['id' => 42, 'is_cat' => false, 'first_id' => 0],
|
||||
$exp->payload['content'],
|
||||
];
|
||||
$this->assertEquals($exp, $test);
|
||||
// test 'show_excerpt'
|
||||
$exp1 = "“This & that, you know‽”";
|
||||
$exp2 = "Pour vous faire mieux connaitre d’ou\u{300} vient l’erreur de ceux qui bla\u{302}ment la volupte\u{301}, et qui louent en…";
|
||||
$test = $this->h->dispatch(new Request("POST", "", json_encode($in[10])));
|
||||
$this->assertArrayHasKey("excerpt", $test->payload['content'][0]);
|
||||
$this->assertArrayHasKey("excerpt", $test->payload['content'][1]);
|
||||
$this->assertSame($exp1, $test->payload['content'][0]['excerpt']);
|
||||
$this->assertSame($exp2, $test->payload['content'][1]['excerpt']);
|
||||
}
|
||||
|
||||
protected function generateHeadlines(int $id): Result {
|
||||
return new Result([
|
||||
[
|
||||
'id' => $id,
|
||||
'url' => 'http://example.com/1',
|
||||
'title' => 'Article title 1',
|
||||
'subscription_title' => "Feed 2112",
|
||||
'author' => '',
|
||||
'content' => '<p>“This & that, you know‽”</p>',
|
||||
'guid' => '',
|
||||
'published_date' => '2000-01-01 00:00:00',
|
||||
'edited_date' => '2000-01-01 00:00:00',
|
||||
'modified_date' => '2000-01-01 01:00:00',
|
||||
'unread' => 0,
|
||||
'starred' => 0,
|
||||
'edition' => 101,
|
||||
'subscription' => 12,
|
||||
'fingerprint' => 'f5cb8bfc1c7396dc9816af212a3e2ac5221585c2a00bf7ccb6aabd95dcfcd6a6:fb0bc8f8cb08913dc5a497db700e327f1d34e4987402687d494a5891f24714d4:18fdd4fa93d693128c43b004399e5c9cea6c261ddfa002518d3669f55d8c2207',
|
||||
'media_url' => null,
|
||||
'media_type' => null,
|
||||
'note' => "",
|
||||
],
|
||||
[
|
||||
'id' => 2112,
|
||||
'url' => 'http://example.com/2',
|
||||
'title' => 'Article title 2',
|
||||
'subscription_title' => "Feed 11",
|
||||
'author' => 'J. King',
|
||||
'content' => $this->richContent,
|
||||
'guid' => '5be8a5a46ecd52ed132191c8d27fb1af6b3d4edc00234c5d9f8f0e10562ed3b7',
|
||||
'published_date' => '2000-01-02 00:00:00',
|
||||
'edited_date' => '2000-01-02 00:00:02',
|
||||
'modified_date' => '2000-01-02 02:00:00',
|
||||
'unread' => 1,
|
||||
'starred' => 1,
|
||||
'edition' => 202,
|
||||
'subscription' => 8,
|
||||
'fingerprint' => '0e86d2de822a174fe3c44a466953e63ca1f1a58a19cbf475fce0855d4e3d5153:13075894189c47ffcfafd1dfe7fbb539f7c74a69d35a399b3abf8518952714f9:2abd0a8cba83b8214a66c8f0293ba63e467d720540e29ff8ddcdab069d4f1c9e',
|
||||
'media_url' => "http://example.com/text",
|
||||
'media_type' => "text/plain",
|
||||
'note' => "Note 2",
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
protected function outputHeadlines(int $id): Response {
|
||||
return $this->respGood([
|
||||
[
|
||||
'id' => $id,
|
||||
'guid' => null,
|
||||
'title' => 'Article title 1',
|
||||
'link' => 'http://example.com/1',
|
||||
'labels' => [],
|
||||
'unread' => false,
|
||||
'marked' => false,
|
||||
'published' => false,
|
||||
'author' => '',
|
||||
'updated' => strtotime('2000-01-01 00:00:00'),
|
||||
'is_updated' => false,
|
||||
'feed_id' => 12,
|
||||
'feed_title' => "Feed 2112",
|
||||
'score' => 0,
|
||||
'note' => null,
|
||||
'lang' => "",
|
||||
'tags' => [],
|
||||
'comments_count' => 0,
|
||||
'comments_link' => "",
|
||||
],
|
||||
[
|
||||
'id' => 2112,
|
||||
'guid' => "SHA256:5be8a5a46ecd52ed132191c8d27fb1af6b3d4edc00234c5d9f8f0e10562ed3b7",
|
||||
'title' => 'Article title 2',
|
||||
'link' => 'http://example.com/2',
|
||||
'labels' => [
|
||||
[-1025, "Logical", "", ""],
|
||||
[-1027, "Fascinating", "", ""],
|
||||
],
|
||||
'unread' => true,
|
||||
'marked' => true,
|
||||
'published' => false,
|
||||
'author' => "J. King",
|
||||
'updated' => strtotime('2000-01-02 00:00:02'),
|
||||
'is_updated' => true,
|
||||
'feed_id' => 8,
|
||||
'feed_title' => "Feed 11",
|
||||
'score' => 0,
|
||||
'note' => "Note 2",
|
||||
'lang' => "",
|
||||
'tags' => ["Boring", "Illogical"],
|
||||
'comments_count' => 0,
|
||||
'comments_link' => "",
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -76,10 +76,14 @@
|
|||
<file>Db/SQLite3/Database/TestDatabaseCleanupSQLite3.php</file>
|
||||
</testsuite>
|
||||
<testsuite name="Controllers">
|
||||
<file>REST/NextCloudNews/TestNCNVersionDiscovery.php</file>
|
||||
<file>REST/NextCloudNews/TestNCNV1_2.php</file>
|
||||
<file>REST/TinyTinyRSS/TestTinyTinyAPI.php</file>
|
||||
<file>REST/TinyTinyRSS/TestTinyTinyIcon.php</file>
|
||||
<testsuite name="NCNv1">
|
||||
<file>REST/NextCloudNews/TestNCNVersionDiscovery.php</file>
|
||||
<file>REST/NextCloudNews/TestNCNV1_2.php</file>
|
||||
</testsuite>
|
||||
<testsuite name="TTRSS">
|
||||
<file>REST/TinyTinyRSS/TestTinyTinyAPI.php</file>
|
||||
<file>REST/TinyTinyRSS/TestTinyTinyIcon.php</file>
|
||||
</testsuite>
|
||||
</testsuite>
|
||||
<testsuite name="Refresh service">
|
||||
<file>Service/TestService.php</file>
|
||||
|
|
Loading…
Reference in a new issue