diff --git a/lib/REST/NextCloudNews/V1_2.php b/lib/REST/NextCloudNews/V1_2.php index a43692e..8cca370 100644 --- a/lib/REST/NextCloudNews/V1_2.php +++ b/lib/REST/NextCloudNews/V1_2.php @@ -119,7 +119,7 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler { ]; // the first path element is the overall scope of the request $scope = $url[0]; - // any URL components which are only digits should be replaced with "#", for easier comparison (integer segments are IDs, and we don't care about the specific ID) + // any URL components which are only digits should be replaced with "0", for easier comparison (integer segments are IDs, and we don't care about the specific ID) for($a = 0; $a < sizeof($url); $a++) { if($this->validateInt($url[$a])) $url[$a] = "0"; } @@ -185,9 +185,9 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler { 'author' => "author", 'pubDate' => "edited_date", 'body' => "content", - 'enclsoureMime' => "media_type", + 'enclosureMime' => "media_type", 'enclosureLink' => "media_url", - 'feedId' => "feed", + 'feedId' => "subscription", 'unread' => "unread", 'starred' => "starred", 'lastModified' => "modified_date", @@ -263,6 +263,7 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler { return new Response(204); } + // mark all articles associated with a folder as read protected function folderMarkRead(array $url, array $data): Response { $c = new Context; if(isset($data['newestItemId'])) { @@ -426,6 +427,7 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler { return new Response(204); } + // mark all articles associated with a subscription as read protected function subscriptionMarkRead(array $url, array $data): Response { $c = new Context; if(isset($data['newestItemId'])) { @@ -446,6 +448,7 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler { return new Response(204); } + // list articles and their properties protected function articleList(array $url, array $data): Response { // set the context options supplied by the client $c = new Context; @@ -499,6 +502,7 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler { return new Response(200, $out); } + // mark all articles as read protected function articleMarkReadAll(array $url, array $data): Response { $c = new Context; if(isset($data['newestItemId'])) { @@ -513,6 +517,7 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler { return new Response(204); } + // mark a single article as read protected function articleMarkRead(array $url, array $data): Response { // initialize the matching context $c = new Context; @@ -528,6 +533,7 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler { return new Response(204); } + // mark a single article as read protected function articleMarkStarred(array $url, array $data): Response { // initialize the matching context $c = new Context; @@ -535,7 +541,7 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler { // determine whether to mark read or unread $set = ($url[3]=="star"); try { - Data::$db->articleMark(Data::$user->id, ['star' => $set], $c); + Data::$db->articleMark(Data::$user->id, ['starred' => $set], $c); } catch(ExceptionInput $e) { // ID is not valid return new Response(404); @@ -543,9 +549,8 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler { return new Response(204); } + // mark an array of articles as read protected function articleMarkReadMulti(array $url, array $data): Response { - // initialize the matching context - $c = new Context; // determine whether to mark read or unread $set = ($url[1]=="read"); // if the input data is not at all valid, return an error @@ -554,6 +559,8 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler { $t = Data::$db->begin(); $in = array_chunk($data['items'], 50); for($a = 0; $a < sizeof($in); $a++) { + // initialize the matching context + $c = new Context; $c->editions($in[$a]); try { Data::$db->articleMark(Data::$user->id, ['read' => $set], $c); @@ -563,10 +570,9 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler { return new Response(204); } + // mark an array of articles as starred protected function articleMarkStarredMulti(array $url, array $data): Response { - // initialize the matching context - $c = new Context; - // determine whether to mark read or unread + // determine whether to mark starred or unstarred $set = ($url[1]=="star"); // if the input data is not at all valid, return an error if(!isset($data['items']) || !is_array($data['items'])) return new Response(422); @@ -574,6 +580,8 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler { $t = Data::$db->begin(); $in = array_chunk(array_column($data['items'], "guidHash"), 50); for($a = 0; $a < sizeof($in); $a++) { + // initialize the matching context + $c = new Context; $c->articles($in[$a]); try { Data::$db->articleMark(Data::$user->id, ['starred' => $set], $c); diff --git a/tests/REST/NextCloudNews/TestNCNV1_2.php b/tests/REST/NextCloudNews/TestNCNV1_2.php index 854f268..03c8f6e 100644 --- a/tests/REST/NextCloudNews/TestNCNV1_2.php +++ b/tests/REST/NextCloudNews/TestNCNV1_2.php @@ -5,6 +5,8 @@ use JKingWeb\Arsse\REST\Request; use JKingWeb\Arsse\REST\Response; use JKingWeb\Arsse\Test\Result; use JKingWeb\Arsse\Misc\Context; +use JKingWeb\Arsse\Db\ExceptionInput; +use JKingWeb\Arsse\Db\Transaction; use Phake; @@ -74,6 +76,187 @@ class TestNCNV1_2 extends Test\AbstractTest { ], ], ]; + protected $articles = [ + 'db' => [ + [ + 'id' => 101, + 'url' => 'http://example.com/1', + 'title' => 'Article title 1', + 'author' => '', + 'content' => '

Article content 1

', + 'guid' => 'e433653cef2e572eee4215fa299a4a5af9137b2cefd6283c85bd69a32915beda', + 'published_date' => '2000-01-01 00:00:00', + 'edited_date' => '2000-01-01 00:00:01', + 'modified_date' => '2000-01-01 01:00:00', + 'unread' => 1, + 'starred' => 0, + 'edition' => 101, + 'subscription' => 8, + 'fingerprint' => 'f5cb8bfc1c7396dc9816af212a3e2ac5221585c2a00bf7ccb6aabd95dcfcd6a6:fb0bc8f8cb08913dc5a497db700e327f1d34e4987402687d494a5891f24714d4:18fdd4fa93d693128c43b004399e5c9cea6c261ddfa002518d3669f55d8c2207', + 'media_url' => null, + 'media_type' => null, + ], + [ + 'id' => 102, + 'url' => 'http://example.com/2', + 'title' => 'Article title 2', + 'author' => '', + 'content' => '

Article content 2

', + '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' => 0, + 'starred' => 0, + 'edition' => 202, + 'subscription' => 8, + 'fingerprint' => '0e86d2de822a174fe3c44a466953e63ca1f1a58a19cbf475fce0855d4e3d5153:13075894189c47ffcfafd1dfe7fbb539f7c74a69d35a399b3abf8518952714f9:2abd0a8cba83b8214a66c8f0293ba63e467d720540e29ff8ddcdab069d4f1c9e', + 'media_url' => "http://example.com/text", + 'media_type' => "text/plain", + ], + [ + 'id' => 103, + 'url' => 'http://example.com/3', + 'title' => 'Article title 3', + 'author' => '', + 'content' => '

Article content 3

', + 'guid' => '31a6594500a48b59fcc8a075ce82b946c9c3c782460d088bd7b8ef3ede97ad92', + 'published_date' => '2000-01-03 00:00:00', + 'edited_date' => '2000-01-03 00:00:03', + 'modified_date' => '2000-01-03 03:00:00', + 'unread' => 1, + 'starred' => 1, + 'edition' => 203, + 'subscription' => 9, + 'fingerprint' => 'f74b06b240bd08abf4d3fdfc20dba6a6f6eb8b4f1a00e9a617efd63a87180a4b:b278380e984cefe63f0e412b88ffc9cb0befdfa06fdc00bace1da99a8daff406:ad622b31e739cd3a3f3c788991082cf4d2f7a8773773008e75f0572e58cd373b', + 'media_url' => "http://example.com/video", + 'media_type' => "video/webm", + ], + [ + 'id' => 104, + 'url' => 'http://example.com/4', + 'title' => 'Article title 4', + 'author' => '', + 'content' => '

Article content 4

', + 'guid' => '804e517d623390e71497982c77cf6823180342ebcd2e7d5e32da1e55b09dd180', + 'published_date' => '2000-01-04 00:00:00', + 'edited_date' => '2000-01-04 00:00:04', + 'modified_date' => '2000-01-04 04:00:00', + 'unread' => 0, + 'starred' => 1, + 'edition' => 204, + 'subscription' => 9, + 'fingerprint' => 'f3615c7f16336d3ea242d35cf3fc17dbc4ee3afb78376bf49da2dd7a5a25dec8:f11c2b4046f207579aeb9c69a8c20ca5461cef49756ccfa5ba5e2344266da3b3:ab2da63276acce431250b18d3d49b988b226a99c7faadf275c90b751aee05be9', + 'media_url' => "http://example.com/image", + 'media_type' => "image/svg+xml", + ], + [ + 'id' => 105, + 'url' => 'http://example.com/5', + 'title' => 'Article title 5', + 'author' => '', + 'content' => '

Article content 5

', + 'guid' => 'db3e736c2c492f5def5c5da33ddcbea1824040e9ced2142069276b0a6e291a41', + 'published_date' => '2000-01-05 00:00:00', + 'edited_date' => '2000-01-05 00:00:05', + 'modified_date' => '2000-01-05 05:00:00', + 'unread' => 1, + 'starred' => 0, + 'edition' => 305, + 'subscription' => 10, + 'fingerprint' => 'd40da96e39eea6c55948ccbe9b3d275b5f931298288dbe953990c5f496097022:834240f84501b5341d375414718204ec421561f3825d34c22bf9182203e42900:43b970ac6ec5f8a9647b2c7e4eed8b1d7f62e154a95eed748b0294c1256764ba', + 'media_url' => "http://example.com/audio", + 'media_type' => "audio/ogg", + ], + ], + 'rest' => [ + [ + 'guidHash' => 101, + 'url' => 'http://example.com/1', + 'title' => 'Article title 1', + 'author' => '', + 'body' => '

Article content 1

', + 'guid' => 'e433653cef2e572eee4215fa299a4a5af9137b2cefd6283c85bd69a32915beda', + 'pubDate' => 946684801, + 'lastModified' => 946688400, + 'unread' => true, + 'starred' => false, + 'id' => 101, + 'feedId' => 8, + 'fingerprint' => 'f5cb8bfc1c7396dc9816af212a3e2ac5221585c2a00bf7ccb6aabd95dcfcd6a6:fb0bc8f8cb08913dc5a497db700e327f1d34e4987402687d494a5891f24714d4:18fdd4fa93d693128c43b004399e5c9cea6c261ddfa002518d3669f55d8c2207', + 'enclosureLink' => null, + 'enclosureMime' => null, + ], + [ + 'guidHash' => 102, + 'url' => 'http://example.com/2', + 'title' => 'Article title 2', + 'author' => '', + 'body' => '

Article content 2

', + 'guid' => '5be8a5a46ecd52ed132191c8d27fb1af6b3d4edc00234c5d9f8f0e10562ed3b7', + 'pubDate' => 946771202, + 'lastModified' => 946778400, + 'unread' => false, + 'starred' => false, + 'id' => 202, + 'feedId' => 8, + 'fingerprint' => '0e86d2de822a174fe3c44a466953e63ca1f1a58a19cbf475fce0855d4e3d5153:13075894189c47ffcfafd1dfe7fbb539f7c74a69d35a399b3abf8518952714f9:2abd0a8cba83b8214a66c8f0293ba63e467d720540e29ff8ddcdab069d4f1c9e', + 'enclosureLink' => "http://example.com/text", + 'enclosureMime' => "text/plain", + ], + [ + 'guidHash' => 103, + 'url' => 'http://example.com/3', + 'title' => 'Article title 3', + 'author' => '', + 'body' => '

Article content 3

', + 'guid' => '31a6594500a48b59fcc8a075ce82b946c9c3c782460d088bd7b8ef3ede97ad92', + 'pubDate' => 946857603, + 'lastModified' => 946868400, + 'unread' => true, + 'starred' => true, + 'id' => 203, + 'feedId' => 9, + 'fingerprint' => 'f74b06b240bd08abf4d3fdfc20dba6a6f6eb8b4f1a00e9a617efd63a87180a4b:b278380e984cefe63f0e412b88ffc9cb0befdfa06fdc00bace1da99a8daff406:ad622b31e739cd3a3f3c788991082cf4d2f7a8773773008e75f0572e58cd373b', + 'enclosureLink' => "http://example.com/video", + 'enclosureMime' => "video/webm", + ], + [ + 'guidHash' => 104, + 'url' => 'http://example.com/4', + 'title' => 'Article title 4', + 'author' => '', + 'body' => '

Article content 4

', + 'guid' => '804e517d623390e71497982c77cf6823180342ebcd2e7d5e32da1e55b09dd180', + 'pubDate' => 946944004, + 'lastModified' => 946958400, + 'unread' => false, + 'starred' => true, + 'id' => 204, + 'feedId' => 9, + 'fingerprint' => 'f3615c7f16336d3ea242d35cf3fc17dbc4ee3afb78376bf49da2dd7a5a25dec8:f11c2b4046f207579aeb9c69a8c20ca5461cef49756ccfa5ba5e2344266da3b3:ab2da63276acce431250b18d3d49b988b226a99c7faadf275c90b751aee05be9', + 'enclosureLink' => "http://example.com/image", + 'enclosureMime' => "image/svg+xml", + ], + [ + 'guidHash' => 105, + 'url' => 'http://example.com/5', + 'title' => 'Article title 5', + 'author' => '', + 'body' => '

Article content 5

', + 'guid' => 'db3e736c2c492f5def5c5da33ddcbea1824040e9ced2142069276b0a6e291a41', + 'pubDate' => 947030405, + 'lastModified' => 947048400, + 'unread' => true, + 'starred' => false, + 'id' => 305, + 'feedId' => 10, + 'fingerprint' => 'd40da96e39eea6c55948ccbe9b3d275b5f931298288dbe953990c5f496097022:834240f84501b5341d375414718204ec421561f3825d34c22bf9182203e42900:43b970ac6ec5f8a9647b2c7e4eed8b1d7f62e154a95eed748b0294c1256764ba', + 'enclosureLink' => "http://example.com/audio", + 'enclosureMime' => "audio/ogg", + ], + ], + ]; function setUp() { $this->clearData(); @@ -84,6 +267,7 @@ class TestNCNV1_2 extends Test\AbstractTest { Data::$user->id = "john.doe@example.com"; // create a mock database interface Data::$db = Phake::mock(Database::Class); + Phake::when(Data::$db)->begin->thenReturn(Phake::mock(Transaction::class)); $this->h = new REST\NextCloudNews\V1_2(); } @@ -165,14 +349,14 @@ class TestNCNV1_2 extends Test\AbstractTest { ['id' => 2, 'name' => "Hardware", 'parent' => null], ]; // set of various mocks for testing - Phake::when(Data::$db)->folderAdd(Data::$user->id, $in[0])->thenReturn(1)->thenThrow(new \JKingWeb\Arsse\Db\ExceptionInput("constraintViolation")); // error on the second call - Phake::when(Data::$db)->folderAdd(Data::$user->id, $in[1])->thenReturn(2)->thenThrow(new \JKingWeb\Arsse\Db\ExceptionInput("constraintViolation")); // error on the second call + Phake::when(Data::$db)->folderAdd(Data::$user->id, $in[0])->thenReturn(1)->thenThrow(new ExceptionInput("constraintViolation")); // error on the second call + Phake::when(Data::$db)->folderAdd(Data::$user->id, $in[1])->thenReturn(2)->thenThrow(new ExceptionInput("constraintViolation")); // error on the second call Phake::when(Data::$db)->folderPropertiesGet(Data::$user->id, 1)->thenReturn($out[0]); Phake::when(Data::$db)->folderPropertiesGet(Data::$user->id, 2)->thenReturn($out[1]); // set up mocks that produce errors - Phake::when(Data::$db)->folderAdd(Data::$user->id, [])->thenThrow(new \JKingWeb\Arsse\Db\ExceptionInput("missing")); - Phake::when(Data::$db)->folderAdd(Data::$user->id, ['name' => ""])->thenThrow(new \JKingWeb\Arsse\Db\ExceptionInput("missing")); - Phake::when(Data::$db)->folderAdd(Data::$user->id, ['name' => " "])->thenThrow(new \JKingWeb\Arsse\Db\ExceptionInput("whitespace")); + Phake::when(Data::$db)->folderAdd(Data::$user->id, [])->thenThrow(new ExceptionInput("missing")); + Phake::when(Data::$db)->folderAdd(Data::$user->id, ['name' => ""])->thenThrow(new ExceptionInput("missing")); + Phake::when(Data::$db)->folderAdd(Data::$user->id, ['name' => " "])->thenThrow(new ExceptionInput("whitespace")); // correctly add two folders, using different means $exp = new Response(200, ['folders' => [$out[0]]]); $this->assertEquals($exp, $this->h->dispatch(new Request("POST", "/folders", json_encode($in[0]), 'application/json'))); @@ -195,7 +379,7 @@ class TestNCNV1_2 extends Test\AbstractTest { } function testRemoveAFolder() { - Phake::when(Data::$db)->folderRemove(Data::$user->id, 1)->thenReturn(true)->thenThrow(new \JKingWeb\Arsse\Db\ExceptionInput("subjectMissing")); + Phake::when(Data::$db)->folderRemove(Data::$user->id, 1)->thenReturn(true)->thenThrow(new ExceptionInput("subjectMissing")); $exp = new Response(204); $this->assertEquals($exp, $this->h->dispatch(new Request("DELETE", "/folders/1"))); // fail on the second invocation because it no longer exists @@ -213,11 +397,11 @@ class TestNCNV1_2 extends Test\AbstractTest { [], ]; Phake::when(Data::$db)->folderPropertiesSet(Data::$user->id, 1, $in[0])->thenReturn(true); - Phake::when(Data::$db)->folderPropertiesSet(Data::$user->id, 2, $in[1])->thenThrow(new \JKingWeb\Arsse\Db\ExceptionInput("constraintViolation")); - Phake::when(Data::$db)->folderPropertiesSet(Data::$user->id, 1, $in[2])->thenThrow(new \JKingWeb\Arsse\Db\ExceptionInput("missing")); - Phake::when(Data::$db)->folderPropertiesSet(Data::$user->id, 1, $in[3])->thenThrow(new \JKingWeb\Arsse\Db\ExceptionInput("whitespace")); + Phake::when(Data::$db)->folderPropertiesSet(Data::$user->id, 2, $in[1])->thenThrow(new ExceptionInput("constraintViolation")); + Phake::when(Data::$db)->folderPropertiesSet(Data::$user->id, 1, $in[2])->thenThrow(new ExceptionInput("missing")); + Phake::when(Data::$db)->folderPropertiesSet(Data::$user->id, 1, $in[3])->thenThrow(new ExceptionInput("whitespace")); Phake::when(Data::$db)->folderPropertiesSet(Data::$user->id, 1, $in[4])->thenReturn(true); // this should be stopped by the handler before the request gets to the database - Phake::when(Data::$db)->folderPropertiesSet(Data::$user->id, 3, $this->anything())->thenThrow(new \JKingWeb\Arsse\Db\ExceptionInput("subjectMissing")); // folder ID 3 does not exist + Phake::when(Data::$db)->folderPropertiesSet(Data::$user->id, 3, $this->anything())->thenThrow(new ExceptionInput("subjectMissing")); // folder ID 3 does not exist $exp = new Response(204); $this->assertEquals($exp, $this->h->dispatch(new Request("PUT", "/folders/1", json_encode($in[0]), 'application/json'))); $exp = new Response(409); @@ -268,13 +452,13 @@ class TestNCNV1_2 extends Test\AbstractTest { [], ]; // set up the necessary mocks - Phake::when(Data::$db)->subscriptionAdd(Data::$user->id, "http://example.com/news.atom")->thenReturn(2112)->thenThrow(new \JKingWeb\Arsse\Db\ExceptionInput("constraintViolation")); // error on the second call - Phake::when(Data::$db)->subscriptionAdd(Data::$user->id, "http://example.org/news.atom")->thenReturn( 42 )->thenThrow(new \JKingWeb\Arsse\Db\ExceptionInput("constraintViolation")); // error on the second call + Phake::when(Data::$db)->subscriptionAdd(Data::$user->id, "http://example.com/news.atom")->thenReturn(2112)->thenThrow(new ExceptionInput("constraintViolation")); // error on the second call + Phake::when(Data::$db)->subscriptionAdd(Data::$user->id, "http://example.org/news.atom")->thenReturn( 42 )->thenThrow(new ExceptionInput("constraintViolation")); // error on the second call Phake::when(Data::$db)->subscriptionPropertiesGet(Data::$user->id, 2112)->thenReturn($this->feeds['db'][0]); Phake::when(Data::$db)->subscriptionPropertiesGet(Data::$user->id, 42)->thenReturn($this->feeds['db'][1]); Phake::when(Data::$db)->editionLatest(Data::$user->id, (new Context)->subscription(2112))->thenReturn(0); Phake::when(Data::$db)->editionLatest(Data::$user->id, (new Context)->subscription( 42))->thenReturn(4758915); - Phake::when(Data::$db)->subscriptionPropertiesSet(Data::$user->id, 2112, ['folder' => 3])->thenThrow(new \JKingWeb\Arsse\Db\ExceptionInput("idMissing")); // folder ID 3 does not exist + Phake::when(Data::$db)->subscriptionPropertiesSet(Data::$user->id, 2112, ['folder' => 3])->thenThrow(new ExceptionInput("idMissing")); // folder ID 3 does not exist Phake::when(Data::$db)->subscriptionPropertiesSet(Data::$user->id, 42, ['folder' => 8])->thenReturn(true); // set up a mock for a bad feed Phake::when(Data::$db)->subscriptionAdd(Data::$user->id, "http://example.net/news.atom")->thenThrow(new \JKingWeb\Arsse\Feed\Exception("http://example.net/news.atom", new \PicoFeed\Client\InvalidUrlException())); @@ -294,7 +478,7 @@ class TestNCNV1_2 extends Test\AbstractTest { } function testRemoveASubscription() { - Phake::when(Data::$db)->subscriptionRemove(Data::$user->id, 1)->thenReturn(true)->thenThrow(new \JKingWeb\Arsse\Db\ExceptionInput("subjectMissing")); + Phake::when(Data::$db)->subscriptionRemove(Data::$user->id, 1)->thenReturn(true)->thenThrow(new ExceptionInput("subjectMissing")); $exp = new Response(204); $this->assertEquals($exp, $this->h->dispatch(new Request("DELETE", "/feeds/1"))); // fail on the second invocation because it no longer exists @@ -312,8 +496,8 @@ class TestNCNV1_2 extends Test\AbstractTest { ]; Phake::when(Data::$db)->subscriptionPropertiesSet(Data::$user->id, 1, ['folder' => 42])->thenReturn(true); Phake::when(Data::$db)->subscriptionPropertiesSet(Data::$user->id, 1, ['folder' => null])->thenReturn(true); - Phake::when(Data::$db)->subscriptionPropertiesSet(Data::$user->id, 1, ['folder' => 2112])->thenThrow(new \JKingWeb\Arsse\Db\ExceptionInput("idMissing")); // folder does not exist - Phake::when(Data::$db)->subscriptionPropertiesSet(Data::$user->id, 42, $this->anything())->thenThrow(new \JKingWeb\Arsse\Db\ExceptionInput("subjectMissing")); // subscription does not exist + Phake::when(Data::$db)->subscriptionPropertiesSet(Data::$user->id, 1, ['folder' => 2112])->thenThrow(new ExceptionInput("idMissing")); // folder does not exist + Phake::when(Data::$db)->subscriptionPropertiesSet(Data::$user->id, 42, $this->anything())->thenThrow(new ExceptionInput("subjectMissing")); // subscription does not exist $exp = new Response(204); $this->assertEquals($exp, $this->h->dispatch(new Request("PUT", "/feeds/1/move", json_encode($in[0]), 'application/json'))); $exp = new Response(204); @@ -335,10 +519,10 @@ class TestNCNV1_2 extends Test\AbstractTest { ]; Phake::when(Data::$db)->subscriptionPropertiesSet(Data::$user->id, 1, $this->identicalTo(['title' => null]))->thenReturn(true); Phake::when(Data::$db)->subscriptionPropertiesSet(Data::$user->id, 1, $this->identicalTo(['title' => "Ook"]))->thenReturn(true); - Phake::when(Data::$db)->subscriptionPropertiesSet(Data::$user->id, 1, $this->identicalTo(['title' => " "]))->thenThrow(new \JKingWeb\Arsse\Db\ExceptionInput("whitespace")); - Phake::when(Data::$db)->subscriptionPropertiesSet(Data::$user->id, 1, $this->identicalTo(['title' => ""]))->thenThrow(new \JKingWeb\Arsse\Db\ExceptionInput("missing")); - Phake::when(Data::$db)->subscriptionPropertiesSet(Data::$user->id, 1, $this->identicalTo(['title' => false]))->thenThrow(new \JKingWeb\Arsse\Db\ExceptionInput("missing")); - Phake::when(Data::$db)->subscriptionPropertiesSet(Data::$user->id, 42, $this->anything())->thenThrow(new \JKingWeb\Arsse\Db\ExceptionInput("subjectMissing")); + Phake::when(Data::$db)->subscriptionPropertiesSet(Data::$user->id, 1, $this->identicalTo(['title' => " "]))->thenThrow(new ExceptionInput("whitespace")); + Phake::when(Data::$db)->subscriptionPropertiesSet(Data::$user->id, 1, $this->identicalTo(['title' => ""]))->thenThrow(new ExceptionInput("missing")); + Phake::when(Data::$db)->subscriptionPropertiesSet(Data::$user->id, 1, $this->identicalTo(['title' => false]))->thenThrow(new ExceptionInput("missing")); + Phake::when(Data::$db)->subscriptionPropertiesSet(Data::$user->id, 42, $this->anything())->thenThrow(new ExceptionInput("subjectMissing")); $exp = new Response(204); $this->assertEquals($exp, $this->h->dispatch(new Request("PUT", "/feeds/1/rename", json_encode($in[0]), 'application/json'))); $exp = new Response(204); @@ -379,7 +563,7 @@ class TestNCNV1_2 extends Test\AbstractTest { ['feed' => 42], // invalid input ]; Phake::when(Data::$db)->feedUpdate( 42)->thenReturn(true); - Phake::when(Data::$db)->feedUpdate(2112)->thenThrow(new \JKingWeb\Arsse\Db\ExceptionInput("subjectMissing")); + Phake::when(Data::$db)->feedUpdate(2112)->thenThrow(new ExceptionInput("subjectMissing")); $exp = new Response(204); $this->assertEquals($exp, $this->h->dispatch(new Request("GET", "/feeds/update", json_encode($in[0]), 'application/json'))); $exp = new Response(404); @@ -393,4 +577,183 @@ class TestNCNV1_2 extends Test\AbstractTest { $exp = new Response(403); $this->assertEquals($exp, $this->h->dispatch(new Request("GET", "/feeds/update", json_encode($in[0]), 'application/json'))); } + + function testListArticles() { + $res = new Result($this->articles['db']); + $t = new \DateTime; + $in = [ + ['type' => 0, 'id' => 42], + ['type' => 1, 'id' => 2112], + ['type' => 2, 'id' => 0], + ['type' => 3, 'id' => 0], + ['oldestFirst' => true, 'batchSize' => 10, 'offset' => 5], + ['oldestFirst' => false, 'batchSize' => 5, 'offset' => 5], + ['getRead' => true], + ['getRead' => false], + ['lastModified' => $t->getTimestamp()], + ]; + Phake::when(Data::$db)->articleList(Data::$user->id, $this->anything())->thenReturn($res); + Phake::when(Data::$db)->articleList(Data::$user->id, (new Context)->reverse(true)->subscription(42))->thenThrow(new ExceptionInput("idMissing")); + Phake::when(Data::$db)->articleList(Data::$user->id, (new Context)->reverse(true)->folder(2112))->thenThrow(new ExceptionInput("idMissing")); + $exp = new Response(200, ['items' => $this->articles['rest']]); + // check the contents of the response + $this->assertEquals($exp, $this->h->dispatch(new Request("GET", "/items"))); // first instance of base context + $this->assertEquals($exp, $this->h->dispatch(new Request("GET", "/items/updated"))); // second instance of base context + // check error conditions + $exp = new Response(422); + $this->assertEquals($exp, $this->h->dispatch(new Request("GET", "/items", json_encode($in[0]), 'application/json'))); + $this->assertEquals($exp, $this->h->dispatch(new Request("GET", "/items", json_encode($in[1]), 'application/json'))); + // simply run through the remainder of the input for later method verification + $this->h->dispatch(new Request("GET", "/items", json_encode($in[2]), 'application/json')); + $this->h->dispatch(new Request("GET", "/items", json_encode($in[3]), 'application/json')); // third instance of base context + $this->h->dispatch(new Request("GET", "/items", json_encode($in[4]), 'application/json')); + $this->h->dispatch(new Request("GET", "/items", json_encode($in[5]), 'application/json')); + $this->h->dispatch(new Request("GET", "/items", json_encode($in[6]), 'application/json')); // fourth instance of base context + $this->h->dispatch(new Request("GET", "/items", json_encode($in[7]), 'application/json')); + $this->h->dispatch(new Request("GET", "/items", json_encode($in[8]), 'application/json')); + // perform method verifications + Phake::verify(Data::$db, Phake::times(4))->articleList(Data::$user->id, (new Context)->reverse(true)); + Phake::verify(Data::$db)->articleList(Data::$user->id, (new Context)->reverse(true)->subscription(42)); + Phake::verify(Data::$db)->articleList(Data::$user->id, (new Context)->reverse(true)->folder(2112)); + Phake::verify(Data::$db)->articleList(Data::$user->id, (new Context)->reverse(true)->starred(true)); + Phake::verify(Data::$db)->articleList(Data::$user->id, (new Context)->reverse(false)->limit(10)->oldestEdition(6)); + Phake::verify(Data::$db)->articleList(Data::$user->id, (new Context)->reverse(true)->limit(5)->latestEdition(4)); + Phake::verify(Data::$db)->articleList(Data::$user->id, (new Context)->reverse(true)->unread(true)); + Phake::verify(Data::$db)->articleList(Data::$user->id, (new Context)->reverse(true)->modifiedSince($t)); + } + + function testMarkAFolderRead() { + $read = ['read' => true]; + $in = json_encode(['newestItemId' => 2112]); + Phake::when(Data::$db)->articleMark(Data::$user->id, $read, (new Context)->folder(1)->latestEdition(2112))->thenReturn(true); + Phake::when(Data::$db)->articleMark(Data::$user->id, $read, (new Context)->folder(42)->latestEdition(2112))->thenThrow(new ExceptionInput("idMissing")); // folder doesn't exist + $exp = new Response(204); + $this->assertEquals($exp, $this->h->dispatch(new Request("PUT", "/folders/1/read", $in, 'application/json'))); + $this->assertEquals($exp, $this->h->dispatch(new Request("PUT", "/folders/1/read?newestItemId=2112"))); + $exp = new Response(422); + $this->assertEquals($exp, $this->h->dispatch(new Request("PUT", "/folders/1/read"))); + $this->assertEquals($exp, $this->h->dispatch(new Request("PUT", "/folders/1/read?newestItemId=ook"))); + $exp = new Response(404); + $this->assertEquals($exp, $this->h->dispatch(new Request("PUT", "/folders/42/read", $in, 'application/json'))); + } + + function testMarkASubscriptionRead() { + $read = ['read' => true]; + $in = json_encode(['newestItemId' => 2112]); + Phake::when(Data::$db)->articleMark(Data::$user->id, $read, (new Context)->subscription(1)->latestEdition(2112))->thenReturn(true); + Phake::when(Data::$db)->articleMark(Data::$user->id, $read, (new Context)->subscription(42)->latestEdition(2112))->thenThrow(new ExceptionInput("idMissing")); // subscription doesn't exist + $exp = new Response(204); + $this->assertEquals($exp, $this->h->dispatch(new Request("PUT", "/feeds/1/read", $in, 'application/json'))); + $this->assertEquals($exp, $this->h->dispatch(new Request("PUT", "/feeds/1/read?newestItemId=2112"))); + $exp = new Response(422); + $this->assertEquals($exp, $this->h->dispatch(new Request("PUT", "/feeds/1/read"))); + $this->assertEquals($exp, $this->h->dispatch(new Request("PUT", "/feeds/1/read?newestItemId=ook"))); + $exp = new Response(404); + $this->assertEquals($exp, $this->h->dispatch(new Request("PUT", "/feeds/42/read", $in, 'application/json'))); + } + + function testMarkAllItemsRead() { + $read = ['read' => true]; + $in = json_encode(['newestItemId' => 2112]); + Phake::when(Data::$db)->articleMark(Data::$user->id, $read, (new Context)->latestEdition(2112))->thenReturn(true); + $exp = new Response(204); + $this->assertEquals($exp, $this->h->dispatch(new Request("PUT", "/items/read", $in, 'application/json'))); + $this->assertEquals($exp, $this->h->dispatch(new Request("PUT", "/items/read?newestItemId=2112"))); + $exp = new Response(422); + $this->assertEquals($exp, $this->h->dispatch(new Request("PUT", "/items/read"))); + $this->assertEquals($exp, $this->h->dispatch(new Request("PUT", "/items/read?newestItemId=ook"))); + } + + function testChangeMarksOfASingleArticle() { + $read = ['read' => true]; + $unread = ['read' => false]; + $star = ['starred' => true]; + $unstar = ['starred' => false]; + Phake::when(Data::$db)->articleMark(Data::$user->id, $read, (new Context)->edition(1))->thenReturn(true); + Phake::when(Data::$db)->articleMark(Data::$user->id, $read, (new Context)->edition(42))->thenThrow(new ExceptionInput("subjectMissing")); // edition doesn't exist doesn't exist + Phake::when(Data::$db)->articleMark(Data::$user->id, $unread, (new Context)->edition(2))->thenReturn(true); + Phake::when(Data::$db)->articleMark(Data::$user->id, $unread, (new Context)->edition(47))->thenThrow(new ExceptionInput("subjectMissing")); // edition doesn't exist doesn't exist + Phake::when(Data::$db)->articleMark(Data::$user->id, $star, (new Context)->article(3))->thenReturn(true); + Phake::when(Data::$db)->articleMark(Data::$user->id, $star, (new Context)->article(2112))->thenThrow(new ExceptionInput("subjectMissing")); // article doesn't exist doesn't exist + Phake::when(Data::$db)->articleMark(Data::$user->id, $unstar, (new Context)->article(4))->thenReturn(true); + Phake::when(Data::$db)->articleMark(Data::$user->id, $unstar, (new Context)->article(1337))->thenThrow(new ExceptionInput("subjectMissing")); // article doesn't exist doesn't exist + $exp = new Response(204); + $this->assertEquals($exp, $this->h->dispatch(new Request("PUT", "/items/1/read"))); + $this->assertEquals($exp, $this->h->dispatch(new Request("PUT", "/items/2/unread"))); + $this->assertEquals($exp, $this->h->dispatch(new Request("PUT", "/items/1/3/star"))); + $this->assertEquals($exp, $this->h->dispatch(new Request("PUT", "/items/4400/4/unstar"))); + $exp = new Response(404); + $this->assertEquals($exp, $this->h->dispatch(new Request("PUT", "/items/42/read"))); + $this->assertEquals($exp, $this->h->dispatch(new Request("PUT", "/items/47/unread"))); + $this->assertEquals($exp, $this->h->dispatch(new Request("PUT", "/items/1/2112/star"))); + $this->assertEquals($exp, $this->h->dispatch(new Request("PUT", "/items/4400/1337/unstar"))); + Phake::verify(Data::$db, Phake::times(8))->articleMark(Data::$user->id, $this->anything(), $this->anything()); + } + + function testChangeMarksOfMultipleArticles() { + $read = ['read' => true]; + $unread = ['read' => false]; + $star = ['starred' => true]; + $unstar = ['starred' => false]; + $in = [ + ["ook","eek","ack"], + range(100,199), + range(100,149), + range(150,199), + ]; + $inStar = $in; + for($a = 0; $a < sizeof($inStar); $a++) { + for($b = 0; $b < sizeof($inStar[$a]); $b++) { + $inStar[$a][$b] = ['feedId' => 2112, 'guidHash' => $inStar[$a][$b]]; + } + } + Phake::when(Data::$db)->articleMark(Data::$user->id, $this->anything(), $this->anything())->thenReturn(true); + Phake::when(Data::$db)->articleMark(Data::$user->id, $this->anything(), (new Context)->editions([]))->thenThrow(new ExceptionInput("tooShort")); // data model function requires one valid integer for multiples + Phake::when(Data::$db)->articleMark(Data::$user->id, $this->anything(), (new Context)->editions($in[1]))->thenThrow(new ExceptionInput("tooLong")); // data model function for limited to 50 items for multiples + $exp = new Response(422); + $this->assertEquals($exp, $this->h->dispatch(new Request("PUT", "/items/read/multiple"))); + $this->assertEquals($exp, $this->h->dispatch(new Request("PUT", "/items/unread/multiple"))); + $this->assertEquals($exp, $this->h->dispatch(new Request("PUT", "/items/star/multiple"))); + $this->assertEquals($exp, $this->h->dispatch(new Request("PUT", "/items/unstar/multiple"))); + $this->assertEquals($exp, $this->h->dispatch(new Request("PUT", "/items/read/multiple", json_encode(['items' => "ook"]), 'application/json'))); + $this->assertEquals($exp, $this->h->dispatch(new Request("PUT", "/items/unread/multiple", json_encode(['items' => "ook"]), 'application/json'))); + $this->assertEquals($exp, $this->h->dispatch(new Request("PUT", "/items/star/multiple", json_encode(['items' => "ook"]), 'application/json'))); + $this->assertEquals($exp, $this->h->dispatch(new Request("PUT", "/items/unstar/multiple", json_encode(['items' => "ook"]), 'application/json'))); + $exp = new Response(204); + $this->assertEquals($exp, $this->h->dispatch(new Request("PUT", "/items/read/multiple", json_encode(['items' => []]), 'application/json'))); + $this->assertEquals($exp, $this->h->dispatch(new Request("PUT", "/items/unread/multiple", json_encode(['items' => []]), 'application/json'))); + $this->assertEquals($exp, $this->h->dispatch(new Request("PUT", "/items/read/multiple", json_encode(['items' => $in[0]]), 'application/json'))); + $this->assertEquals($exp, $this->h->dispatch(new Request("PUT", "/items/unread/multiple", json_encode(['items' => $in[0]]), 'application/json'))); + $this->assertEquals($exp, $this->h->dispatch(new Request("PUT", "/items/read/multiple", json_encode(['items' => $in[1]]), 'application/json'))); + $this->assertEquals($exp, $this->h->dispatch(new Request("PUT", "/items/unread/multiple", json_encode(['items' => $in[1]]), 'application/json'))); + $exp = new Response(204); + $this->assertEquals($exp, $this->h->dispatch(new Request("PUT", "/items/star/multiple", json_encode(['items' => []]), 'application/json'))); + $this->assertEquals($exp, $this->h->dispatch(new Request("PUT", "/items/unstar/multiple", json_encode(['items' => []]), 'application/json'))); + $this->assertEquals($exp, $this->h->dispatch(new Request("PUT", "/items/star/multiple", json_encode(['items' => $inStar[0]]), 'application/json'))); + $this->assertEquals($exp, $this->h->dispatch(new Request("PUT", "/items/unstar/multiple", json_encode(['items' => $inStar[0]]), 'application/json'))); + $this->assertEquals($exp, $this->h->dispatch(new Request("PUT", "/items/star/multiple", json_encode(['items' => $inStar[1]]), 'application/json'))); + $this->assertEquals($exp, $this->h->dispatch(new Request("PUT", "/items/unstar/multiple", json_encode(['items' => $inStar[1]]), 'application/json'))); + // ensure the data model was queried appropriately for read/unread + Phake::verify(Data::$db)->articleMark(Data::$user->id, $read, (new Context)->editions([])); + Phake::verify(Data::$db)->articleMark(Data::$user->id, $read, (new Context)->editions($in[0])); + Phake::verify(Data::$db, Phake::times(0))->articleMark(Data::$user->id, $read, (new Context)->editions($in[1])); + Phake::verify(Data::$db)->articleMark(Data::$user->id, $read, (new Context)->editions($in[2])); + Phake::verify(Data::$db)->articleMark(Data::$user->id, $read, (new Context)->editions($in[3])); + Phake::verify(Data::$db)->articleMark(Data::$user->id, $unread, (new Context)->editions([])); + Phake::verify(Data::$db)->articleMark(Data::$user->id, $unread, (new Context)->editions($in[0])); + Phake::verify(Data::$db, Phake::times(0))->articleMark(Data::$user->id, $unread, (new Context)->editions($in[1])); + Phake::verify(Data::$db)->articleMark(Data::$user->id, $unread, (new Context)->editions($in[2])); + Phake::verify(Data::$db)->articleMark(Data::$user->id, $unread, (new Context)->editions($in[3])); + // ensure the data model was queried appropriately for star/unstar + Phake::verify(Data::$db)->articleMark(Data::$user->id, $star, (new Context)->articles([])); + Phake::verify(Data::$db)->articleMark(Data::$user->id, $star, (new Context)->articles($in[0])); + Phake::verify(Data::$db, Phake::times(0))->articleMark(Data::$user->id, $star, (new Context)->articles($in[1])); + Phake::verify(Data::$db)->articleMark(Data::$user->id, $star, (new Context)->articles($in[2])); + Phake::verify(Data::$db)->articleMark(Data::$user->id, $star, (new Context)->articles($in[3])); + Phake::verify(Data::$db)->articleMark(Data::$user->id, $unstar, (new Context)->articles([])); + Phake::verify(Data::$db)->articleMark(Data::$user->id, $unstar, (new Context)->articles($in[0])); + Phake::verify(Data::$db, Phake::times(0))->articleMark(Data::$user->id, $unstar, (new Context)->articles($in[1])); + Phake::verify(Data::$db)->articleMark(Data::$user->id, $unstar, (new Context)->articles($in[2])); + Phake::verify(Data::$db)->articleMark(Data::$user->id, $unstar, (new Context)->articles($in[3])); + } } \ No newline at end of file