diff --git a/lib/CLI.php b/lib/CLI.php index 96936980..218e3d30 100644 --- a/lib/CLI.php +++ b/lib/CLI.php @@ -7,6 +7,7 @@ declare(strict_types=1); namespace JKingWeb\Arsse; use JKingWeb\Arsse\REST\Fever\User as Fever; +use JKingWeb\Arsse\ImportExport\OPML; class CLI { const USAGE = << [--oldpass=] [--fever] arsse.php user auth [--fever] + arsse.php export [] [-f | --flat] arsse.php --version arsse.php --help | -h @@ -54,6 +56,12 @@ USAGE_TEXT; return true; } + protected function resolveFile($file, string $mode): string { + // TODO: checking read/write permissions on the provided path may be useful + $stdinOrStdout = in_array($mode, ["r", "r+"]) ? "php://input" : "php://output"; + return ($file === "-" ? null : $file) ?? $stdinOrStdout; + } + public function dispatch(array $argv = null) { $argv = $argv ?? $_SERVER['argv']; $argv0 = array_shift($argv); @@ -62,7 +70,12 @@ USAGE_TEXT; 'help' => false, ]); try { - switch ($this->command(["--help", "--version", "daemon", "feed refresh", "feed refresh-all", "conf save-defaults", "user"], $args)) { + $cmd = $this->command(["--help", "--version", "daemon", "feed refresh", "feed refresh-all", "conf save-defaults", "user", "export"], $args); + if ($cmd && !in_array($cmd, ["--help", "--version", "conf save-defaults"])) { + // only certain commands don't require configuration to be loaded + $this->loadConf(); + } + switch ($cmd) { case "--help": echo $this->usage($argv0).\PHP_EOL; return 0; @@ -70,23 +83,22 @@ USAGE_TEXT; echo Arsse::VERSION.\PHP_EOL; return 0; case "daemon": - $this->loadConf(); - $this->getService()->watch(true); + $this->getInstance(Service::class)->watch(true); return 0; case "feed refresh": - $this->loadConf(); return (int) !Arsse::$db->feedUpdate((int) $args[''], true); case "feed refresh-all": - $this->loadConf(); - $this->getService()->watch(false); + $this->getInstance(Service::class)->watch(false); return 0; case "conf save-defaults": - $file = $args['']; - $file = ($file === "-" ? null : $file) ?? "php://output"; - return (int) !($this->getConf())->exportFile($file, true); + $file = $this->resolveFile($args[''], "w"); + return (int) !$this->getInstance(Conf::class)->exportFile($file, true); case "user": - $this->loadConf(); return $this->userManage($args); + case "export": + $u = $args['']; + $file = $this->resolveFile($args[''], "w"); + return (int) !$this->getInstance(OPML::class)->exportFile($file, $u, $args['--flat']); } } catch (AbstractException $e) { $this->logError($e->getMessage()); @@ -99,19 +111,8 @@ USAGE_TEXT; fwrite(STDERR, $msg.\PHP_EOL); } - /** @codeCoverageIgnore */ - protected function getService(): Service { - return new Service; - } - - /** @codeCoverageIgnore */ - protected function getConf(): Conf { - return new Conf; - } - - /** @codeCoverageIgnore */ - protected function getFever(): Fever { - return new Fever; + protected function getInstance(string $class) { + return new $class; } protected function userManage($args): int { @@ -120,7 +121,7 @@ USAGE_TEXT; return $this->userAddOrSetPassword("add", $args[""], $args[""]); case "set-pass": if ($args['--fever']) { - $passwd = $this->getFever()->register($args[""], $args[""]); + $passwd = $this->getInstance(Fever::class)->register($args[""], $args[""]); if (is_null($args[""])) { echo $passwd.\PHP_EOL; } @@ -130,7 +131,7 @@ USAGE_TEXT; } case "unset-pass": if ($args['--fever']) { - $this->getFever()->unregister($args[""]); + $this->getInstance(Fever::class)->unregister($args[""]); } else { Arsse::$user->passwordUnset($args[""], $args["--oldpass"]); } @@ -162,7 +163,7 @@ USAGE_TEXT; } protected function userAuthenticate(string $user, string $password, bool $fever = false): int { - $result = $fever ? $this->getFever()->authenticate($user, $password) : Arsse::$user->auth($user, $password); + $result = $fever ? $this->getInstance(Fever::class)->authenticate($user, $password) : Arsse::$user->auth($user, $password); if ($result) { echo Arsse::$lang->msg("CLI.Auth.Success").\PHP_EOL; return 0; diff --git a/tests/cases/CLI/TestCLI.php b/tests/cases/CLI/TestCLI.php index 3f1c3d30..56202d94 100644 --- a/tests/cases/CLI/TestCLI.php +++ b/tests/cases/CLI/TestCLI.php @@ -13,6 +13,7 @@ use JKingWeb\Arsse\Database; use JKingWeb\Arsse\Service; use JKingWeb\Arsse\CLI; use JKingWeb\Arsse\REST\Fever\User as FeverUser; +use JKingWeb\Arsse\ImportExport\OPML; use Phake; /** @covers \JKingWeb\Arsse\CLI */ @@ -68,21 +69,21 @@ class TestCLI extends \JKingWeb\Arsse\Test\AbstractTest { public function testStartTheDaemon() { $srv = Phake::mock(Service::class); Phake::when($srv)->watch->thenReturn(new \DateTimeImmutable); - Phake::when($this->cli)->getService->thenReturn($srv); + Phake::when($this->cli)->getInstance(Service::class)->thenReturn($srv); $this->assertConsole($this->cli, "arsse.php daemon", 0); $this->assertLoaded(true); Phake::verify($srv)->watch(true); - Phake::verify($this->cli)->getService; + Phake::verify($this->cli)->getInstance(Service::class); } public function testRefreshAllFeeds() { $srv = Phake::mock(Service::class); Phake::when($srv)->watch->thenReturn(new \DateTimeImmutable); - Phake::when($this->cli)->getService->thenReturn($srv); + Phake::when($this->cli)->getInstance(Service::class)->thenReturn($srv); $this->assertConsole($this->cli, "arsse.php feed refresh-all", 0); $this->assertLoaded(true); Phake::verify($srv)->watch(false); - Phake::verify($this->cli)->getService; + Phake::verify($this->cli)->getInstance(Service::class); } /** @dataProvider provideFeedUpdates */ @@ -108,7 +109,7 @@ class TestCLI extends \JKingWeb\Arsse\Test\AbstractTest { Phake::when($conf)->exportFile("php://output", true)->thenReturn(true); Phake::when($conf)->exportFile("good.conf", true)->thenReturn(true); Phake::when($conf)->exportFile("bad.conf", true)->thenThrow(new \JKingWeb\Arsse\Conf\Exception("fileUnwritable")); - Phake::when($this->cli)->getConf->thenReturn($conf); + Phake::when($this->cli)->getInstance(Conf::class)->thenReturn($conf); $this->assertConsole($this->cli, $cmd, $exitStatus); $this->assertLoaded(false); Phake::verify($conf)->exportFile($file, true); @@ -179,7 +180,7 @@ class TestCLI extends \JKingWeb\Arsse\Test\AbstractTest { \Phake::when($fever)->authenticate->thenReturn(false); \Phake::when($fever)->authenticate("john.doe@example.com", "ashalla")->thenReturn(true); \Phake::when($fever)->authenticate("jane.doe@example.com", "thx1138")->thenReturn(true); - \Phake::when($this->cli)->getFever->thenReturn($fever); + \Phake::when($this->cli)->getInstance(FeverUser::class)->thenReturn($fever); $this->assertConsole($this->cli, $cmd, $exitStatus, $output); } @@ -234,7 +235,7 @@ class TestCLI extends \JKingWeb\Arsse\Test\AbstractTest { Arsse::$user->method("passwordSet")->will($this->returnCallback($passwordChange)); $fever = \Phake::mock(FeverUser::class); \Phake::when($fever)->register->thenReturnCallback($passwordChange); - \Phake::when($this->cli)->getFever->thenReturn($fever); + \Phake::when($this->cli)->getInstance(FeverUser::class)->thenReturn($fever); $this->assertConsole($this->cli, $cmd, $exitStatus, $output); } @@ -264,7 +265,7 @@ class TestCLI extends \JKingWeb\Arsse\Test\AbstractTest { Arsse::$user->method("passwordUnset")->will($this->returnCallback($passwordClear)); $fever = \Phake::mock(FeverUser::class); \Phake::when($fever)->unregister->thenReturnCallback($passwordClear); - \Phake::when($this->cli)->getFever->thenReturn($fever); + \Phake::when($this->cli)->getInstance(FeverUser::class)->thenReturn($fever); $this->assertConsole($this->cli, $cmd, $exitStatus, $output); } @@ -276,4 +277,37 @@ class TestCLI extends \JKingWeb\Arsse\Test\AbstractTest { ["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) { + $opml = Phake::mock(OPML::class); + Phake::when($opml)->exportFile("php://output", $user, $flat)->thenReturn(true); + Phake::when($opml)->exportFile("good.opml", $user, $flat)->thenReturn(true); + Phake::when($opml)->exportFile("bad.opml", $user, $flat)->thenThrow(new \JKingWeb\Arsse\ImportExport\Exception("fileUnwritable")); + Phake::when($this->cli)->getInstance(OPML::class)->thenReturn($opml); + $this->assertConsole($this->cli, $cmd, $exitStatus); + $this->assertLoaded(true); + Phake::verify($opml)->exportFile($file, $user, $flat); + } + + public function provideOpmlExports() { + 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], + ]; + } }