1
1
Fork 0
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:
J. King 2017-11-22 20:18:16 -05:00
parent faf00d63ba
commit c669273792
3 changed files with 495 additions and 40 deletions

View file

@ -41,14 +41,19 @@ Protocol difference so far:
- IDs for enclosures are ommitted as we don't give them IDs - IDs for enclosures are ommitted as we don't give them IDs
- Searching in getHeadlines is not yet implemented - 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) - 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 { class API extends \JKingWeb\Arsse\REST\AbstractHandler {
const LEVEL = 14; const LEVEL = 14; // emulated API level
const VERSION = "17.4"; const VERSION = "17.4"; // emulated TT-RSS version
const LABEL_OFFSET = 1024; 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 // special feeds
const FEED_ARCHIVED = 0; const FEED_ARCHIVED = 0;
const FEED_STARRED = -1; 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_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` '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` 'include_attachments' => ValueInfo::T_BOOL | ValueInfo::M_DROP, // whether to include article enclosures in `getHeadlines`
'view_mode' => ValueInfo::T_STRING, 'view_mode' => ValueInfo::T_STRING, // various filters for `getHeadlines`
'since_id' => ValueInfo::T_INT, '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, 'order_by' => ValueInfo::T_STRING, // sort order for `getHeadlines`
'sanitize' => ValueInfo::T_BOOL | ValueInfo::M_DROP, 'include_header' => ValueInfo::T_BOOL | ValueInfo::M_DROP, // whether to attach a header to the results of `getHeadlines`
'force_update' => ValueInfo::T_BOOL | ValueInfo::M_DROP, 'search' => ValueInfo::T_STRING, // search string for `getHeadlines` (not yet implemented)
'has_sandbox' => ValueInfo::T_BOOL | ValueInfo::M_DROP,
'include_header' => ValueInfo::T_BOOL | ValueInfo::M_DROP,
'search' => ValueInfo::T_STRING,
'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_INT, // whether to set, clear, or toggle the selected state in `updateArticle`
'data' => ValueInfo::T_STRING, // note text in `updateArticle` if setting a note 'data' => ValueInfo::T_STRING, // note text in `updateArticle` if setting a note
'pref_name' => ValueInfo::T_STRING, // preference identifier in `getPref`
]; ];
// generic error construct // generic error construct
const FATAL_ERR = [ const FATAL_ERR = [
@ -1232,9 +1233,102 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler {
$data = $this->normalizeInput($data, self::VALID_INPUT, "unix"); $data = $this->normalizeInput($data, self::VALID_INPUT, "unix");
// fetch the list of IDs // fetch the list of IDs
$out = []; $out = [];
try {
foreach ($this->fetchArticles($data, Database::LIST_MINIMAL) as $row) { foreach ($this->fetchArticles($data, Database::LIST_MINIMAL) as $row) {
$out[] = ['id' => $row['id']]; $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; return $out;
} }
@ -1340,6 +1434,21 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler {
throw new \JKingWeb\Arsse\Exception("constantUnknown", $viewMode); // @codeCoverageIgnore throw new \JKingWeb\Arsse\Exception("constantUnknown", $viewMode); // @codeCoverageIgnore
} }
// TODO: implement searching // 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 // set the limit and offset
if ($data['limit'] > 0) { if ($data['limit'] > 0) {
$c->limit($data['limit']); $c->limit($data['limit']);
@ -1352,11 +1461,6 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler {
$c->oldestArticle($data['since_id'] + 1); $c->oldestArticle($data['since_id'] + 1);
} }
// return results // return results
try {
return Arsse::$db->articleList(Arsse::$user->id, $c, $fields); return Arsse::$db->articleList(Arsse::$user->id, $c, $fields);
} catch (ExceptionInput $e) {
// if a category/feed does not exist
return new ResultEmpty;
}
} }
} }

View file

@ -93,6 +93,29 @@ class TestTinyTinyAPI extends Test\AbstractTest {
'note' => "Note 2", '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 dou\u{300} vient
lerreur 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
linventeur de la
ve\u{301}rite\u{301}, et, pour
ainsi dire, par larchitecte
de la vie heureuse.
</p>
</section>
LONG_STRING;
protected function respGood($content = null, $seq = 0): Response { protected function respGood($content = null, $seq = 0): Response {
return new Response(200, [ 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])))); $this->assertEquals($this->respGood([$exp[0]]), $this->h->dispatch(new Request("POST", "", json_encode($in[5]))));
} }
public function testGetCompactHeadlines() { public function testRetrieveCompactHeadlines() {
$in1 = [ $in1 = [
// erroneous input // erroneous input
['op' => "getCompactHeadlines", 'sid' => "PriestsOfSyrinx"], ['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)->articleList->thenReturn(new Result([['id' => 0]]));
Phake::when(Arsse::$db)->articleCount->thenReturn(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)->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")); $c = (new Context)->reverse(true);
Phake::when(Arsse::$db)->articleList($this->anything(), new Context, Database::LIST_MINIMAL)->thenReturn(new Result($this->articles)); 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(), (new Context)->starred(true), Database::LIST_MINIMAL)->thenReturn(new Result([['id' => 1]])); Phake::when(Arsse::$db)->articleList($this->anything(), $c, Database::LIST_MINIMAL)->thenReturn(new Result($this->articles));
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(), (clone $c)->starred(true), Database::LIST_MINIMAL)->thenReturn(new Result([['id' => 1]]));
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(), (clone $c)->label(1088), Database::LIST_MINIMAL)->thenReturn(new Result([['id' => 2]]));
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(), (clone $c)->unread(true), Database::LIST_MINIMAL)->thenReturn(new Result([['id' => 3]]));
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(), (clone $c)->label(1088)->unread(true), Database::LIST_MINIMAL)->thenReturn(new Result([['id' => 4]]));
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(), (clone $c)->subscription(42)->starred(true), Database::LIST_MINIMAL)->thenReturn(new Result([['id' => 5]]));
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(), (clone $c)->subscription(42)->annotated(true), Database::LIST_MINIMAL)->thenReturn(new Result([['id' => 6]]));
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(), (clone $c)->limit(5), Database::LIST_MINIMAL)->thenReturn(new Result([['id' => 7]]));
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(), (clone $c)->offset(2), Database::LIST_MINIMAL)->thenReturn(new Result([['id' => 8]]));
Phake::when(Arsse::$db)->articleList($this->anything(), (new Context)->oldestArticle(48), Database::LIST_MINIMAL)->thenReturn(new Result([['id' => 10]])); 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 = [ $out1 = [
$this->respErr("INCORRECT_USAGE"), $this->respErr("INCORRECT_USAGE"),
$this->respGood([]), $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"); $this->assertEquals($out1[$a], $this->h->dispatch(new Request("POST", "", json_encode($in1[$a]))), "Test $a failed");
} }
for ($a = 0; $a < sizeof($in2); $a++) { 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(), (clone $c)->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(), (clone $c)->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(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"); $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 dou\u{300} vient lerreur 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>&ldquo;This &amp; that, you know&#8253;&rdquo;</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' => "",
],
]);
}
} }

View file

@ -76,11 +76,15 @@
<file>Db/SQLite3/Database/TestDatabaseCleanupSQLite3.php</file> <file>Db/SQLite3/Database/TestDatabaseCleanupSQLite3.php</file>
</testsuite> </testsuite>
<testsuite name="Controllers"> <testsuite name="Controllers">
<testsuite name="NCNv1">
<file>REST/NextCloudNews/TestNCNVersionDiscovery.php</file> <file>REST/NextCloudNews/TestNCNVersionDiscovery.php</file>
<file>REST/NextCloudNews/TestNCNV1_2.php</file> <file>REST/NextCloudNews/TestNCNV1_2.php</file>
</testsuite>
<testsuite name="TTRSS">
<file>REST/TinyTinyRSS/TestTinyTinyAPI.php</file> <file>REST/TinyTinyRSS/TestTinyTinyAPI.php</file>
<file>REST/TinyTinyRSS/TestTinyTinyIcon.php</file> <file>REST/TinyTinyRSS/TestTinyTinyIcon.php</file>
</testsuite> </testsuite>
</testsuite>
<testsuite name="Refresh service"> <testsuite name="Refresh service">
<file>Service/TestService.php</file> <file>Service/TestService.php</file>
</testsuite> </testsuite>