cli = $this->partialMock(CLI::class); $this->cli->logError->returns(null); $this->cli->loadConf->returns(true); $this->dbMock = $this->mock(Database::class); } public function assertConsole(string $command, int $exitStatus, string $output = "", bool $pattern = false): void { Arsse::$obj = $this->objMock->get(); Arsse::$db = $this->dbMock->get(); $argv = \Clue\Arguments\split($command); $output = strlen($output) ? $output.\PHP_EOL : ""; if ($pattern) { $this->expectOutputRegex($output); } else { $this->expectOutputString($output); } $this->assertSame($exitStatus, $this->cli->get()->dispatch($argv)); } public function testPrintVersion(): void { $this->assertConsole("arsse.php --version", 0, Arsse::VERSION); $this->cli->loadConf->never()->called(); } /** @dataProvider provideHelpText */ public function testPrintHelp(string $cmd, string $name): void { $this->assertConsole($cmd, 0, str_replace("arsse.php", $name, CLI::USAGE)); $this->cli->loadConf->never()->called(); } public function provideHelpText(): iterable { return [ ["arsse.php --help", "arsse.php"], ["arsse --help", "arsse"], ["thearsse --help", "thearsse"], ["arsse.php -h", "arsse.php"], ["arsse -h", "arsse"], ["thearsse -h", "thearsse"], ]; } public function testStartTheDaemon(): void { $srv = $this->mock(Service::class); $srv->watch->returns(new \DateTimeImmutable); $this->objMock->get->with(Service::class)->returns($srv->get()); $this->assertConsole("arsse.php daemon", 0); $this->cli->loadConf->called(); $srv->watch->calledWith(true); } public function testStartTheForkingDaemon(): void { $f = tempnam(sys_get_temp_dir(), "arsse"); $srv = $this->mock(Service::class); $srv->watch->returns(new \DateTimeImmutable); $daemon = $this->mock(Daemon::class); $daemon->checkPIDFilePath->returns($f); $daemon->fork->returns(null); $this->objMock->get->with(Service::class)->returns($srv->get()); $this->objMock->get->with(Daemon::class)->returns($daemon->get()); $this->assertConsole("arsse.php daemon --fork=arsse.pid", 0); $this->assertFileDoesNotExist($f); Phony::inOrder( $daemon->checkPIDFilePath->calledWith("arsse.pid"), $daemon->fork->calledWith($f), $this->cli->loadConf->called(), $srv->watch->calledWith(true) ); } public function testFailToStartTheForkingDaemon(): void { $srv = $this->mock(Service::class); $srv->watch->returns(new \DateTimeImmutable); $daemon = $this->mock(Daemon::class); $daemon->checkPIDFilePath->throws(new Service\Exception("pidDuplicate", ['pid' => 2112])); $daemon->fork->returns(null); $this->objMock->get->with(Service::class)->returns($srv->get()); $this->objMock->get->with(Daemon::class)->returns($daemon->get()); $this->assertConsole("arsse.php daemon --fork=arsse.pid", 10809); $daemon->checkPIDFilePath->calledWith("arsse.pid"); $daemon->fork->never()->called(); $this->cli->loadConf->never()->called(); $srv->watch->never()->called(); } public function testRefreshAllFeeds(): void { $srv = $this->mock(Service::class); $srv->watch->returns(new \DateTimeImmutable); $this->objMock->get->with(Service::class)->returns($srv->get()); $this->assertConsole("arsse.php feed refresh-all", 0); $this->cli->loadConf->called(); $srv->watch->calledWith(false); } /** @dataProvider provideFeedUpdates */ public function testRefreshAFeed(string $cmd, int $exitStatus, string $output): void { $this->dbMock->feedUpdate->with(1, true)->returns(true); $this->dbMock->feedUpdate->with(2, true)->throws(new \JKingWeb\Arsse\Feed\Exception("", ['url' => "http://example.com/"], $this->mockGuzzleException(ClientException::class, "", 404))); $this->assertConsole($cmd, $exitStatus, $output); $this->cli->loadConf->called(); $this->dbMock->feedUpdate->called(); } public function provideFeedUpdates(): iterable { return [ ["arsse.php feed refresh 1", 0, ""], ["arsse.php feed refresh 2", 10502, ""], ]; } /** @dataProvider provideDefaultConfigurationSaves */ public function testSaveTheDefaultConfiguration(string $cmd, int $exitStatus, string $file): void { $conf = $this->mock(Conf::class); $conf->exportFile->with("php://output", true)->returns(true); $conf->exportFile->with("good.conf", true)->returns(true); $conf->exportFile->with("bad.conf", true)->throws(new \JKingWeb\Arsse\Conf\Exception("fileUnwritable")); $this->objMock->get->with(Conf::class)->returns($conf->get()); $this->assertConsole($cmd, $exitStatus); $this->cli->loadConf->never()->called(); $conf->exportFile->calledWith($file, true); } public function provideDefaultConfigurationSaves(): iterable { return [ ["arsse.php conf save-defaults", 0, "php://output"], ["arsse.php conf save-defaults -", 0, "php://output"], ["arsse.php conf save-defaults good.conf", 0, "good.conf"], ["arsse.php conf save-defaults bad.conf", 10304, "bad.conf"], ]; } /** @dataProvider provideUserList */ public function testListUsers(string $cmd, array $list, int $exitStatus, string $output): void { Arsse::$user = \Phake::mock(User::class); \Phake::when(Arsse::$user)->list()->thenReturn($list); $this->assertConsole($cmd, $exitStatus, $output); } public function provideUserList(): iterable { $list = ["john.doe@example.com", "jane.doe@example.com"]; $str = implode(PHP_EOL, $list); return [ ["arsse.php user list", $list, 0, $str], ["arsse.php user", $list, 0, $str], ["arsse.php user list", [], 0, ""], ["arsse.php user", [], 0, ""], ]; } /** @dataProvider provideUserAdditions */ public function testAddAUser(string $cmd, int $exitStatus, string $output): void { Arsse::$user = \Phake::mock(User::class); \Phake::when(Arsse::$user)->add("john.doe@example.com", $this->anything())->thenThrow(new \JKingWeb\Arsse\User\ExceptionConflict("alreadyExists")); \Phake::when(Arsse::$user)->add("jane.doe@example.com", $this->anything())->thenReturnCallback(function($u, $p) { return $p; }); \Phake::when(Arsse::$user)->add("jane.doe@example.com", null)->thenReturn("random password"); $this->assertConsole($cmd, $exitStatus, $output); } public function provideUserAdditions(): iterable { return [ ["arsse.php user add john.doe@example.com", 10403, ""], ["arsse.php user add jane.doe@example.com", 0, "random password"], ["arsse.php user add jane.doe@example.com superman", 0, ""], ]; } public function testAddAUserAsAdministrator(): void { Arsse::$user = \Phake::mock(User::class); \Phake::when(Arsse::$user)->add->thenReturn("random password"); \Phake::when(Arsse::$user)->propertiesSet->thenReturn([]); $this->assertConsole("arsse.php user add jane.doe@example.com --admin", 0, "random password"); \Phake::verify(Arsse::$user)->add("jane.doe@example.com", null); \Phake::verify(Arsse::$user)->propertiesSet("jane.doe@example.com", ['admin' => true]); } /** @dataProvider provideUserAuthentication */ public function testAuthenticateAUser(string $cmd, int $exitStatus, string $output): void { Arsse::$user = \Phake::mock(User::class); \Phake::when(Arsse::$user)->auth->thenReturn(false); \Phake::when(Arsse::$user)->auth("john.doe@example.com", "secret")->thenReturn(true); \Phake::when(Arsse::$user)->auth("jane.doe@example.com", "superman")->thenReturn(true); $fever = $this->mock(FeverUser::class); $fever->authenticate->returns(false); $fever->authenticate->with("john.doe@example.com", "ashalla")->returns(true); $fever->authenticate->with("jane.doe@example.com", "thx1138")->returns(true); $this->objMock->get->with(FeverUser::class)->returns($fever->get()); $this->assertConsole($cmd, $exitStatus, $output); } public function provideUserAuthentication(): iterable { $l = new \JKingWeb\Arsse\Lang; $success = $l("CLI.Auth.Success"); $failure = $l("CLI.Auth.Failure"); return [ ["arsse.php user auth john.doe@example.com secret", 0, $success], ["arsse.php user auth john.doe@example.com superman", 1, $failure], ["arsse.php user auth jane.doe@example.com secret", 1, $failure], ["arsse.php user auth jane.doe@example.com superman", 0, $success], ["arsse.php user auth john.doe@example.com ashalla --fever", 0, $success], ["arsse.php user auth john.doe@example.com thx1138 --fever", 1, $failure], ["arsse.php user auth --fever jane.doe@example.com ashalla", 1, $failure], ["arsse.php user auth --fever jane.doe@example.com thx1138", 0, $success], ]; } /** @dataProvider provideUserRemovals */ public function testRemoveAUser(string $cmd, int $exitStatus, string $output): void { Arsse::$user = \Phake::mock(User::class); \Phake::when(Arsse::$user)->remove->thenThrow(new \JKingWeb\Arsse\User\ExceptionConflict("doesNotExist")); \Phake::when(Arsse::$user)->remove("john.doe@example.com")->thenReturn(true); $this->assertConsole($cmd, $exitStatus, $output); } public function provideUserRemovals(): iterable { return [ ["arsse.php user remove john.doe@example.com", 0, ""], ["arsse.php user remove jane.doe@example.com", 10402, ""], ]; } /** @dataProvider provideUserPasswordChanges */ public function testChangeAUserPassword(string $cmd, int $exitStatus, string $output): void { $passwordChange = function($user, $pass = null) { switch ($user) { case "jane.doe@example.com": throw new \JKingWeb\Arsse\User\ExceptionConflict("doesNotExist"); case "john.doe@example.com": return is_null($pass) ? "random password" : $pass; } }; // FIXME: Phake is somehow unable to mock the User class correctly, so we use PHPUnit's mocks instead Arsse::$user = $this->createMock(User::class); Arsse::$user->method("passwordSet")->will($this->returnCallback($passwordChange)); $fever = $this->mock(FeverUser::class); $fever->register->does($passwordChange); $this->objMock->get->with(FeverUser::class)->returns($fever->get()); $this->assertConsole($cmd, $exitStatus, $output); } public function provideUserPasswordChanges(): iterable { return [ ["arsse.php user set-pass john.doe@example.com", 0, "random password"], ["arsse.php user set-pass john.doe@example.com superman", 0, ""], ["arsse.php user set-pass jane.doe@example.com", 10402, ""], ["arsse.php user set-pass john.doe@example.com --fever", 0, "random password"], ["arsse.php user set-pass --fever john.doe@example.com superman", 0, ""], ["arsse.php user set-pass jane.doe@example.com --fever", 10402, ""], ]; } /** @dataProvider provideUserPasswordClearings */ public function testClearAUserPassword(string $cmd, int $exitStatus, string $output): void { $passwordClear = function($user) { switch ($user) { case "jane.doe@example.com": throw new \JKingWeb\Arsse\User\ExceptionConflict("doesNotExist"); case "john.doe@example.com": return true; } }; // FIXME: Phake is somehow unable to mock the User class correctly, so we use PHPUnit's mocks instead Arsse::$user = $this->createMock(User::class); Arsse::$user->method("passwordUnset")->will($this->returnCallback($passwordClear)); $fever = $this->mock(FeverUser::class); $fever->unregister->does($passwordClear); $this->objMock->get->with(FeverUser::class)->returns($fever->get()); $this->assertConsole($cmd, $exitStatus, $output); } public function provideUserPasswordClearings(): iterable { return [ ["arsse.php user unset-pass john.doe@example.com", 0, ""], ["arsse.php user unset-pass jane.doe@example.com", 10402, ""], ["arsse.php user unset-pass john.doe@example.com --fever", 0, ""], ["arsse.php user unset-pass jane.doe@example.com --fever", 10402, ""], ]; } /** @dataProvider provideOpmlExports */ public function testExportToOpml(string $cmd, int $exitStatus, string $file, string $user, bool $flat): void { $opml = $this->mock(OPML::class); $opml->exportFile->with("php://output", $user, $flat)->returns(true); $opml->exportFile->with("good.opml", $user, $flat)->returns(true); $opml->exportFile->with("bad.opml", $user, $flat)->throws(new \JKingWeb\Arsse\ImportExport\Exception("fileUnwritable")); $this->objMock->get->with(OPML::class)->returns($opml->get()); $this->assertConsole($cmd, $exitStatus); $this->cli->loadConf->called(); $opml->exportFile->calledWith($file, $user, $flat); } public function provideOpmlExports(): iterable { return [ ["arsse.php export john.doe@example.com", 0, "php://output", "john.doe@example.com", false], ["arsse.php export john.doe@example.com -", 0, "php://output", "john.doe@example.com", false], ["arsse.php export john.doe@example.com good.opml", 0, "good.opml", "john.doe@example.com", false], ["arsse.php export john.doe@example.com bad.opml", 10604, "bad.opml", "john.doe@example.com", false], ["arsse.php export john.doe@example.com --flat", 0, "php://output", "john.doe@example.com", true], ["arsse.php export john.doe@example.com - --flat", 0, "php://output", "john.doe@example.com", true], ["arsse.php export --flat john.doe@example.com good.opml", 0, "good.opml", "john.doe@example.com", true], ["arsse.php export john.doe@example.com bad.opml --flat", 10604, "bad.opml", "john.doe@example.com", true], ["arsse.php export jane.doe@example.com", 0, "php://output", "jane.doe@example.com", false], ["arsse.php export jane.doe@example.com -", 0, "php://output", "jane.doe@example.com", false], ["arsse.php export jane.doe@example.com good.opml", 0, "good.opml", "jane.doe@example.com", false], ["arsse.php export jane.doe@example.com bad.opml", 10604, "bad.opml", "jane.doe@example.com", false], ["arsse.php export jane.doe@example.com --flat", 0, "php://output", "jane.doe@example.com", true], ["arsse.php export jane.doe@example.com - --flat", 0, "php://output", "jane.doe@example.com", true], ["arsse.php export --flat jane.doe@example.com good.opml", 0, "good.opml", "jane.doe@example.com", true], ["arsse.php export jane.doe@example.com bad.opml --flat", 10604, "bad.opml", "jane.doe@example.com", true], ["arsse.php export john.doe@example.com -f", 0, "php://output", "john.doe@example.com", true], ["arsse.php export john.doe@example.com - -f", 0, "php://output", "john.doe@example.com", true], ["arsse.php export -f john.doe@example.com good.opml", 0, "good.opml", "john.doe@example.com", true], ["arsse.php export john.doe@example.com bad.opml -f", 10604, "bad.opml", "john.doe@example.com", true], ["arsse.php export jane.doe@example.com -f", 0, "php://output", "jane.doe@example.com", true], ["arsse.php export jane.doe@example.com - -f", 0, "php://output", "jane.doe@example.com", true], ["arsse.php export -f jane.doe@example.com good.opml", 0, "good.opml", "jane.doe@example.com", true], ["arsse.php export jane.doe@example.com bad.opml -f", 10604, "bad.opml", "jane.doe@example.com", true], ]; } /** @dataProvider provideOpmlImports */ public function testImportFromOpml(string $cmd, int $exitStatus, string $file, string $user, bool $flat, bool $replace): void { $opml = $this->mock(OPML::class); $opml->importFile->with("php://input", $user, $flat, $replace)->returns(true); $opml->importFile->with("good.opml", $user, $flat, $replace)->returns(true); $opml->importFile->with("bad.opml", $user, $flat, $replace)->throws(new \JKingWeb\Arsse\ImportExport\Exception("fileUnreadable")); $this->objMock->get->with(OPML::class)->returns($opml->get()); $this->assertConsole($cmd, $exitStatus); $this->cli->loadConf->called(); $opml->importFile->calledWith($file, $user, $flat, $replace); } public function provideOpmlImports(): iterable { return [ ["arsse.php import john.doe@example.com", 0, "php://input", "john.doe@example.com", false, false], ["arsse.php import john.doe@example.com -", 0, "php://input", "john.doe@example.com", false, false], ["arsse.php import john.doe@example.com good.opml", 0, "good.opml", "john.doe@example.com", false, false], ["arsse.php import john.doe@example.com bad.opml", 10603, "bad.opml", "john.doe@example.com", false, false], ["arsse.php import john.doe@example.com --flat", 0, "php://input", "john.doe@example.com", true, false], ["arsse.php import john.doe@example.com - --flat", 0, "php://input", "john.doe@example.com", true, false], ["arsse.php import --flat john.doe@example.com good.opml", 0, "good.opml", "john.doe@example.com", true, false], ["arsse.php import john.doe@example.com bad.opml --flat", 10603, "bad.opml", "john.doe@example.com", true, false], ["arsse.php import jane.doe@example.com", 0, "php://input", "jane.doe@example.com", false, false], ["arsse.php import jane.doe@example.com -", 0, "php://input", "jane.doe@example.com", false, false], ["arsse.php import jane.doe@example.com good.opml", 0, "good.opml", "jane.doe@example.com", false, false], ["arsse.php import jane.doe@example.com bad.opml", 10603, "bad.opml", "jane.doe@example.com", false, false], ["arsse.php import jane.doe@example.com --flat", 0, "php://input", "jane.doe@example.com", true, false], ["arsse.php import jane.doe@example.com - --flat", 0, "php://input", "jane.doe@example.com", true, false], ["arsse.php import --flat jane.doe@example.com good.opml", 0, "good.opml", "jane.doe@example.com", true, false], ["arsse.php import jane.doe@example.com bad.opml --flat", 10603, "bad.opml", "jane.doe@example.com", true, false], ["arsse.php import john.doe@example.com --replace", 0, "php://input", "john.doe@example.com", false, true], ["arsse.php import john.doe@example.com - -r", 0, "php://input", "john.doe@example.com", false, true], ["arsse.php import --replace john.doe@example.com good.opml", 0, "good.opml", "john.doe@example.com", false, true], ["arsse.php import -r john.doe@example.com bad.opml", 10603, "bad.opml", "john.doe@example.com", false, true], ["arsse.php import --replace john.doe@example.com --flat", 0, "php://input", "john.doe@example.com", true, true], ["arsse.php import -r john.doe@example.com - --flat", 0, "php://input", "john.doe@example.com", true, true], ["arsse.php import --flat john.doe@example.com good.opml -r", 0, "good.opml", "john.doe@example.com", true, true], ["arsse.php import --replace john.doe@example.com bad.opml --flat", 10603, "bad.opml", "john.doe@example.com", true, true], ["arsse.php import jane.doe@example.com -r ", 0, "php://input", "jane.doe@example.com", false, true], ["arsse.php import jane.doe@example.com - --replace", 0, "php://input", "jane.doe@example.com", false, true], ["arsse.php import -r jane.doe@example.com good.opml", 0, "good.opml", "jane.doe@example.com", false, true], ["arsse.php import --replace jane.doe@example.com bad.opml", 10603, "bad.opml", "jane.doe@example.com", false, true], ["arsse.php import jane.doe@example.com --flat -r", 0, "php://input", "jane.doe@example.com", true, true], ["arsse.php import jane.doe@example.com - --flat --replace", 0, "php://input", "jane.doe@example.com", true, true], ["arsse.php import --flat jane.doe@example.com good.opml -r", 0, "good.opml", "jane.doe@example.com", true, true], ["arsse.php import jane.doe@example.com bad.opml --replace --flat", 10603, "bad.opml", "jane.doe@example.com", true, true], ]; } public function testShowMetadataOfAUser(): void { $data = [ 'num' => 42, 'admin' => false, 'lang' => "en-ca", 'tz' => "America/Toronto", 'root_folder_name' => null, 'sort_asc' => true, 'theme' => null, 'page_size' => 50, 'shortcuts' => true, 'gestures' => null, 'reading_time' => false, 'stylesheet' => "body {color:gray}", ]; $exp = implode(\PHP_EOL, [ "num 42", "admin false", "lang 'en-ca'", "tz 'America/Toronto'", "root_folder_name NULL", "sort_asc true", "theme NULL", "page_size 50", "shortcuts true", "gestures NULL", "reading_time false", "stylesheet 'body {color:gray}'", ]); Arsse::$user = \Phake::mock(User::class); \Phake::when(Arsse::$user)->propertiesGet->thenReturn($data); $this->assertConsole("arsse.php user show john.doe@example.com", 0, $exp); \Phake::verify(Arsse::$user)->propertiesGet("john.doe@example.com"); } /** @dataProvider provideMetadataChanges */ public function testSetMetadataOfAUser(string $cmd, string $user, array $in, array $out, int $exp): void { Arsse::$user = \Phake::mock(User::class); \Phake::when(Arsse::$user)->propertiesSet->thenReturn($out); $this->assertConsole($cmd, $exp, ""); \Phake::verify(Arsse::$user)->propertiesSet($user, $in); } public function provideMetadataChanges(): iterable { return [ ["arsse.php user set john admin true", "john", ['admin' => "true"], ['admin' => "true"], 0], ["arsse.php user set john bogus 1", "john", ['bogus' => "1"], [], 1], ["arsse.php user unset john admin", "john", ['admin' => null], ['admin' => null], 0], ["arsse.php user unset john bogus", "john", ['bogus' => null], [], 1], ]; } public function testListTokens(): void { $data = [ ['label' => 'Ook', 'id' => "TOKEN 1"], ['label' => 'Eek', 'id' => "TOKEN 2"], ['label' => null, 'id' => "TOKEN 3"], ['label' => 'Ack', 'id' => "TOKEN 4"], ]; $exp = implode(\PHP_EOL, [ "TOKEN 3 ", "TOKEN 4 Ack", "TOKEN 2 Eek", "TOKEN 1 Ook", ]); $t = $this->mock(MinifluxToken::class); $t->tokenList->returns($data); $this->objMock->get->with(MinifluxToken::class)->returns($t->get()); $this->assertConsole("arsse.php token list john", 0, $exp); $t->tokenList->calledWith("john"); } public function testCreateToken(): void { $t = $this->mock(MinifluxToken::class); $t->tokenGenerate->returns("RANDOM TOKEN"); $this->objMock->get->with(MinifluxToken::class)->returns($t->get()); $this->assertConsole("arse.php token create jane", 0, "RANDOM TOKEN"); $t->tokenGenerate->calledWith("jane", null); } public function testCreateTokenWithLabel(): void { $t = $this->mock(MinifluxToken::class); $t->tokenGenerate->returns("RANDOM TOKEN"); $this->objMock->get->with(MinifluxToken::class)->returns($t->get()); $this->assertConsole("arse.php token create jane Ook", 0, "RANDOM TOKEN"); $t->tokenGenerate->calledWith("jane", "Ook"); } public function testRevokeAToken(): void { $this->dbMock->tokenRevoke->returns(true); $this->assertConsole("arse.php token revoke jane TOKEN_ID", 0); $this->dbMock->tokenRevoke->calledWith("jane", "miniflux.login", "TOKEN_ID"); } public function testRevokeAllTokens(): void { $this->dbMock->tokenRevoke->returns(true); $this->assertConsole("arse.php token revoke jane", 0); $this->dbMock->tokenRevoke->calledWith("jane", "miniflux.login", null); } }