diff --git a/build.xml b/build.xml
index 32f5a6cc..a2912bdb 100644
--- a/build.xml
+++ b/build.xml
@@ -8,6 +8,7 @@
Article content 1
', + 'guid' => '', + '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, + 'note' => "", + ], + [ + 'id' => 102, + 'url' => 'http://example.com/2', + 'title' => 'Article title 2', + 'subscription_title' => "Feed 11", + 'author' => 'J. King', + '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", + 'note' => "Note 2", + ], + ]; + // text from https://corrigeur.fr/lorem-ipsum-traduction-origine.php + protected $richContent = <<+ Pour vous faire mieux + connaitre d’ou\u{300} vient + l’erreur de ceux qui + bla\u{302}ment la + volupte\u{301}, et qui louent + en quelque sorte la douleur, + je vais entrer dans une + explication plus + e\u{301}tendue, et vous faire + voir tout ce qui a + e\u{301}te\u{301} dit + la\u{300}-dessus par + l’inventeur de la + ve\u{301}rite\u{301}, et, pour + ainsi dire, par l’architecte + de la vie heureuse. +
+ +LONG_STRING; + + protected function req($data) : Response { + return $this->h->dispatch(new Request("POST", "", json_encode($data))); + } + + protected function respGood($content = null, $seq = 0): Response { + return new Response(200, [ + 'seq' => $seq, + 'status' => 0, + 'content' => $content, + ]); + } + + protected function respErr(string $msg, $content = [], $seq = 0): Response { + $err = ['error' => $msg]; + return new Response(200, [ + 'seq' => $seq, + 'status' => 1, + 'content' => array_merge($err, $content, $err), + ]); + } + + protected function assertResponse(Response $exp, Response $act, string $text = null) { + $this->assertEquals($exp, $act, $text); + $this->assertSame($exp->payload, $act->payload, $text); + } + + public function setUp() { + $this->clearData(); + Arsse::$conf = new Conf(); + // create a mock user manager + Arsse::$user = Phake::mock(User::class); + Phake::when(Arsse::$user)->auth->thenReturn(true); + Phake::when(Arsse::$user)->rightsGet->thenReturn(100); + Arsse::$user->id = "john.doe@example.com"; + // create a mock database interface + Arsse::$db = Phake::mock(Database::class); + Phake::when(Arsse::$db)->begin->thenReturn(Phake::mock(Transaction::class)); + Phake::when(Arsse::$db)->sessionResume->thenThrow(new \JKingWeb\Arsse\User\ExceptionSession("invalid")); + Phake::when(Arsse::$db)->sessionResume("PriestsOfSyrinx")->thenReturn([ + 'id' => "PriestsOfSyrinx", + 'created' => "2000-01-01 00:00:00", + 'expires' => "2112-12-21 21:12:00", + 'user' => Arsse::$user->id, + ]); + $this->h = new REST\TinyTinyRSS\API(); + } + + public function tearDown() { + $this->clearData(); + } + + public function testHandleOptionsRequest() { + $exp = new Response(204, "", "", [ + "Allow: POST", + "Accept: application/json, text/json", + ]); + $this->assertResponse($exp, $this->h->dispatch(new Request("OPTIONS", ""))); + } + + public function testHandleInvalidData() { + $exp = $this->respErr("MALFORMED_INPUT", [], null); + $this->assertResponse($exp, $this->h->dispatch(new Request("POST", "", "This is not valid JSON data"))); + $this->assertResponse($exp, $this->h->dispatch(new Request("POST", "", ""))); // lack of data is also an error + } + + public function testLogIn() { + Phake::when(Arsse::$user)->auth(Arsse::$user->id, "superman")->thenReturn(false); + Phake::when(Arsse::$db)->sessionCreate->thenReturn("PriestsOfSyrinx")->thenReturn("SolarFederation"); + $data = [ + 'op' => "login", + 'user' => Arsse::$user->id, + 'password' => "secret", + ]; + $exp = $this->respGood(['session_id' => "PriestsOfSyrinx", 'api_level' => \JKingWeb\Arsse\REST\TinyTinyRSS\API::LEVEL]); + $this->assertResponse($exp, $this->req($data)); + $exp = $this->respGood(['session_id' => "SolarFederation", 'api_level' => \JKingWeb\Arsse\REST\TinyTinyRSS\API::LEVEL]); + $this->assertResponse($exp, $this->req($data)); + // test a failed log-in + $data['password'] = "superman"; + $exp = $this->respErr("LOGIN_ERROR"); + $this->assertResponse($exp, $this->req($data)); + // logging in should never try to resume a session + Phake::verify(Arsse::$db, Phake::times(0))->sessionResume($this->anything()); + } + + public function testHandleGenericError() { + Phake::when(Arsse::$user)->auth(Arsse::$user->id, $this->anything())->thenThrow(new \JKingWeb\Arsse\Db\ExceptionTimeout("general")); + $data = [ + 'op' => "login", + 'user' => Arsse::$user->id, + 'password' => "secret", + ]; + $exp = new Response(500); + $this->assertResponse($exp, $this->req($data)); + } + + public function testLogOut() { + Phake::when(Arsse::$db)->sessionDestroy->thenReturn(true); + $data = [ + 'op' => "logout", + 'sid' => "PriestsOfSyrinx", + ]; + $exp = $this->respGood(['status' => "OK"]); + $this->assertResponse($exp, $this->req($data)); + Phake::verify(Arsse::$db)->sessionDestroy(Arsse::$user->id, "PriestsOfSyrinx"); + } + + public function testValidateASession() { + $data = [ + 'op' => "isLoggedIn", + 'sid' => "PriestsOfSyrinx", + ]; + $exp = $this->respGood(['status' => true]); + $this->assertResponse($exp, $this->req($data)); + $data['sid'] = "SolarFederation"; + $exp = $this->respErr("NOT_LOGGED_IN"); + $this->assertResponse($exp, $this->req($data)); + } + + public function testHandleUnknownMethods() { + $exp = $this->respErr("UNKNOWN_METHOD", ['method' => "thisMethodDoesNotExist"]); + $data = [ + 'op' => "thisMethodDoesNotExist", + 'sid' => "PriestsOfSyrinx", + ]; + $this->assertResponse($exp, $this->req($data)); + } + + public function testHandleMixedCaseMethods() { + $data = [ + 'op' => "isLoggedIn", + 'sid' => "PriestsOfSyrinx", + ]; + $exp = $this->respGood(['status' => true]); + $this->assertResponse($exp, $this->req($data)); + $data['op'] = "isloggedin"; + $this->assertResponse($exp, $this->req($data)); + $data['op'] = "ISLOGGEDIN"; + $this->assertResponse($exp, $this->req($data)); + $data['op'] = "iSlOgGeDiN"; + $this->assertResponse($exp, $this->req($data)); + } + + public function testRetrieveServerVersion() { + $data = [ + 'op' => "getVersion", + 'sid' => "PriestsOfSyrinx", + ]; + $exp = $this->respGood([ + 'version' => \JKingWeb\Arsse\REST\TinyTinyRSS\API::VERSION, + 'arsse_version' => Arsse::VERSION, + ]); + $this->assertResponse($exp, $this->req($data)); + } + + public function testRetrieveProtocolLevel() { + $data = [ + 'op' => "getApiLevel", + 'sid' => "PriestsOfSyrinx", + ]; + $exp = $this->respGood(['level' => \JKingWeb\Arsse\REST\TinyTinyRSS\API::LEVEL]); + $this->assertResponse($exp, $this->req($data)); + } + + public function testAddACategory() { + $in = [ + ['op' => "addCategory", 'sid' => "PriestsOfSyrinx", 'caption' => "Software"], + ['op' => "addCategory", 'sid' => "PriestsOfSyrinx", 'caption' => "Hardware", 'parent_id' => 1], + ['op' => "addCategory", 'sid' => "PriestsOfSyrinx", 'caption' => "Hardware", 'parent_id' => 2112], + ['op' => "addCategory", 'sid' => "PriestsOfSyrinx"], + ['op' => "addCategory", 'sid' => "PriestsOfSyrinx", 'caption' => ""], + ['op' => "addCategory", 'sid' => "PriestsOfSyrinx", 'caption' => " "], + ]; + $db = [ + ['name' => "Software", 'parent' => null], + ['name' => "Hardware", 'parent' => 1], + ['name' => "Hardware", 'parent' => 2112], + ]; + $out = [ + ['id' => 2, 'name' => "Software", 'parent' => null], + ['id' => 3, 'name' => "Hardware", 'parent' => 1], + ['id' => 1, 'name' => "Politics", 'parent' => null], + ]; + // set of various mocks for testing + Phake::when(Arsse::$db)->folderAdd(Arsse::$user->id, $db[0])->thenReturn(2)->thenThrow(new ExceptionInput("constraintViolation")); // error on the second call + Phake::when(Arsse::$db)->folderAdd(Arsse::$user->id, $db[1])->thenReturn(3)->thenThrow(new ExceptionInput("constraintViolation")); // error on the second call + Phake::when(Arsse::$db)->folderList(Arsse::$user->id, null, false)->thenReturn(new Result([$out[0], $out[2]])); + Phake::when(Arsse::$db)->folderList(Arsse::$user->id, 1, false)->thenReturn(new Result([$out[1]])); + // set up mocks that produce errors + Phake::when(Arsse::$db)->folderAdd(Arsse::$user->id, $db[2])->thenThrow(new ExceptionInput("idMissing")); // parent folder does not exist + Phake::when(Arsse::$db)->folderAdd(Arsse::$user->id, [])->thenThrow(new ExceptionInput("missing")); + Phake::when(Arsse::$db)->folderAdd(Arsse::$user->id, ['name' => "", 'parent' => null])->thenThrow(new ExceptionInput("missing")); + Phake::when(Arsse::$db)->folderAdd(Arsse::$user->id, ['name' => " ", 'parent' => null])->thenThrow(new ExceptionInput("whitespace")); + // correctly add two folders + $exp = $this->respGood("2"); + $this->assertResponse($exp, $this->req($in[0])); + $exp = $this->respGood("3"); + $this->assertResponse($exp, $this->req($in[1])); + // attempt to add the two folders again + $exp = $this->respGood("2"); + $this->assertResponse($exp, $this->req($in[0])); + $exp = $this->respGood("3"); + $this->assertResponse($exp, $this->req($in[1])); + Phake::verify(Arsse::$db)->folderList(Arsse::$user->id, null, false); + Phake::verify(Arsse::$db)->folderList(Arsse::$user->id, 1, false); + // add a folder to a missing parent (silently fails) + $exp = $this->respGood(false); + $this->assertResponse($exp, $this->req($in[2])); + // add some invalid folders + $exp = $this->respErr("INCORRECT_USAGE"); + $this->assertResponse($exp, $this->req($in[3])); + $this->assertResponse($exp, $this->req($in[4])); + $this->assertResponse($exp, $this->req($in[5])); + } + + public function testRemoveACategory() { + $in = [ + ['op' => "removeCategory", 'sid' => "PriestsOfSyrinx", 'category_id' => 42], + ['op' => "removeCategory", 'sid' => "PriestsOfSyrinx", 'category_id' => 2112], + ['op' => "removeCategory", 'sid' => "PriestsOfSyrinx", 'category_id' => -1], + ]; + Phake::when(Arsse::$db)->folderRemove(Arsse::$user->id, $this->anything())->thenThrow(new ExceptionInput("subjectMissing")); + Phake::when(Arsse::$db)->folderRemove(Arsse::$user->id, 42)->thenReturn(true)->thenThrow(new ExceptionInput("subjectMissing")); + // succefully delete a folder + $exp = $this->respGood(); + $this->assertResponse($exp, $this->req($in[0])); + // try deleting it again (this should silently fail) + $exp = $this->respGood(); + $this->assertResponse($exp, $this->req($in[0])); + // delete a folder which does not exist (this should also silently fail) + $exp = $this->respGood(); + $this->assertResponse($exp, $this->req($in[1])); + // delete an invalid folder (causes an error) + $exp = $this->respErr("INCORRECT_USAGE"); + $this->assertResponse($exp, $this->req($in[2])); + Phake::verify(Arsse::$db, Phake::times(3))->folderRemove(Arsse::$user->id, $this->anything()); + } + + public function testMoveACategory() { + $in = [ + ['op' => "moveCategory", 'sid' => "PriestsOfSyrinx", 'category_id' => 42, 'parent_id' => 1], + ['op' => "moveCategory", 'sid' => "PriestsOfSyrinx", 'category_id' => 2112, 'parent_id' => 2], + ['op' => "moveCategory", 'sid' => "PriestsOfSyrinx", 'category_id' => 42, 'parent_id' => 0], + ['op' => "moveCategory", 'sid' => "PriestsOfSyrinx", 'category_id' => 42, 'parent_id' => 47], + ['op' => "moveCategory", 'sid' => "PriestsOfSyrinx", 'category_id' => -1, 'parent_id' => 1], + ['op' => "moveCategory", 'sid' => "PriestsOfSyrinx", 'category_id' => 42, 'parent_id' => -1], + ['op' => "moveCategory", 'sid' => "PriestsOfSyrinx", 'category_id' => 42], + ['op' => "moveCategory", 'sid' => "PriestsOfSyrinx", 'parent_id' => -1], + ['op' => "moveCategory", 'sid' => "PriestsOfSyrinx"], + ]; + $db = [ + [Arsse::$user->id, 42, ['parent' => 1]], + [Arsse::$user->id, 2112, ['parent' => 2]], + [Arsse::$user->id, 42, ['parent' => 0]], + [Arsse::$user->id, 42, ['parent' => 47]], + [Arsse::$user->id, -1, ['parent' => 1]], + [Arsse::$user->id, 42, ['parent' => -1]], + [Arsse::$user->id, 42, ['parent' => 0]], + [Arsse::$user->id, 0, ['parent' => -1]], + [Arsse::$user->id, 0, ['parent' => 0]], + ]; + Phake::when(Arsse::$db)->folderPropertiesSet(...$db[0])->thenReturn(true); + Phake::when(Arsse::$db)->folderPropertiesSet(...$db[1])->thenThrow(new ExceptionInput("subjectMissing")); + Phake::when(Arsse::$db)->folderPropertiesSet(...$db[2])->thenThrow(new ExceptionInput("constraintViolation")); + Phake::when(Arsse::$db)->folderPropertiesSet(...$db[3])->thenThrow(new ExceptionInput("idMissing")); + Phake::when(Arsse::$db)->folderPropertiesSet(...$db[4])->thenThrow(new ExceptionInput("typeViolation")); + Phake::when(Arsse::$db)->folderPropertiesSet(...$db[5])->thenThrow(new ExceptionInput("typeViolation")); + Phake::when(Arsse::$db)->folderPropertiesSet(...$db[6])->thenThrow(new ExceptionInput("constraintViolation")); + Phake::when(Arsse::$db)->folderPropertiesSet(...$db[7])->thenThrow(new ExceptionInput("typeViolation")); + Phake::when(Arsse::$db)->folderPropertiesSet(...$db[8])->thenThrow(new ExceptionInput("typeViolation")); + // succefully move a folder + $exp = $this->respGood(); + $this->assertResponse($exp, $this->req($in[0])); + // move a folder which does not exist (this should silently fail) + $exp = $this->respGood(); + $this->assertResponse($exp, $this->req($in[1])); + // move a folder causing a duplication (this should also silently fail) + $exp = $this->respGood(); + $this->assertResponse($exp, $this->req($in[2])); + $this->assertResponse($exp, $this->req($in[3])); + $this->assertResponse($exp, $this->req($in[6])); + // all the rest should cause errors + $exp = $this->respErr("INCORRECT_USAGE"); + $this->assertResponse($exp, $this->req($in[4])); + $this->assertResponse($exp, $this->req($in[5])); + $this->assertResponse($exp, $this->req($in[7])); + $this->assertResponse($exp, $this->req($in[8])); + Phake::verify(Arsse::$db, Phake::times(5))->folderPropertiesSet(Arsse::$user->id, $this->anything(), $this->anything()); + } + + public function testRenameACategory() { + $in = [ + ['op' => "renameCategory", 'sid' => "PriestsOfSyrinx", 'category_id' => 42, 'caption' => "Ook"], + ['op' => "renameCategory", 'sid' => "PriestsOfSyrinx", 'category_id' => 2112, 'caption' => "Eek"], + ['op' => "renameCategory", 'sid' => "PriestsOfSyrinx", 'category_id' => 42, 'caption' => "Eek"], + ['op' => "renameCategory", 'sid' => "PriestsOfSyrinx", 'category_id' => 42, 'caption' => ""], + ['op' => "renameCategory", 'sid' => "PriestsOfSyrinx", 'category_id' => 42, 'caption' => " "], + ['op' => "renameCategory", 'sid' => "PriestsOfSyrinx", 'category_id' => -1, 'caption' => "Ook"], + ['op' => "renameCategory", 'sid' => "PriestsOfSyrinx", 'category_id' => 42], + ['op' => "renameCategory", 'sid' => "PriestsOfSyrinx", 'caption' => "Ook"], + ['op' => "renameCategory", 'sid' => "PriestsOfSyrinx"], + ]; + $db = [ + [Arsse::$user->id, 42, ['name' => "Ook"]], + [Arsse::$user->id, 2112, ['name' => "Eek"]], + [Arsse::$user->id, 42, ['name' => "Eek"]], + ]; + Phake::when(Arsse::$db)->folderPropertiesSet(...$db[0])->thenReturn(true); + Phake::when(Arsse::$db)->folderPropertiesSet(...$db[1])->thenThrow(new ExceptionInput("subjectMissing")); + Phake::when(Arsse::$db)->folderPropertiesSet(...$db[2])->thenThrow(new ExceptionInput("constraintViolation")); + // succefully rename a folder + $exp = $this->respGood(); + $this->assertResponse($exp, $this->req($in[0])); + // rename a folder which does not exist (this should silently fail) + $exp = $this->respGood(); + $this->assertResponse($exp, $this->req($in[1])); + // rename a folder causing a duplication (this should also silently fail) + $exp = $this->respGood(); + $this->assertResponse($exp, $this->req($in[2])); + // all the rest should cause errors + $exp = $this->respErr("INCORRECT_USAGE"); + $this->assertResponse($exp, $this->req($in[3])); + $this->assertResponse($exp, $this->req($in[4])); + $this->assertResponse($exp, $this->req($in[5])); + $this->assertResponse($exp, $this->req($in[6])); + $this->assertResponse($exp, $this->req($in[7])); + $this->assertResponse($exp, $this->req($in[8])); + Phake::verify(Arsse::$db, Phake::times(3))->folderPropertiesSet(Arsse::$user->id, $this->anything(), $this->anything()); + } + + public function testAddASubscription() { + $in = [ + ['op' => "subscribeToFeed", 'sid' => "PriestsOfSyrinx", 'feed_url' => "http://example.com/0"], + ['op' => "subscribeToFeed", 'sid' => "PriestsOfSyrinx", 'feed_url' => "http://example.com/1", 'category_id' => 42], + ['op' => "subscribeToFeed", 'sid' => "PriestsOfSyrinx", 'feed_url' => "http://example.com/2", 'category_id' => 2112], + ['op' => "subscribeToFeed", 'sid' => "PriestsOfSyrinx", 'feed_url' => "http://example.com/3"], + ['op' => "subscribeToFeed", 'sid' => "PriestsOfSyrinx", 'feed_url' => "http://localhost:8000/Feed/Discovery/Valid"], + ['op' => "subscribeToFeed", 'sid' => "PriestsOfSyrinx", 'feed_url' => "http://localhost:8000/Feed/Discovery/Invalid"], + ['op' => "subscribeToFeed", 'sid' => "PriestsOfSyrinx", 'feed_url' => "http://example.com/6"], + ['op' => "subscribeToFeed", 'sid' => "PriestsOfSyrinx", 'feed_url' => "http://example.com/7"], + ['op' => "subscribeToFeed", 'sid' => "PriestsOfSyrinx", 'feed_url' => "http://example.com/8", 'category_id' => 47], + ['op' => "subscribeToFeed", 'sid' => "PriestsOfSyrinx", 'feed_url' => "http://example.com/9", 'category_id' => 1], + // these don't even query the database as the input is syntactically invalid + ['op' => "subscribeToFeed", 'sid' => "PriestsOfSyrinx"], + ['op' => "subscribeToFeed", 'sid' => "PriestsOfSyrinx", 'feed_url' => "http://example.com/", 'login' => []], + ['op' => "subscribeToFeed", 'sid' => "PriestsOfSyrinx", 'feed_url' => "http://example.com/", 'login' => "", 'password' => []], + ['op' => "subscribeToFeed", 'sid' => "PriestsOfSyrinx", 'feed_url' => "http://example.com/", 'category_id' => -1], + ]; + $db = [ + [Arsse::$user->id, "http://example.com/0", "", ""], + [Arsse::$user->id, "http://example.com/1", "", ""], + [Arsse::$user->id, "http://example.com/2", "", ""], + [Arsse::$user->id, "http://example.com/3", "", ""], + [Arsse::$user->id, "http://localhost:8000/Feed/Discovery/Valid", "", ""], + [Arsse::$user->id, "http://localhost:8000/Feed/Discovery/Invalid", "", ""], + [Arsse::$user->id, "http://example.com/6", "", ""], + [Arsse::$user->id, "http://example.com/7", "", ""], + [Arsse::$user->id, "http://example.com/8", "", ""], + [Arsse::$user->id, "http://example.com/9", "", ""], + ]; + $out = [ + ['code' => 1, 'feed_id' => 2], + ['code' => 5, 'message' => (new \JKingWeb\Arsse\Feed\Exception("http://example.com/1", new \PicoFeed\Client\UnauthorizedException()))->getMessage()], + ['code' => 1, 'feed_id' => 0], + ['code' => 0, 'feed_id' => 3], + ['code' => 0, 'feed_id' => 1], + ['code' => 3, 'message' => (new \JKingWeb\Arsse\Feed\Exception("http://localhost:8000/Feed/Discovery/Invalid", new \PicoFeed\Reader\SubscriptionNotFoundException()))->getMessage()], + ['code' => 2, 'message' => (new \JKingWeb\Arsse\Feed\Exception("http://example.com/6", new \PicoFeed\Client\InvalidUrlException()))->getMessage()], + ['code' => 6, 'message' => (new \JKingWeb\Arsse\Feed\Exception("http://example.com/7", new \PicoFeed\Parser\MalformedXmlException()))->getMessage()], + ['code' => 1, 'feed_id' => 4], + ['code' => 0, 'feed_id' => 4], + ]; + $list = [ + ['id' => 1, 'url' => "http://localhost:8000/Feed/Discovery/Feed"], + ['id' => 2, 'url' => "http://example.com/0"], + ['id' => 3, 'url' => "http://example.com/3"], + ['id' => 4, 'url' => "http://example.com/9"], + ]; + Phake::when(Arsse::$db)->subscriptionAdd(...$db[0])->thenReturn(2); + Phake::when(Arsse::$db)->subscriptionAdd(...$db[1])->thenThrow(new \JKingWeb\Arsse\Feed\Exception("http://example.com/1", new \PicoFeed\Client\UnauthorizedException())); + Phake::when(Arsse::$db)->subscriptionAdd(...$db[2])->thenReturn(2); + Phake::when(Arsse::$db)->subscriptionAdd(...$db[3])->thenThrow(new ExceptionInput("constraintViolation")); + Phake::when(Arsse::$db)->subscriptionAdd(...$db[4])->thenThrow(new ExceptionInput("constraintViolation")); + Phake::when(Arsse::$db)->subscriptionAdd(...$db[5])->thenThrow(new ExceptionInput("constraintViolation")); + Phake::when(Arsse::$db)->subscriptionAdd(...$db[6])->thenThrow(new \JKingWeb\Arsse\Feed\Exception("http://example.com/6", new \PicoFeed\Client\InvalidUrlException())); + Phake::when(Arsse::$db)->subscriptionAdd(...$db[7])->thenThrow(new \JKingWeb\Arsse\Feed\Exception("http://example.com/7", new \PicoFeed\Parser\MalformedXmlException())); + Phake::when(Arsse::$db)->subscriptionAdd(...$db[8])->thenReturn(4); + Phake::when(Arsse::$db)->subscriptionAdd(...$db[9])->thenThrow(new ExceptionInput("constraintViolation")); + Phake::when(Arsse::$db)->folderPropertiesGet(Arsse::$user->id, 42)->thenReturn(['id' => 42]); + Phake::when(Arsse::$db)->folderPropertiesGet(Arsse::$user->id, 47)->thenReturn(['id' => 47]); + Phake::when(Arsse::$db)->folderPropertiesGet(Arsse::$user->id, 2112)->thenThrow(new ExceptionInput("subjectMissing")); + Phake::when(Arsse::$db)->subscriptionPropertiesSet(Arsse::$user->id, $this->anything(), $this->anything())->thenReturn(true); + Phake::when(Arsse::$db)->subscriptionPropertiesSet(Arsse::$user->id, 4, $this->anything())->thenThrow(new ExceptionInput("idMissing")); + Phake::when(Arsse::$db)->subscriptionList(Arsse::$user->id)->thenReturn(new Result($list)); + for ($a = 0; $a < (sizeof($in) - 4); $a++) { + $exp = $this->respGood($out[$a]); + $this->assertResponse($exp, $this->req($in[$a]), "Failed test $a"); + } + $exp = $this->respErr("INCORRECT_USAGE"); + for ($a = (sizeof($in) - 4); $a < sizeof($in); $a++) { + $this->assertResponse($exp, $this->req($in[$a]), "Failed test $a"); + } + Phake::verify(Arsse::$db, Phake::times(0))->subscriptionPropertiesSet(Arsse::$user->id, 4, ['folder' => 1]); + } + + public function testRemoveASubscription() { + $in = [ + ['op' => "unsubscribeFeed", 'sid' => "PriestsOfSyrinx", 'feed_id' => 42], + ['op' => "unsubscribeFeed", 'sid' => "PriestsOfSyrinx", 'feed_id' => 2112], + ['op' => "unsubscribeFeed", 'sid' => "PriestsOfSyrinx", 'feed_id' => -1], + ['op' => "unsubscribeFeed", 'sid' => "PriestsOfSyrinx"], + ]; + Phake::when(Arsse::$db)->subscriptionRemove(Arsse::$user->id, $this->anything())->thenThrow(new ExceptionInput("typeViolation")); + Phake::when(Arsse::$db)->subscriptionRemove(Arsse::$user->id, 2112)->thenThrow(new ExceptionInput("subjectMissing")); + Phake::when(Arsse::$db)->subscriptionRemove(Arsse::$user->id, 42)->thenReturn(true)->thenThrow(new ExceptionInput("subjectMissing")); + // succefully delete a folder + $exp = $this->respGood(['status' => "OK"]); + $this->assertResponse($exp, $this->req($in[0])); + // try deleting it again (this should noisily fail, as should everything else) + $exp = $this->respErr("FEED_NOT_FOUND"); + $this->assertResponse($exp, $this->req($in[0])); + $this->assertResponse($exp, $this->req($in[1])); + $this->assertResponse($exp, $this->req($in[2])); + $this->assertResponse($exp, $this->req($in[3])); + Phake::verify(Arsse::$db, Phake::times(5))->subscriptionRemove(Arsse::$user->id, $this->anything()); + } + + public function testMoveASubscription() { + $in = [ + ['op' => "moveFeed", 'sid' => "PriestsOfSyrinx", 'feed_id' => 42, 'category_id' => 1], + ['op' => "moveFeed", 'sid' => "PriestsOfSyrinx", 'feed_id' => 2112, 'category_id' => 2], + ['op' => "moveFeed", 'sid' => "PriestsOfSyrinx", 'feed_id' => 42, 'category_id' => 0], + ['op' => "moveFeed", 'sid' => "PriestsOfSyrinx", 'feed_id' => 42, 'category_id' => 47], + ['op' => "moveFeed", 'sid' => "PriestsOfSyrinx", 'feed_id' => -1, 'category_id' => 1], + ['op' => "moveFeed", 'sid' => "PriestsOfSyrinx", 'feed_id' => 42, 'category_id' => -1], + ['op' => "moveFeed", 'sid' => "PriestsOfSyrinx", 'feed_id' => 42], + ['op' => "moveFeed", 'sid' => "PriestsOfSyrinx", 'category_id' => -1], + ['op' => "moveFeed", 'sid' => "PriestsOfSyrinx"], + ]; + $db = [ + [Arsse::$user->id, 42, ['folder' => 1]], + [Arsse::$user->id, 2112, ['folder' => 2]], + [Arsse::$user->id, 42, ['folder' => 0]], + [Arsse::$user->id, 42, ['folder' => 47]], + ]; + Phake::when(Arsse::$db)->subscriptionPropertiesSet(...$db[0])->thenReturn(true); + Phake::when(Arsse::$db)->subscriptionPropertiesSet(...$db[1])->thenThrow(new ExceptionInput("subjectMissing")); + Phake::when(Arsse::$db)->subscriptionPropertiesSet(...$db[2])->thenThrow(new ExceptionInput("constraintViolation")); + Phake::when(Arsse::$db)->subscriptionPropertiesSet(...$db[3])->thenThrow(new ExceptionInput("constraintViolation")); + // succefully move a subscription + $exp = $this->respGood(); + $this->assertResponse($exp, $this->req($in[0])); + // move a subscription which does not exist (this should silently fail) + $exp = $this->respGood(); + $this->assertResponse($exp, $this->req($in[1])); + // move a subscription causing a duplication (this should also silently fail) + $exp = $this->respGood(); + $this->assertResponse($exp, $this->req($in[2])); + $this->assertResponse($exp, $this->req($in[3])); + // all the rest should cause errors + $exp = $this->respErr("INCORRECT_USAGE"); + $this->assertResponse($exp, $this->req($in[4])); + $this->assertResponse($exp, $this->req($in[5])); + $this->assertResponse($exp, $this->req($in[6])); + $this->assertResponse($exp, $this->req($in[7])); + $this->assertResponse($exp, $this->req($in[8])); + Phake::verify(Arsse::$db, Phake::times(4))->subscriptionPropertiesSet(Arsse::$user->id, $this->anything(), $this->anything()); + } + + public function testRenameASubscription() { + $in = [ + ['op' => "renameFeed", 'sid' => "PriestsOfSyrinx", 'feed_id' => 42, 'caption' => "Ook"], + ['op' => "renameFeed", 'sid' => "PriestsOfSyrinx", 'feed_id' => 2112, 'caption' => "Eek"], + ['op' => "renameFeed", 'sid' => "PriestsOfSyrinx", 'feed_id' => 42, 'caption' => "Eek"], + ['op' => "renameFeed", 'sid' => "PriestsOfSyrinx", 'feed_id' => 42, 'caption' => ""], + ['op' => "renameFeed", 'sid' => "PriestsOfSyrinx", 'feed_id' => 42, 'caption' => " "], + ['op' => "renameFeed", 'sid' => "PriestsOfSyrinx", 'feed_id' => -1, 'caption' => "Ook"], + ['op' => "renameFeed", 'sid' => "PriestsOfSyrinx", 'feed_id' => 42], + ['op' => "renameFeed", 'sid' => "PriestsOfSyrinx", 'caption' => "Ook"], + ['op' => "renameFeed", 'sid' => "PriestsOfSyrinx"], + ]; + $db = [ + [Arsse::$user->id, 42, ['name' => "Ook"]], + [Arsse::$user->id, 2112, ['name' => "Eek"]], + [Arsse::$user->id, 42, ['name' => "Eek"]], + ]; + Phake::when(Arsse::$db)->subscriptionPropertiesSet(...$db[0])->thenReturn(true); + Phake::when(Arsse::$db)->subscriptionPropertiesSet(...$db[1])->thenThrow(new ExceptionInput("subjectMissing")); + Phake::when(Arsse::$db)->subscriptionPropertiesSet(...$db[2])->thenThrow(new ExceptionInput("constraintViolation")); + // succefully rename a subscription + $exp = $this->respGood(); + $this->assertResponse($exp, $this->req($in[0])); + // rename a subscription which does not exist (this should silently fail) + $exp = $this->respGood(); + $this->assertResponse($exp, $this->req($in[1])); + // rename a subscription causing a duplication (this should also silently fail) + $exp = $this->respGood(); + $this->assertResponse($exp, $this->req($in[2])); + // all the rest should cause errors + $exp = $this->respErr("INCORRECT_USAGE"); + $this->assertResponse($exp, $this->req($in[3])); + $this->assertResponse($exp, $this->req($in[4])); + $this->assertResponse($exp, $this->req($in[5])); + $this->assertResponse($exp, $this->req($in[6])); + $this->assertResponse($exp, $this->req($in[7])); + $this->assertResponse($exp, $this->req($in[8])); + Phake::verify(Arsse::$db, Phake::times(3))->subscriptionPropertiesSet(Arsse::$user->id, $this->anything(), $this->anything()); + } + + public function testRetrieveTheGlobalUnreadCount() { + $in = ['op' => "getUnread", 'sid' => "PriestsOfSyrinx"]; + Phake::when(Arsse::$db)->subscriptionList(Arsse::$user->id)->thenReturn(new Result([ + ['id' => 1, 'unread' => 2112], + ['id' => 2, 'unread' => 42], + ['id' => 3, 'unread' => 47], + ])); + $exp = $this->respGood(['unread' => (string) (2112 + 42 + 47)]); + $this->assertResponse($exp, $this->req($in)); + } + + public function testRetrieveTheServerConfiguration() { + $in = ['op' => "getConfig", 'sid' => "PriestsOfSyrinx"]; + $interval = Service::interval(); + $valid = (new \DateTimeImmutable("now", new \DateTimezone("UTC")))->sub($interval); + $invalid = $valid->sub($interval)->sub($interval); + Phake::when(Arsse::$db)->metaGet("service_last_checkin")->thenReturn(Date::transform($valid, "sql"))->thenReturn(Date::transform($invalid, "sql")); + Phake::when(Arsse::$db)->subscriptionCount(Arsse::$user->id)->thenReturn(12)->thenReturn(2); + $exp = [ + ['icons_dir' => "feed-icons", 'icons_url' => "feed-icons", 'daemon_is_running' => true, 'num_feeds' => 12], + ['icons_dir' => "feed-icons", 'icons_url' => "feed-icons", 'daemon_is_running' => false, 'num_feeds' => 2], + ]; + $this->assertResponse($this->respGood($exp[0]), $this->req($in)); + $this->assertResponse($this->respGood($exp[1]), $this->req($in)); + } + + public function testUpdateAFeed() { + $in = [ + ['op' => "updateFeed", 'sid' => "PriestsOfSyrinx", 'feed_id' => 1], + ['op' => "updateFeed", 'sid' => "PriestsOfSyrinx", 'feed_id' => 2], + ['op' => "updateFeed", 'sid' => "PriestsOfSyrinx", 'feed_id' => -1], + ['op' => "updateFeed", 'sid' => "PriestsOfSyrinx"], + ]; + Phake::when(Arsse::$db)->feedUpdate(11)->thenReturn(true); + Phake::when(Arsse::$db)->subscriptionPropertiesGet(Arsse::$user->id, 1)->thenReturn(['id' => 1, 'feed' => 11]); + Phake::when(Arsse::$db)->subscriptionPropertiesGet(Arsse::$user->id, 2)->thenThrow(new ExceptionInput("subjectMissing")); + $exp = $this->respGood(['status' => "OK"]); + $this->assertResponse($exp, $this->req($in[0])); + Phake::verify(Arsse::$db)->feedUpdate(11); + $exp = $this->respErr("FEED_NOT_FOUND"); + $this->assertResponse($exp, $this->req($in[1])); + $exp = $this->respErr("INCORRECT_USAGE"); + $this->assertResponse($exp, $this->req($in[2])); + $this->assertResponse($exp, $this->req($in[3])); + } + + public function testAddALabel() { + $in = [ + ['op' => "addLabel", 'sid' => "PriestsOfSyrinx", 'caption' => "Software"], + ['op' => "addLabel", 'sid' => "PriestsOfSyrinx", 'caption' => "Hardware",], + ['op' => "addLabel", 'sid' => "PriestsOfSyrinx"], + ['op' => "addLabel", 'sid' => "PriestsOfSyrinx", 'caption' => ""], + ['op' => "addLabel", 'sid' => "PriestsOfSyrinx", 'caption' => " "], + ]; + $db = [ + ['name' => "Software"], + ['name' => "Hardware"], + ]; + $out = [ + ['id' => 2, 'name' => "Software"], + ['id' => 3, 'name' => "Hardware"], + ['id' => 1, 'name' => "Politics"], + ]; + // set of various mocks for testing + Phake::when(Arsse::$db)->labelAdd(Arsse::$user->id, $db[0])->thenReturn(2)->thenThrow(new ExceptionInput("constraintViolation")); // error on the second call + Phake::when(Arsse::$db)->labelAdd(Arsse::$user->id, $db[1])->thenReturn(3)->thenThrow(new ExceptionInput("constraintViolation")); // error on the second call + Phake::when(Arsse::$db)->labelPropertiesGet(Arsse::$user->id, "Software", true)->thenReturn($out[0]); + Phake::when(Arsse::$db)->labelPropertiesGet(Arsse::$user->id, "Hardware", true)->thenReturn($out[1]); + // set up mocks that produce errors + Phake::when(Arsse::$db)->labelAdd(Arsse::$user->id, [])->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")); + // correctly add two labels + $exp = $this->respGood((-1 * API::LABEL_OFFSET) - 2); + $this->assertResponse($exp, $this->req($in[0])); + $exp = $this->respGood((-1 * API::LABEL_OFFSET) - 3); + $this->assertResponse($exp, $this->req($in[1])); + // attempt to add the two labels again + $exp = $this->respGood((-1 * API::LABEL_OFFSET) - 2); + $this->assertResponse($exp, $this->req($in[0])); + $exp = $this->respGood((-1 * API::LABEL_OFFSET) - 3); + $this->assertResponse($exp, $this->req($in[1])); + Phake::verify(Arsse::$db)->labelPropertiesGet(Arsse::$user->id, "Software", true); + Phake::verify(Arsse::$db)->labelPropertiesGet(Arsse::$user->id, "Hardware", true); + // add some invalid labels + $exp = $this->respErr("INCORRECT_USAGE"); + $this->assertResponse($exp, $this->req($in[2])); + $this->assertResponse($exp, $this->req($in[3])); + $this->assertResponse($exp, $this->req($in[4])); + } + + public function testRemoveALabel() { + $in = [ + ['op' => "removeLabel", 'sid' => "PriestsOfSyrinx", 'label_id' => -1042], + ['op' => "removeLabel", 'sid' => "PriestsOfSyrinx", 'label_id' => -2112], + ['op' => "removeLabel", 'sid' => "PriestsOfSyrinx", 'label_id' => 1], + ['op' => "removeLabel", 'sid' => "PriestsOfSyrinx", 'label_id' => 0], + ['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, 18)->thenReturn(true)->thenThrow(new ExceptionInput("subjectMissing")); + // succefully delete a label + $exp = $this->respGood(); + $this->assertResponse($exp, $this->req($in[0])); + // try deleting it again (this should silently fail) + $exp = $this->respGood(); + $this->assertResponse($exp, $this->req($in[0])); + // delete a label which does not exist (this should also silently fail) + $exp = $this->respGood(); + $this->assertResponse($exp, $this->req($in[1])); + // delete some invalid labels (causes an error) + $exp = $this->respErr("INCORRECT_USAGE"); + $this->assertResponse($exp, $this->req($in[2])); + $this->assertResponse($exp, $this->req($in[3])); + $this->assertResponse($exp, $this->req($in[4])); + Phake::verify(Arsse::$db, Phake::times(2))->labelRemove(Arsse::$user->id, 18); + Phake::verify(Arsse::$db)->labelRemove(Arsse::$user->id, 1088); + } + + public function testRenameALabel() { + $in = [ + ['op' => "renameLabel", 'sid' => "PriestsOfSyrinx", 'label_id' => -1042, 'caption' => "Ook"], + ['op' => "renameLabel", 'sid' => "PriestsOfSyrinx", 'label_id' => -2112, 'caption' => "Eek"], + ['op' => "renameLabel", 'sid' => "PriestsOfSyrinx", 'label_id' => -1042, 'caption' => "Eek"], + ['op' => "renameLabel", 'sid' => "PriestsOfSyrinx", 'label_id' => -1042, 'caption' => ""], + ['op' => "renameLabel", 'sid' => "PriestsOfSyrinx", 'label_id' => -1042, 'caption' => " "], + ['op' => "renameLabel", 'sid' => "PriestsOfSyrinx", 'label_id' => -1042], + ['op' => "renameLabel", 'sid' => "PriestsOfSyrinx", 'label_id' => -1, 'caption' => "Ook"], + ['op' => "renameLabel", 'sid' => "PriestsOfSyrinx", 'caption' => "Ook"], + ['op' => "renameLabel", 'sid' => "PriestsOfSyrinx"], + ]; + $db = [ + [Arsse::$user->id, 18, ['name' => "Ook"]], + [Arsse::$user->id, 1088, ['name' => "Eek"]], + [Arsse::$user->id, 18, ['name' => "Eek"]], + [Arsse::$user->id, 18, ['name' => ""]], + [Arsse::$user->id, 18, ['name' => " "]], + [Arsse::$user->id, 18, ['name' => ""]], + ]; + 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[2])->thenThrow(new ExceptionInput("constraintViolation")); + Phake::when(Arsse::$db)->labelPropertiesSet(...$db[3])->thenThrow(new ExceptionInput("typeViolation")); + Phake::when(Arsse::$db)->labelPropertiesSet(...$db[4])->thenThrow(new ExceptionInput("typeViolation")); + Phake::when(Arsse::$db)->labelPropertiesSet(...$db[5])->thenThrow(new ExceptionInput("typeViolation")); + // succefully rename a label + $exp = $this->respGood(); + $this->assertResponse($exp, $this->req($in[0])); + // rename a label which does not exist (this should silently fail) + $exp = $this->respGood(); + $this->assertResponse($exp, $this->req($in[1])); + // rename a label causing a duplication (this should also silently fail) + $exp = $this->respGood(); + $this->assertResponse($exp, $this->req($in[2])); + // all the rest should cause errors + $exp = $this->respErr("INCORRECT_USAGE"); + $this->assertResponse($exp, $this->req($in[3])); + $this->assertResponse($exp, $this->req($in[4])); + $this->assertResponse($exp, $this->req($in[5])); + $this->assertResponse($exp, $this->req($in[6])); + $this->assertResponse($exp, $this->req($in[7])); + $this->assertResponse($exp, $this->req($in[8])); + Phake::verify(Arsse::$db, Phake::times(6))->labelPropertiesSet(Arsse::$user->id, $this->anything(), $this->anything()); + } + + public function testRetrieveCategoryLists() { + $in = [ + ['op' => "getCategories", 'sid' => "PriestsOfSyrinx", 'include_empty' => true], + ['op' => "getCategories", 'sid' => "PriestsOfSyrinx"], + ['op' => "getCategories", 'sid' => "PriestsOfSyrinx", 'unread_only' => true], + ['op' => "getCategories", 'sid' => "PriestsOfSyrinx", 'enable_nested' => true, 'include_empty' => true], + ['op' => "getCategories", 'sid' => "PriestsOfSyrinx", 'enable_nested' => 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($this->starred); + $exp = [ + [ + ['id' => "5", 'title' => "Local", 'unread' => 10, 'order_id' => 1], + ['id' => "6", 'title' => "National", 'unread' => 18, 'order_id' => 2], + ['id' => "4", 'title' => "Photography", 'unread' => 0, 'order_id' => 3], + ['id' => "3", 'title' => "Politics", 'unread' => 0, 'order_id' => 4], + ['id' => "2", 'title' => "Rocketry", 'unread' => 5, 'order_id' => 5], + ['id' => "1", 'title' => "Science", 'unread' => 2, 'order_id' => 6], + ['id' => 0, 'title' => "Uncategorized", 'unread' => 0], + ['id' => -1, 'title' => "Special", 'unread' => 11], + ['id' => -2, 'title' => "Labels", 'unread' => "6"], + ], + [ + ['id' => "5", 'title' => "Local", 'unread' => 10, 'order_id' => 1], + ['id' => "6", 'title' => "National", 'unread' => 18, 'order_id' => 2], + ['id' => "3", 'title' => "Politics", 'unread' => 0, 'order_id' => 4], + ['id' => "2", 'title' => "Rocketry", 'unread' => 5, 'order_id' => 5], + ['id' => "1", 'title' => "Science", 'unread' => 2, 'order_id' => 6], + ['id' => 0, 'title' => "Uncategorized", 'unread' => 0], + ['id' => -1, 'title' => "Special", 'unread' => 11], + ['id' => -2, 'title' => "Labels", 'unread' => "6"], + ], + [ + ['id' => "5", 'title' => "Local", 'unread' => 10, 'order_id' => 1], + ['id' => "6", 'title' => "National", 'unread' => 18, 'order_id' => 2], + ['id' => "2", 'title' => "Rocketry", 'unread' => 5, 'order_id' => 5], + ['id' => "1", 'title' => "Science", 'unread' => 2, 'order_id' => 6], + ['id' => -1, 'title' => "Special", 'unread' => 11], + ['id' => -2, 'title' => "Labels", 'unread' => "6"], + ], + [ + ['id' => "4", 'title' => "Photography", 'unread' => 0, 'order_id' => 1], + ['id' => "3", 'title' => "Politics", 'unread' => 28, 'order_id' => 2], + ['id' => "1", 'title' => "Science", 'unread' => 7, 'order_id' => 3], + ['id' => 0, 'title' => "Uncategorized", 'unread' => 0], + ['id' => -1, 'title' => "Special", 'unread' => 11], + ['id' => -2, 'title' => "Labels", 'unread' => "6"], + ], + [ + ['id' => "3", 'title' => "Politics", 'unread' => 28, 'order_id' => 2], + ['id' => "1", 'title' => "Science", 'unread' => 7, 'order_id' => 3], + ['id' => 0, 'title' => "Uncategorized", 'unread' => 0], + ['id' => -1, 'title' => "Special", 'unread' => 11], + ['id' => -2, 'title' => "Labels", 'unread' => "6"], + ], + [ + ['id' => "3", 'title' => "Politics", 'unread' => 28, 'order_id' => 2], + ['id' => "1", 'title' => "Science", 'unread' => 7, 'order_id' => 3], + ['id' => -1, 'title' => "Special", 'unread' => 11], + ['id' => -2, 'title' => "Labels", 'unread' => "6"], + ], + ]; + for ($a = 0; $a < sizeof($in); $a++) { + $this->assertResponse($this->respGood($exp[$a]), $this->req($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($this->starred); + $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' => 0, 'auxcounter' => 2], + ['id' => "3", 'updated' => "2016-05-23T06:40:02", 'counter' => 2, 'has_img' => 1], + ['id' => "4", 'updated' => "2017-10-09T15:58:34", 'counter' => 6, 'has_img' => 1], + ['id' => "1", 'updated' => "2017-09-15T22:54:16", 'counter' => 5, 'has_img' => 0], + ['id' => "5", 'updated' => "2017-07-07T17:07:17", 'counter' => 12, 'has_img' => 0], + ['id' => "2", 'updated' => "2011-11-11T11:11:11", 'counter' => 10, 'has_img' => 1], + ['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' => 6], + ]; + $this->assertResponse($this->respGood($exp), $this->req($in)); + } + + public function testRetrieveTheLabelList() { + $in = [ + ['op' => "getLabels", 'sid' => "PriestsOfSyrinx"], + ['op' => "getLabels", 'sid' => "PriestsOfSyrinx", 'article_id' => 1], + ['op' => "getLabels", 'sid' => "PriestsOfSyrinx", 'article_id' => 2], + ['op' => "getLabels", 'sid' => "PriestsOfSyrinx", 'article_id' => 3], + ['op' => "getLabels", 'sid' => "PriestsOfSyrinx", 'article_id' => 4], + ]; + Phake::when(Arsse::$db)->labelList($this->anything())->thenReturn(new Result($this->labels)); + Phake::when(Arsse::$db)->articleLabelsGet($this->anything(), 1)->thenReturn([1,3]); + Phake::when(Arsse::$db)->articleLabelsGet($this->anything(), 2)->thenReturn([3]); + Phake::when(Arsse::$db)->articleLabelsGet($this->anything(), 3)->thenReturn([]); + Phake::when(Arsse::$db)->articleLabelsGet($this->anything(), 4)->thenThrow(new ExceptionInput("idMissing")); + $exp = [ + [ + ['id' => -1027, 'caption' => "Fascinating", 'fg_color' => "", 'bg_color' => "", 'checked' => false], + ['id' => -1029, 'caption' => "Interesting", 'fg_color' => "", 'bg_color' => "", 'checked' => false], + ['id' => -1025, 'caption' => "Logical", 'fg_color' => "", 'bg_color' => "", 'checked' => false], + ], + [ + ['id' => -1027, 'caption' => "Fascinating", 'fg_color' => "", 'bg_color' => "", 'checked' => true], + ['id' => -1029, 'caption' => "Interesting", 'fg_color' => "", 'bg_color' => "", 'checked' => false], + ['id' => -1025, 'caption' => "Logical", 'fg_color' => "", 'bg_color' => "", 'checked' => true], + ], + [ + ['id' => -1027, 'caption' => "Fascinating", 'fg_color' => "", 'bg_color' => "", 'checked' => true], + ['id' => -1029, 'caption' => "Interesting", 'fg_color' => "", 'bg_color' => "", 'checked' => false], + ['id' => -1025, 'caption' => "Logical", 'fg_color' => "", 'bg_color' => "", 'checked' => false], + ], + [ + ['id' => -1027, 'caption' => "Fascinating", 'fg_color' => "", 'bg_color' => "", 'checked' => false], + ['id' => -1029, 'caption' => "Interesting", 'fg_color' => "", 'bg_color' => "", 'checked' => false], + ['id' => -1025, 'caption' => "Logical", 'fg_color' => "", 'bg_color' => "", 'checked' => false], + ], + [ + ['id' => -1027, 'caption' => "Fascinating", 'fg_color' => "", 'bg_color' => "", 'checked' => false], + ['id' => -1029, 'caption' => "Interesting", 'fg_color' => "", 'bg_color' => "", 'checked' => false], + ['id' => -1025, 'caption' => "Logical", 'fg_color' => "", 'bg_color' => "", 'checked' => false], + ], + ]; + for ($a = 0; $a < sizeof($in); $a++) { + $this->assertResponse($this->respGood($exp[$a]), $this->req($in[$a]), "Test $a failed"); + } + } + + public function testAssignArticlesToALabel() { + $list = [ + range(1,100), + range(1,50), + range(51,100), + ]; + $in = [ + ['op' => "setArticleLabel", 'sid' => "PriestsOfSyrinx", 'label_id' => -2112, 'article_ids' => implode(",", $list[0])], + ['op' => "setArticleLabel", 'sid' => "PriestsOfSyrinx", 'label_id' => -2112, 'article_ids' => implode(",", $list[0]), 'assign' => true], + ['op' => "setArticleLabel", 'sid' => "PriestsOfSyrinx", 'label_id' => -2112], + ['op' => "setArticleLabel", 'sid' => "PriestsOfSyrinx", 'label_id' => -42], + ['op' => "setArticleLabel", 'sid' => "PriestsOfSyrinx", 'label_id' => 42], + ['op' => "setArticleLabel", 'sid' => "PriestsOfSyrinx", 'label_id' => 0], + ['op' => "setArticleLabel", 'sid' => "PriestsOfSyrinx"], + ]; + Phake::when(Arsse::$db)->labelArticlesSet(Arsse::$user->id, $this->anything(), (new Context)->articles([]), $this->anything())->thenThrow(new ExceptionInput("tooShort")); // data model function requires one valid integer for multiples + Phake::when(Arsse::$db)->labelArticlesSet(Arsse::$user->id, $this->anything(), (new Context)->articles($list[0]), $this->anything())->thenThrow(new ExceptionInput("tooLong")); // data model function limited to 50 items for multiples + Phake::when(Arsse::$db)->labelArticlesSet(Arsse::$user->id, 1088, (new Context)->articles($list[1]), true)->thenReturn(42); + Phake::when(Arsse::$db)->labelArticlesSet(Arsse::$user->id, 1088, (new Context)->articles($list[2]), true)->thenReturn(47); + Phake::when(Arsse::$db)->labelArticlesSet(Arsse::$user->id, 1088, (new Context)->articles($list[1]), false)->thenReturn(5); + Phake::when(Arsse::$db)->labelArticlesSet(Arsse::$user->id, 1088, (new Context)->articles($list[2]), false)->thenReturn(2); + $exp = $this->respGood(['status' => "OK", 'updated' => 89]); + $this->assertResponse($exp, $this->req($in[0])); + Phake::verify(Arsse::$db)->labelArticlesSet(Arsse::$user->id, 1088, (new Context)->articles($list[1]), true); + Phake::verify(Arsse::$db)->labelArticlesSet(Arsse::$user->id, 1088, (new Context)->articles($list[2]), true); + $exp = $this->respGood(['status' => "OK", 'updated' => 7]); + $this->assertResponse($exp, $this->req($in[1])); + Phake::verify(Arsse::$db)->labelArticlesSet(Arsse::$user->id, 1088, (new Context)->articles($list[1]), false); + Phake::verify(Arsse::$db)->labelArticlesSet(Arsse::$user->id, 1088, (new Context)->articles($list[2]), false); + $exp = $this->respGood(['status' => "OK", 'updated' => 0]); + $this->assertResponse($exp, $this->req($in[2])); + $exp = $this->respErr("INCORRECT_USAGE"); + $this->assertResponse($exp, $this->req($in[3])); + $this->assertResponse($exp, $this->req($in[4])); + $this->assertResponse($exp, $this->req($in[5])); + $this->assertResponse($exp, $this->req($in[6])); + } + + public function testRetrieveFeedTree() { + $in = [ + ['op' => "getFeedTree", 'sid' => "PriestsOfSyrinx", 'include_empty' => true], + ['op' => "getFeedTree", 'sid' => "PriestsOfSyrinx"], + ]; + Phake::when(Arsse::$db)->folderList($this->anything(), null, true)->thenReturn(new Result($this->folders)); + Phake::when(Arsse::$db)->subscriptionList($this->anything())->thenReturn(new Result($this->subscriptions)); + Phake::when(Arsse::$db)->labelList($this->anything(), true)->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($this->starred); + // the expectations are packed tightly since they're very verbose; one can use var_export() (or convert to JSON) to pretty-print them + $exp = ['categories'=>['identifier'=>'id','label'=>'name','items'=>[['name'=>'Special','id'=>'CAT:-1','bare_id'=>-1,'type'=>'category','unread'=>0,'items'=>[['name'=>'All articles','id'=>'FEED:-4','bare_id'=>-4,'icon'=>'images/folder.png','unread'=>35,'type'=>'feed','auxcounter'=>0,'error'=>'','updated'=>'',],['name'=>'Fresh articles','id'=>'FEED:-3','bare_id'=>-3,'icon'=>'images/fresh.png','unread'=>7,'type'=>'feed','auxcounter'=>0,'error'=>'','updated'=>'',],['name'=>'Starred articles','id'=>'FEED:-1','bare_id'=>-1,'icon'=>'images/star.png','unread'=>4,'type'=>'feed','auxcounter'=>0,'error'=>'','updated'=>'',],['name'=>'Published articles','id'=>'FEED:-2','bare_id'=>-2,'icon'=>'images/feed.png','unread'=>0,'type'=>'feed','auxcounter'=>0,'error'=>'','updated'=>'',],['name'=>'Archived articles','id'=>'FEED:0','bare_id'=>0,'icon'=>'images/archive.png','unread'=>0,'type'=>'feed','auxcounter'=>0,'error'=>'','updated'=>'',],['name'=>'Recently read','id'=>'FEED:-6','bare_id'=>-6,'icon'=>'images/time.png','unread'=>0,'type'=>'feed','auxcounter'=>0,'error'=>'','updated'=>'',],],],['name'=>'Labels','id'=>'CAT:-2','bare_id'=>-2,'type'=>'category','unread'=>6,'items'=>[['name'=>'Fascinating','id'=>'FEED:-1027','bare_id'=>-1027,'unread'=>0,'icon'=>'images/label.png','type'=>'feed','auxcounter'=>0,'error'=>'','updated'=>'','fg_color'=>'','bg_color'=>'',],['name'=>'Interesting','id'=>'FEED:-1029','bare_id'=>-1029,'unread'=>0,'icon'=>'images/label.png','type'=>'feed','auxcounter'=>0,'error'=>'','updated'=>'','fg_color'=>'','bg_color'=>'',],['name'=>'Logical','id'=>'FEED:-1025','bare_id'=>-1025,'unread'=>0,'icon'=>'images/label.png','type'=>'feed','auxcounter'=>0,'error'=>'','updated'=>'','fg_color'=>'','bg_color'=>'',],],],['name'=>'Photography','id'=>'CAT:4','bare_id'=>4,'parent_id'=>null,'type'=>'category','auxcounter'=>0,'unread'=>0,'child_unread'=>0,'checkbox'=>false,'param'=>'(0 feeds)','items'=>[],],['name'=>'Politics','id'=>'CAT:3','bare_id'=>3,'parent_id'=>null,'type'=>'category','auxcounter'=>0,'unread'=>0,'child_unread'=>0,'checkbox'=>false,'param'=>'(3 feeds)','items'=>[['name'=>'Local','id'=>'CAT:5','bare_id'=>5,'parent_id'=>3,'type'=>'category','auxcounter'=>0,'unread'=>0,'child_unread'=>0,'checkbox'=>false,'param'=>'(1 feed)','items'=>[['name'=>'Toronto Star','id'=>'FEED:2','bare_id'=>2,'icon'=>'feed-icons/2.ico','error'=>'oops','param'=>'2011-11-11T11:11:11','unread'=>0,'auxcounter'=>0,'checkbox'=>false,],],],['name'=>'National','id'=>'CAT:6','bare_id'=>6,'parent_id'=>3,'type'=>'category','auxcounter'=>0,'unread'=>0,'child_unread'=>0,'checkbox'=>false,'param'=>'(2 feeds)','items'=>[['name'=>'CBC News','id'=>'FEED:4','bare_id'=>4,'icon'=>'feed-icons/4.ico','error'=>'','param'=>'2017-10-09T15:58:34','unread'=>0,'auxcounter'=>0,'checkbox'=>false,],['name'=>'Ottawa Citizen','id'=>'FEED:5','bare_id'=>5,'icon'=>false,'error'=>'','param'=>'2017-07-07T17:07:17','unread'=>0,'auxcounter'=>0,'checkbox'=>false,],],],],],['name'=>'Science','id'=>'CAT:1','bare_id'=>1,'parent_id'=>null,'type'=>'category','auxcounter'=>0,'unread'=>0,'child_unread'=>0,'checkbox'=>false,'param'=>'(2 feeds)','items'=>[['name'=>'Rocketry','id'=>'CAT:2','bare_id'=>2,'parent_id'=>1,'type'=>'category','auxcounter'=>0,'unread'=>0,'child_unread'=>0,'checkbox'=>false,'param'=>'(1 feed)','items'=>[['name'=>'NASA JPL','id'=>'FEED:1','bare_id'=>1,'icon'=>false,'error'=>'','param'=>'2017-09-15T22:54:16','unread'=>0,'auxcounter'=>0,'checkbox'=>false,],],],['name'=>'Ars Technica','id'=>'FEED:3','bare_id'=>3,'icon'=>'feed-icons/3.ico','error'=>'argh','param'=>'2016-05-23T06:40:02','unread'=>0,'auxcounter'=>0,'checkbox'=>false,],],],['name'=>'Uncategorized','id'=>'CAT:0','bare_id'=>0,'type'=>'category','auxcounter'=>0,'unread'=>0,'child_unread'=>0,'checkbox'=>false,'parent_id'=>null,'param'=>'(1 feed)','items'=>[['name'=>'Eurogamer','id'=>'FEED:6','bare_id'=>6,'icon'=>'feed-icons/6.ico','error'=>'','param'=>'2010-02-12T20:08:47','unread'=>0,'auxcounter'=>0,'checkbox'=>false,],],],],],]; + $this->assertResponse($this->respGood($exp), $this->req($in[0])); + $exp = ['categories'=>['identifier'=>'id','label'=>'name','items'=>[['name'=>'Special','id'=>'CAT:-1','bare_id'=>-1,'type'=>'category','unread'=>0,'items'=>[['name'=>'All articles','id'=>'FEED:-4','bare_id'=>-4,'icon'=>'images/folder.png','unread'=>35,'type'=>'feed','auxcounter'=>0,'error'=>'','updated'=>'',],['name'=>'Fresh articles','id'=>'FEED:-3','bare_id'=>-3,'icon'=>'images/fresh.png','unread'=>7,'type'=>'feed','auxcounter'=>0,'error'=>'','updated'=>'',],['name'=>'Starred articles','id'=>'FEED:-1','bare_id'=>-1,'icon'=>'images/star.png','unread'=>4,'type'=>'feed','auxcounter'=>0,'error'=>'','updated'=>'',],['name'=>'Published articles','id'=>'FEED:-2','bare_id'=>-2,'icon'=>'images/feed.png','unread'=>0,'type'=>'feed','auxcounter'=>0,'error'=>'','updated'=>'',],['name'=>'Archived articles','id'=>'FEED:0','bare_id'=>0,'icon'=>'images/archive.png','unread'=>0,'type'=>'feed','auxcounter'=>0,'error'=>'','updated'=>'',],['name'=>'Recently read','id'=>'FEED:-6','bare_id'=>-6,'icon'=>'images/time.png','unread'=>0,'type'=>'feed','auxcounter'=>0,'error'=>'','updated'=>'',],],],['name'=>'Labels','id'=>'CAT:-2','bare_id'=>-2,'type'=>'category','unread'=>6,'items'=>[['name'=>'Fascinating','id'=>'FEED:-1027','bare_id'=>-1027,'unread'=>0,'icon'=>'images/label.png','type'=>'feed','auxcounter'=>0,'error'=>'','updated'=>'','fg_color'=>'','bg_color'=>'',],['name'=>'Interesting','id'=>'FEED:-1029','bare_id'=>-1029,'unread'=>0,'icon'=>'images/label.png','type'=>'feed','auxcounter'=>0,'error'=>'','updated'=>'','fg_color'=>'','bg_color'=>'',],['name'=>'Logical','id'=>'FEED:-1025','bare_id'=>-1025,'unread'=>0,'icon'=>'images/label.png','type'=>'feed','auxcounter'=>0,'error'=>'','updated'=>'','fg_color'=>'','bg_color'=>'',],],],['name'=>'Politics','id'=>'CAT:3','bare_id'=>3,'parent_id'=>null,'type'=>'category','auxcounter'=>0,'unread'=>0,'child_unread'=>0,'checkbox'=>false,'param'=>'(3 feeds)','items'=>[['name'=>'Local','id'=>'CAT:5','bare_id'=>5,'parent_id'=>3,'type'=>'category','auxcounter'=>0,'unread'=>0,'child_unread'=>0,'checkbox'=>false,'param'=>'(1 feed)','items'=>[['name'=>'Toronto Star','id'=>'FEED:2','bare_id'=>2,'icon'=>'feed-icons/2.ico','error'=>'oops','param'=>'2011-11-11T11:11:11','unread'=>0,'auxcounter'=>0,'checkbox'=>false,],],],['name'=>'National','id'=>'CAT:6','bare_id'=>6,'parent_id'=>3,'type'=>'category','auxcounter'=>0,'unread'=>0,'child_unread'=>0,'checkbox'=>false,'param'=>'(2 feeds)','items'=>[['name'=>'CBC News','id'=>'FEED:4','bare_id'=>4,'icon'=>'feed-icons/4.ico','error'=>'','param'=>'2017-10-09T15:58:34','unread'=>0,'auxcounter'=>0,'checkbox'=>false,],['name'=>'Ottawa Citizen','id'=>'FEED:5','bare_id'=>5,'icon'=>false,'error'=>'','param'=>'2017-07-07T17:07:17','unread'=>0,'auxcounter'=>0,'checkbox'=>false,],],],],],['name'=>'Science','id'=>'CAT:1','bare_id'=>1,'parent_id'=>null,'type'=>'category','auxcounter'=>0,'unread'=>0,'child_unread'=>0,'checkbox'=>false,'param'=>'(2 feeds)','items'=>[['name'=>'Rocketry','id'=>'CAT:2','bare_id'=>2,'parent_id'=>1,'type'=>'category','auxcounter'=>0,'unread'=>0,'child_unread'=>0,'checkbox'=>false,'param'=>'(1 feed)','items'=>[['name'=>'NASA JPL','id'=>'FEED:1','bare_id'=>1,'icon'=>false,'error'=>'','param'=>'2017-09-15T22:54:16','unread'=>0,'auxcounter'=>0,'checkbox'=>false,],],],['name'=>'Ars Technica','id'=>'FEED:3','bare_id'=>3,'icon'=>'feed-icons/3.ico','error'=>'argh','param'=>'2016-05-23T06:40:02','unread'=>0,'auxcounter'=>0,'checkbox'=>false,],],],['name'=>'Uncategorized','id'=>'CAT:0','bare_id'=>0,'type'=>'category','auxcounter'=>0,'unread'=>0,'child_unread'=>0,'checkbox'=>false,'parent_id'=>null,'param'=>'(1 feed)','items'=>[['name'=>'Eurogamer','id'=>'FEED:6','bare_id'=>6,'icon'=>'feed-icons/6.ico','error'=>'','param'=>'2010-02-12T20:08:47','unread'=>0,'auxcounter'=>0,'checkbox'=>false,],],],],],]; + $this->assertResponse($this->respGood($exp), $this->req($in[1])); + } + + public function testMarkFeedsAsRead() { + $in1 = [ + // no-ops + ['op' => "catchupFeed", 'sid' => "PriestsOfSyrinx"], + ['op' => "catchupFeed", 'sid' => "PriestsOfSyrinx", 'feed_id' => 0], + ['op' => "catchupFeed", 'sid' => "PriestsOfSyrinx", 'feed_id' => -2], + ['op' => "catchupFeed", 'sid' => "PriestsOfSyrinx", 'feed_id' => -6], + ['op' => "catchupFeed", 'sid' => "PriestsOfSyrinx", 'feed_id' => -1, 'is_cat' => true], + ['op' => "catchupFeed", 'sid' => "PriestsOfSyrinx", 'feed_id' => -3, 'is_cat' => true], + ['op' => "catchupFeed", 'sid' => "PriestsOfSyrinx", 'feed_id' => -4, 'is_cat' => true], + ]; + $in2 = [ + // simple contexts + ['op' => "catchupFeed", 'sid' => "PriestsOfSyrinx", 'feed_id' => -1], + ['op' => "catchupFeed", 'sid' => "PriestsOfSyrinx", 'feed_id' => -4], + ['op' => "catchupFeed", 'sid' => "PriestsOfSyrinx", 'feed_id' => -2112], + ['op' => "catchupFeed", 'sid' => "PriestsOfSyrinx", 'feed_id' => 2112], + ['op' => "catchupFeed", 'sid' => "PriestsOfSyrinx", 'feed_id' => 42, 'is_cat' => true], + ['op' => "catchupFeed", 'sid' => "PriestsOfSyrinx", 'feed_id' => 0, 'is_cat' => true], + ['op' => "catchupFeed", 'sid' => "PriestsOfSyrinx", 'feed_id' => -2, 'is_cat' => true], + ]; + $in3 = [ + // this one has a tricky time-based context + ['op' => "catchupFeed", 'sid' => "PriestsOfSyrinx", 'feed_id' => -3], + ]; + Phake::when(Arsse::$db)->articleMark->thenThrow(new ExceptionInput("typeViolation")); + $exp = $this->respGood(['status' => "OK"]); + // verify the above are in fact no-ops + for ($a = 0; $a < sizeof($in1); $a++) { + $this->assertResponse($exp, $this->req($in1[$a]), "Test $a failed"); + } + Phake::verify(Arsse::$db, Phake::times(0))->articleMark; + // verify the simple contexts + for ($a = 0; $a < sizeof($in2); $a++) { + $this->assertResponse($exp, $this->req($in2[$a]), "Test $a failed"); + } + Phake::verify(Arsse::$db)->articleMark($this->anything(), ['read' => true], new Context); + Phake::verify(Arsse::$db)->articleMark($this->anything(), ['read' => true], (new Context)->starred(true)); + Phake::verify(Arsse::$db)->articleMark($this->anything(), ['read' => true], (new Context)->label(1088)); + Phake::verify(Arsse::$db)->articleMark($this->anything(), ['read' => true], (new Context)->subscription(2112)); + Phake::verify(Arsse::$db)->articleMark($this->anything(), ['read' => true], (new Context)->folder(42)); + Phake::verify(Arsse::$db)->articleMark($this->anything(), ['read' => true], (new Context)->folderShallow(0)); + Phake::verify(Arsse::$db)->articleMark($this->anything(), ['read' => true], (new Context)->labelled(true)); + // verify the time-based mock + $t = Date::sub("PT24H"); + for ($a = 0; $a < sizeof($in3); $a++) { + $this->assertResponse($exp, $this->req($in3[$a]), "Test $a failed"); + } + Phake::verify(Arsse::$db)->articleMark($this->anything(), ['read' => true], (new Context)->modifiedSince($t)); + } + + public function testRetrieveFeedList() { + $in1 = [ + ['op' => "getFeeds", 'sid' => "PriestsOfSyrinx"], + ['op' => "getFeeds", 'sid' => "PriestsOfSyrinx", 'cat_id' => -1], + ['op' => "getFeeds", 'sid' => "PriestsOfSyrinx", 'cat_id' => -1, 'unread_only' => true], + ['op' => "getFeeds", 'sid' => "PriestsOfSyrinx", 'cat_id' => -2], + ['op' => "getFeeds", 'sid' => "PriestsOfSyrinx", 'cat_id' => -2, 'unread_only' => true], + ['op' => "getFeeds", 'sid' => "PriestsOfSyrinx", 'cat_id' => -3], + ['op' => "getFeeds", 'sid' => "PriestsOfSyrinx", 'cat_id' => -3, 'unread_only' => true], + ['op' => "getFeeds", 'sid' => "PriestsOfSyrinx", 'cat_id' => -4], + ['op' => "getFeeds", 'sid' => "PriestsOfSyrinx", 'cat_id' => -4, 'unread_only' => true], + ['op' => "getFeeds", 'sid' => "PriestsOfSyrinx", 'cat_id' => 6], + ['op' => "getFeeds", 'sid' => "PriestsOfSyrinx", 'cat_id' => 6, 'limit' => 1], + ['op' => "getFeeds", 'sid' => "PriestsOfSyrinx", 'cat_id' => 6, 'limit' => 1, 'offset' => 1], + ['op' => "getFeeds", 'sid' => "PriestsOfSyrinx", 'cat_id' => 1], + ['op' => "getFeeds", 'sid' => "PriestsOfSyrinx", 'cat_id' => 1, 'include_nested' => true], + ]; + $in2 = [ + // these should all return an empty list + ['op' => "getFeeds", 'sid' => "PriestsOfSyrinx", 'cat_id' => 0, 'unread_only' => true], + ['op' => "getFeeds", 'sid' => "PriestsOfSyrinx", 'cat_id' => 2112], + ['op' => "getFeeds", 'sid' => "PriestsOfSyrinx", 'cat_id' => 2112, 'include_nested' => true], + ['op' => "getFeeds", 'sid' => "PriestsOfSyrinx", 'cat_id' => 6, 'limit' => -42], + ['op' => "getFeeds", 'sid' => "PriestsOfSyrinx", 'cat_id' => 6, 'offset' => 2], + ]; + // statistical mocks + Phake::when(Arsse::$db)->articleStarred($this->anything())->thenReturn($this->starred); + Phake::when(Arsse::$db)->articleCount->thenReturn(7); // FIXME: this should check an unread+modifiedSince context + Phake::when(Arsse::$db)->articleCount($this->anything(), (new Context)->unread(true))->thenReturn(35); + // label mocks + 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)); + // subscription and folder list and unread count mocks + Phake::when(Arsse::$db)->folderList->thenThrow(new ExceptionInput("subjectMissing")); + Phake::when(Arsse::$db)->subscriptionList->thenThrow(new ExceptionInput("subjectMissing")); + Phake::when(Arsse::$db)->folderList($this->anything())->thenReturn(new Result($this->folders)); + Phake::when(Arsse::$db)->subscriptionList($this->anything(), null, true)->thenReturn(new Result($this->subscriptions)); + Phake::when(Arsse::$db)->subscriptionList($this->anything(), null, false)->thenReturn(new Result($this->filterSubs(null))); + Phake::when(Arsse::$db)->folderList($this->anything(), null)->thenReturn(new Result($this->folders)); + Phake::when(Arsse::$db)->folderList($this->anything(), null, false)->thenReturn(new Result($this->filterFolders(null))); + foreach ($this->folders as $f) { + Phake::when(Arsse::$db)->folderList($this->anything(), $f['id'], false)->thenReturn(new Result($this->filterFolders($f['id']))); + Phake::when(Arsse::$db)->articleCount($this->anything(), (new Context)->unread(true)->folder($f['id']))->thenReturn($this->reduceFolders($f['id'])); + Phake::when(Arsse::$db)->subscriptionList($this->anything(), $f['id'], false)->thenReturn(new Result($this->filterSubs($f['id']))); + } + $exp = [ + [ + ['id' => 6, 'title' => 'Eurogamer', 'unread' => 0, 'cat_id' => 0, 'feed_url' => " http://example.com/6", 'has_icon' => true, 'last_updated' => 1266005327, 'order_id' => 1], + ], + [ + ['id' => -1, 'title' => "Starred articles", 'unread' => "4", 'cat_id' => -1], + ['id' => -2, 'title' => "Published articles", 'unread' => "0", 'cat_id' => -1], + ['id' => -3, 'title' => "Fresh articles", 'unread' => "7", 'cat_id' => -1], + ['id' => -4, 'title' => "All articles", 'unread' => "35", 'cat_id' => -1], + ['id' => -6, 'title' => "Recently read", 'unread' => 0, 'cat_id' => -1], + ['id' => 0, 'title' => "Archived articles", 'unread' => "0", 'cat_id' => -1], + ], + [ + ['id' => -1, 'title' => "Starred articles", 'unread' => "4", 'cat_id' => -1], + ['id' => -3, 'title' => "Fresh articles", 'unread' => "7", 'cat_id' => -1], + ['id' => -4, 'title' => "All articles", 'unread' => "35", 'cat_id' => -1], + ], + [ + ['id' => -1027, 'title' => "Fascinating", 'unread' => "6", 'cat_id' => -2], + ['id' => -1025, 'title' => "Logical", 'unread' => "0", 'cat_id' => -2], + ], + [ + ['id' => -1027, 'title' => "Fascinating", 'unread' => "6", 'cat_id' => -2], + ], + [ + ['id' => 3, 'title' => 'Ars Technica', 'unread' => 2, 'cat_id' => 1, 'feed_url' => " http://example.com/3", 'has_icon' => true, 'last_updated' => 1463985602, 'order_id' => 1], + ['id' => 4, 'title' => 'CBC News', 'unread' => 6, 'cat_id' => 6, 'feed_url' => " http://example.com/4", 'has_icon' => true, 'last_updated' => 1507564714, 'order_id' => 2], + ['id' => 6, 'title' => 'Eurogamer', 'unread' => 0, 'cat_id' => 0, 'feed_url' => " http://example.com/6", 'has_icon' => true, 'last_updated' => 1266005327, 'order_id' => 3], + ['id' => 1, 'title' => 'NASA JPL', 'unread' => 5, 'cat_id' => 2, 'feed_url' => " http://example.com/1", 'has_icon' => false, 'last_updated' => 1505516056, 'order_id' => 4], + ['id' => 5, 'title' => 'Ottawa Citizen', 'unread' => 12, 'cat_id' => 6, 'feed_url' => " http://example.com/5", 'has_icon' => false, 'last_updated' => 1499447237, 'order_id' => 5], + ['id' => 2, 'title' => 'Toronto Star', 'unread' => 10, 'cat_id' => 5, 'feed_url' => " http://example.com/2", 'has_icon' => true, 'last_updated' => 1321009871, 'order_id' => 6], + ], + [ + ['id' => 3, 'title' => 'Ars Technica', 'unread' => 2, 'cat_id' => 1, 'feed_url' => " http://example.com/3", 'has_icon' => true, 'last_updated' => 1463985602, 'order_id' => 1], + ['id' => 4, 'title' => 'CBC News', 'unread' => 6, 'cat_id' => 6, 'feed_url' => " http://example.com/4", 'has_icon' => true, 'last_updated' => 1507564714, 'order_id' => 2], + ['id' => 1, 'title' => 'NASA JPL', 'unread' => 5, 'cat_id' => 2, 'feed_url' => " http://example.com/1", 'has_icon' => false, 'last_updated' => 1505516056, 'order_id' => 4], + ['id' => 5, 'title' => 'Ottawa Citizen', 'unread' => 12, 'cat_id' => 6, 'feed_url' => " http://example.com/5", 'has_icon' => false, 'last_updated' => 1499447237, 'order_id' => 5], + ['id' => 2, 'title' => 'Toronto Star', 'unread' => 10, 'cat_id' => 5, 'feed_url' => " http://example.com/2", 'has_icon' => true, 'last_updated' => 1321009871, 'order_id' => 6], + ], + [ + ['id' => -1027, 'title' => "Fascinating", 'unread' => "6", 'cat_id' => -2], + ['id' => -1025, 'title' => "Logical", 'unread' => "0", 'cat_id' => -2], + ['id' => -1, 'title' => "Starred articles", 'unread' => "4", 'cat_id' => -1], + ['id' => -2, 'title' => "Published articles", 'unread' => "0", 'cat_id' => -1], + ['id' => -3, 'title' => "Fresh articles", 'unread' => "7", 'cat_id' => -1], + ['id' => -4, 'title' => "All articles", 'unread' => "35", 'cat_id' => -1], + ['id' => -6, 'title' => "Recently read", 'unread' => 0, 'cat_id' => -1], + ['id' => 0, 'title' => "Archived articles", 'unread' => "0", 'cat_id' => -1], + ['id' => 3, 'title' => 'Ars Technica', 'unread' => 2, 'cat_id' => 1, 'feed_url' => " http://example.com/3", 'has_icon' => true, 'last_updated' => 1463985602, 'order_id' => 1], + ['id' => 4, 'title' => 'CBC News', 'unread' => 6, 'cat_id' => 6, 'feed_url' => " http://example.com/4", 'has_icon' => true, 'last_updated' => 1507564714, 'order_id' => 2], + ['id' => 6, 'title' => 'Eurogamer', 'unread' => 0, 'cat_id' => 0, 'feed_url' => " http://example.com/6", 'has_icon' => true, 'last_updated' => 1266005327, 'order_id' => 3], + ['id' => 1, 'title' => 'NASA JPL', 'unread' => 5, 'cat_id' => 2, 'feed_url' => " http://example.com/1", 'has_icon' => false, 'last_updated' => 1505516056, 'order_id' => 4], + ['id' => 5, 'title' => 'Ottawa Citizen', 'unread' => 12, 'cat_id' => 6, 'feed_url' => " http://example.com/5", 'has_icon' => false, 'last_updated' => 1499447237, 'order_id' => 5], + ['id' => 2, 'title' => 'Toronto Star', 'unread' => 10, 'cat_id' => 5, 'feed_url' => " http://example.com/2", 'has_icon' => true, 'last_updated' => 1321009871, 'order_id' => 6], + ], + [ + ['id' => -1027, 'title' => "Fascinating", 'unread' => "6", 'cat_id' => -2], + ['id' => -1, 'title' => "Starred articles", 'unread' => "4", 'cat_id' => -1], + ['id' => -3, 'title' => "Fresh articles", 'unread' => "7", 'cat_id' => -1], + ['id' => -4, 'title' => "All articles", 'unread' => "35", 'cat_id' => -1], + ['id' => 3, 'title' => 'Ars Technica', 'unread' => 2, 'cat_id' => 1, 'feed_url' => " http://example.com/3", 'has_icon' => true, 'last_updated' => 1463985602, 'order_id' => 1], + ['id' => 4, 'title' => 'CBC News', 'unread' => 6, 'cat_id' => 6, 'feed_url' => " http://example.com/4", 'has_icon' => true, 'last_updated' => 1507564714, 'order_id' => 2], + ['id' => 1, 'title' => 'NASA JPL', 'unread' => 5, 'cat_id' => 2, 'feed_url' => " http://example.com/1", 'has_icon' => false, 'last_updated' => 1505516056, 'order_id' => 4], + ['id' => 5, 'title' => 'Ottawa Citizen', 'unread' => 12, 'cat_id' => 6, 'feed_url' => " http://example.com/5", 'has_icon' => false, 'last_updated' => 1499447237, 'order_id' => 5], + ['id' => 2, 'title' => 'Toronto Star', 'unread' => 10, 'cat_id' => 5, 'feed_url' => " http://example.com/2", 'has_icon' => true, 'last_updated' => 1321009871, 'order_id' => 6], + ], + [ + ['id' => 4, 'title' => 'CBC News', 'unread' => 6, 'cat_id' => 6, 'feed_url' => " http://example.com/4", 'has_icon' => true, 'last_updated' => 1507564714, 'order_id' => 1], + ['id' => 5, 'title' => 'Ottawa Citizen', 'unread' => 12, 'cat_id' => 6, 'feed_url' => " http://example.com/5", 'has_icon' => false, 'last_updated' => 1499447237, 'order_id' => 2], + ], + [ + ['id' => 4, 'title' => 'CBC News', 'unread' => 6, 'cat_id' => 6, 'feed_url' => " http://example.com/4", 'has_icon' => true, 'last_updated' => 1507564714, 'order_id' => 1], + ], + [ + ['id' => 5, 'title' => 'Ottawa Citizen', 'unread' => 12, 'cat_id' => 6, 'feed_url' => " http://example.com/5", 'has_icon' => false, 'last_updated' => 1499447237, 'order_id' => 2], + ], + [ + ['id' => 3, 'title' => 'Ars Technica', 'unread' => 2, 'cat_id' => 1, 'feed_url' => " http://example.com/3", 'has_icon' => true, 'last_updated' => 1463985602, 'order_id' => 1], + ], + [ + ['id' => 2, 'title' => "Rocketry", 'unread' => 5, 'is_cat' => true, 'order_id' => 1], + ['id' => 3, 'title' => 'Ars Technica', 'unread' => 2, 'cat_id' => 1, 'feed_url' => " http://example.com/3", 'has_icon' => true, 'last_updated' => 1463985602, 'order_id' => 1], + ], + ]; + for ($a = 0; $a < sizeof($in1); $a++) { + $this->assertResponse($this->respGood($exp[$a]), $this->req($in1[$a]), "Test $a failed"); + } + for ($a = 0; $a < sizeof($in2); $a++) { + $this->assertResponse($this->respGood([]), $this->req($in2[$a]), "Test $a failed"); + } + } + + protected function filterFolders(int $id = null): array { + return array_filter($this->folders, function($value) use ($id) {return $value['parent']==$id;}); + } + + protected function filterSubs(int $folder = null): array { + return array_filter($this->subscriptions, function($value) use ($folder) {return $value['folder']==$folder;}); + } + + protected function reduceFolders(int $id = null) : int { + $out = 0; + foreach ($this->filterFolders($id) as $f) { + $out += $this->reduceFolders($f['id']); + } + $out += array_reduce(array_filter($this->subscriptions, function($value) use ($id) {return $value['folder']==$id;}), function($sum, $value) {return $sum + $value['unread'];}, 0); + return $out; + } + + public function testChangeArticles() { + $in = [ + ['op' => "updateArticle", 'sid' => "PriestsOfSyrinx"], + ['op' => "updateArticle", 'sid' => "PriestsOfSyrinx", 'article_ids' => "42, 2112, -1"], + + ['op' => "updateArticle", 'sid' => "PriestsOfSyrinx", 'article_ids' => "42, 2112, -1", 'field' => 0], + ['op' => "updateArticle", 'sid' => "PriestsOfSyrinx", 'article_ids' => "42, 2112, -1", 'field' => 0, 'mode' => 0], + ['op' => "updateArticle", 'sid' => "PriestsOfSyrinx", 'article_ids' => "42, 2112, -1", 'field' => 0, 'mode' => 1], + ['op' => "updateArticle", 'sid' => "PriestsOfSyrinx", 'article_ids' => "42, 2112, -1", 'field' => 0, 'mode' => 2], + ['op' => "updateArticle", 'sid' => "PriestsOfSyrinx", 'article_ids' => "42, 2112, -1", 'field' => 0, 'mode' => 3], // invalid mode + + ['op' => "updateArticle", 'sid' => "PriestsOfSyrinx", 'article_ids' => "42, 2112, -1", 'field' => 1], // Published feed' no-op + ['op' => "updateArticle", 'sid' => "PriestsOfSyrinx", 'article_ids' => "42, 2112, -1", 'field' => 1, 'mode' => 0], + ['op' => "updateArticle", 'sid' => "PriestsOfSyrinx", 'article_ids' => "42, 2112, -1", 'field' => 1, 'mode' => 1], + ['op' => "updateArticle", 'sid' => "PriestsOfSyrinx", 'article_ids' => "42, 2112, -1", 'field' => 1, 'mode' => 2], + ['op' => "updateArticle", 'sid' => "PriestsOfSyrinx", 'article_ids' => "42, 2112, -1", 'field' => 1, 'mode' => 3], // invalid mode + + ['op' => "updateArticle", 'sid' => "PriestsOfSyrinx", 'article_ids' => "42, 2112, -1", 'field' => 2], + ['op' => "updateArticle", 'sid' => "PriestsOfSyrinx", 'article_ids' => "42, 2112, -1", 'field' => 2, 'mode' => 0], + ['op' => "updateArticle", 'sid' => "PriestsOfSyrinx", 'article_ids' => "42, 2112, -1", 'field' => 2, 'mode' => 1], + ['op' => "updateArticle", 'sid' => "PriestsOfSyrinx", 'article_ids' => "42, 2112, -1", 'field' => 2, 'mode' => 2], + ['op' => "updateArticle", 'sid' => "PriestsOfSyrinx", 'article_ids' => "42, 2112, -1", 'field' => 2, 'mode' => 3], // invalid mode + + ['op' => "updateArticle", 'sid' => "PriestsOfSyrinx", 'article_ids' => "42, 2112, -1", 'field' => 3], + ['op' => "updateArticle", 'sid' => "PriestsOfSyrinx", 'article_ids' => "42, 2112, -1", 'field' => 3, 'mode' => 0], + ['op' => "updateArticle", 'sid' => "PriestsOfSyrinx", 'article_ids' => "42, 2112, -1", 'field' => 3, 'mode' => 1], + ['op' => "updateArticle", 'sid' => "PriestsOfSyrinx", 'article_ids' => "42, 2112, -1", 'field' => 3, 'mode' => 2], + ['op' => "updateArticle", 'sid' => "PriestsOfSyrinx", 'article_ids' => "42, 2112, -1", 'field' => 3, 'mode' => 3], // invalid mode + ['op' => "updateArticle", 'sid' => "PriestsOfSyrinx", 'article_ids' => "42, 2112, -1", 'field' => 3, 'data' => "eh"], + + ['op' => "updateArticle", 'sid' => "PriestsOfSyrinx", 'article_ids' => "42, 2112, -1", 'field' => 4], // invalid field + ['op' => "updateArticle", 'sid' => "PriestsOfSyrinx", 'article_ids' => "0, -1", 'field' => 3], // no valid IDs + ]; + Phake::when(Arsse::$db)->articleMark->thenReturn(1); + Phake::when(Arsse::$db)->articleMark($this->anything(), ['starred' => false], (new Context)->articles([42, 2112]))->thenReturn(2); + Phake::when(Arsse::$db)->articleMark($this->anything(), ['starred' => true], (new Context)->articles([42, 2112]))->thenReturn(4); + Phake::when(Arsse::$db)->articleMark($this->anything(), ['starred' => false], (new Context)->articles([42, 2112])->starred(true))->thenReturn(8); + Phake::when(Arsse::$db)->articleMark($this->anything(), ['starred' => true], (new Context)->articles([42, 2112])->starred(false))->thenReturn(16); + Phake::when(Arsse::$db)->articleMark($this->anything(), ['read' => true], (new Context)->articles([42, 2112]))->thenReturn(32); // false is read for TT-RSS + Phake::when(Arsse::$db)->articleMark($this->anything(), ['read' => false], (new Context)->articles([42, 2112]))->thenReturn(64); + Phake::when(Arsse::$db)->articleMark($this->anything(), ['read' => true], (new Context)->articles([42, 2112])->unread(true))->thenReturn(128); + Phake::when(Arsse::$db)->articleMark($this->anything(), ['read' => false], (new Context)->articles([42, 2112])->unread(false))->thenReturn(256); + Phake::when(Arsse::$db)->articleMark($this->anything(), ['note' => ""], (new Context)->articles([42, 2112]))->thenReturn(512); + Phake::when(Arsse::$db)->articleMark($this->anything(), ['note' => "eh"], (new Context)->articles([42, 2112]))->thenReturn(1024); + $out = [ + $this->respErr("INCORRECT_USAGE"), + $this->respGood(['status' => "OK", 'updated' => 2]), + + $this->respGood(['status' => "OK", 'updated' => 2]), + $this->respGood(['status' => "OK", 'updated' => 2]), + $this->respGood(['status' => "OK", 'updated' => 4]), + $this->respGood(['status' => "OK", 'updated' => 24]), + $this->respErr("INCORRECT_USAGE"), + + $this->respGood(['status' => "OK", 'updated' => 0]), + $this->respGood(['status' => "OK", 'updated' => 0]), + $this->respGood(['status' => "OK", 'updated' => 0]), + $this->respGood(['status' => "OK", 'updated' => 0]), + $this->respErr("INCORRECT_USAGE"), + + $this->respGood(['status' => "OK", 'updated' => 32]), + $this->respGood(['status' => "OK", 'updated' => 32]), + $this->respGood(['status' => "OK", 'updated' => 64]), + $this->respGood(['status' => "OK", 'updated' => 384]), + $this->respErr("INCORRECT_USAGE"), + + $this->respGood(['status' => "OK", 'updated' => 512]), + $this->respGood(['status' => "OK", 'updated' => 512]), + $this->respGood(['status' => "OK", 'updated' => 512]), + $this->respGood(['status' => "OK", 'updated' => 512]), + $this->respGood(['status' => "OK", 'updated' => 512]), + $this->respGood(['status' => "OK", 'updated' => 1024]), + + $this->respErr("INCORRECT_USAGE"), + $this->respErr("INCORRECT_USAGE"), + ]; + for ($a = 0; $a < sizeof($in); $a++) { + $this->assertResponse($out[$a], $this->req($in[$a]), "Test $a failed"); + } + } + + public function testListArticles() { + $in = [ + // error conditions + ['op' => "getArticle", 'sid' => "PriestsOfSyrinx"], + ['op' => "getArticle", 'sid' => "PriestsOfSyrinx", 'article_id' => 0], + ['op' => "getArticle", 'sid' => "PriestsOfSyrinx", 'article_id' => -1], + ['op' => "getArticle", 'sid' => "PriestsOfSyrinx", 'article_id' => "0,-1"], + // acceptable input + ['op' => "getArticle", 'sid' => "PriestsOfSyrinx", 'article_id' => "101,102"], + ['op' => "getArticle", 'sid' => "PriestsOfSyrinx", 'article_id' => "101"], + ['op' => "getArticle", 'sid' => "PriestsOfSyrinx", 'article_id' => "102"], + ]; + 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($this->anything(), 101)->thenReturn([]); + Phake::when(Arsse::$db)->articleLabelsGet($this->anything(), 102)->thenReturn([1,3]); + Phake::when(Arsse::$db)->articleList($this->anything(), (new Context)->articles([101, 102]))->thenReturn(new Result($this->articles)); + Phake::when(Arsse::$db)->articleList($this->anything(), (new Context)->articles([101]))->thenReturn(new Result([$this->articles[0]])); + Phake::when(Arsse::$db)->articleList($this->anything(), (new Context)->articles([102]))->thenReturn(new Result([$this->articles[1]])); + $exp = $this->respErr("INCORRECT_USAGE"); + $this->assertResponse($exp, $this->req($in[0])); + $this->assertResponse($exp, $this->req($in[1])); + $this->assertResponse($exp, $this->req($in[2])); + $this->assertResponse($exp, $this->req($in[3])); + $exp = [ + [ + 'id' => "101", + 'guid' => null, + 'title' => 'Article title 1', + 'link' => 'http://example.com/1', + 'labels' => [], + 'unread' => true, + 'marked' => false, + 'published' => false, + 'comments' => "", + 'author' => '', + 'updated' => strtotime('2000-01-01 00:00:01'), + 'feed_id' => "8", + 'feed_title' => "Feed 11", + 'attachments' => [], + 'score' => 0, + 'note' => null, + 'lang' => "", + 'content' => 'Article content 1
', + ], + [ + 'id' => "102", + 'guid' => "SHA256:5be8a5a46ecd52ed132191c8d27fb1af6b3d4edc00234c5d9f8f0e10562ed3b7", + 'title' => 'Article title 2', + 'link' => 'http://example.com/2', + 'labels' => [ + [-1025, "Logical", "", ""], + [-1027, "Fascinating", "", ""], + ], + 'unread' => false, + 'marked' => false, + 'published' => false, + 'comments' => "", + 'author' => "J. King", + 'updated' => strtotime('2000-01-02 00:00:02'), + 'feed_id' => "8", + 'feed_title' => "Feed 11", + 'attachments' => [ + [ + 'id' => "0", + 'content_url' => "http://example.com/text", + 'content_type' => "text/plain", + 'title' => "", + 'duration' => "", + 'width' => "", + 'height' => "", + 'post_id' => "102", + ], + ], + 'score' => 0, + 'note' => "Note 2", + 'lang' => "", + 'content' => 'Article content 2
', + ], + ]; + $this->assertResponse($this->respGood($exp), $this->req($in[4])); + $this->assertResponse($this->respGood([$exp[0]]), $this->req($in[5])); + $this->assertResponse($this->respGood([$exp[1]]), $this->req($in[6])); + // test the special case when labels are not used + Phake::when(Arsse::$db)->labelList($this->anything())->thenReturn(new Result([])); + Phake::when(Arsse::$db)->labelList($this->anything(), false)->thenReturn(new Result([])); + $this->assertResponse($this->respGood([$exp[0]]), $this->req($in[5])); + } + + public function testRetrieveCompactHeadlines() { + $in1 = [ + // erroneous input + ['op' => "getCompactHeadlines", 'sid' => "PriestsOfSyrinx"], + // empty results + ['op' => "getCompactHeadlines", 'sid' => "PriestsOfSyrinx", 'feed_id' => 0], + ['op' => "getCompactHeadlines", 'sid' => "PriestsOfSyrinx", 'feed_id' => -2], + ['op' => "getCompactHeadlines", 'sid' => "PriestsOfSyrinx", 'feed_id' => -2, 'is_cat' => true], // is_cat is not used in getCompactHeadlines + ['op' => "getCompactHeadlines", 'sid' => "PriestsOfSyrinx", 'feed_id' => 2112], + ['op' => "getCompactHeadlines", 'sid' => "PriestsOfSyrinx", 'feed_id' => -4, 'view_mode' => "published"], + ['op' => "getCompactHeadlines", 'sid' => "PriestsOfSyrinx", 'feed_id' => -6, 'view_mode' => "unread"], + // non-empty results + ['op' => "getCompactHeadlines", 'sid' => "PriestsOfSyrinx", 'feed_id' => -4], + ['op' => "getCompactHeadlines", 'sid' => "PriestsOfSyrinx", 'feed_id' => -1], + ['op' => "getCompactHeadlines", 'sid' => "PriestsOfSyrinx", 'feed_id' => -2112], + ['op' => "getCompactHeadlines", 'sid' => "PriestsOfSyrinx", 'feed_id' => -4, 'view_mode' => "adaptive"], + ['op' => "getCompactHeadlines", 'sid' => "PriestsOfSyrinx", 'feed_id' => -2112, 'view_mode' => "adaptive"], + ['op' => "getCompactHeadlines", 'sid' => "PriestsOfSyrinx", 'feed_id' => -2112, 'view_mode' => "unread"], + ['op' => "getCompactHeadlines", 'sid' => "PriestsOfSyrinx", 'feed_id' => 42, 'view_mode' => "marked"], + ['op' => "getCompactHeadlines", 'sid' => "PriestsOfSyrinx", 'feed_id' => 42, 'view_mode' => "has_note"], + ['op' => "getCompactHeadlines", 'sid' => "PriestsOfSyrinx", 'feed_id' => -4, 'limit' => 5], + ['op' => "getCompactHeadlines", 'sid' => "PriestsOfSyrinx", 'feed_id' => -4, 'skip' => 2], + ['op' => "getCompactHeadlines", 'sid' => "PriestsOfSyrinx", 'feed_id' => -4, 'limit' => 5, 'skip' => 2], + ['op' => "getCompactHeadlines", 'sid' => "PriestsOfSyrinx", 'feed_id' => -4, 'since_id' => 47], + ]; + $in2 = [ + // time-based contexts, handled separately + ['op' => "getCompactHeadlines", 'sid' => "PriestsOfSyrinx", 'feed_id' => -6], + ['op' => "getCompactHeadlines", 'sid' => "PriestsOfSyrinx", 'feed_id' => -6, 'view_mode' => "adaptive"], + ['op' => "getCompactHeadlines", 'sid' => "PriestsOfSyrinx", 'feed_id' => -3], + ['op' => "getCompactHeadlines", 'sid' => "PriestsOfSyrinx", 'feed_id' => -3, 'view_mode' => "marked"], + ]; + Phake::when(Arsse::$db)->articleList->thenReturn(new Result([['id' => 0]])); + Phake::when(Arsse::$db)->articleCount->thenReturn(0); + Phake::when(Arsse::$db)->articleCount($this->anything(), (new Context)->unread(true))->thenReturn(1); + $c = (new Context)->reverse(true); + Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->subscription(2112), Database::LIST_MINIMAL)->thenThrow(new ExceptionInput("subjectMissing")); + Phake::when(Arsse::$db)->articleList($this->anything(), $c, Database::LIST_MINIMAL)->thenReturn(new Result($this->articles)); + Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->starred(true), Database::LIST_MINIMAL)->thenReturn(new Result([['id' => 1]])); + Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->label(1088), Database::LIST_MINIMAL)->thenReturn(new Result([['id' => 2]])); + Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->unread(true), Database::LIST_MINIMAL)->thenReturn(new Result([['id' => 3]])); + Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->label(1088)->unread(true), Database::LIST_MINIMAL)->thenReturn(new Result([['id' => 4]])); + Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->subscription(42)->starred(true), Database::LIST_MINIMAL)->thenReturn(new Result([['id' => 5]])); + Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->subscription(42)->annotated(true), Database::LIST_MINIMAL)->thenReturn(new Result([['id' => 6]])); + Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->limit(5), Database::LIST_MINIMAL)->thenReturn(new Result([['id' => 7]])); + Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->offset(2), Database::LIST_MINIMAL)->thenReturn(new Result([['id' => 8]])); + Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->limit(5)->offset(2), Database::LIST_MINIMAL)->thenReturn(new Result([['id' => 9]])); + Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->oldestArticle(48), Database::LIST_MINIMAL)->thenReturn(new Result([['id' => 10]])); + $out1 = [ + $this->respErr("INCORRECT_USAGE"), + $this->respGood([]), + $this->respGood([]), + $this->respGood([]), + $this->respGood([]), + $this->respGood([]), + $this->respGood([]), + $this->respGood([['id' => 101],['id' => 102]]), + $this->respGood([['id' => 1]]), + $this->respGood([['id' => 2]]), + $this->respGood([['id' => 3]]), + $this->respGood([['id' => 2]]), // the result is 2 rather than 4 because there are no unread, so the unread context is not used + $this->respGood([['id' => 4]]), + $this->respGood([['id' => 5]]), + $this->respGood([['id' => 6]]), + $this->respGood([['id' => 7]]), + $this->respGood([['id' => 8]]), + $this->respGood([['id' => 9]]), + $this->respGood([['id' => 10]]), + ]; + $out2 = [ + $this->respGood([['id' => 1001]]), + $this->respGood([['id' => 1001]]), + $this->respGood([['id' => 1002]]), + $this->respGood([['id' => 1003]]), + ]; + for ($a = 0; $a < sizeof($in1); $a++) { + $this->assertResponse($out1[$a], $this->req($in1[$a]), "Test $a failed"); + } + for ($a = 0; $a < sizeof($in2); $a++) { + Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->unread(false)->markedSince(Date::sub("PT24H")), Database::LIST_MINIMAL)->thenReturn(new Result([['id' => 1001]])); + Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->unread(true)->modifiedSince(Date::sub("PT24H")), Database::LIST_MINIMAL)->thenReturn(new Result([['id' => 1002]])); + Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->unread(true)->modifiedSince(Date::sub("PT24H"))->starred(true), Database::LIST_MINIMAL)->thenReturn(new Result([['id' => 1003]])); + $this->assertResponse($out2[$a], $this->req($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->req($in1[$a]), "Test $a failed"); + } + for ($a = 0; $a < sizeof($in2); $a++) { + $this->assertResponse($out2[$a], $this->req($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->assertResponse($out3[$a], $this->req($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->req($in[0]); + $this->assertResponse($this->outputHeadlines(1), $test); + // test 'show_content' + $test = $this->req($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->req($in[2]); + $exp = [ + [ + 'id' => "0", + '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->req($in[3]); + $exp = $this->outputHeadlines(1); + $exp->payload['content'] = [ + ['id' => -4, 'is_cat' => false, 'first_id' => 1], + $exp->payload['content'], + ]; + $this->assertResponse($exp, $test); + // test 'include_header' with a category + $test = $this->req($in[4]); + $exp = $this->outputHeadlines(1); + $exp->payload['content'] = [ + ['id' => -3, 'is_cat' => true, 'first_id' => 1], + $exp->payload['content'], + ]; + $this->assertResponse($exp, $test); + // test 'include_header' with an empty result + $test = $this->req($in[5]); + $exp = $this->respGood([ + ['id' => -1, 'is_cat' => true, 'first_id' => 0], + [], + ]); + $this->assertResponse($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->req($in[6]); + $exp = $this->respGood([ + ['id' => 2112, 'is_cat' => false, 'first_id' => 0], + [], + ]); + $this->assertResponse($exp, $test); + // test 'include_header' with ascending order + $test = $this->req($in[7]); + $exp = $this->outputHeadlines(1); + $exp->payload['content'] = [ + ['id' => -4, 'is_cat' => false, 'first_id' => 0], + $exp->payload['content'], + ]; + $this->assertResponse($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->req($in[8]); + $exp = $this->outputHeadlines(1); + $exp->payload['content'] = [ + ['id' => 42, 'is_cat' => false, 'first_id' => 1867], + $exp->payload['content'], + ]; + $this->assertResponse($exp, $test); + // test 'include_header' with skip and ascending order + $test = $this->req($in[9]); + $exp = $this->outputHeadlines(1); + $exp->payload['content'] = [ + ['id' => 42, 'is_cat' => false, 'first_id' => 0], + $exp->payload['content'], + ]; + $this->assertResponse($exp, $test); + // test 'show_excerpt' + $exp1 = "“This & that, you know‽”"; + $exp2 = "Pour vous faire mieux connaitre d’ou\u{300} vient l’erreur de ceux qui bla\u{302}ment la volupte\u{301}, et qui louent en…"; + $test = $this->req($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' => '“This & that, you know‽”
', + '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' => "", + 'always_display_attachments' => false, + ], + [ + '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' => "", + 'always_display_attachments' => false, + ], + ]); + } +} diff --git a/tests/REST/TinyTinyRSS/TestTinyTinyIcon.php b/tests/REST/TinyTinyRSS/TestTinyTinyIcon.php new file mode 100644 index 00000000..1b439b30 --- /dev/null +++ b/tests/REST/TinyTinyRSS/TestTinyTinyIcon.php @@ -0,0 +1,52 @@ + */ +class TestTinyTinyIcon extends Test\AbstractTest { + protected $h; + + public function setUp() { + $this->clearData(); + Arsse::$conf = new Conf(); + // create a mock user manager + // create a mock database interface + Arsse::$db = Phake::mock(Database::class); + $this->h = new REST\TinyTinyRSS\Icon(); + } + + public function tearDown() { + $this->clearData(); + } + + public function testRetrieveFavion() { + Phake::when(Arsse::$db)->subscriptionFavicon->thenReturn(""); + Phake::when(Arsse::$db)->subscriptionFavicon(42)->thenReturn("http://example.com/favicon.ico"); + Phake::when(Arsse::$db)->subscriptionFavicon(2112)->thenReturn("http://example.net/logo.png"); + Phake::when(Arsse::$db)->subscriptionFavicon(1337)->thenReturn("http://example.org/icon.gif\r\nLocation: http://bad.example.com/"); + // these requests should succeed + $exp = new Response(301, "", "", ["Location: http://example.com/favicon.ico"]); + $this->assertEquals($exp, $this->h->dispatch(new Request("GET", "42.ico"))); + $exp = new Response(301, "", "", ["Location: http://example.net/logo.png"]); + $this->assertEquals($exp, $this->h->dispatch(new Request("GET", "2112.ico"))); + $exp = new Response(301, "", "", ["Location: http://example.org/icon.gif"]); + $this->assertEquals($exp, $this->h->dispatch(new Request("GET", "1337.ico"))); + // these requests should fail + $exp = new Response(404); + $this->assertEquals($exp, $this->h->dispatch(new Request("GET", "ook.ico"))); + $this->assertEquals($exp, $this->h->dispatch(new Request("GET", "ook"))); + $this->assertEquals($exp, $this->h->dispatch(new Request("GET", "47.ico"))); + $this->assertEquals($exp, $this->h->dispatch(new Request("GET", "2112.png"))); + // only GET is allowed + $exp = new Response(405, "", "", ["Allow: GET"]); + $this->assertEquals($exp, $this->h->dispatch(new Request("PUT", "2112.ico"))); + } +} diff --git a/tests/lib/Database/SeriesArticle.php b/tests/lib/Database/SeriesArticle.php index 30da2840..caf65372 100644 --- a/tests/lib/Database/SeriesArticle.php +++ b/tests/lib/Database/SeriesArticle.php @@ -6,6 +6,7 @@ declare(strict_types=1); namespace JKingWeb\Arsse\Test\Database; +use JKingWeb\Arsse\Database; use JKingWeb\Arsse\Arsse; use JKingWeb\Arsse\Misc\Context; use JKingWeb\Arsse\Misc\Date; @@ -49,21 +50,22 @@ trait SeriesArticle { 'columns' => [ 'id' => "int", 'url' => "str", + 'title' => "str", ], 'rows' => [ - [1,"http://example.com/1"], - [2,"http://example.com/2"], - [3,"http://example.com/3"], - [4,"http://example.com/4"], - [5,"http://example.com/5"], - [6,"http://example.com/6"], - [7,"http://example.com/7"], - [8,"http://example.com/8"], - [9,"http://example.com/9"], - [10,"http://example.com/10"], - [11,"http://example.com/11"], - [12,"http://example.com/12"], - [13,"http://example.com/13"], + [1,"http://example.com/1", "Feed 1"], + [2,"http://example.com/2", "Feed 2"], + [3,"http://example.com/3", "Feed 3"], + [4,"http://example.com/4", "Feed 4"], + [5,"http://example.com/5", "Feed 5"], + [6,"http://example.com/6", "Feed 6"], + [7,"http://example.com/7", "Feed 7"], + [8,"http://example.com/8", "Feed 8"], + [9,"http://example.com/9", "Feed 9"], + [10,"http://example.com/10", "Feed 10"], + [11,"http://example.com/11", "Feed 11"], + [12,"http://example.com/12", "Feed 12"], + [13,"http://example.com/13", "Feed 13"], ] ], 'arsse_subscriptions' => [ @@ -72,22 +74,23 @@ trait SeriesArticle { 'owner' => "str", 'feed' => "int", 'folder' => "int", + 'title' => "str", ], 'rows' => [ - [1,"john.doe@example.com",1,null], - [2,"john.doe@example.com",2,null], - [3,"john.doe@example.com",3,1], - [4,"john.doe@example.com",4,6], - [5,"john.doe@example.com",10,5], - [6,"jane.doe@example.com",1,null], - [7,"jane.doe@example.com",10,null], - [8,"john.doe@example.org",11,null], - [9,"john.doe@example.org",12,null], - [10,"john.doe@example.org",13,null], - [11,"john.doe@example.net",10,null], - [12,"john.doe@example.net",2,9], - [13,"john.doe@example.net",3,8], - [14,"john.doe@example.net",4,7], + [1, "john.doe@example.com",1, null,"Subscription 1"], + [2, "john.doe@example.com",2, null,null], + [3, "john.doe@example.com",3, 1,"Subscription 3"], + [4, "john.doe@example.com",4, 6,null], + [5, "john.doe@example.com",10, 5,"Subscription 5"], + [6, "jane.doe@example.com",1, null,null], + [7, "jane.doe@example.com",10,null,"Subscription 7"], + [8, "john.doe@example.org",11,null,null], + [9, "john.doe@example.org",12,null,"Subscription 9"], + [10,"john.doe@example.org",13,null,null], + [11,"john.doe@example.net",10,null,"Subscription 11"], + [12,"john.doe@example.net",2, 9,null], + [13,"john.doe@example.net",3, 8,"Subscription 13"], + [14,"john.doe@example.net",4, 7,null], ] ], 'arsse_articles' => [ @@ -193,29 +196,76 @@ trait SeriesArticle { 'article' => "int", 'read' => "bool", 'starred' => "bool", - 'modified' => "datetime" + 'modified' => "datetime", + 'note' => "str", ], 'rows' => [ - [1, 1,1,1,'2000-01-01 00:00:00'], - [5, 19,1,0,'2000-01-01 00:00:00'], - [5, 20,0,1,'2010-01-01 00:00:00'], - [7, 20,1,0,'2010-01-01 00:00:00'], - [8, 102,1,0,'2000-01-02 02:00:00'], - [9, 103,0,1,'2000-01-03 03:00:00'], - [9, 104,1,1,'2000-01-04 04:00:00'], - [10,105,0,0,'2000-01-05 05:00:00'], - [11, 19,0,0,'2017-01-01 00:00:00'], - [11, 20,1,0,'2017-01-01 00:00:00'], - [12, 3,0,1,'2017-01-01 00:00:00'], - [12, 4,1,1,'2017-01-01 00:00:00'], + [1, 1,1,1,'2000-01-01 00:00:00',''], + [5, 19,1,0,'2016-01-01 00:00:00',''], + [5, 20,0,1,'2005-01-01 00:00:00',''], + [7, 20,1,0,'2010-01-01 00:00:00',''], + [8, 102,1,0,'2000-01-02 02:00:00','Note 2'], + [9, 103,0,1,'2000-01-03 03:00:00','Note 3'], + [9, 104,1,1,'2000-01-04 04:00:00','Note 4'], + [10,105,0,0,'2000-01-05 05:00:00',''], + [11, 19,0,0,'2017-01-01 00:00:00','ook'], + [11, 20,1,0,'2017-01-01 00:00:00','eek'], + [12, 3,0,1,'2017-01-01 00:00:00','ack'], + [12, 4,1,1,'2017-01-01 00:00:00','ach'], + [1, 2,0,0,'2010-01-01 00:00:00','Some Note'], ] ], + 'arsse_categories' => [ // author-supplied categories + 'columns' => [ + 'article' => "int", + 'name' => "str", + ], + 'rows' => [ + [19,"Fascinating"], + [19,"Logical"], + [20,"Interesting"], + [20,"Logical"], + ], + ], + 'arsse_labels' => [ + 'columns' => [ + 'id' => "int", + 'owner' => "str", + 'name' => "str", + ], + 'rows' => [ + [1,"john.doe@example.com","Interesting"], + [2,"john.doe@example.com","Fascinating"], + [3,"jane.doe@example.com","Boring"], + [4,"john.doe@example.com","Lonely"], + ], + ], + 'arsse_label_members' => [ + 'columns' => [ + 'label' => "int", + 'article' => "int", + 'subscription' => "int", + 'assigned' => "bool", + 'modified' => "datetime", + ], + 'rows' => [ + [1, 1,1,1,'2000-01-01 00:00:00'], + [2, 1,1,1,'2000-01-01 00:00:00'], + [1,19,5,1,'2000-01-01 00:00:00'], + [2,20,5,1,'2000-01-01 00:00:00'], + [1, 5,3,0,'2000-01-01 00:00:00'], + [2, 5,3,1,'2000-01-01 00:00:00'], + [4, 7,4,0,'2000-01-01 00:00:00'], + [4, 8,4,1,'2015-01-01 00:00:00'], + ], + ], ]; protected $matches = [ [ 'id' => 101, 'url' => 'http://example.com/1', 'title' => 'Article title 1', + 'subscription_title' => "Feed 11", 'author' => '', 'content' => 'Article content 1
', 'guid' => 'e433653cef2e572eee4215fa299a4a5af9137b2cefd6283c85bd69a32915beda', @@ -229,11 +279,13 @@ trait SeriesArticle { 'fingerprint' => 'f5cb8bfc1c7396dc9816af212a3e2ac5221585c2a00bf7ccb6aabd95dcfcd6a6:fb0bc8f8cb08913dc5a497db700e327f1d34e4987402687d494a5891f24714d4:18fdd4fa93d693128c43b004399e5c9cea6c261ddfa002518d3669f55d8c2207', 'media_url' => null, 'media_type' => null, + 'note' => "", ], [ 'id' => 102, 'url' => 'http://example.com/2', 'title' => 'Article title 2', + 'subscription_title' => "Feed 11", 'author' => '', 'content' => 'Article content 2
', 'guid' => '5be8a5a46ecd52ed132191c8d27fb1af6b3d4edc00234c5d9f8f0e10562ed3b7', @@ -247,11 +299,13 @@ trait SeriesArticle { 'fingerprint' => '0e86d2de822a174fe3c44a466953e63ca1f1a58a19cbf475fce0855d4e3d5153:13075894189c47ffcfafd1dfe7fbb539f7c74a69d35a399b3abf8518952714f9:2abd0a8cba83b8214a66c8f0293ba63e467d720540e29ff8ddcdab069d4f1c9e', 'media_url' => "http://example.com/text", 'media_type' => "text/plain", + 'note' => "Note 2", ], [ 'id' => 103, 'url' => 'http://example.com/3', 'title' => 'Article title 3', + 'subscription_title' => "Subscription 9", 'author' => '', 'content' => 'Article content 3
', 'guid' => '31a6594500a48b59fcc8a075ce82b946c9c3c782460d088bd7b8ef3ede97ad92', @@ -265,11 +319,13 @@ trait SeriesArticle { 'fingerprint' => 'f74b06b240bd08abf4d3fdfc20dba6a6f6eb8b4f1a00e9a617efd63a87180a4b:b278380e984cefe63f0e412b88ffc9cb0befdfa06fdc00bace1da99a8daff406:ad622b31e739cd3a3f3c788991082cf4d2f7a8773773008e75f0572e58cd373b', 'media_url' => "http://example.com/video", 'media_type' => "video/webm", + 'note' => "Note 3", ], [ 'id' => 104, 'url' => 'http://example.com/4', 'title' => 'Article title 4', + 'subscription_title' => "Subscription 9", 'author' => '', 'content' => 'Article content 4
', 'guid' => '804e517d623390e71497982c77cf6823180342ebcd2e7d5e32da1e55b09dd180', @@ -283,11 +339,13 @@ trait SeriesArticle { 'fingerprint' => 'f3615c7f16336d3ea242d35cf3fc17dbc4ee3afb78376bf49da2dd7a5a25dec8:f11c2b4046f207579aeb9c69a8c20ca5461cef49756ccfa5ba5e2344266da3b3:ab2da63276acce431250b18d3d49b988b226a99c7faadf275c90b751aee05be9', 'media_url' => "http://example.com/image", 'media_type' => "image/svg+xml", + 'note' => "Note 4", ], [ 'id' => 105, 'url' => 'http://example.com/5', 'title' => 'Article title 5', + 'subscription_title' => "Feed 13", 'author' => '', 'content' => 'Article content 5
', 'guid' => 'db3e736c2c492f5def5c5da33ddcbea1824040e9ced2142069276b0a6e291a41', @@ -301,11 +359,32 @@ trait SeriesArticle { 'fingerprint' => 'd40da96e39eea6c55948ccbe9b3d275b5f931298288dbe953990c5f496097022:834240f84501b5341d375414718204ec421561f3825d34c22bf9182203e42900:43b970ac6ec5f8a9647b2c7e4eed8b1d7f62e154a95eed748b0294c1256764ba', 'media_url' => "http://example.com/audio", 'media_type' => "audio/ogg", + 'note' => "", + ], + ]; + protected $fields = [ + Database::LIST_MINIMAL => [ + "id", "subscription", "feed", "modified_date", "marked_date", "unread", "starred", "edition", "edited_date", + ], + Database::LIST_CONSERVATIVE => [ + "id", "subscription", "feed", "modified_date", "marked_date", "unread", "starred", "edition", "edited_date", + "url", "title", "subscription_title", "author", "guid", "published_date", "fingerprint", + ], + Database::LIST_TYPICAL => [ + "id", "subscription", "feed", "modified_date", "marked_date", "unread", "starred", "edition", "edited_date", + "url", "title", "subscription_title", "author", "guid", "published_date", "fingerprint", + "content", "media_url", "media_type", + ], + Database::LIST_FULL => [ + "id", "subscription", "feed", "modified_date", "marked_date", "unread", "starred", "edition", "edited_date", + "url", "title", "subscription_title", "author", "guid", "published_date", "fingerprint", + "content", "media_url", "media_type", + "note", ], ]; public function setUpSeries() { - $this->checkTables = ['arsse_marks' => ["subscription","article","read","starred","modified"],]; + $this->checkTables = ['arsse_marks' => ["subscription","article","read","starred","modified","note"],]; $this->user = "john.doe@example.net"; } @@ -321,12 +400,14 @@ trait SeriesArticle { // get all items for user $exp = [1,2,3,4,5,6,7,8,19,20]; $this->compareIds($exp, new Context); + $this->compareIds($exp, (new Context)->articles(range(1, Database::LIMIT_ARTICLES * 3))); // get items from a folder tree - $exp = [5,6,7,8]; - $this->compareIds($exp, (new Context)->folder(1)); + $this->compareIds([5,6,7,8], (new Context)->folder(1)); // get items from a leaf folder - $exp = [7,8]; - $this->compareIds($exp, (new Context)->folder(6)); + $this->compareIds([7,8], (new Context)->folder(6)); + // get items from a non-leaf folder without descending + $this->compareIds([1,2,3,4], (new Context)->folderShallow(0)); + $this->compareIds([5,6], (new Context)->folderShallow(1)); // get items from a single subscription $exp = [19,20]; $this->compareIds($exp, (new Context)->subscription(5)); @@ -342,13 +423,21 @@ trait SeriesArticle { $this->compareIds([19], (new Context)->subscription(5)->latestEdition(19)); $this->compareIds([20], (new Context)->subscription(5)->oldestEdition(999)); $this->compareIds([20], (new Context)->subscription(5)->oldestEdition(1001)); - // get items relative to modification date + // get items relative to article ID + $this->compareIds([1,2,3], (new Context)->latestArticle(3)); + $this->compareIds([19,20], (new Context)->oldestArticle(19)); + // get items relative to (feed) modification date $exp = [2,4,6,8,20]; $this->compareIds($exp, (new Context)->modifiedSince("2005-01-01T00:00:00Z")); $this->compareIds($exp, (new Context)->modifiedSince("2010-01-01T00:00:00Z")); $exp = [1,3,5,7,19]; $this->compareIds($exp, (new Context)->notModifiedSince("2005-01-01T00:00:00Z")); $this->compareIds($exp, (new Context)->notModifiedSince("2000-01-01T00:00:00Z")); + // get items relative to (user) modification date (both marks and labels apply) + $this->compareIds([8,19], (new Context)->markedSince("2014-01-01T00:00:00Z")); + $this->compareIds([2,4,6,8,19,20], (new Context)->markedSince("2010-01-01T00:00:00Z")); + $this->compareIds([1,2,3,4,5,6,7,20], (new Context)->notMarkedSince("2014-01-01T00:00:00Z")); + $this->compareIds([1,3,5,7], (new Context)->notMarkedSince("2005-01-01T00:00:00Z")); // paged results $this->compareIds([1], (new Context)->limit(1)); $this->compareIds([2], (new Context)->limit(1)->oldestEdition(1+1)); @@ -359,6 +448,24 @@ trait SeriesArticle { $this->compareIds([19], (new Context)->reverse(true)->limit(1)->latestEdition(1001-1)); $this->compareIds([8], (new Context)->reverse(true)->limit(1)->latestEdition(19-1)); $this->compareIds([7,6], (new Context)->reverse(true)->limit(2)->latestEdition(8-1)); + // get articles by label ID + $this->compareIds([1,19], (new Context)->label(1)); + $this->compareIds([1,5,20], (new Context)->label(2)); + // get articles by label name + $this->compareIds([1,19], (new Context)->labelName("Interesting")); + $this->compareIds([1,5,20], (new Context)->labelName("Fascinating")); + // get articles with any or no label + $this->compareIds([1,5,8,19,20], (new Context)->labelled(true)); + $this->compareIds([2,3,4,6,7], (new Context)->labelled(false)); + // get a specific article or edition + $this->compareIds([20], (new Context)->article(20)); + $this->compareIds([20], (new Context)->edition(1001)); + // get multiple specific articles or editions + $this->compareIds([1,20], (new Context)->articles([1,20,50])); + $this->compareIds([1,20], (new Context)->editions([1,1001,50])); + // get articles base on whether or not they have notes + $this->compareIds([1,3,4,5,6,7,8,19,20], (new Context)->annotated(false)); + $this->compareIds([2], (new Context)->annotated(true)); } public function testListArticlesOfAMissingFolder() { @@ -374,6 +481,16 @@ trait SeriesArticle { public function testListArticlesCheckingProperties() { $this->user = "john.doe@example.org"; $this->assertResult($this->matches, Arsse::$db->articleList($this->user)); + // check that the different fieldset groups return the expected columns + foreach ($this->fields as $constant => $columns) { + $test = array_keys(Arsse::$db->articleList($this->user, (new Context)->article(101), $constant)->getRow()); + sort($columns); + sort($test); + $this->assertEquals($columns, $test, "Fields do not match expectation for verbosity $constant"); + } + // check that an unknown fieldset produces an exception + $this->assertException("constantUnknown"); + Arsse::$db->articleList($this->user, (new Context)->article(101), \PHP_INT_MAX); } public function testListArticlesWithoutAuthority() { @@ -401,10 +518,10 @@ trait SeriesArticle { $state['arsse_marks']['rows'][8][4] = $now; $state['arsse_marks']['rows'][10][2] = 1; $state['arsse_marks']['rows'][10][4] = $now; - $state['arsse_marks']['rows'][] = [13,5,1,0,$now]; - $state['arsse_marks']['rows'][] = [13,6,1,0,$now]; - $state['arsse_marks']['rows'][] = [14,7,1,0,$now]; - $state['arsse_marks']['rows'][] = [14,8,1,0,$now]; + $state['arsse_marks']['rows'][] = [13,5,1,0,$now,'']; + $state['arsse_marks']['rows'][] = [13,6,1,0,$now,'']; + $state['arsse_marks']['rows'][] = [14,7,1,0,$now,'']; + $state['arsse_marks']['rows'][] = [14,8,1,0,$now,'']; $this->compareExpectations($state); } @@ -427,10 +544,10 @@ trait SeriesArticle { $state['arsse_marks']['rows'][8][4] = $now; $state['arsse_marks']['rows'][9][3] = 1; $state['arsse_marks']['rows'][9][4] = $now; - $state['arsse_marks']['rows'][] = [13,5,0,1,$now]; - $state['arsse_marks']['rows'][] = [13,6,0,1,$now]; - $state['arsse_marks']['rows'][] = [14,7,0,1,$now]; - $state['arsse_marks']['rows'][] = [14,8,0,1,$now]; + $state['arsse_marks']['rows'][] = [13,5,0,1,$now,'']; + $state['arsse_marks']['rows'][] = [13,6,0,1,$now,'']; + $state['arsse_marks']['rows'][] = [14,7,0,1,$now,'']; + $state['arsse_marks']['rows'][] = [14,8,0,1,$now,'']; $this->compareExpectations($state); } @@ -459,10 +576,10 @@ trait SeriesArticle { $state['arsse_marks']['rows'][9][4] = $now; $state['arsse_marks']['rows'][10][2] = 1; $state['arsse_marks']['rows'][10][4] = $now; - $state['arsse_marks']['rows'][] = [13,5,1,1,$now]; - $state['arsse_marks']['rows'][] = [13,6,1,1,$now]; - $state['arsse_marks']['rows'][] = [14,7,1,1,$now]; - $state['arsse_marks']['rows'][] = [14,8,1,1,$now]; + $state['arsse_marks']['rows'][] = [13,5,1,1,$now,'']; + $state['arsse_marks']['rows'][] = [13,6,1,1,$now,'']; + $state['arsse_marks']['rows'][] = [14,7,1,1,$now,'']; + $state['arsse_marks']['rows'][] = [14,8,1,1,$now,'']; $this->compareExpectations($state); } @@ -477,10 +594,10 @@ trait SeriesArticle { $state['arsse_marks']['rows'][9][4] = $now; $state['arsse_marks']['rows'][11][2] = 0; $state['arsse_marks']['rows'][11][4] = $now; - $state['arsse_marks']['rows'][] = [13,5,0,1,$now]; - $state['arsse_marks']['rows'][] = [13,6,0,1,$now]; - $state['arsse_marks']['rows'][] = [14,7,0,1,$now]; - $state['arsse_marks']['rows'][] = [14,8,0,1,$now]; + $state['arsse_marks']['rows'][] = [13,5,0,1,$now,'']; + $state['arsse_marks']['rows'][] = [13,6,0,1,$now,'']; + $state['arsse_marks']['rows'][] = [14,7,0,1,$now,'']; + $state['arsse_marks']['rows'][] = [14,8,0,1,$now,'']; $this->compareExpectations($state); } @@ -495,10 +612,29 @@ trait SeriesArticle { $state['arsse_marks']['rows'][10][4] = $now; $state['arsse_marks']['rows'][11][3] = 0; $state['arsse_marks']['rows'][11][4] = $now; - $state['arsse_marks']['rows'][] = [13,5,1,0,$now]; - $state['arsse_marks']['rows'][] = [13,6,1,0,$now]; - $state['arsse_marks']['rows'][] = [14,7,1,0,$now]; - $state['arsse_marks']['rows'][] = [14,8,1,0,$now]; + $state['arsse_marks']['rows'][] = [13,5,1,0,$now,'']; + $state['arsse_marks']['rows'][] = [13,6,1,0,$now,'']; + $state['arsse_marks']['rows'][] = [14,7,1,0,$now,'']; + $state['arsse_marks']['rows'][] = [14,8,1,0,$now,'']; + $this->compareExpectations($state); + } + + public function testSetNoteForAllArticles() { + Arsse::$db->articleMark($this->user, ['note'=>"New note"]); + $now = Date::transform(time(), "sql"); + $state = $this->primeExpectations($this->data, $this->checkTables); + $state['arsse_marks']['rows'][8][5] = "New note"; + $state['arsse_marks']['rows'][8][4] = $now; + $state['arsse_marks']['rows'][9][5] = "New note"; + $state['arsse_marks']['rows'][9][4] = $now; + $state['arsse_marks']['rows'][10][5] = "New note"; + $state['arsse_marks']['rows'][10][4] = $now; + $state['arsse_marks']['rows'][11][5] = "New note"; + $state['arsse_marks']['rows'][11][4] = $now; + $state['arsse_marks']['rows'][] = [13,5,0,0,$now,'New note']; + $state['arsse_marks']['rows'][] = [13,6,0,0,$now,'New note']; + $state['arsse_marks']['rows'][] = [14,7,0,0,$now,'New note']; + $state['arsse_marks']['rows'][] = [14,8,0,0,$now,'New note']; $this->compareExpectations($state); } @@ -506,10 +642,10 @@ trait SeriesArticle { Arsse::$db->articleMark($this->user, ['read'=>true], (new Context)->folder(7)); $now = Date::transform(time(), "sql"); $state = $this->primeExpectations($this->data, $this->checkTables); - $state['arsse_marks']['rows'][] = [13,5,1,0,$now]; - $state['arsse_marks']['rows'][] = [13,6,1,0,$now]; - $state['arsse_marks']['rows'][] = [14,7,1,0,$now]; - $state['arsse_marks']['rows'][] = [14,8,1,0,$now]; + $state['arsse_marks']['rows'][] = [13,5,1,0,$now,'']; + $state['arsse_marks']['rows'][] = [13,6,1,0,$now,'']; + $state['arsse_marks']['rows'][] = [14,7,1,0,$now,'']; + $state['arsse_marks']['rows'][] = [14,8,1,0,$now,'']; $this->compareExpectations($state); } @@ -517,8 +653,8 @@ trait SeriesArticle { Arsse::$db->articleMark($this->user, ['read'=>true], (new Context)->folder(8)); $now = Date::transform(time(), "sql"); $state = $this->primeExpectations($this->data, $this->checkTables); - $state['arsse_marks']['rows'][] = [13,5,1,0,$now]; - $state['arsse_marks']['rows'][] = [13,6,1,0,$now]; + $state['arsse_marks']['rows'][] = [13,5,1,0,$now,'']; + $state['arsse_marks']['rows'][] = [13,6,1,0,$now,'']; $this->compareExpectations($state); } @@ -531,8 +667,8 @@ trait SeriesArticle { Arsse::$db->articleMark($this->user, ['read'=>true], (new Context)->subscription(13)); $now = Date::transform(time(), "sql"); $state = $this->primeExpectations($this->data, $this->checkTables); - $state['arsse_marks']['rows'][] = [13,5,1,0,$now]; - $state['arsse_marks']['rows'][] = [13,6,1,0,$now]; + $state['arsse_marks']['rows'][] = [13,5,1,0,$now,'']; + $state['arsse_marks']['rows'][] = [13,6,1,0,$now,'']; $this->compareExpectations($state); } @@ -556,7 +692,7 @@ trait SeriesArticle { $state = $this->primeExpectations($this->data, $this->checkTables); $state['arsse_marks']['rows'][9][3] = 1; $state['arsse_marks']['rows'][9][4] = $now; - $state['arsse_marks']['rows'][] = [14,7,0,1,$now]; + $state['arsse_marks']['rows'][] = [14,7,0,1,$now,'']; $this->compareExpectations($state); } @@ -569,7 +705,7 @@ trait SeriesArticle { $state['arsse_marks']['rows'][9][4] = $now; $state['arsse_marks']['rows'][11][2] = 0; $state['arsse_marks']['rows'][11][4] = $now; - $state['arsse_marks']['rows'][] = [14,7,0,1,$now]; + $state['arsse_marks']['rows'][] = [14,7,0,1,$now,'']; $this->compareExpectations($state); } @@ -579,8 +715,7 @@ trait SeriesArticle { } public function testMarkTooManyMultipleArticles() { - $this->assertException("tooLong", "Db", "ExceptionInput"); - Arsse::$db->articleMark($this->user, ['read'=>false,'starred'=>true], (new Context)->articles(range(1, 51))); + $this->assertSame(7, Arsse::$db->articleMark($this->user, ['read'=>false,'starred'=>true], (new Context)->articles(range(1, Database::LIMIT_ARTICLES * 3)))); } public function testMarkAMissingArticle() { @@ -603,7 +738,7 @@ trait SeriesArticle { $state = $this->primeExpectations($this->data, $this->checkTables); $state['arsse_marks']['rows'][9][3] = 1; $state['arsse_marks']['rows'][9][4] = $now; - $state['arsse_marks']['rows'][] = [14,7,0,1,$now]; + $state['arsse_marks']['rows'][] = [14,7,0,1,$now,'']; $this->compareExpectations($state); } @@ -635,7 +770,7 @@ trait SeriesArticle { $state['arsse_marks']['rows'][9][4] = $now; $state['arsse_marks']['rows'][11][2] = 0; $state['arsse_marks']['rows'][11][4] = $now; - $state['arsse_marks']['rows'][] = [14,7,0,1,$now]; + $state['arsse_marks']['rows'][] = [14,7,0,1,$now,'']; $this->compareExpectations($state); } @@ -645,8 +780,7 @@ trait SeriesArticle { } public function testMarkTooManyMultipleEditions() { - $this->assertException("tooLong", "Db", "ExceptionInput"); - Arsse::$db->articleMark($this->user, ['read'=>false,'starred'=>true], (new Context)->editions(range(1, 51))); + $this->assertSame(7, Arsse::$db->articleMark($this->user, ['read'=>false,'starred'=>true], (new Context)->editions(range(1, 51)))); } public function testMarkAStaleEditionUnread() { @@ -701,15 +835,15 @@ trait SeriesArticle { $state = $this->primeExpectations($this->data, $this->checkTables); $state['arsse_marks']['rows'][8][3] = 1; $state['arsse_marks']['rows'][8][4] = $now; - $state['arsse_marks']['rows'][] = [13,5,0,1,$now]; - $state['arsse_marks']['rows'][] = [13,6,0,1,$now]; - $state['arsse_marks']['rows'][] = [14,7,0,1,$now]; - $state['arsse_marks']['rows'][] = [14,8,0,1,$now]; + $state['arsse_marks']['rows'][] = [13,5,0,1,$now,'']; + $state['arsse_marks']['rows'][] = [13,6,0,1,$now,'']; + $state['arsse_marks']['rows'][] = [14,7,0,1,$now,'']; + $state['arsse_marks']['rows'][] = [14,8,0,1,$now,'']; $this->compareExpectations($state); } - public function testMarkByLastModified() { - Arsse::$db->articleMark($this->user, ['starred'=>true], (new Context)->modifiedSince('2017-01-01T00:00:00Z')); + public function testMarkByLastMarked() { + Arsse::$db->articleMark($this->user, ['starred'=>true], (new Context)->markedSince('2017-01-01T00:00:00Z')); $now = Date::transform(time(), "sql"); $state = $this->primeExpectations($this->data, $this->checkTables); $state['arsse_marks']['rows'][8][3] = 1; @@ -719,12 +853,12 @@ trait SeriesArticle { $this->compareExpectations($state); } - public function testMarkByNotLastModified() { - Arsse::$db->articleMark($this->user, ['starred'=>true], (new Context)->notModifiedSince('2000-01-01T00:00:00Z')); + public function testMarkByNotLastMarked() { + Arsse::$db->articleMark($this->user, ['starred'=>true], (new Context)->notMarkedSince('2000-01-01T00:00:00Z')); $now = Date::transform(time(), "sql"); $state = $this->primeExpectations($this->data, $this->checkTables); - $state['arsse_marks']['rows'][] = [13,5,0,1,$now]; - $state['arsse_marks']['rows'][] = [14,7,0,1,$now]; + $state['arsse_marks']['rows'][] = [13,5,0,1,$now,'']; + $state['arsse_marks']['rows'][] = [14,7,0,1,$now,'']; $this->compareExpectations($state); } @@ -734,17 +868,30 @@ trait SeriesArticle { Arsse::$db->articleMark($this->user, ['read'=>false]); } - public function testCountStarredArticles() { - $this->assertSame(2, Arsse::$db->articleStarredCount("john.doe@example.com")); - $this->assertSame(2, Arsse::$db->articleStarredCount("john.doe@example.org")); - $this->assertSame(2, Arsse::$db->articleStarredCount("john.doe@example.net")); - $this->assertSame(0, Arsse::$db->articleStarredCount("jane.doe@example.com")); + public function testCountArticles() { + $this->assertSame(2, Arsse::$db->articleCount("john.doe@example.com", (new Context)->starred(true))); + $this->assertSame(4, Arsse::$db->articleCount("john.doe@example.com", (new Context)->folder(1))); + $this->assertSame(0, Arsse::$db->articleCount("jane.doe@example.com", (new Context)->starred(true))); + $this->assertSame(10, Arsse::$db->articleCount("john.doe@example.com", (new Context)->articles(range(1, Database::LIMIT_ARTICLES *3)))); } - public function testCountStarredArticlesWithoutAuthority() { + public function testCountArticlesWithoutAuthority() { Phake::when(Arsse::$user)->authorize->thenReturn(false); $this->assertException("notAuthorized", "User", "ExceptionAuthz"); - Arsse::$db->articleStarredCount($this->user); + 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() { @@ -762,4 +909,44 @@ trait SeriesArticle { $this->assertException("notAuthorized", "User", "ExceptionAuthz"); Arsse::$db->editionLatest($this->user); } + + public function testListTheLabelsOfAnArticle() { + $this->assertEquals([2,1], Arsse::$db->articleLabelsGet("john.doe@example.com", 1)); + $this->assertEquals([2], Arsse::$db->articleLabelsGet("john.doe@example.com", 5)); + $this->assertEquals([], Arsse::$db->articleLabelsGet("john.doe@example.com", 2)); + $this->assertEquals(["Fascinating","Interesting"], Arsse::$db->articleLabelsGet("john.doe@example.com", 1, true)); + $this->assertEquals(["Fascinating"], Arsse::$db->articleLabelsGet("john.doe@example.com", 5, true)); + $this->assertEquals([], Arsse::$db->articleLabelsGet("john.doe@example.com", 2, true)); + } + + public function testListTheLabelsOfAMissingArticle() { + $this->assertException("subjectMissing", "Db", "ExceptionInput"); + Arsse::$db->articleLabelsGet($this->user, 101); + } + + public function testListTheLabelsOfAnArticleWithoutAuthority() { + Phake::when(Arsse::$user)->authorize->thenReturn(false); + $this->assertException("notAuthorized", "User", "ExceptionAuthz"); + Arsse::$db->articleLabelsGet("john.doe@example.com", 1); + } + + public function testListTheCategoriesOfAnArticle() { + $exp = ["Fascinating", "Logical"]; + $this->assertSame($exp, Arsse::$db->articleCategoriesGet($this->user, 19)); + $exp = ["Interesting", "Logical"]; + $this->assertSame($exp, Arsse::$db->articleCategoriesGet($this->user, 20)); + $exp = []; + $this->assertSame($exp, Arsse::$db->articleCategoriesGet($this->user, 4)); + } + + public function testListTheCategoriesOfAMissingArticle() { + $this->assertException("subjectMissing", "Db", "ExceptionInput"); + Arsse::$db->articleCategoriesGet($this->user, 101); + } + + public function testListTheCategoriesOfAnArticleWithoutAuthority() { + Phake::when(Arsse::$user)->authorize->thenReturn(false); + $this->assertException("notAuthorized", "User", "ExceptionAuthz"); + Arsse::$db->articleCategoriesGet($this->user, 19); + } } diff --git a/tests/lib/Database/SeriesCleanup.php b/tests/lib/Database/SeriesCleanup.php index e121d1d6..759b2989 100644 --- a/tests/lib/Database/SeriesCleanup.php +++ b/tests/lib/Database/SeriesCleanup.php @@ -17,6 +17,8 @@ trait SeriesCleanup { $daybefore = gmdate("Y-m-d H:i:s", strtotime("now - 2 days")); $daysago = gmdate("Y-m-d H:i:s", strtotime("now - 7 days")); $weeksago = gmdate("Y-m-d H:i:s", strtotime("now - 21 days")); + $soon = gmdate("Y-m-d H:i:s", strtotime("now + 1 minute")); + $faroff = gmdate("Y-m-d H:i:s", strtotime("now + 1 hour")); $this->data = [ 'arsse_users' => [ 'columns' => [ @@ -29,6 +31,21 @@ trait SeriesCleanup { ["john.doe@example.com", "", "John Doe"], ], ], + 'arsse_sessions' => [ + 'columns' => [ + 'id' => "str", + 'created' => "datetime", + 'expires' => "datetime", + 'user' => "str", + ], + 'rows' => [ + ["a", $nowish, $faroff, "jane.doe@example.com"], // not expired and recently created, thus kept + ["b", $nowish, $soon, "jane.doe@example.com"], // not expired and recently created, thus kept + ["c", $daysago, $soon, "jane.doe@example.com"], // created more than a day ago, thus deleted + ["d", $nowish, $nowish, "jane.doe@example.com"], // recently created but expired, thus deleted + ["e", $daysago, $nowish, "jane.doe@example.com"], // created more than a day ago and expired, thus deleted + ], + ], 'arsse_feeds' => [ 'columns' => [ 'id' => "int", @@ -169,4 +186,15 @@ trait SeriesCleanup { ]); $this->compareExpectations($state); } + + public function testCleanUpExpiredSessions() { + Arsse::$db->sessionCleanup(); + $state = $this->primeExpectations($this->data, [ + 'arsse_sessions' => ["id"] + ]); + foreach ([3,4,5] as $id) { + unset($state['arsse_sessions']['rows'][$id - 1]); + } + $this->compareExpectations($state); + } } diff --git a/tests/lib/Database/SeriesFolder.php b/tests/lib/Database/SeriesFolder.php index b0f2b42a..d2d5b251 100644 --- a/tests/lib/Database/SeriesFolder.php +++ b/tests/lib/Database/SeriesFolder.php @@ -113,16 +113,16 @@ trait SeriesFolder { public function testListRootFolders() { $exp = [ - ['id' => 5, 'name' => "Politics", 'parent' => null], - ['id' => 1, 'name' => "Technology", 'parent' => null], + ['id' => 5, 'name' => "Politics", 'parent' => null, 'children' => 0], + ['id' => 1, 'name' => "Technology", 'parent' => null, 'children' => 2], ]; - $this->assertSame($exp, Arsse::$db->folderList("john.doe@example.com", null, false)->getAll()); + $this->assertResult($exp, Arsse::$db->folderList("john.doe@example.com", null, false)); $exp = [ - ['id' => 4, 'name' => "Politics", 'parent' => null], + ['id' => 4, 'name' => "Politics", 'parent' => null, 'children' => 0], ]; - $this->assertSame($exp, Arsse::$db->folderList("jane.doe@example.com", null, false)->getAll()); + $this->assertResult($exp, Arsse::$db->folderList("jane.doe@example.com", null, false)); $exp = []; - $this->assertSame($exp, Arsse::$db->folderList("admin@example.net", null, false)->getAll()); + $this->assertResult($exp, Arsse::$db->folderList("admin@example.net", null, false)); Phake::verify(Arsse::$user)->authorize("john.doe@example.com", "folderList"); Phake::verify(Arsse::$user)->authorize("jane.doe@example.com", "folderList"); Phake::verify(Arsse::$user)->authorize("admin@example.net", "folderList"); @@ -130,21 +130,21 @@ trait SeriesFolder { public function testListFoldersRecursively() { $exp = [ - ['id' => 5, 'name' => "Politics", 'parent' => null], - ['id' => 6, 'name' => "Politics", 'parent' => 2], - ['id' => 3, 'name' => "Rocketry", 'parent' => 1], - ['id' => 2, 'name' => "Software", 'parent' => 1], - ['id' => 1, 'name' => "Technology", 'parent' => null], + ['id' => 5, 'name' => "Politics", 'parent' => null, 'children' => 0], + ['id' => 6, 'name' => "Politics", 'parent' => 2, 'children' => 0], + ['id' => 3, 'name' => "Rocketry", 'parent' => 1, 'children' => 0], + ['id' => 2, 'name' => "Software", 'parent' => 1, 'children' => 1], + ['id' => 1, 'name' => "Technology", 'parent' => null, 'children' => 2], ]; - $this->assertSame($exp, Arsse::$db->folderList("john.doe@example.com", null, true)->getAll()); + $this->assertResult($exp, Arsse::$db->folderList("john.doe@example.com", null, true)); $exp = [ - ['id' => 6, 'name' => "Politics", 'parent' => 2], - ['id' => 3, 'name' => "Rocketry", 'parent' => 1], - ['id' => 2, 'name' => "Software", 'parent' => 1], + ['id' => 6, 'name' => "Politics", 'parent' => 2, 'children' => 0], + ['id' => 3, 'name' => "Rocketry", 'parent' => 1, 'children' => 0], + ['id' => 2, 'name' => "Software", 'parent' => 1, 'children' => 1], ]; - $this->assertSame($exp, Arsse::$db->folderList("john.doe@example.com", 1, true)->getAll()); + $this->assertResult($exp, Arsse::$db->folderList("john.doe@example.com", 1, true)); $exp = []; - $this->assertSame($exp, Arsse::$db->folderList("jane.doe@example.com", 4, true)->getAll()); + $this->assertResult($exp, Arsse::$db->folderList("jane.doe@example.com", 4, true)); Phake::verify(Arsse::$user, Phake::times(2))->authorize("john.doe@example.com", "folderList"); Phake::verify(Arsse::$user)->authorize("jane.doe@example.com", "folderList"); } diff --git a/tests/lib/Database/SeriesLabel.php b/tests/lib/Database/SeriesLabel.php new file mode 100644 index 00000000..c764b046 --- /dev/null +++ b/tests/lib/Database/SeriesLabel.php @@ -0,0 +1,517 @@ + [ + 'columns' => [ + 'id' => 'str', + 'password' => 'str', + 'name' => 'str', + ], + 'rows' => [ + ["jane.doe@example.com", "", "Jane Doe"], + ["john.doe@example.com", "", "John Doe"], + ["john.doe@example.org", "", "John Doe"], + ["john.doe@example.net", "", "John Doe"], + ], + ], + 'arsse_folders' => [ + 'columns' => [ + 'id' => "int", + 'owner' => "str", + 'parent' => "int", + 'name' => "str", + ], + 'rows' => [ + [1, "john.doe@example.com", null, "Technology"], + [2, "john.doe@example.com", 1, "Software"], + [3, "john.doe@example.com", 1, "Rocketry"], + [4, "jane.doe@example.com", null, "Politics"], + [5, "john.doe@example.com", null, "Politics"], + [6, "john.doe@example.com", 2, "Politics"], + [7, "john.doe@example.net", null, "Technology"], + [8, "john.doe@example.net", 7, "Software"], + [9, "john.doe@example.net", null, "Politics"], + ] + ], + 'arsse_feeds' => [ + 'columns' => [ + 'id' => "int", + 'url' => "str", + ], + 'rows' => [ + [1,"http://example.com/1"], + [2,"http://example.com/2"], + [3,"http://example.com/3"], + [4,"http://example.com/4"], + [5,"http://example.com/5"], + [6,"http://example.com/6"], + [7,"http://example.com/7"], + [8,"http://example.com/8"], + [9,"http://example.com/9"], + [10,"http://example.com/10"], + [11,"http://example.com/11"], + [12,"http://example.com/12"], + [13,"http://example.com/13"], + ] + ], + 'arsse_subscriptions' => [ + 'columns' => [ + 'id' => "int", + 'owner' => "str", + 'feed' => "int", + 'folder' => "int", + ], + 'rows' => [ + [1,"john.doe@example.com",1,null], + [2,"john.doe@example.com",2,null], + [3,"john.doe@example.com",3,1], + [4,"john.doe@example.com",4,6], + [5,"john.doe@example.com",10,5], + [6,"jane.doe@example.com",1,null], + [7,"jane.doe@example.com",10,null], + [8,"john.doe@example.org",11,null], + [9,"john.doe@example.org",12,null], + [10,"john.doe@example.org",13,null], + [11,"john.doe@example.net",10,null], + [12,"john.doe@example.net",2,9], + [13,"john.doe@example.net",3,8], + [14,"john.doe@example.net",4,7], + ] + ], + 'arsse_articles' => [ + 'columns' => [ + 'id' => "int", + 'feed' => "int", + 'url' => "str", + 'title' => "str", + 'author' => "str", + 'published' => "datetime", + 'edited' => "datetime", + 'content' => "str", + 'guid' => "str", + 'url_title_hash' => "str", + 'url_content_hash' => "str", + 'title_content_hash' => "str", + 'modified' => "datetime", + ], + 'rows' => [ + [1,1,null,null,null,null,null,null,null,"","","","2000-01-01T00:00:00Z"], + [2,1,null,null,null,null,null,null,null,"","","","2010-01-01T00:00:00Z"], + [3,2,null,null,null,null,null,null,null,"","","","2000-01-01T00:00:00Z"], + [4,2,null,null,null,null,null,null,null,"","","","2010-01-01T00:00:00Z"], + [5,3,null,null,null,null,null,null,null,"","","","2000-01-01T00:00:00Z"], + [6,3,null,null,null,null,null,null,null,"","","","2010-01-01T00:00:00Z"], + [7,4,null,null,null,null,null,null,null,"","","","2000-01-01T00:00:00Z"], + [8,4,null,null,null,null,null,null,null,"","","","2010-01-01T00:00:00Z"], + [9,5,null,null,null,null,null,null,null,"","","","2000-01-01T00:00:00Z"], + [10,5,null,null,null,null,null,null,null,"","","","2010-01-01T00:00:00Z"], + [11,6,null,null,null,null,null,null,null,"","","","2000-01-01T00:00:00Z"], + [12,6,null,null,null,null,null,null,null,"","","","2010-01-01T00:00:00Z"], + [13,7,null,null,null,null,null,null,null,"","","","2000-01-01T00:00:00Z"], + [14,7,null,null,null,null,null,null,null,"","","","2010-01-01T00:00:00Z"], + [15,8,null,null,null,null,null,null,null,"","","","2000-01-01T00:00:00Z"], + [16,8,null,null,null,null,null,null,null,"","","","2010-01-01T00:00:00Z"], + [17,9,null,null,null,null,null,null,null,"","","","2000-01-01T00:00:00Z"], + [18,9,null,null,null,null,null,null,null,"","","","2010-01-01T00:00:00Z"], + [19,10,null,null,null,null,null,null,null,"","","","2000-01-01T00:00:00Z"], + [20,10,null,null,null,null,null,null,null,"","","","2010-01-01T00:00:00Z"], + [101,11,'http://example.com/1','Article title 1','','2000-01-01 00:00:00','2000-01-01 00:00:01','Article content 1
','e433653cef2e572eee4215fa299a4a5af9137b2cefd6283c85bd69a32915beda','f5cb8bfc1c7396dc9816af212a3e2ac5221585c2a00bf7ccb6aabd95dcfcd6a6','fb0bc8f8cb08913dc5a497db700e327f1d34e4987402687d494a5891f24714d4','18fdd4fa93d693128c43b004399e5c9cea6c261ddfa002518d3669f55d8c2207','2000-01-01 01:00:00'], + [102,11,'http://example.com/2','Article title 2','','2000-01-02 00:00:00','2000-01-02 00:00:02','Article content 2
','5be8a5a46ecd52ed132191c8d27fb1af6b3d4edc00234c5d9f8f0e10562ed3b7','0e86d2de822a174fe3c44a466953e63ca1f1a58a19cbf475fce0855d4e3d5153','13075894189c47ffcfafd1dfe7fbb539f7c74a69d35a399b3abf8518952714f9','2abd0a8cba83b8214a66c8f0293ba63e467d720540e29ff8ddcdab069d4f1c9e','2000-01-02 02:00:00'], + [103,12,'http://example.com/3','Article title 3','','2000-01-03 00:00:00','2000-01-03 00:00:03','Article content 3
','31a6594500a48b59fcc8a075ce82b946c9c3c782460d088bd7b8ef3ede97ad92','f74b06b240bd08abf4d3fdfc20dba6a6f6eb8b4f1a00e9a617efd63a87180a4b','b278380e984cefe63f0e412b88ffc9cb0befdfa06fdc00bace1da99a8daff406','ad622b31e739cd3a3f3c788991082cf4d2f7a8773773008e75f0572e58cd373b','2000-01-03 03:00:00'], + [104,12,'http://example.com/4','Article title 4','','2000-01-04 00:00:00','2000-01-04 00:00:04','Article content 4
','804e517d623390e71497982c77cf6823180342ebcd2e7d5e32da1e55b09dd180','f3615c7f16336d3ea242d35cf3fc17dbc4ee3afb78376bf49da2dd7a5a25dec8','f11c2b4046f207579aeb9c69a8c20ca5461cef49756ccfa5ba5e2344266da3b3','ab2da63276acce431250b18d3d49b988b226a99c7faadf275c90b751aee05be9','2000-01-04 04:00:00'], + [105,13,'http://example.com/5','Article title 5','','2000-01-05 00:00:00','2000-01-05 00:00:05','Article content 5
','db3e736c2c492f5def5c5da33ddcbea1824040e9ced2142069276b0a6e291a41','d40da96e39eea6c55948ccbe9b3d275b5f931298288dbe953990c5f496097022','834240f84501b5341d375414718204ec421561f3825d34c22bf9182203e42900','43b970ac6ec5f8a9647b2c7e4eed8b1d7f62e154a95eed748b0294c1256764ba','2000-01-05 05:00:00'], + ] + ], + 'arsse_enclosures' => [ + 'columns' => [ + 'article' => "int", + 'url' => "str", + 'type' => "str", + ], + 'rows' => [ + [102,"http://example.com/text","text/plain"], + [103,"http://example.com/video","video/webm"], + [104,"http://example.com/image","image/svg+xml"], + [105,"http://example.com/audio","audio/ogg"], + + ] + ], + 'arsse_editions' => [ + 'columns' => [ + 'id' => "int", + 'article' => "int", + ], + 'rows' => [ + [1,1], + [2,2], + [3,3], + [4,4], + [5,5], + [6,6], + [7,7], + [8,8], + [9,9], + [10,10], + [11,11], + [12,12], + [13,13], + [14,14], + [15,15], + [16,16], + [17,17], + [18,18], + [19,19], + [20,20], + [101,101], + [102,102], + [103,103], + [104,104], + [105,105], + [202,102], + [203,103], + [204,104], + [205,105], + [305,105], + [1001,20], + ] + ], + 'arsse_marks' => [ + 'columns' => [ + 'subscription' => "int", + 'article' => "int", + 'read' => "bool", + 'starred' => "bool", + 'modified' => "datetime" + ], + 'rows' => [ + [1, 1,1,1,'2000-01-01 00:00:00'], + [5, 19,1,0,'2000-01-01 00:00:00'], + [5, 20,0,1,'2010-01-01 00:00:00'], + [7, 20,1,0,'2010-01-01 00:00:00'], + [8, 102,1,0,'2000-01-02 02:00:00'], + [9, 103,0,1,'2000-01-03 03:00:00'], + [9, 104,1,1,'2000-01-04 04:00:00'], + [10,105,0,0,'2000-01-05 05:00:00'], + [11, 19,0,0,'2017-01-01 00:00:00'], + [11, 20,1,0,'2017-01-01 00:00:00'], + [12, 3,0,1,'2017-01-01 00:00:00'], + [12, 4,1,1,'2017-01-01 00:00:00'], + ] + ], + 'arsse_labels' => [ + 'columns' => [ + 'id' => "int", + 'owner' => "str", + 'name' => "str", + ], + 'rows' => [ + [1,"john.doe@example.com","Interesting"], + [2,"john.doe@example.com","Fascinating"], + [3,"jane.doe@example.com","Boring"], + [4,"john.doe@example.com","Lonely"], + ], + ], + 'arsse_label_members' => [ + 'columns' => [ + 'label' => "int", + 'article' => "int", + 'subscription' => "int", + 'assigned' => "bool", + ], + 'rows' => [ + [1, 1,1,1], + [2, 1,1,1], + [1,19,5,1], + [2,20,5,1], + [1, 5,3,0], + [2, 5,3,1], + ], + ], + ]; + + public function setUpSeries() { + $this->checkLabels = ['arsse_labels' => ["id","owner","name"]]; + $this->checkMembers = ['arsse_label_members' => ["label","article","subscription","assigned"]]; + $this->user = "john.doe@example.com"; + } + + public function testAddALabel() { + $user = "john.doe@example.com"; + $labelID = $this->nextID("arsse_labels"); + $this->assertSame($labelID, Arsse::$db->labelAdd($user, ['name' => "Entertaining"])); + Phake::verify(Arsse::$user)->authorize($user, "labelAdd"); + $state = $this->primeExpectations($this->data, $this->checkLabels); + $state['arsse_labels']['rows'][] = [$labelID, $user, "Entertaining"]; + $this->compareExpectations($state); + } + + public function testAddADuplicateLabel() { + $this->assertException("constraintViolation", "Db", "ExceptionInput"); + Arsse::$db->labelAdd("john.doe@example.com", ['name' => "Interesting"]); + } + + public function testAddALabelWithAMissingName() { + $this->assertException("missing", "Db", "ExceptionInput"); + Arsse::$db->labelAdd("john.doe@example.com", []); + } + + public function testAddALabelWithABlankName() { + $this->assertException("missing", "Db", "ExceptionInput"); + Arsse::$db->labelAdd("john.doe@example.com", ['name' => ""]); + } + + public function testAddALabelWithAWhitespaceName() { + $this->assertException("whitespace", "Db", "ExceptionInput"); + Arsse::$db->labelAdd("john.doe@example.com", ['name' => " "]); + } + + public function testAddALabelWithoutAuthority() { + Phake::when(Arsse::$user)->authorize->thenReturn(false); + $this->assertException("notAuthorized", "User", "ExceptionAuthz"); + Arsse::$db->labelAdd("john.doe@example.com", ['name' => "Boring"]); + } + + public function testListLabels() { + $exp = [ + ['id' => 2, 'name' => "Fascinating", 'articles' => 3, 'read' => 1], + ['id' => 1, 'name' => "Interesting", 'articles' => 2, 'read' => 2], + ['id' => 4, 'name' => "Lonely", 'articles' => 0, 'read' => 0], + ]; + $this->assertResult($exp, Arsse::$db->labelList("john.doe@example.com")); + $exp = [ + ['id' => 3, 'name' => "Boring", 'articles' => 0], + ]; + $this->assertResult($exp, Arsse::$db->labelList("jane.doe@example.com")); + $exp = []; + $this->assertResult($exp, Arsse::$db->labelList("jane.doe@example.com", false)); + Phake::verify(Arsse::$user)->authorize("john.doe@example.com", "labelList"); + } + + public function testListLabelsWithoutAuthority() { + Phake::when(Arsse::$user)->authorize->thenReturn(false); + $this->assertException("notAuthorized", "User", "ExceptionAuthz"); + Arsse::$db->labelList("john.doe@example.com"); + } + + public function testRemoveALabel() { + $this->assertTrue(Arsse::$db->labelRemove("john.doe@example.com", 1)); + Phake::verify(Arsse::$user)->authorize("john.doe@example.com", "labelRemove"); + $state = $this->primeExpectations($this->data, $this->checkLabels); + array_shift($state['arsse_labels']['rows']); + $this->compareExpectations($state); + } + + public function testRemoveALabelByName() { + $this->assertTrue(Arsse::$db->labelRemove("john.doe@example.com", "Interesting", true)); + Phake::verify(Arsse::$user)->authorize("john.doe@example.com", "labelRemove"); + $state = $this->primeExpectations($this->data, $this->checkLabels); + array_shift($state['arsse_labels']['rows']); + $this->compareExpectations($state); + } + + public function testRemoveAMissingLabel() { + $this->assertException("subjectMissing", "Db", "ExceptionInput"); + Arsse::$db->labelRemove("john.doe@example.com", 2112); + } + + public function testRemoveAnInvalidLabel() { + $this->assertException("typeViolation", "Db", "ExceptionInput"); + Arsse::$db->labelRemove("john.doe@example.com", -1); + } + + public function testRemoveAnInvalidLabelByName() { + $this->assertException("typeViolation", "Db", "ExceptionInput"); + Arsse::$db->labelRemove("john.doe@example.com", [], true); + } + + public function testRemoveALabelOfTheWrongOwner() { + $this->assertException("subjectMissing", "Db", "ExceptionInput"); + Arsse::$db->labelRemove("john.doe@example.com", 3); // label ID 3 belongs to Jane + } + + public function testRemoveALabelWithoutAuthority() { + Phake::when(Arsse::$user)->authorize->thenReturn(false); + $this->assertException("notAuthorized", "User", "ExceptionAuthz"); + Arsse::$db->labelRemove("john.doe@example.com", 1); + } + + public function testGetThePropertiesOfALabel() { + $exp = [ + 'id' => 2, + 'name' => "Fascinating", + 'articles' => 3, + 'read' => 1, + ]; + $this->assertArraySubset($exp, Arsse::$db->labelPropertiesGet("john.doe@example.com", 2)); + $this->assertArraySubset($exp, Arsse::$db->labelPropertiesGet("john.doe@example.com", "Fascinating", true)); + Phake::verify(Arsse::$user, Phake::times(2))->authorize("john.doe@example.com", "labelPropertiesGet"); + } + + public function testGetThePropertiesOfAMissingLabel() { + $this->assertException("subjectMissing", "Db", "ExceptionInput"); + Arsse::$db->labelPropertiesGet("john.doe@example.com", 2112); + } + + public function testGetThePropertiesOfAnInvalidLabel() { + $this->assertException("typeViolation", "Db", "ExceptionInput"); + Arsse::$db->labelPropertiesGet("john.doe@example.com", -1); + } + + public function testGetThePropertiesOfAnInvalidLabelByName() { + $this->assertException("typeViolation", "Db", "ExceptionInput"); + Arsse::$db->labelPropertiesGet("john.doe@example.com", [], true); + } + + public function testGetThePropertiesOfALabelOfTheWrongOwner() { + $this->assertException("subjectMissing", "Db", "ExceptionInput"); + Arsse::$db->labelPropertiesGet("john.doe@example.com", 3); // label ID 3 belongs to Jane + } + + public function testGetThePropertiesOfALabelWithoutAuthority() { + Phake::when(Arsse::$user)->authorize->thenReturn(false); + $this->assertException("notAuthorized", "User", "ExceptionAuthz"); + Arsse::$db->labelPropertiesGet("john.doe@example.com", 1); + } + + public function testMakeNoChangesToALabel() { + $this->assertFalse(Arsse::$db->labelPropertiesSet("john.doe@example.com", 1, [])); + } + + public function testRenameALabel() { + $this->assertTrue(Arsse::$db->labelPropertiesSet("john.doe@example.com", 1, ['name' => "Curious"])); + Phake::verify(Arsse::$user)->authorize("john.doe@example.com", "labelPropertiesSet"); + $state = $this->primeExpectations($this->data, $this->checkLabels); + $state['arsse_labels']['rows'][0][2] = "Curious"; + $this->compareExpectations($state); + } + + public function testRenameALabelByName() { + $this->assertTrue(Arsse::$db->labelPropertiesSet("john.doe@example.com", "Interesting", ['name' => "Curious"], true)); + Phake::verify(Arsse::$user)->authorize("john.doe@example.com", "labelPropertiesSet"); + $state = $this->primeExpectations($this->data, $this->checkLabels); + $state['arsse_labels']['rows'][0][2] = "Curious"; + $this->compareExpectations($state); + } + + public function testRenameALabelToTheEmptyString() { + $this->assertException("missing", "Db", "ExceptionInput"); + $this->assertTrue(Arsse::$db->labelPropertiesSet("john.doe@example.com", 1, ['name' => ""])); + } + + public function testRenameALabelToWhitespaceOnly() { + $this->assertException("whitespace", "Db", "ExceptionInput"); + $this->assertTrue(Arsse::$db->labelPropertiesSet("john.doe@example.com", 1, ['name' => " "])); + } + + public function testRenameALabelToAnInvalidValue() { + $this->assertException("typeViolation", "Db", "ExceptionInput"); + $this->assertTrue(Arsse::$db->labelPropertiesSet("john.doe@example.com", 1, ['name' => []])); + } + + public function testCauseALabelCollision() { + $this->assertException("constraintViolation", "Db", "ExceptionInput"); + Arsse::$db->labelPropertiesSet("john.doe@example.com", 1, ['name' => "Fascinating"]); + } + + public function testSetThePropertiesOfAMissingLabel() { + $this->assertException("subjectMissing", "Db", "ExceptionInput"); + Arsse::$db->labelPropertiesSet("john.doe@example.com", 2112, ['name' => "Exciting"]); + } + + public function testSetThePropertiesOfAnInvalidLabel() { + $this->assertException("typeViolation", "Db", "ExceptionInput"); + Arsse::$db->labelPropertiesSet("john.doe@example.com", -1, ['name' => "Exciting"]); + } + + public function testSetThePropertiesOfAnInvalidLabelByName() { + $this->assertException("typeViolation", "Db", "ExceptionInput"); + Arsse::$db->labelPropertiesSet("john.doe@example.com", [], ['name' => "Exciting"], true); + } + + public function testSetThePropertiesOfALabelForTheWrongOwner() { + $this->assertException("subjectMissing", "Db", "ExceptionInput"); + Arsse::$db->labelPropertiesSet("john.doe@example.com", 3, ['name' => "Exciting"]); // label ID 3 belongs to Jane + } + + public function testSetThePropertiesOfALabelWithoutAuthority() { + Phake::when(Arsse::$user)->authorize->thenReturn(false); + $this->assertException("notAuthorized", "User", "ExceptionAuthz"); + Arsse::$db->labelPropertiesSet("john.doe@example.com", 1, ['name' => "Exciting"]); + } + + public function testListLabelledArticles() { + $exp = [1,19]; + $this->assertEquals($exp, Arsse::$db->labelArticlesGet("john.doe@example.com", 1)); + $this->assertEquals($exp, Arsse::$db->labelArticlesGet("john.doe@example.com", "Interesting", true)); + $exp = [1,5,20]; + $this->assertEquals($exp, Arsse::$db->labelArticlesGet("john.doe@example.com", 2)); + $this->assertEquals($exp, Arsse::$db->labelArticlesGet("john.doe@example.com", "Fascinating", true)); + $exp = []; + $this->assertEquals($exp, Arsse::$db->labelArticlesGet("john.doe@example.com", 4)); + $this->assertEquals($exp, Arsse::$db->labelArticlesGet("john.doe@example.com", "Lonely", true)); + } + + public function testListLabelledArticlesForAMissingLabel() { + $this->assertException("subjectMissing", "Db", "ExceptionInput"); + Arsse::$db->labelArticlesGet("john.doe@example.com", 3); + } + + public function testListLabelledArticlesForAnInvalidLabel() { + $this->assertException("typeViolation", "Db", "ExceptionInput"); + Arsse::$db->labelArticlesGet("john.doe@example.com", -1); + } + + public function testListLabelledArticlesWithoutAuthority() { + Phake::when(Arsse::$user)->authorize->thenReturn(false); + $this->assertException("notAuthorized", "User", "ExceptionAuthz"); + Arsse::$db->labelArticlesGet("john.doe@example.com", 1); + } + + public function testApplyALabelToArticles() { + Arsse::$db->labelArticlesSet("john.doe@example.com", 1, (new Context)->articles([2,5])); + $state = $this->primeExpectations($this->data, $this->checkMembers); + $state['arsse_label_members']['rows'][4][3] = 1; + $state['arsse_label_members']['rows'][] = [1,2,1,1]; + $this->compareExpectations($state); + } + + public function testClearALabelFromArticles() { + Arsse::$db->labelArticlesSet("john.doe@example.com", 1, (new Context)->articles([1,5]), true); + $state = $this->primeExpectations($this->data, $this->checkMembers); + $state['arsse_label_members']['rows'][0][3] = 0; + $this->compareExpectations($state); + } + + public function testApplyALabelToArticlesByName() { + Arsse::$db->labelArticlesSet("john.doe@example.com", "Interesting", (new Context)->articles([2,5]), false, true); + $state = $this->primeExpectations($this->data, $this->checkMembers); + $state['arsse_label_members']['rows'][4][3] = 1; + $state['arsse_label_members']['rows'][] = [1,2,1,1]; + $this->compareExpectations($state); + } + + public function testClearALabelFromArticlesByName() { + Arsse::$db->labelArticlesSet("john.doe@example.com", "Interesting", (new Context)->articles([1,5]), true, true); + $state = $this->primeExpectations($this->data, $this->checkMembers); + $state['arsse_label_members']['rows'][0][3] = 0; + $this->compareExpectations($state); + } + + public function testApplyALabelToArticlesWithoutAuthority() { + Phake::when(Arsse::$user)->authorize->thenReturn(false); + $this->assertException("notAuthorized", "User", "ExceptionAuthz"); + Arsse::$db->labelArticlesSet("john.doe@example.com", 1, (new Context)->articles([2,5])); + } +} diff --git a/tests/lib/Database/SeriesSession.php b/tests/lib/Database/SeriesSession.php new file mode 100644 index 00000000..2f9c93cd --- /dev/null +++ b/tests/lib/Database/SeriesSession.php @@ -0,0 +1,122 @@ +data = [ + 'arsse_users' => [ + 'columns' => [ + 'id' => 'str', + 'password' => 'str', + 'name' => 'str', + ], + 'rows' => [ + ["jane.doe@example.com", "", "Jane Doe"], + ["john.doe@example.com", "", "John Doe"], + ], + ], + 'arsse_sessions' => [ + 'columns' => [ + 'id' => "str", + 'user' => "str", + 'created' => "datetime", + 'expires' => "datetime", + ], + 'rows' => [ + ["80fa94c1a11f11e78667001e673b2560", "jane.doe@example.com", $past, $faroff], + ["27c6de8da13311e78667001e673b2560", "jane.doe@example.com", $past, $past], // expired + ["ab3b3eb8a13311e78667001e673b2560", "jane.doe@example.com", $old, $future], // too old + ["da772f8fa13c11e78667001e673b2560", "john.doe@example.com", $past, $future], + ], + ], + ]; + } + + public function testResumeAValidSession() { + $exp1 = [ + 'id' => "80fa94c1a11f11e78667001e673b2560", + 'user' => "jane.doe@example.com" + ]; + $exp2 = [ + 'id' => "da772f8fa13c11e78667001e673b2560", + 'user' => "john.doe@example.com" + ]; + $this->assertArraySubset($exp1, Arsse::$db->sessionResume("80fa94c1a11f11e78667001e673b2560")); + $this->assertArraySubset($exp2, Arsse::$db->sessionResume("da772f8fa13c11e78667001e673b2560")); + $now = time(); + // sessions near timeout should be refreshed automatically + $state = $this->primeExpectations($this->data, ['arsse_sessions' => ["id", "created", "expires", "user"]]); + $state['arsse_sessions']['rows'][3][2] = Date::transform(Date::add(Arsse::$conf->userSessionTimeout, $now), "sql"); + $this->compareExpectations($state); + // session resumption should not check authorization + Phake::when(Arsse::$user)->authorize->thenReturn(false); + $this->assertArraySubset($exp1, Arsse::$db->sessionResume("80fa94c1a11f11e78667001e673b2560")); + } + + public function testResumeAMissingSession() { + $this->assertException("invalid", "User", "ExceptionSession"); + Arsse::$db->sessionResume("thisSessionDoesNotExist"); + } + + public function testResumeAnExpiredSession() { + $this->assertException("invalid", "User", "ExceptionSession"); + Arsse::$db->sessionResume("27c6de8da13311e78667001e673b2560"); + } + + public function testResumeAStaleSession() { + $this->assertException("invalid", "User", "ExceptionSession"); + Arsse::$db->sessionResume("ab3b3eb8a13311e78667001e673b2560"); + } + + public function testCreateASession() { + $user = "jane.doe@example.com"; + $id = Arsse::$db->sessionCreate($user); + $now = time(); + $state = $this->primeExpectations($this->data, ['arsse_sessions' => ["id", "created", "expires", "user"]]); + $state['arsse_sessions']['rows'][] = [$id, Date::transform($now, "sql"), Date::transform(Date::add(Arsse::$conf->userSessionTimeout, $now), "sql"), $user]; + $this->compareExpectations($state); + } + + public function testCreateASessionWithoutAuthority() { + Phake::when(Arsse::$user)->authorize->thenReturn(false); + $this->assertException("notAuthorized", "User", "ExceptionAuthz"); + Arsse::$db->sessionCreate("jane.doe@example.com"); + } + + public function testDestroyASession() { + $user = "jane.doe@example.com"; + $id = "80fa94c1a11f11e78667001e673b2560"; + $this->assertTrue(Arsse::$db->sessionDestroy($user, $id)); + $state = $this->primeExpectations($this->data, ['arsse_sessions' => ["id", "created", "expires", "user"]]); + unset($state['arsse_sessions']['rows'][0]); + $this->compareExpectations($state); + // destroying a session which does not exist is not an error + $this->assertFalse(Arsse::$db->sessionDestroy($user, $id)); + } + + public function testDestroyASessionForTheWrongUser() { + $user = "john.doe@example.com"; + $id = "80fa94c1a11f11e78667001e673b2560"; + $this->assertFalse(Arsse::$db->sessionDestroy($user, $id)); + } + + public function testDestroyASessionWithoutAuthority() { + Phake::when(Arsse::$user)->authorize->thenReturn(false); + $this->assertException("notAuthorized", "User", "ExceptionAuthz"); + Arsse::$db->sessionDestroy("jane.doe@example.com", "80fa94c1a11f11e78667001e673b2560"); + } +} diff --git a/tests/lib/Database/SeriesSubscription.php b/tests/lib/Database/SeriesSubscription.php index 13cbc82a..78f240e3 100644 --- a/tests/lib/Database/SeriesSubscription.php +++ b/tests/lib/Database/SeriesSubscription.php @@ -48,6 +48,7 @@ trait SeriesSubscription { 'username' => "str", 'password' => "str", 'next_fetch' => "datetime", + 'favicon' => "str", ], 'rows' => [] // filled in the series setup ], @@ -108,9 +109,9 @@ trait SeriesSubscription { public function setUpSeries() { $this->data['arsse_feeds']['rows'] = [ - [1,"http://example.com/feed1", "Ook", "", "",strtotime("now")], - [2,"http://example.com/feed2", "Eek", "", "",strtotime("now - 1 hour")], - [3,"http://example.com/feed3", "Ack", "", "",strtotime("now + 1 hour")], + [1,"http://example.com/feed1", "Ook", "", "",strtotime("now"),''], + [2,"http://example.com/feed2", "Eek", "", "",strtotime("now - 1 hour"),'http://example.com/favicon.ico'], + [3,"http://example.com/feed3", "Ack", "", "",strtotime("now + 1 hour"),''], ]; // initialize a partial mock of the Database object to later manipulate the feedUpdate method Arsse::$db = Phake::partialMock(Database::class, $this->drv); @@ -261,6 +262,21 @@ trait SeriesSubscription { } public function testListSubscriptionsInAFolder() { + $exp = [ + [ + 'url' => "http://example.com/feed2", + 'title' => "Eek", + 'folder' => null, + 'top_folder' => null, + 'unread' => 4, + 'pinned' => 1, + 'order_type' => 2, + ], + ]; + $this->assertResult($exp, Arsse::$db->subscriptionList($this->user, null, false)); + } + + public function testListSubscriptionsWithoutRecursion() { $exp = [ [ 'url' => "http://example.com/feed3", @@ -273,6 +289,7 @@ trait SeriesSubscription { ], ]; $this->assertResult($exp, Arsse::$db->subscriptionList($this->user, 2)); + } public function testListSubscriptionsInAMissingFolder() { @@ -286,6 +303,22 @@ trait SeriesSubscription { Arsse::$db->subscriptionList($this->user); } + public function testCountSubscriptions() { + $this->assertSame(2, Arsse::$db->subscriptionCount($this->user)); + $this->assertSame(1, Arsse::$db->subscriptionCount($this->user, 2)); + } + + public function testCountSubscriptionsInAMissingFolder() { + $this->assertException("idMissing", "Db", "ExceptionInput"); + Arsse::$db->subscriptionCount($this->user, 4); + } + + public function testCountSubscriptionsWithoutAuthority() { + Phake::when(Arsse::$user)->authorize->thenReturn(false); + $this->assertException("notAuthorized", "User", "ExceptionAuthz"); + Arsse::$db->subscriptionCount($this->user); + } + public function testGetThePropertiesOfAMissingSubscription() { $this->assertException("subjectMissing", "Db", "ExceptionInput"); Arsse::$db->subscriptionPropertiesGet($this->user, 2112); @@ -321,6 +354,9 @@ trait SeriesSubscription { ]); $state['arsse_subscriptions']['rows'][0] = [1,"john.doe@example.com",2,null,3,0,0]; $this->compareExpectations($state); + // making no changes is a valid result + Arsse::$db->subscriptionPropertiesSet($this->user, 1, ['unhinged' => true]); + $this->compareExpectations($state); } public function testMoveASubscriptionToAMissingFolder() { @@ -371,4 +407,20 @@ trait SeriesSubscription { $this->assertException("notAuthorized", "User", "ExceptionAuthz"); Arsse::$db->subscriptionPropertiesSet($this->user, 1, ['folder' => null]); } + + public function testRetrieveTheFaviconOfASubscription() { + $exp = "http://example.com/favicon.ico"; + $this->assertSame($exp, Arsse::$db->subscriptionFavicon(1)); + $this->assertSame($exp, Arsse::$db->subscriptionFavicon(2)); + $this->assertSame('', Arsse::$db->subscriptionFavicon(3)); + $this->assertSame('', Arsse::$db->subscriptionFavicon(4)); + // authorization shouldn't have any bearing on this function + Phake::when(Arsse::$user)->authorize->thenReturn(false); + $this->assertSame($exp, Arsse::$db->subscriptionFavicon(1)); + $this->assertSame($exp, Arsse::$db->subscriptionFavicon(2)); + $this->assertSame('', Arsse::$db->subscriptionFavicon(3)); + $this->assertSame('', Arsse::$db->subscriptionFavicon(4)); + // invalid IDs should simply return an empty string + $this->assertSame('', Arsse::$db->subscriptionFavicon(-2112)); + } } diff --git a/tests/lib/Database/SeriesUser.php b/tests/lib/Database/SeriesUser.php index e59a6482..64780175 100644 --- a/tests/lib/Database/SeriesUser.php +++ b/tests/lib/Database/SeriesUser.php @@ -213,6 +213,9 @@ trait SeriesUser { $state = $this->primeExpectations($this->data, ['arsse_users' => ['id','password','name','rights']]); $state['arsse_users']['rows'][0][2] = "James Kirk"; $this->compareExpectations($state); + // making now changes should make no changes :) + Arsse::$db->userPropertiesSet("admin@example.net", ['lifeform' => "tribble"]); + $this->compareExpectations($state); } public function testSetThePropertiesOfAMissingUser() { diff --git a/tests/lib/Result.php b/tests/lib/Result.php index eeeb6cb3..7381a344 100644 --- a/tests/lib/Result.php +++ b/tests/lib/Result.php @@ -17,21 +17,24 @@ class Result implements \JKingWeb\Arsse\Db\Result { // actual public methods public function getValue() { - $arr = $this->next(); if ($this->valid()) { - $keys = array_keys($arr); - return $arr[array_shift($keys)]; + $keys = array_keys($this->current()); + $out = $this->current()[array_shift($keys)]; + $this->next(); + return $out; } + $this->next(); return null; } public function getRow() { - $arr = $this->next(); - return ($this->valid() ? $arr : null); + $out = ($this->valid() ? $this->current() : null); + $this->next(); + return $out; } public function getAll(): array { - return $this->set; + return iterator_to_array($this, false); } public function changes() { @@ -56,22 +59,22 @@ class Result implements \JKingWeb\Arsse\Db\Result { // PHP iterator methods public function valid() { - return !is_null(key($this->set)); + return $this->pos < sizeof($this->set); } public function next() { - return next($this->set); + $this->pos++; } public function current() { - return current($this->set); + return $this->set[$this->key()]; } public function key() { - return key($this->set); + return array_keys($this->set)[$this->pos]; } public function rewind() { - reset($this->set); + $this->pos = 0; } } diff --git a/tests/phpunit.xml b/tests/phpunit.xml index 44ec358e..0e06627e 100644 --- a/tests/phpunit.xml +++ b/tests/phpunit.xml @@ -18,6 +18,14 @@