From fa62a0982e36e4e47bcf4e9a29ec2e516b9ba6bd Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Thu, 26 Oct 2017 23:49:54 -0500 Subject: [PATCH] Refactor startup modification and environment variable services Better setup, more flexibility, more tests. --- .../Repository/ServerRepositoryInterface.php | 9 +- .../Controllers/Admin/ServersController.php | 7 +- .../Server/Settings/StartupController.php | 2 + app/Models/Node.php | 2 + app/Models/User.php | 3 + .../Eloquent/ServerRepository.php | 17 +- app/Services/Servers/EnvironmentService.php | 83 ++++--- .../ServerConfigurationStructureService.php | 19 +- .../Servers/ServerCreationService.php | 87 ++++--- .../Servers/StartupModificationService.php | 129 +++++------ .../Servers/VariableValidatorService.php | 88 ++----- app/Traits/Services/HasUserLevels.php | 44 ++++ config/pterodactyl.php | 15 ++ database/factories/ModelFactory.php | 3 + .../Servers/EnvironmentServiceTest.php | 156 ++++++++----- ...erverConfigurationStructureServiceTest.php | 2 +- .../Servers/ServerCreationServiceTest.php | 217 ++++++++--------- .../StartupModificationServiceTest.php | 121 ++++++++-- .../Servers/VariableValidatorServiceTest.php | 219 ++++++++---------- 19 files changed, 660 insertions(+), 563 deletions(-) create mode 100644 app/Traits/Services/HasUserLevels.php diff --git a/app/Contracts/Repository/ServerRepositoryInterface.php b/app/Contracts/Repository/ServerRepositoryInterface.php index 7031b27e6..9e597bb73 100644 --- a/app/Contracts/Repository/ServerRepositoryInterface.php +++ b/app/Contracts/Repository/ServerRepositoryInterface.php @@ -69,12 +69,11 @@ interface ServerRepositoryInterface extends RepositoryInterface, SearchableInter /** * Return enough data to be used for the creation of a server via the daemon. * - * @param int $id - * @return \Illuminate\Database\Eloquent\Collection|\Illuminate\Database\Eloquent\Model - * - * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + * @param \Pterodactyl\Models\Server $server + * @param bool $refresh + * @return \Pterodactyl\Models\Server */ - public function getDataForCreation($id); + public function getDataForCreation(Server $server, bool $refresh = false): Server; /** * Return a server as well as associated databases and their hosts. diff --git a/app/Http/Controllers/Admin/ServersController.php b/app/Http/Controllers/Admin/ServersController.php index a10c56904..f79d257a4 100644 --- a/app/Http/Controllers/Admin/ServersController.php +++ b/app/Http/Controllers/Admin/ServersController.php @@ -11,6 +11,7 @@ namespace Pterodactyl\Http\Controllers\Admin; use Javascript; use Illuminate\Http\Request; +use Pterodactyl\Models\User; use Pterodactyl\Models\Server; use Prologue\Alerts\AlertsMessageBag; use Pterodactyl\Exceptions\DisplayException; @@ -570,10 +571,8 @@ class ServersController extends Controller */ public function saveStartup(Request $request, Server $server) { - $this->startupModificationService->isAdmin()->handle( - $server, - $request->except('_token') - ); + $this->startupModificationService->setUserLevel(User::USER_LEVEL_ADMIN); + $this->startupModificationService->handle($server, $request->except('_token')); $this->alert->success(trans('admin/server.alerts.startup_changed'))->flash(); return redirect()->route('admin.servers.view.startup', $server->id); diff --git a/app/Http/Controllers/Server/Settings/StartupController.php b/app/Http/Controllers/Server/Settings/StartupController.php index f5ea20a47..5d299c42e 100644 --- a/app/Http/Controllers/Server/Settings/StartupController.php +++ b/app/Http/Controllers/Server/Settings/StartupController.php @@ -3,6 +3,7 @@ namespace Pterodactyl\Http\Controllers\Server\Settings; use Illuminate\Http\Request; +use Pterodactyl\Models\User; use Illuminate\Http\RedirectResponse; use Prologue\Alerts\AlertsMessageBag; use Pterodactyl\Http\Controllers\Controller; @@ -84,6 +85,7 @@ class StartupController extends Controller */ public function update(UpdateStartupParametersFormRequest $request): RedirectResponse { + $this->modificationService->setUserLevel(User::USER_LEVEL_USER); $this->modificationService->handle($request->attributes->get('server'), $request->normalize()); $this->alert->success(trans('server.config.startup.edited'))->flash(); diff --git a/app/Models/Node.php b/app/Models/Node.php index 71f5614b5..cc22a724e 100644 --- a/app/Models/Node.php +++ b/app/Models/Node.php @@ -20,6 +20,8 @@ class Node extends Model implements CleansAttributes, ValidableContract { use Eloquence, Notifiable, Validable; + const DAEMON_SECRET_LENGTH = 36; + /** * The table associated with the model. * diff --git a/app/Models/User.php b/app/Models/User.php index 9f063c8ed..7b09165aa 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -32,6 +32,9 @@ class User extends Model implements { use Authenticatable, Authorizable, CanResetPassword, Eloquence, Notifiable, Validable; + const USER_LEVEL_USER = 0; + const USER_LEVEL_ADMIN = 1; + /** * Level of servers to display when using access() on a user. * diff --git a/app/Repositories/Eloquent/ServerRepository.php b/app/Repositories/Eloquent/ServerRepository.php index da15c89b1..ac023ff17 100644 --- a/app/Repositories/Eloquent/ServerRepository.php +++ b/app/Repositories/Eloquent/ServerRepository.php @@ -137,16 +137,21 @@ class ServerRepository extends EloquentRepository implements ServerRepositoryInt } /** - * {@inheritdoc} + * Return enough data to be used for the creation of a server via the daemon. + * + * @param \Pterodactyl\Models\Server $server + * @param bool $refresh + * @return \Pterodactyl\Models\Server */ - public function getDataForCreation($id) + public function getDataForCreation(Server $server, bool $refresh = false): Server { - $instance = $this->getBuilder()->with(['allocation', 'allocations', 'pack', 'egg'])->find($id, $this->getColumns()); - if (! $instance) { - throw new RecordNotFoundException(); + foreach (['allocation', 'allocations', 'pack', 'egg'] as $relation) { + if (! $server->relationLoaded($relation) || $refresh) { + $server->load($relation); + } } - return $instance; + return $server; } /** diff --git a/app/Services/Servers/EnvironmentService.php b/app/Services/Servers/EnvironmentService.php index 987d90b2d..177034e10 100644 --- a/app/Services/Servers/EnvironmentService.php +++ b/app/Services/Servers/EnvironmentService.php @@ -1,42 +1,37 @@ . - * - * This software is licensed under the terms of the MIT license. - * https://opensource.org/licenses/MIT - */ namespace Pterodactyl\Services\Servers; use Pterodactyl\Models\Server; +use Illuminate\Contracts\Config\Repository as ConfigRepository; use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; class EnvironmentService { - const ENVIRONMENT_CASTS = [ - 'STARTUP' => 'startup', - 'P_SERVER_LOCATION' => 'location.short', - 'P_SERVER_UUID' => 'uuid', - ]; - /** * @var array */ - protected $additional = []; + private $additional = []; + + /** + * @var \Illuminate\Contracts\Config\Repository + */ + private $config; /** * @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface */ - protected $repository; + private $repository; /** * EnvironmentService constructor. * + * @param \Illuminate\Contracts\Config\Repository $config * @param \Pterodactyl\Contracts\Repository\ServerRepositoryInterface $repository */ - public function __construct(ServerRepositoryInterface $repository) + public function __construct(ConfigRepository $config, ServerRepositoryInterface $repository) { + $this->config = $config; $this->repository = $repository; } @@ -46,42 +41,70 @@ class EnvironmentService * * @param string $key * @param callable $closure - * @return $this */ - public function setEnvironmentKey($key, callable $closure) + public function setEnvironmentKey(string $key, callable $closure) { - $this->additional[] = [$key, $closure]; + $this->additional[$key] = $closure; + } - return $this; + /** + * Return the dynamically added additional keys. + * + * @return array + */ + public function getEnvironmentKeys(): array + { + return $this->additional; } /** * Take all of the environment variables configured for this server and return * them in an easy to process format. * - * @param int|\Pterodactyl\Models\Server $server + * @param \Pterodactyl\Models\Server $server * @return array * * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException */ - public function process($server) + public function handle(Server $server): array { - if (! $server instanceof Server) { - $server = $this->repository->find($server); - } - $variables = $this->repository->getVariablesWithValues($server->id); - // Process static environment variables defined in this file. - foreach (self::ENVIRONMENT_CASTS as $key => $object) { + // Process environment variables defined in this file. This is done first + // in order to allow run-time and config defined variables to take + // priority over built-in values. + foreach ($this->getEnvironmentMappings() as $key => $object) { $variables[$key] = object_get($server, $object); } + // Process variables set in the configuration file. + foreach ($this->config->get('pterodactyl.environment_mappings', []) as $key => $object) { + if (is_callable($object)) { + $variables[$key] = call_user_func($object, $server); + } else { + $variables[$key] = object_get($server, $object); + } + } + // Process dynamically included environment variables. - foreach ($this->additional as $item) { - $variables[$item[0]] = call_user_func($item[1], $server); + foreach ($this->additional as $key => $closure) { + $variables[$key] = call_user_func($closure, $server); } return $variables; } + + /** + * Return a mapping of Panel default environment variables. + * + * @return array + */ + final private function getEnvironmentMappings(): array + { + return [ + 'STARTUP' => 'startup', + 'P_SERVER_LOCATION' => 'location.short', + 'P_SERVER_UUID' => 'uuid', + ]; + } } diff --git a/app/Services/Servers/ServerConfigurationStructureService.php b/app/Services/Servers/ServerConfigurationStructureService.php index b92191711..d91d9db95 100644 --- a/app/Services/Servers/ServerConfigurationStructureService.php +++ b/app/Services/Servers/ServerConfigurationStructureService.php @@ -19,12 +19,12 @@ class ServerConfigurationStructureService /** * @var \Pterodactyl\Services\Servers\EnvironmentService */ - protected $environment; + private $environment; /** * @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface */ - protected $repository; + private $repository; /** * ServerConfigurationStructureService constructor. @@ -41,19 +41,21 @@ class ServerConfigurationStructureService } /** - * @param int|\Pterodactyl\Models\Server $server + * Return a configuration array for a specific server when passed a server model. + * + * @param \Pterodactyl\Models\Server $server * @return array + * * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException */ - public function handle($server): array + public function handle(Server $server): array { - if (! $server instanceof Server || array_diff(self::REQUIRED_RELATIONS, $server->getRelations())) { - $server = $this->repository->getDataForCreation(is_digit($server) ? $server : $server->id); + if (array_diff(self::REQUIRED_RELATIONS, $server->getRelations())) { + $server = $this->repository->getDataForCreation($server); } return [ 'uuid' => $server->uuid, - 'user' => $server->username, 'build' => [ 'default' => [ 'ip' => $server->allocation->ip, @@ -62,7 +64,7 @@ class ServerConfigurationStructureService 'ports' => $server->allocations->groupBy('ip')->map(function ($item) { return $item->pluck('port'); })->toArray(), - 'env' => $this->environment->process($server), + 'env' => $this->environment->handle($server), 'memory' => (int) $server->memory, 'swap' => (int) $server->swap, 'io' => (int) $server->io, @@ -70,7 +72,6 @@ class ServerConfigurationStructureService 'disk' => (int) $server->disk, 'image' => $server->image, ], - 'keys' => [], 'service' => [ 'egg' => $server->egg->uuid, 'pack' => object_get($server, 'pack.uuid'), diff --git a/app/Services/Servers/ServerCreationService.php b/app/Services/Servers/ServerCreationService.php index 33d23b000..86e580a22 100644 --- a/app/Services/Servers/ServerCreationService.php +++ b/app/Services/Servers/ServerCreationService.php @@ -1,18 +1,12 @@ . - * - * This software is licensed under the terms of the MIT license. - * https://opensource.org/licenses/MIT - */ namespace Pterodactyl\Services\Servers; use Ramsey\Uuid\Uuid; +use Pterodactyl\Models\Node; +use Pterodactyl\Models\User; use GuzzleHttp\Exception\RequestException; use Illuminate\Database\ConnectionInterface; -use Pterodactyl\Services\Nodes\NodeCreationService; use Pterodactyl\Contracts\Repository\NodeRepositoryInterface; use Pterodactyl\Contracts\Repository\UserRepositoryInterface; use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; @@ -26,47 +20,47 @@ class ServerCreationService /** * @var \Pterodactyl\Contracts\Repository\AllocationRepositoryInterface */ - protected $allocationRepository; + private $allocationRepository; /** * @var \Pterodactyl\Services\Servers\ServerConfigurationStructureService */ - protected $configurationStructureService; + private $configurationStructureService; /** * @var \Illuminate\Database\ConnectionInterface */ - protected $connection; + private $connection; /** * @var \Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface */ - protected $daemonServerRepository; + private $daemonServerRepository; /** * @var \Pterodactyl\Contracts\Repository\NodeRepositoryInterface */ - protected $nodeRepository; + private $nodeRepository; /** * @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface */ - protected $repository; + private $repository; /** * @var \Pterodactyl\Contracts\Repository\ServerVariableRepositoryInterface */ - protected $serverVariableRepository; + private $serverVariableRepository; /** * @var \Pterodactyl\Contracts\Repository\UserRepositoryInterface */ - protected $userRepository; + private $userRepository; /** * @var \Pterodactyl\Services\Servers\VariableValidatorService */ - protected $validatorService; + private $validatorService; /** * CreationService constructor. @@ -116,33 +110,30 @@ class ServerCreationService public function create(array $data) { // @todo auto-deployment - $validator = $this->validatorService->isAdmin()->setFields($data['environment'])->validate($data['egg_id']); - $uniqueShort = str_random(8); $this->connection->beginTransaction(); - $server = $this->repository->create([ 'uuid' => Uuid::uuid4()->toString(), - 'uuidShort' => $uniqueShort, - 'node_id' => $data['node_id'], - 'name' => $data['name'], - 'description' => $data['description'], + 'uuidShort' => str_random(8), + 'node_id' => array_get($data, 'node_id'), + 'name' => array_get($data, 'name'), + 'description' => array_get($data, 'description'), 'skip_scripts' => isset($data['skip_scripts']), 'suspended' => false, - 'owner_id' => $data['owner_id'], - 'memory' => $data['memory'], - 'swap' => $data['swap'], - 'disk' => $data['disk'], - 'io' => $data['io'], - 'cpu' => $data['cpu'], + 'owner_id' => array_get($data, 'owner_id'), + 'memory' => array_get($data, 'memory'), + 'swap' => array_get($data, 'swap'), + 'disk' => array_get($data, 'disk'), + 'io' => array_get($data, 'io'), + 'cpu' => array_get($data, 'cpu'), 'oom_disabled' => isset($data['oom_disabled']), - 'allocation_id' => $data['allocation_id'], - 'nest_id' => $data['nest_id'], - 'egg_id' => $data['egg_id'], + 'allocation_id' => array_get($data, 'allocation_id'), + 'nest_id' => array_get($data, 'nest_id'), + 'egg_id' => array_get($data, 'egg_id'), 'pack_id' => (! isset($data['pack_id']) || $data['pack_id'] == 0) ? null : $data['pack_id'], - 'startup' => $data['startup'], - 'daemonSecret' => str_random(NodeCreationService::DAEMON_SECRET_LENGTH), - 'image' => $data['docker_image'], + 'startup' => array_get($data, 'startup'), + 'daemonSecret' => str_random(Node::DAEMON_SECRET_LENGTH), + 'image' => array_get($data, 'docker_image'), ]); // Process allocations and assign them to the server in the database. @@ -154,17 +145,21 @@ class ServerCreationService $this->allocationRepository->assignAllocationsToServer($server->id, $records); // Process the passed variables and store them in the database. - $records = []; - foreach ($validator->getResults() as $result) { - $records[] = [ - 'server_id' => $server->id, - 'variable_id' => $result['id'], - 'variable_value' => $result['value'], - ]; - } + $this->validatorService->setUserLevel(User::USER_LEVEL_ADMIN); + $results = $this->validatorService->handle(array_get($data, 'egg_id'), array_get($data, 'environment', [])); - $this->serverVariableRepository->insert($records); - $structure = $this->configurationStructureService->handle($server->id); + $records = $results->map(function ($result) use ($server) { + return [ + 'server_id' => $server->id, + 'variable_id' => $result->id, + 'variable_value' => $result->value, + ]; + })->toArray(); + + if (! empty($records)) { + $this->serverVariableRepository->insert($records); + } + $structure = $this->configurationStructureService->handle($server); // Create the server on the daemon & commit it to the database. try { diff --git a/app/Services/Servers/StartupModificationService.php b/app/Services/Servers/StartupModificationService.php index 78460c197..ae91fb3ba 100644 --- a/app/Services/Servers/StartupModificationService.php +++ b/app/Services/Servers/StartupModificationService.php @@ -1,17 +1,12 @@ . - * - * This software is licensed under the terms of the MIT license. - * https://opensource.org/licenses/MIT - */ namespace Pterodactyl\Services\Servers; +use Pterodactyl\Models\User; use Pterodactyl\Models\Server; use GuzzleHttp\Exception\RequestException; use Illuminate\Database\ConnectionInterface; +use Pterodactyl\Traits\Services\HasUserLevels; use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; use Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException; use Pterodactyl\Contracts\Repository\ServerVariableRepositoryInterface; @@ -19,40 +14,37 @@ use Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface as DaemonS class StartupModificationService { - /** - * @var bool - */ - protected $admin = false; + use HasUserLevels; /** * @var \Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface */ - protected $daemonServerRepository; + private $daemonServerRepository; /** * @var \Illuminate\Database\ConnectionInterface */ - protected $connection; + private $connection; /** * @var \Pterodactyl\Services\Servers\EnvironmentService */ - protected $environmentService; + private $environmentService; /** * @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface */ - protected $repository; + private $repository; /** * @var \Pterodactyl\Contracts\Repository\ServerVariableRepositoryInterface */ - protected $serverVariableRepository; + private $serverVariableRepository; /** * @var \Pterodactyl\Services\Servers\VariableValidatorService */ - protected $validatorService; + private $validatorService; /** * StartupModificationService constructor. @@ -80,81 +72,39 @@ class StartupModificationService $this->validatorService = $validatorService; } - /** - * Determine if this function should run at an administrative level. - * - * @param bool $bool - * @return $this - */ - public function isAdmin($bool = true) - { - $this->admin = $bool; - - return $this; - } - /** * Process startup modification for a server. * - * @param int|\Pterodactyl\Models\Server $server - * @param array $data + * @param \Pterodactyl\Models\Server $server + * @param array $data * * @throws \Pterodactyl\Exceptions\DisplayException * @throws \Pterodactyl\Exceptions\Model\DataValidationException * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException */ - public function handle($server, array $data) + public function handle(Server $server, array $data) { - if (! $server instanceof Server) { - $server = $this->repository->find($server); - } - - if ( - $server->nest_id != array_get($data, 'nest_id', $server->nest_id) || - $server->egg_id != array_get($data, 'egg_id', $server->egg_id) || - $server->pack_id != array_get($data, 'pack_id', $server->pack_id) - ) { - $hasServiceChanges = true; - } - $this->connection->beginTransaction(); if (! is_null(array_get($data, 'environment'))) { - $validator = $this->validatorService->isAdmin($this->admin) - ->setFields(array_get($data, 'environment', [])) - ->validate(array_get($data, 'egg_id', $server->egg_id)); + $this->validatorService->setUserLevel($this->getUserLevel()); + $results = $this->validatorService->handle(array_get($data, 'egg_id', $server->egg_id), array_get($data, 'environment', [])); - foreach ($validator->getResults() as $result) { + $results->each(function ($result) use ($server) { $this->serverVariableRepository->withoutFresh()->updateOrCreate([ 'server_id' => $server->id, - 'variable_id' => $result['id'], + 'variable_id' => $result->id, ], [ - 'variable_value' => $result['value'], + 'variable_value' => $result->value, ]); - } + }); } - $daemonData = [ - 'build' => [ - 'env|overwrite' => $this->environmentService->process($server), - ], - ]; + $daemonData = ['build' => [ + 'env|overwrite' => $this->environmentService->handle($server), + ]]; - if ($this->admin) { - $server = $this->repository->update($server->id, [ - 'installed' => 0, - 'startup' => array_get($data, 'startup', $server->startup), - 'nest_id' => array_get($data, 'nest_id', $server->nest_id), - 'egg_id' => array_get($data, 'egg_id', $server->egg_id), - 'pack_id' => array_get($data, 'pack_id', $server->pack_id) > 0 ? array_get($data, 'pack_id', $server->pack_id) : null, - 'skip_scripts' => isset($data['skip_scripts']), - ]); - - if (isset($hasServiceChanges)) { - $daemonData['service'] = array_merge( - $this->repository->withColumns(['id', 'egg_id', 'pack_id'])->getDaemonServiceData($server->id), - ['skip_scripts' => isset($data['skip_scripts'])] - ); - } + if ($this->isUserLevel(User::USER_LEVEL_ADMIN)) { + $this->updateAdministrativeSettings($data, $server, $daemonData); } try { @@ -166,4 +116,37 @@ class StartupModificationService $this->connection->commit(); } + + /** + * Update certain administrative settings for a server in the DB. + * + * @param array $data + * @param \Pterodactyl\Models\Server $server + * @param array $daemonData + * + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + */ + private function updateAdministrativeSettings(array $data, Server &$server, array &$daemonData) + { + $server = $this->repository->update($server->id, [ + 'installed' => 0, + 'startup' => array_get($data, 'startup', $server->startup), + 'nest_id' => array_get($data, 'nest_id', $server->nest_id), + 'egg_id' => array_get($data, 'egg_id', $server->egg_id), + 'pack_id' => array_get($data, 'pack_id', $server->pack_id) > 0 ? array_get($data, 'pack_id', $server->pack_id) : null, + 'skip_scripts' => isset($data['skip_scripts']), + ]); + + if ( + $server->nest_id != array_get($data, 'nest_id', $server->nest_id) || + $server->egg_id != array_get($data, 'egg_id', $server->egg_id) || + $server->pack_id != array_get($data, 'pack_id', $server->pack_id) + ) { + $daemonData['service'] = array_merge( + $this->repository->withColumns(['id', 'egg_id', 'pack_id'])->getDaemonServiceData($server->id), + ['skip_scripts' => isset($data['skip_scripts'])] + ); + } + } } diff --git a/app/Services/Servers/VariableValidatorService.php b/app/Services/Servers/VariableValidatorService.php index 7340d2f7b..b4f722c79 100644 --- a/app/Services/Servers/VariableValidatorService.php +++ b/app/Services/Servers/VariableValidatorService.php @@ -9,6 +9,9 @@ namespace Pterodactyl\Services\Servers; +use Pterodactyl\Models\User; +use Illuminate\Support\Collection; +use Pterodactyl\Traits\Services\HasUserLevels; use Pterodactyl\Exceptions\DisplayValidationException; use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; use Illuminate\Contracts\Validation\Factory as ValidationFactory; @@ -17,20 +20,7 @@ use Pterodactyl\Contracts\Repository\ServerVariableRepositoryInterface; class VariableValidatorService { - /** - * @var bool - */ - protected $isAdmin = false; - - /** - * @var array - */ - protected $fields = []; - - /** - * @var array - */ - protected $results = []; + use HasUserLevels; /** * @var \Pterodactyl\Contracts\Repository\EggVariableRepositoryInterface @@ -72,56 +62,26 @@ class VariableValidatorService $this->validator = $validator; } - /** - * Set the fields with populated data to validate. - * - * @param array $fields - * @return $this - */ - public function setFields(array $fields) - { - $this->fields = $fields; - - return $this; - } - - /** - * Set this function to be running at the administrative level. - * - * @param bool $bool - * @return $this - */ - public function isAdmin($bool = true) - { - $this->isAdmin = $bool; - - return $this; - } - /** * Validate all of the passed data aganist the given service option variables. * - * @param int $option - * @return $this + * @param int $egg + * @param array $fields + * @return \Illuminate\Support\Collection */ - public function validate($option) + public function handle(int $egg, array $fields = []): Collection { - $variables = $this->optionVariableRepository->findWhere([['egg_id', '=', $option]]); - if (count($variables) === 0) { - $this->results = []; + $variables = $this->optionVariableRepository->findWhere([['egg_id', '=', $egg]]); - return $this; - } - - $variables->each(function ($item) { - // Skip doing anything if user is not an admin and variable is not user viewable - // or editable. - if (! $this->isAdmin && (! $item->user_editable || ! $item->user_viewable)) { - return; + return $variables->map(function ($item) use ($fields) { + // Skip doing anything if user is not an admin and + // variable is not user viewable or editable. + if (! $this->isUserLevel(User::USER_LEVEL_ADMIN) && (! $item->user_editable || ! $item->user_viewable)) { + return false; } $validator = $this->validator->make([ - 'variable_value' => array_key_exists($item->env_variable, $this->fields) ? $this->fields[$item->env_variable] : null, + 'variable_value' => array_get($fields, $item->env_variable), ], [ 'variable_value' => $item->rules, ]); @@ -136,23 +96,13 @@ class VariableValidatorService )); } - $this->results[] = [ + return (object) [ 'id' => $item->id, 'key' => $item->env_variable, - 'value' => $this->fields[$item->env_variable], + 'value' => array_get($fields, $item->env_variable), ]; + })->filter(function ($item) { + return is_object($item); }); - - return $this; - } - - /** - * Return the final results after everything has been validated. - * - * @return array - */ - public function getResults() - { - return $this->results; } } diff --git a/app/Traits/Services/HasUserLevels.php b/app/Traits/Services/HasUserLevels.php new file mode 100644 index 000000000..d2d95e233 --- /dev/null +++ b/app/Traits/Services/HasUserLevels.php @@ -0,0 +1,44 @@ +userLevel = $level; + } + + /** + * Determine which level this function is running at. + * + * @return int + */ + public function getUserLevel(): int + { + return $this->userLevel; + } + + /** + * Determine if the current user level is set to a specific level. + * + * @param int $level + * @return bool + */ + public function isUserLevel(int $level): bool + { + return $this->getUserLevel() === $level; + } +} diff --git a/config/pterodactyl.php b/config/pterodactyl.php index 2bf1b2c8a..f232d8a1e 100644 --- a/config/pterodactyl.php +++ b/config/pterodactyl.php @@ -184,4 +184,19 @@ return [ 'daemon/*', 'remote/*', ], + + /* + |-------------------------------------------------------------------------- + | Dynamic Environment Variables + |-------------------------------------------------------------------------- + | + | Place dynamic environment variables here that should be auto-appended + | to server environment fields when the server is created or updated. + | + | Items should be in 'key' => 'value' format, where key is the environment + | variable name, and value is the server-object key. For example: + | + | 'P_SERVER_CREATED_AT' => 'created_at' + */ + 'environment_variables' => [], ]; diff --git a/database/factories/ModelFactory.php b/database/factories/ModelFactory.php index 233aeee01..54ba984be 100644 --- a/database/factories/ModelFactory.php +++ b/database/factories/ModelFactory.php @@ -29,6 +29,9 @@ $factory->define(Pterodactyl\Models\Server::class, function (Faker\Generator $fa 'io' => 500, 'cpu' => 0, 'oom_disabled' => 0, + 'allocation_id' => $faker->randomNumber(), + 'nest_id' => $faker->randomNumber(), + 'egg_id' => $faker->randomNumber(), 'pack_id' => null, 'installed' => 1, 'created_at' => \Carbon\Carbon::now(), diff --git a/tests/Unit/Services/Servers/EnvironmentServiceTest.php b/tests/Unit/Services/Servers/EnvironmentServiceTest.php index f0ff4fef7..435eb9bfb 100644 --- a/tests/Unit/Services/Servers/EnvironmentServiceTest.php +++ b/tests/Unit/Services/Servers/EnvironmentServiceTest.php @@ -1,11 +1,4 @@ . - * - * This software is licensed under the terms of the MIT license. - * https://opensource.org/licenses/MIT - */ namespace Tests\Unit\Services\Servers; @@ -13,25 +6,23 @@ use Mockery as m; use Tests\TestCase; use Pterodactyl\Models\Server; use Pterodactyl\Models\Location; +use Illuminate\Contracts\Config\Repository; use Pterodactyl\Services\Servers\EnvironmentService; use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; class EnvironmentServiceTest extends TestCase { - /** - * @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface - */ - protected $repository; + const CONFIG_MAPPING = 'pterodactyl.environment_mappings'; /** - * @var \Pterodactyl\Services\Servers\EnvironmentService + * @var \Illuminate\Contracts\Config\Repository|\Mockery\Mock */ - protected $service; + private $config; /** - * @var \Pterodactyl\Models\Server + * @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface|\Mockery\Mock */ - protected $server; + private $repository; /** * Setup tests. @@ -40,24 +31,23 @@ class EnvironmentServiceTest extends TestCase { parent::setUp(); + $this->config = m::mock(Repository::class); $this->repository = m::mock(ServerRepositoryInterface::class); - $this->server = factory(Server::class)->make([ - 'location' => factory(Location::class)->make(), - ]); - - $this->service = new EnvironmentService($this->repository); } /** - * Test that set environment key function returns an instance of the class. + * Test that set environment key stores the key into a retreviable array. */ - public function testSettingEnvironmentKeyShouldReturnInstanceOfSelf() + public function testSettingEnvironmentKeyPersistsItInArray() { - $instance = $this->service->setEnvironmentKey('TEST_KEY', function () { + $service = $this->getService(); + + $service->setEnvironmentKey('TEST_KEY', function () { return true; }); - $this->assertInstanceOf(EnvironmentService::class, $instance); + $this->assertNotEmpty($service->getEnvironmentKeys()); + $this->assertArrayHasKey('TEST_KEY', $service->getEnvironmentKeys()); } /** @@ -65,22 +55,17 @@ class EnvironmentServiceTest extends TestCase */ public function testProcessShouldReturnDefaultEnvironmentVariablesForAServer() { - $this->repository->shouldReceive('getVariablesWithValues')->with($this->server->id)->once()->andReturn([ + $model = $this->getServerModel(); + $this->config->shouldReceive('get')->with(self::CONFIG_MAPPING, [])->once()->andReturn([]); + $this->repository->shouldReceive('getVariablesWithValues')->with($model->id)->once()->andReturn([ 'TEST_VARIABLE' => 'Test Variable', ]); - $response = $this->service->process($this->server); - - $this->assertEquals(count(EnvironmentService::ENVIRONMENT_CASTS) + 1, count($response), 'Assert response contains correct amount of items.'); - $this->assertTrue(is_array($response), 'Assert that response is an array.'); - + $response = $this->getService()->handle($model); + $this->assertNotEmpty($response); + $this->assertEquals(4, count($response)); $this->assertArrayHasKey('TEST_VARIABLE', $response); - $this->assertEquals('Test Variable', $response['TEST_VARIABLE']); - - foreach (EnvironmentService::ENVIRONMENT_CASTS as $key => $value) { - $this->assertArrayHasKey($key, $response); - $this->assertEquals(object_get($this->server, $value), $response[$key]); - } + $this->assertSame('Test Variable', $response['TEST_VARIABLE']); } /** @@ -88,43 +73,106 @@ class EnvironmentServiceTest extends TestCase */ public function testProcessShouldReturnKeySetAtRuntime() { - $this->repository->shouldReceive('getVariablesWithValues')->with($this->server->id)->once()->andReturn([]); + $model = $this->getServerModel(); + $this->config->shouldReceive('get')->with(self::CONFIG_MAPPING, [])->once()->andReturn([]); + $this->repository->shouldReceive('getVariablesWithValues')->with($model->id)->once()->andReturn([]); - $response = $this->service->setEnvironmentKey('TEST_VARIABLE', function ($server) { + $service = $this->getService(); + $service->setEnvironmentKey('TEST_VARIABLE', function ($server) { return $server->uuidShort; - })->process($this->server); + }); - $this->assertTrue(is_array($response), 'Assert response is an array.'); + $response = $service->handle($model); + + $this->assertNotEmpty($response); $this->assertArrayHasKey('TEST_VARIABLE', $response); - $this->assertEquals($this->server->uuidShort, $response['TEST_VARIABLE']); + $this->assertSame($model->uuidShort, $response['TEST_VARIABLE']); } /** - * Test that duplicate variables provided at run-time override the defaults. + * Test that duplicate variables provided in config override the defaults. + */ + public function testProcessShouldAllowOverwritingVaraiblesWithConfigurationFile() + { + $model = $this->getServerModel(); + $this->repository->shouldReceive('getVariablesWithValues')->with($model->id)->once()->andReturn([]); + $this->config->shouldReceive('get')->with(self::CONFIG_MAPPING, [])->once()->andReturn([ + 'P_SERVER_UUID' => 'name', + ]); + + $response = $this->getService()->handle($model); + + $this->assertNotEmpty($response); + $this->assertSame(3, count($response)); + $this->assertArrayHasKey('P_SERVER_UUID', $response); + $this->assertSame($model->name, $response['P_SERVER_UUID']); + } + + /** + * Test that config based environment variables can be done using closures. + */ + public function testVariablesSetInConfigurationAllowForClosures() + { + $model = $this->getServerModel(); + $this->config->shouldReceive('get')->with(self::CONFIG_MAPPING, [])->once()->andReturn([ + 'P_SERVER_UUID' => function ($server) { + return $server->id * 2; + }, + ]); + $this->repository->shouldReceive('getVariablesWithValues')->with($model->id)->once()->andReturn([]); + + $response = $this->getService()->handle($model); + + $this->assertNotEmpty($response); + $this->assertSame(3, count($response)); + $this->assertArrayHasKey('P_SERVER_UUID', $response); + $this->assertSame($model->id * 2, $response['P_SERVER_UUID']); + } + + /** + * Test that duplicate variables provided at run-time override the defaults and those + * that are defined in the configuration file. */ public function testProcessShouldAllowOverwritingDefaultVariablesWithRuntimeProvided() { - $this->repository->shouldReceive('getVariablesWithValues')->with($this->server->id)->once()->andReturn([]); + $model = $this->getServerModel(); + $this->config->shouldReceive('get')->with(self::CONFIG_MAPPING, [])->once()->andReturn([ + 'P_SERVER_UUID' => 'overwritten-config', + ]); + $this->repository->shouldReceive('getVariablesWithValues')->with($model->id)->once()->andReturn([]); - $response = $this->service->setEnvironmentKey('P_SERVER_UUID', function ($server) { + $service = $this->getService(); + $service->setEnvironmentKey('P_SERVER_UUID', function ($model) { return 'overwritten'; - })->process($this->server); + }); - $this->assertTrue(is_array($response), 'Assert response is an array.'); + $response = $service->handle($model); + + $this->assertNotEmpty($response); + $this->assertSame(3, count($response)); $this->assertArrayHasKey('P_SERVER_UUID', $response); - $this->assertEquals('overwritten', $response['P_SERVER_UUID']); + $this->assertSame('overwritten', $response['P_SERVER_UUID']); } /** - * Test that function can run when an ID is provided rather than a server model. + * Return an instance of the service with mocked dependencies. + * + * @return \Pterodactyl\Services\Servers\EnvironmentService */ - public function testProcessShouldAcceptAnIntegerInPlaceOfAServerModel() + private function getService(): EnvironmentService { - $this->repository->shouldReceive('find')->with($this->server->id)->once()->andReturn($this->server); - $this->repository->shouldReceive('getVariablesWithValues')->with($this->server->id)->once()->andReturn([]); + return new EnvironmentService($this->config, $this->repository); + } - $response = $this->service->process($this->server->id); - - $this->assertTrue(is_array($response), 'Assert that response is an array.'); + /** + * Return a server model with a location relationship to be used in the tests. + * + * @return \Pterodactyl\Models\Server + */ + private function getServerModel(): Server + { + return factory(Server::class)->make([ + 'location' => factory(Location::class)->make(), + ]); } } diff --git a/tests/Unit/Services/Servers/ServerConfigurationStructureServiceTest.php b/tests/Unit/Services/Servers/ServerConfigurationStructureServiceTest.php index e3b51693a..ceca81758 100644 --- a/tests/Unit/Services/Servers/ServerConfigurationStructureServiceTest.php +++ b/tests/Unit/Services/Servers/ServerConfigurationStructureServiceTest.php @@ -50,7 +50,7 @@ class ServerConfigurationStructureServiceTest extends TestCase })->toArray(); $this->repository->shouldReceive('getDataForCreation')->with($model)->once()->andReturn($model); - $this->environment->shouldReceive('process')->with($model)->once()->andReturn(['environment_array']); + $this->environment->shouldReceive('handle')->with($model)->once()->andReturn(['environment_array']); $response = $this->getService()->handle($model); $this->assertNotEmpty($response); diff --git a/tests/Unit/Services/Servers/ServerCreationServiceTest.php b/tests/Unit/Services/Servers/ServerCreationServiceTest.php index 89e67d916..c430cc22b 100644 --- a/tests/Unit/Services/Servers/ServerCreationServiceTest.php +++ b/tests/Unit/Services/Servers/ServerCreationServiceTest.php @@ -1,18 +1,13 @@ . - * - * This software is licensed under the terms of the MIT license. - * https://opensource.org/licenses/MIT - */ namespace Tests\Unit\Services\Servers; use Mockery as m; use Tests\TestCase; -use phpmock\phpunit\PHPMock; +use Pterodactyl\Models\User; use Tests\Traits\MocksUuids; +use Pterodactyl\Models\Server; +use Tests\Traits\MocksRequestException; use GuzzleHttp\Exception\RequestException; use Illuminate\Database\ConnectionInterface; use Pterodactyl\Exceptions\PterodactylException; @@ -32,86 +27,57 @@ use Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface as DaemonS */ class ServerCreationServiceTest extends TestCase { - use MocksUuids, PHPMock; + use MocksRequestException, MocksUuids; /** * @var \Pterodactyl\Contracts\Repository\AllocationRepositoryInterface|\Mockery\Mock */ - protected $allocationRepository; + private $allocationRepository; /** * @var \Pterodactyl\Services\Servers\ServerConfigurationStructureService|\Mockery\Mock */ - protected $configurationStructureService; + private $configurationStructureService; /** * @var \Illuminate\Database\ConnectionInterface|\Mockery\Mock */ - protected $connection; + private $connection; /** * @var \Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface|\Mockery\Mock */ - protected $daemonServerRepository; - - /** - * @var array - */ - protected $data = [ - 'node_id' => 1, - 'name' => 'SomeName', - 'description' => null, - 'owner_id' => 1, - 'memory' => 128, - 'disk' => 128, - 'swap' => 0, - 'io' => 500, - 'cpu' => 0, - 'allocation_id' => 1, - 'allocation_additional' => [2, 3], - 'environment' => [ - 'TEST_VAR_1' => 'var1-value', - ], - 'nest_id' => 1, - 'egg_id' => 1, - 'startup' => 'startup-param', - 'docker_image' => 'some/image', - ]; + private $daemonServerRepository; /** * @var \GuzzleHttp\Exception\RequestException|\Mockery\Mock */ - protected $exception; + private $exception; /** * @var \Pterodactyl\Contracts\Repository\NodeRepositoryInterface|\Mockery\Mock */ - protected $nodeRepository; + private $nodeRepository; /** * @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface|\Mockery\Mock */ - protected $repository; + private $repository; /** * @var \Pterodactyl\Contracts\Repository\ServerVariableRepositoryInterface|\Mockery\Mock */ - protected $serverVariableRepository; - - /** - * @var \Pterodactyl\Services\Servers\ServerCreationService - */ - protected $service; + private $serverVariableRepository; /** * @var \Pterodactyl\Contracts\Repository\UserRepositoryInterface|\Mockery\Mock */ - protected $userRepository; + private $userRepository; /** * @var \Pterodactyl\Services\Servers\VariableValidatorService|\Mockery\Mock */ - protected $validatorService; + private $validatorService; /** * Setup tests. @@ -130,11 +96,87 @@ class ServerCreationServiceTest extends TestCase $this->serverVariableRepository = m::mock(ServerVariableRepositoryInterface::class); $this->userRepository = m::mock(UserRepositoryInterface::class); $this->validatorService = m::mock(VariableValidatorService::class); + } - $this->getFunctionMock('\\Pterodactyl\\Services\\Servers', 'str_random') - ->expects($this->any())->willReturn('random_string'); + /** + * Test core functionality of the creation process. + */ + public function testCreateShouldHitAllOfTheNecessaryServicesAndStoreTheServer() + { + $model = factory(Server::class)->make([ + 'uuid' => $this->getKnownUuid(), + ]); - $this->service = new ServerCreationService( + $this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); + $this->repository->shouldReceive('create')->with(m::subset([ + 'uuid' => $this->getKnownUuid(), + 'node_id' => $model->node_id, + 'owner_id' => $model->owner_id, + 'nest_id' => $model->nest_id, + 'egg_id' => $model->egg_id, + ]))->once()->andReturn($model); + + $this->allocationRepository->shouldReceive('assignAllocationsToServer')->with($model->id, [$model->allocation_id])->once()->andReturnNull(); + + $this->validatorService->shouldReceive('setUserLevel')->with(User::USER_LEVEL_ADMIN)->once()->andReturnNull(); + $this->validatorService->shouldReceive('handle')->with($model->egg_id, [])->once()->andReturn( + collect([(object) ['id' => 123, 'value' => 'var1-value']]) + ); + + $this->serverVariableRepository->shouldReceive('insert')->with([ + [ + 'server_id' => $model->id, + 'variable_id' => 123, + 'variable_value' => 'var1-value', + ], + ])->once()->andReturnNull(); + $this->configurationStructureService->shouldReceive('handle')->with($model)->once()->andReturn(['test' => 'struct']); + + $this->daemonServerRepository->shouldReceive('setNode')->with($model->node_id)->once()->andReturnSelf(); + $this->daemonServerRepository->shouldReceive('create')->with(['test' => 'struct'], ['start_on_completion' => false])->once()->andReturnNull(); + $this->connection->shouldReceive('commit')->withNoArgs()->once()->andReturnNull(); + + $response = $this->getService()->create($model->toArray()); + + $this->assertSame($model, $response); + } + + /** + * Test handling of node timeout or other daemon error. + */ + public function testExceptionShouldBeThrownIfTheRequestFails() + { + $this->configureExceptionMock(); + + $model = factory(Server::class)->make([ + 'uuid' => $this->getKnownUuid(), + ]); + + $this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); + $this->repository->shouldReceive('create')->once()->andReturn($model); + $this->allocationRepository->shouldReceive('assignAllocationsToServer')->once()->andReturnNull(); + $this->validatorService->shouldReceive('setUserLevel')->once()->andReturnNull(); + $this->validatorService->shouldReceive('handle')->once()->andReturn(collect([])); + $this->configurationStructureService->shouldReceive('handle')->once()->andReturn([]); + $this->daemonServerRepository->shouldReceive('setNode')->with($model->node_id)->once()->andThrow($this->exception); + $this->connection->shouldReceive('rollBack')->withNoArgs()->once()->andReturnNull(); + + try { + $this->getService()->create($model->toArray()); + } catch (PterodactylException $exception) { + $this->assertInstanceOf(DaemonConnectionException::class, $exception); + $this->assertInstanceOf(RequestException::class, $exception->getPrevious()); + } + } + + /** + * Return an instance of the service with mocked dependencies. + * + * @return \Pterodactyl\Services\Servers\ServerCreationService + */ + private function getService(): ServerCreationService + { + return new ServerCreationService( $this->allocationRepository, $this->connection, $this->daemonServerRepository, @@ -146,77 +188,4 @@ class ServerCreationServiceTest extends TestCase $this->validatorService ); } - - /** - * Test core functionality of the creation process. - */ - public function testCreateShouldHitAllOfTheNecessaryServicesAndStoreTheServer() - { - $this->validatorService->shouldReceive('isAdmin')->withNoArgs()->once()->andReturnSelf() - ->shouldReceive('setFields')->with($this->data['environment'])->once()->andReturnSelf() - ->shouldReceive('validate')->with($this->data['egg_id'])->once()->andReturnSelf(); - - $this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); - - $this->repository->shouldReceive('create')->with(m::subset([ - 'uuid' => $this->getKnownUuid(), - 'node_id' => $this->data['node_id'], - 'owner_id' => 1, - 'nest_id' => 1, - 'egg_id' => 1, - ]))->once()->andReturn((object) [ - 'node_id' => 1, - 'id' => 1, - ]); - - $this->allocationRepository->shouldReceive('assignAllocationsToServer')->with(1, [1, 2, 3])->once()->andReturnNull(); - $this->validatorService->shouldReceive('getResults')->withNoArgs()->once()->andReturn([[ - 'id' => 1, - 'key' => 'TEST_VAR_1', - 'value' => 'var1-value', - ]]); - - $this->serverVariableRepository->shouldReceive('insert')->with([[ - 'server_id' => 1, - 'variable_id' => 1, - 'variable_value' => 'var1-value', - ]])->once()->andReturnNull(); - - $this->configurationStructureService->shouldReceive('handle')->with(1)->once()->andReturn(['test' => 'struct']); - - $this->daemonServerRepository->shouldReceive('setNode')->with(1)->once()->andReturnSelf() - ->shouldReceive('create')->with(['test' => 'struct'], ['start_on_completion' => false])->once()->andReturnNull(); - $this->connection->shouldReceive('commit')->withNoArgs()->once()->andReturnNull(); - - $response = $this->service->create($this->data); - - $this->assertEquals(1, $response->id); - $this->assertEquals(1, $response->node_id); - } - - /** - * Test handling of node timeout or other daemon error. - */ - public function testExceptionShouldBeThrownIfTheRequestFails() - { - $this->validatorService->shouldReceive('isAdmin->setFields->validate->getResults')->once()->andReturn([]); - $this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); - $this->repository->shouldReceive('create')->once()->andReturn((object) [ - 'node_id' => 1, - 'id' => 1, - ]); - - $this->allocationRepository->shouldReceive('assignAllocationsToServer')->once()->andReturnNull(); - $this->serverVariableRepository->shouldReceive('insert')->with([])->once()->andReturnNull(); - $this->configurationStructureService->shouldReceive('handle')->once()->andReturnNull(); - $this->daemonServerRepository->shouldReceive('setNode->create')->once()->andThrow($this->exception); - $this->exception->shouldReceive('getResponse')->withNoArgs()->once()->andReturnNull(); - $this->connection->shouldReceive('rollBack')->withNoArgs()->once()->andReturnNull(); - - try { - $this->service->create($this->data); - } catch (PterodactylException $exception) { - $this->assertInstanceOf(DaemonConnectionException::class, $exception); - } - } } diff --git a/tests/Unit/Services/Servers/StartupModificationServiceTest.php b/tests/Unit/Services/Servers/StartupModificationServiceTest.php index d35ad551b..5d8076ae8 100644 --- a/tests/Unit/Services/Servers/StartupModificationServiceTest.php +++ b/tests/Unit/Services/Servers/StartupModificationServiceTest.php @@ -11,6 +11,7 @@ namespace Tests\Unit\Services\Servers; use Mockery as m; use Tests\TestCase; +use Pterodactyl\Models\User; use Pterodactyl\Models\Server; use Illuminate\Database\ConnectionInterface; use Pterodactyl\Services\Servers\EnvironmentService; @@ -25,37 +26,32 @@ class StartupModificationServiceTest extends TestCase /** * @var \Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface|\Mockery\Mock */ - protected $daemonServerRepository; + private $daemonServerRepository; /** * @var \Illuminate\Database\ConnectionInterface|\Mockery\Mock */ - protected $connection; + private $connection; /** * @var \Pterodactyl\Services\Servers\EnvironmentService|\Mockery\Mock */ - protected $environmentService; + private $environmentService; /** * @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface|\Mockery\Mock */ - protected $repository; + private $repository; /** * @var \Pterodactyl\Contracts\Repository\ServerVariableRepositoryInterface|\Mockery\Mock */ - protected $serverVariableRepository; - - /** - * @var \Pterodactyl\Services\Servers\StartupModificationService - */ - protected $service; + private $serverVariableRepository; /** * @var \Pterodactyl\Services\Servers\VariableValidatorService|\Mockery\Mock */ - protected $validatorService; + private $validatorService; /** * Setup tests. @@ -70,8 +66,97 @@ class StartupModificationServiceTest extends TestCase $this->repository = m::mock(ServerRepositoryInterface::class); $this->serverVariableRepository = m::mock(ServerVariableRepositoryInterface::class); $this->validatorService = m::mock(VariableValidatorService::class); + } - $this->service = new StartupModificationService( + /** + * Test startup modification as a non-admin user. + */ + public function testStartupModifiedAsNormalUser() + { + $model = factory(Server::class)->make(); + + $this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); + $this->validatorService->shouldReceive('setUserLevel')->with(User::USER_LEVEL_USER)->once()->andReturnNull(); + $this->validatorService->shouldReceive('handle')->with(123, ['test' => 'abcd1234'])->once()->andReturn( + collect([(object) ['id' => 1, 'value' => 'stored-value']]) + ); + + $this->serverVariableRepository->shouldReceive('withoutFresh')->withNoArgs()->once()->andReturnSelf(); + $this->serverVariableRepository->shouldReceive('updateOrCreate')->with([ + 'server_id' => $model->id, + 'variable_id' => 1, + ], ['variable_value' => 'stored-value'])->once()->andReturnNull(); + + $this->environmentService->shouldReceive('handle')->with($model)->once()->andReturn(['env']); + $this->daemonServerRepository->shouldReceive('setNode')->with($model->node_id)->once()->andReturnSelf(); + $this->daemonServerRepository->shouldReceive('setAccessServer')->with($model->uuid)->once()->andReturnSelf(); + $this->daemonServerRepository->shouldReceive('update')->with([ + 'build' => ['env|overwrite' => ['env']], + ])->once()->andReturnSelf(); + + $this->connection->shouldReceive('commit')->withNoArgs()->once()->andReturnNull(); + + $this->getService()->handle($model, ['egg_id' => 123, 'environment' => ['test' => 'abcd1234']]); + $this->assertTrue(true); + } + + /** + * Test startup modification as an admin user. + */ + public function testStartupModificationAsAdminUser() + { + $model = factory(Server::class)->make([ + 'egg_id' => 123, + ]); + + $this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); + $this->validatorService->shouldReceive('setUserLevel')->with(User::USER_LEVEL_ADMIN)->once()->andReturnNull(); + $this->validatorService->shouldReceive('handle')->with(456, ['test' => 'abcd1234'])->once()->andReturn( + collect([(object) ['id' => 1, 'value' => 'stored-value']]) + ); + + $this->serverVariableRepository->shouldReceive('withoutFresh')->withNoArgs()->once()->andReturnSelf(); + $this->serverVariableRepository->shouldReceive('updateOrCreate')->with([ + 'server_id' => $model->id, + 'variable_id' => 1, + ], ['variable_value' => 'stored-value'])->once()->andReturnNull(); + + $this->environmentService->shouldReceive('handle')->with($model)->once()->andReturn(['env']); + + $this->repository->shouldReceive('update')->with($model->id, m::subset([ + 'installed' => 0, + 'egg_id' => 456, + 'pack_id' => 789, + ]))->once()->andReturn($model); + $this->repository->shouldReceive('withColumns->getDaemonServiceData')->with($model->id)->once()->andReturn([]); + + $this->daemonServerRepository->shouldReceive('setNode')->with($model->node_id)->once()->andReturnSelf(); + $this->daemonServerRepository->shouldReceive('setAccessServer')->with($model->uuid)->once()->andReturnSelf(); + $this->daemonServerRepository->shouldReceive('update')->with([ + 'build' => [ + 'env|overwrite' => ['env'], + ], + 'service' => [ + 'skip_scripts' => false, + ], + ])->once()->andReturnSelf(); + + $this->connection->shouldReceive('commit')->withNoArgs()->once()->andReturnNull(); + + $service = $this->getService(); + $service->setUserLevel(User::USER_LEVEL_ADMIN); + $service->handle($model, ['egg_id' => 456, 'pack_id' => 789, 'environment' => ['test' => 'abcd1234']]); + $this->assertTrue(true); + } + + /** + * Return an instance of the service with mocked dependencies. + * + * @return \Pterodactyl\Services\Servers\StartupModificationService + */ + private function getService(): StartupModificationService + { + return new StartupModificationService( $this->connection, $this->daemonServerRepository, $this->environmentService, @@ -80,16 +165,4 @@ class StartupModificationServiceTest extends TestCase $this->validatorService ); } - - /** - * Test startup is modified when user is not an administrator. - * - * @todo this test works, but not for the right reasons... - */ - public function testStartupIsModifiedAsNonAdmin() - { - $model = factory(Server::class)->make(); - - $this->assertTrue(true); - } } diff --git a/tests/Unit/Services/Servers/VariableValidatorServiceTest.php b/tests/Unit/Services/Servers/VariableValidatorServiceTest.php index ce2eaf7d8..b949b3ae8 100644 --- a/tests/Unit/Services/Servers/VariableValidatorServiceTest.php +++ b/tests/Unit/Services/Servers/VariableValidatorServiceTest.php @@ -11,8 +11,11 @@ namespace Tests\Unit\Services\Servers; use Mockery as m; use Tests\TestCase; +use Pterodactyl\Models\User; +use Illuminate\Support\Collection; use Pterodactyl\Models\EggVariable; use Illuminate\Contracts\Validation\Factory; +use Pterodactyl\Exceptions\PterodactylException; use Pterodactyl\Exceptions\DisplayValidationException; use Pterodactyl\Services\Servers\VariableValidatorService; use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; @@ -22,35 +25,25 @@ use Pterodactyl\Contracts\Repository\ServerVariableRepositoryInterface; class VariableValidatorServiceTest extends TestCase { /** - * @var \Pterodactyl\Contracts\Repository\EggVariableRepositoryInterface + * @var \Pterodactyl\Contracts\Repository\EggVariableRepositoryInterface|\Mockery\Mock */ protected $optionVariableRepository; /** - * @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface + * @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface|\Mockery\Mock */ protected $serverRepository; /** - * @var \Pterodactyl\Contracts\Repository\ServerVariableRepositoryInterface + * @var \Pterodactyl\Contracts\Repository\ServerVariableRepositoryInterface|\Mockery\Mock */ protected $serverVariableRepository; /** - * @var \Pterodactyl\Services\Servers\VariableValidatorService - */ - protected $service; - - /** - * @var \Illuminate\Validation\Factory + * @var \Illuminate\Contracts\Validation\Factory|\Mockery\Mock */ protected $validator; - /** - * @var \Illuminate\Support\Collection - */ - protected $variables; - /** * Setup tests. */ @@ -58,56 +51,10 @@ class VariableValidatorServiceTest extends TestCase { parent::setUp(); - $this->variables = collect( - [ - factory(EggVariable::class)->states('editable', 'viewable')->make(), - factory(EggVariable::class)->states('viewable')->make(), - factory(EggVariable::class)->states('editable')->make(), - factory(EggVariable::class)->make(), - ] - ); - $this->optionVariableRepository = m::mock(EggVariableRepositoryInterface::class); $this->serverRepository = m::mock(ServerRepositoryInterface::class); $this->serverVariableRepository = m::mock(ServerVariableRepositoryInterface::class); $this->validator = m::mock(Factory::class); - - $this->service = new VariableValidatorService( - $this->optionVariableRepository, - $this->serverRepository, - $this->serverVariableRepository, - $this->validator - ); - } - - /** - * Test that setting fields returns an instance of the class. - */ - public function testSettingFieldsShouldReturnInstanceOfSelf() - { - $response = $this->service->setFields([]); - - $this->assertInstanceOf(VariableValidatorService::class, $response); - } - - /** - * Test that setting administrator value returns an instance of the class. - */ - public function testSettingAdminShouldReturnInstanceOfSelf() - { - $response = $this->service->isAdmin(); - - $this->assertInstanceOf(VariableValidatorService::class, $response); - } - - /** - * Test that getting the results returns an array of values. - */ - public function testGettingResultsReturnsAnArrayOfValues() - { - $response = $this->service->getResults(); - - $this->assertTrue(is_array($response)); } /** @@ -115,13 +62,11 @@ class VariableValidatorServiceTest extends TestCase */ public function testEmptyResultSetShouldBeReturnedIfNoVariablesAreFound() { - $this->optionVariableRepository->shouldReceive('findWhere')->with([['egg_id', '=', 1]])->andReturn([]); + $this->optionVariableRepository->shouldReceive('findWhere')->with([['egg_id', '=', 1]])->andReturn(collect([])); - $response = $this->service->validate(1); - - $this->assertInstanceOf(VariableValidatorService::class, $response); - $this->assertTrue(is_array($response->getResults())); - $this->assertEmpty($response->getResults()); + $response = $this->getService()->handle(1, []); + $this->assertEmpty($response); + $this->assertInstanceOf(Collection::class, $response); } /** @@ -129,31 +74,34 @@ class VariableValidatorServiceTest extends TestCase */ public function testValidatorShouldNotProcessVariablesSetAsNotUserEditableWhenAdminFlagIsNotPassed() { - $this->optionVariableRepository->shouldReceive('findWhere')->with([['egg_id', '=', 1]])->andReturn($this->variables); + $variables = $this->getVariableCollection(); + $this->optionVariableRepository->shouldReceive('findWhere')->with([['egg_id', '=', 1]])->andReturn($variables); $this->validator->shouldReceive('make')->with([ 'variable_value' => 'Test_SomeValue_0', ], [ - 'variable_value' => $this->variables[0]->rules, - ])->once()->andReturnSelf() - ->shouldReceive('fails')->withNoArgs()->once()->andReturn(false); + 'variable_value' => $variables[0]->rules, + ])->once()->andReturnSelf(); + $this->validator->shouldReceive('fails')->withNoArgs()->once()->andReturn(false); - $response = $this->service->setFields([ - $this->variables[0]->env_variable => 'Test_SomeValue_0', - $this->variables[1]->env_variable => 'Test_SomeValue_1', - $this->variables[2]->env_variable => 'Test_SomeValue_2', - $this->variables[3]->env_variable => 'Test_SomeValue_3', - ])->validate(1)->getResults(); + $response = $this->getService()->handle(1, [ + $variables[0]->env_variable => 'Test_SomeValue_0', + $variables[1]->env_variable => 'Test_SomeValue_1', + $variables[2]->env_variable => 'Test_SomeValue_2', + $variables[3]->env_variable => 'Test_SomeValue_3', + ]); - $this->assertEquals(1, count($response), 'Assert response has a single item in array.'); - $this->assertArrayHasKey('0', $response); - $this->assertArrayHasKey('id', $response[0]); - $this->assertArrayHasKey('key', $response[0]); - $this->assertArrayHasKey('value', $response[0]); + $this->assertNotEmpty($response); + $this->assertInstanceOf(Collection::class, $response); + $this->assertEquals(1, $response->count(), 'Assert response has a single item in collection.'); - $this->assertEquals($this->variables[0]->id, $response[0]['id']); - $this->assertEquals($this->variables[0]->env_variable, $response[0]['key']); - $this->assertEquals('Test_SomeValue_0', $response[0]['value']); + $variable = $response->first(); + $this->assertObjectHasAttribute('id', $variable); + $this->assertObjectHasAttribute('key', $variable); + $this->assertObjectHasAttribute('value', $variable); + $this->assertSame($variables[0]->id, $variable->id); + $this->assertSame($variables[0]->env_variable, $variable->key); + $this->assertSame('Test_SomeValue_0', $variable->value); } /** @@ -161,36 +109,39 @@ class VariableValidatorServiceTest extends TestCase */ public function testValidatorShouldProcessAllVariablesWhenAdminFlagIsSet() { - $this->optionVariableRepository->shouldReceive('findWhere')->with([['egg_id', '=', 1]])->andReturn($this->variables); + $variables = $this->getVariableCollection(); + $this->optionVariableRepository->shouldReceive('findWhere')->with([['egg_id', '=', 1]])->andReturn($variables); - foreach ($this->variables as $key => $variable) { + foreach ($variables as $key => $variable) { $this->validator->shouldReceive('make')->with([ 'variable_value' => 'Test_SomeValue_' . $key, ], [ - 'variable_value' => $this->variables[$key]->rules, - ])->andReturnSelf() - ->shouldReceive('fails')->withNoArgs()->once()->andReturn(false); + 'variable_value' => $variables[$key]->rules, + ])->once()->andReturnSelf(); + $this->validator->shouldReceive('fails')->withNoArgs()->once()->andReturn(false); } - $response = $this->service->isAdmin()->setFields([ - $this->variables[0]->env_variable => 'Test_SomeValue_0', - $this->variables[1]->env_variable => 'Test_SomeValue_1', - $this->variables[2]->env_variable => 'Test_SomeValue_2', - $this->variables[3]->env_variable => 'Test_SomeValue_3', - ])->validate(1)->getResults(); + $service = $this->getService(); + $service->setUserLevel(User::USER_LEVEL_ADMIN); + $response = $service->handle(1, [ + $variables[0]->env_variable => 'Test_SomeValue_0', + $variables[1]->env_variable => 'Test_SomeValue_1', + $variables[2]->env_variable => 'Test_SomeValue_2', + $variables[3]->env_variable => 'Test_SomeValue_3', + ]); - $this->assertEquals(4, count($response), 'Assert response has all four items in array.'); + $this->assertNotEmpty($response); + $this->assertInstanceOf(Collection::class, $response); + $this->assertEquals(4, $response->count(), 'Assert response has all four items in collection.'); - foreach ($response as $key => $values) { - $this->assertArrayHasKey($key, $response); - $this->assertArrayHasKey('id', $response[$key]); - $this->assertArrayHasKey('key', $response[$key]); - $this->assertArrayHasKey('value', $response[$key]); - - $this->assertEquals($this->variables[$key]->id, $response[$key]['id']); - $this->assertEquals($this->variables[$key]->env_variable, $response[$key]['key']); - $this->assertEquals('Test_SomeValue_' . $key, $response[$key]['value']); - } + $response->each(function ($variable, $key) use ($variables) { + $this->assertObjectHasAttribute('id', $variable); + $this->assertObjectHasAttribute('key', $variable); + $this->assertObjectHasAttribute('value', $variable); + $this->assertSame($variables[$key]->id, $variable->id); + $this->assertSame($variables[$key]->env_variable, $variable->key); + $this->assertSame('Test_SomeValue_' . $key, $variable->value); + }); } /** @@ -198,31 +149,63 @@ class VariableValidatorServiceTest extends TestCase */ public function testValidatorShouldThrowExceptionWhenAValidationErrorIsEncountered() { - $this->optionVariableRepository->shouldReceive('findWhere')->with([['egg_id', '=', 1]])->andReturn($this->variables); + $variables = $this->getVariableCollection(); + $this->optionVariableRepository->shouldReceive('findWhere')->with([['egg_id', '=', 1]])->andReturn($variables); $this->validator->shouldReceive('make')->with([ 'variable_value' => null, ], [ - 'variable_value' => $this->variables[0]->rules, - ])->once()->andReturnSelf() - ->shouldReceive('fails')->withNoArgs()->once()->andReturn(true); + 'variable_value' => $variables[0]->rules, + ])->once()->andReturnSelf(); + $this->validator->shouldReceive('fails')->withNoArgs()->once()->andReturn(true); - $this->validator->shouldReceive('errors')->withNoArgs()->once()->andReturnSelf() - ->shouldReceive('toArray')->withNoArgs()->once()->andReturn([]); + $this->validator->shouldReceive('errors')->withNoArgs()->once()->andReturnSelf(); + $this->validator->shouldReceive('toArray')->withNoArgs()->once()->andReturn([]); try { - $this->service->setFields([ - $this->variables[0]->env_variable => null, - ])->validate(1); - } catch (DisplayValidationException $exception) { - $decoded = json_decode($exception->getMessage()); + $this->getService()->handle(1, [$variables[0]->env_variable => null]); + } catch (PterodactylException $exception) { + $this->assertInstanceOf(DisplayValidationException::class, $exception); + $decoded = json_decode($exception->getMessage()); $this->assertEquals(0, json_last_error(), 'Assert that response is decodable JSON.'); $this->assertObjectHasAttribute('notice', $decoded); $this->assertEquals( - trans('admin/server.exceptions.bad_variable', ['name' => $this->variables[0]->name]), + trans('admin/server.exceptions.bad_variable', ['name' => $variables[0]->name]), $decoded->notice[0] ); } } + + /** + * Return a collection of fake variables to use for testing. + * + * @return \Illuminate\Support\Collection + */ + private function getVariableCollection(): Collection + { + return collect( + [ + factory(EggVariable::class)->states('editable', 'viewable')->make(), + factory(EggVariable::class)->states('viewable')->make(), + factory(EggVariable::class)->states('editable')->make(), + factory(EggVariable::class)->make(), + ] + ); + } + + /** + * Return an instance of the service with mocked dependencies. + * + * @return \Pterodactyl\Services\Servers\VariableValidatorService + */ + private function getService(): VariableValidatorService + { + return new VariableValidatorService( + $this->optionVariableRepository, + $this->serverRepository, + $this->serverVariableRepository, + $this->validator + ); + } }