diff --git a/app/Commands/BaseCommand.php b/app/Commands/BaseCommand.php index b5d1eff..4e79b9b 100644 --- a/app/Commands/BaseCommand.php +++ b/app/Commands/BaseCommand.php @@ -2,17 +2,17 @@ namespace App\Commands; +use App\Commands\Concerns\InteractsWithIO; use App\Helpers\Configuration; use DeliciousBrains\SpinupWp\SpinupWp; use Exception; use GuzzleHttp\Client; -use Illuminate\Support\Collection; use LaravelZero\Framework\Commands\Command; -use Symfony\Component\Console\Formatter\OutputFormatterStyle; -use Symfony\Component\Console\Helper\Table; abstract class BaseCommand extends Command { + use InteractsWithIO; + protected Configuration $config; protected SpinupWp $spinupwp; @@ -35,7 +35,7 @@ public function handle(): int { if ($this->requiresToken && !$this->config->isConfigured()) { $this->error("You must first run 'spinupwp configure' in order to set up your API token."); - return 1; + return self::FAILURE; } try { @@ -52,21 +52,21 @@ public function handle(): int ])); } - $this->format($this->action()); - - return 0; + return $this->action(); } catch (Exception $e) { $this->error($e->getMessage()); - return 1; + return self::FAILURE; } } protected function apiToken(): string { $apiToken = $this->config->get('api_token', $this->profile()); + if (!$apiToken) { throw new Exception("The API token for the profile {$this->profile()} is not yet configured"); } + return $apiToken; } @@ -75,137 +75,9 @@ protected function profile(): string if (is_string($this->option('profile'))) { return $this->option('profile'); } - return 'default'; - } - - protected function format($resource): void - { - if (empty($resource) || ($resource instanceof Collection && $resource->isEmpty())) { - return; - } - - $this->setStyles(); - - if ($this->displayFormat() === 'table' && $this->largeOutput) { - $this->largeOutput($resource); - return; - } - - if ($this->displayFormat() === 'table') { - $this->toTable($resource); - return; - } - - $this->toJson($resource); - } - - protected function setStyles(): void - { - if (!$this->output->getFormatter()->hasStyle('enabled')) { - $this->output->getFormatter()->setStyle( - 'enabled', - new OutputFormatterStyle('green'), - ); - } - - if (!$this->output->getFormatter()->hasStyle('disabled')) { - $this->output->getFormatter()->setStyle( - 'disabled', - new OutputFormatterStyle('red'), - ); - } - } - - protected function displayFormat(): string - { - if (is_string($this->option('format'))) { - return $this->option('format'); - } - return $this->config->get('format', $this->profile()); - } - - protected function toJson($resource): void - { - if (!is_array($resource)) { - $resource = $resource->toArray(); - } - $this->line(json_encode($resource, JSON_PRETTY_PRINT)); - } - - protected function toTable($resource): void - { - $tableHeaders = []; - - if ($resource instanceof Collection) { - $firstElement = $resource->first(); - - if (!is_array($firstElement)) { - $firstElement = $firstElement->toArray(); - } - - $tableHeaders = array_keys($firstElement); - - $rows = []; - $resource->each(function ($item) use (&$rows) { - if (!is_array($item)) { - $item->toArray(); - } - - $row = array_map(function ($value) { - if (is_array($value)) { - $value = ''; - } - if (is_bool($value)) { - $value = $value ? 'Y' : 'N'; - } - return $value; - }, array_values($item)); - - $rows[] = $row; - }); - } - - $this->table( - $tableHeaders, - $rows, - ); - } - - protected function largeOutput(array $resource): void - { - $table = new Table($this->output); - $rows = []; - - foreach ($resource as $key => $value) { - $rows[] = ['' . $key . '', $value]; - } - - $table->setRows($rows)->setStyle('default'); - - if (!empty($this->columnsMaxWidths)) { - foreach ($this->columnsMaxWidths as $column) { - $table->setColumnMaxWidth($column[0], $column[1]); - } - } - - $table->render(); - } - - protected function formatBytes(int $bytes, int $precision = 1, bool $trueSize = false): string - { - $units = ['B', 'KB', 'MB', 'GB', 'TB']; - $block = ($trueSize) ? 1024 : 1000; - - $bytes = max($bytes, 0); - $pow = floor(($bytes ? log($bytes) : 0) / log($block)); - $pow = min($pow, count($units) - 1); - $bytes /= pow($block, $pow); - - $total = ($trueSize || $precision > 0) ? round($bytes, $precision) : floor($bytes); - - return $total . ' ' . $units[$pow]; + return 'default'; } - abstract protected function action(); + abstract protected function action(): int; } diff --git a/app/Commands/Concerns/InteractsWithIO.php b/app/Commands/Concerns/InteractsWithIO.php new file mode 100644 index 0000000..fa86e8b --- /dev/null +++ b/app/Commands/Concerns/InteractsWithIO.php @@ -0,0 +1,178 @@ +isEmpty())) { + return; + } + + $this->setStyles(); + + if ($this->largeOutput && $this->displayFormat() === 'table') { + $this->largeOutput($resource); + return; + } + + if ($this->displayFormat() === 'table') { + $this->toTable($resource); + return; + } + + $this->toJson($resource); + } + + protected function setStyles(): void + { + if (!$this->output->getFormatter()->hasStyle('enabled')) { + $this->output->getFormatter()->setStyle( + 'enabled', + new OutputFormatterStyle('green'), + ); + } + + if (!$this->output->getFormatter()->hasStyle('disabled')) { + $this->output->getFormatter()->setStyle( + 'disabled', + new OutputFormatterStyle('red'), + ); + } + } + + protected function displayFormat(): string + { + if (is_string($this->option('format'))) { + return $this->option('format'); + } + + return (string) $this->config->get('format', $this->profile()); + } + + /** + * @param mixed $resource + */ + protected function toJson($resource): void + { + $this->line((string) json_encode($resource->toArray(), JSON_PRETTY_PRINT)); + } + + /** + * @param mixed $resource + */ + protected function toTable($resource): void + { + $rows = []; + $tableHeaders = []; + + if ($resource instanceof Collection) { + $firstElement = $resource->first(); + + if (!is_array($firstElement)) { + $firstElement = $firstElement->toArray(); + } + + $tableHeaders = array_keys($firstElement); + + $resource->each(function ($item) use (&$rows) { + if (!is_array($item)) { + $item->toArray(); + } + + $row = array_map(function ($value) { + if (is_array($value)) { + $value = ''; + } + if (is_bool($value)) { + $value = $value ? 'Y' : 'N'; + } + return $value; + }, array_values($item)); + + $rows[] = $row; + }); + } + + $this->table($tableHeaders, $rows); + } + + public function askToSelectSite(string $question): int + { + $choices = collect($this->spinupwp->sites->list()); + + return $this->askToSelect( + $question, + $choices->keyBy('id')->map(fn ($site) => $site->domain)->toArray() + ); + } + + public function askToSelectServer(string $question): int + { + $choices = collect($this->spinupwp->servers->list()); + + return $this->askToSelect( + $question, + $choices->keyBy('id')->map(fn ($server) => $server->name)->toArray() + ); + } + + /** + * @param mixed $default + */ + protected function askToSelect(string $question, array $choices, $default = null): int + { + $question = new class($question, $choices, $default) extends ChoiceQuestion { + public function isAssoc(array $array): bool + { + return true; + } + }; + + return (int) $this->output->askQuestion($question); + } + + protected function largeOutput(array $resource): void + { + $table = new Table($this->output); + $rows = []; + + foreach ($resource as $key => $value) { + $rows[] = ['' . $key . '', $value]; + } + + $table->setRows($rows)->setStyle('default'); + + if (!empty($this->columnsMaxWidths)) { + foreach ($this->columnsMaxWidths as $column) { + $table->setColumnMaxWidth($column[0], $column[1]); + } + } + + $table->render(); + } + + protected function formatBytes(int $bytes, int $precision = 1, bool $trueSize = false): string + { + $units = ['B', 'KB', 'MB', 'GB', 'TB']; + $block = ($trueSize) ? 1024 : 1000; + + $bytes = max($bytes, 0); + $pow = floor(($bytes ? log($bytes) : 0) / log($block)); + $pow = min($pow, count($units) - 1); + $bytes /= pow($block, $pow); + + $total = ($trueSize || $precision > 0) ? round($bytes, $precision) : floor($bytes); + + return $total . ' ' . $units[$pow]; + } +} diff --git a/app/Commands/Concerns/InteractsWithRemote.php b/app/Commands/Concerns/InteractsWithRemote.php new file mode 100644 index 0000000..b6ba71f --- /dev/null +++ b/app/Commands/Concerns/InteractsWithRemote.php @@ -0,0 +1,23 @@ + config('app.ssh_timeout'), + 'ControlMaster' => 'auto', + 'ControlPath' => $this->config->sshControlPath(), + 'ControlPersist' => 100, + 'LogLevel' => 'QUIET', + ])->map(function ($value, $option) { + return "-o $option=$value"; + })->implode(' '); + + passthru("ssh {$options} -t {$user}@{$host} -p {$port} '{$command}'", $exitCode); + + return (int) $exitCode; + } +} diff --git a/app/Commands/ConfigureCommand.php b/app/Commands/ConfigureCommand.php index 13e9ac1..baf17eb 100644 --- a/app/Commands/ConfigureCommand.php +++ b/app/Commands/ConfigureCommand.php @@ -19,16 +19,14 @@ public function handle(): int } if (!empty($this->config->get('api_token', $profile))) { - $this->alert("A profile named {$profile} is already configured"); - $response = $this->ask('Do you want to overwrite the existing configuration? (y/n)', 'y'); + $this->alert("A profile named \"{$profile}\" is already configured"); - while (!in_array($response, ['y', 'n'])) { - $this->error("Please type 'y' or 'n'"); - $response = $this->ask('Do you want to overwrite the existing configuration? (y/n)', 'y'); - } + do { + $response = strtolower($this->ask('Do you want to overwrite the existing profile? (y/n)', 'y')); + } while (!in_array($response, ['y', 'n'])); if ($response === 'n') { - return 0; + return self::SUCCESS; } } @@ -41,18 +39,27 @@ public function handle(): int $defaultFormat = null; while (!in_array($defaultFormat, config('app.output_formats'))) { - $defaultFormat = $this->ask('Default output format (json/table)', null); + $defaultFormat = $this->anticipate('Default output format (json/table)', [ + 'json', + 'table', + ], 'table'); } $this->config->set('api_token', $apiKey, $profile); $this->config->set('format', $defaultFormat, $profile); - $this->info('SpinupWP CLI configured successfully'); - return 0; + $this->info('Profile configured successfully.'); + + if ($profile !== 'default') { + $this->line('To use this profile, add the --profile option to your command:'); + $this->line("`spinupwp servers:list --profile={$profile}`"); + } + + return self::SUCCESS; } - protected function action() + protected function action(): int { - return null; + return self::INVALID; } } diff --git a/app/Commands/Servers/GetCommand.php b/app/Commands/Servers/GetCommand.php index ba839ff..b68ec05 100644 --- a/app/Commands/Servers/GetCommand.php +++ b/app/Commands/Servers/GetCommand.php @@ -6,46 +6,50 @@ class GetCommand extends BaseCommand { - protected $signature = 'servers:get {server_id} {--format=} {--profile=}'; + protected $signature = 'servers:get + {server_id : The server to output} + {--format=} + {--profile=}'; protected $description = 'Get a server'; protected bool $largeOutput = true; - public function action() + public function action(): int { $this->columnsMaxWidths[] = [1, 50]; $serverId = $this->argument('server_id'); - - $server = $this->spinupwp->servers->get($serverId); - - if ($this->displayFormat() === 'json') { - return $server; + $server = $this->spinupwp->servers->get((int) $serverId); + + if ($this->displayFormat() === 'table') { + $server = [ + 'ID' => $server->id, + 'Name' => $server->name, + 'Provider Name' => $server->provider_name, + 'IP Address' => $server->ip_address, + 'SSH Port' => $server->ssh_port, + 'Ubuntu' => $server->ubuntu_version, + 'Timezone' => $server->timezone, + 'Region' => $server->region, + 'Size' => $server->size, + 'Disk Space' => $this->formatBytes($server->disk_space['used']) . ' of ' . $this->formatBytes($server->disk_space['total'], 0) . ' used', + 'Database Server' => $server->database['server'], + 'Database Host' => $server->database['host'], + 'Database Port' => $server->database['port'], + 'SSH Public Key' => $server->ssh_publickey, + 'Git Public Key' => $server->git_publickey, + 'Connection Status' => ucfirst($server->connection_status), + 'Reboot Required' => $server->reboot_required ? 'Yes' : 'No', + 'Upgrade Required' => $server->upgrade_required ? 'Yes' : 'No', + 'Install Notes' => $server->install_notes ?? '', + 'Created At' => $server->created_at, + 'Status' => ucfirst($server->status), + ]; } - return [ - 'ID' => $server->id, - 'Name' => $server->name, - 'Provider Name' => $server->provider_name, - 'IP Address' => $server->ip_address, - 'SSH Port' => $server->ssh_port, - 'Ubuntu' => $server->ubuntu_version, - 'Timezone' => $server->timezone, - 'Region' => $server->region, - 'Size' => $server->size, - 'Disk Space' => $this->formatBytes($server->disk_space['used']) . ' of ' . $this->formatBytes($server->disk_space['total'], 0) . ' used', - 'Database Server' => $server->database['server'], - 'Database Host' => $server->database['host'], - 'Database Port' => $server->database['port'], - 'SSH Public Key' => $server->ssh_publickey, - 'Git Public Key' => $server->git_publickey, - 'Connection Status' => ucfirst($server->connection_status), - 'Reboot Required' => $server->reboot_required ? 'Yes' : 'No', - 'Upgrade Required' => $server->upgrade_required ? 'Yes' : 'No', - 'Install Notes' => $server->install_notes ?? '', - 'Created At' => $server->created_at, - 'Status' => ucfirst($server->status), - ]; + $this->format($server); + + return self::SUCCESS; } } diff --git a/app/Commands/Servers/ListCommand.php b/app/Commands/Servers/ListCommand.php index a8a4b7b..cc30771 100644 --- a/app/Commands/Servers/ListCommand.php +++ b/app/Commands/Servers/ListCommand.php @@ -6,29 +6,33 @@ class ListCommand extends BaseCommand { - protected $signature = 'servers:list {--format=} {--profile=}'; + protected $signature = 'servers:list + {--format=} + {--profile=}'; - protected $description = 'Retrieves a list of servers'; + protected $description = 'List all servers'; - protected function action() + protected function action(): int { $servers = collect($this->spinupwp->servers->list()); if ($servers->isEmpty()) { $this->warn('No servers found.'); - return $servers; + return self::SUCCESS; } - if ($this->displayFormat() === 'json') { - return $servers; + if ($this->displayFormat() === 'table') { + $servers->transform(fn ($server) => [ + 'ID' => $server->id, + 'Name' => $server->name, + 'IP Address' => $server->ip_address, + 'Ubuntu' => $server->ubuntu_version, + 'Database' => $server->database['server'], + ]); } - return $servers->map(fn ($server) => [ - 'ID' => $server->id, - 'Name' => $server->name, - 'IP Address' => $server->ip_address, - 'Ubuntu' => $server->ubuntu_version, - 'Database' => $server->database['server'], - ]); + $this->format($servers); + + return self::SUCCESS; } } diff --git a/app/Commands/Servers/SshCommand.php b/app/Commands/Servers/SshCommand.php new file mode 100644 index 0000000..8526f1d --- /dev/null +++ b/app/Commands/Servers/SshCommand.php @@ -0,0 +1,80 @@ +argument('server_id'); + + if (empty($serverId)) { + $serverId = $this->askToSelectServer('Which server would you like to start an SSH session for'); + } + + $server = $this->spinupwp->servers->get((int) $serverId); + $user = $this->establishUser(); + + $this->line("Establishing a secure connection to [{$server->name}] as [{$user}]..."); + + $exitCode = $this->ssh( + $user, + $server->ip_address, + $server->ssh_port, + ); + + if ($exitCode === 255) { + $this->error("Unable to connect to \"{$server->name}\". Have you added your SSH key to the \"{$user}\" user?"); + } + + return $exitCode; + } + + protected function establishUser(): string + { + $user = $this->argument('user'); + $defaultUser = $this->config->get('ssh_user', $this->profile(), null); + + if (is_string($user)) { + if (is_null($defaultUser)) { + $this->askToSetDefaultUser($user); + } + + return $user; + } + + if (!empty($defaultUser)) { + return $defaultUser; + } + + $user = $this->ask('Which user would you like to connect as'); + + if (is_null($defaultUser)) { + $this->askToSetDefaultUser($user); + } + + return $user; + } + + protected function askToSetDefaultUser(string $user): void + { + do { + $response = strtolower($this->ask("Do you want to set \"{$user}\" as your default SSH user? (y/n)", 'y')); + } while (!in_array($response, ['y', 'n'])); + + $value = $response === 'y' ? $user : ''; + $this->config->set('ssh_user', $value, $this->profile()); + } +} diff --git a/app/Commands/Sites/GetCommand.php b/app/Commands/Sites/GetCommand.php index f10e718..055a850 100644 --- a/app/Commands/Sites/GetCommand.php +++ b/app/Commands/Sites/GetCommand.php @@ -7,19 +7,24 @@ class GetCommand extends BaseCommand { - protected $signature = 'sites:get {site_id} {--format=} {--profile=}'; + protected $signature = 'sites:get + {site_id : The site to output} + {--format=} + {--profile=}'; protected $description = 'Get a site'; protected bool $largeOutput = true; - public function action() + public function action(): int { - $site = $this->spinupwp->sites->get($this->argument('site_id')); + $site = $this->spinupwp->sites->get((int) $this->argument('site_id')); if ($this->displayFormat() === 'json') { - return $site; + $this->toJson($site); + return self::SUCCESS; } + $additionalDomains = ''; if (!empty($site->additional_domains)) { @@ -59,7 +64,9 @@ public function action() $data['Created At'] = $site->created_at; $data['Status'] = ucfirst($site->status); - return $data; + $this->format($data); + + return self::SUCCESS; } public function backupsData(Site $site, array $data): array diff --git a/app/Commands/Sites/ListCommand.php b/app/Commands/Sites/ListCommand.php index c957dee..484a706 100644 --- a/app/Commands/Sites/ListCommand.php +++ b/app/Commands/Sites/ListCommand.php @@ -6,11 +6,14 @@ class ListCommand extends BaseCommand { - protected $signature = 'sites:list {server_id? : Only list sites belonging to this server} {--format=} {--profile=}'; + protected $signature = 'sites:list + {server_id? : Only list sites belonging to this server} + {--format=} + {--profile=}'; - protected $description = 'Retrieves a list of sites'; + protected $description = 'List all sites'; - protected function action() + protected function action(): int { $serverId = $this->argument('server_id'); @@ -22,21 +25,23 @@ protected function action() if ($sites->isEmpty()) { $this->warn('No sites found.'); - return $sites; + return self::SUCCESS; } - if ($this->displayFormat() === 'json') { - return $sites; + if ($this->displayFormat() === 'table') { + $sites->transform(fn ($site) => [ + 'ID' => $site->id, + 'Server ID' => $site->server_id, + 'Domain' => $site->domain, + 'Site User' => $site->site_user, + 'PHP' => $site->php_version, + 'Page Cache' => $site->page_cache['enabled'], + 'HTTPS' => $site->https['enabled'], + ]); } - return $sites->map(fn ($site) => [ - 'ID' => $site->id, - 'Server ID' => $site->server_id, - 'Domain' => $site->domain, - 'Site User' => $site->site_user, - 'PHP' => $site->php_version, - 'Page Cache' => $site->page_cache['enabled'], - 'HTTPS' => $site->https['enabled'], - ]); + $this->format($sites); + + return self::SUCCESS; } } diff --git a/app/Commands/Sites/SshCommand.php b/app/Commands/Sites/SshCommand.php new file mode 100644 index 0000000..2e653fa --- /dev/null +++ b/app/Commands/Sites/SshCommand.php @@ -0,0 +1,61 @@ +argument('site_id'); + + if (empty($siteId)) { + $siteId = $this->askToSelectSite('Which site would you like to start an SSH session for'); + } + + $site = $this->spinupwp->sites->get((int) $siteId); + $server = $this->spinupwp->servers->get($site->server_id); + + $this->line("Establishing a secure connection to [{$server->name}] as [{$site->site_user}]..."); + + $exitCode = $this->ssh( + $site->site_user, + $server->ip_address, + $server->ssh_port, + $this->cdCommand(), + ); + + if ($exitCode === 255) { + $this->error("Unable to connect to \"{$server->name}\". Have you added your SSH key to the \"{$site->site_user}\" user?"); + } + + return $exitCode; + } + + protected function cdCommand(): string + { + $cdCommand = ''; + $cdFlags = ['files', 'logs']; + + foreach ($cdFlags as $flag) { + if ($this->option($flag)) { + $cdCommand = "cd ./{$flag}; bash --login"; + break; + } + } + + return $cdCommand; + } +} diff --git a/app/Helpers/Configuration.php b/app/Helpers/Configuration.php index 621f812..0d034d1 100644 --- a/app/Helpers/Configuration.php +++ b/app/Helpers/Configuration.php @@ -3,6 +3,7 @@ namespace App\Helpers; use Illuminate\Support\Arr; +use Illuminate\Support\Facades\File; class Configuration { @@ -21,7 +22,7 @@ public function isConfigured(): bool return file_exists($this->configFilePath()); } - public function get(string $key, string $profile = 'default', string $default = ''): string + public function get(string $key, string $profile = 'default', ?string $default = ''): ?string { $this->config = $this->readConfig(); if (empty($this->config)) { @@ -74,10 +75,21 @@ protected function readConfig(): array public function configFilePath(): string { - if (!file_exists($this->path)) { - mkdir($this->path); + if (!File::isDirectory($this->path)) { + File::makeDirectory($this->path); } return $this->path . 'config.json'; } + + public function sshControlPath(): string + { + $sshPath = $this->path . 'ssh/'; + + if (!File::isDirectory($sshPath)) { + File::makeDirectory($sshPath); + } + + return $sshPath . '%h-%p-%r'; + } } diff --git a/config/app.php b/config/app.php index 4994197..a2bdd8b 100644 --- a/config/app.php +++ b/config/app.php @@ -62,4 +62,6 @@ 'table', ], + 'ssh_timeout' => 5, + ]; diff --git a/tests/Feature/Commands/ConfigureCommandTest.php b/tests/Feature/Commands/ConfigureCommandTest.php index 6f9103d..a6edfbd 100644 --- a/tests/Feature/Commands/ConfigureCommandTest.php +++ b/tests/Feature/Commands/ConfigureCommandTest.php @@ -13,7 +13,7 @@ $this->artisan('configure') ->expectsQuestion('SpinupWP API token', 'my-spinupwp-api-token') ->expectsQuestion('Default output format (json/table)', 'json') - ->expectsOutput('SpinupWP CLI configured successfully'); + ->expectsOutput('Profile configured successfully.'); expect((resolve(Configuration::class))->get('api_token'))->toEqual('my-spinupwp-api-token'); }); diff --git a/tests/Feature/Commands/ServersSshCommandTest.php b/tests/Feature/Commands/ServersSshCommandTest.php new file mode 100644 index 0000000..4424464 --- /dev/null +++ b/tests/Feature/Commands/ServersSshCommandTest.php @@ -0,0 +1,108 @@ +clientMock->shouldReceive('request')->once()->with('GET', 'servers/1', [])->andReturn( + new Response(200, [], json_encode([ + 'data' => [ + 'id' => 1, + 'name' => 'hellfishmedia', + 'ip_address' => '123.123.123.123', + 'ssh_port' => 22, + ], + ])) + ); + + $this->artisan('servers:ssh 1 johndoe') + ->expectsQuestion('Do you want to set "johndoe" as your default SSH user? (y/n)', 'y') + ->expectsOutput('Establishing a secure connection to [hellfishmedia] as [johndoe]...') + ->assertExitCode(255); +}); + +test('ssh command with server ID supplied and no SSH user', function () { + $this->clientMock->shouldReceive('request')->once()->with('GET', 'servers/1', [])->andReturn( + new Response(200, [], json_encode([ + 'data' => [ + 'id' => 1, + 'name' => 'hellfishmedia', + 'ip_address' => '123.123.123.123', + 'ssh_port' => 22, + ], + ])) + ); + + $this->artisan('servers:ssh 1') + ->expectsQuestion('Which user would you like to connect as', 'johndoe') + ->expectsQuestion('Do you want to set "johndoe" as your default SSH user? (y/n)', 'y') + ->expectsOutput('Establishing a secure connection to [hellfishmedia] as [johndoe]...') + ->assertExitCode(255); +}); + +test('ssh command with no server ID supplied', function () { + $this->clientMock->shouldReceive('request')->once()->with('GET', 'servers?page=1', [])->andReturn( + new Response(200, [], json_encode([ + 'data' => [ + [ + 'id' => 1, + 'name' => 'hellfishmedia', + 'ip_address' => '123.123.123.123', + 'ssh_port' => 22, + ], + ], + 'pagination' => [ + 'previous' => null, + 'next' => null, + 'count' => 1, + ], + ])) + ); + + $this->clientMock->shouldReceive('request')->once()->with('GET', 'servers/1', [])->andReturn( + new Response(200, [], json_encode([ + 'data' => [ + 'id' => 1, + 'name' => 'hellfishmedia', + 'ip_address' => '123.123.123.123', + 'ssh_port' => 22, + ], + ])) + ); + + $this->artisan('servers:ssh') + ->expectsQuestion('Which server would you like to start an SSH session for', '1') + ->expectsQuestion('Which user would you like to connect as', 'johndoe') + ->expectsQuestion('Do you want to set "johndoe" as your default SSH user? (y/n)', 'y') + ->expectsOutput('Establishing a secure connection to [hellfishmedia] as [johndoe]...') + ->assertExitCode(255); +}); + +test('ssh command with server ID supplied and default SSH user', function () { + setTestConfigFile([ + 'ssh_user' => 'janedoe', + ]); + + $this->clientMock->shouldReceive('request')->once()->with('GET', 'servers/1', [])->andReturn( + new Response(200, [], json_encode([ + 'data' => [ + 'id' => 1, + 'name' => 'hellfishmedia', + 'ip_address' => '123.123.123.123', + 'ssh_port' => 22, + ], + ])) + ); + + $this->artisan('servers:ssh 1') + ->expectsOutput('Establishing a secure connection to [hellfishmedia] as [janedoe]...') + ->assertExitCode(255); +}); diff --git a/tests/Feature/Commands/SitesSshCommandTest.php b/tests/Feature/Commands/SitesSshCommandTest.php new file mode 100644 index 0000000..0ed939b --- /dev/null +++ b/tests/Feature/Commands/SitesSshCommandTest.php @@ -0,0 +1,86 @@ +clientMock->shouldReceive('request')->once()->with('GET', 'sites/1', [])->andReturn( + new Response(200, [], json_encode([ + 'data' => [ + 'id' => 1, + 'server_id' => 1, + 'domain' => 'hellfishmedia.com', + 'site_user' => 'hellfish', + ], + ])) + ); + + $this->clientMock->shouldReceive('request')->once()->with('GET', 'servers/1', [])->andReturn( + new Response(200, [], json_encode([ + 'data' => [ + 'id' => 1, + 'name' => 'hellfishmedia', + 'ip_address' => '123.123.123.123', + 'ssh_port' => 22, + ], + ])) + ); + + $this->artisan('sites:ssh 1 --profile=johndoe') + ->expectsOutput('Establishing a secure connection to [hellfishmedia] as [hellfish]...') + ->assertExitCode(255); +}); + +test('ssh command with no site ID', function () { + $this->clientMock->shouldReceive('request')->once()->with('GET', 'sites?page=1', [])->andReturn( + new Response(200, [], json_encode([ + 'data' => [ + [ + 'id' => 1, + 'server_id' => 1, + 'domain' => 'hellfishmedia.com', + 'site_user' => 'hellfish', + ], + ], + 'pagination' => [ + 'previous' => null, + 'next' => null, + 'count' => 1, + ], + ])) + ); + + $this->clientMock->shouldReceive('request')->once()->with('GET', 'sites/1', [])->andReturn( + new Response(200, [], json_encode([ + 'data' => [ + 'id' => 1, + 'server_id' => 1, + 'domain' => 'hellfishmedia.com', + 'site_user' => 'hellfish', + ], + ])) + ); + + $this->clientMock->shouldReceive('request')->once()->with('GET', 'servers/1', [])->andReturn( + new Response(200, [], json_encode([ + 'data' => [ + 'id' => 1, + 'name' => 'hellfishmedia', + 'ip_address' => '123.123.123.123', + 'ssh_port' => 22, + ], + ])) + ); + + $this->artisan('sites:ssh --profile=johndoe') + ->expectsQuestion('Which site would you like to start an SSH session for', '1') + ->expectsOutput('Establishing a secure connection to [hellfishmedia] as [hellfish]...') + ->assertExitCode(255); +}); diff --git a/tests/Pest.php b/tests/Pest.php index e82e738..9af839a 100644 --- a/tests/Pest.php +++ b/tests/Pest.php @@ -22,6 +22,7 @@ ->beforeEach(function () { $this->clientMock = Mockery::mock(Client::class); $this->spinupwp = resolve(SpinupWp::class)->setClient($this->clientMock)->setApiKey('123'); + config()->set('app.ssh_timeout', -1); }) ->in('Feature', 'Unit'); @@ -51,14 +52,14 @@ | */ -function setTestConfigFile() +function setTestConfigFile($profileData = []) { $config = resolve(Configuration::class); file_put_contents($config->configFilePath(), json_encode([ - 'default' => [ + 'default' => array_merge([ 'api_token' => 'myapikey123', 'format' => 'json', - ], + ], $profileData), ], JSON_PRETTY_PRINT)); }