diff --git a/app/Actions/Database/LinkUser.php b/app/Actions/Database/LinkUser.php index 166afc2..43026e8 100755 --- a/app/Actions/Database/LinkUser.php +++ b/app/Actions/Database/LinkUser.php @@ -27,8 +27,7 @@ public function link(DatabaseUser $databaseUser, array $input): void ->whereIn('name', $input['databases']) ->count(); if (count($input['databases']) !== $dbs) { - throw ValidationException::withMessages(['databases' => __('Databases not found!')]) - ->errorBag('linkUser'); + throw ValidationException::withMessages(['databases' => __('Databases not found!')]); } $databaseUser->databases = $input['databases']; diff --git a/app/Actions/Database/RunBackup.php b/app/Actions/Database/RunBackup.php index 8f37bf2..7ea949d 100644 --- a/app/Actions/Database/RunBackup.php +++ b/app/Actions/Database/RunBackup.php @@ -9,7 +9,7 @@ class RunBackup { - public function run(Backup $backup): void + public function run(Backup $backup): BackupFile { $file = new BackupFile([ 'backup_id' => $backup->id, @@ -26,5 +26,7 @@ public function run(Backup $backup): void $file->status = BackupFileStatus::FAILED; $file->save(); })->onConnection('ssh'); + + return $file; } } diff --git a/app/Actions/Queue/ManageQueue.php b/app/Actions/Queue/ManageQueue.php index b73bf5f..4a89fb3 100644 --- a/app/Actions/Queue/ManageQueue.php +++ b/app/Actions/Queue/ManageQueue.php @@ -3,7 +3,6 @@ namespace App\Actions\Queue; use App\Enums\QueueStatus; -use App\Enums\ServiceStatus; use App\Models\Queue; class ManageQueue @@ -14,10 +13,7 @@ public function start(Queue $queue): void $queue->save(); dispatch(function () use ($queue) { $queue->server->processManager()->handler()->start($queue->id, $queue->site_id); - $queue->status = ServiceStatus::READY; - $queue->save(); - })->catch(function () use ($queue) { - $queue->status = ServiceStatus::FAILED; + $queue->status = QueueStatus::RUNNING; $queue->save(); })->onConnection('ssh'); } @@ -28,10 +24,7 @@ public function stop(Queue $queue): void $queue->save(); dispatch(function () use ($queue) { $queue->server->processManager()->handler()->stop($queue->id, $queue->site_id); - $queue->status = ServiceStatus::STOPPED; - $queue->save(); - })->catch(function () use ($queue) { - $queue->status = ServiceStatus::FAILED; + $queue->status = QueueStatus::STOPPED; $queue->save(); })->onConnection('ssh'); } @@ -42,10 +35,7 @@ public function restart(Queue $queue): void $queue->save(); dispatch(function () use ($queue) { $queue->server->processManager()->handler()->restart($queue->id, $queue->site_id); - $queue->status = ServiceStatus::READY; - $queue->save(); - })->catch(function () use ($queue) { - $queue->status = ServiceStatus::FAILED; + $queue->status = QueueStatus::RUNNING; $queue->save(); })->onConnection('ssh'); } diff --git a/app/Actions/Script/CreateScript.php b/app/Actions/Script/CreateScript.php deleted file mode 100644 index a469a6c..0000000 --- a/app/Actions/Script/CreateScript.php +++ /dev/null @@ -1,41 +0,0 @@ -validateInputs($input); - - $script = new Script([ - 'user_id' => $creator->id, - 'name' => $input['name'], - 'content' => $input['content'], - ]); - $script->save(); - - return $script; - } - - /** - * @throws ValidationException - */ - private function validateInputs(array $input): void - { - $rules = [ - 'name' => 'required', - 'content' => 'required', - ]; - - Validator::make($input, $rules)->validateWithBag('createScript'); - } -} diff --git a/app/Actions/Script/GetScripts.php b/app/Actions/Script/GetScripts.php deleted file mode 100755 index 5aeac76..0000000 --- a/app/Actions/Script/GetScripts.php +++ /dev/null @@ -1,17 +0,0 @@ -scripts() - ->orderBy('id', 'desc') - ->paginate(6) - ->onEachSide(1); - } -} diff --git a/app/Actions/Script/UpdateScript.php b/app/Actions/Script/UpdateScript.php deleted file mode 100644 index 267ca35..0000000 --- a/app/Actions/Script/UpdateScript.php +++ /dev/null @@ -1,37 +0,0 @@ -validateInputs($input); - - $script->name = $input['name']; - $script->content = $input['content']; - $script->save(); - - return $script; - } - - /** - * @throws ValidationException - */ - private function validateInputs(array $input): void - { - $rules = [ - 'name' => 'required', - 'content' => 'required', - ]; - - Validator::make($input, $rules)->validateWithBag('updateScript'); - } -} diff --git a/app/Actions/Server/GetServers.php b/app/Actions/Server/GetServers.php deleted file mode 100755 index d7d33a3..0000000 --- a/app/Actions/Server/GetServers.php +++ /dev/null @@ -1,14 +0,0 @@ -latest()->get(); - } -} diff --git a/app/Actions/ServerProvider/CreateServerProvider.php b/app/Actions/ServerProvider/CreateServerProvider.php index 06bb7f3..bdcb453 100644 --- a/app/Actions/ServerProvider/CreateServerProvider.php +++ b/app/Actions/ServerProvider/CreateServerProvider.php @@ -28,7 +28,7 @@ public function create(User $user, array $input): ServerProvider } catch (Exception) { throw ValidationException::withMessages([ 'provider' => [ - __("Couldn't connect to provider. Please check your credentials and try again later."), + sprintf("Couldn't connect to %s. Please check your credentials.", $input['provider']), ], ]); } diff --git a/app/Actions/ServerProvider/DeleteServerProvider.php b/app/Actions/ServerProvider/DeleteServerProvider.php new file mode 100644 index 0000000..873ddb0 --- /dev/null +++ b/app/Actions/ServerProvider/DeleteServerProvider.php @@ -0,0 +1,21 @@ +servers()->exists()) { + throw new Exception('This server provider is being used by a server.'); + } + + $serverProvider->delete(); + } +} diff --git a/app/Actions/Service/Manage.php b/app/Actions/Service/Manage.php index 761c536..a7a9a90 100644 --- a/app/Actions/Service/Manage.php +++ b/app/Actions/Service/Manage.php @@ -19,9 +19,6 @@ public function start(Service $service): void $service->status = ServiceStatus::FAILED; } $service->save(); - })->catch(function () use ($service) { - $service->status = ServiceStatus::FAILED; - $service->save(); })->onConnection('ssh'); } @@ -37,9 +34,6 @@ public function stop(Service $service): void $service->status = ServiceStatus::FAILED; } $service->save(); - })->catch(function () use ($service) { - $service->status = ServiceStatus::FAILED; - $service->save(); })->onConnection('ssh'); } @@ -55,9 +49,6 @@ public function restart(Service $service): void $service->status = ServiceStatus::FAILED; } $service->save(); - })->catch(function () use ($service) { - $service->status = ServiceStatus::FAILED; - $service->save(); })->onConnection('ssh'); } } diff --git a/app/Actions/SourceControl/DeleteSourceControl.php b/app/Actions/SourceControl/DeleteSourceControl.php new file mode 100644 index 0000000..e084056 --- /dev/null +++ b/app/Actions/SourceControl/DeleteSourceControl.php @@ -0,0 +1,17 @@ +sites()->exists()) { + throw new \Exception('This source control is being used by a site.'); + } + + $sourceControl->delete(); + } +} diff --git a/app/Actions/StorageProvider/DeleteStorageProvider.php b/app/Actions/StorageProvider/DeleteStorageProvider.php new file mode 100644 index 0000000..d8cd5cf --- /dev/null +++ b/app/Actions/StorageProvider/DeleteStorageProvider.php @@ -0,0 +1,21 @@ +backups()->exists()) { + throw new Exception('This storage provider is being used by a backup.'); + } + + $storageProvider->delete(); + } +} diff --git a/app/Console/Commands/RunBackupCommand.php b/app/Console/Commands/RunBackupCommand.php index d349a7b..c6d3dfe 100644 --- a/app/Console/Commands/RunBackupCommand.php +++ b/app/Console/Commands/RunBackupCommand.php @@ -15,14 +15,19 @@ class RunBackupCommand extends Command public function handle(): void { + $total = 0; + Backup::query() ->where('interval', $this->argument('interval')) ->where('status', BackupStatus::RUNNING) - ->chunk(100, function ($backups) { + ->chunk(100, function ($backups) use (&$total) { /** @var Backup $backup */ foreach ($backups as $backup) { app(RunBackup::class)->run($backup); + $total++; } }); + + $this->info("{$total} backups started"); } } diff --git a/app/Exceptions/SourceControlIsNotConnected.php b/app/Exceptions/SourceControlIsNotConnected.php index e93deaf..7b0f1aa 100755 --- a/app/Exceptions/SourceControlIsNotConnected.php +++ b/app/Exceptions/SourceControlIsNotConnected.php @@ -2,13 +2,8 @@ namespace App\Exceptions; -use App\Models\SourceControl; use Exception; class SourceControlIsNotConnected extends Exception { - public function __construct(protected SourceControl|string|null $sourceControl, ?string $message = null) - { - parent::__construct($message ?? 'Source control is not connected'); - } } diff --git a/app/Helpers/SSH.php b/app/Helpers/SSH.php index 86e6f5e..d2d91b4 100755 --- a/app/Helpers/SSH.php +++ b/app/Helpers/SSH.php @@ -88,7 +88,7 @@ public function connect(bool $sftp = false): void Log::error('Error connecting', [ 'msg' => $e->getMessage(), ]); - throw $e; + throw new SSHConnectionError($e->getMessage()); } } diff --git a/app/Http/Controllers/Settings/ProjectController.php b/app/Http/Controllers/Settings/ProjectController.php index 085e1f1..3d1f5c2 100644 --- a/app/Http/Controllers/Settings/ProjectController.php +++ b/app/Http/Controllers/Settings/ProjectController.php @@ -47,11 +47,14 @@ public function update(Request $request, Project $project): HtmxResponse public function delete(Project $project): RedirectResponse { + /** @var User $user */ + $user = auth()->user(); + /** @var Project $project */ - $project = auth()->user()->projects()->findOrFail($project->id); + $project = $user->projects()->findOrFail($project->id); try { - app(DeleteProject::class)->delete(auth()->user(), $project); + app(DeleteProject::class)->delete($user, $project); } catch (ValidationException $e) { Toast::error($e->getMessage()); diff --git a/app/Http/Controllers/Settings/ServerProviderController.php b/app/Http/Controllers/Settings/ServerProviderController.php index 9d7b76e..bae2092 100644 --- a/app/Http/Controllers/Settings/ServerProviderController.php +++ b/app/Http/Controllers/Settings/ServerProviderController.php @@ -3,6 +3,7 @@ namespace App\Http\Controllers\Settings; use App\Actions\ServerProvider\CreateServerProvider; +use App\Actions\ServerProvider\DeleteServerProvider; use App\Facades\Toast; use App\Helpers\HtmxResponse; use App\Http\Controllers\Controller; @@ -32,14 +33,15 @@ public function connect(Request $request): HtmxResponse return htmx()->redirect(route('server-providers')); } - /** - * @TODO Update servers using this provider - */ - public function delete(int $id): RedirectResponse + public function delete(ServerProvider $serverProvider): RedirectResponse { - $serverProvider = ServerProvider::query()->findOrFail($id); + try { + app(DeleteServerProvider::class)->delete($serverProvider); + } catch (\Exception $e) { + Toast::error($e->getMessage()); - $serverProvider->delete(); + return back(); + } Toast::success('Server provider deleted.'); diff --git a/app/Http/Controllers/Settings/SourceControlController.php b/app/Http/Controllers/Settings/SourceControlController.php index c17ae98..f3bb097 100644 --- a/app/Http/Controllers/Settings/SourceControlController.php +++ b/app/Http/Controllers/Settings/SourceControlController.php @@ -3,6 +3,7 @@ namespace App\Http\Controllers\Settings; use App\Actions\SourceControl\ConnectSourceControl; +use App\Actions\SourceControl\DeleteSourceControl; use App\Facades\Toast; use App\Helpers\HtmxResponse; use App\Http\Controllers\Controller; @@ -11,9 +12,6 @@ use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; -/** - * @TODO Assign user to source control - */ class SourceControlController extends Controller { public function index(): View @@ -34,11 +32,15 @@ public function connect(Request $request): HtmxResponse return htmx()->redirect(route('source-controls')); } - public function delete(int $id): RedirectResponse + public function delete(SourceControl $sourceControl): RedirectResponse { - $sourceControl = SourceControl::query()->findOrFail($id); + try { + app(DeleteSourceControl::class)->delete($sourceControl); + } catch (\Exception $e) { + Toast::error($e->getMessage()); - $sourceControl->delete(); + return back(); + } Toast::success('Source control deleted.'); diff --git a/app/Http/Controllers/Settings/StorageProviderController.php b/app/Http/Controllers/Settings/StorageProviderController.php index bc57676..939c56b 100644 --- a/app/Http/Controllers/Settings/StorageProviderController.php +++ b/app/Http/Controllers/Settings/StorageProviderController.php @@ -3,6 +3,7 @@ namespace App\Http\Controllers\Settings; use App\Actions\StorageProvider\CreateStorageProvider; +use App\Actions\StorageProvider\DeleteStorageProvider; use App\Facades\Toast; use App\Helpers\HtmxResponse; use App\Http\Controllers\Controller; @@ -32,14 +33,15 @@ public function connect(Request $request): HtmxResponse return htmx()->redirect(route('storage-providers')); } - /** - * @TODO Update servers using this provider - */ - public function delete(int $id): RedirectResponse + public function delete(StorageProvider $storageProvider): RedirectResponse { - $storageProvider = StorageProvider::query()->findOrFail($id); + try { + app(DeleteStorageProvider::class)->delete($storageProvider); + } catch (\Exception $e) { + Toast::error($e->getMessage()); - $storageProvider->delete(); + return back(); + } Toast::success('Storage provider deleted.'); diff --git a/app/Http/Controllers/SiteSettingController.php b/app/Http/Controllers/SiteSettingController.php index 6382af3..b2b940f 100644 --- a/app/Http/Controllers/SiteSettingController.php +++ b/app/Http/Controllers/SiteSettingController.php @@ -3,6 +3,7 @@ namespace App\Http\Controllers; use App\Facades\Toast; +use App\Helpers\HtmxResponse; use App\Models\Server; use App\Models\Site; use Illuminate\Contracts\View\View; @@ -43,7 +44,7 @@ public function updateVhost(Server $server, Site $site, Request $request): Redir return back(); } - public function updatePHPVersion(Server $server, Site $site, Request $request): RedirectResponse + public function updatePHPVersion(Server $server, Site $site, Request $request): HtmxResponse { $this->validate($request, [ 'version' => [ @@ -60,6 +61,6 @@ public function updatePHPVersion(Server $server, Site $site, Request $request): Toast::error($e->getMessage()); } - return back(); + return htmx()->back(); } } diff --git a/app/Models/Redirect.php b/app/Models/Redirect.php deleted file mode 100644 index 6221029..0000000 --- a/app/Models/Redirect.php +++ /dev/null @@ -1,36 +0,0 @@ - 'integer', - 'mode' => 'integer', - ]; - - public function site(): BelongsTo - { - return $this->belongsTo(Site::class); - } -} diff --git a/app/Models/Script.php b/app/Models/Script.php deleted file mode 100644 index 8b085ae..0000000 --- a/app/Models/Script.php +++ /dev/null @@ -1,38 +0,0 @@ - 'integer', - ]; - - public function creator(): BelongsTo - { - return $this->belongsTo(User::class); - } - - public function executions(): HasMany - { - return $this->hasMany(ScriptExecution::class, 'script_id'); - } -} diff --git a/app/Models/ScriptExecution.php b/app/Models/ScriptExecution.php deleted file mode 100644 index f0aa0cd..0000000 --- a/app/Models/ScriptExecution.php +++ /dev/null @@ -1,42 +0,0 @@ - 'integer', - 'server_id' => 'integer', - ]; - - public function script(): BelongsTo - { - return $this->belongsTo(Script::class); - } - - public function server(): BelongsTo - { - return $this->belongsTo(Server::class); - } -} diff --git a/app/Models/Server.php b/app/Models/Server.php index e6dfea4..7e63f01 100755 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -51,7 +51,6 @@ * @property FirewallRule[] $firewallRules * @property CronJob[] $cronJobs * @property Queue[] $queues - * @property ScriptExecution[] $scriptExecutions * @property Backup[] $backups * @property Queue[] $daemons * @property SshKey[] $sshKeys @@ -121,7 +120,6 @@ public static function boot(): void $server->cronJobs()->delete(); $server->queues()->delete(); $server->daemons()->delete(); - $server->scriptExecutions()->delete(); $server->sshKeys()->detach(); if (File::exists($server->sshKey()['public_key_path'])) { File::delete($server->sshKey()['public_key_path']); @@ -187,11 +185,6 @@ public function queues(): HasMany return $this->hasMany(Queue::class); } - public function scriptExecutions(): HasMany - { - return $this->hasMany(ScriptExecution::class); - } - public function backups(): HasMany { return $this->hasMany(Backup::class); diff --git a/app/Models/ServerLog.php b/app/Models/ServerLog.php index 79c8895..2efee2c 100755 --- a/app/Models/ServerLog.php +++ b/app/Models/ServerLog.php @@ -4,6 +4,7 @@ use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Relations\BelongsTo; +use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Storage; use Illuminate\Support\Str; @@ -38,8 +39,12 @@ public static function boot(): void parent::boot(); static::deleting(function (ServerLog $log) { - if (Storage::disk($log->disk)->exists($log->name)) { - Storage::disk($log->disk)->delete($log->name); + try { + if (Storage::disk($log->disk)->exists($log->name)) { + Storage::disk($log->disk)->delete($log->name); + } + } catch (\Exception $e) { + Log::error($e->getMessage(), ['exception' => $e]); } }); } diff --git a/app/Models/ServerProvider.php b/app/Models/ServerProvider.php index 366663c..130c39d 100644 --- a/app/Models/ServerProvider.php +++ b/app/Models/ServerProvider.php @@ -4,6 +4,7 @@ use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Relations\BelongsTo; +use Illuminate\Database\Eloquent\Relations\HasMany; /** * @property int $user_id @@ -40,4 +41,9 @@ public function getCredentials(): array { return $this->credentials; } + + public function servers(): HasMany + { + return $this->hasMany(Server::class, 'provider_id'); + } } diff --git a/app/Models/Site.php b/app/Models/Site.php index b1355de..66bd533 100755 --- a/app/Models/Site.php +++ b/app/Models/Site.php @@ -33,7 +33,6 @@ * @property Deployment[] $deployments * @property ?GitHook $gitHook * @property DeploymentScript $deploymentScript - * @property Redirect[] $redirects * @property Queue[] $queues * @property Ssl[] $ssls * @property ?Ssl $activeSsl @@ -76,7 +75,6 @@ public static function boot(): void parent::boot(); static::deleting(function (Site $site) { - $site->redirects()->delete(); $site->queues()->delete(); $site->ssls()->delete(); $site->deployments()->delete(); @@ -116,11 +114,6 @@ public function deploymentScript(): HasOne return $this->hasOne(DeploymentScript::class); } - public function redirects(): HasMany - { - return $this->hasMany(Redirect::class); - } - public function queues(): HasMany { return $this->hasMany(Queue::class); @@ -192,8 +185,8 @@ public function php(): ?Service public function changePHPVersion($version): void { - $this->php_version = $version; $this->server->webserver()->handler()->changePHPVersion($this, $version); + $this->php_version = $version; $this->save(); } diff --git a/app/Models/SourceControl.php b/app/Models/SourceControl.php index 00f16d2..0abf2d1 100755 --- a/app/Models/SourceControl.php +++ b/app/Models/SourceControl.php @@ -4,6 +4,7 @@ use App\SourceControlProviders\SourceControlProvider; use Illuminate\Database\Eloquent\Factories\HasFactory; +use Illuminate\Database\Eloquent\Relations\HasMany; /** * @property string $provider @@ -37,4 +38,9 @@ public function getRepo(?string $repo = null): ?array { return $this->provider()->getRepo($repo); } + + public function sites(): HasMany + { + return $this->hasMany(Site::class); + } } diff --git a/app/Models/StorageProvider.php b/app/Models/StorageProvider.php index bd69d9e..7fe9b4c 100644 --- a/app/Models/StorageProvider.php +++ b/app/Models/StorageProvider.php @@ -4,6 +4,7 @@ use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Relations\BelongsTo; +use Illuminate\Database\Eloquent\Relations\HasMany; /** * @property int $user_id @@ -39,4 +40,9 @@ public function provider(): \App\StorageProviders\StorageProvider return new $providerClass($this); } + + public function backups(): HasMany + { + return $this->hasMany(Backup::class, 'storage_id'); + } } diff --git a/app/Models/User.php b/app/Models/User.php index 5d18ddb..219e4b7 100755 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -86,11 +86,6 @@ public function serverProviders(): HasMany return $this->hasMany(ServerProvider::class); } - public function scripts(): HasMany - { - return $this->hasMany(Script::class, 'user_id'); - } - public function sourceControl(string $provider): HasOne { return $this->hasOne(SourceControl::class)->where('provider', $provider); diff --git a/app/NotificationChannels/Telegram.php b/app/NotificationChannels/Telegram.php index b692323..017399b 100644 --- a/app/NotificationChannels/Telegram.php +++ b/app/NotificationChannels/Telegram.php @@ -60,6 +60,6 @@ private function sendToTelegram(string $text): void 'text' => $text, 'parse_mode' => 'markdown', 'disable_web_page_preview' => true, - ]); + ])->throw(); } } diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index d4e8133..321ca5d 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -5,7 +5,6 @@ use App\Helpers\Notifier; use App\Helpers\SSH; use App\Helpers\Toast; -use App\Support\SocialiteProviders\DropboxProvider; use Illuminate\Contracts\Container\BindingResolutionException; use Illuminate\Http\Resources\Json\ResourceCollection; use Illuminate\Support\Facades\URL; @@ -39,26 +38,8 @@ public function boot(): void return new Toast; }); - $this->extendSocialite(); - if (str(config('app.url'))->startsWith('https://')) { URL::forceScheme('https'); } } - - /** - * @throws BindingResolutionException - */ - private function extendSocialite(): void - { - $socialite = $this->app->make('Laravel\Socialite\Contracts\Factory'); - $socialite->extend( - 'dropbox', - function ($app) use ($socialite) { - $config = $app['config']['services.dropbox']; - - return $socialite->buildProvider(DropboxProvider::class, $config); - } - ); - } } diff --git a/app/Support/SocialiteProviders/DropboxProvider.php b/app/Support/SocialiteProviders/DropboxProvider.php deleted file mode 100644 index a0253d9..0000000 --- a/app/Support/SocialiteProviders/DropboxProvider.php +++ /dev/null @@ -1,78 +0,0 @@ -buildAuthUrlFromBase('https://www.dropbox.com/oauth2/authorize', $state); - } - - /** - * {@inheritdoc} - */ - protected function getTokenUrl(): string - { - return 'https://api.dropboxapi.com/oauth2/token'; - } - - /** - * {@inheritdoc} - */ - protected function getTokenFields($code): array - { - return array_merge(parent::getTokenFields($code), [ - 'grant_type' => 'authorization_code', - ]); - } - - /** - * {@inheritdoc} - * - * @throws GuzzleException - */ - protected function getUserByToken($token) - { - $response = $this->getHttpClient()->post('https://api.dropboxapi.com/2/users/get_current_account', [ - 'headers' => [ - 'Authorization' => 'Bearer '.$token, - ], - ]); - - return json_decode($response->getBody(), true); - } - - /** - * {@inheritdoc} - */ - protected function mapUserToObject(array $user): User - { - return (new User)->setRaw($user)->map([ - 'id' => $user['account_id'], - 'nickname' => null, - 'name' => $user['name']['display_name'], - 'email' => $user['email'], - 'avatar' => Arr::get($user, 'profile_photo_url'), - ]); - } -} diff --git a/app/Support/Testing/SSHFake.php b/app/Support/Testing/SSHFake.php index a2213f7..def89d1 100644 --- a/app/Support/Testing/SSHFake.php +++ b/app/Support/Testing/SSHFake.php @@ -2,6 +2,7 @@ namespace App\Support\Testing; +use App\Exceptions\SSHConnectionError; use App\Helpers\SSH; use Illuminate\Support\Traits\ReflectsClosures; use PHPUnit\Framework\Assert; @@ -14,11 +15,25 @@ class SSHFake extends SSH protected ?string $output; + protected bool $connectionWillFail = false; + public function __construct(?string $output = null) { $this->output = $output; } + public function connectionWillFail(): void + { + $this->connectionWillFail = true; + } + + public function connect(bool $sftp = false): void + { + if ($this->connectionWillFail) { + throw new SSHConnectionError('Connection failed'); + } + } + public function exec(string|array $commands, string $log = '', ?int $siteId = null): string { if ($log) { @@ -45,6 +60,11 @@ public function exec(string|array $commands, string $log = '', ?int $siteId = nu return $output; } + public function upload(string $local, string $remote): void + { + $this->log = null; + } + public function assertExecuted(array|string $commands): void { if (! $this->commands) { diff --git a/composer.json b/composer.json index a7a334e..0d9ae80 100644 --- a/composer.json +++ b/composer.json @@ -17,7 +17,6 @@ "laravel/fortify": "^1.17", "laravel/framework": "^10.0", "laravel/sanctum": "^3.2", - "laravel/socialite": "^5.2", "laravel/tinker": "^2.8", "opcodesio/log-viewer": "^3.0", "phpseclib/phpseclib": "~3.0" diff --git a/composer.lock b/composer.lock index ff190b1..22920d5 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "56d10a007fc0e676b85e8559984d1867", + "content-hash": "3e51f672a4139d2a840cdcd2e48ccaaf", "packages": [ { "name": "aws/aws-crt-php", @@ -2200,76 +2200,6 @@ }, "time": "2023-11-08T14:08:06+00:00" }, - { - "name": "laravel/socialite", - "version": "v5.12.1", - "source": { - "type": "git", - "url": "https://github.com/laravel/socialite.git", - "reference": "7dae1b072573809f32ab6dcf4aebb57c8b3e8acf" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/laravel/socialite/zipball/7dae1b072573809f32ab6dcf4aebb57c8b3e8acf", - "reference": "7dae1b072573809f32ab6dcf4aebb57c8b3e8acf", - "shasum": "" - }, - "require": { - "ext-json": "*", - "guzzlehttp/guzzle": "^6.0|^7.0", - "illuminate/contracts": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0", - "illuminate/http": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0", - "illuminate/support": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0", - "league/oauth1-client": "^1.10.1", - "php": "^7.2|^8.0" - }, - "require-dev": { - "mockery/mockery": "^1.0", - "orchestra/testbench": "^4.0|^5.0|^6.0|^7.0|^8.0|^9.0", - "phpstan/phpstan": "^1.10", - "phpunit/phpunit": "^8.0|^9.3|^10.4" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "5.x-dev" - }, - "laravel": { - "providers": [ - "Laravel\\Socialite\\SocialiteServiceProvider" - ], - "aliases": { - "Socialite": "Laravel\\Socialite\\Facades\\Socialite" - } - } - }, - "autoload": { - "psr-4": { - "Laravel\\Socialite\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Taylor Otwell", - "email": "taylor@laravel.com" - } - ], - "description": "Laravel wrapper around OAuth 1 & OAuth 2 libraries.", - "homepage": "https://laravel.com", - "keywords": [ - "laravel", - "oauth" - ], - "support": { - "issues": "https://github.com/laravel/socialite/issues", - "source": "https://github.com/laravel/socialite" - }, - "time": "2024-02-16T08:58:20+00:00" - }, { "name": "laravel/tinker", "version": "v2.9.0", @@ -2730,82 +2660,6 @@ ], "time": "2024-01-28T23:22:08+00:00" }, - { - "name": "league/oauth1-client", - "version": "v1.10.1", - "source": { - "type": "git", - "url": "https://github.com/thephpleague/oauth1-client.git", - "reference": "d6365b901b5c287dd41f143033315e2f777e1167" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/thephpleague/oauth1-client/zipball/d6365b901b5c287dd41f143033315e2f777e1167", - "reference": "d6365b901b5c287dd41f143033315e2f777e1167", - "shasum": "" - }, - "require": { - "ext-json": "*", - "ext-openssl": "*", - "guzzlehttp/guzzle": "^6.0|^7.0", - "guzzlehttp/psr7": "^1.7|^2.0", - "php": ">=7.1||>=8.0" - }, - "require-dev": { - "ext-simplexml": "*", - "friendsofphp/php-cs-fixer": "^2.17", - "mockery/mockery": "^1.3.3", - "phpstan/phpstan": "^0.12.42", - "phpunit/phpunit": "^7.5||9.5" - }, - "suggest": { - "ext-simplexml": "For decoding XML-based responses." - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0-dev", - "dev-develop": "2.0-dev" - } - }, - "autoload": { - "psr-4": { - "League\\OAuth1\\Client\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Ben Corlett", - "email": "bencorlett@me.com", - "homepage": "http://www.webcomm.com.au", - "role": "Developer" - } - ], - "description": "OAuth 1.0 Client Library", - "keywords": [ - "Authentication", - "SSO", - "authorization", - "bitbucket", - "identity", - "idp", - "oauth", - "oauth1", - "single sign on", - "trello", - "tumblr", - "twitter" - ], - "support": { - "issues": "https://github.com/thephpleague/oauth1-client/issues", - "source": "https://github.com/thephpleague/oauth1-client/tree/v1.10.1" - }, - "time": "2022-04-15T14:02:14+00:00" - }, { "name": "monolog/monolog", "version": "3.5.0", @@ -9529,5 +9383,5 @@ "ext-ftp": "*" }, "platform-dev": [], - "plugin-api-version": "2.6.0" + "plugin-api-version": "2.2.0" } diff --git a/database/factories/RedirectFactory.php b/database/factories/RedirectFactory.php deleted file mode 100644 index b2ab1cf..0000000 --- a/database/factories/RedirectFactory.php +++ /dev/null @@ -1,25 +0,0 @@ - $this->faker->randomNumber(), - 'mode' => $this->faker->randomNumber(), - 'from' => $this->faker->word(), - 'to' => $this->faker->word(), - 'status' => $this->faker->word(), - 'created_at' => Carbon::now(), - 'updated_at' => Carbon::now(), - ]; - } -} diff --git a/database/factories/ScriptExecutionFactory.php b/database/factories/ScriptExecutionFactory.php deleted file mode 100644 index 7140f1a..0000000 --- a/database/factories/ScriptExecutionFactory.php +++ /dev/null @@ -1,26 +0,0 @@ - $this->faker->word(), - 'finished_at' => Carbon::now(), - 'created_at' => Carbon::now(), - 'updated_at' => Carbon::now(), - 'script_id' => Script::factory(), - 'server_id' => Server::factory(), - ]; - } -} diff --git a/database/factories/ScriptFactory.php b/database/factories/ScriptFactory.php deleted file mode 100644 index 983669a..0000000 --- a/database/factories/ScriptFactory.php +++ /dev/null @@ -1,15 +0,0 @@ -|<=|=>|=|!=" }, { token: "paren.lparen", regex: "[\\[\\(\\{]" }, { token: "paren.rparen", regex: "[\\]\\)\\}]" }, { token: "text", regex: "\\s+" }] } }; r.inherits(u, i), t.ShHighlightRules = u }) diff --git a/public/static/libs/ace/theme-github.js b/public/static/libs/ace/theme-github.js new file mode 100644 index 0000000..e5dd93d --- /dev/null +++ b/public/static/libs/ace/theme-github.js @@ -0,0 +1,5 @@ +ace.define("ace/theme/github", ["require", "exports", "module", "ace/lib/dom"], function (e, t, n) { + t.isDark = !1, t.cssClass = "ace-github", t.cssText = '/* CSS style content from github\'s default pygments highlighter template.Cursor and selection styles from textmate.css. */.ace-github .ace_gutter {background: #e8e8e8;color: #AAA;}.ace-github .ace_scroller {background: #fff;}.ace-github .ace_keyword {font-weight: bold;}.ace-github .ace_string {color: #D14;}.ace-github .ace_variable.ace_class {color: teal;}.ace-github .ace_constant.ace_numeric {color: #099;}.ace-github .ace_constant.ace_buildin {color: #0086B3;}.ace-github .ace_support.ace_function {color: #0086B3;}.ace-github .ace_comment {color: #998;font-style: italic;}.ace-github .ace_variable.ace_language {color: #0086B3;}.ace-github .ace_paren {font-weight: bold;}.ace-github .ace_boolean {font-weight: bold;}.ace-github .ace_string.ace_regexp {color: #009926;font-weight: normal;}.ace-github .ace_variable.ace_instance {color: teal;}.ace-github .ace_constant.ace_language {font-weight: bold;}.ace-github .ace_text-layer {}.ace-github .ace_cursor {border-left: 2px solid black;}.ace-github .ace_overwrite-cursors .ace_cursor {border-left: 0px;border-bottom: 1px solid black;}.ace-github .ace_marker-layer .ace_active-line {background: rgb(255, 255, 204);}.ace-github .ace_marker-layer .ace_selection {background: rgb(181, 213, 255);}.ace-github.ace_multiselect .ace_selection.ace_start {box-shadow: 0 0 3px 0px white;border-radius: 2px;}/* bold keywords cause cursor issues for some fonts *//* this disables bold style for editor and keeps for static highlighter */.ace-github.ace_nobold .ace_line > span {font-weight: normal !important;}.ace-github .ace_marker-layer .ace_step {background: rgb(252, 255, 0);}.ace-github .ace_marker-layer .ace_stack {background: rgb(164, 229, 101);}.ace-github .ace_marker-layer .ace_bracket {margin: -1px 0 0 -1px;border: 1px solid rgb(192, 192, 192);}.ace-github .ace_gutter-active-line {background-color : rgba(0, 0, 0, 0.07);}.ace-github .ace_marker-layer .ace_selected-word {background: rgb(250, 250, 255);border: 1px solid rgb(200, 200, 250);}.ace-github .ace_print-margin {width: 1px;background: #e8e8e8;}.ace-github .ace_indent-guide {background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAACCAYAAACZgbYnAAAAE0lEQVQImWP4////f4bLly//BwAmVgd1/w11/gAAAABJRU5ErkJggg==") right repeat-y;}'; + var r = e("../lib/dom"); + r.importCssString(t.cssText, t.cssClass) +}) diff --git a/public/static/libs/ace/theme-one-dark.js b/public/static/libs/ace/theme-one-dark.js new file mode 100644 index 0000000..eb2ec1d --- /dev/null +++ b/public/static/libs/ace/theme-one-dark.js @@ -0,0 +1,5 @@ +ace.define("ace/theme/one-dark", ["require", "exports", "module", "ace/lib/dom"], function (e, t, n) { + t.isDark = !1, t.cssClass = "ace-one-dark", t.cssText = '/* CSS style content from one-dark\'s default pygments highlighter template.Cursor and selection styles from textmate.css. */.ace-one-dark .ace_gutter{background:#282c34;color:#6a6f7a}.ace-one-dark .ace_print-margin{width:1px;background:#e8e8e8}.ace-one-dark{background-color:#282c34;color:#abb2bf}.ace-one-dark .ace_cursor{color:#528bff}.ace-one-dark .ace_marker-layer .ace_selection{background:#3d4350}.ace-one-dark.ace_multiselect .ace_selection.ace_start{box-shadow:0 0 3px 0 #282c34;border-radius:2px}.ace-one-dark .ace_marker-layer .ace_step{background:#c6dbae}.ace-one-dark .ace_marker-layer .ace_bracket{margin:-1px 0 0 -1px;border:1px solid #747369}.ace-one-dark .ace_marker-layer .ace_active-line{background:rgba(76,87,103,.19)}.ace-one-dark .ace_gutter-active-line{background-color:rgba(76,87,103,.19)}.ace-one-dark .ace_marker-layer .ace_selected-word{border:1px solid #3d4350}.ace-one-dark .ace_fold{background-color:#61afef;border-color:#abb2bf}.ace-one-dark .ace_keyword{color:#c678dd}.ace-one-dark .ace_keyword.ace_operator{color:#c678dd}.ace-one-dark .ace_keyword.ace_other.ace_unit{color:#d19a66}.ace-one-dark .ace_constant.ace_language{color:#d19a66}.ace-one-dark .ace_constant.ace_numeric{color:#d19a66}.ace-one-dark .ace_constant.ace_character{color:#56b6c2}.ace-one-dark .ace_constant.ace_other{color:#56b6c2}.ace-one-dark .ace_support.ace_function{color:#61afef}.ace-one-dark .ace_support.ace_constant{color:#d19a66}.ace-one-dark .ace_support.ace_class{color:#e5c07b}.ace-one-dark .ace_support.ace_type{color:#e5c07b}.ace-one-dark .ace_storage{color:#c678dd}.ace-one-dark .ace_storage.ace_type{color:#c678dd}.ace-one-dark .ace_invalid{color:#fff;background-color:#f2777a}.ace-one-dark .ace_invalid.ace_deprecated{color:#272b33;background-color:#d27b53}.ace-one-dark .ace_string{color:#98c379}.ace-one-dark .ace_string.ace_regexp{color:#e06c75}.ace-one-dark .ace_comment{font-style:italic;color:#5c6370}.ace-one-dark .ace_variable{color:#e06c75}.ace-one-dark .ace_variable.ace_parameter{color:#d19a66}.ace-one-dark .ace_meta.ace_tag{color:#e06c75}.ace-one-dark .ace_entity.ace_other.ace_attribute-name{color:#e06c75}.ace-one-dark .ace_entity.ace_name.ace_function{color:#61afef}.ace-one-dark .ace_entity.ace_name.ace_tag{color:#e06c75}.ace-one-dark .ace_markup.ace_heading{color:#98c379}.ace-one-dark .ace_indent-guide{background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAACCAYAAACZgbYnAAAAEklEQVQImWPQ09NrYAgMjP4PAAtGAwchHMyAAAAAAElFTkSuQmCC) right repeat-y}'; + var r = e("../lib/dom"); + r.importCssString(t.cssText, t.cssClass) +}) diff --git a/resources/views/databases/partials/database-backups.blade.php b/resources/views/databases/partials/database-backups.blade.php index 5eaf828..c2c650e 100644 --- a/resources/views/databases/partials/database-backups.blade.php +++ b/resources/views/databases/partials/database-backups.blade.php @@ -16,6 +16,7 @@ {{ __("Database") }} {{ __("Created") }} + {{ __("Storage") }} {{ __("Status") }} @@ -25,6 +26,7 @@ + {{ $backup->storage->profile }} ({{ $backup->storage->provider }})
@include("databases.partials.backup-status", ["status" => $backup->status]) diff --git a/routes/settings.php b/routes/settings.php index 1c3c9f9..b548e61 100644 --- a/routes/settings.php +++ b/routes/settings.php @@ -29,7 +29,7 @@ Route::prefix('settings/server-providers')->group(function () { Route::get('/', [ServerProviderController::class, 'index'])->name('server-providers'); Route::post('connect', [ServerProviderController::class, 'connect'])->name('server-providers.connect'); - Route::delete('delete/{id}', [ServerProviderController::class, 'delete']) + Route::delete('delete/{serverProvider}', [ServerProviderController::class, 'delete']) ->name('server-providers.delete'); }); @@ -37,7 +37,7 @@ Route::prefix('settings/source-controls')->group(function () { Route::get('/', [SourceControlController::class, 'index'])->name('source-controls'); Route::post('connect', [SourceControlController::class, 'connect'])->name('source-controls.connect'); - Route::delete('delete/{id}', [SourceControlController::class, 'delete']) + Route::delete('delete/{sourceControl}', [SourceControlController::class, 'delete']) ->name('source-controls.delete'); }); @@ -45,7 +45,7 @@ Route::prefix('settings/storage-providers')->group(function () { Route::get('/', [StorageProviderController::class, 'index'])->name('storage-providers'); Route::post('connect', [StorageProviderController::class, 'connect'])->name('storage-providers.connect'); - Route::delete('delete/{id}', [StorageProviderController::class, 'delete']) + Route::delete('delete/{storageProvider}', [StorageProviderController::class, 'delete']) ->name('storage-providers.delete'); }); diff --git a/tests/Feature/ApplicationTest.php b/tests/Feature/ApplicationTest.php index 8b407db..c1ba120 100644 --- a/tests/Feature/ApplicationTest.php +++ b/tests/Feature/ApplicationTest.php @@ -184,4 +184,70 @@ public function test_update_env_file(): void SSH::assertExecutedContains('echo "APP_ENV=production" | tee /home/vito/'.$this->site->domain.'/.env'); } + + public function test_git_hook_deployment(): void + { + SSH::fake(); + Http::fake([ + 'github.com/*' => Http::response([ + 'sha' => '123', + 'commit' => [ + 'message' => 'test commit message', + 'name' => 'test commit name', + 'email' => 'user@example.com', + 'url' => 'https://github.com', + ], + ], 200), + ]); + + $hook = GitHook::factory()->create([ + 'site_id' => $this->site->id, + 'source_control_id' => $this->site->source_control_id, + 'secret' => 'secret', + 'events' => ['push'], + 'actions' => ['deploy'], + ]); + + $this->site->deploymentScript->update([ + 'content' => 'git pull', + ]); + + $this->post(route('git-hooks'), [ + 'secret' => 'secret', + ])->assertSessionDoesntHaveErrors(); + + $this->assertDatabaseHas('deployments', [ + 'site_id' => $this->site->id, + 'deployment_script_id' => $this->site->deploymentScript->id, + 'status' => DeploymentStatus::FINISHED, + ]); + } + + public function test_git_hook_deployment_invalid_secret(): void + { + SSH::fake(); + Http::fake(); + + $hook = GitHook::factory()->create([ + 'site_id' => $this->site->id, + 'source_control_id' => $this->site->source_control_id, + 'secret' => 'secret', + 'events' => ['push'], + 'actions' => ['deploy'], + ]); + + $this->site->deploymentScript->update([ + 'content' => 'git pull', + ]); + + $this->post(route('git-hooks'), [ + 'secret' => 'invalid-secret', + ])->assertNotFound(); + + $this->assertDatabaseMissing('deployments', [ + 'site_id' => $this->site->id, + 'deployment_script_id' => $this->site->deploymentScript->id, + 'status' => DeploymentStatus::FINISHED, + ]); + } } diff --git a/tests/Feature/Auth/AuthenticationTest.php b/tests/Feature/Auth/AuthenticationTest.php index 66ec952..d9b5d09 100644 --- a/tests/Feature/Auth/AuthenticationTest.php +++ b/tests/Feature/Auth/AuthenticationTest.php @@ -37,4 +37,11 @@ public function test_users_can_not_authenticate_with_invalid_password(): void $this->assertGuest(); } + + public function test_redirect_if_not_authenticated(): void + { + $response = $this->get('/servers'); + + $response->assertRedirect('/login'); + } } diff --git a/tests/Feature/DatabaseBackupTest.php b/tests/Feature/DatabaseBackupTest.php index 3302cc1..81be50f 100644 --- a/tests/Feature/DatabaseBackupTest.php +++ b/tests/Feature/DatabaseBackupTest.php @@ -2,6 +2,7 @@ namespace Tests\Feature; +use App\Actions\Database\RunBackup; use App\Enums\BackupFileStatus; use App\Enums\BackupStatus; use App\Facades\SSH; @@ -9,6 +10,7 @@ use App\Models\Database; use App\Models\StorageProvider; use Illuminate\Foundation\Testing\RefreshDatabase; +use Illuminate\Support\Facades\Bus; use Illuminate\Support\Facades\Http; use Tests\TestCase; @@ -48,6 +50,34 @@ public function test_create_backup(): void ]); } + public function test_create_custom_interval_backup(): void + { + Bus::fake(); + + $this->actingAs($this->user); + + $database = Database::factory()->create([ + 'server_id' => $this->server, + ]); + + $storage = StorageProvider::factory()->create([ + 'user_id' => $this->user->id, + 'provider' => \App\Enums\StorageProvider::DROPBOX, + ]); + + $this->post(route('servers.databases.backups.store', $this->server), [ + 'backup_database' => $database->id, + 'backup_storage' => $storage->id, + 'backup_interval' => 'custom', + 'backup_custom' => '* * * * *', + 'backup_keep' => '10', + ])->assertSessionDoesntHaveErrors(); + + $this->assertDatabaseHas('backups', [ + 'status' => BackupStatus::RUNNING, + ]); + } + public function test_see_backups_list(): void { $this->actingAs($this->user); @@ -97,4 +127,43 @@ public function test_delete_backup(): void 'id' => $backup->id, ]); } + + public function test_restore_backup(): void + { + Http::fake(); + SSH::fake(); + + $this->actingAs($this->user); + + $database = Database::factory()->create([ + 'server_id' => $this->server, + ]); + + $storage = StorageProvider::factory()->create([ + 'user_id' => $this->user->id, + 'provider' => \App\Enums\StorageProvider::DROPBOX, + ]); + + $backup = Backup::factory()->create([ + 'server_id' => $this->server->id, + 'database_id' => $database->id, + 'storage_id' => $storage->id, + ]); + + $backupFile = app(RunBackup::class)->run($backup); + + $this->post(route('servers.databases.backups.files.restore', [ + 'server' => $this->server, + 'backup' => $backup, + 'backupFile' => $backupFile, + ]), [ + 'database' => $database->id, + ]) + ->assertSessionDoesntHaveErrors(); + + $this->assertDatabaseHas('backup_files', [ + 'id' => $backupFile->id, + 'status' => BackupFileStatus::RESTORED, + ]); + } } diff --git a/tests/Feature/DatabaseTest.php b/tests/Feature/DatabaseTest.php index e3e5625..21c7ffa 100644 --- a/tests/Feature/DatabaseTest.php +++ b/tests/Feature/DatabaseTest.php @@ -3,6 +3,7 @@ namespace Tests\Feature; use App\Enums\DatabaseStatus; +use App\Enums\DatabaseUserStatus; use App\Facades\SSH; use App\Models\Database; use Illuminate\Foundation\Testing\RefreshDatabase; @@ -28,6 +29,34 @@ public function test_create_database(): void ]); } + public function test_create_database_with_user(): void + { + $this->actingAs($this->user); + + SSH::fake(); + + $this->post(route('servers.databases.store', $this->server), [ + 'name' => 'database', + 'user' => 'on', + 'username' => 'user', + 'password' => 'password', + 'remote' => 'on', + 'host' => '%', + ])->assertSessionDoesntHaveErrors(); + + $this->assertDatabaseHas('databases', [ + 'name' => 'database', + 'status' => DatabaseStatus::READY, + ]); + + $this->assertDatabaseHas('database_users', [ + 'username' => 'user', + 'databases' => json_encode(['database']), + 'host' => '%', + 'status' => DatabaseUserStatus::READY, + ]); + } + public function test_see_databases_list(): void { $this->actingAs($this->user); diff --git a/tests/Feature/DatabaseUserTest.php b/tests/Feature/DatabaseUserTest.php index fe4ffbc..2859c69 100644 --- a/tests/Feature/DatabaseUserTest.php +++ b/tests/Feature/DatabaseUserTest.php @@ -29,6 +29,26 @@ public function test_create_database_user(): void ]); } + public function test_create_database_user_with_remote(): void + { + $this->actingAs($this->user); + + SSH::fake(); + + $this->post(route('servers.databases.users.store', $this->server), [ + 'username' => 'user', + 'password' => 'password', + 'remote' => 'on', + 'host' => '%', + ])->assertSessionDoesntHaveErrors(); + + $this->assertDatabaseHas('database_users', [ + 'username' => 'user', + 'host' => '%', + 'status' => DatabaseUserStatus::READY, + ]); + } + public function test_see_database_users_list(): void { $this->actingAs($this->user); @@ -58,4 +78,26 @@ public function test_delete_database_user(): void 'id' => $databaseUser->id, ]); } + + public function test_unlink_database(): void + { + $this->actingAs($this->user); + + SSH::fake(); + + $databaseUser = DatabaseUser::factory()->create([ + 'server_id' => $this->server, + ]); + + $this->post(route('servers.databases.users.link', [ + 'server' => $this->server, + 'databaseUser' => $databaseUser, + ]), []) + ->assertSessionDoesntHaveErrors(); + + $this->assertDatabaseHas('database_users', [ + 'username' => $databaseUser->username, + 'databases' => json_encode([]), + ]); + } } diff --git a/tests/Feature/NotificationChannelsTest.php b/tests/Feature/NotificationChannelsTest.php index 4b772da..192a944 100644 --- a/tests/Feature/NotificationChannelsTest.php +++ b/tests/Feature/NotificationChannelsTest.php @@ -26,12 +26,35 @@ public function test_add_email_channel(): void /** @var \App\Models\NotificationChannel $channel */ $channel = \App\Models\NotificationChannel::query() ->where('provider', NotificationChannel::EMAIL) + ->where('label', 'Email') ->first(); $this->assertEquals('email@example.com', $channel->data['email']); $this->assertTrue($channel->connected); } + public function test_cannot_add_email_channel(): void + { + config()->set('mail.default', 'smtp'); + config()->set('mail.mailers.smtp.host', '127.0.0.1'); // invalid host + + $this->actingAs($this->user); + + $this->post(route('notification-channels.add'), [ + 'provider' => NotificationChannel::EMAIL, + 'email' => 'email@example.com', + 'label' => 'Email', + ])->assertSessionHasErrors(); + + /** @var \App\Models\NotificationChannel $channel */ + $channel = \App\Models\NotificationChannel::query() + ->where('provider', NotificationChannel::EMAIL) + ->where('label', 'Email') + ->first(); + + $this->assertNull($channel); + } + public function test_add_slack_channel(): void { $this->actingAs($this->user); @@ -53,6 +76,28 @@ public function test_add_slack_channel(): void $this->assertTrue($channel->connected); } + public function test_cannot_add_slack_channel(): void + { + $this->actingAs($this->user); + + Http::fake([ + 'slack.com/*' => Http::response(['ok' => false], 401), + ]); + + $this->post(route('notification-channels.add'), [ + 'provider' => NotificationChannel::SLACK, + 'webhook_url' => 'https://hooks.slack.com/services/123/token', + 'label' => 'Slack', + ])->assertSessionHasErrors(); + + /** @var \App\Models\NotificationChannel $channel */ + $channel = \App\Models\NotificationChannel::query() + ->where('provider', NotificationChannel::SLACK) + ->first(); + + $this->assertNull($channel); + } + public function test_add_discord_channel(): void { $this->actingAs($this->user); @@ -74,9 +119,27 @@ public function test_add_discord_channel(): void $this->assertTrue($channel->connected); } - /* - * @TODO fix json comparison - */ + public function test_cannot_add_discord_channel(): void + { + $this->actingAs($this->user); + + Http::fake([ + 'discord.com/*' => Http::response(['ok' => false], 401), + ]); + + $this->post(route('notification-channels.add'), [ + 'provider' => NotificationChannel::DISCORD, + 'webhook_url' => 'https://discord.com/api/webhooks/123/token', + 'label' => 'Discord', + ])->assertSessionHasErrors(); + + /** @var \App\Models\NotificationChannel $channel */ + $channel = \App\Models\NotificationChannel::query() + ->where('provider', NotificationChannel::DISCORD) + ->first(); + + $this->assertNull($channel); + } public function test_add_telegram_channel(): void { @@ -101,6 +164,29 @@ public function test_add_telegram_channel(): void $this->assertTrue($channel->connected); } + public function test_cannot_add_telegram_channel(): void + { + $this->actingAs($this->user); + + Http::fake([ + 'api.telegram.org/*' => Http::response(['ok' => false], 401), + ]); + + $this->post(route('notification-channels.add'), [ + 'provider' => NotificationChannel::TELEGRAM, + 'bot_token' => 'token', + 'chat_id' => '123', + 'label' => 'Telegram', + ])->assertSessionHasErrors(); + + /** @var \App\Models\NotificationChannel $channel */ + $channel = \App\Models\NotificationChannel::query() + ->where('provider', NotificationChannel::TELEGRAM) + ->first(); + + $this->assertNull($channel); + } + public function test_see_channels_list(): void { $this->actingAs($this->user); diff --git a/tests/Feature/PHPTest.php b/tests/Feature/PHPTest.php index 6726653..c7b131c 100644 --- a/tests/Feature/PHPTest.php +++ b/tests/Feature/PHPTest.php @@ -135,4 +135,32 @@ public function test_extension_already_installed(): void 'extension' => 'gmp', ]))->assertSessionHasErrors(); } + + public function test_get_php_ini(): void + { + SSH::fake('[PHP ini]'); + + $this->actingAs($this->user); + + $this->get(route('servers.php.get-ini', [ + 'server' => $this->server, + 'version' => '8.2', + ]))->assertSessionHas('ini'); + } + + public function test_update_php_ini(): void + { + SSH::fake(); + + $this->actingAs($this->user); + + $this->post(route('servers.php.update-ini', [ + 'server' => $this->server, + 'version' => '8.2', + 'ini' => 'new ini', + ])) + ->assertSessionDoesntHaveErrors() + ->assertSessionHas('toast.type', 'success') + ->assertSessionHas('toast.message', 'PHP ini updated!'); + } } diff --git a/tests/Feature/ProjectsTest.php b/tests/Feature/ProjectsTest.php index 919a15f..fcf9d89 100644 --- a/tests/Feature/ProjectsTest.php +++ b/tests/Feature/ProjectsTest.php @@ -68,4 +68,16 @@ public function test_edit_project(): void 'name' => 'new-name', ]); } + + public function test_cannot_delete_last_project(): void + { + $this->actingAs($this->user); + + $this->delete(route('projects.delete', [ + 'project' => $this->user->currentProject, + ])) + ->assertSessionDoesntHaveErrors() + ->assertSessionHas('toast.type', 'error') + ->assertSessionHas('toast.message', 'Cannot delete the last project.'); + } } diff --git a/tests/Feature/QueuesTest.php b/tests/Feature/QueuesTest.php index b75a0a0..f7a6f0d 100644 --- a/tests/Feature/QueuesTest.php +++ b/tests/Feature/QueuesTest.php @@ -86,4 +86,85 @@ public function test_create_queue() 'status' => QueueStatus::RUNNING, ]); } + + public function test_start_queue(): void + { + SSH::fake(); + + $this->actingAs($this->user); + + $queue = Queue::factory()->create([ + 'server_id' => $this->server->id, + 'site_id' => $this->site->id, + 'status' => QueueStatus::STOPPED, + ]); + + $this->post( + route('servers.sites.queues.action', [ + 'action' => 'start', + 'server' => $this->server, + 'site' => $this->site, + 'queue' => $queue, + ]) + )->assertSessionDoesntHaveErrors(); + + $this->assertDatabaseHas('queues', [ + 'id' => $queue->id, + 'status' => QueueStatus::RUNNING, + ]); + } + + public function test_stop_queue(): void + { + SSH::fake(); + + $this->actingAs($this->user); + + $queue = Queue::factory()->create([ + 'server_id' => $this->server->id, + 'site_id' => $this->site->id, + 'status' => QueueStatus::RUNNING, + ]); + + $this->post( + route('servers.sites.queues.action', [ + 'action' => 'stop', + 'server' => $this->server, + 'site' => $this->site, + 'queue' => $queue, + ]) + )->assertSessionDoesntHaveErrors(); + + $this->assertDatabaseHas('queues', [ + 'id' => $queue->id, + 'status' => QueueStatus::STOPPED, + ]); + } + + public function test_restart_queue(): void + { + SSH::fake(); + + $this->actingAs($this->user); + + $queue = Queue::factory()->create([ + 'server_id' => $this->server->id, + 'site_id' => $this->site->id, + 'status' => QueueStatus::RUNNING, + ]); + + $this->post( + route('servers.sites.queues.action', [ + 'action' => 'restart', + 'server' => $this->server, + 'site' => $this->site, + 'queue' => $queue, + ]) + )->assertSessionDoesntHaveErrors(); + + $this->assertDatabaseHas('queues', [ + 'id' => $queue->id, + 'status' => QueueStatus::RUNNING, + ]); + } } diff --git a/tests/Feature/ServerProvidersTest.php b/tests/Feature/ServerProvidersTest.php index 1646a2c..1272e73 100644 --- a/tests/Feature/ServerProvidersTest.php +++ b/tests/Feature/ServerProvidersTest.php @@ -11,20 +11,52 @@ class ServerProvidersTest extends TestCase { use RefreshDatabase; - public function test_connect_hetzner(): void + /** + * @dataProvider data + */ + public function test_connect_provider(string $provider, array $input): void { $this->actingAs($this->user); Http::fake(); - $this->post(route('server-providers.connect'), [ - 'provider' => ServerProvider::HETZNER, - 'name' => 'profile', - 'token' => 'token', - ])->assertSessionDoesntHaveErrors(); + $data = array_merge( + [ + 'provider' => $provider, + 'name' => 'profile', + ], + $input + ); + $this->post(route('server-providers.connect'), $data)->assertSessionDoesntHaveErrors(); $this->assertDatabaseHas('server_providers', [ - 'provider' => ServerProvider::HETZNER, + 'provider' => $provider, + 'profile' => 'profile', + ]); + } + + /** + * @dataProvider data + */ + public function test_cannot_connect_to_provider(string $provider, array $input): void + { + $this->actingAs($this->user); + + Http::fake([ + '*' => Http::response([], 401), + ]); + + $data = array_merge( + [ + 'provider' => $provider, + 'name' => 'profile', + ], + $input + ); + $this->post(route('server-providers.connect'), $data)->assertSessionHasErrors(); + + $this->assertDatabaseMissing('server_providers', [ + 'provider' => $provider, 'profile' => 'profile', ]); } @@ -41,19 +73,86 @@ public function test_see_providers_list(): void ->assertSee($provider->profile); } - public function test_delete_provider(): void + /** + * @dataProvider data + */ + public function test_delete_provider(string $provider): void { $this->actingAs($this->user); $provider = \App\Models\ServerProvider::factory()->create([ 'user_id' => $this->user->id, + 'provider' => $provider, ]); - $this->delete(route('server-providers.delete', $provider->id)) + $this->delete(route('server-providers.delete', $provider)) ->assertSessionDoesntHaveErrors(); $this->assertDatabaseMissing('server_providers', [ 'id' => $provider->id, ]); } + + /** + * @dataProvider data + */ + public function test_cannot_delete_provider(string $provider): void + { + $this->actingAs($this->user); + + $provider = \App\Models\ServerProvider::factory()->create([ + 'user_id' => $this->user->id, + 'provider' => $provider, + ]); + + $this->server->update([ + 'provider_id' => $provider->id, + ]); + + $this->delete(route('server-providers.delete', $provider)) + ->assertSessionDoesntHaveErrors() + ->assertSessionHas('toast.type', 'error') + ->assertSessionHas('toast.message', 'This server provider is being used by a server.'); + + $this->assertDatabaseHas('server_providers', [ + 'id' => $provider->id, + ]); + } + + public static function data(): array + { + return [ + // [ + // ServerProvider::AWS, + // [ + // 'key' => 'key', + // 'secret' => 'secret', + // ], + // ], + [ + ServerProvider::LINODE, + [ + 'token' => 'token', + ], + ], + [ + ServerProvider::DIGITALOCEAN, + [ + 'token' => 'token', + ], + ], + [ + ServerProvider::VULTR, + [ + 'token' => 'token', + ], + ], + [ + ServerProvider::HETZNER, + [ + 'token' => 'token', + ], + ], + ]; + } } diff --git a/tests/Feature/ServerTest.php b/tests/Feature/ServerTest.php index 5063aee..c8f9d61 100644 --- a/tests/Feature/ServerTest.php +++ b/tests/Feature/ServerTest.php @@ -10,17 +10,17 @@ use App\Enums\ServiceStatus; use App\Enums\Webserver; use App\Facades\SSH; +use App\NotificationChannels\Email\NotificationMail; use Illuminate\Foundation\Testing\RefreshDatabase; +use Illuminate\Support\Facades\Http; +use Illuminate\Support\Facades\Mail; use Tests\TestCase; -/** - * @TODO add more tests - */ class ServerTest extends TestCase { use RefreshDatabase; - public function test_create_custom_server(): void + public function test_create_regular_server(): void { $this->actingAs($this->user); @@ -76,6 +76,62 @@ public function test_create_custom_server(): void ]); } + public function test_create_database_server(): void + { + $this->actingAs($this->user); + + SSH::fake('Active: active'); // fake output for service installations + + $this->post(route('servers.create'), [ + 'type' => ServerType::DATABASE, + 'provider' => ServerProvider::CUSTOM, + 'name' => 'test', + 'ip' => '2.2.2.2', + 'port' => '22', + 'os' => OperatingSystem::UBUNTU22, + 'database' => Database::MYSQL80, + ])->assertSessionDoesntHaveErrors(); + + $server = \App\Models\Server::query()->where('ip', '2.2.2.2')->first(); + + $this->assertDatabaseHas('servers', [ + 'name' => 'test', + 'ip' => '2.2.2.2', + 'status' => ServerStatus::READY, + ]); + + $this->assertDatabaseMissing('services', [ + 'server_id' => $server->id, + 'type' => 'php', + 'version' => '8.2', + 'status' => ServiceStatus::READY, + ]); + + $this->assertDatabaseMissing('services', [ + 'server_id' => $server->id, + 'type' => 'webserver', + 'name' => 'nginx', + 'version' => 'latest', + 'status' => ServiceStatus::READY, + ]); + + $this->assertDatabaseHas('services', [ + 'server_id' => $server->id, + 'type' => 'database', + 'name' => 'mysql', + 'version' => '8.0', + 'status' => ServiceStatus::READY, + ]); + + $this->assertDatabaseHas('services', [ + 'server_id' => $server->id, + 'type' => 'firewall', + 'name' => 'ufw', + 'version' => 'latest', + 'status' => ServiceStatus::READY, + ]); + } + public function test_delete_server(): void { $this->actingAs($this->user); @@ -89,4 +145,139 @@ public function test_delete_server(): void 'id' => $this->server->id, ]); } + + public function test_cannot_delete_on_provider(): void + { + Mail::fake(); + Http::fake([ + '*' => Http::response([], 401), + ]); + + $this->actingAs($this->user); + + $provider = \App\Models\ServerProvider::factory()->create([ + 'user_id' => $this->user->id, + 'provider' => ServerProvider::HETZNER, + 'credentials' => [ + 'token' => 'token', + ], + ]); + + $this->server->update([ + 'provider' => ServerProvider::HETZNER, + 'provider_id' => $provider->id, + 'provider_data' => [ + 'hetzner_id' => 1, + 'ssh_key_id' => 1, + ], + ]); + + $this->delete(route('servers.delete', $this->server)) + ->assertSessionDoesntHaveErrors(); + + $this->assertDatabaseMissing('servers', [ + 'id' => $this->server->id, + ]); + + Mail::assertSent(NotificationMail::class); + } + + public function test_check_connection_is_ready(): void + { + SSH::fake(); + + $this->actingAs($this->user); + + $this->server->update(['status' => ServerStatus::DISCONNECTED]); + + $this->post(route('servers.settings.check-connection', $this->server)) + ->assertSessionDoesntHaveErrors(); + + $this->assertDatabaseHas('servers', [ + 'id' => $this->server->id, + 'status' => ServerStatus::READY, + ]); + } + + public function test_connection_failed(): void + { + SSH::fake()->connectionWillFail(); + + $this->actingAs($this->user); + + $this->server->update(['status' => ServerStatus::READY]); + + $this->post(route('servers.settings.check-connection', $this->server)) + ->assertSessionDoesntHaveErrors(); + + $this->assertDatabaseHas('servers', [ + 'id' => $this->server->id, + 'status' => ServerStatus::DISCONNECTED, + ]); + } + + public function test_reboot_server(): void + { + SSH::fake(); + + $this->actingAs($this->user); + + $this->post(route('servers.settings.reboot', $this->server)) + ->assertSessionDoesntHaveErrors(); + + $this->assertDatabaseHas('servers', [ + 'id' => $this->server->id, + 'status' => ServerStatus::DISCONNECTED, + ]); + } + + public function test_edit_server(): void + { + $this->actingAs($this->user); + + $this->post(route('servers.settings.edit', $this->server), [ + 'name' => 'new-name', + ])->assertSessionDoesntHaveErrors(); + + $this->assertDatabaseHas('servers', [ + 'id' => $this->server->id, + 'name' => 'new-name', + ]); + } + + public function test_edit_server_ip_address(): void + { + SSH::fake(); + + $this->actingAs($this->user); + + $this->post(route('servers.settings.edit', $this->server), [ + 'ip' => '2.2.2.2', + ])->assertSessionDoesntHaveErrors(); + + $this->assertDatabaseHas('servers', [ + 'id' => $this->server->id, + 'ip' => '2.2.2.2', + 'status' => ServerStatus::READY, + ]); + } + + public function test_edit_server_ip_address_and_disconnect(): void + { + SSH::fake()->connectionWillFail(); + + $this->actingAs($this->user); + + $this->post(route('servers.settings.edit', $this->server), [ + 'ip' => '2.2.2.2', + 'port' => 2222, + ])->assertSessionDoesntHaveErrors(); + + $this->assertDatabaseHas('servers', [ + 'id' => $this->server->id, + 'ip' => '2.2.2.2', + 'port' => 2222, + 'status' => ServerStatus::DISCONNECTED, + ]); + } } diff --git a/tests/Feature/SitesTest.php b/tests/Feature/SitesTest.php index e1cfe19..22ec47a 100644 --- a/tests/Feature/SitesTest.php +++ b/tests/Feature/SitesTest.php @@ -160,4 +160,16 @@ public static function create_data(): array ], ]; } + + public function test_see_logs(): void + { + $this->actingAs($this->user); + + $this->get(route('servers.sites.logs', [ + 'server' => $this->server, + 'site' => $this->site, + ])) + ->assertOk() + ->assertSee('Logs'); + } } diff --git a/tests/Feature/SourceControlsTest.php b/tests/Feature/SourceControlsTest.php index 3b50b28..77e018c 100644 --- a/tests/Feature/SourceControlsTest.php +++ b/tests/Feature/SourceControlsTest.php @@ -5,7 +5,6 @@ use App\Models\SourceControl; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Facades\Http; -use JsonException; use Tests\TestCase; class SourceControlsTest extends TestCase @@ -14,8 +13,6 @@ class SourceControlsTest extends TestCase /** * @dataProvider data - * - * @throws JsonException */ public function test_connect_provider(string $provider, ?string $customUrl): void { @@ -43,8 +40,6 @@ public function test_connect_provider(string $provider, ?string $customUrl): voi /** * @dataProvider data - * - * @throws JsonException */ public function test_delete_provider(string $provider): void { @@ -64,6 +59,33 @@ public function test_delete_provider(string $provider): void ]); } + /** + * @dataProvider data + */ + public function test_cannot_delete_provider(string $provider): void + { + $this->actingAs($this->user); + + /** @var SourceControl $sourceControl */ + $sourceControl = SourceControl::factory()->create([ + 'provider' => $provider, + 'profile' => 'test', + ]); + + $this->site->update([ + 'source_control_id' => $sourceControl->id, + ]); + + $this->delete(route('source-controls.delete', $sourceControl->id)) + ->assertSessionDoesntHaveErrors() + ->assertSessionHas('toast.type', 'error') + ->assertSessionHas('toast.message', 'This source control is being used by a site.'); + + $this->assertDatabaseHas('source_controls', [ + 'id' => $sourceControl->id, + ]); + } + public static function data(): array { return [ diff --git a/tests/Feature/StorageProvidersTest.php b/tests/Feature/StorageProvidersTest.php index a612e63..190b17d 100644 --- a/tests/Feature/StorageProvidersTest.php +++ b/tests/Feature/StorageProvidersTest.php @@ -3,6 +3,8 @@ namespace Tests\Feature; use App\Enums\StorageProvider; +use App\Models\Backup; +use App\Models\Database; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Facades\Http; use Tests\TestCase; @@ -57,4 +59,32 @@ public function test_delete_provider(): void 'id' => $provider->id, ]); } + + public function test_cannot_delete_provider(): void + { + $this->actingAs($this->user); + + $database = Database::factory()->create([ + 'server_id' => $this->server, + ]); + + $provider = \App\Models\StorageProvider::factory()->create([ + 'user_id' => $this->user->id, + ]); + + Backup::factory()->create([ + 'server_id' => $this->server->id, + 'database_id' => $database->id, + 'storage_id' => $provider->id, + ]); + + $this->delete(route('storage-providers.delete', $provider->id)) + ->assertSessionDoesntHaveErrors() + ->assertSessionHas('toast.type', 'error') + ->assertSessionHas('toast.message', 'This storage provider is being used by a backup.'); + + $this->assertDatabaseHas('storage_providers', [ + 'id' => $provider->id, + ]); + } } diff --git a/tests/TestCase.php b/tests/TestCase.php index 90a40fd..6b2c37c 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -3,6 +3,7 @@ namespace Tests; use App\Enums\Database; +use App\Enums\NotificationChannel; use App\Enums\ServiceStatus; use App\Enums\Webserver; use App\Models\Server; @@ -31,6 +32,14 @@ public function setUp(): void $this->user = User::factory()->create(); + \App\Models\NotificationChannel::factory()->create([ + 'provider' => NotificationChannel::EMAIL, + 'connected' => true, + 'data' => [ + 'email' => 'user@example.com', + ], + ]); + $this->setupServer(); $this->setupSite(); @@ -38,6 +47,15 @@ public function setUp(): void $this->setupKeys(); } + public function tearDown(): void + { + parent::tearDown(); + + if (File::exists(storage_path('app/key-pairs-test'))) { + File::deleteDirectory(storage_path('app/key-pairs-test')); + } + } + private function setupServer(): void { $this->server = Server::factory()->create([ diff --git a/tests/Unit/Commands/CreateUserCommandTest.php b/tests/Unit/Commands/CreateUserCommandTest.php new file mode 100644 index 0000000..45e0664 --- /dev/null +++ b/tests/Unit/Commands/CreateUserCommandTest.php @@ -0,0 +1,42 @@ +artisan('user:create', [ + 'name' => 'John Doe', + 'email' => 'john@doe.com', + 'password' => 'password', + ])->expectsOutput('User created!'); + + $this->assertDatabaseHas('users', [ + 'name' => 'John Doe', + 'email' => 'john@doe.com', + ]); + + /** @var User $user */ + $user = User::query()->where('email', 'john@doe.com')->first(); + + $this->assertDatabaseHas('projects', [ + 'user_id' => $user->id, + ]); + } + + public function test_skip_existing_user(): void + { + $this->artisan('user:create', [ + 'name' => 'John Doe', + 'email' => $this->user->email, + 'password' => 'password', + ])->expectsOutput('User already exists. Skipping...'); + } +} diff --git a/tests/Unit/Commands/RunBackupCommandTest.php b/tests/Unit/Commands/RunBackupCommandTest.php new file mode 100644 index 0000000..6b81ae3 --- /dev/null +++ b/tests/Unit/Commands/RunBackupCommandTest.php @@ -0,0 +1,46 @@ +artisan('backups:run "* * * * *"') + ->expectsOutput('0 backups started'); + } + + public function test_run_backups(): void + { + SSH::fake(); + + $database = Database::factory()->create([ + 'server_id' => $this->server, + ]); + + $storage = StorageProvider::factory()->create([ + 'user_id' => $this->user->id, + 'provider' => \App\Enums\StorageProvider::DROPBOX, + ]); + + $backup = Backup::factory()->create([ + 'server_id' => $this->server->id, + 'database_id' => $database->id, + 'storage_id' => $storage->id, + 'interval' => '1 * * * *', + 'keep_backups' => 10, + ]); + + $this->artisan('backups:run "1 * * * *"') + ->expectsOutput('1 backups started'); + } +} diff --git a/tests/Unit/Models/ServerModelTest.php b/tests/Unit/Models/ServerModelTest.php index 1ef3eb2..ce33576 100644 --- a/tests/Unit/Models/ServerModelTest.php +++ b/tests/Unit/Models/ServerModelTest.php @@ -2,6 +2,8 @@ namespace Tests\Unit\Models; +use App\Enums\ServerStatus; +use App\Facades\SSH; use Illuminate\Foundation\Testing\RefreshDatabase; use Tests\TestCase; @@ -17,4 +19,32 @@ public function test_should_have_default_service() $php->refresh(); $this->assertTrue($php->is_default); } + + public function test_check_connection_is_ready(): void + { + SSH::fake(); + + $this->server->update(['status' => ServerStatus::DISCONNECTED]); + + $this->server->checkConnection(); + + $this->assertDatabaseHas('servers', [ + 'id' => $this->server->id, + 'status' => ServerStatus::READY, + ]); + } + + public function test_connection_failed(): void + { + SSH::fake()->connectionWillFail(); + + $this->server->update(['status' => ServerStatus::READY]); + + $this->server->checkConnection(); + + $this->assertDatabaseHas('servers', [ + 'id' => $this->server->id, + 'status' => ServerStatus::DISCONNECTED, + ]); + } }