1
1
Fork 0
mirror of https://code.mensbeam.com/MensBeam/Arsse.git synced 2024-12-22 21:22:40 +00:00

Implement TTTRSS getCounters operation; fixes #79

This commit is contained in:
J. King 2017-10-11 12:55:50 -04:00
parent c9c6891567
commit 20ff08a431
7 changed files with 243 additions and 93 deletions

View file

@ -305,7 +305,8 @@ class Database {
$q = new Query( $q = new Query(
"SELECT "SELECT
id,name,parent, id,name,parent,
(select count(*) from arsse_folders as parents where parents.parent is arsse_folders.id) as children (select count(*) from arsse_folders as parents where parents.parent is arsse_folders.id) as children,
(select count(*) from arsse_subscriptions where folder is arsse_folders.id) as feeds
FROM arsse_folders" FROM arsse_folders"
); );
if (!$recursive) { if (!$recursive) {
@ -508,6 +509,7 @@ class Database {
"SELECT "SELECT
arsse_subscriptions.id as id, arsse_subscriptions.id as id,
feed,url,favicon,source,folder,pinned,err_count,err_msg,order_type,added, feed,url,favicon,source,folder,pinned,err_count,err_msg,order_type,added,
arsse_feeds.updated as updated,
topmost.top as top_folder, topmost.top as top_folder,
coalesce(arsse_subscriptions.title, arsse_feeds.title) as title, coalesce(arsse_subscriptions.title, arsse_feeds.title) as title,
(SELECT count(*) from arsse_articles where feed is arsse_subscriptions.feed) - (SELECT count(*) from arsse_marks where subscription is arsse_subscriptions.id and read is 1) as unread (SELECT count(*) from arsse_articles where feed is arsse_subscriptions.feed) - (SELECT count(*) from arsse_marks where subscription is arsse_subscriptions.id and read is 1) as unread
@ -1002,6 +1004,21 @@ class Database {
return $this->db->prepare($q->getQuery(), $q->getTypes())->run($q->getValues())->getValue(); return $this->db->prepare($q->getQuery(), $q->getTypes())->run($q->getValues())->getValue();
} }
public function articleStarred(string $user): array {
if (!Arsse::$user->authorize($user, __FUNCTION__)) {
throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
}
return $this->db->prepare(
"SELECT
count(*) as total,
coalesce(sum(not read),0) as unread,
coalesce(sum(read),0) as read
FROM (
select read from arsse_marks where starred is 1 and subscription in (select id from arsse_subscriptions where owner is ?)
)", "str"
)->run($user)->getRow();
}
public function articleCleanup(): bool { public function articleCleanup(): bool {
$query = $this->db->prepare( $query = $this->db->prepare(
"WITH target_feed(id,subs) as (". "WITH target_feed(id,subs) as (".

View file

@ -7,7 +7,6 @@ use JKingWeb\Arsse\Misc\Date;
abstract class AbstractStatement implements Statement { abstract class AbstractStatement implements Statement {
protected $types = []; protected $types = [];
protected $isNullable = []; protected $isNullable = [];
protected $values = ['pre' => [], 'post' => []];
abstract public function runArray(array $values = []): Result; abstract public function runArray(array $values = []): Result;

View file

@ -395,7 +395,7 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler {
$out[] = $this->feedTranslate($sub); $out[] = $this->feedTranslate($sub);
} }
$out = ['feeds' => $out]; $out = ['feeds' => $out];
$out['starredCount'] = Arsse::$db->articleCount(Arsse::$user->id, (new Context)->starred(true)); $out['starredCount'] = Arsse::$db->articleStarred(Arsse::$user->id)['total'];
$newest = Arsse::$db->editionLatest(Arsse::$user->id); $newest = Arsse::$db->editionLatest(Arsse::$user->id);
if ($newest) { if ($newest) {
$out['newestItemId'] = $newest; $out['newestItemId'] = $newest;

View file

@ -17,13 +17,13 @@ use JKingWeb\Arsse\REST\Response;
/* /*
Protocol difference so far: Protocol difference so far:
- handling of incorrect Content-Type and/or HTTP method is different - Handling of incorrect Content-Type and/or HTTP method is different
- TT-RSS accepts whitespace-only names; we do not - TT-RSS accepts whitespace-only names; we do not
- TT-RSS allows two folders to share the same name under the same parent; we do not - TT-RSS allows two folders to share the same name under the same parent; we do not
- Session lifetime is much shorter by default (does TT-RSS even expire sessions?) - Session lifetime is much shorter by default (does TT-RSS even expire sessions?)
- Categories and feeds will always be sorted alphabetically (the protocol does not allow for clients to re-order) - Categories and feeds will always be sorted alphabetically (the protocol does not allow for clients to re-order)
- Label IDs decrease from -11 instead of from -1025 - The "Archived" virtual feed is non-functional (the protocol does not allow archiving)
- The "Published" virtual feed is non-functional (this will not be implemented in the near term)
*/ */
@ -31,14 +31,12 @@ Protocol difference so far:
class API extends \JKingWeb\Arsse\REST\AbstractHandler { class API extends \JKingWeb\Arsse\REST\AbstractHandler {
const LEVEL = 14; const LEVEL = 14;
const VERSION = "17.4"; const VERSION = "17.4";
const LABEL_OFFSET = 1024;
const FATAL_ERR = [ const FATAL_ERR = [
'seq' => null, 'seq' => null,
'status' => 1, 'status' => 1,
'content' => ['error' => "NOT_LOGGED_IN"], 'content' => ['error' => "NOT_LOGGED_IN"],
]; ];
const OVERRIDE = [
'auth' => ["login"],
];
public function __construct() { public function __construct() {
} }
@ -65,8 +63,8 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler {
'sid' => null, 'sid' => null,
], $data); ], $data);
try { try {
if (!in_array($data['op'], self::OVERRIDE['auth'])) { if (strtolower((string) $data['op']) != "login") {
// unless otherwise specified, a session identifier is required // unless logging in, a session identifier is required
$this->resumeSession($data['sid']); $this->resumeSession($data['sid']);
} }
$method = "op".ucfirst($data['op']); $method = "op".ucfirst($data['op']);
@ -148,19 +146,109 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler {
return ['status' => true]; return ['status' => true];
} }
public function opGetConfig(array $data): array {
return [
'icons_dir' => "feed-icons",
'icons_url' => "feed-icons",
'daemon_is_running' => Service::hasCheckedIn(),
'num_feeds' => Arsse::$db->subscriptionCount(Arsse::$user->id),
];
}
public function opGetUnread(array $data): array {
// simply sum the unread count of each subscription
$out = 0;
foreach (Arsse::$db->subscriptionList(Arsse::$user->id) as $sub) {
$out += $sub['unread'];
}
return ['unread' => $out];
}
public function opGetCounters(array $data): array {
$user = Arsse::$user->id;
$starred = Arsse::$db->articleStarred($user);
$fresh = Arsse::$db->articleCount($user, (new Context)->unread(true)->modifiedSince(Date::sub("PT24H")));
$countAll = 0;
$countSubs = 0;
$feeds = [];
$labels = [];
// do a first pass on categories: add the ID to a lookup table and set the unread counter to zero
$categories = Arsse::$db->folderList($user)->getAll();
$catmap = [];
for ($a = 0; $a < sizeof($categories); $a++) {
$catmap[(int) $categories[$a]['id']] = $a;
$categories[$a]['counter'] = 0;
}
// add the "Uncategorized" and "Labels" virtual categories to the list
$catmap[0] = sizeof($categories);
$categories[] = ['id' => 0, 'name' => Arsse::$lang->msg("API.TTRSS.Category.Uncategorized"), 'parent' => 0, 'children' => 0, 'counter' => 0];
$catmap[-2] = sizeof($categories);
$categories[] = ['id' => -2, 'name' => Arsse::$lang->msg("API.TTRSS.Category.Labels"), 'parent' => 0, 'children' => 0, 'counter' => 0];
// prepare data for each subscription; we also add unread counts for their host categories
foreach (Arsse::$db->subscriptionList($user) as $f) {
if ($f['unread']) {
// add the feed to the list of feeds
$feeds[] = ['id' => $f['id'], 'updated' => Date::transform($f['updated'], "iso8601", "sql"),'counter' => $f['unread'], 'has_img' => (int) (strlen((string) $f['favicon']) > 0)];
// add the feed's unread count to the global unread count
$countAll += $f['unread'];
// add the feed's unread count to its category unread count
$categories[$catmap[(int) $f['folder']]]['counter'] += $f['unread'];
}
// increment the global feed count
$countSubs += 1;
}
// prepare data for each non-empty label
foreach (Arsse::$db->labelList($user, false) as $l) {
$unread = $l['articles'] - $l['read'];
$labels[] = ['id' => $this->labelOut($l['id']), 'counter' => $unread, 'auxcounter' => $l['articles']];
$categories[$catmap[-2]]['counter'] += $unread;
}
// do a second pass on categories, summing descendant unread counts for ancestors, pruning categories with no unread, and building a final category list
$cats = [];
while ($categories) {
foreach ($categories as $c) {
if ($c['children']) {
// only act on leaf nodes
continue;
}
if ($c['parent']) {
// if the category has a parent, add its counter to the parent's counter, and decrement the parent's child count
$categories[$catmap[$c['parent']]]['counter'] += $c['counter'];
$categories[$catmap[$c['parent']]]['children'] -= 1;
}
if ($c['counter']) {
// if the category's counter is non-zero, add the category to the output list
$cats[] = ['id' => $c['id'], 'kind' => "cat", 'counter' => $c['counter']];
}
// remove the category from the input list
unset($categories[$catmap[$c['id']]]);
}
}
// prepare data for the virtual feeds and other counters
$special = [
['id' => "global-unread", 'counter' => $countAll], //this should not count archived articles, but we do not have an archive
['id' => "subscribed-feeds", 'counter' => $countSubs],
['id' => 0, 'counter' => 0, 'auxcounter' => 0], // Archived articles
['id' => -1, 'counter' => $starred['unread'], 'auxcounter' => $starred['total']], // Starred articles
['id' => -2, 'counter' => 0, 'auxcounter' => 0], // Published articles
['id' => -3, 'counter' => $fresh, 'auxcounter' => 0], // Fresh articles
['id' => -4, 'counter' => $countAll, 'auxcounter' => 0], // All articles
];
return array_merge($special, $labels, $feeds, $cats);
}
public function opGetCategories(array $data): array { public function opGetCategories(array $data): array {
// normalize input // normalize input
$all = isset($data['include_empty']) ? ValueInfo::bool($data['include_empty'], false) : false; $all = isset($data['include_empty']) ? ValueInfo::bool($data['include_empty'], false) : false;
$read = !(isset($data['unread_only']) ? ValueInfo::bool($data['unread_only'], false) : false); $read = !(isset($data['unread_only']) ? ValueInfo::bool($data['unread_only'], false) : false);
$deep = !(isset($data['enable_nested']) ? ValueInfo::bool($data['enable_nested'], false) : false); $deep = !(isset($data['enable_nested']) ? ValueInfo::bool($data['enable_nested'], false) : false);
$user = Arsse::$user->id; $user = Arsse::$user->id;
// for each category, add the ID to a lookup table, set the number of unread and feeds to zero, and assign an increasing order index // for each category, add the ID to a lookup table, set the number of unread to zero, and assign an increasing order index
$cats = Arsse::$db->folderList($user, null, $deep)->getAll(); $cats = Arsse::$db->folderList($user, null, $deep)->getAll();
$map = []; $map = [];
for ($a = 0; $a < sizeof($cats); $a++) { for ($a = 0; $a < sizeof($cats); $a++) {
$map[$cats[$a]['id']] = $a; $map[$cats[$a]['id']] = $a;
$cats[$a]['unread'] = 0; $cats[$a]['unread'] = 0;
$cats[$a]['feeds'] = 0;
$cats[$a]['order'] = $a + 1; $cats[$a]['order'] = $a + 1;
} }
// add the "Uncategorized", "Special", and "Labels" virtual categories to the list // add the "Uncategorized", "Special", and "Labels" virtual categories to the list
@ -176,7 +264,9 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler {
// note we use top_folder if we're in "nested" mode // note we use top_folder if we're in "nested" mode
$f = $map[(int) ($deep ? $sub['folder'] : $sub['top_folder'])]; $f = $map[(int) ($deep ? $sub['folder'] : $sub['top_folder'])];
$cats[$f]['unread'] += $sub['unread']; $cats[$f]['unread'] += $sub['unread'];
$cats[$f]['feeds'] += 1; if (!$cats[$f]['id']) {
$cats[$f]['feeds'] += 1;
}
} }
// for each label, add the unread count to the labels category, and increment the labels category's feed count // for each label, add the unread count to the labels category, and increment the labels category's feed count
$labels = Arsse::$db->labelList($user); $labels = Arsse::$db->labelList($user);
@ -188,7 +278,7 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler {
// get the unread counts for the special feeds // get the unread counts for the special feeds
// FIXME: this is pretty inefficient // FIXME: this is pretty inefficient
$f = $map[-1]; $f = $map[-1];
$cats[$f]['unread'] += Arsse::$db->articleCount($user, (new Context)->unread(true)->starred(true)); // starred $cats[$f]['unread'] += Arsse::$db->articleStarred($user)['unread']; // starred
$cats[$f]['unread'] += Arsse::$db->articleCount($user, (new Context)->unread(true)->modifiedSince(Date::sub("PT24H"))); // fresh $cats[$f]['unread'] += Arsse::$db->articleCount($user, (new Context)->unread(true)->modifiedSince(Date::sub("PT24H"))); // fresh
if (!$read) { if (!$read) {
// if we're only including unread entries, remove any categories with zero unread items (this will by definition also exclude empties) // if we're only including unread entries, remove any categories with zero unread items (this will by definition also exclude empties)
@ -439,24 +529,6 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler {
return null; return null;
} }
public function opGetUnread(array $data): array {
// simply sum the unread count of each subscription
$out = 0;
foreach (Arsse::$db->subscriptionList(Arsse::$user->id) as $sub) {
$out += $sub['unread'];
}
return ['unread' => $out];
}
public function opGetConfig(array $data): array {
return [
'icons_dir' => "feed-icons",
'icons_url' => "feed-icons",
'daemon_is_running' => Service::hasCheckedIn(),
'num_feeds' => Arsse::$db->subscriptionCount(Arsse::$user->id),
];
}
public function opUpdateFeed(array $data): array { public function opUpdateFeed(array $data): array {
if (!isset($data['feed_id']) || !ValueInfo::id($data['feed_id'])) { if (!isset($data['feed_id']) || !ValueInfo::id($data['feed_id'])) {
// if the feed is invalid, throw an error // if the feed is invalid, throw an error
@ -471,14 +543,14 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler {
} }
protected function labelIn($id): int { protected function labelIn($id): int {
if (!(ValueInfo::int($id) & ValueInfo::NEG) || $id > -11) { if (!(ValueInfo::int($id) & ValueInfo::NEG) || $id > (-1 - self::LABEL_OFFSET)) {
throw new Exception("INCORRECT_USAGE"); throw new Exception("INCORRECT_USAGE");
} }
return (abs($id) - 10); return (abs($id) - self::LABEL_OFFSET);
} }
protected function labelOut(int $id): int { protected function labelOut(int $id): int {
return ($id * -1 - 10); return ($id * -1 - self::LABEL_OFFSET);
} }
public function opAddLabel(array $data) { public function opAddLabel(array $data) {

View file

@ -475,7 +475,7 @@ class TestNCNV1_2 extends Test\AbstractTest {
'newestItemId' => 4758915, 'newestItemId' => 4758915,
]; ];
Phake::when(Arsse::$db)->subscriptionList(Arsse::$user->id)->thenReturn(new Result([]))->thenReturn(new Result($this->feeds['db'])); Phake::when(Arsse::$db)->subscriptionList(Arsse::$user->id)->thenReturn(new Result([]))->thenReturn(new Result($this->feeds['db']));
Phake::when(Arsse::$db)->articleCount(Arsse::$user->id, (new Context)->starred(true))->thenReturn(0)->thenReturn(5); Phake::when(Arsse::$db)->articleStarred(Arsse::$user->id)->thenReturn(['total' => 0])->thenReturn(['total' => 5]);
Phake::when(Arsse::$db)->editionLatest(Arsse::$user->id)->thenReturn(0)->thenReturn(4758915); Phake::when(Arsse::$db)->editionLatest(Arsse::$user->id)->thenReturn(0)->thenReturn(4758915);
$exp = new Response(200, $exp1); $exp = new Response(200, $exp1);
$this->assertEquals($exp, $this->h->dispatch(new Request("GET", "/feeds"))); $this->assertEquals($exp, $this->h->dispatch(new Request("GET", "/feeds")));

View file

@ -9,12 +9,43 @@ use JKingWeb\Arsse\Misc\Date;
use JKingWeb\Arsse\Misc\Context; use JKingWeb\Arsse\Misc\Context;
use JKingWeb\Arsse\Db\ExceptionInput; use JKingWeb\Arsse\Db\ExceptionInput;
use JKingWeb\Arsse\Db\Transaction; use JKingWeb\Arsse\Db\Transaction;
use JKingWeb\Arsse\REST\TinyTinyRSS\API;
use Phake; use Phake;
/** @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 TestTinyTinyAPI extends Test\AbstractTest { class TestTinyTinyAPI extends Test\AbstractTest {
protected $h; protected $h;
protected $folders = [
['id' => 5, 'parent' => 3, 'children' => 0, 'feeds' => 1, 'name' => "Local"],
['id' => 6, 'parent' => 3, 'children' => 0, 'feeds' => 2, 'name' => "National"],
['id' => 4, 'parent' => null, 'children' => 0, 'feeds' => 0, 'name' => "Photography"],
['id' => 3, 'parent' => null, 'children' => 2, 'feeds' => 0, 'name' => "Politics"],
['id' => 2, 'parent' => 1, 'children' => 0, 'feeds' => 1, 'name' => "Rocketry"],
['id' => 1, 'parent' => null, 'children' => 1, 'feeds' => 1, 'name' => "Science"],
];
protected $topFolders = [
['id' => 4, 'parent' => null, 'children' => 0, 'feeds' => 0, 'name' => "Photography"],
['id' => 3, 'parent' => null, 'children' => 2, 'feeds' => 0, 'name' => "Politics"],
['id' => 1, 'parent' => null, 'children' => 1, 'feeds' => 1, 'name' => "Science"],
];
protected $subscriptions = [
['id' => 6, 'folder' => null, 'top_folder' => null, 'unread' => 0, 'updated' => "2010-02-12 20:08:47", 'favicon' => 'http://example.com/6.png'],
['id' => 3, 'folder' => 1, 'top_folder' => 1, 'unread' => 2, 'updated' => "2016-05-23 06:40:02", 'favicon' => 'http://example.com/3.png'],
['id' => 1, 'folder' => 2, 'top_folder' => 1, 'unread' => 5, 'updated' => "2017-09-15 22:54:16", 'favicon' => null],
['id' => 2, 'folder' => 5, 'top_folder' => 3, 'unread' => 10, 'updated' => "2011-11-11 11:11:11", 'favicon' => 'http://example.com/2.png'],
['id' => 5, 'folder' => 6, 'top_folder' => 3, 'unread' => 12, 'updated' => "2017-07-07 17:07:17", 'favicon' => ''],
['id' => 4, 'folder' => 6, 'top_folder' => 3, 'unread' => 6, 'updated' => "2017-10-09 15:58:34", 'favicon' => 'http://example.com/4.png'],
];
protected $labels = [
['id' => 5, 'articles' => 0, 'read' => 0],
['id' => 3, 'articles' => 100, 'read' => 94],
['id' => 1, 'articles' => 2, 'read' => 0],
];
protected $usedLabels = [
['id' => 3, 'articles' => 100, 'read' => 94],
['id' => 1, 'articles' => 2, 'read' => 0],
];
protected function respGood($content = null, $seq = 0): Response { protected function respGood($content = null, $seq = 0): Response {
return new Response(200, [ return new Response(200, [
@ -33,6 +64,19 @@ class TestTinyTinyAPI extends Test\AbstractTest {
]); ]);
} }
protected function assertResponse(Response $exp, Response $act, string $text = null) {
if ($exp->payload['status']) {
// if the expectation is an error response, do a straight object comparison
$this->assertEquals($exp, $act, $text);
} else {
// otherwise just compare their content
foreach ($act->payload['content'] as $record) {
$this->assertContains($record, $exp->payload['content'], $text);
}
$this->assertCount(sizeof($exp->payload['content']), $act->payload['content'], $text);
}
}
public function setUp() { public function setUp() {
$this->clearData(); $this->clearData();
Arsse::$conf = new Conf(); Arsse::$conf = new Conf();
@ -529,14 +573,14 @@ class TestTinyTinyAPI extends Test\AbstractTest {
Phake::when(Arsse::$db)->labelAdd(Arsse::$user->id, ['name' => ""])->thenThrow(new ExceptionInput("missing")); Phake::when(Arsse::$db)->labelAdd(Arsse::$user->id, ['name' => ""])->thenThrow(new ExceptionInput("missing"));
Phake::when(Arsse::$db)->labelAdd(Arsse::$user->id, ['name' => " "])->thenThrow(new ExceptionInput("whitespace")); Phake::when(Arsse::$db)->labelAdd(Arsse::$user->id, ['name' => " "])->thenThrow(new ExceptionInput("whitespace"));
// correctly add two labels // correctly add two labels
$exp = $this->respGood(-12); $exp = $this->respGood((-1 * API::LABEL_OFFSET) - 2);
$this->assertEquals($exp, $this->h->dispatch(new Request("POST", "", json_encode($in[0])))); $this->assertEquals($exp, $this->h->dispatch(new Request("POST", "", json_encode($in[0]))));
$exp = $this->respGood(-13); $exp = $this->respGood((-1 * API::LABEL_OFFSET) - 3);
$this->assertEquals($exp, $this->h->dispatch(new Request("POST", "", json_encode($in[1])))); $this->assertEquals($exp, $this->h->dispatch(new Request("POST", "", json_encode($in[1]))));
// attempt to add the two labels again // attempt to add the two labels again
$exp = $this->respGood(-12); $exp = $this->respGood((-1 * API::LABEL_OFFSET) - 2);
$this->assertEquals($exp, $this->h->dispatch(new Request("POST", "", json_encode($in[0])))); $this->assertEquals($exp, $this->h->dispatch(new Request("POST", "", json_encode($in[0]))));
$exp = $this->respGood(-13); $exp = $this->respGood((-1 * API::LABEL_OFFSET) - 3);
$this->assertEquals($exp, $this->h->dispatch(new Request("POST", "", json_encode($in[1])))); $this->assertEquals($exp, $this->h->dispatch(new Request("POST", "", json_encode($in[1]))));
Phake::verify(Arsse::$db)->labelPropertiesGet(Arsse::$user->id, "Software", true); Phake::verify(Arsse::$db)->labelPropertiesGet(Arsse::$user->id, "Software", true);
Phake::verify(Arsse::$db)->labelPropertiesGet(Arsse::$user->id, "Hardware", true); Phake::verify(Arsse::$db)->labelPropertiesGet(Arsse::$user->id, "Hardware", true);
@ -549,14 +593,14 @@ class TestTinyTinyAPI extends Test\AbstractTest {
public function testRemoveALabel() { public function testRemoveALabel() {
$in = [ $in = [
['op' => "removeLabel", 'sid' => "PriestsOfSyrinx", 'label_id' => -42], ['op' => "removeLabel", 'sid' => "PriestsOfSyrinx", 'label_id' => -1042],
['op' => "removeLabel", 'sid' => "PriestsOfSyrinx", 'label_id' => -2112], ['op' => "removeLabel", 'sid' => "PriestsOfSyrinx", 'label_id' => -2112],
['op' => "removeLabel", 'sid' => "PriestsOfSyrinx", 'label_id' => 1], ['op' => "removeLabel", 'sid' => "PriestsOfSyrinx", 'label_id' => 1],
['op' => "removeLabel", 'sid' => "PriestsOfSyrinx", 'label_id' => 0], ['op' => "removeLabel", 'sid' => "PriestsOfSyrinx", 'label_id' => 0],
['op' => "removeLabel", 'sid' => "PriestsOfSyrinx", 'label_id' => -10], ['op' => "removeLabel", 'sid' => "PriestsOfSyrinx", 'label_id' => -10],
]; ];
Phake::when(Arsse::$db)->labelRemove(Arsse::$user->id, $this->anything())->thenThrow(new ExceptionInput("subjectMissing")); Phake::when(Arsse::$db)->labelRemove(Arsse::$user->id, $this->anything())->thenThrow(new ExceptionInput("subjectMissing"));
Phake::when(Arsse::$db)->labelRemove(Arsse::$user->id, 32)->thenReturn(true)->thenThrow(new ExceptionInput("subjectMissing")); Phake::when(Arsse::$db)->labelRemove(Arsse::$user->id, 18)->thenReturn(true)->thenThrow(new ExceptionInput("subjectMissing"));
// succefully delete a label // succefully delete a label
$exp = $this->respGood(); $exp = $this->respGood();
$this->assertEquals($exp, $this->h->dispatch(new Request("POST", "", json_encode($in[0])))); $this->assertEquals($exp, $this->h->dispatch(new Request("POST", "", json_encode($in[0]))));
@ -571,29 +615,29 @@ class TestTinyTinyAPI extends Test\AbstractTest {
$this->assertEquals($exp, $this->h->dispatch(new Request("POST", "", json_encode($in[2])))); $this->assertEquals($exp, $this->h->dispatch(new Request("POST", "", json_encode($in[2]))));
$this->assertEquals($exp, $this->h->dispatch(new Request("POST", "", json_encode($in[3])))); $this->assertEquals($exp, $this->h->dispatch(new Request("POST", "", json_encode($in[3]))));
$this->assertEquals($exp, $this->h->dispatch(new Request("POST", "", json_encode($in[4])))); $this->assertEquals($exp, $this->h->dispatch(new Request("POST", "", json_encode($in[4]))));
Phake::verify(Arsse::$db, Phake::times(2))->labelRemove(Arsse::$user->id, 32); Phake::verify(Arsse::$db, Phake::times(2))->labelRemove(Arsse::$user->id, 18);
Phake::verify(Arsse::$db)->labelRemove(Arsse::$user->id, 2102); Phake::verify(Arsse::$db)->labelRemove(Arsse::$user->id, 1088);
} }
public function testRenameALabel() { public function testRenameALabel() {
$in = [ $in = [
['op' => "renameLabel", 'sid' => "PriestsOfSyrinx", 'label_id' => -42, 'caption' => "Ook"], ['op' => "renameLabel", 'sid' => "PriestsOfSyrinx", 'label_id' => -1042, 'caption' => "Ook"],
['op' => "renameLabel", 'sid' => "PriestsOfSyrinx", 'label_id' => -2112, 'caption' => "Eek"], ['op' => "renameLabel", 'sid' => "PriestsOfSyrinx", 'label_id' => -2112, 'caption' => "Eek"],
['op' => "renameLabel", 'sid' => "PriestsOfSyrinx", 'label_id' => -42, 'caption' => "Eek"], ['op' => "renameLabel", 'sid' => "PriestsOfSyrinx", 'label_id' => -1042, 'caption' => "Eek"],
['op' => "renameLabel", 'sid' => "PriestsOfSyrinx", 'label_id' => -42, 'caption' => ""], ['op' => "renameLabel", 'sid' => "PriestsOfSyrinx", 'label_id' => -1042, 'caption' => ""],
['op' => "renameLabel", 'sid' => "PriestsOfSyrinx", 'label_id' => -42, 'caption' => " "], ['op' => "renameLabel", 'sid' => "PriestsOfSyrinx", 'label_id' => -1042, 'caption' => " "],
['op' => "renameLabel", 'sid' => "PriestsOfSyrinx", 'label_id' => -42], ['op' => "renameLabel", 'sid' => "PriestsOfSyrinx", 'label_id' => -1042],
['op' => "renameLabel", 'sid' => "PriestsOfSyrinx", 'label_id' => -1, 'caption' => "Ook"], ['op' => "renameLabel", 'sid' => "PriestsOfSyrinx", 'label_id' => -1, 'caption' => "Ook"],
['op' => "renameLabel", 'sid' => "PriestsOfSyrinx", 'caption' => "Ook"], ['op' => "renameLabel", 'sid' => "PriestsOfSyrinx", 'caption' => "Ook"],
['op' => "renameLabel", 'sid' => "PriestsOfSyrinx"], ['op' => "renameLabel", 'sid' => "PriestsOfSyrinx"],
]; ];
$db = [ $db = [
[Arsse::$user->id, 32, ['name' => "Ook"]], [Arsse::$user->id, 18, ['name' => "Ook"]],
[Arsse::$user->id, 2102, ['name' => "Eek"]], [Arsse::$user->id, 1088, ['name' => "Eek"]],
[Arsse::$user->id, 32, ['name' => "Eek"]], [Arsse::$user->id, 18, ['name' => "Eek"]],
[Arsse::$user->id, 32, ['name' => ""]], [Arsse::$user->id, 18, ['name' => ""]],
[Arsse::$user->id, 32, ['name' => " "]], [Arsse::$user->id, 18, ['name' => " "]],
[Arsse::$user->id, 32, ['name' => ""]], [Arsse::$user->id, 18, ['name' => ""]],
]; ];
Phake::when(Arsse::$db)->labelPropertiesSet(...$db[0])->thenReturn(true); Phake::when(Arsse::$db)->labelPropertiesSet(...$db[0])->thenReturn(true);
Phake::when(Arsse::$db)->labelPropertiesSet(...$db[1])->thenThrow(new ExceptionInput("subjectMissing")); Phake::when(Arsse::$db)->labelPropertiesSet(...$db[1])->thenThrow(new ExceptionInput("subjectMissing"));
@ -622,38 +666,6 @@ class TestTinyTinyAPI extends Test\AbstractTest {
} }
public function testRetrieveCategoryLists() { public function testRetrieveCategoryLists() {
$folders = [
['id' => 5, 'parent' => 3, 'children' => 0, 'name' => "Local"],
['id' => 6, 'parent' => 3, 'children' => 0, 'name' => "National"],
['id' => 4, 'parent' => null, 'children' => 0, 'name' => "Photography"],
['id' => 3, 'parent' => null, 'children' => 2, 'name' => "Politics"],
['id' => 2, 'parent' => 1, 'children' => 0, 'name' => "Rocketry"],
['id' => 1, 'parent' => null, 'children' => 1, 'name' => "Science"],
];
$topFolders = [
['id' => 4, 'parent' => null, 'children' => 0, 'name' => "Photography"],
['id' => 3, 'parent' => null, 'children' => 2, 'name' => "Politics"],
['id' => 1, 'parent' => null, 'children' => 1, 'name' => "Science"],
];
$subscriptions = [
['folder' => null, 'top_folder' => null, 'unread' => 0],
['folder' => 1, 'top_folder' => 1, 'unread' => 2],
['folder' => 2, 'top_folder' => 1, 'unread' => 5],
['folder' => 5, 'top_folder' => 3, 'unread' => 10],
['folder' => 6, 'top_folder' => 3, 'unread' => 12],
['folder' => 6, 'top_folder' => 3, 'unread' => 6],
];
$labels = [
['articles' => 0, 'read' => 0],
['articles' => 100, 'read' => 94],
['articles' => 2, 'read' => 0],
];
Phake::when(Arsse::$db)->folderList($this->anything(), null, true)->thenReturn(new Result($folders));
Phake::when(Arsse::$db)->folderList($this->anything(), null, false)->thenReturn(new Result($topFolders));
Phake::when(Arsse::$db)->subscriptionList($this->anything())->thenReturn(new Result($subscriptions));
Phake::when(Arsse::$db)->labelList($this->anything())->thenReturn(new Result($labels));
Phake::when(Arsse::$db)->articleCount($this->anything(), $this->anything())->thenReturn(7); // FIXME: this should check an unread+modifiedSince context
Phake::when(Arsse::$db)->articleCount($this->anything(), (new Context)->unread(true)->starred(true))->thenReturn(4);
$in = [ $in = [
['op' => "getCategories", 'sid' => "PriestsOfSyrinx", 'include_empty' => true], ['op' => "getCategories", 'sid' => "PriestsOfSyrinx", 'include_empty' => true],
['op' => "getCategories", 'sid' => "PriestsOfSyrinx"], ['op' => "getCategories", 'sid' => "PriestsOfSyrinx"],
@ -662,6 +674,12 @@ class TestTinyTinyAPI extends Test\AbstractTest {
['op' => "getCategories", 'sid' => "PriestsOfSyrinx", 'enable_nested' => true], ['op' => "getCategories", 'sid' => "PriestsOfSyrinx", 'enable_nested' => true],
['op' => "getCategories", 'sid' => "PriestsOfSyrinx", 'enable_nested' => true, 'unread_only' => true], ['op' => "getCategories", 'sid' => "PriestsOfSyrinx", 'enable_nested' => true, 'unread_only' => true],
]; ];
Phake::when(Arsse::$db)->folderList($this->anything(), null, true)->thenReturn(new Result($this->folders));
Phake::when(Arsse::$db)->folderList($this->anything(), null, false)->thenReturn(new Result($this->topFolders));
Phake::when(Arsse::$db)->subscriptionList($this->anything())->thenReturn(new Result($this->subscriptions));
Phake::when(Arsse::$db)->labelList($this->anything())->thenReturn(new Result($this->labels));
Phake::when(Arsse::$db)->articleCount($this->anything(), $this->anything())->thenReturn(7); // FIXME: this should check an unread+modifiedSince context
Phake::when(Arsse::$db)->articleStarred($this->anything())->thenReturn(['total' => 10, 'unread' => 4, 'read' => 6]);
$exp = [ $exp = [
[ [
['id' => 5, 'title' => "Local", 'unread' => 10, 'order_id' => 1], ['id' => 5, 'title' => "Local", 'unread' => 10, 'order_id' => 1],
@ -718,4 +736,36 @@ class TestTinyTinyAPI extends Test\AbstractTest {
$this->assertEquals($this->respGood($exp[$a]), $this->h->dispatch(new Request("POST", "", json_encode($in[$a]))), "Test $a failed"); $this->assertEquals($this->respGood($exp[$a]), $this->h->dispatch(new Request("POST", "", json_encode($in[$a]))), "Test $a failed");
} }
} }
public function testRetrieveCounterList() {
$in = ['op' => "getCounters", 'sid' => "PriestsOfSyrinx"];
Phake::when(Arsse::$db)->folderList($this->anything())->thenReturn(new Result($this->folders));
Phake::when(Arsse::$db)->subscriptionList($this->anything())->thenReturn(new Result($this->subscriptions));
Phake::when(Arsse::$db)->labelList($this->anything(), false)->thenReturn(new Result($this->usedLabels));
Phake::when(Arsse::$db)->articleCount($this->anything(), $this->anything())->thenReturn(7); // FIXME: this should check an unread+modifiedSince context
Phake::when(Arsse::$db)->articleStarred($this->anything())->thenReturn(['total' => 10, 'unread' => 4, 'read' => 6]);
$exp = [
['id' => "global-unread", 'counter' => 35],
['id' => "subscribed-feeds", 'counter' => 6],
['id' => 0, 'counter' => 0, 'auxcounter' => 0],
['id' => -1, 'counter' => 4, 'auxcounter' => 10],
['id' => -2, 'counter' => 0, 'auxcounter' => 0],
['id' => -3, 'counter' => 7, 'auxcounter' => 0],
['id' => -4, 'counter' => 35, 'auxcounter' => 0],
['id' => -1027, 'counter' => 6, 'auxcounter' => 100],
['id' => -1025, 'counter' => 2, 'auxcounter' => 2],
['id' => 3, 'has_img' => 1, 'counter' => 2, 'updated' => "2016-05-23T06:40:02"],
['id' => 1, 'has_img' => 0, 'counter' => 5, 'updated' => "2017-09-15T22:54:16"],
['id' => 2, 'has_img' => 1, 'counter' => 10, 'updated' => "2011-11-11T11:11:11"],
['id' => 5, 'has_img' => 0, 'counter' => 12, 'updated' => "2017-07-07T17:07:17"],
['id' => 4, 'has_img' => 1, 'counter' => 6, 'updated' => "2017-10-09T15:58:34"],
['id' => 5, 'kind' => "cat", 'counter' => 10],
['id' => 6, 'kind' => "cat", 'counter' => 18],
['id' => 3, 'kind' => "cat", 'counter' => 28],
['id' => 2, 'kind' => "cat", 'counter' => 5],
['id' => 1, 'kind' => "cat", 'counter' => 7],
['id' => -2, 'kind' => "cat", 'counter' => 8],
];
$this->assertResponse($this->respGood($exp), $this->h->dispatch(new Request("POST", "", json_encode($in))));
}
} }

View file

@ -730,17 +730,29 @@ trait SeriesArticle {
Arsse::$db->articleMark($this->user, ['read'=>false]); Arsse::$db->articleMark($this->user, ['read'=>false]);
} }
public function testCountStarredArticles() { public function testCountArticles() {
$this->assertSame(2, Arsse::$db->articleCount("john.doe@example.com", (new Context)->starred(true))); $this->assertSame(2, Arsse::$db->articleCount("john.doe@example.com", (new Context)->starred(true)));
$this->assertSame(2, Arsse::$db->articleCount("john.doe@example.org", (new Context)->starred(true))); $this->assertSame(4, Arsse::$db->articleCount("john.doe@example.com", (new Context)->folder(1)));
$this->assertSame(2, Arsse::$db->articleCount("john.doe@example.net", (new Context)->starred(true)));
$this->assertSame(0, Arsse::$db->articleCount("jane.doe@example.com", (new Context)->starred(true))); $this->assertSame(0, Arsse::$db->articleCount("jane.doe@example.com", (new Context)->starred(true)));
} }
public function testCountStarredArticlesWithoutAuthority() { public function testCountArticlesWithoutAuthority() {
Phake::when(Arsse::$user)->authorize->thenReturn(false); Phake::when(Arsse::$user)->authorize->thenReturn(false);
$this->assertException("notAuthorized", "User", "ExceptionAuthz"); $this->assertException("notAuthorized", "User", "ExceptionAuthz");
Arsse::$db->articleCount($this->user, (new Context)->starred(true)); Arsse::$db->articleCount($this->user);
}
public function testFetchStarredCounts() {
$exp1 = ['total' => 2, 'unread' => 1, 'read' => 1];
$exp2 = ['total' => 0, 'unread' => 0, 'read' => 0];
$this->assertSame($exp1, Arsse::$db->articleStarred("john.doe@example.com"));
$this->assertSame($exp2, Arsse::$db->articleStarred("jane.doe@example.com"));
}
public function testFetchStarredCountsWithoutAuthority() {
Phake::when(Arsse::$user)->authorize->thenReturn(false);
$this->assertException("notAuthorized", "User", "ExceptionAuthz");
Arsse::$db->articleStarred($this->user);
} }
public function testFetchLatestEdition() { public function testFetchLatestEdition() {