Merge branch 'develop' into pr/2454

This commit is contained in:
Dane Everitt 2020-10-11 12:15:53 -07:00
commit d8e3e0a5f7
No known key found for this signature in database
GPG key ID: EEA66103B3D71F53
85 changed files with 2338 additions and 3033 deletions

View file

@ -42,18 +42,6 @@ interface DatabaseRepositoryInterface extends RepositoryInterface
*/ */
public function getDatabasesForHost(int $host, int $count = 25): LengthAwarePaginator; public function getDatabasesForHost(int $host, int $count = 25): LengthAwarePaginator;
/**
* Create a new database if it does not already exist on the host with
* the provided details.
*
* @param array $data
* @return \Pterodactyl\Models\Database
*
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
* @throws \Pterodactyl\Exceptions\Repository\DuplicateDatabaseNameException
*/
public function createIfNotExists(array $data): Database;
/** /**
* Create a new database on a given connection. * Create a new database on a given connection.
* *

View file

@ -54,15 +54,4 @@ interface NodeRepositoryInterface extends RepositoryInterface
* @return \Illuminate\Support\Collection * @return \Illuminate\Support\Collection
*/ */
public function getNodesForServerCreation(): Collection; public function getNodesForServerCreation(): Collection;
/**
* Return the IDs of all nodes that exist in the provided locations and have the space
* available to support the additional disk and memory provided.
*
* @param array $locations
* @param int $disk
* @param int $memory
* @return \Illuminate\Support\LazyCollection
*/
public function getNodesWithResourceUse(array $locations, int $disk, int $memory): LazyCollection;
} }

View file

@ -102,9 +102,11 @@ class MountController extends Controller
public function create(MountFormRequest $request) public function create(MountFormRequest $request)
{ {
/** @var \Pterodactyl\Models\Mount $mount */ /** @var \Pterodactyl\Models\Mount $mount */
$mount = Mount::query()->create(array_merge($request->validated(), [ $model = (new Mount())->fill($request->validated());
'uuid' => Uuid::uuid4()->toString(), $model->forceFill(['uuid' => Uuid::uuid4()->toString()]);
]));
$model->saveOrFail();
$mount = $model->fresh();
$this->alert->success('Mount was created successfully.')->flash(); $this->alert->success('Mount was created successfully.')->flash();

View file

@ -1,15 +1,9 @@
<?php <?php
/**
* Pterodactyl - Panel
* Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com>.
*
* This software is licensed under the terms of the MIT license.
* https://opensource.org/licenses/MIT
*/
namespace Pterodactyl\Http\Controllers\Admin\Nests; namespace Pterodactyl\Http\Controllers\Admin\Nests;
use Illuminate\View\View; use Illuminate\View\View;
use Pterodactyl\Models\Egg;
use Illuminate\Http\RedirectResponse; use Illuminate\Http\RedirectResponse;
use Prologue\Alerts\AlertsMessageBag; use Prologue\Alerts\AlertsMessageBag;
use Pterodactyl\Http\Controllers\Controller; use Pterodactyl\Http\Controllers\Controller;
@ -81,14 +75,14 @@ class EggScriptController extends Controller
* Handle a request to update the installation script for an Egg. * Handle a request to update the installation script for an Egg.
* *
* @param \Pterodactyl\Http\Requests\Admin\Egg\EggScriptFormRequest $request * @param \Pterodactyl\Http\Requests\Admin\Egg\EggScriptFormRequest $request
* @param int $egg * @param \Pterodactyl\Models\Egg $egg
* @return \Illuminate\Http\RedirectResponse * @return \Illuminate\Http\RedirectResponse
* *
* @throws \Pterodactyl\Exceptions\Model\DataValidationException * @throws \Pterodactyl\Exceptions\Model\DataValidationException
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
* @throws \Pterodactyl\Exceptions\Service\Egg\InvalidCopyFromException * @throws \Pterodactyl\Exceptions\Service\Egg\InvalidCopyFromException
*/ */
public function update(EggScriptFormRequest $request, int $egg): RedirectResponse public function update(EggScriptFormRequest $request, Egg $egg): RedirectResponse
{ {
$this->installScriptService->handle($egg, $request->normalize()); $this->installScriptService->handle($egg, $request->normalize());
$this->alert->success(trans('admin/nests.eggs.notices.script_updated'))->flash(); $this->alert->success(trans('admin/nests.eggs.notices.script_updated'))->flash();

View file

@ -102,7 +102,7 @@ class EggShareController extends Controller
* Update an existing Egg using a new imported file. * Update an existing Egg using a new imported file.
* *
* @param \Pterodactyl\Http\Requests\Admin\Egg\EggImportFormRequest $request * @param \Pterodactyl\Http\Requests\Admin\Egg\EggImportFormRequest $request
* @param int $egg * @param \Pterodactyl\Models\Egg $egg
* @return \Illuminate\Http\RedirectResponse * @return \Illuminate\Http\RedirectResponse
* *
* @throws \Pterodactyl\Exceptions\Model\DataValidationException * @throws \Pterodactyl\Exceptions\Model\DataValidationException
@ -110,7 +110,7 @@ class EggShareController extends Controller
* @throws \Pterodactyl\Exceptions\Service\Egg\BadJsonFormatException * @throws \Pterodactyl\Exceptions\Service\Egg\BadJsonFormatException
* @throws \Pterodactyl\Exceptions\Service\InvalidFileUploadException * @throws \Pterodactyl\Exceptions\Service\InvalidFileUploadException
*/ */
public function update(EggImportFormRequest $request, int $egg): RedirectResponse public function update(EggImportFormRequest $request, Egg $egg): RedirectResponse
{ {
$this->updateImporterService->handle($egg, $request->file('import_file')); $this->updateImporterService->handle($egg, $request->file('import_file'));
$this->alert->success(trans('admin/nests.eggs.notices.updated_via_import'))->flash(); $this->alert->success(trans('admin/nests.eggs.notices.updated_via_import'))->flash();

View file

@ -252,7 +252,7 @@ class ServersController extends Controller
*/ */
public function reinstallServer(Server $server) public function reinstallServer(Server $server)
{ {
$this->reinstallService->reinstall($server); $this->reinstallService->handle($server);
$this->alert->success(trans('admin/server.alerts.server_reinstalled'))->flash(); $this->alert->success(trans('admin/server.alerts.server_reinstalled'))->flash();
return redirect()->route('admin.servers.view.manage', $server->id); return redirect()->route('admin.servers.view.manage', $server->id);
@ -362,7 +362,7 @@ class ServersController extends Controller
public function newDatabase(StoreServerDatabaseRequest $request, Server $server) public function newDatabase(StoreServerDatabaseRequest $request, Server $server)
{ {
$this->databaseManagementService->create($server, [ $this->databaseManagementService->create($server, [
'database' => $request->input('database'), 'database' => DatabaseManagementService::generateUniqueDatabaseName($request->input('database'), $server->id),
'remote' => $request->input('remote'), 'remote' => $request->input('remote'),
'database_host_id' => $request->input('database_host_id'), 'database_host_id' => $request->input('database_host_id'),
'max_connections' => $request->input('max_connections'), 'max_connections' => $request->input('max_connections'),
@ -409,7 +409,7 @@ class ServersController extends Controller
['id', '=', $database], ['id', '=', $database],
]); ]);
$this->databaseManagementService->delete($database->id); $this->databaseManagementService->delete($database);
return response('', 204); return response('', 204);
} }

View file

@ -86,8 +86,8 @@ class UserController extends Controller
{ {
$users = QueryBuilder::for( $users = QueryBuilder::for(
User::query()->select('users.*') User::query()->select('users.*')
->selectRaw('COUNT(subusers.id) as subuser_of_count') ->selectRaw('COUNT(DISTINCT(subusers.id)) as subuser_of_count')
->selectRaw('COUNT(servers.id) as servers_count') ->selectRaw('COUNT(DISTINCT(servers.id)) as servers_count')
->leftJoin('subusers', 'subusers.user_id', '=', 'users.id') ->leftJoin('subusers', 'subusers.user_id', '=', 'users.id')
->leftJoin('servers', 'servers.owner_id', '=', 'users.id') ->leftJoin('servers', 'servers.owner_id', '=', 'users.id')
->groupBy('users.id') ->groupBy('users.id')

View file

@ -110,7 +110,9 @@ class DatabaseController extends ApplicationApiController
*/ */
public function store(StoreServerDatabaseRequest $request, Server $server): JsonResponse public function store(StoreServerDatabaseRequest $request, Server $server): JsonResponse
{ {
$database = $this->databaseManagementService->create($server, $request->validated()); $database = $this->databaseManagementService->create($server, array_merge($request->validated(), [
'database' => $request->databaseName(),
]));
return $this->fractal->item($database) return $this->fractal->item($database)
->transformWith($this->getTransformer(ServerDatabaseTransformer::class)) ->transformWith($this->getTransformer(ServerDatabaseTransformer::class))
@ -133,7 +135,7 @@ class DatabaseController extends ApplicationApiController
*/ */
public function delete(ServerDatabaseWriteRequest $request): Response public function delete(ServerDatabaseWriteRequest $request): Response
{ {
$this->databaseManagementService->delete($request->getModel(Database::class)->id); $this->databaseManagementService->delete($request->getModel(Database::class));
return response('', 204); return response('', 204);
} }

View file

@ -82,7 +82,7 @@ class ServerManagementController extends ApplicationApiController
*/ */
public function reinstall(ServerWriteRequest $request, Server $server): Response public function reinstall(ServerWriteRequest $request, Server $server): Response
{ {
$this->reinstallServerService->reinstall($server); $this->reinstallServerService->handle($server);
return $this->returnNoContent(); return $this->returnNoContent();
} }

View file

@ -129,7 +129,7 @@ class DatabaseController extends ClientApiController
*/ */
public function delete(DeleteDatabaseRequest $request, Server $server, Database $database): Response public function delete(DeleteDatabaseRequest $request, Server $server, Database $database): Response
{ {
$this->managementService->delete($database->id); $this->managementService->delete($database);
return Response::create('', Response::HTTP_NO_CONTENT); return Response::create('', Response::HTTP_NO_CONTENT);
} }

View file

@ -69,7 +69,7 @@ class SettingsController extends ClientApiController
*/ */
public function reinstall(ReinstallServerRequest $request, Server $server) public function reinstall(ReinstallServerRequest $request, Server $server)
{ {
$this->reinstallServerService->reinstall($server); $this->reinstallServerService->handle($server);
return new JsonResponse([], Response::HTTP_ACCEPTED); return new JsonResponse([], Response::HTTP_ACCEPTED);
} }

View file

@ -100,7 +100,7 @@ class StartupController extends ClientApiController
'server_id' => $server->id, 'server_id' => $server->id,
'variable_id' => $variable->id, 'variable_id' => $variable->id,
], [ ], [
'variable_value' => $request->input('value'), 'variable_value' => $request->input('value') ?? '',
]); ]);
$variable = $variable->refresh(); $variable = $variable->refresh();

View file

@ -2,9 +2,12 @@
namespace Pterodactyl\Http\Requests\Api\Application\Servers\Databases; namespace Pterodactyl\Http\Requests\Api\Application\Servers\Databases;
use Webmozart\Assert\Assert;
use Pterodactyl\Models\Server;
use Illuminate\Validation\Rule; use Illuminate\Validation\Rule;
use Illuminate\Database\Query\Builder; use Illuminate\Database\Query\Builder;
use Pterodactyl\Services\Acl\Api\AdminAcl; use Pterodactyl\Services\Acl\Api\AdminAcl;
use Pterodactyl\Services\Databases\DatabaseManagementService;
use Pterodactyl\Http\Requests\Api\Application\ApplicationApiRequest; use Pterodactyl\Http\Requests\Api\Application\ApplicationApiRequest;
class StoreServerDatabaseRequest extends ApplicationApiRequest class StoreServerDatabaseRequest extends ApplicationApiRequest
@ -26,14 +29,16 @@ class StoreServerDatabaseRequest extends ApplicationApiRequest
*/ */
public function rules(): array public function rules(): array
{ {
$server = $this->route()->parameter('server');
return [ return [
'database' => [ 'database' => [
'required', 'required',
'string', 'alpha_dash',
'min:1', 'min:1',
'max:24', 'max:48',
Rule::unique('databases')->where(function (Builder $query) { Rule::unique('databases')->where(function (Builder $query) use ($server) {
$query->where('database_host_id', $this->input('host') ?? 0); $query->where('server_id', $server->id)->where('database', $this->databaseName());
}), }),
], ],
'remote' => 'required|string|regex:/^[0-9%.]{1,15}$/', 'remote' => 'required|string|regex:/^[0-9%.]{1,15}$/',
@ -68,4 +73,18 @@ class StoreServerDatabaseRequest extends ApplicationApiRequest
'database' => 'Database Name', 'database' => 'Database Name',
]; ];
} }
/**
* Returns the database name in the expected format.
*
* @return string
*/
public function databaseName(): string
{
$server = $this->route()->parameter('server');
Assert::isInstanceOf($server, Server::class);
return DatabaseManagementService::generateUniqueDatabaseName($this->input('database'), $server->id);
}
} }

View file

@ -2,6 +2,7 @@
namespace Pterodactyl\Http\Requests\Api\Client\Account; namespace Pterodactyl\Http\Requests\Api\Client\Account;
use Pterodactyl\Models\ApiKey;
use Pterodactyl\Http\Requests\Api\Client\ClientApiRequest; use Pterodactyl\Http\Requests\Api\Client\ClientApiRequest;
class StoreApiKeyRequest extends ClientApiRequest class StoreApiKeyRequest extends ClientApiRequest
@ -11,9 +12,11 @@ class StoreApiKeyRequest extends ClientApiRequest
*/ */
public function rules(): array public function rules(): array
{ {
$rules = ApiKey::getRules();
return [ return [
'description' => 'required|string|min:4', 'description' => $rules['memo'],
'allowed_ips' => 'array', 'allowed_ips' => $rules['allowed_ips'],
'allowed_ips.*' => 'ip', 'allowed_ips.*' => 'ip',
]; ];
} }

View file

@ -2,9 +2,14 @@
namespace Pterodactyl\Http\Requests\Api\Client\Servers\Databases; namespace Pterodactyl\Http\Requests\Api\Client\Servers\Databases;
use Webmozart\Assert\Assert;
use Pterodactyl\Models\Server;
use Illuminate\Validation\Rule;
use Pterodactyl\Models\Permission; use Pterodactyl\Models\Permission;
use Illuminate\Database\Query\Builder;
use Pterodactyl\Contracts\Http\ClientPermissionsRequest; use Pterodactyl\Contracts\Http\ClientPermissionsRequest;
use Pterodactyl\Http\Requests\Api\Client\ClientApiRequest; use Pterodactyl\Http\Requests\Api\Client\ClientApiRequest;
use Pterodactyl\Services\Databases\DatabaseManagementService;
class StoreDatabaseRequest extends ClientApiRequest implements ClientPermissionsRequest class StoreDatabaseRequest extends ClientApiRequest implements ClientPermissionsRequest
{ {
@ -21,9 +26,35 @@ class StoreDatabaseRequest extends ClientApiRequest implements ClientPermissions
*/ */
public function rules(): array public function rules(): array
{ {
$server = $this->route()->parameter('server');
Assert::isInstanceOf($server, Server::class);
return [ return [
'database' => 'required|alpha_dash|min:3|max:48', 'database' => [
'required',
'alpha_dash',
'min:1',
'max:48',
// Yes, I am aware that you could have the same database name across two unique hosts. However,
// I don't really care about that for this validation. We just want to make sure it is unique to
// the server itself. No need for complexity.
Rule::unique('databases')->where(function (Builder $query) use ($server) {
$query->where('server_id', $server->id)
->where('database', DatabaseManagementService::generateUniqueDatabaseName($this->input('database'), $server->id));
}),
],
'remote' => 'required|string|regex:/^[0-9%.]{1,15}$/', 'remote' => 'required|string|regex:/^[0-9%.]{1,15}$/',
]; ];
} }
/**
* @return array
*/
public function messages()
{
return [
'database.unique' => 'The database name you have selected is already in use by this server.',
];
}
} }

View file

@ -24,7 +24,7 @@ class UpdateStartupVariableRequest extends ClientApiRequest
{ {
return [ return [
'key' => 'required|string', 'key' => 'required|string',
'value' => 'present|string', 'value' => 'present',
]; ];
} }
} }

View file

@ -3,10 +3,10 @@
namespace Pterodactyl\Jobs\Schedule; namespace Pterodactyl\Jobs\Schedule;
use Exception; use Exception;
use Carbon\Carbon;
use Pterodactyl\Jobs\Job; use Pterodactyl\Jobs\Job;
use Carbon\CarbonImmutable;
use Pterodactyl\Models\Task;
use InvalidArgumentException; use InvalidArgumentException;
use Illuminate\Container\Container;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
@ -15,39 +15,25 @@ use Pterodactyl\Repositories\Eloquent\TaskRepository;
use Pterodactyl\Services\Backups\InitiateBackupService; use Pterodactyl\Services\Backups\InitiateBackupService;
use Pterodactyl\Repositories\Wings\DaemonPowerRepository; use Pterodactyl\Repositories\Wings\DaemonPowerRepository;
use Pterodactyl\Repositories\Wings\DaemonCommandRepository; use Pterodactyl\Repositories\Wings\DaemonCommandRepository;
use Pterodactyl\Contracts\Repository\TaskRepositoryInterface;
use Pterodactyl\Contracts\Repository\ScheduleRepositoryInterface;
class RunTaskJob extends Job implements ShouldQueue class RunTaskJob extends Job implements ShouldQueue
{ {
use DispatchesJobs, InteractsWithQueue, SerializesModels; use DispatchesJobs, InteractsWithQueue, SerializesModels;
/** /**
* @var int * @var \Pterodactyl\Models\Task
*/
public $schedule;
/**
* @var int
*/ */
public $task; public $task;
/**
* @var \Pterodactyl\Repositories\Eloquent\TaskRepository
*/
protected $taskRepository;
/** /**
* RunTaskJob constructor. * RunTaskJob constructor.
* *
* @param int $task * @param \Pterodactyl\Models\Task $task
* @param int $schedule
*/ */
public function __construct(int $task, int $schedule) public function __construct(Task $task)
{ {
$this->queue = config('pterodactyl.queues.standard'); $this->queue = config('pterodactyl.queues.standard');
$this->task = $task; $this->task = $task;
$this->schedule = $schedule;
} }
/** /**
@ -58,7 +44,6 @@ class RunTaskJob extends Job implements ShouldQueue
* @param \Pterodactyl\Repositories\Wings\DaemonPowerRepository $powerRepository * @param \Pterodactyl\Repositories\Wings\DaemonPowerRepository $powerRepository
* @param \Pterodactyl\Repositories\Eloquent\TaskRepository $taskRepository * @param \Pterodactyl\Repositories\Eloquent\TaskRepository $taskRepository
* *
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
* @throws \Throwable * @throws \Throwable
*/ */
public function handle( public function handle(
@ -67,36 +52,32 @@ class RunTaskJob extends Job implements ShouldQueue
DaemonPowerRepository $powerRepository, DaemonPowerRepository $powerRepository,
TaskRepository $taskRepository TaskRepository $taskRepository
) { ) {
$this->taskRepository = $taskRepository;
$task = $this->taskRepository->getTaskForJobProcess($this->task);
$server = $task->getRelation('server');
// Do not process a task that is not set to active. // Do not process a task that is not set to active.
if (! $task->getRelation('schedule')->is_active) { if (! $this->task->schedule->is_active) {
$this->markTaskNotQueued(); $this->markTaskNotQueued();
$this->markScheduleComplete(); $this->markScheduleComplete();
return; return;
} }
$server = $this->task->server;
// Perform the provided task against the daemon. // Perform the provided task against the daemon.
switch ($task->action) { switch ($this->task->action) {
case 'power': case 'power':
$powerRepository->setServer($server)->send($task->payload); $powerRepository->setServer($server)->send($this->task->payload);
break; break;
case 'command': case 'command':
$commandRepository->setServer($server)->send($task->payload); $commandRepository->setServer($server)->send($this->task->payload);
break; break;
case 'backup': case 'backup':
$backupService->setIgnoredFiles(explode(PHP_EOL, $task->payload))->handle($server, null); $backupService->setIgnoredFiles(explode(PHP_EOL, $this->task->payload))->handle($server, null);
break; break;
default: default:
throw new InvalidArgumentException('Cannot run a task that points to a non-existent action.'); throw new InvalidArgumentException('Cannot run a task that points to a non-existent action.');
} }
$this->markTaskNotQueued(); $this->markTaskNotQueued();
$this->queueNextTask($task->sequence_id); $this->queueNextTask();
} }
/** /**
@ -112,23 +93,23 @@ class RunTaskJob extends Job implements ShouldQueue
/** /**
* Get the next task in the schedule and queue it for running after the defined period of wait time. * Get the next task in the schedule and queue it for running after the defined period of wait time.
*
* @param int $sequence
*
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/ */
private function queueNextTask($sequence) private function queueNextTask()
{ {
$nextTask = $this->taskRepository->getNextTask($this->schedule, $sequence); /** @var \Pterodactyl\Models\Task|null $nextTask */
$nextTask = Task::query()->where('schedule_id', $this->task->schedule_id)
->where('sequence_id', $this->task->sequence_id + 1)
->first();
if (is_null($nextTask)) { if (is_null($nextTask)) {
$this->markScheduleComplete(); $this->markScheduleComplete();
return; return;
} }
$this->taskRepository->update($nextTask->id, ['is_queued' => true]); $nextTask->update(['is_queued' => true]);
$this->dispatch((new self($nextTask->id, $this->schedule))->delay($nextTask->time_offset));
$this->dispatch((new self($nextTask))->delay($nextTask->time_offset));
} }
/** /**
@ -136,13 +117,10 @@ class RunTaskJob extends Job implements ShouldQueue
*/ */
private function markScheduleComplete() private function markScheduleComplete()
{ {
Container::getInstance() $this->task->schedule()->update([
->make(ScheduleRepositoryInterface::class) 'is_processing' => false,
->withoutFreshModel() 'last_run_at' => CarbonImmutable::now()->toDateTimeString(),
->update($this->schedule, [ ]);
'is_processing' => false,
'last_run_at' => Carbon::now()->toDateTimeString(),
]);
} }
/** /**
@ -150,8 +128,6 @@ class RunTaskJob extends Job implements ShouldQueue
*/ */
private function markTaskNotQueued() private function markTaskNotQueued()
{ {
Container::getInstance() $this->task->update(['is_queued' => false]);
->make(TaskRepositoryInterface::class)
->update($this->task, ['is_queued' => false]);
} }
} }

View file

@ -7,6 +7,7 @@ use Illuminate\Validation\Rule;
use Illuminate\Container\Container; use Illuminate\Container\Container;
use Illuminate\Contracts\Validation\Factory; use Illuminate\Contracts\Validation\Factory;
use Illuminate\Database\Eloquent\Model as IlluminateModel; use Illuminate\Database\Eloquent\Model as IlluminateModel;
use Pterodactyl\Exceptions\Model\DataValidationException;
abstract class Model extends IlluminateModel abstract class Model extends IlluminateModel
{ {
@ -55,7 +56,11 @@ abstract class Model extends IlluminateModel
static::$validatorFactory = Container::getInstance()->make(Factory::class); static::$validatorFactory = Container::getInstance()->make(Factory::class);
static::saving(function (Model $model) { static::saving(function (Model $model) {
return $model->validate(); if (! $model->validate()) {
throw new DataValidationException($model->getValidator());
}
return true;
}); });
} }
@ -147,9 +152,9 @@ abstract class Model extends IlluminateModel
} }
return $this->getValidator()->setData( return $this->getValidator()->setData(
// Trying to do self::toArray() here will leave out keys based on the whitelist/blacklist // Trying to do self::toArray() here will leave out keys based on the whitelist/blacklist
// for that model. Doing this will return all of the attributes in a format that can // for that model. Doing this will return all of the attributes in a format that can
// properly be validated. // properly be validated.
$this->addCastAttributesToArray( $this->addCastAttributesToArray(
$this->getAttributes(), $this->getMutatedAttributes() $this->getAttributes(), $this->getMutatedAttributes()
) )

View file

@ -15,7 +15,7 @@ use Znck\Eloquent\Traits\BelongsToThrough;
* @property string $name * @property string $name
* @property string $description * @property string $description
* @property bool $skip_scripts * @property bool $skip_scripts
* @property int $suspended * @property bool $suspended
* @property int $owner_id * @property int $owner_id
* @property int $memory * @property int $memory
* @property int $swap * @property int $swap
@ -133,7 +133,7 @@ class Server extends Model
protected $casts = [ protected $casts = [
'node_id' => 'integer', 'node_id' => 'integer',
'skip_scripts' => 'boolean', 'skip_scripts' => 'boolean',
'suspended' => 'integer', 'suspended' => 'boolean',
'owner_id' => 'integer', 'owner_id' => 'integer',
'memory' => 'integer', 'memory' => 'integer',
'swap' => 'integer', 'swap' => 'integer',

View file

@ -93,31 +93,6 @@ class DatabaseRepository extends EloquentRepository implements DatabaseRepositor
->paginate($count, $this->getColumns()); ->paginate($count, $this->getColumns());
} }
/**
* Create a new database if it does not already exist on the host with
* the provided details.
*
* @param array $data
* @return \Pterodactyl\Models\Database
*
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
* @throws \Pterodactyl\Exceptions\Repository\DuplicateDatabaseNameException
*/
public function createIfNotExists(array $data): Database
{
$count = $this->getBuilder()->where([
['server_id', '=', array_get($data, 'server_id')],
['database_host_id', '=', array_get($data, 'database_host_id')],
['database', '=', array_get($data, 'database')],
])->count();
if ($count > 0) {
throw new DuplicateDatabaseNameException('A database with those details already exists for the specified server.');
}
return $this->create($data);
}
/** /**
* Create a new database on a given connection. * Create a new database on a given connection.
* *

View file

@ -171,28 +171,4 @@ class NodeRepository extends EloquentRepository implements NodeRepositoryInterfa
return $instance->first(); return $instance->first();
} }
/**
* Return the IDs of all nodes that exist in the provided locations and have the space
* available to support the additional disk and memory provided.
*
* @param array $locations
* @param int $disk
* @param int $memory
* @return \Illuminate\Support\LazyCollection
*/
public function getNodesWithResourceUse(array $locations, int $disk, int $memory): LazyCollection
{
$instance = $this->getBuilder()
->select(['nodes.id', 'nodes.memory', 'nodes.disk', 'nodes.memory_overallocate', 'nodes.disk_overallocate'])
->selectRaw('IFNULL(SUM(servers.memory), 0) as sum_memory, IFNULL(SUM(servers.disk), 0) as sum_disk')
->leftJoin('servers', 'servers.node_id', '=', 'nodes.id')
->where('nodes.public', 1);
if (! empty($locations)) {
$instance->whereIn('nodes.location_id', $locations);
}
return $instance->groupBy('nodes.id')->cursor();
}
} }

View file

@ -3,18 +3,29 @@
namespace Pterodactyl\Services\Databases; namespace Pterodactyl\Services\Databases;
use Exception; use Exception;
use InvalidArgumentException;
use Pterodactyl\Models\Server; use Pterodactyl\Models\Server;
use Pterodactyl\Models\Database; use Pterodactyl\Models\Database;
use Pterodactyl\Helpers\Utilities; use Pterodactyl\Helpers\Utilities;
use Illuminate\Database\ConnectionInterface; use Illuminate\Database\ConnectionInterface;
use Symfony\Component\VarDumper\Cloner\Data;
use Illuminate\Contracts\Encryption\Encrypter; use Illuminate\Contracts\Encryption\Encrypter;
use Pterodactyl\Extensions\DynamicDatabaseConnection; use Pterodactyl\Extensions\DynamicDatabaseConnection;
use Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface; use Pterodactyl\Repositories\Eloquent\DatabaseRepository;
use Pterodactyl\Exceptions\Repository\DuplicateDatabaseNameException;
use Pterodactyl\Exceptions\Service\Database\TooManyDatabasesException; use Pterodactyl\Exceptions\Service\Database\TooManyDatabasesException;
use Pterodactyl\Exceptions\Service\Database\DatabaseClientFeatureNotEnabledException; use Pterodactyl\Exceptions\Service\Database\DatabaseClientFeatureNotEnabledException;
class DatabaseManagementService class DatabaseManagementService
{ {
/**
* The regex used to validate that the database name passed through to the function is
* in the expected format.
*
* @see \Pterodactyl\Services\Databases\DatabaseManagementService::generateUniqueDatabaseName()
*/
private const MATCH_NAME_REGEX = '/^(s[\d]+_)(.*)$/';
/** /**
* @var \Illuminate\Database\ConnectionInterface * @var \Illuminate\Database\ConnectionInterface
*/ */
@ -31,7 +42,7 @@ class DatabaseManagementService
private $encrypter; private $encrypter;
/** /**
* @var \Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface * @var \Pterodactyl\Repositories\Eloquent\DatabaseRepository
*/ */
private $repository; private $repository;
@ -50,13 +61,13 @@ class DatabaseManagementService
* *
* @param \Illuminate\Database\ConnectionInterface $connection * @param \Illuminate\Database\ConnectionInterface $connection
* @param \Pterodactyl\Extensions\DynamicDatabaseConnection $dynamic * @param \Pterodactyl\Extensions\DynamicDatabaseConnection $dynamic
* @param \Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface $repository * @param \Pterodactyl\Repositories\Eloquent\DatabaseRepository $repository
* @param \Illuminate\Contracts\Encryption\Encrypter $encrypter * @param \Illuminate\Contracts\Encryption\Encrypter $encrypter
*/ */
public function __construct( public function __construct(
ConnectionInterface $connection, ConnectionInterface $connection,
DynamicDatabaseConnection $dynamic, DynamicDatabaseConnection $dynamic,
DatabaseRepositoryInterface $repository, DatabaseRepository $repository,
Encrypter $encrypter Encrypter $encrypter
) { ) {
$this->connection = $connection; $this->connection = $connection;
@ -65,6 +76,21 @@ class DatabaseManagementService
$this->repository = $repository; $this->repository = $repository;
} }
/**
* Generates a unique database name for the given server. This name should be passed through when
* calling this handle function for this service, otherwise the database will be created with
* whatever name is provided.
*
* @param string $name
* @param int $serverId
* @return string
*/
public static function generateUniqueDatabaseName(string $name, int $serverId): string
{
// Max of 48 characters, including the s123_ that we append to the front.
return sprintf('s%d_%s', $serverId, substr($name, 0, 48 - strlen("s{$serverId}_")));
}
/** /**
* Set wether or not this class should validate that the server has enough slots * Set wether or not this class should validate that the server has enough slots
* left before creating the new database. * left before creating the new database.
@ -104,12 +130,15 @@ class DatabaseManagementService
} }
} }
// Max of 48 characters, including the s123_ that we append to the front. // Protect against developer mistakes...
$truncatedName = substr($data['database'], 0, 48 - strlen("s{$server->id}_")); if (empty($data['database']) || ! preg_match(self::MATCH_NAME_REGEX, $data['database'])) {
throw new InvalidArgumentException(
'The database name passed to DatabaseManagementService::handle MUST be prefixed with "s{server_id}_".'
);
}
$data = array_merge($data, [ $data = array_merge($data, [
'server_id' => $server->id, 'server_id' => $server->id,
'database' => $truncatedName,
'username' => sprintf('u%d_%s', $server->id, str_random(10)), 'username' => sprintf('u%d_%s', $server->id, str_random(10)),
'password' => $this->encrypter->encrypt( 'password' => $this->encrypter->encrypt(
Utilities::randomStringWithSpecialCharacters(24) Utilities::randomStringWithSpecialCharacters(24)
@ -120,7 +149,8 @@ class DatabaseManagementService
try { try {
return $this->connection->transaction(function () use ($data, &$database) { return $this->connection->transaction(function () use ($data, &$database) {
$database = $this->repository->createIfNotExists($data); $database = $this->createModel($data);
$this->dynamic->set('dynamic', $data['database_host_id']); $this->dynamic->set('dynamic', $data['database_host_id']);
$this->repository->createDatabase($database->database); $this->repository->createDatabase($database->database);
@ -139,7 +169,7 @@ class DatabaseManagementService
$this->repository->dropUser($database->username, $database->remote); $this->repository->dropUser($database->username, $database->remote);
$this->repository->flush(); $this->repository->flush();
} }
} catch (Exception $exception) { } catch (Exception $deletionException) {
// Do nothing here. We've already encountered an issue before this point so no // Do nothing here. We've already encountered an issue before this point so no
// reason to prioritize this error over the initial one. // reason to prioritize this error over the initial one.
} }
@ -151,20 +181,48 @@ class DatabaseManagementService
/** /**
* Delete a database from the given host server. * Delete a database from the given host server.
* *
* @param int $id * @param \Pterodactyl\Models\Database $database
* @return bool|null * @return bool|null
* *
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException * @throws \Exception
*/ */
public function delete($id) public function delete(Database $database)
{ {
$database = $this->repository->find($id);
$this->dynamic->set('dynamic', $database->database_host_id); $this->dynamic->set('dynamic', $database->database_host_id);
$this->repository->dropDatabase($database->database); $this->repository->dropDatabase($database->database);
$this->repository->dropUser($database->username, $database->remote); $this->repository->dropUser($database->username, $database->remote);
$this->repository->flush(); $this->repository->flush();
return $this->repository->delete($id); return $database->delete();
}
/**
* Create the database if there is not an identical match in the DB. While you can technically
* have the same name across multiple hosts, for the sake of keeping this logic easy to understand
* and avoiding user confusion we will ignore the specific host and just look across all hosts.
*
* @param array $data
* @return \Pterodactyl\Models\Database
*
* @throws \Pterodactyl\Exceptions\Repository\DuplicateDatabaseNameException
* @throws \Throwable
*/
protected function createModel(array $data): Database
{
$exists = Database::query()->where('server_id', $data['server_id'])
->where('database', $data['database'])
->exists();
if ($exists) {
throw new DuplicateDatabaseNameException(
'A database with that name already exists for this server.'
);
}
$database = (new Database)->forceFill($data);
$database->saveOrFail();
return $database;
} }
} }

View file

@ -2,44 +2,27 @@
namespace Pterodactyl\Services\Databases; namespace Pterodactyl\Services\Databases;
use Webmozart\Assert\Assert;
use Pterodactyl\Models\Server; use Pterodactyl\Models\Server;
use Pterodactyl\Models\Database; use Pterodactyl\Models\Database;
use Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface; use Pterodactyl\Models\DatabaseHost;
use Pterodactyl\Contracts\Repository\DatabaseHostRepositoryInterface;
use Pterodactyl\Exceptions\Service\Database\NoSuitableDatabaseHostException; use Pterodactyl\Exceptions\Service\Database\NoSuitableDatabaseHostException;
class DeployServerDatabaseService class DeployServerDatabaseService
{ {
/**
* @var \Pterodactyl\Contracts\Repository\DatabaseHostRepositoryInterface
*/
private $databaseHostRepository;
/** /**
* @var \Pterodactyl\Services\Databases\DatabaseManagementService * @var \Pterodactyl\Services\Databases\DatabaseManagementService
*/ */
private $managementService; private $managementService;
/**
* @var \Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface
*/
private $repository;
/** /**
* ServerDatabaseCreationService constructor. * ServerDatabaseCreationService constructor.
* *
* @param \Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface $repository
* @param \Pterodactyl\Contracts\Repository\DatabaseHostRepositoryInterface $databaseHostRepository
* @param \Pterodactyl\Services\Databases\DatabaseManagementService $managementService * @param \Pterodactyl\Services\Databases\DatabaseManagementService $managementService
*/ */
public function __construct( public function __construct(DatabaseManagementService $managementService)
DatabaseRepositoryInterface $repository, {
DatabaseHostRepositoryInterface $databaseHostRepository,
DatabaseManagementService $managementService
) {
$this->databaseHostRepository = $databaseHostRepository;
$this->managementService = $managementService; $this->managementService = $managementService;
$this->repository = $repository;
} }
/** /**
@ -53,28 +36,26 @@ class DeployServerDatabaseService
*/ */
public function handle(Server $server, array $data): Database public function handle(Server $server, array $data): Database
{ {
$allowRandom = config('pterodactyl.client_features.databases.allow_random'); Assert::notEmpty($data['database'] ?? null);
$hosts = $this->databaseHostRepository->setColumns(['id'])->findWhere([ Assert::notEmpty($data['remote'] ?? null);
['node_id', '=', $server->node_id],
]);
if ($hosts->isEmpty() && ! $allowRandom) {
throw new NoSuitableDatabaseHostException;
}
$hosts = DatabaseHost::query()->get()->toBase();
if ($hosts->isEmpty()) { if ($hosts->isEmpty()) {
$hosts = $this->databaseHostRepository->setColumns(['id'])->all(); throw new NoSuitableDatabaseHostException;
if ($hosts->isEmpty()) { } else {
$nodeHosts = $hosts->where('node_id', $server->node_id)->toBase();
if ($nodeHosts->isEmpty() && ! config('pterodactyl.client_features.databases.allow_random')) {
throw new NoSuitableDatabaseHostException; throw new NoSuitableDatabaseHostException;
} }
} }
$host = $hosts->random();
return $this->managementService->create($server, [ return $this->managementService->create($server, [
'database_host_id' => $host->id, 'database_host_id' => $nodeHosts->isEmpty()
'database' => array_get($data, 'database'), ? $hosts->random()->id
'remote' => array_get($data, 'remote'), : $nodeHosts->random()->id,
'database' => DatabaseManagementService::generateUniqueDatabaseName($data['database'], $server->id),
'remote' => $data['remote'],
]); ]);
} }
} }

View file

@ -3,16 +3,12 @@
namespace Pterodactyl\Services\Deployment; namespace Pterodactyl\Services\Deployment;
use Webmozart\Assert\Assert; use Webmozart\Assert\Assert;
use Pterodactyl\Contracts\Repository\NodeRepositoryInterface; use Pterodactyl\Models\Node;
use Illuminate\Support\LazyCollection;
use Pterodactyl\Exceptions\Service\Deployment\NoViableNodeException; use Pterodactyl\Exceptions\Service\Deployment\NoViableNodeException;
class FindViableNodesService class FindViableNodesService
{ {
/**
* @var \Pterodactyl\Contracts\Repository\NodeRepositoryInterface
*/
private $repository;
/** /**
* @var array * @var array
*/ */
@ -28,16 +24,6 @@ class FindViableNodesService
*/ */
protected $memory; protected $memory;
/**
* FindViableNodesService constructor.
*
* @param \Pterodactyl\Contracts\Repository\NodeRepositoryInterface $repository
*/
public function __construct(NodeRepositoryInterface $repository)
{
$this->repository = $repository;
}
/** /**
* Set the locations that should be searched through to locate available nodes. * Set the locations that should be searched through to locate available nodes.
* *
@ -46,6 +32,8 @@ class FindViableNodesService
*/ */
public function setLocations(array $locations): self public function setLocations(array $locations): self
{ {
Assert::allInteger($locations, 'An array of location IDs should be provided when calling setLocations.');
$this->locations = $locations; $this->locations = $locations;
return $this; return $this;
@ -90,32 +78,34 @@ class FindViableNodesService
* are tossed out, as are any nodes marked as non-public, meaning automatic * are tossed out, as are any nodes marked as non-public, meaning automatic
* deployments should not be done against them. * deployments should not be done against them.
* *
* @return int[] * @return \Pterodactyl\Models\Node[]|\Illuminate\Support\Collection
* @throws \Pterodactyl\Exceptions\Service\Deployment\NoViableNodeException * @throws \Pterodactyl\Exceptions\Service\Deployment\NoViableNodeException
*/ */
public function handle(): array public function handle()
{ {
Assert::integer($this->disk, 'Calls to ' . __METHOD__ . ' must have the disk space set as an integer, received %s'); Assert::integer($this->disk, 'Disk space must be an int, got %s');
Assert::integer($this->memory, 'Calls to ' . __METHOD__ . ' must have the memory usage set as an integer, received %s'); Assert::integer($this->memory, 'Memory usage must be an int, got %s');
$nodes = $this->repository->getNodesWithResourceUse($this->locations, $this->disk, $this->memory); $query = Node::query()->select('nodes.*')
$viable = []; ->selectRaw('IFNULL(SUM(servers.memory), 0) as sum_memory')
->selectRaw('IFNULL(SUM(servers.disk), 0) as sum_disk')
->leftJoin('servers', 'servers.node_id', '=', 'nodes.id')
->where('nodes.public', 1);
foreach ($nodes as $node) { if (! empty($this->locations)) {
$memoryLimit = $node->memory * (1 + ($node->memory_overallocate / 100)); $query = $query->whereIn('nodes.location_id', $this->locations);
$diskLimit = $node->disk * (1 + ($node->disk_overallocate / 100));
if (($node->sum_memory + $this->memory) > $memoryLimit || ($node->sum_disk + $this->disk) > $diskLimit) {
continue;
}
$viable[] = $node->id;
} }
if (empty($viable)) { $results = $query->groupBy('nodes.id')
->havingRaw('(IFNULL(SUM(servers.memory), 0) + ?) <= (nodes.memory * (1 + (nodes.memory_overallocate / 100)))', [ $this->memory ])
->havingRaw('(IFNULL(SUM(servers.disk), 0) + ?) <= (nodes.disk * (1 + (nodes.disk_overallocate / 100)))', [ $this->disk ])
->get()
->toBase();
if ($results->isEmpty()) {
throw new NoViableNodeException(trans('exceptions.deployment.no_viable_nodes')); throw new NoViableNodeException(trans('exceptions.deployment.no_viable_nodes'));
} }
return $viable; return $results;
} }
} }

View file

@ -1,11 +1,4 @@
<?php <?php
/**
* Pterodactyl - Panel
* Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com>.
*
* This software is licensed under the terms of the MIT license.
* https://opensource.org/licenses/MIT
*/
namespace Pterodactyl\Services\Eggs\Scripts; namespace Pterodactyl\Services\Eggs\Scripts;
@ -40,12 +33,8 @@ class InstallScriptService
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
* @throws \Pterodactyl\Exceptions\Service\Egg\InvalidCopyFromException * @throws \Pterodactyl\Exceptions\Service\Egg\InvalidCopyFromException
*/ */
public function handle($egg, array $data) public function handle(Egg $egg, array $data)
{ {
if (! $egg instanceof Egg) {
$egg = $this->repository->find($egg);
}
if (! is_null(array_get($data, 'copy_script_from'))) { if (! is_null(array_get($data, 'copy_script_from'))) {
if (! $this->repository->isCopyableScript(array_get($data, 'copy_script_from'), $egg->nest_id)) { if (! $this->repository->isCopyableScript(array_get($data, 'copy_script_from'), $egg->nest_id)) {
throw new InvalidCopyFromException(trans('exceptions.nest.egg.invalid_copy_id')); throw new InvalidCopyFromException(trans('exceptions.nest.egg.invalid_copy_id'));

View file

@ -1,11 +1,4 @@
<?php <?php
/**
* Pterodactyl - Panel
* Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com>.
*
* This software is licensed under the terms of the MIT license.
* https://opensource.org/licenses/MIT
*/
namespace Pterodactyl\Services\Eggs\Sharing; namespace Pterodactyl\Services\Eggs\Sharing;

View file

@ -1,11 +1,4 @@
<?php <?php
/**
* Pterodactyl - Panel
* Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com>.
*
* This software is licensed under the terms of the MIT license.
* https://opensource.org/licenses/MIT
*/
namespace Pterodactyl\Services\Eggs\Sharing; namespace Pterodactyl\Services\Eggs\Sharing;

View file

@ -2,6 +2,7 @@
namespace Pterodactyl\Services\Eggs\Sharing; namespace Pterodactyl\Services\Eggs\Sharing;
use Pterodactyl\Models\Egg;
use Illuminate\Http\UploadedFile; use Illuminate\Http\UploadedFile;
use Illuminate\Database\ConnectionInterface; use Illuminate\Database\ConnectionInterface;
use Pterodactyl\Contracts\Repository\EggRepositoryInterface; use Pterodactyl\Contracts\Repository\EggRepositoryInterface;
@ -46,7 +47,7 @@ class EggUpdateImporterService
/** /**
* Update an existing Egg using an uploaded JSON file. * Update an existing Egg using an uploaded JSON file.
* *
* @param int $egg * @param \Pterodactyl\Models\Egg $egg
* @param \Illuminate\Http\UploadedFile $file * @param \Illuminate\Http\UploadedFile $file
* *
* @throws \Pterodactyl\Exceptions\Model\DataValidationException * @throws \Pterodactyl\Exceptions\Model\DataValidationException
@ -54,7 +55,7 @@ class EggUpdateImporterService
* @throws \Pterodactyl\Exceptions\Service\Egg\BadJsonFormatException * @throws \Pterodactyl\Exceptions\Service\Egg\BadJsonFormatException
* @throws \Pterodactyl\Exceptions\Service\InvalidFileUploadException * @throws \Pterodactyl\Exceptions\Service\InvalidFileUploadException
*/ */
public function handle(int $egg, UploadedFile $file) public function handle(Egg $egg, UploadedFile $file)
{ {
if ($file->getError() !== UPLOAD_ERR_OK || ! $file->isFile()) { if ($file->getError() !== UPLOAD_ERR_OK || ! $file->isFile()) {
throw new InvalidFileUploadException( throw new InvalidFileUploadException(
@ -81,7 +82,7 @@ class EggUpdateImporterService
} }
$this->connection->beginTransaction(); $this->connection->beginTransaction();
$this->repository->update($egg, [ $this->repository->update($egg->id, [
'author' => object_get($parsed, 'author'), 'author' => object_get($parsed, 'author'),
'name' => object_get($parsed, 'name'), 'name' => object_get($parsed, 'name'),
'description' => object_get($parsed, 'description'), 'description' => object_get($parsed, 'description'),
@ -99,19 +100,19 @@ class EggUpdateImporterService
// Update Existing Variables // Update Existing Variables
collect($parsed->variables)->each(function ($variable) use ($egg) { collect($parsed->variables)->each(function ($variable) use ($egg) {
$this->variableRepository->withoutFreshModel()->updateOrCreate([ $this->variableRepository->withoutFreshModel()->updateOrCreate([
'egg_id' => $egg, 'egg_id' => $egg->id,
'env_variable' => $variable->env_variable, 'env_variable' => $variable->env_variable,
], collect($variable)->except(['egg_id', 'env_variable'])->toArray()); ], collect($variable)->except(['egg_id', 'env_variable'])->toArray());
}); });
$imported = collect($parsed->variables)->pluck('env_variable')->toArray(); $imported = collect($parsed->variables)->pluck('env_variable')->toArray();
$existing = $this->variableRepository->setColumns(['id', 'env_variable'])->findWhere([['egg_id', '=', $egg]]); $existing = $this->variableRepository->setColumns(['id', 'env_variable'])->findWhere([['egg_id', '=', $egg->id]]);
// Delete variables not present in the import. // Delete variables not present in the import.
collect($existing)->each(function ($variable) use ($egg, $imported) { collect($existing)->each(function ($variable) use ($egg, $imported) {
if (! in_array($variable->env_variable, $imported)) { if (! in_array($variable->env_variable, $imported)) {
$this->variableRepository->deleteWhere([ $this->variableRepository->deleteWhere([
['egg_id', '=', $egg], ['egg_id', '=', $egg->id],
['env_variable', '=', $variable->env_variable], ['env_variable', '=', $variable->env_variable],
]); ]);
} }

View file

@ -73,7 +73,7 @@ class ProcessScheduleService
$this->taskRepository->update($task->id, ['is_queued' => true]); $this->taskRepository->update($task->id, ['is_queued' => true]);
$this->dispatcher->dispatch( $this->dispatcher->dispatch(
(new RunTaskJob($task->id, $schedule->id))->delay($task->time_offset) (new RunTaskJob($task))->delay($task->time_offset)
); );
} }
} }

View file

@ -4,8 +4,6 @@ namespace Pterodactyl\Services\Servers;
use Pterodactyl\Models\Server; use Pterodactyl\Models\Server;
use Pterodactyl\Models\EggVariable; use Pterodactyl\Models\EggVariable;
use Illuminate\Contracts\Config\Repository as ConfigRepository;
use Pterodactyl\Contracts\Repository\ServerRepositoryInterface;
class EnvironmentService class EnvironmentService
{ {
@ -14,28 +12,6 @@ class EnvironmentService
*/ */
private $additional = []; private $additional = [];
/**
* @var \Illuminate\Contracts\Config\Repository
*/
private $config;
/**
* @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface
*/
private $repository;
/**
* EnvironmentService constructor.
*
* @param \Illuminate\Contracts\Config\Repository $config
* @param \Pterodactyl\Contracts\Repository\ServerRepositoryInterface $repository
*/
public function __construct(ConfigRepository $config, ServerRepositoryInterface $repository)
{
$this->config = $config;
$this->repository = $repository;
}
/** /**
* Dynamically configure additional environment variables to be assigned * Dynamically configure additional environment variables to be assigned
* with a specific server. * with a specific server.
@ -79,7 +55,7 @@ class EnvironmentService
} }
// Process variables set in the configuration file. // Process variables set in the configuration file.
foreach ($this->config->get('pterodactyl.environment_variables', []) as $key => $object) { foreach (config('pterodactyl.environment_variables', []) as $key => $object) {
$variables->put( $variables->put(
$key, is_callable($object) ? call_user_func($object, $server) : object_get($server, $object) $key, is_callable($object) ? call_user_func($object, $server) : object_get($server, $object)
); );

View file

@ -19,26 +19,18 @@ class ReinstallServerService
*/ */
private $connection; private $connection;
/**
* @var \Pterodactyl\Repositories\Eloquent\ServerRepository
*/
private $repository;
/** /**
* ReinstallService constructor. * ReinstallService constructor.
* *
* @param \Illuminate\Database\ConnectionInterface $connection * @param \Illuminate\Database\ConnectionInterface $connection
* @param \Pterodactyl\Repositories\Wings\DaemonServerRepository $daemonServerRepository * @param \Pterodactyl\Repositories\Wings\DaemonServerRepository $daemonServerRepository
* @param \Pterodactyl\Repositories\Eloquent\ServerRepository $repository
*/ */
public function __construct( public function __construct(
ConnectionInterface $connection, ConnectionInterface $connection,
DaemonServerRepository $daemonServerRepository, DaemonServerRepository $daemonServerRepository
ServerRepository $repository
) { ) {
$this->daemonServerRepository = $daemonServerRepository; $this->daemonServerRepository = $daemonServerRepository;
$this->connection = $connection; $this->connection = $connection;
$this->repository = $repository;
} }
/** /**
@ -49,16 +41,14 @@ class ReinstallServerService
* *
* @throws \Throwable * @throws \Throwable
*/ */
public function reinstall(Server $server) public function handle(Server $server)
{ {
return $this->connection->transaction(function () use ($server) { return $this->connection->transaction(function () use ($server) {
$updated = $this->repository->update($server->id, [ $server->forceFill(['installed' => Server::STATUS_INSTALLING])->save();
'installed' => Server::STATUS_INSTALLING,
], true, true);
$this->daemonServerRepository->setServer($server)->reinstall(); $this->daemonServerRepository->setServer($server)->reinstall();
return $updated; return $server->refresh();
}); });
} }
} }

View file

@ -1,17 +1,9 @@
<?php <?php
/**
* Pterodactyl - Panel
* Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com>.
*
* This software is licensed under the terms of the MIT license.
* https://opensource.org/licenses/MIT
*/
namespace Pterodactyl\Services\Servers; namespace Pterodactyl\Services\Servers;
use Pterodactyl\Models\Mount; use Pterodactyl\Models\Mount;
use Pterodactyl\Models\Server; use Pterodactyl\Models\Server;
use Pterodactyl\Contracts\Repository\ServerRepositoryInterface;
class ServerConfigurationStructureService class ServerConfigurationStructureService
{ {
@ -22,22 +14,13 @@ class ServerConfigurationStructureService
*/ */
private $environment; private $environment;
/**
* @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface
*/
private $repository;
/** /**
* ServerConfigurationStructureService constructor. * ServerConfigurationStructureService constructor.
* *
* @param \Pterodactyl\Contracts\Repository\ServerRepositoryInterface $repository
* @param \Pterodactyl\Services\Servers\EnvironmentService $environment * @param \Pterodactyl\Services\Servers\EnvironmentService $environment
*/ */
public function __construct( public function __construct(EnvironmentService $environment)
ServerRepositoryInterface $repository, {
EnvironmentService $environment
) {
$this->repository = $repository;
$this->environment = $environment; $this->environment = $environment;
} }
@ -50,8 +33,6 @@ class ServerConfigurationStructureService
* @param \Pterodactyl\Models\Server $server * @param \Pterodactyl\Models\Server $server
* @param bool $legacy * @param bool $legacy
* @return array * @return array
*
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/ */
public function handle(Server $server, bool $legacy = false): array public function handle(Server $server, bool $legacy = false): array
{ {
@ -72,7 +53,7 @@ class ServerConfigurationStructureService
{ {
return [ return [
'uuid' => $server->uuid, 'uuid' => $server->uuid,
'suspended' => (bool) $server->suspended, 'suspended' => $server->suspended,
'environment' => $this->environment->handle($server), 'environment' => $this->environment->handle($server),
'invocation' => $server->startup, 'invocation' => $server->startup,
'skip_egg_scripts' => $server->skip_scripts, 'skip_egg_scripts' => $server->skip_scripts,
@ -112,8 +93,6 @@ class ServerConfigurationStructureService
* *
* @param \Pterodactyl\Models\Server $server * @param \Pterodactyl\Models\Server $server
* @return array * @return array
*
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/ */
protected function returnLegacyFormat(Server $server) protected function returnLegacyFormat(Server $server)
{ {

View file

@ -4,7 +4,9 @@ namespace Pterodactyl\Services\Servers;
use Ramsey\Uuid\Uuid; use Ramsey\Uuid\Uuid;
use Illuminate\Support\Arr; use Illuminate\Support\Arr;
use Pterodactyl\Models\Egg;
use Pterodactyl\Models\User; use Pterodactyl\Models\User;
use Webmozart\Assert\Assert;
use Pterodactyl\Models\Server; use Pterodactyl\Models\Server;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Pterodactyl\Models\Allocation; use Pterodactyl\Models\Allocation;
@ -13,7 +15,6 @@ use Pterodactyl\Models\Objects\DeploymentObject;
use Pterodactyl\Repositories\Eloquent\EggRepository; use Pterodactyl\Repositories\Eloquent\EggRepository;
use Pterodactyl\Repositories\Eloquent\ServerRepository; use Pterodactyl\Repositories\Eloquent\ServerRepository;
use Pterodactyl\Repositories\Wings\DaemonServerRepository; use Pterodactyl\Repositories\Wings\DaemonServerRepository;
use Pterodactyl\Repositories\Eloquent\AllocationRepository;
use Pterodactyl\Services\Deployment\FindViableNodesService; use Pterodactyl\Services\Deployment\FindViableNodesService;
use Pterodactyl\Repositories\Eloquent\ServerVariableRepository; use Pterodactyl\Repositories\Eloquent\ServerVariableRepository;
use Pterodactyl\Services\Deployment\AllocationSelectionService; use Pterodactyl\Services\Deployment\AllocationSelectionService;
@ -21,11 +22,6 @@ use Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException;
class ServerCreationService class ServerCreationService
{ {
/**
* @var \Pterodactyl\Repositories\Eloquent\AllocationRepository
*/
private $allocationRepository;
/** /**
* @var \Pterodactyl\Services\Deployment\AllocationSelectionService * @var \Pterodactyl\Services\Deployment\AllocationSelectionService
*/ */
@ -79,7 +75,6 @@ class ServerCreationService
/** /**
* CreationService constructor. * CreationService constructor.
* *
* @param \Pterodactyl\Repositories\Eloquent\AllocationRepository $allocationRepository
* @param \Pterodactyl\Services\Deployment\AllocationSelectionService $allocationSelectionService * @param \Pterodactyl\Services\Deployment\AllocationSelectionService $allocationSelectionService
* @param \Illuminate\Database\ConnectionInterface $connection * @param \Illuminate\Database\ConnectionInterface $connection
* @param \Pterodactyl\Repositories\Wings\DaemonServerRepository $daemonServerRepository * @param \Pterodactyl\Repositories\Wings\DaemonServerRepository $daemonServerRepository
@ -92,7 +87,6 @@ class ServerCreationService
* @param \Pterodactyl\Services\Servers\VariableValidatorService $validatorService * @param \Pterodactyl\Services\Servers\VariableValidatorService $validatorService
*/ */
public function __construct( public function __construct(
AllocationRepository $allocationRepository,
AllocationSelectionService $allocationSelectionService, AllocationSelectionService $allocationSelectionService,
ConnectionInterface $connection, ConnectionInterface $connection,
DaemonServerRepository $daemonServerRepository, DaemonServerRepository $daemonServerRepository,
@ -105,7 +99,6 @@ class ServerCreationService
VariableValidatorService $validatorService VariableValidatorService $validatorService
) { ) {
$this->allocationSelectionService = $allocationSelectionService; $this->allocationSelectionService = $allocationSelectionService;
$this->allocationRepository = $allocationRepository;
$this->configurationStructureService = $configurationStructureService; $this->configurationStructureService = $configurationStructureService;
$this->connection = $connection; $this->connection = $connection;
$this->findViableNodesService = $findViableNodesService; $this->findViableNodesService = $findViableNodesService;
@ -130,15 +123,12 @@ class ServerCreationService
* @throws \Throwable * @throws \Throwable
* @throws \Pterodactyl\Exceptions\DisplayException * @throws \Pterodactyl\Exceptions\DisplayException
* @throws \Illuminate\Validation\ValidationException * @throws \Illuminate\Validation\ValidationException
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
* @throws \Pterodactyl\Exceptions\Service\Deployment\NoViableNodeException * @throws \Pterodactyl\Exceptions\Service\Deployment\NoViableNodeException
* @throws \Pterodactyl\Exceptions\Service\Deployment\NoViableAllocationException * @throws \Pterodactyl\Exceptions\Service\Deployment\NoViableAllocationException
*/ */
public function handle(array $data, DeploymentObject $deployment = null): Server public function handle(array $data, DeploymentObject $deployment = null): Server
{ {
$this->connection->beginTransaction();
// If a deployment object has been passed we need to get the allocation // If a deployment object has been passed we need to get the allocation
// that the server should use, and assign the node from that allocation. // that the server should use, and assign the node from that allocation.
if ($deployment instanceof DeploymentObject) { if ($deployment instanceof DeploymentObject) {
@ -149,37 +139,42 @@ class ServerCreationService
// Auto-configure the node based on the selected allocation // Auto-configure the node based on the selected allocation
// if no node was defined. // if no node was defined.
if (is_null(Arr::get($data, 'node_id'))) { if (empty($data['node_id'])) {
$data['node_id'] = $this->getNodeFromAllocation($data['allocation_id']); Assert::false(empty($data['allocation_id']), 'Expected a non-empty allocation_id in server creation data.');
$data['node_id'] = Allocation::query()->findOrFail($data['allocation_id'])->node_id;
} }
if (is_null(Arr::get($data, 'nest_id'))) { if (empty($data['nest_id'])) {
/** @var \Pterodactyl\Models\Egg $egg */ Assert::false(empty($data['egg_id']), 'Expected a non-empty egg_id in server creation data.');
$egg = $this->eggRepository->setColumns(['id', 'nest_id'])->find(Arr::get($data, 'egg_id'));
$data['nest_id'] = $egg->nest_id; $data['nest_id'] = Egg::query()->findOrFail($data['egg_id'])->nest_id;
} }
$eggVariableData = $this->validatorService $eggVariableData = $this->validatorService
->setUserLevel(User::USER_LEVEL_ADMIN) ->setUserLevel(User::USER_LEVEL_ADMIN)
->handle(Arr::get($data, 'egg_id'), Arr::get($data, 'environment', [])); ->handle(Arr::get($data, 'egg_id'), Arr::get($data, 'environment', []));
// Create the server and assign any additional allocations to it.
$server = $this->createModel($data);
$this->storeAssignedAllocations($server, $data);
$this->storeEggVariables($server, $eggVariableData);
// Due to the design of the Daemon, we need to persist this server to the disk // Due to the design of the Daemon, we need to persist this server to the disk
// before we can actually create it on the Daemon. // before we can actually create it on the Daemon.
// //
// If that connection fails out we will attempt to perform a cleanup by just // If that connection fails out we will attempt to perform a cleanup by just
// deleting the server itself from the system. // deleting the server itself from the system.
$this->connection->commit(); /** @var \Pterodactyl\Models\Server $server */
$server = $this->connection->transaction(function () use ($data, $eggVariableData) {
// Create the server and assign any additional allocations to it.
$server = $this->createModel($data);
$structure = $this->configurationStructureService->handle($server); $this->storeAssignedAllocations($server, $data);
$this->storeEggVariables($server, $eggVariableData);
return $server;
});
try { try {
$this->daemonServerRepository->setServer($server)->create($structure); $this->daemonServerRepository->setServer($server)->create(
$this->configurationStructureService->handle($server)
);
} catch (DaemonConnectionException $exception) { } catch (DaemonConnectionException $exception) {
$this->serverDeletionService->withForce(true)->handle($server); $this->serverDeletionService->withForce(true)->handle($server);
@ -208,7 +203,7 @@ class ServerCreationService
->handle(); ->handle();
return $this->allocationSelectionService->setDedicated($deployment->isDedicated()) return $this->allocationSelectionService->setDedicated($deployment->isDedicated())
->setNodes($nodes) ->setNodes($nodes->pluck('id')->toArray())
->setPorts($deployment->getPorts()) ->setPorts($deployment->getPorts())
->handle(); ->handle();
} }
@ -269,7 +264,7 @@ class ServerCreationService
$records = array_merge($records, $data['allocation_additional']); $records = array_merge($records, $data['allocation_additional']);
} }
$this->allocationRepository->updateWhereIn('id', $records, [ Allocation::query()->whereIn('id', $records)->update([
'server_id' => $server->id, 'server_id' => $server->id,
]); ]);
} }
@ -295,22 +290,6 @@ class ServerCreationService
} }
} }
/**
* Get the node that an allocation belongs to.
*
* @param int $id
* @return int
*
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/
private function getNodeFromAllocation(int $id): int
{
/** @var \Pterodactyl\Models\Allocation $allocation */
$allocation = $this->allocationRepository->setColumns(['id', 'node_id'])->find($id);
return $allocation->node_id;
}
/** /**
* Create a unique UUID and UUID-Short combo for a server. * Create a unique UUID and UUID-Short combo for a server.
* *

View file

@ -3,11 +3,10 @@
namespace Pterodactyl\Services\Servers; namespace Pterodactyl\Services\Servers;
use Exception; use Exception;
use Psr\Log\LoggerInterface; use Illuminate\Http\Response;
use Pterodactyl\Models\Server; use Pterodactyl\Models\Server;
use Illuminate\Support\Facades\Log;
use Illuminate\Database\ConnectionInterface; use Illuminate\Database\ConnectionInterface;
use Pterodactyl\Repositories\Eloquent\ServerRepository;
use Pterodactyl\Repositories\Eloquent\DatabaseRepository;
use Pterodactyl\Repositories\Wings\DaemonServerRepository; use Pterodactyl\Repositories\Wings\DaemonServerRepository;
use Pterodactyl\Services\Databases\DatabaseManagementService; use Pterodactyl\Services\Databases\DatabaseManagementService;
use Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException; use Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException;
@ -29,50 +28,26 @@ class ServerDeletionService
*/ */
private $daemonServerRepository; private $daemonServerRepository;
/**
* @var \Pterodactyl\Repositories\Eloquent\DatabaseRepository
*/
private $databaseRepository;
/** /**
* @var \Pterodactyl\Services\Databases\DatabaseManagementService * @var \Pterodactyl\Services\Databases\DatabaseManagementService
*/ */
private $databaseManagementService; private $databaseManagementService;
/**
* @var \Pterodactyl\Repositories\Eloquent\ServerRepository
*/
private $repository;
/**
* @var \Psr\Log\LoggerInterface
*/
private $writer;
/** /**
* DeletionService constructor. * DeletionService constructor.
* *
* @param \Illuminate\Database\ConnectionInterface $connection * @param \Illuminate\Database\ConnectionInterface $connection
* @param \Pterodactyl\Repositories\Wings\DaemonServerRepository $daemonServerRepository * @param \Pterodactyl\Repositories\Wings\DaemonServerRepository $daemonServerRepository
* @param \Pterodactyl\Repositories\Eloquent\DatabaseRepository $databaseRepository
* @param \Pterodactyl\Services\Databases\DatabaseManagementService $databaseManagementService * @param \Pterodactyl\Services\Databases\DatabaseManagementService $databaseManagementService
* @param \Pterodactyl\Repositories\Eloquent\ServerRepository $repository
* @param \Psr\Log\LoggerInterface $writer
*/ */
public function __construct( public function __construct(
ConnectionInterface $connection, ConnectionInterface $connection,
DaemonServerRepository $daemonServerRepository, DaemonServerRepository $daemonServerRepository,
DatabaseRepository $databaseRepository, DatabaseManagementService $databaseManagementService
DatabaseManagementService $databaseManagementService,
ServerRepository $repository,
LoggerInterface $writer
) { ) {
$this->connection = $connection; $this->connection = $connection;
$this->daemonServerRepository = $daemonServerRepository; $this->daemonServerRepository = $daemonServerRepository;
$this->databaseRepository = $databaseRepository;
$this->databaseManagementService = $databaseManagementService; $this->databaseManagementService = $databaseManagementService;
$this->repository = $repository;
$this->writer = $writer;
} }
/** /**
@ -101,27 +76,39 @@ class ServerDeletionService
try { try {
$this->daemonServerRepository->setServer($server)->delete(); $this->daemonServerRepository->setServer($server)->delete();
} catch (DaemonConnectionException $exception) { } catch (DaemonConnectionException $exception) {
if ($this->force) { // If there is an error not caused a 404 error and this isn't a forced delete,
$this->writer->warning($exception); // go ahead and bail out. We specifically ignore a 404 since that can be assumed
} else { // to be a safe error, meaning the server doesn't exist at all on Wings so there
// is no reason we need to bail out from that.
if (! $this->force && $exception->getStatusCode() !== Response::HTTP_NOT_FOUND) {
throw $exception; throw $exception;
} }
Log::warning($exception);
} }
$this->connection->transaction(function () use ($server) { $this->connection->transaction(function () use ($server) {
$this->databaseRepository->setColumns('id')->findWhere([['server_id', '=', $server->id]])->each(function ($item) { foreach ($server->databases as $database) {
try { try {
$this->databaseManagementService->delete($item->id); $this->databaseManagementService->delete($database);
} catch (Exception $exception) { } catch (Exception $exception) {
if ($this->force) { if (!$this->force) {
$this->writer->warning($exception);
} else {
throw $exception; throw $exception;
} }
}
});
$this->repository->delete($server->id); // Oh well, just try to delete the database entry we have from the database
// so that the server itself can be deleted. This will leave it dangling on
// the host instance, but we couldn't delete it anyways so not sure how we would
// handle this better anyways.
//
// @see https://github.com/pterodactyl/panel/issues/2085
$database->delete();
Log::warning($exception);
}
}
$server->delete();
}); });
} }
} }

View file

@ -2,13 +2,13 @@
namespace Pterodactyl\Services\Servers; namespace Pterodactyl\Services\Servers;
use Illuminate\Support\Arr;
use Pterodactyl\Models\Egg;
use Pterodactyl\Models\User; use Pterodactyl\Models\User;
use Pterodactyl\Models\Server; use Pterodactyl\Models\Server;
use Pterodactyl\Models\ServerVariable;
use Illuminate\Database\ConnectionInterface; use Illuminate\Database\ConnectionInterface;
use Pterodactyl\Traits\Services\HasUserLevels; use Pterodactyl\Traits\Services\HasUserLevels;
use Pterodactyl\Contracts\Repository\EggRepositoryInterface;
use Pterodactyl\Contracts\Repository\ServerRepositoryInterface;
use Pterodactyl\Contracts\Repository\ServerVariableRepositoryInterface;
class StartupModificationService class StartupModificationService
{ {
@ -19,63 +19,21 @@ class StartupModificationService
*/ */
private $connection; private $connection;
/**
* @var \Pterodactyl\Contracts\Repository\EggRepositoryInterface
*/
private $eggRepository;
/**
* @var \Pterodactyl\Services\Servers\EnvironmentService
*/
private $environmentService;
/**
* @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface
*/
private $repository;
/**
* @var \Pterodactyl\Contracts\Repository\ServerVariableRepositoryInterface
*/
private $serverVariableRepository;
/** /**
* @var \Pterodactyl\Services\Servers\VariableValidatorService * @var \Pterodactyl\Services\Servers\VariableValidatorService
*/ */
private $validatorService; private $validatorService;
/**
* @var \Pterodactyl\Services\Servers\ServerConfigurationStructureService
*/
private $structureService;
/** /**
* StartupModificationService constructor. * StartupModificationService constructor.
* *
* @param \Illuminate\Database\ConnectionInterface $connection * @param \Illuminate\Database\ConnectionInterface $connection
* @param \Pterodactyl\Contracts\Repository\EggRepositoryInterface $eggRepository
* @param \Pterodactyl\Services\Servers\EnvironmentService $environmentService
* @param \Pterodactyl\Contracts\Repository\ServerRepositoryInterface $repository
* @param \Pterodactyl\Services\Servers\ServerConfigurationStructureService $structureService
* @param \Pterodactyl\Contracts\Repository\ServerVariableRepositoryInterface $serverVariableRepository
* @param \Pterodactyl\Services\Servers\VariableValidatorService $validatorService * @param \Pterodactyl\Services\Servers\VariableValidatorService $validatorService
*/ */
public function __construct( public function __construct(ConnectionInterface $connection, VariableValidatorService $validatorService)
ConnectionInterface $connection, {
EggRepositoryInterface $eggRepository,
EnvironmentService $environmentService,
ServerRepositoryInterface $repository,
ServerConfigurationStructureService $structureService,
ServerVariableRepositoryInterface $serverVariableRepository,
VariableValidatorService $validatorService
) {
$this->connection = $connection; $this->connection = $connection;
$this->eggRepository = $eggRepository;
$this->environmentService = $environmentService;
$this->repository = $repository;
$this->serverVariableRepository = $serverVariableRepository;
$this->validatorService = $validatorService; $this->validatorService = $validatorService;
$this->structureService = $structureService;
} }
/** /**
@ -85,34 +43,42 @@ class StartupModificationService
* @param array $data * @param array $data
* @return \Pterodactyl\Models\Server * @return \Pterodactyl\Models\Server
* *
* @throws \Illuminate\Validation\ValidationException * @throws \Throwable
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/ */
public function handle(Server $server, array $data): Server public function handle(Server $server, array $data): Server
{ {
$this->connection->beginTransaction(); return $this->connection->transaction(function () use ($server, $data) {
if (! is_null(array_get($data, 'environment'))) { if (! empty($data['environment'])) {
$this->validatorService->setUserLevel($this->getUserLevel()); $egg = $this->isUserLevel(User::USER_LEVEL_ADMIN) ? ($data['egg_id'] ?? $server->egg_id) : $server->egg_id;
$results = $this->validatorService->handle(array_get($data, 'egg_id', $server->egg_id), array_get($data, 'environment', []));
$results->each(function ($result) use ($server) { $results = $this->validatorService
$this->serverVariableRepository->withoutFreshModel()->updateOrCreate([ ->setUserLevel($this->getUserLevel())
'server_id' => $server->id, ->handle($egg, $data['environment']);
'variable_id' => $result->id,
], [
'variable_value' => $result->value ?? '',
]);
});
}
if ($this->isUserLevel(User::USER_LEVEL_ADMIN)) { foreach ($results as $result) {
$this->updateAdministrativeSettings($data, $server); ServerVariable::query()->updateOrCreate(
} [
'server_id' => $server->id,
'variable_id' => $result->id,
],
['variable_value' => $result->value ?? '']
);
}
}
$this->connection->commit(); if ($this->isUserLevel(User::USER_LEVEL_ADMIN)) {
$this->updateAdministrativeSettings($data, $server);
}
return $server; // Calling ->refresh() rather than ->fresh() here causes it to return the
// variables as triplicates for some reason? Not entirely sure, should dig
// in more to figure it out, but luckily we have a test case covering this
// specific call so we can be assured we're not breaking it _here_ at least.
//
// TODO(dane): this seems like a red-flag for the code powering the relationship
// that should be looked into more.
return $server->fresh();
});
} }
/** /**
@ -120,28 +86,26 @@ class StartupModificationService
* *
* @param array $data * @param array $data
* @param \Pterodactyl\Models\Server $server * @param \Pterodactyl\Models\Server $server
*
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/ */
private function updateAdministrativeSettings(array $data, Server &$server) protected function updateAdministrativeSettings(array $data, Server &$server)
{ {
if ( $eggId = Arr::get($data, 'egg_id');
is_digit(array_get($data, 'egg_id'))
&& $data['egg_id'] != $server->egg_id if (is_digit($eggId) && $server->egg_id !== (int)$eggId) {
&& is_null(array_get($data, 'nest_id')) /** @var \Pterodactyl\Models\Egg $egg */
) { $egg = Egg::query()->findOrFail($data['egg_id']);
$egg = $this->eggRepository->setColumns(['id', 'nest_id'])->find($data['egg_id']);
$data['nest_id'] = $egg->nest_id; $server = $server->forceFill([
'egg_id' => $egg->id,
'nest_id' => $egg->nest_id,
]);
} }
$server = $this->repository->update($server->id, [ $server->forceFill([
'installed' => 0, 'installed' => 0,
'startup' => array_get($data, 'startup', $server->startup), 'startup' => $data['startup'] ?? $server->startup,
'nest_id' => array_get($data, 'nest_id', $server->nest_id), 'skip_scripts' => $data['skip_scripts'] ?? isset($data['skip_scripts']),
'egg_id' => array_get($data, 'egg_id', $server->egg_id), 'image' => $data['docker_image'] ?? $server->image,
'skip_scripts' => array_get($data, 'skip_scripts') ?? isset($data['skip_scripts']), ])->save();
'image' => array_get($data, 'docker_image', $server->image),
]);
} }
} }

View file

@ -2,12 +2,10 @@
namespace Pterodactyl\Services\Servers; namespace Pterodactyl\Services\Servers;
use Psr\Log\LoggerInterface;
use Webmozart\Assert\Assert; use Webmozart\Assert\Assert;
use Pterodactyl\Models\Server; use Pterodactyl\Models\Server;
use Illuminate\Database\ConnectionInterface; use Illuminate\Database\ConnectionInterface;
use Pterodactyl\Repositories\Wings\DaemonServerRepository; use Pterodactyl\Repositories\Wings\DaemonServerRepository;
use Pterodactyl\Contracts\Repository\ServerRepositoryInterface;
class SuspensionService class SuspensionService
{ {
@ -19,16 +17,6 @@ class SuspensionService
*/ */
private $connection; private $connection;
/**
* @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface
*/
private $repository;
/**
* @var \Psr\Log\LoggerInterface
*/
private $writer;
/** /**
* @var \Pterodactyl\Repositories\Wings\DaemonServerRepository * @var \Pterodactyl\Repositories\Wings\DaemonServerRepository
*/ */
@ -39,25 +27,19 @@ class SuspensionService
* *
* @param \Illuminate\Database\ConnectionInterface $connection * @param \Illuminate\Database\ConnectionInterface $connection
* @param \Pterodactyl\Repositories\Wings\DaemonServerRepository $daemonServerRepository * @param \Pterodactyl\Repositories\Wings\DaemonServerRepository $daemonServerRepository
* @param \Pterodactyl\Contracts\Repository\ServerRepositoryInterface $repository
* @param \Psr\Log\LoggerInterface $writer
*/ */
public function __construct( public function __construct(
ConnectionInterface $connection, ConnectionInterface $connection,
DaemonServerRepository $daemonServerRepository, DaemonServerRepository $daemonServerRepository
ServerRepositoryInterface $repository,
LoggerInterface $writer
) { ) {
$this->connection = $connection; $this->connection = $connection;
$this->repository = $repository;
$this->writer = $writer;
$this->daemonServerRepository = $daemonServerRepository; $this->daemonServerRepository = $daemonServerRepository;
} }
/** /**
* Suspends a server on the system. * Suspends a server on the system.
* *
* @param int|\Pterodactyl\Models\Server $server * @param \Pterodactyl\Models\Server $server
* @param string $action * @param string $action
* *
* @throws \Throwable * @throws \Throwable
@ -66,15 +48,16 @@ class SuspensionService
{ {
Assert::oneOf($action, [self::ACTION_SUSPEND, self::ACTION_UNSUSPEND]); Assert::oneOf($action, [self::ACTION_SUSPEND, self::ACTION_UNSUSPEND]);
if ( $isSuspending = $action === self::ACTION_SUSPEND;
$action === self::ACTION_SUSPEND && $server->suspended || // Nothing needs to happen if we're suspending the server and it is already
$action === self::ACTION_UNSUSPEND && ! $server->suspended // suspended in the database. Additionally, nothing needs to happen if the server
) { // is not suspended and we try to un-suspend the instance.
if ($isSuspending === $server->suspended) {
return; return;
} }
$this->connection->transaction(function () use ($action, $server) { $this->connection->transaction(function () use ($action, $server) {
$this->repository->withoutFreshModel()->update($server->id, [ $server->update([
'suspended' => $action === self::ACTION_SUSPEND, 'suspended' => $action === self::ACTION_SUSPEND,
]); ]);

View file

@ -11,32 +11,15 @@ namespace Pterodactyl\Services\Servers;
use Pterodactyl\Models\User; use Pterodactyl\Models\User;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Pterodactyl\Models\EggVariable;
use Illuminate\Validation\ValidationException; use Illuminate\Validation\ValidationException;
use Pterodactyl\Traits\Services\HasUserLevels; use Pterodactyl\Traits\Services\HasUserLevels;
use Pterodactyl\Contracts\Repository\ServerRepositoryInterface;
use Illuminate\Contracts\Validation\Factory as ValidationFactory; use Illuminate\Contracts\Validation\Factory as ValidationFactory;
use Pterodactyl\Contracts\Repository\EggVariableRepositoryInterface;
use Pterodactyl\Contracts\Repository\ServerVariableRepositoryInterface;
class VariableValidatorService class VariableValidatorService
{ {
use HasUserLevels; use HasUserLevels;
/**
* @var \Pterodactyl\Contracts\Repository\EggVariableRepositoryInterface
*/
private $optionVariableRepository;
/**
* @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface
*/
private $serverRepository;
/**
* @var \Pterodactyl\Contracts\Repository\ServerVariableRepositoryInterface
*/
private $serverVariableRepository;
/** /**
* @var \Illuminate\Contracts\Validation\Factory * @var \Illuminate\Contracts\Validation\Factory
*/ */
@ -45,20 +28,10 @@ class VariableValidatorService
/** /**
* VariableValidatorService constructor. * VariableValidatorService constructor.
* *
* @param \Pterodactyl\Contracts\Repository\EggVariableRepositoryInterface $optionVariableRepository
* @param \Pterodactyl\Contracts\Repository\ServerRepositoryInterface $serverRepository
* @param \Pterodactyl\Contracts\Repository\ServerVariableRepositoryInterface $serverVariableRepository
* @param \Illuminate\Contracts\Validation\Factory $validator * @param \Illuminate\Contracts\Validation\Factory $validator
*/ */
public function __construct( public function __construct(ValidationFactory $validator)
EggVariableRepositoryInterface $optionVariableRepository, {
ServerRepositoryInterface $serverRepository,
ServerVariableRepositoryInterface $serverVariableRepository,
ValidationFactory $validator
) {
$this->optionVariableRepository = $optionVariableRepository;
$this->serverRepository = $serverRepository;
$this->serverVariableRepository = $serverVariableRepository;
$this->validator = $validator; $this->validator = $validator;
} }
@ -72,16 +45,18 @@ class VariableValidatorService
*/ */
public function handle(int $egg, array $fields = []): Collection public function handle(int $egg, array $fields = []): Collection
{ {
$variables = $this->optionVariableRepository->findWhere([['egg_id', '=', $egg]]); $query = EggVariable::query()->where('egg_id', $egg);
if (! $this->isUserLevel(User::USER_LEVEL_ADMIN)) {
// Don't attempt to validate variables if they aren't user editable
// and we're not running this at an admin level.
$query = $query->where('user_editable', true)->where('user_viewable', true);
}
/** @var \Pterodactyl\Models\EggVariable[] $variables */
$variables = $query->get();
$data = $rules = $customAttributes = []; $data = $rules = $customAttributes = [];
foreach ($variables as $variable) { foreach ($variables as $variable) {
// Don't attempt to validate variables if they aren't user editable
// and we're not running this at an admin level.
if (! $variable->user_editable && ! $this->isUserLevel(User::USER_LEVEL_ADMIN)) {
continue;
}
$data['environment'][$variable->env_variable] = array_get($fields, $variable->env_variable); $data['environment'][$variable->env_variable] = array_get($fields, $variable->env_variable);
$rules['environment.' . $variable->env_variable] = $variable->rules; $rules['environment.' . $variable->env_variable] = $variable->rules;
$customAttributes['environment.' . $variable->env_variable] = trans('validation.internal.variable_value', ['env' => $variable->name]); $customAttributes['environment.' . $variable->env_variable] = trans('validation.internal.variable_value', ['env' => $variable->name]);
@ -92,23 +67,12 @@ class VariableValidatorService
throw new ValidationException($validator); throw new ValidationException($validator);
} }
$response = $variables->filter(function ($item) { return Collection::make($variables)->map(function ($item) use ($fields) {
// Skip doing anything if user is not an admin and variable is not user viewable or editable. return (object)[
if (! $this->isUserLevel(User::USER_LEVEL_ADMIN) && (! $item->user_editable || ! $item->user_viewable)) {
return false;
}
return true;
})->map(function ($item) use ($fields) {
return (object) [
'id' => $item->id, 'id' => $item->id,
'key' => $item->env_variable, 'key' => $item->env_variable,
'value' => array_get($fields, $item->env_variable), 'value' => $fields[$item->env_variable] ?? null,
]; ];
})->filter(function ($item) {
return is_object($item);
}); });
return $response;
} }
} }

View file

@ -67,7 +67,7 @@ class ServerTransformer extends BaseClientTransformer
'allocations' => $server->allocation_limit, 'allocations' => $server->allocation_limit,
'backups' => $server->backup_limit, 'backups' => $server->backup_limit,
], ],
'is_suspended' => $server->suspended !== 0, 'is_suspended' => $server->suspended,
'is_installing' => $server->installed !== 1, 'is_installing' => $server->installed !== 1,
]; ];
} }

View file

@ -1,5 +1,6 @@
<?php <?php
use Monolog\Handler\NullHandler;
use Monolog\Handler\StreamHandler; use Monolog\Handler\StreamHandler;
return [ return [
@ -75,5 +76,10 @@ return [
'driver' => 'errorlog', 'driver' => 'errorlog',
'level' => 'debug', 'level' => 'debug',
], ],
'null' => [
'driver' => 'monolog',
'handler' => NullHandler::class,
],
], ],
]; ];

View file

@ -1,5 +1,6 @@
<?php <?php
use Carbon\Carbon;
use Ramsey\Uuid\Uuid; use Ramsey\Uuid\Uuid;
use Cake\Chronos\Chronos; use Cake\Chronos\Chronos;
use Illuminate\Support\Str; use Illuminate\Support\Str;
@ -7,6 +8,7 @@ use Pterodactyl\Models\Node;
use Faker\Generator as Faker; use Faker\Generator as Faker;
use Pterodactyl\Models\ApiKey; use Pterodactyl\Models\ApiKey;
/** @var \Illuminate\Database\Eloquent\Factory $factory */
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
| Model Factories | Model Factories
@ -35,8 +37,8 @@ $factory->define(Pterodactyl\Models\Server::class, function (Faker $faker) {
'installed' => 1, 'installed' => 1,
'database_limit' => null, 'database_limit' => null,
'allocation_limit' => null, 'allocation_limit' => null,
'created_at' => \Carbon\Carbon::now(), 'created_at' => Carbon::now(),
'updated_at' => \Carbon\Carbon::now(), 'updated_at' => Carbon::now(),
]; ];
}); });
@ -160,8 +162,8 @@ $factory->define(Pterodactyl\Models\Database::class, function (Faker $faker) {
'username' => str_random(10), 'username' => str_random(10),
'remote' => '%', 'remote' => '%',
'password' => $password ?: bcrypt('test123'), 'password' => $password ?: bcrypt('test123'),
'created_at' => \Carbon\Carbon::now()->toDateTimeString(), 'created_at' => Carbon::now()->toDateTimeString(),
'updated_at' => \Carbon\Carbon::now()->toDateTimeString(), 'updated_at' => Carbon::now()->toDateTimeString(),
]; ];
}); });
@ -190,7 +192,7 @@ $factory->define(Pterodactyl\Models\ApiKey::class, function (Faker $faker) {
'token' => $token ?: $token = encrypt(str_random(Pterodactyl\Models\ApiKey::KEY_LENGTH)), 'token' => $token ?: $token = encrypt(str_random(Pterodactyl\Models\ApiKey::KEY_LENGTH)),
'allowed_ips' => null, 'allowed_ips' => null,
'memo' => 'Test Function Key', 'memo' => 'Test Function Key',
'created_at' => \Carbon\Carbon::now()->toDateTimeString(), 'created_at' => Carbon::now()->toDateTimeString(),
'updated_at' => \Carbon\Carbon::now()->toDateTimeString(), 'updated_at' => Carbon::now()->toDateTimeString(),
]; ];
}); });

View file

@ -0,0 +1,41 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class ChangeUniqueDatabaseNameToAccountForServer extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('databases', function (Blueprint $table) {
$table->dropUnique(['database_host_id', 'database']);
});
Schema::table('databases', function (Blueprint $table) {
$table->unique(['database_host_id', 'server_id', 'database']);
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('databases', function (Blueprint $table) {
$table->dropUnique(['database_host_id', 'server_id', 'database']);
});
Schema::table('databases', function (Blueprint $table) {
$table->unique(['database_host_id', 'database']);
});
}
}

View file

@ -130,7 +130,7 @@ class EggSeeder extends Seeder
['nest_id', '=', $nest->id], ['nest_id', '=', $nest->id],
]); ]);
$this->updateImporterService->handle($egg->id, $file); $this->updateImporterService->handle($egg, $file);
$this->command->info('Updated ' . $decoded->name); $this->command->info('Updated ' . $decoded->name);
} catch (RecordNotFoundException $exception) { } catch (RecordNotFoundException $exception) {

View file

@ -121,6 +121,8 @@ class ApiKeyControllerTest extends ClientApiIntegrationTestCase
/** /**
* Test that a bad request results in a validation error being returned by the API. * Test that a bad request results in a validation error being returned by the API.
*
* @see https://github.com/pterodactyl/panel/issues/2457
*/ */
public function testValidationErrorIsReturnedForBadRequests() public function testValidationErrorIsReturnedForBadRequests()
{ {
@ -135,6 +137,15 @@ class ApiKeyControllerTest extends ClientApiIntegrationTestCase
$response->assertStatus(Response::HTTP_UNPROCESSABLE_ENTITY); $response->assertStatus(Response::HTTP_UNPROCESSABLE_ENTITY);
$response->assertJsonPath('errors.0.meta.rule', 'required'); $response->assertJsonPath('errors.0.meta.rule', 'required');
$response->assertJsonPath('errors.0.detail', 'The description field is required.'); $response->assertJsonPath('errors.0.detail', 'The description field is required.');
$response = $this->actingAs($user)->postJson('/api/client/account/api-keys', [
'description' => str_repeat('a', 501),
'allowed_ips' => ['127.0.0.1'],
]);
$response->assertStatus(Response::HTTP_UNPROCESSABLE_ENTITY);
$response->assertJsonPath('errors.0.meta.rule', 'max');
$response->assertJsonPath('errors.0.detail', 'The description may not be greater than 500 characters.');
} }
/** /**

View file

@ -9,7 +9,6 @@ use Pterodactyl\Models\Node;
use Pterodactyl\Models\Task; use Pterodactyl\Models\Task;
use Pterodactyl\Models\User; use Pterodactyl\Models\User;
use Webmozart\Assert\Assert; use Webmozart\Assert\Assert;
use Pterodactyl\Models\Model;
use Pterodactyl\Models\Server; use Pterodactyl\Models\Server;
use Pterodactyl\Models\Subuser; use Pterodactyl\Models\Subuser;
use Pterodactyl\Models\Location; use Pterodactyl\Models\Location;

View file

@ -0,0 +1,69 @@
<?php
namespace Pterodactyl\Tests\Integration\Api\Client\Server\Startup;
use Pterodactyl\Models\User;
use Pterodactyl\Models\Permission;
use Pterodactyl\Models\EggVariable;
use Pterodactyl\Tests\Integration\Api\Client\ClientApiIntegrationTestCase;
class GetStartupAndVariablesTest extends ClientApiIntegrationTestCase
{
/**
* Test that the startup command and variables are returned for a server, but only the variables
* that can be viewed by a user (e.g. user_viewable=true).
*
* @param array $permissions
* @dataProvider permissionsDataProvider
*/
public function testStartupVariablesAreReturnedForServer($permissions)
{
/** @var \Pterodactyl\Models\Server $server */
[$user, $server] = $this->generateTestAccount($permissions);
$egg = $this->cloneEggAndVariables($server->egg);
// BUNGEE_VERSION should never be returned back to the user in this API call, either in
// the array of variables, or revealed in the startup command.
$egg->variables()->first()->update([
'user_viewable' => false,
]);
$server->fill([
'egg_id' => $egg->id,
'startup' => 'java {{SERVER_JARFILE}} --version {{BUNGEE_VERSION}}',
])->save();
$server = $server->refresh();
$response = $this->actingAs($user)->getJson($this->link($server) . "/startup");
$response->assertOk();
$response->assertJsonPath('meta.startup_command', 'java bungeecord.jar --version [hidden]');
$response->assertJsonPath('meta.raw_startup_command', $server->startup);
$response->assertJsonPath('object', 'list');
$response->assertJsonCount(1, 'data');
$response->assertJsonPath('data.0.object', EggVariable::RESOURCE_NAME);
$this->assertJsonTransformedWith($response->json('data.0.attributes'), $egg->variables[1]);
}
/**
* Test that a user without the required permission, or who does not have any permission to
* access the server cannot get the startup information for it.
*/
public function testStartupDataIsNotReturnedWithoutPermission()
{
[$user, $server] = $this->generateTestAccount([Permission::ACTION_WEBSOCKET_CONNECT]);
$this->actingAs($user)->getJson($this->link($server) . "/startup")->assertForbidden();
$user2 = factory(User::class)->create();
$this->actingAs($user2)->getJson($this->link($server) . "/startup")->assertNotFound();
}
/**
* @return array[]
*/
public function permissionsDataProvider()
{
return [[[]], [[Permission::ACTION_STARTUP_READ]]];
}
}

View file

@ -0,0 +1,161 @@
<?php
namespace Pterodactyl\Tests\Integration\Api\Client\Server\Startup;
use Pterodactyl\Models\User;
use Illuminate\Http\Response;
use Pterodactyl\Models\Permission;
use Pterodactyl\Models\EggVariable;
use Pterodactyl\Tests\Integration\Api\Client\ClientApiIntegrationTestCase;
class UpdateStartupVariableTest extends ClientApiIntegrationTestCase
{
/**
* Test that a startup variable can be edited successfully for a server.
*
* @param array $permissions
* @dataProvider permissionsDataProvider
*/
public function testStartupVariableCanBeUpdated($permissions)
{
/** @var \Pterodactyl\Models\Server $server */
[$user, $server] = $this->generateTestAccount($permissions);
$server->fill([
'startup' => 'java {{SERVER_JARFILE}} --version {{BUNGEE_VERSION}}',
])->save();
$response = $this->actingAs($user)->putJson($this->link($server) . '/startup/variable', [
'key' => 'BUNGEE_VERSION',
'value' => '1.2.3',
]);
$response->assertStatus(Response::HTTP_UNPROCESSABLE_ENTITY);
$response->assertJsonPath('errors.0.code', 'ValidationException');
$response->assertJsonPath('errors.0.detail', 'The value may only contain letters and numbers.');
$response = $this->actingAs($user)->putJson($this->link($server) . '/startup/variable', [
'key' => 'BUNGEE_VERSION',
'value' => '123',
]);
$response->assertOk();
$response->assertJsonPath('object', EggVariable::RESOURCE_NAME);
$this->assertJsonTransformedWith($response->json('attributes'), $server->variables[0]);
$response->assertJsonPath('meta.startup_command', 'java bungeecord.jar --version 123');
$response->assertJsonPath('meta.raw_startup_command', $server->startup);
}
/**
* Test that variables that are either not user_viewable, or not user_editable, cannot be
* updated via this endpoint.
*
* @param array $permissions
* @dataProvider permissionsDataProvider
*/
public function testStartupVariableCannotBeUpdatedIfNotUserViewableOrEditable(array $permissions)
{
/** @var \Pterodactyl\Models\Server $server */
[$user, $server] = $this->generateTestAccount($permissions);
$egg = $this->cloneEggAndVariables($server->egg);
$egg->variables()->where('env_variable', 'BUNGEE_VERSION')->update(['user_viewable' => false]);
$egg->variables()->where('env_variable', 'SERVER_JARFILE')->update(['user_editable' => false]);
$server->fill(['egg_id' => $egg->id])->save();
$server->refresh();
$response = $this->actingAs($user)->putJson($this->link($server) . '/startup/variable', [
'key' => 'BUNGEE_VERSION',
'value' => '123',
]);
$response->assertStatus(Response::HTTP_BAD_REQUEST);
$response->assertJsonPath('errors.0.code', 'BadRequestHttpException');
$response->assertJsonPath('errors.0.detail', 'The environment variable you are trying to edit does not exist.');
$response = $this->actingAs($user)->putJson($this->link($server) . '/startup/variable', [
'key' => 'SERVER_JARFILE',
'value' => 'server2.jar',
]);
$response->assertStatus(Response::HTTP_BAD_REQUEST);
$response->assertJsonPath('errors.0.code', 'BadRequestHttpException');
$response->assertJsonPath('errors.0.detail', 'The environment variable you are trying to edit is read-only.');
}
/**
* Test that a hidden variable is not included in the startup_command output for the server if
* a different variable is updated.
*/
public function testHiddenVariablesAreNotReturnedInStartupCommandWhenUpdatingVariable()
{
/** @var \Pterodactyl\Models\Server $server */
[$user, $server] = $this->generateTestAccount();
$egg = $this->cloneEggAndVariables($server->egg);
$egg->variables()->first()->update(['user_viewable' => false]);
$server->fill([
'egg_id' => $egg->id,
'startup' => 'java {{SERVER_JARFILE}} --version {{BUNGEE_VERSION}}',
])->save();
$server->refresh();
$response = $this->actingAs($user)->putJson($this->link($server) . '/startup/variable', [
'key' => 'SERVER_JARFILE',
'value' => 'server2.jar',
]);
$response->assertOk();
$response->assertJsonPath('meta.startup_command', 'java server2.jar --version [hidden]');
$response->assertJsonPath('meta.raw_startup_command', $server->startup);
}
/**
* Test that an egg variable with a validation rule of 'nullable|string' works if no value
* is passed through in the request.
*
* @see https://github.com/pterodactyl/panel/issues/2433
*/
public function testEggVariableWithNullableStringIsNotRequired()
{
/** @var \Pterodactyl\Models\Server $server */
[$user, $server] = $this->generateTestAccount();
$egg = $this->cloneEggAndVariables($server->egg);
$egg->variables()->first()->update(['rules' => 'nullable|string']);
$server->fill(['egg_id' => $egg->id])->save();
$server->refresh();
$response = $this->actingAs($user)->putJson($this->link($server) . '/startup/variable', [
'key' => 'BUNGEE_VERSION',
'value' => '',
]);
$response->assertOk();
$response->assertJsonPath('attributes.server_value', null);
}
/**
* Test that a variable cannot be updated if the user does not have permission to perform
* that action, or they aren't assigned at all to the server.
*/
public function testStartupVariableCannotBeUpdatedIfNotUserViewable()
{
[$user, $server] = $this->generateTestAccount([Permission::ACTION_WEBSOCKET_CONNECT]);
$this->actingAs($user)->putJson($this->link($server) . "/startup/variable")->assertForbidden();
$user2 = factory(User::class)->create();
$this->actingAs($user2)->putJson($this->link($server) . "/startup/variable")->assertNotFound();
}
/**
* @return \array[][]
*/
public function permissionsDataProvider()
{
return [[[]], [[Permission::ACTION_STARTUP_UPDATE]]];
}
}

View file

@ -0,0 +1,59 @@
<?php
namespace Pterodactyl\Tests\Integration\Http\Controllers\Admin;
use Illuminate\Support\Str;
use Illuminate\Http\Request;
use Pterodactyl\Models\User;
use Pterodactyl\Models\Subuser;
use Illuminate\Pagination\LengthAwarePaginator;
use Pterodactyl\Tests\Integration\IntegrationTestCase;
use Pterodactyl\Http\Controllers\Admin\UserController;
class UserControllerTest extends IntegrationTestCase
{
/**
* Test that the index route controller for the user listing returns the expected user
* data with the number of servers they are assigned to, and the number of servers they
* are a subuser of.
*
* @see https://github.com/pterodactyl/panel/issues/2469
*/
public function testIndexReturnsExpectedData()
{
$unique = Str::random(16);
$users = [
factory(User::class)->create(['username' => $unique . '_1']),
factory(User::class)->create(['username' => $unique . '_2']),
];
$servers = [
$this->createServerModel(['owner_id' => $users[0]->id]),
$this->createServerModel(['owner_id' => $users[0]->id]),
$this->createServerModel(['owner_id' => $users[0]->id]),
$this->createServerModel(['owner_id' => $users[1]->id]),
];
Subuser::query()->forceCreate(['server_id' => $servers[0]->id, 'user_id' => $users[1]->id]);
Subuser::query()->forceCreate(['server_id' => $servers[1]->id, 'user_id' => $users[1]->id]);
/** @var \Pterodactyl\Http\Controllers\Admin\UserController $controller */
$controller = $this->app->make(UserController::class);
$request = Request::create('/admin/users?filter[username]=' . $unique, 'GET');
$this->app->instance(Request::class, $request);
$data = $controller->index($request)->getData();
$this->assertArrayHasKey('users', $data);
$this->assertInstanceOf(LengthAwarePaginator::class, $data['users']);
/** @var \Pterodactyl\Models\User[] $response */
$response = $data['users']->items();
$this->assertCount(2, $response);
$this->assertInstanceOf(User::class, $response[0]);
$this->assertSame(3, (int)$response[0]->servers_count);
$this->assertSame(0, (int)$response[0]->subuser_of_count);
$this->assertSame(1, (int)$response[1]->servers_count);
$this->assertSame(2, (int)$response[1]->subuser_of_count);
}
}

View file

@ -0,0 +1,225 @@
<?php
namespace Pterodactyl\Tests\Integration\Services\Databases;
use Mockery;
use Exception;
use BadMethodCallException;
use InvalidArgumentException;
use Pterodactyl\Models\Database;
use Pterodactyl\Models\DatabaseHost;
use Pterodactyl\Tests\Integration\IntegrationTestCase;
use Pterodactyl\Repositories\Eloquent\DatabaseRepository;
use Pterodactyl\Services\Databases\DatabaseManagementService;
use Pterodactyl\Exceptions\Repository\DuplicateDatabaseNameException;
use Pterodactyl\Exceptions\Service\Database\TooManyDatabasesException;
use Pterodactyl\Exceptions\Service\Database\DatabaseClientFeatureNotEnabledException;
class DatabaseManagementServiceTest extends IntegrationTestCase
{
/** @var \Mockery\MockInterface */
private $repository;
/**
* Setup tests.
*/
public function setUp(): void
{
parent::setUp();
config()->set('pterodactyl.client_features.databases.enabled', true);
$this->repository = Mockery::mock(DatabaseRepository::class);
$this->swap(DatabaseRepository::class, $this->repository);
}
/**
* Test that the name generated by the unique name function is what we expect.
*/
public function testUniqueDatabaseNameIsGeneratedCorrectly()
{
$this->assertSame('s1_example', DatabaseManagementService::generateUniqueDatabaseName('example', 1));
$this->assertSame('s123_something_else', DatabaseManagementService::generateUniqueDatabaseName('something_else', 123));
$this->assertSame('s123_' . str_repeat('a', 43), DatabaseManagementService::generateUniqueDatabaseName(str_repeat('a', 100), 123));
}
/**
* Test that disabling the client database feature flag prevents the creation of databases.
*/
public function testExceptionIsThrownIfClientDatabasesAreNotEnabled()
{
config()->set('pterodactyl.client_features.databases.enabled', false);
$this->expectException(DatabaseClientFeatureNotEnabledException::class);
$server = $this->createServerModel();
$this->getService()->create($server, []);
}
/**
* Test that a server at its database limit cannot have an additional one created if
* the $validateDatabaseLimit flag is not set to false.
*/
public function testDatabaseCannotBeCreatedIfServerHasReachedLimit()
{
$server = $this->createServerModel(['database_limit' => 2]);
$host = factory(DatabaseHost::class)->create(['node_id' => $server->node_id]);
factory(Database::class)->times(2)->create(['server_id' => $server->id, 'database_host_id' => $host->id]);
$this->expectException(TooManyDatabasesException::class);
$this->getService()->create($server, []);
}
/**
* Test that a missing or invalid database name format causes an exception to be thrown.
*
* @param array $data
* @dataProvider invalidDataDataProvider
*/
public function testEmptyDatabaseNameOrInvalidNameTriggersAnException($data)
{
$server = $this->createServerModel();
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('The database name passed to DatabaseManagementService::handle MUST be prefixed with "s{server_id}_".');
$this->getService()->create($server, $data);
}
/**
* Test that creating a server database with an identical name triggers an exception.
*/
public function testCreatingDatabaseWithIdenticalNameTriggersAnException()
{
$server = $this->createServerModel();
$name = DatabaseManagementService::generateUniqueDatabaseName('soemthing', $server->id);
$host = factory(DatabaseHost::class)->create(['node_id' => $server->node_id]);
$host2 = factory(DatabaseHost::class)->create(['node_id' => $server->node_id]);
factory(Database::class)->create([
'database' => $name,
'database_host_id' => $host->id,
'server_id' => $server->id,
]);
$this->expectException(DuplicateDatabaseNameException::class);
$this->expectExceptionMessage('A database with that name already exists for this server.');
// Try to create a database with the same name as a database on a different host. We expect
// this to fail since we don't account for the specific host when checking uniqueness.
$this->getService()->create($server, [
'database' => $name,
'database_host_id' => $host2->id,
]);
$this->assertDatabaseMissing('databases', ['server_id' => $server->id]);
}
/**
* Test that a server database can be created successfully.
*/
public function testServerDatabaseCanBeCreated()
{
$server = $this->createServerModel();
$name = DatabaseManagementService::generateUniqueDatabaseName('soemthing', $server->id);
$host = factory(DatabaseHost::class)->create(['node_id' => $server->node_id]);
$this->repository->expects('createDatabase')->with($name);
$username = null;
$secondUsername = null;
$password = null;
// The value setting inside the closures if to avoid throwing an exception during the
// assertions that would get caught by the functions catcher and thus lead to the exception
// being swallowed incorrectly.
$this->repository->expects('createUser')->with(
Mockery::on(function ($value) use (&$username) {
$username = $value;
return true;
}),
'%',
Mockery::on(function ($value) use (&$password) {
$password = $value;
return true;
}),
null
);
$this->repository->expects('assignUserToDatabase')->with($name, Mockery::on(function ($value) use (&$secondUsername) {
$secondUsername = $value;
return true;
}), '%');
$this->repository->expects('flush')->withNoArgs();
$response = $this->getService()->create($server, [
'remote' => '%',
'database' => $name,
'database_host_id' => $host->id,
]);
$this->assertInstanceOf(Database::class, $response);
$this->assertSame($response->server_id, $server->id);
$this->assertRegExp('/^(u[\d]+_)(\w){10}$/', $username);
$this->assertSame($username, $secondUsername);
$this->assertSame(24, strlen($password));
$this->assertDatabaseHas('databases', ['server_id' => $server->id, 'id' => $response->id]);
}
/**
* Test that an exception encountered while creating the database leads to cleanup code being called
* and any exceptions encountered while cleaning up go unreported.
*/
public function testExceptionEncounteredWhileCreatingDatabaseAttemptsToCleanup()
{
$server = $this->createServerModel();
$name = DatabaseManagementService::generateUniqueDatabaseName('soemthing', $server->id);
$host = factory(DatabaseHost::class)->create(['node_id' => $server->node_id]);
$this->repository->expects('createDatabase')->with($name)->andThrows(new BadMethodCallException);
$this->repository->expects('dropDatabase')->with($name);
$this->repository->expects('dropUser')->withAnyArgs()->andThrows(new InvalidArgumentException);
$this->expectException(BadMethodCallException::class);
$this->getService()->create($server, [
'remote' => '%',
'database' => $name,
'database_host_id' => $host->id,
]);
$this->assertDatabaseMissing('databases', ['server_id' => $server->id]);
}
/**
* @return array
*/
public function invalidDataDataProvider(): array
{
return [
[[]],
[['database' => '']],
[['database' => 'something']],
[['database' => 's_something']],
[['database' => 's12s_something']],
[['database' => 's12something']],
];
}
/**
* @return \Pterodactyl\Services\Databases\DatabaseManagementService
*/
private function getService()
{
return $this->app->make(DatabaseManagementService::class);
}
}

View file

@ -0,0 +1,168 @@
<?php
namespace Pterodactyl\Tests\Integration\Services\Databases;
use Mockery;
use Pterodactyl\Models\Node;
use InvalidArgumentException;
use Pterodactyl\Models\Database;
use Pterodactyl\Models\DatabaseHost;
use Symfony\Component\VarDumper\Cloner\Data;
use Pterodactyl\Tests\Integration\IntegrationTestCase;
use Pterodactyl\Services\Databases\DatabaseManagementService;
use Pterodactyl\Services\Databases\DeployServerDatabaseService;
use Pterodactyl\Exceptions\Service\Database\NoSuitableDatabaseHostException;
class DeployServerDatabaseServiceTest extends IntegrationTestCase
{
/** @var \Mockery\MockInterface */
private $managementService;
/**
* Setup tests.
*/
public function setUp(): void
{
parent::setUp();
$this->managementService = Mockery::mock(DatabaseManagementService::class);
$this->swap(DatabaseManagementService::class, $this->managementService);
}
/**
* Ensure we reset the config to the expected value.
*/
protected function tearDown(): void
{
config()->set('pterodactyl.client_features.databases.allow_random', true);
Database::query()->delete();
DatabaseHost::query()->delete();
parent::tearDown();
}
/**
* Test that an error is thrown if either the database name or the remote host are empty.
*
* @param array $data
* @dataProvider invalidDataProvider
*/
public function testErrorIsThrownIfDatabaseNameIsEmpty($data)
{
$server = $this->createServerModel();
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessageMatches('/^Expected a non-empty value\. Got: /',);
$this->getService()->handle($server, $data);
}
/**
* Test that an error is thrown if there are no database hosts on the same node as the
* server and the allow_random config value is false.
*/
public function testErrorIsThrownIfNoDatabaseHostsExistOnNode()
{
$server = $this->createServerModel();
$node = factory(Node::class)->create(['location_id' => $server->location->id]);
factory(DatabaseHost::class)->create(['node_id' => $node->id]);
config()->set('pterodactyl.client_features.databases.allow_random', false);
$this->expectException(NoSuitableDatabaseHostException::class);
$this->getService()->handle($server, [
'database' => 'something',
'remote' => '%',
]);
}
/**
* Test that an error is thrown if no database hosts exist at all on the system.
*/
public function testErrorIsThrownIfNoDatabaseHostsExistOnSystem()
{
$server = $this->createServerModel();
$this->expectException(NoSuitableDatabaseHostException::class);
$this->getService()->handle($server, [
'database' => 'something',
'remote' => '%',
]);
}
/**
* Test that a database host on the same node as the server is preferred.
*/
public function testDatabaseHostOnSameNodeIsPreferred()
{
$server = $this->createServerModel();
$node = factory(Node::class)->create(['location_id' => $server->location->id]);
factory(DatabaseHost::class)->create(['node_id' => $node->id]);
$host = factory(DatabaseHost::class)->create(['node_id' => $server->node_id]);
$this->managementService->expects('create')->with($server, [
'database_host_id' => $host->id,
'database' => "s{$server->id}_something",
'remote' => '%',
])->andReturns(new Database);
$response = $this->getService()->handle($server, [
'database' => 'something',
'remote' => '%',
]);
$this->assertInstanceOf(Database::class, $response);
}
/**
* Test that a database host not assigned to the same node as the server is used if
* there are no same-node hosts and the allow_random configuration value is set to
* true.
*/
public function testDatabaseHostIsSelectedIfNoSuitableHostExistsOnSameNode()
{
$server = $this->createServerModel();
$node = factory(Node::class)->create(['location_id' => $server->location->id]);
$host = factory(DatabaseHost::class)->create(['node_id' => $node->id]);
$this->managementService->expects('create')->with($server, [
'database_host_id' => $host->id,
'database' => "s{$server->id}_something",
'remote' => '%',
])->andReturns(new Database);
$response = $this->getService()->handle($server, [
'database' => 'something',
'remote' => '%',
]);
$this->assertInstanceOf(Database::class, $response);
}
/**
* @return array
*/
public function invalidDataProvider(): array
{
return [
[['remote' => '%']],
[['database' => null, 'remote' => '%']],
[['database' => '', 'remote' => '%']],
[['database' => '']],
[['database' => '', 'remote' => '']],
];
}
/**
* @return \Pterodactyl\Services\Databases\DeployServerDatabaseService
*/
private function getService()
{
return $this->app->make(DeployServerDatabaseService::class);
}
}

View file

@ -0,0 +1,163 @@
<?php
namespace Pterodactyl\Tests\Integration\Services\Deployment;
use Pterodactyl\Models\Node;
use InvalidArgumentException;
use Pterodactyl\Models\Server;
use Pterodactyl\Models\Location;
use Pterodactyl\Models\Database;
use Illuminate\Support\Collection;
use Pterodactyl\Tests\Integration\IntegrationTestCase;
use Pterodactyl\Services\Deployment\FindViableNodesService;
use Pterodactyl\Exceptions\Service\Deployment\NoViableNodeException;
class FindViableNodesServiceTest extends IntegrationTestCase
{
public function setUp(): void
{
parent::setUp();
Database::query()->delete();
Server::query()->delete();
Node::query()->delete();
}
public function testExceptionIsThrownIfNoDiskSpaceHasBeenSet()
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('Disk space must be an int, got NULL');
$this->getService()->handle();
}
public function testExceptionIsThrownIfNoMemoryHasBeenSet()
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('Memory usage must be an int, got NULL');
$this->getService()->setDisk(10)->handle();
}
public function testExpectedNodeIsReturnedForLocation()
{
/** @var \Pterodactyl\Models\Location[] $locations */
$locations = factory(Location::class)->times(2)->create();
/** @var \Pterodactyl\Models\Node[] $nodes */
$nodes = [
// This node should never be returned once we've completed the initial test which
// runs without a location filter.
factory(Node::class)->create([
'location_id' => $locations[0]->id,
'memory' => 2048,
'disk' => 1024 * 100,
]),
factory(Node::class)->create([
'location_id' => $locations[1]->id,
'memory' => 1024,
'disk' => 10240,
'disk_overallocate' => 10,
]),
factory(Node::class)->create([
'location_id' => $locations[1]->id,
'memory' => 1024 * 4,
'memory_overallocate' => 50,
'disk' => 102400,
]),
];
// Expect that all of the nodes are returned as we're under all of their limits
// and there is no location filter being provided.
$response = $this->getService()->setDisk(512)->setMemory(512)->handle();
$this->assertInstanceOf(Collection::class, $response);
$this->assertCount(3, $response);
$this->assertInstanceOf(Node::class, $response[0]);
// Expect that only the last node is returned because it is the only one with enough
// memory available to this instance.
$response = $this->getService()->setDisk(512)->setMemory(2049)->handle();
$this->assertInstanceOf(Collection::class, $response);
$this->assertCount(1, $response);
$this->assertSame($nodes[2]->id, $response[0]->id);
// Helper, I am lazy.
$base = function () use ($locations) {
return $this->getService()->setLocations([ $locations[1]->id ])->setDisk(512);
};
// Expect that we can create this server on either node since the disk and memory
// limits are below the allowed amount.
$response = $base()->setMemory(512)->handle();
$this->assertCount(2, $response);
$this->assertSame(2, $response->where('location_id', $locations[1]->id)->count());
// Expect that we can only create this server on the second node since the memory
// allocated is over the amount of memory available to the first node.
$response = $base()->setMemory(2048)->handle();
$this->assertCount(1, $response);
$this->assertSame($nodes[2]->id, $response[0]->id);
// Expect that we can only create this server on the second node since the disk
// allocated is over the limit assigned to the first node (even with the overallocate).
$response = $base()->setDisk(20480)->setMemory(256)->handle();
$this->assertCount(1, $response);
$this->assertSame($nodes[2]->id, $response[0]->id);
// Expect that we could create the server on either node since the disk allocated is
// right at the limit for Node 1 when the overallocate value is included in the calc.
$response = $base()->setDisk(11264)->setMemory(256)->handle();
$this->assertCount(2, $response);
// Create two servers on the first node so that the disk space used is equal to the
// base amount available to the node (without overallocation included).
$servers = Collection::make([
$this->createServerModel(['node_id' => $nodes[1]->id, 'disk' => 5120]),
$this->createServerModel(['node_id' => $nodes[1]->id, 'disk' => 5120]),
]);
// Expect that we cannot create a server with a 1GB disk on the first node since there
// is not enough space (even with the overallocate) available to the node.
$response = $base()->setDisk(1024)->setMemory(256)->handle();
$this->assertCount(1, $response);
$this->assertSame($nodes[2]->id, $response[0]->id);
// Cleanup servers since we need to test some other stuff with memory here.
$servers->each->delete();
// Expect that no viable node can be found when the memory limit for the given instance
// is greater than either node can support, even with the overallocation limits taken
// into account.
$this->expectException(NoViableNodeException::class);
$base()->setMemory(10000)->handle();
// Create four servers so that the memory used for the second node is equal to the total
// limit for that node (pre-overallocate calculation).
Collection::make([
$this->createServerModel(['node_id' => $nodes[2]->id, 'memory' => 1024]),
$this->createServerModel(['node_id' => $nodes[2]->id, 'memory' => 1024]),
$this->createServerModel(['node_id' => $nodes[2]->id, 'memory' => 1024]),
$this->createServerModel(['node_id' => $nodes[2]->id, 'memory' => 1024]),
]);
// Expect that either node can support this server when we account for the overallocate
// value of the second node.
$response = $base()->setMemory(500)->handle();
$this->assertCount(2, $response);
$this->assertSame(2, $response->where('location_id', $locations[1]->id)->count());
// Expect that only the first node can support this server when we go over the remaining
// memory for the second nodes overallocate calculation.
$response = $base()->setMemory(640)->handle();
$this->assertCount(1, $response);
$this->assertSame($nodes[1]->id, $response[0]->id);
}
/**
* @return \Pterodactyl\Services\Deployment\FindViableNodesService
*/
private function getService()
{
return $this->app->make(FindViableNodesService::class);
}
}

View file

@ -0,0 +1,213 @@
<?php
namespace Pterodactyl\Tests\Integration\Services\Servers;
use Mockery;
use Pterodactyl\Models\Egg;
use Pterodactyl\Models\Node;
use Pterodactyl\Models\User;
use GuzzleHttp\Psr7\Request;
use GuzzleHttp\Psr7\Response;
use Pterodactyl\Models\Server;
use Pterodactyl\Models\Location;
use Pterodactyl\Models\Allocation;
use Illuminate\Foundation\Testing\WithFaker;
use Illuminate\Validation\ValidationException;
use GuzzleHttp\Exception\BadResponseException;
use Pterodactyl\Models\Objects\DeploymentObject;
use Pterodactyl\Tests\Integration\IntegrationTestCase;
use Pterodactyl\Services\Servers\ServerCreationService;
use Pterodactyl\Repositories\Wings\DaemonServerRepository;
use Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException;
class ServerCreationServiceTest extends IntegrationTestCase
{
use WithFaker;
/** @var \Mockery\MockInterface */
private $daemonServerRepository;
/**
* Stub the calls to Wings so that we don't actually hit those API endpoints.
*/
public function setUp(): void
{
parent::setUp();
$this->daemonServerRepository = Mockery::mock(DaemonServerRepository::class);
$this->swap(DaemonServerRepository::class, $this->daemonServerRepository);
}
/**
* Test that a server can be created when a deployment object is provided to the service.
*
* This doesn't really do anything super complicated, we'll rely on other more specific
* tests to cover that the logic being used does indeed find suitable nodes and ports. For
* this test we just care that it is recognized and passed off to those functions.
*/
public function testServerIsCreatedWithDeploymentObject()
{
/** @var \Pterodactyl\Models\User $user */
$user = factory(User::class)->create();
/** @var \Pterodactyl\Models\Node $node */
$node = factory(Node::class)->create([
'location_id' => factory(Location::class)->create()->id,
]);
/** @var \Pterodactyl\Models\Allocation[]|\Illuminate\Database\Eloquent\Collection $allocations */
$allocations = factory(Allocation::class)->times(5)->create([
'node_id' => $node->id,
]);
$deployment = (new DeploymentObject())->setDedicated(true)->setLocations([$node->location_id])->setPorts([
$allocations[0]->port,
]);
/** @noinspection PhpParamsInspection */
$egg = $this->cloneEggAndVariables(Egg::query()->findOrFail(1));
// We want to make sure that the validator service runs as an admin, and not as a regular
// user when saving variables.
$egg->variables()->first()->update([
'user_editable' => false,
]);
$data = [
'name' => $this->faker->name,
'description' => $this->faker->sentence,
'owner_id' => $user->id,
'memory' => 256,
'swap' => 128,
'disk' => 100,
'io' => 500,
'cpu' => 0,
'startup' => 'java server2.jar',
'image' => 'java:8',
'egg_id' => $egg->id,
'allocation_additional' => [
$allocations[4]->id,
],
'environment' => [
'BUNGEE_VERSION' => '123',
'SERVER_JARFILE' => 'server2.jar',
],
];
$this->daemonServerRepository->expects('setServer')->andReturnSelf();
$this->daemonServerRepository->expects('create')->with(Mockery::on(function ($value) {
$this->assertIsArray($value);
// Just check for some keys to make sure we're getting the expected configuration
// structure back. Other tests exist to confirm it is the correct structure.
$this->assertArrayHasKey('uuid', $value);
$this->assertArrayHasKey('environment', $value);
$this->assertArrayHasKey('invocation', $value);
return true;
}))->andReturnUndefined();
try {
$this->getService()->handle(array_merge($data, [
'environment' => [
'BUNGEE_VERSION' => '',
'SERVER_JARFILE' => 'server2.jar',
],
]), $deployment);
$this->assertTrue(false, 'This statement should not be reached.');
} catch (ValidationException $exception) {
$this->assertCount(1, $exception->errors());
$this->assertArrayHasKey('environment.BUNGEE_VERSION', $exception->errors());
$this->assertSame('The Bungeecord Version variable field is required.', $exception->errors()['environment.BUNGEE_VERSION'][0]);
}
$response = $this->getService()->handle($data, $deployment);
$this->assertInstanceOf(Server::class, $response);
$this->assertNotNull($response->uuid);
$this->assertSame($response->uuidShort, substr($response->uuid, 0, 8));
$this->assertSame($egg->id, $response->egg_id);
$this->assertCount(2, $response->variables);
$this->assertSame('123', $response->variables[0]->server_value);
$this->assertSame('server2.jar', $response->variables[1]->server_value);
foreach ($data as $key => $value) {
if (in_array($key, ['allocation_additional', 'environment'])) {
continue;
}
$this->assertSame($value, $response->{$key});
}
$this->assertCount(2, $response->allocations);
$this->assertSame($response->allocation_id, $response->allocations[0]->id);
$this->assertSame($allocations[0]->id, $response->allocations[0]->id);
$this->assertSame($allocations[4]->id, $response->allocations[1]->id);
$this->assertFalse($response->suspended);
$this->assertTrue($response->oom_disabled);
$this->assertEmpty($response->database_limit);
$this->assertEmpty($response->allocation_limit);
$this->assertEmpty($response->backup_limit);
}
/**
* Test that a server is deleted from the Panel if Wings returns an error during the creation
* process.
*/
public function testErrorEncounteredByWingsCausesServerToBeDeleted()
{
/** @var \Pterodactyl\Models\User $user */
$user = factory(User::class)->create();
/** @var \Pterodactyl\Models\Node $node */
$node = factory(Node::class)->create([
'location_id' => factory(Location::class)->create()->id,
]);
/** @var \Pterodactyl\Models\Allocation $allocation */
$allocation = factory(Allocation::class)->create([
'node_id' => $node->id,
]);
$data = [
'name' => $this->faker->name,
'description' => $this->faker->sentence,
'owner_id' => $user->id,
'allocation_id' => $allocation->id,
'node_id' => $allocation->node_id,
'memory' => 256,
'swap' => 128,
'disk' => 100,
'io' => 500,
'cpu' => 0,
'startup' => 'java server2.jar',
'image' => 'java:8',
'egg_id' => 1,
'environment' => [
'BUNGEE_VERSION' => '123',
'SERVER_JARFILE' => 'server2.jar',
],
];
$this->daemonServerRepository->expects('setServer->create')->andThrows(
new DaemonConnectionException(
new BadResponseException('Bad request', new Request('POST', '/create'), new Response(500))
)
);
$this->daemonServerRepository->expects('setServer->delete')->andReturnUndefined();
$this->expectException(DaemonConnectionException::class);
$this->getService()->handle($data);
$this->assertDatabaseMissing('servers', ['owner_id' => $user->id]);
}
/**
* @return \Pterodactyl\Services\Servers\ServerCreationService
*/
private function getService()
{
return $this->app->make(ServerCreationService::class);
}
}

View file

@ -0,0 +1,166 @@
<?php
namespace Pterodactyl\Tests\Integration\Services\Servers;
use Mockery;
use Exception;
use GuzzleHttp\Psr7\Request;
use GuzzleHttp\Psr7\Response;
use Pterodactyl\Models\Database;
use Pterodactyl\Models\DatabaseHost;
use GuzzleHttp\Exception\BadResponseException;
use Pterodactyl\Tests\Integration\IntegrationTestCase;
use Pterodactyl\Services\Servers\ServerDeletionService;
use Pterodactyl\Repositories\Wings\DaemonServerRepository;
use Pterodactyl\Services\Databases\DatabaseManagementService;
use Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException;
class ServerDeletionServiceTest extends IntegrationTestCase
{
/** @var \Mockery\MockInterface */
private $daemonServerRepository;
/** @var \Mockery\MockInterface */
private $databaseManagementService;
private static $defaultLogger;
/**
* Stub out services that we don't want to test in here.
*/
public function setUp(): void
{
parent::setUp();
self::$defaultLogger = config('logging.default');
// There will be some log calls during this test, don't actually write to the disk.
config()->set('logging.default', 'null');
$this->daemonServerRepository = Mockery::mock(DaemonServerRepository::class);
$this->databaseManagementService = Mockery::mock(DatabaseManagementService::class);
$this->app->instance(DaemonServerRepository::class, $this->daemonServerRepository);
$this->app->instance(DatabaseManagementService::class, $this->databaseManagementService);
}
/**
* Reset the log driver.
*/
protected function tearDown(): void
{
config()->set('logging.default', self::$defaultLogger);
self::$defaultLogger = null;
parent::tearDown();
}
/**
* Test that a server is not deleted if the force option is not set and an error
* is returned by wings.
*/
public function testRegularDeleteFailsIfWingsReturnsError()
{
$server = $this->createServerModel();
$this->expectException(DaemonConnectionException::class);
$this->daemonServerRepository->expects('setServer->delete')->withNoArgs()->andThrows(
new DaemonConnectionException(new BadResponseException('Bad request', new Request('GET', '/test')))
);
$this->getService()->handle($server);
$this->assertDatabaseHas('servers', ['id' => $server->id]);
}
/**
* Test that a 404 from Wings while deleting a server does not cause the deletion to fail.
*/
public function testRegularDeleteIgnores404FromWings()
{
$server = $this->createServerModel();
$this->daemonServerRepository->expects('setServer->delete')->withNoArgs()->andThrows(
new DaemonConnectionException(new BadResponseException('Bad request', new Request('GET', '/test'), new Response(404)))
);
$this->getService()->handle($server);
$this->assertDatabaseMissing('servers', ['id' => $server->id]);
}
/**
* Test that an error from Wings does not cause the deletion to fail if the server is being
* force deleted.
*/
public function testForceDeleteIgnoresExceptionFromWings()
{
$server = $this->createServerModel();
$this->daemonServerRepository->expects('setServer->delete')->withNoArgs()->andThrows(
new DaemonConnectionException(new BadResponseException('Bad request', new Request('GET', '/test'), new Response(500)))
);
$this->getService()->withForce(true)->handle($server);
$this->assertDatabaseMissing('servers', ['id' => $server->id]);
}
/**
* Test that a non-force-delete call does not delete the server if one of the databases
* cannot be deleted from the host.
*/
public function testExceptionWhileDeletingStopsProcess()
{
$server = $this->createServerModel();
$host = factory(DatabaseHost::class)->create();
/** @var \Pterodactyl\Models\Database $db */
$db = factory(Database::class)->create(['database_host_id' => $host->id, 'server_id' => $server->id]);
$server->refresh();
$this->daemonServerRepository->expects('setServer->delete')->withNoArgs()->andReturnUndefined();
$this->databaseManagementService->expects('delete')->with(Mockery::on(function ($value) use ($db) {
return $value instanceof Database && $value->id === $db->id;
}))->andThrows(new Exception);
$this->expectException(Exception::class);
$this->getService()->handle($server);
$this->assertDatabaseHas('servers', ['id' => $server->id]);
$this->assertDatabaseHas('databases', ['id' => $db->id]);
}
/**
* Test that a server is deleted even if the server databases cannot be deleted from the host.
*/
public function testExceptionWhileDeletingDatabasesDoesNotAbortIfForceDeleted()
{
$server = $this->createServerModel();
$host = factory(DatabaseHost::class)->create();
/** @var \Pterodactyl\Models\Database $db */
$db = factory(Database::class)->create(['database_host_id' => $host->id, 'server_id' => $server->id]);
$server->refresh();
$this->daemonServerRepository->expects('setServer->delete')->withNoArgs()->andReturnUndefined();
$this->databaseManagementService->expects('delete')->with(Mockery::on(function ($value) use ($db) {
return $value instanceof Database && $value->id === $db->id;
}))->andThrows(new Exception);
$this->getService()->withForce(true)->handle($server);
$this->assertDatabaseMissing('servers', ['id' => $server->id]);
$this->assertDatabaseMissing('databases', ['id' => $db->id]);
}
/**
* @return \Pterodactyl\Services\Servers\ServerDeletionService
*/
private function getService()
{
return $this->app->make(ServerDeletionService::class);
}
}

View file

@ -0,0 +1,171 @@
<?php
namespace Pterodactyl\Tests\Integration\Services\Servers;
use Exception;
use Ramsey\Uuid\Uuid;
use Pterodactyl\Models\Egg;
use Pterodactyl\Models\User;
use Pterodactyl\Models\Nest;
use Pterodactyl\Models\Server;
use Pterodactyl\Models\ServerVariable;
use Illuminate\Validation\ValidationException;
use Pterodactyl\Tests\Integration\IntegrationTestCase;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Pterodactyl\Services\Servers\StartupModificationService;
class StartupModificationServiceTest extends IntegrationTestCase
{
/**
* Test that a non-admin request to modify the server startup parameters does
* not perform any egg or nest updates. This also attempts to pass through an
* egg_id variable which should have no impact if the request is coming from
* a non-admin entity.
*/
public function testNonAdminCanModifyServerVariables()
{
// Theoretically lines up with the Bungeecord Minecraft egg.
$server = $this->createServerModel(['egg_id' => 1]);
try {
$this->app->make(StartupModificationService::class)->handle($server, [
'egg_id' => $server->egg_id + 1,
'environment' => [
'BUNGEE_VERSION' => '$$',
'SERVER_JARFILE' => 'server.jar',
],
]);
$this->assertTrue(false, 'This assertion should not be called.');
} catch (Exception $exception) {
$this->assertInstanceOf(ValidationException::class, $exception);
/** @var \Illuminate\Validation\ValidationException $exception */
$errors = $exception->validator->errors()->toArray();
$this->assertCount(1, $errors);
$this->assertArrayHasKey('environment.BUNGEE_VERSION', $errors);
$this->assertCount(1, $errors['environment.BUNGEE_VERSION']);
$this->assertSame('The Bungeecord Version variable may only contain letters and numbers.', $errors['environment.BUNGEE_VERSION'][0]);
}
ServerVariable::query()->where('variable_id', $server->variables[1]->id)->delete();
$result = $this->getService()
->handle($server, [
'egg_id' => $server->egg_id + 1,
'startup' => 'random gibberish',
'environment' => [
'BUNGEE_VERSION' => '1234',
'SERVER_JARFILE' => 'test.jar',
],
]);
$this->assertInstanceOf(Server::class, $result);
$this->assertCount(2, $result->variables);
$this->assertSame($server->startup, $result->startup);
$this->assertSame('1234', $result->variables[0]->server_value);
$this->assertSame('test.jar', $result->variables[1]->server_value);
}
/**
* Test that modifying an egg as an admin properly updates the data for the server.
*/
public function testServerIsProperlyModifiedAsAdminUser()
{
/** @var \Pterodactyl\Models\Egg $nextEgg */
$nextEgg = Nest::query()->findOrFail(2)->eggs()->firstOrFail();
$server = $this->createServerModel(['egg_id' => 1]);
$this->assertNotSame($nextEgg->id, $server->egg_id);
$this->assertNotSame($nextEgg->nest_id, $server->nest_id);
$response = $this->getService()
->setUserLevel(User::USER_LEVEL_ADMIN)
->handle($server, [
'egg_id' => $nextEgg->id,
'startup' => 'sample startup',
'skip_scripts' => true,
'docker_image' => 'docker/hodor',
]);
$this->assertInstanceOf(Server::class, $response);
$this->assertSame($nextEgg->id, $response->egg_id);
$this->assertSame($nextEgg->nest_id, $response->nest_id);
$this->assertSame('sample startup', $response->startup);
$this->assertSame('docker/hodor', $response->image);
$this->assertTrue($response->skip_scripts);
}
/**
* Test that hidden variables can be updated by an admin but are not affected by a
* regular user who attempts to pass them through.
*/
public function testEnvironmentVariablesCanBeUpdatedByAdmin()
{
$server = $this->createServerModel();
$server->loadMissing(['egg', 'variables']);
$clone = $this->cloneEggAndVariables($server->egg);
// This makes the BUNGEE_VERSION variable not user editable.
$clone->variables()->first()->update([
'user_editable' => false,
]);
$server->fill(['egg_id' => $clone->id])->saveOrFail();
$server->refresh();
ServerVariable::query()->updateOrCreate([
'server_id' => $server->id,
'variable_id' => $server->variables[0]->id,
], ['variable_value' => 'EXIST']);
$response = $this->getService()->handle($server, [
'environment' => [
'BUNGEE_VERSION' => '1234',
'SERVER_JARFILE' => 'test.jar',
],
]);
$this->assertCount(2, $response->variables);
$this->assertSame('EXIST', $response->variables[0]->server_value);
$this->assertSame('test.jar', $response->variables[1]->server_value);
$response = $this->getService()
->setUserLevel(User::USER_LEVEL_ADMIN)
->handle($server, [
'environment' => [
'BUNGEE_VERSION' => '1234',
'SERVER_JARFILE' => 'test.jar',
],
]);
$this->assertCount(2, $response->variables);
$this->assertSame('1234', $response->variables[0]->server_value);
$this->assertSame('test.jar', $response->variables[1]->server_value);
}
/**
* Test that passing an invalid egg ID into the function throws an exception
* rather than silently failing or skipping.
*/
public function testInvalidEggIdTriggersException()
{
$server = $this->createServerModel();
$this->expectException(ModelNotFoundException::class);
$this->getService()
->setUserLevel(User::USER_LEVEL_ADMIN)
->handle($server, ['egg_id' => 123456789]);
}
/**
* @return \Pterodactyl\Services\Servers\StartupModificationService
*/
private function getService()
{
return $this->app->make(StartupModificationService::class);
}
}

View file

@ -0,0 +1,80 @@
<?php
namespace Pterodactyl\Tests\Integration\Services\Servers;
use Mockery;
use InvalidArgumentException;
use Pterodactyl\Services\Servers\SuspensionService;
use Pterodactyl\Tests\Integration\IntegrationTestCase;
use Pterodactyl\Repositories\Wings\DaemonServerRepository;
class SuspensionServiceTest extends IntegrationTestCase
{
/** @var \Mockery\MockInterface */
private $repository;
/**
* Setup test instance.
*/
public function setUp(): void
{
parent::setUp();
$this->repository = Mockery::mock(DaemonServerRepository::class);
$this->app->instance(DaemonServerRepository::class, $this->repository);
}
public function testServerIsSuspendedAndUnsuspended()
{
$server = $this->createServerModel(['suspended' => false]);
$this->repository->expects('setServer')->twice()->andReturnSelf();
$this->repository->expects('suspend')->with(false)->andReturnUndefined();
$this->getService()->toggle($server, SuspensionService::ACTION_SUSPEND);
$server->refresh();
$this->assertTrue($server->suspended);
$this->repository->expects('suspend')->with(true)->andReturnUndefined();
$this->getService()->toggle($server, SuspensionService::ACTION_UNSUSPEND);
$server->refresh();
$this->assertFalse($server->suspended);
}
public function testNoActionIsTakenIfSuspensionStatusIsUnchanged()
{
$server = $this->createServerModel(['suspended' => false]);
$this->getService()->toggle($server, SuspensionService::ACTION_UNSUSPEND);
$server->refresh();
$this->assertFalse($server->suspended);
$server->update(['suspended' => true]);
$this->getService()->toggle($server, SuspensionService::ACTION_SUSPEND);
$server->refresh();
$this->assertTrue($server->suspended);
}
public function testExceptionIsThrownIfInvalidActionsArePassed()
{
$server = $this->createServerModel();
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('Expected one of: "suspend", "unsuspend". Got: "foo"');
$this->getService()->toggle($server, 'foo');
}
/**
* @return \Pterodactyl\Services\Servers\SuspensionService
*/
private function getService()
{
return $this->app->make(SuspensionService::class);
}
}

View file

@ -0,0 +1,137 @@
<?php
namespace Pterodactyl\Tests\Integration\Services\Servers;
use Pterodactyl\Models\Egg;
use Pterodactyl\Models\User;
use Illuminate\Support\Collection;
use Illuminate\Validation\ValidationException;
use Pterodactyl\Tests\Integration\IntegrationTestCase;
use Pterodactyl\Services\Servers\VariableValidatorService;
class VariableValidatorServiceTest extends IntegrationTestCase
{
/**
* Test that enviornment variables for a server are validated as expected.
*/
public function testEnvironmentVariablesCanBeValidated()
{
/** @noinspection PhpParamsInspection */
$egg = $this->cloneEggAndVariables(Egg::query()->findOrFail(1));
try {
$this->getService()->handle($egg->id, [
'BUNGEE_VERSION' => '1.2.3',
]);
$this->assertTrue(false, 'This statement should not be reached.');
} catch (ValidationException $exception) {
$errors = $exception->errors();
$this->assertCount(2, $errors);
$this->assertArrayHasKey('environment.BUNGEE_VERSION', $errors);
$this->assertArrayHasKey('environment.SERVER_JARFILE', $errors);
$this->assertSame('The Bungeecord Version variable may only contain letters and numbers.', $errors['environment.BUNGEE_VERSION'][0]);
$this->assertSame('The Bungeecord Jar File variable field is required.', $errors['environment.SERVER_JARFILE'][0]);
}
$response = $this->getService()->handle($egg->id, [
'BUNGEE_VERSION' => '1234',
'SERVER_JARFILE' => 'server.jar',
]);
$this->assertInstanceOf(Collection::class, $response);
$this->assertCount(2, $response);
$this->assertSame('BUNGEE_VERSION', $response->get(0)->key);
$this->assertSame('1234', $response->get(0)->value);
$this->assertSame('SERVER_JARFILE', $response->get(1)->key);
$this->assertSame('server.jar', $response->get(1)->value);
}
/**
* Test that variables that are user_editable=false do not get validated (or returned) by
* the handler.
*/
public function testNormalUserCannotValidateNonUserEditableVariables()
{
/** @noinspection PhpParamsInspection */
$egg = $this->cloneEggAndVariables(Egg::query()->findOrFail(1));
$egg->variables()->first()->update([
'user_editable' => false,
]);
$response = $this->getService()->handle($egg->id, [
// This is an invalid value, but it shouldn't cause any issues since it should be skipped.
'BUNGEE_VERSION' => '1.2.3',
'SERVER_JARFILE' => 'server.jar',
]);
$this->assertInstanceOf(Collection::class, $response);
$this->assertCount(1, $response);
$this->assertSame('SERVER_JARFILE', $response->get(0)->key);
$this->assertSame('server.jar', $response->get(0)->value);
}
public function testEnvironmentVariablesCanBeUpdatedAsAdmin()
{
/** @noinspection PhpParamsInspection */
$egg = $this->cloneEggAndVariables(Egg::query()->findOrFail(1));
$egg->variables()->first()->update([
'user_editable' => false,
]);
try {
$this->getService()->setUserLevel(User::USER_LEVEL_ADMIN)->handle($egg->id, [
'BUNGEE_VERSION' => '1.2.3',
'SERVER_JARFILE' => 'server.jar',
]);
$this->assertTrue(false, 'This statement should not be reached.');
} catch (ValidationException $exception) {
$this->assertCount(1, $exception->errors());
$this->assertArrayHasKey('environment.BUNGEE_VERSION', $exception->errors());
}
$response = $this->getService()->setUserLevel(User::USER_LEVEL_ADMIN)->handle($egg->id, [
'BUNGEE_VERSION' => '123',
'SERVER_JARFILE' => 'server.jar',
]);
$this->assertInstanceOf(Collection::class, $response);
$this->assertCount(2, $response);
$this->assertSame('BUNGEE_VERSION', $response->get(0)->key);
$this->assertSame('123', $response->get(0)->value);
$this->assertSame('SERVER_JARFILE', $response->get(1)->key);
$this->assertSame('server.jar', $response->get(1)->value);
}
public function testNullableEnvironmentVariablesCanBeUsedCorrectly()
{
/** @noinspection PhpParamsInspection */
$egg = $this->cloneEggAndVariables(Egg::query()->findOrFail(1));
$egg->variables()->where('env_variable', '!=', 'BUNGEE_VERSION')->delete();
$egg->variables()->update(['rules' => 'nullable|string']);
$response = $this->getService()->handle($egg->id, []);
$this->assertCount(1, $response);
$this->assertNull($response->get(0)->value);
$response = $this->getService()->handle($egg->id, ['BUNGEE_VERSION' => null]);
$this->assertCount(1, $response);
$this->assertNull($response->get(0)->value);
$response = $this->getService()->handle($egg->id, ['BUNGEE_VERSION' => '']);
$this->assertCount(1, $response);
$this->assertSame('', $response->get(0)->value);
}
/**
* @return \Pterodactyl\Services\Servers\VariableValidatorService
*/
private function getService()
{
return $this->app->make(VariableValidatorService::class);
}
}

View file

@ -16,6 +16,15 @@ abstract class TestCase extends BaseTestCase
{ {
parent::setUp(); parent::setUp();
// Why, you ask? If we don't force this to false it is possible for certain exceptions
// to show their error message properly in the integration test output, but not actually
// be setup correctly to display thier message in production.
//
// If we expect a message in a test, and it isn't showing up (rather, showing the generic
// "an error occurred" message), we can probably assume that the exception isn't one that
// is recognized as being user viewable.
config()->set('app.debug', false);
$this->setKnownUuidFactory(); $this->setKnownUuidFactory();
} }

View file

@ -2,6 +2,7 @@
namespace Tests\Traits\Integration; namespace Tests\Traits\Integration;
use Ramsey\Uuid\Uuid;
use Pterodactyl\Models\Egg; use Pterodactyl\Models\Egg;
use Pterodactyl\Models\Nest; use Pterodactyl\Models\Nest;
use Pterodactyl\Models\Node; use Pterodactyl\Models\Node;
@ -74,4 +75,27 @@ trait CreatesTestModels
'location', 'user', 'node', 'allocation', 'nest', 'egg', 'location', 'user', 'node', 'allocation', 'nest', 'egg',
])->findOrFail($server->id); ])->findOrFail($server->id);
} }
/**
* Clones a given egg allowing us to make modifications that don't affect other
* tests that rely on the egg existing in the correct state.
*
* @param \Pterodactyl\Models\Egg $egg
* @return \Pterodactyl\Models\Egg
*/
protected function cloneEggAndVariables(Egg $egg): Egg
{
$model = $egg->replicate(['id', 'uuid']);
$model->uuid = Uuid::uuid4()->toString();
$model->push();
/** @var \Pterodactyl\Models\Egg $model */
$model = $model->fresh();
foreach ($egg->variables as $variable) {
$variable->replicate(['id', 'egg_id'])->forceFill(['egg_id' => $model->id])->push();
}
return $model->fresh();
}
} }

View file

@ -0,0 +1,52 @@
<?php
namespace Tests\Traits;
use PDO;
use Mockery;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\MySqlConnection;
use Illuminate\Database\ConnectionResolver;
trait MocksPdoConnection
{
/**
* @var \Illuminate\Database\ConnectionResolverInterface|null
*/
private static $initialResolver;
/**
* Generates a mock PDO connection and injects it into the models so that any actual
* DB call can be properly intercepted.
*
* @return \Mockery\MockInterface
*/
protected function mockPdoConnection()
{
self::$initialResolver = Model::getConnectionResolver();
Model::unsetConnectionResolver();
$connection = new MySqlConnection($mock = Mockery::mock(PDO::class), 'testing_mock');
$resolver = new ConnectionResolver(['mocked' => $connection]);
$resolver->setDefaultConnection('mocked');
Model::setConnectionResolver($resolver);
return $mock;
}
/**
* Resets the mock state.
*/
protected function tearDownPdoMock()
{
if (! self::$initialResolver) {
return;
}
Model::setConnectionResolver(self::$initialResolver);
self::$initialResolver = null;
}
}

View file

@ -36,22 +36,24 @@ class EmailSettingsCommandTest extends CommandTestCase
*/ */
public function testSmtpDriverSelection() public function testSmtpDriverSelection()
{ {
$data = [ // TODO(dane): fix this
'MAIL_DRIVER' => 'smtp', $this->markTestSkipped('Skipped, GitHub actions cannot run successfully.');
'MAIL_HOST' => 'mail.test.com', // $data = [
'MAIL_PORT' => '567', // 'MAIL_DRIVER' => 'smtp',
'MAIL_USERNAME' => 'username', // 'MAIL_HOST' => 'mail.test.com',
'MAIL_PASSWORD' => 'password', // 'MAIL_PORT' => '567',
'MAIL_FROM' => 'mail@from.com', // 'MAIL_USERNAME' => 'username',
'MAIL_FROM_NAME' => 'MailName', // 'MAIL_PASSWORD' => 'password',
'MAIL_ENCRYPTION' => 'tls', // 'MAIL_FROM' => 'mail@from.com',
]; // 'MAIL_FROM_NAME' => 'MailName',
// 'MAIL_ENCRYPTION' => 'tls',
$this->setupCoreFunctions($data); // ];
$display = $this->runCommand($this->command, [], array_values($data)); //
// $this->setupCoreFunctions($data);
$this->assertNotEmpty($display); // $display = $this->runCommand($this->command, [], array_values($data));
$this->assertStringContainsString('Updating stored environment configuration file.', $display); //
// $this->assertNotEmpty($display);
// $this->assertStringContainsString('Updating stored environment configuration file.', $display);
} }
/** /**

View file

@ -45,28 +45,31 @@ class MakeUserCommandTest extends CommandTestCase
*/ */
public function testCommandWithNoPassedOptions() public function testCommandWithNoPassedOptions()
{ {
$user = factory(User::class)->make(['root_admin' => true]); // TODO(dane): fix this
$this->markTestSkipped('Skipped, GitHub actions cannot run successfully.');
$this->creationService->shouldReceive('handle')->with([ // $user = factory(User::class)->make(['root_admin' => true]);
'email' => $user->email, //
'username' => $user->username, // $this->creationService->shouldReceive('handle')->with([
'name_first' => $user->name_first, // 'email' => $user->email,
'name_last' => $user->name_last, // 'username' => $user->username,
'password' => 'Password123', // 'name_first' => $user->name_first,
'root_admin' => $user->root_admin, // 'name_last' => $user->name_last,
])->once()->andReturn($user); // 'password' => 'Password123',
// 'root_admin' => $user->root_admin,
$display = $this->runCommand($this->command, [], [ // ])->once()->andReturn($user);
'yes', $user->email, $user->username, $user->name_first, $user->name_last, 'Password123', //
]); // $display = $this->runCommand($this->command, [], [
// 'yes', $user->email, $user->username, $user->name_first, $user->name_last, 'Password123',
$this->assertNotEmpty($display); // ]);
$this->assertStringContainsString(trans('command/messages.user.ask_password_help'), $display); //
$this->assertStringContainsString($user->uuid, $display); // $this->assertNotEmpty($display);
$this->assertStringContainsString($user->email, $display); // $this->assertStringContainsString(trans('command/messages.user.ask_password_help'), $display);
$this->assertStringContainsString($user->username, $display); // $this->assertStringContainsString($user->uuid, $display);
$this->assertStringContainsString($user->name, $display); // $this->assertStringContainsString($user->email, $display);
$this->assertStringContainsString('Yes', $display); // $this->assertStringContainsString($user->username, $display);
// $this->assertStringContainsString($user->name, $display);
// $this->assertStringContainsString('Yes', $display);
} }
/** /**

View file

@ -1,227 +0,0 @@
<?php
namespace Tests\Unit\Jobs\Schedule;
use Mockery as m;
use Carbon\Carbon;
use Tests\TestCase;
use Cake\Chronos\Chronos;
use Pterodactyl\Models\Task;
use Pterodactyl\Models\User;
use GuzzleHttp\Psr7\Response;
use InvalidArgumentException;
use Pterodactyl\Models\Server;
use Pterodactyl\Models\Schedule;
use Illuminate\Support\Facades\Bus;
use Pterodactyl\Jobs\Schedule\RunTaskJob;
use Pterodactyl\Repositories\Eloquent\TaskRepository;
use Pterodactyl\Services\Backups\InitiateBackupService;
use Pterodactyl\Repositories\Eloquent\ScheduleRepository;
use Pterodactyl\Repositories\Wings\DaemonPowerRepository;
use Pterodactyl\Repositories\Wings\DaemonCommandRepository;
use Pterodactyl\Contracts\Repository\TaskRepositoryInterface;
use Pterodactyl\Contracts\Repository\ScheduleRepositoryInterface;
class RunTaskJobTest extends TestCase
{
/**
* @var \Mockery\MockInterface
*/
private $commandRepository;
/**
* @var \Mockery\MockInterface
*/
private $powerRepository;
/**
* @var \Mockery\MockInterface
*/
private $initiateBackupService;
/**
* @var \Mockery\MockInterface
*/
private $taskRepository;
/**
* @var \Mockery\MockInterface
*/
private $scheduleRepository;
/**
* Setup tests.
*/
public function setUp(): void
{
parent::setUp();
Bus::fake();
Carbon::setTestNow(Carbon::now());
$this->commandRepository = m::mock(DaemonCommandRepository::class);
$this->powerRepository = m::mock(DaemonPowerRepository::class);
$this->taskRepository = m::mock(TaskRepository::class);
$this->initiateBackupService = m::mock(InitiateBackupService::class);
$this->scheduleRepository = m::mock(ScheduleRepository::class);
$this->app->instance(TaskRepositoryInterface::class, $this->taskRepository);
$this->app->instance(ScheduleRepositoryInterface::class, $this->scheduleRepository);
}
/**
* Test power option passed to job.
*/
public function testPowerAction()
{
/** @var \Pterodactyl\Models\Schedule $schedule */
$schedule = factory(Schedule::class)->make(['is_active' => true]);
/** @var \Pterodactyl\Models\Task $task */
$task = factory(Task::class)->make(['action' => 'power', 'sequence_id' => 1]);
/* @var \Pterodactyl\Models\Server $server */
$task->setRelation('server', $server = factory(Server::class)->make());
$task->setRelation('schedule', $schedule);
$server->setRelation('user', factory(User::class)->make());
$this->taskRepository->expects('getTaskForJobProcess')->with($task->id)->andReturn($task);
$this->powerRepository->expects('setServer')->with($task->server)->andReturnSelf()
->getMock()->expects('send')->with($task->payload)->andReturn(new Response);
$this->taskRepository->shouldReceive('update')->with($task->id, ['is_queued' => false])->once()->andReturnNull();
$this->taskRepository->shouldReceive('getNextTask')->with($schedule->id, $task->sequence_id)->once()->andReturnNull();
$this->scheduleRepository->shouldReceive('withoutFreshModel->update')->with($schedule->id, [
'is_processing' => false,
'last_run_at' => Chronos::now()->toDateTimeString(),
])->once()->andReturnNull();
$this->getJobInstance($task->id, $schedule->id);
Bus::assertNotDispatched(RunTaskJob::class);
}
/**
* Test command action passed to job.
*/
public function testCommandAction()
{
$schedule = factory(Schedule::class)->make();
$task = factory(Task::class)->make(['action' => 'command', 'sequence_id' => 1]);
$task->setRelation('server', $server = factory(Server::class)->make());
$task->setRelation('schedule', $schedule);
$server->setRelation('user', factory(User::class)->make());
$this->taskRepository->expects('getTaskForJobProcess')->with($task->id)->andReturn($task);
$this->commandRepository->expects('setServer')->with($task->server)->andReturnSelf()
->getMock()->expects('send')->with($task->payload)->andReturn(new Response);
$this->taskRepository->expects('update')->with($task->id, ['is_queued' => false])->andReturnNull();
$this->taskRepository->expects('getNextTask')->with($schedule->id, $task->sequence_id)->andReturnNull();
$this->scheduleRepository->shouldReceive('withoutFreshModel->update')->with($schedule->id, [
'is_processing' => false,
'last_run_at' => Chronos::now()->toDateTimeString(),
])->once()->andReturnNull();
$this->getJobInstance($task->id, $schedule->id);
Bus::assertNotDispatched(RunTaskJob::class);
}
/**
* Test that the next task in the list is queued if the current one is not the last.
*/
public function testNextTaskQueuedIfExists()
{
$schedule = factory(Schedule::class)->make();
$task = factory(Task::class)->make(['action' => 'command', 'sequence_id' => 1]);
$task->setRelation('server', $server = factory(Server::class)->make());
$task->setRelation('schedule', $schedule);
$server->setRelation('user', factory(User::class)->make());
$this->taskRepository->expects('getTaskForJobProcess')->with($task->id)->andReturn($task);
$this->commandRepository->expects('setServer')->with($task->server)->andReturnSelf()
->getMock()->expects('send')->with($task->payload)->andReturn(new Response);
$this->taskRepository->shouldReceive('update')->with($task->id, ['is_queued' => false])->once()->andReturnNull();
$nextTask = factory(Task::class)->make();
$this->taskRepository->expects('getNextTask')->with($schedule->id, $task->sequence_id)->andReturn($nextTask);
$this->taskRepository->expects('update')->with($nextTask->id, [
'is_queued' => true,
])->andReturnNull();
$this->getJobInstance($task->id, $schedule->id);
Bus::assertDispatched(RunTaskJob::class, function ($job) use ($nextTask, $schedule) {
$this->assertEquals($nextTask->id, $job->task, 'Assert correct task ID is passed to job.');
$this->assertEquals($schedule->id, $job->schedule, 'Assert correct schedule ID is passed to job.');
$this->assertEquals($nextTask->time_offset, $job->delay, 'Assert correct job delay time is set.');
return true;
});
}
/**
* Test that an exception is thrown if an invalid task action is supplied.
*/
public function testInvalidActionPassedToJob()
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('Cannot run a task that points to a non-existent action.');
$schedule = factory(Schedule::class)->make();
$task = factory(Task::class)->make(['action' => 'invalid', 'sequence_id' => 1]);
$task->setRelation('server', $server = factory(Server::class)->make());
$task->setRelation('schedule', $schedule);
$server->setRelation('user', factory(User::class)->make());
$this->taskRepository->expects('getTaskForJobProcess')->with($task->id)->andReturn($task);
$this->getJobInstance($task->id, 1234);
}
/**
* Test that a schedule marked as disabled does not get processed.
*/
public function testScheduleMarkedAsDisabledDoesNotProcess()
{
$schedule = factory(Schedule::class)->make(['is_active' => false]);
$task = factory(Task::class)->make(['action' => 'invalid', 'sequence_id' => 1]);
$task->setRelation('server', $server = factory(Server::class)->make());
$task->setRelation('schedule', $schedule);
$server->setRelation('user', factory(User::class)->make());
$this->taskRepository->shouldReceive('getTaskForJobProcess')->with($task->id)->once()->andReturn($task);
$this->scheduleRepository->shouldReceive('withoutFreshModel->update')->with($schedule->id, [
'is_processing' => false,
'last_run_at' => Chronos::now()->toDateTimeString(),
])->once()->andReturn(1);
$this->taskRepository->shouldReceive('update')->with($task->id, ['is_queued' => false])->once()->andReturn(1);
$this->getJobInstance($task->id, $schedule->id);
$this->assertTrue(true);
}
/**
* Run the job using the mocks provided.
*
* @param int $task
* @param int $schedule
*
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
*/
private function getJobInstance($task, $schedule)
{
return (new RunTaskJob($task, $schedule))->handle(
$this->commandRepository,
$this->initiateBackupService,
$this->powerRepository,
$this->taskRepository
);
}
}

View file

@ -28,9 +28,9 @@ class AllocationDeletionServiceTest extends TestCase
*/ */
public function testAllocationIsDeleted() public function testAllocationIsDeleted()
{ {
$model = factory(Allocation::class)->make(); $model = factory(Allocation::class)->make(['id' => 123]);
$this->repository->shouldReceive('delete')->with($model->id)->once()->andReturn(1); $this->repository->expects('delete')->with($model->id)->andReturns(1);
$response = $this->getService()->handle($model); $response = $this->getService()->handle($model);
$this->assertEquals(1, $response); $this->assertEquals(1, $response);

View file

@ -1,11 +1,4 @@
<?php <?php
/**
* Pterodactyl - Panel
* Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com>.
*
* This software is licensed under the terms of the MIT license.
* https://opensource.org/licenses/MIT
*/
namespace Tests\Unit\Services\Services\Options; namespace Tests\Unit\Services\Services\Options;
@ -41,7 +34,7 @@ class EggUpdateServiceTest extends TestCase
{ {
parent::setUp(); parent::setUp();
$this->model = factory(Egg::class)->make(); $this->model = factory(Egg::class)->make(['id' => 123]);
$this->repository = m::mock(EggRepositoryInterface::class); $this->repository = m::mock(EggRepositoryInterface::class);
$this->service = new EggUpdateService($this->repository); $this->service = new EggUpdateService($this->repository);

View file

@ -1,13 +1,6 @@
<?php <?php
/**
* Pterodactyl - Panel
* Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com>.
*
* This software is licensed under the terms of the MIT license.
* https://opensource.org/licenses/MIT
*/
namespace Tests\Unit\Services\Services\Options; namespace Tests\Unit\Services\Eggs\Scripts;
use Exception; use Exception;
use Mockery as m; use Mockery as m;
@ -30,21 +23,11 @@ class InstallScriptServiceTest extends TestCase
'copy_script_from' => null, 'copy_script_from' => null,
]; ];
/**
* @var \Pterodactyl\Models\Egg
*/
protected $model;
/** /**
* @var \Pterodactyl\Contracts\Repository\EggRepositoryInterface|\Mockery\Mock * @var \Pterodactyl\Contracts\Repository\EggRepositoryInterface|\Mockery\Mock
*/ */
protected $repository; protected $repository;
/**
* @var \Pterodactyl\Services\Eggs\Scripts\InstallScriptService
*/
protected $service;
/** /**
* Setup tests. * Setup tests.
*/ */
@ -52,10 +35,7 @@ class InstallScriptServiceTest extends TestCase
{ {
parent::setUp(); parent::setUp();
$this->model = factory(Egg::class)->make();
$this->repository = m::mock(EggRepositoryInterface::class); $this->repository = m::mock(EggRepositoryInterface::class);
$this->service = new InstallScriptService($this->repository);
} }
/** /**
@ -63,13 +43,13 @@ class InstallScriptServiceTest extends TestCase
*/ */
public function testUpdateWithValidCopyScriptFromAttribute() public function testUpdateWithValidCopyScriptFromAttribute()
{ {
$model = factory(Egg::class)->make(['id' => 123, 'nest_id' => 456]);
$this->data['copy_script_from'] = 1; $this->data['copy_script_from'] = 1;
$this->repository->shouldReceive('isCopyableScript')->with(1, $this->model->nest_id)->once()->andReturn(true); $this->repository->shouldReceive('isCopyableScript')->with(1, $model->nest_id)->once()->andReturn(true);
$this->repository->shouldReceive('withoutFreshModel')->withNoArgs()->once()->andReturnSelf() $this->repository->expects('withoutFreshModel->update')->with($model->id, $this->data)->andReturnNull();
->shouldReceive('update')->with($this->model->id, $this->data)->andReturnNull();
$this->service->handle($this->model, $this->data); $this->getService()->handle($model, $this->data);
} }
/** /**
@ -79,13 +59,13 @@ class InstallScriptServiceTest extends TestCase
{ {
$this->data['copy_script_from'] = 1; $this->data['copy_script_from'] = 1;
$this->repository->shouldReceive('isCopyableScript')->with(1, $this->model->nest_id)->once()->andReturn(false); $this->expectException(InvalidCopyFromException::class);
try { $this->expectExceptionMessage(trans('exceptions.nest.egg.invalid_copy_id'));
$this->service->handle($this->model, $this->data);
} catch (Exception $exception) { $model = factory(Egg::class)->make(['id' => 123, 'nest_id' => 456]);
$this->assertInstanceOf(InvalidCopyFromException::class, $exception);
$this->assertEquals(trans('exceptions.nest.egg.invalid_copy_id'), $exception->getMessage()); $this->repository->expects('isCopyableScript')->with(1, $model->nest_id)->andReturn(false);
} $this->getService()->handle($model, $this->data);
} }
/** /**
@ -93,21 +73,15 @@ class InstallScriptServiceTest extends TestCase
*/ */
public function testUpdateWithoutNewCopyScriptFromAttribute() public function testUpdateWithoutNewCopyScriptFromAttribute()
{ {
$this->repository->shouldReceive('withoutFreshModel')->withNoArgs()->once()->andReturnSelf() $model = factory(Egg::class)->make(['id' => 123, 'nest_id' => 456]);
->shouldReceive('update')->with($this->model->id, $this->data)->andReturnNull();
$this->service->handle($this->model, $this->data); $this->repository->expects('withoutFreshModel->update')->with($model->id, $this->data)->andReturnNull();
$this->getService()->handle($model, $this->data);
} }
/** private function getService()
* Test that an integer can be passed in place of a model.
*/
public function testFunctionAcceptsIntegerInPlaceOfModel()
{ {
$this->repository->shouldReceive('find')->with($this->model->id)->once()->andReturn($this->model); return new InstallScriptService($this->repository);
$this->repository->shouldReceive('withoutFreshModel')->withNoArgs()->once()->andReturnSelf()
->shouldReceive('update')->with($this->model->id, $this->data)->andReturnNull();
$this->service->handle($this->model->id, $this->data);
} }
} }

View file

@ -1,11 +1,4 @@
<?php <?php
/**
* Pterodactyl - Panel
* Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com>.
*
* This software is licensed under the terms of the MIT license.
* https://opensource.org/licenses/MIT
*/
namespace Tests\Unit\Services\Eggs\Sharing; namespace Tests\Unit\Services\Eggs\Sharing;
@ -22,21 +15,11 @@ class EggExporterServiceTest extends TestCase
{ {
use NestedObjectAssertionsTrait; use NestedObjectAssertionsTrait;
/**
* @var \Carbon\Carbon
*/
protected $carbon;
/** /**
* @var \Pterodactyl\Contracts\Repository\EggRepositoryInterface|\Mockery\Mock * @var \Pterodactyl\Contracts\Repository\EggRepositoryInterface|\Mockery\Mock
*/ */
protected $repository; protected $repository;
/**
* @var \Pterodactyl\Services\Eggs\Sharing\EggExporterService
*/
protected $service;
/** /**
* Setup tests. * Setup tests.
*/ */
@ -45,10 +28,8 @@ class EggExporterServiceTest extends TestCase
parent::setUp(); parent::setUp();
Carbon::setTestNow(Carbon::now()); Carbon::setTestNow(Carbon::now());
$this->carbon = new Carbon();
$this->repository = m::mock(EggRepositoryInterface::class);
$this->service = new EggExporterService($this->repository); $this->repository = m::mock(EggRepositoryInterface::class);
} }
/** /**
@ -56,12 +37,17 @@ class EggExporterServiceTest extends TestCase
*/ */
public function testJsonStructureIsExported() public function testJsonStructureIsExported()
{ {
$egg = factory(Egg::class)->make(); $egg = factory(Egg::class)->make([
'id' => 123,
'nest_id' => 456,
]);
$egg->variables = collect([$variable = factory(EggVariable::class)->make()]); $egg->variables = collect([$variable = factory(EggVariable::class)->make()]);
$this->repository->shouldReceive('getWithExportAttributes')->with($egg->id)->once()->andReturn($egg); $this->repository->shouldReceive('getWithExportAttributes')->with($egg->id)->once()->andReturn($egg);
$response = $this->service->handle($egg->id); $service = new EggExporterService($this->repository);
$response = $service->handle($egg->id);
$this->assertNotEmpty($response); $this->assertNotEmpty($response);
$data = json_decode($response); $data = json_decode($response);

View file

@ -1,13 +1,6 @@
<?php <?php
/**
* Pterodactyl - Panel
* Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com>.
*
* This software is licensed under the terms of the MIT license.
* https://opensource.org/licenses/MIT
*/
namespace Tests\Unit\Services\Services\Sharing; namespace Tests\Unit\Services\Eggs\Sharing;
use Mockery as m; use Mockery as m;
use Tests\TestCase; use Tests\TestCase;
@ -17,7 +10,6 @@ use Tests\Traits\MocksUuids;
use Illuminate\Http\UploadedFile; use Illuminate\Http\UploadedFile;
use Pterodactyl\Models\EggVariable; use Pterodactyl\Models\EggVariable;
use Illuminate\Database\ConnectionInterface; use Illuminate\Database\ConnectionInterface;
use Pterodactyl\Exceptions\PterodactylException;
use Pterodactyl\Services\Eggs\Sharing\EggImporterService; use Pterodactyl\Services\Eggs\Sharing\EggImporterService;
use Pterodactyl\Contracts\Repository\EggRepositoryInterface; use Pterodactyl\Contracts\Repository\EggRepositoryInterface;
use Pterodactyl\Contracts\Repository\NestRepositoryInterface; use Pterodactyl\Contracts\Repository\NestRepositoryInterface;
@ -66,9 +58,9 @@ class EggImporterServiceTest extends TestCase
{ {
parent::setUp(); parent::setUp();
$this->file = m::mock(UploadedFile::class);
$this->connection = m::mock(ConnectionInterface::class); $this->connection = m::mock(ConnectionInterface::class);
$this->eggVariableRepository = m::mock(EggVariableRepositoryInterface::class); $this->eggVariableRepository = m::mock(EggVariableRepositoryInterface::class);
$this->file = m::mock(UploadedFile::class);
$this->nestRepository = m::mock(NestRepositoryInterface::class); $this->nestRepository = m::mock(NestRepositoryInterface::class);
$this->repository = m::mock(EggRepositoryInterface::class); $this->repository = m::mock(EggRepositoryInterface::class);
@ -82,13 +74,14 @@ class EggImporterServiceTest extends TestCase
*/ */
public function testEggConfigurationIsImported() public function testEggConfigurationIsImported()
{ {
$egg = factory(Egg::class)->make(); $egg = factory(Egg::class)->make(['id' => 123]);
$nest = factory(Nest::class)->make(); $nest = factory(Nest::class)->make(['id' => 456]);
$this->file->shouldReceive('getError')->withNoArgs()->once()->andReturn(UPLOAD_ERR_OK); $this->file->expects('getError')->andReturn(UPLOAD_ERR_OK);
$this->file->shouldReceive('isFile')->withNoArgs()->once()->andReturn(true); $this->file->expects('isFile')->andReturn(true);
$this->file->shouldReceive('getSize')->withNoArgs()->once()->andReturn(100); $this->file->expects('getSize')->andReturn(100);
$this->file->shouldReceive('openFile->fread')->with(100)->once()->andReturn(json_encode([
$this->file->expects('openFile->fread')->with(100)->once()->andReturn(json_encode([
'meta' => ['version' => 'PTDL_v1'], 'meta' => ['version' => 'PTDL_v1'],
'name' => $egg->name, 'name' => $egg->name,
'author' => $egg->author, 'author' => $egg->author,
@ -122,13 +115,18 @@ class EggImporterServiceTest extends TestCase
*/ */
public function testExceptionIsThrownIfFileIsInvalid() public function testExceptionIsThrownIfFileIsInvalid()
{ {
$this->file->shouldReceive('getError')->withNoArgs()->once()->andReturn(UPLOAD_ERR_NO_FILE); $this->expectException(InvalidFileUploadException::class);
try { $this->expectExceptionMessage(
$this->service->handle($this->file, 1234); 'The selected file ["test.txt"] was not in a valid format to import. (is_file: true is_valid: true err_code: 4 err: UPLOAD_ERR_NO_FILE)'
} catch (PterodactylException $exception) { );
$this->assertInstanceOf(InvalidFileUploadException::class, $exception);
$this->assertEquals(trans('exceptions.nest.importer.file_error'), $exception->getMessage()); $this->file->expects('getFilename')->andReturns('test.txt');
} $this->file->expects('isFile')->andReturns(true);
$this->file->expects('isValid')->andReturns(true);
$this->file->expects('getError')->twice()->andReturns(UPLOAD_ERR_NO_FILE);
$this->file->expects('getErrorMessage')->andReturns('UPLOAD_ERR_NO_FILE');
$this->service->handle($this->file, 1234);
} }
/** /**
@ -136,15 +134,18 @@ class EggImporterServiceTest extends TestCase
*/ */
public function testExceptionIsThrownIfFileIsNotAFile() public function testExceptionIsThrownIfFileIsNotAFile()
{ {
$this->file->shouldReceive('getError')->withNoArgs()->once()->andReturn(UPLOAD_ERR_OK); $this->expectException(InvalidFileUploadException::class);
$this->file->shouldReceive('isFile')->withNoArgs()->once()->andReturn(false); $this->expectExceptionMessage(
'The selected file ["test.txt"] was not in a valid format to import. (is_file: false is_valid: true err_code: 4 err: UPLOAD_ERR_NO_FILE)'
);
try { $this->file->expects('getFilename')->andReturns('test.txt');
$this->service->handle($this->file, 1234); $this->file->expects('isFile')->andReturns(false);
} catch (PterodactylException $exception) { $this->file->expects('isValid')->andReturns(true);
$this->assertInstanceOf(InvalidFileUploadException::class, $exception); $this->file->expects('getError')->twice()->andReturns(UPLOAD_ERR_NO_FILE);
$this->assertEquals(trans('exceptions.nest.importer.file_error'), $exception->getMessage()); $this->file->expects('getErrorMessage')->andReturns('UPLOAD_ERR_NO_FILE');
}
$this->service->handle($this->file, 1234);
} }
/** /**
@ -152,19 +153,18 @@ class EggImporterServiceTest extends TestCase
*/ */
public function testExceptionIsThrownIfJsonMetaDataIsInvalid() public function testExceptionIsThrownIfJsonMetaDataIsInvalid()
{ {
$this->file->shouldReceive('getError')->withNoArgs()->once()->andReturn(UPLOAD_ERR_OK); $this->expectException(InvalidFileUploadException::class);
$this->file->shouldReceive('isFile')->withNoArgs()->once()->andReturn(true); $this->expectExceptionMessage(trans('exceptions.nest.importer.invalid_json_provided'));
$this->file->shouldReceive('getSize')->withNoArgs()->once()->andReturn(100);
$this->file->shouldReceive('openFile->fread')->with(100)->once()->andReturn(json_encode([ $this->file->expects('getError')->andReturn(UPLOAD_ERR_OK);
$this->file->expects('isFile')->andReturn(true);
$this->file->expects('getSize')->andReturn(100);
$this->file->expects('openFile->fread')->with(100)->andReturn(json_encode([
'meta' => ['version' => 'hodor'], 'meta' => ['version' => 'hodor'],
])); ]));
try { $this->service->handle($this->file, 1234);
$this->service->handle($this->file, 1234);
} catch (PterodactylException $exception) {
$this->assertInstanceOf(InvalidFileUploadException::class, $exception);
$this->assertEquals(trans('exceptions.nest.importer.invalid_json_provided'), $exception->getMessage());
}
} }
/** /**
@ -172,18 +172,16 @@ class EggImporterServiceTest extends TestCase
*/ */
public function testExceptionIsThrownIfBadJsonIsProvided() public function testExceptionIsThrownIfBadJsonIsProvided()
{ {
$this->file->shouldReceive('getError')->withNoArgs()->once()->andReturn(UPLOAD_ERR_OK); $this->expectException(BadJsonFormatException::class);
$this->file->shouldReceive('isFile')->withNoArgs()->once()->andReturn(true); $this->expectExceptionMessage(trans('exceptions.nest.importer.json_error', [
$this->file->shouldReceive('getSize')->withNoArgs()->once()->andReturn(100); 'error' => 'Syntax error',
$this->file->shouldReceive('openFile->fread')->with(100)->once()->andReturn('}'); ]));
try { $this->file->expects('getError')->andReturn(UPLOAD_ERR_OK);
$this->service->handle($this->file, 1234); $this->file->expects('isFile')->andReturn(true);
} catch (PterodactylException $exception) { $this->file->expects('getSize')->andReturn(100);
$this->assertInstanceOf(BadJsonFormatException::class, $exception); $this->file->expects('openFile->fread')->with(100)->andReturn('}');
$this->assertEquals(trans('exceptions.nest.importer.json_error', [
'error' => json_last_error_msg(), $this->service->handle($this->file, 1234);
]), $exception->getMessage());
}
} }
} }

View file

@ -62,7 +62,7 @@ class EggUpdateImporterServiceTest extends TestCase
*/ */
public function testEggIsUpdated() public function testEggIsUpdated()
{ {
$egg = factory(Egg::class)->make(); $egg = factory(Egg::class)->make(['id' => 123]);
$variable = factory(EggVariable::class)->make(); $variable = factory(EggVariable::class)->make();
$this->file->shouldReceive('getError')->withNoArgs()->once()->andReturn(UPLOAD_ERR_OK); $this->file->shouldReceive('getError')->withNoArgs()->once()->andReturn(UPLOAD_ERR_OK);
@ -91,7 +91,7 @@ class EggUpdateImporterServiceTest extends TestCase
$this->connection->shouldReceive('commit')->withNoArgs()->once()->andReturnNull(); $this->connection->shouldReceive('commit')->withNoArgs()->once()->andReturnNull();
$this->service->handle($egg->id, $this->file); $this->service->handle($egg, $this->file);
$this->assertTrue(true); $this->assertTrue(true);
} }
@ -101,7 +101,7 @@ class EggUpdateImporterServiceTest extends TestCase
*/ */
public function testVariablesMissingFromImportAreDeleted() public function testVariablesMissingFromImportAreDeleted()
{ {
$egg = factory(Egg::class)->make(); $egg = factory(Egg::class)->make(['id' => 123]);
$variable1 = factory(EggVariable::class)->make(); $variable1 = factory(EggVariable::class)->make();
$variable2 = factory(EggVariable::class)->make(); $variable2 = factory(EggVariable::class)->make();
@ -136,7 +136,7 @@ class EggUpdateImporterServiceTest extends TestCase
$this->connection->shouldReceive('commit')->withNoArgs()->once()->andReturnNull(); $this->connection->shouldReceive('commit')->withNoArgs()->once()->andReturnNull();
$this->service->handle($egg->id, $this->file); $this->service->handle($egg, $this->file);
$this->assertTrue(true); $this->assertTrue(true);
} }
@ -145,13 +145,13 @@ class EggUpdateImporterServiceTest extends TestCase
*/ */
public function testExceptionIsThrownIfFileIsInvalid() public function testExceptionIsThrownIfFileIsInvalid()
{ {
$this->file->shouldReceive('getError')->withNoArgs()->once()->andReturn(UPLOAD_ERR_NO_FILE); $egg = factory(Egg::class)->make(['id' => 123]);
try {
$this->service->handle(1234, $this->file); $this->expectException(InvalidFileUploadException::class);
} catch (PterodactylException $exception) { $this->expectExceptionMessageMatches('/^The selected file \["test\.txt"\] was not in a valid format to import\./');
$this->assertInstanceOf(InvalidFileUploadException::class, $exception); $file = new UploadedFile('test.txt', 'original.txt', 'application/json', UPLOAD_ERR_NO_FILE, true);
$this->assertEquals(trans('exceptions.nest.importer.file_error'), $exception->getMessage());
} $this->service->handle($egg, $file);
} }
/** /**
@ -159,15 +159,18 @@ class EggUpdateImporterServiceTest extends TestCase
*/ */
public function testExceptionIsThrownIfFileIsNotAFile() public function testExceptionIsThrownIfFileIsNotAFile()
{ {
$this->file->shouldReceive('getError')->withNoArgs()->once()->andReturn(UPLOAD_ERR_OK); $egg = factory(Egg::class)->make(['id' => 123]);
$this->file->shouldReceive('isFile')->withNoArgs()->once()->andReturn(false);
try { $this->expectException(InvalidFileUploadException::class);
$this->service->handle(1234, $this->file); $this->expectExceptionMessageMatches('/^The selected file \["test\.txt"\] was not in a valid format to import\./');
} catch (PterodactylException $exception) {
$this->assertInstanceOf(InvalidFileUploadException::class, $exception); $file = m::mock(
$this->assertEquals(trans('exceptions.nest.importer.file_error'), $exception->getMessage()); new UploadedFile('test.txt', 'original.txt', 'application/json', UPLOAD_ERR_INI_SIZE, true)
} )->makePartial();
$file->expects('isFile')->andReturnFalse();
$this->service->handle($egg, $file);
} }
/** /**
@ -175,6 +178,8 @@ class EggUpdateImporterServiceTest extends TestCase
*/ */
public function testExceptionIsThrownIfJsonMetaDataIsInvalid() public function testExceptionIsThrownIfJsonMetaDataIsInvalid()
{ {
$egg = factory(Egg::class)->make(['id' => 123]);
$this->file->shouldReceive('getError')->withNoArgs()->once()->andReturn(UPLOAD_ERR_OK); $this->file->shouldReceive('getError')->withNoArgs()->once()->andReturn(UPLOAD_ERR_OK);
$this->file->shouldReceive('isFile')->withNoArgs()->once()->andReturn(true); $this->file->shouldReceive('isFile')->withNoArgs()->once()->andReturn(true);
$this->file->shouldReceive('getSize')->withNoArgs()->once()->andReturn(100); $this->file->shouldReceive('getSize')->withNoArgs()->once()->andReturn(100);
@ -183,7 +188,7 @@ class EggUpdateImporterServiceTest extends TestCase
])); ]));
try { try {
$this->service->handle(1234, $this->file); $this->service->handle($egg, $this->file);
} catch (PterodactylException $exception) { } catch (PterodactylException $exception) {
$this->assertInstanceOf(InvalidFileUploadException::class, $exception); $this->assertInstanceOf(InvalidFileUploadException::class, $exception);
$this->assertEquals(trans('exceptions.nest.importer.invalid_json_provided'), $exception->getMessage()); $this->assertEquals(trans('exceptions.nest.importer.invalid_json_provided'), $exception->getMessage());
@ -195,13 +200,15 @@ class EggUpdateImporterServiceTest extends TestCase
*/ */
public function testExceptionIsThrownIfBadJsonIsProvided() public function testExceptionIsThrownIfBadJsonIsProvided()
{ {
$egg = factory(Egg::class)->make(['id' => 123]);
$this->file->shouldReceive('getError')->withNoArgs()->once()->andReturn(UPLOAD_ERR_OK); $this->file->shouldReceive('getError')->withNoArgs()->once()->andReturn(UPLOAD_ERR_OK);
$this->file->shouldReceive('isFile')->withNoArgs()->once()->andReturn(true); $this->file->shouldReceive('isFile')->withNoArgs()->once()->andReturn(true);
$this->file->shouldReceive('getSize')->withNoArgs()->once()->andReturn(100); $this->file->shouldReceive('getSize')->withNoArgs()->once()->andReturn(100);
$this->file->shouldReceive('openFile->fread')->with(100)->once()->andReturn('}'); $this->file->shouldReceive('openFile->fread')->with(100)->once()->andReturn('}');
try { try {
$this->service->handle(1234, $this->file); $this->service->handle($egg, $this->file);
} catch (PterodactylException $exception) { } catch (PterodactylException $exception) {
$this->assertInstanceOf(BadJsonFormatException::class, $exception); $this->assertInstanceOf(BadJsonFormatException::class, $exception);
$this->assertEquals(trans('exceptions.nest.importer.json_error', [ $this->assertEquals(trans('exceptions.nest.importer.json_error', [

View file

@ -1,11 +1,4 @@
<?php <?php
/**
* Pterodactyl - Panel
* Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com>.
*
* This software is licensed under the terms of the MIT license.
* https://opensource.org/licenses/MIT
*/
namespace Tests\Unit\Services\Nodes; namespace Tests\Unit\Services\Nodes;
@ -90,7 +83,7 @@ class NodeDeletionServiceTest extends TestCase
*/ */
public function testModelCanBePassedToFunctionInPlaceOfNodeId() public function testModelCanBePassedToFunctionInPlaceOfNodeId()
{ {
$node = factory(Node::class)->make(); $node = factory(Node::class)->make(['id' => 123]);
$this->serverRepository->shouldReceive('setColumns')->with('id')->once()->andReturnSelf() $this->serverRepository->shouldReceive('setColumns')->with('id')->once()->andReturnSelf()
->shouldReceive('findCountWhere')->with([['node_id', '=', $node->id]])->once()->andReturn(0); ->shouldReceive('findCountWhere')->with([['node_id', '=', $node->id]])->once()->andReturn(0);

View file

@ -47,7 +47,7 @@ class ProcessScheduleServiceTest extends TestCase
*/ */
public function testScheduleIsUpdatedAndRun() public function testScheduleIsUpdatedAndRun()
{ {
$model = factory(Schedule::class)->make(); $model = factory(Schedule::class)->make(['id' => 123]);
$model->setRelation('tasks', collect([$task = factory(Task::class)->make([ $model->setRelation('tasks', collect([$task = factory(Task::class)->make([
'sequence_id' => 1, 'sequence_id' => 1,
])])); ])]));
@ -65,14 +65,12 @@ class ProcessScheduleServiceTest extends TestCase
$this->dispatcher->shouldReceive('dispatch')->with(m::on(function ($class) use ($model, $task) { $this->dispatcher->shouldReceive('dispatch')->with(m::on(function ($class) use ($model, $task) {
$this->assertInstanceOf(RunTaskJob::class, $class); $this->assertInstanceOf(RunTaskJob::class, $class);
$this->assertSame($task->time_offset, $class->delay); $this->assertSame($task->time_offset, $class->delay);
$this->assertSame($task->id, $class->task); $this->assertSame($task->id, $class->task->id);
$this->assertSame($model->id, $class->schedule);
return true; return true;
}))->once(); }))->once();
$this->getService()->handle($model); $this->getService()->handle($model);
$this->assertTrue(true);
} }
/** /**

View file

@ -6,19 +6,13 @@ use Mockery as m;
use Tests\TestCase; use Tests\TestCase;
use Pterodactyl\Models\Server; use Pterodactyl\Models\Server;
use Pterodactyl\Models\Location; use Pterodactyl\Models\Location;
use Illuminate\Contracts\Config\Repository; use Illuminate\Support\Collection;
use Pterodactyl\Models\EggVariable;
use Pterodactyl\Services\Servers\EnvironmentService; use Pterodactyl\Services\Servers\EnvironmentService;
use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; use Pterodactyl\Contracts\Repository\ServerRepositoryInterface;
class EnvironmentServiceTest extends TestCase class EnvironmentServiceTest extends TestCase
{ {
const CONFIG_MAPPING = 'pterodactyl.environment_variables';
/**
* @var \Illuminate\Contracts\Config\Repository|\Mockery\Mock
*/
private $config;
/** /**
* @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface|\Mockery\Mock * @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface|\Mockery\Mock
*/ */
@ -30,9 +24,7 @@ class EnvironmentServiceTest extends TestCase
public function setUp(): void public function setUp(): void
{ {
parent::setUp(); parent::setUp();
config()->set('pterodactyl.environment_variables', []);
$this->config = m::mock(Repository::class);
$this->repository = m::mock(ServerRepositoryInterface::class);
} }
/** /**
@ -55,15 +47,17 @@ class EnvironmentServiceTest extends TestCase
*/ */
public function testProcessShouldReturnDefaultEnvironmentVariablesForAServer() public function testProcessShouldReturnDefaultEnvironmentVariablesForAServer()
{ {
$model = $this->getServerModel(); $model = $this->getServerModel([
$this->config->shouldReceive('get')->with(self::CONFIG_MAPPING, [])->once()->andReturn([]); 'TEST_VARIABLE' => factory(EggVariable::class)->make([
$this->repository->shouldReceive('getVariablesWithValues')->with($model->id)->once()->andReturn([ 'id' => 987,
'TEST_VARIABLE' => 'Test Variable', 'env_variable' => 'TEST_VARIABLE',
'default_value' => 'Test Variable',
]),
]); ]);
$response = $this->getService()->handle($model); $response = $this->getService()->handle($model);
$this->assertNotEmpty($response); $this->assertNotEmpty($response);
$this->assertEquals(4, count($response)); $this->assertCount(4, $response);
$this->assertArrayHasKey('TEST_VARIABLE', $response); $this->assertArrayHasKey('TEST_VARIABLE', $response);
$this->assertSame('Test Variable', $response['TEST_VARIABLE']); $this->assertSame('Test Variable', $response['TEST_VARIABLE']);
} }
@ -73,10 +67,7 @@ class EnvironmentServiceTest extends TestCase
*/ */
public function testProcessShouldReturnKeySetAtRuntime() public function testProcessShouldReturnKeySetAtRuntime()
{ {
$model = $this->getServerModel(); $model = $this->getServerModel([]);
$this->config->shouldReceive('get')->with(self::CONFIG_MAPPING, [])->once()->andReturn([]);
$this->repository->shouldReceive('getVariablesWithValues')->with($model->id)->once()->andReturn([]);
$service = $this->getService(); $service = $this->getService();
$service->setEnvironmentKey('TEST_VARIABLE', function ($server) { $service->setEnvironmentKey('TEST_VARIABLE', function ($server) {
return $server->uuidShort; return $server->uuidShort;
@ -94,12 +85,11 @@ class EnvironmentServiceTest extends TestCase
*/ */
public function testProcessShouldAllowOverwritingVariablesWithConfigurationFile() public function testProcessShouldAllowOverwritingVariablesWithConfigurationFile()
{ {
$model = $this->getServerModel(); config()->set('pterodactyl.environment_variables', [
$this->repository->shouldReceive('getVariablesWithValues')->with($model->id)->once()->andReturn([]);
$this->config->shouldReceive('get')->with(self::CONFIG_MAPPING, [])->once()->andReturn([
'P_SERVER_UUID' => 'name', 'P_SERVER_UUID' => 'name',
]); ]);
$model = $this->getServerModel([]);
$response = $this->getService()->handle($model); $response = $this->getService()->handle($model);
$this->assertNotEmpty($response); $this->assertNotEmpty($response);
@ -113,14 +103,13 @@ class EnvironmentServiceTest extends TestCase
*/ */
public function testVariablesSetInConfigurationAllowForClosures() public function testVariablesSetInConfigurationAllowForClosures()
{ {
$model = $this->getServerModel(); config()->set('pterodactyl.environment_variables', [
$this->config->shouldReceive('get')->with(self::CONFIG_MAPPING, [])->once()->andReturn([
'P_SERVER_UUID' => function ($server) { 'P_SERVER_UUID' => function ($server) {
return $server->id * 2; return $server->id * 2;
}, },
]); ]);
$this->repository->shouldReceive('getVariablesWithValues')->with($model->id)->once()->andReturn([]);
$model = $this->getServerModel([]);
$response = $this->getService()->handle($model); $response = $this->getService()->handle($model);
$this->assertNotEmpty($response); $this->assertNotEmpty($response);
@ -135,12 +124,11 @@ class EnvironmentServiceTest extends TestCase
*/ */
public function testProcessShouldAllowOverwritingDefaultVariablesWithRuntimeProvided() public function testProcessShouldAllowOverwritingDefaultVariablesWithRuntimeProvided()
{ {
$model = $this->getServerModel(); config()->set('pterodactyl.environment_variables', [
$this->config->shouldReceive('get')->with(self::CONFIG_MAPPING, [])->once()->andReturn([
'P_SERVER_UUID' => 'overwritten-config', 'P_SERVER_UUID' => 'overwritten-config',
]); ]);
$this->repository->shouldReceive('getVariablesWithValues')->with($model->id)->once()->andReturn([]);
$model = $this->getServerModel([]);
$service = $this->getService(); $service = $this->getService();
$service->setEnvironmentKey('P_SERVER_UUID', function ($model) { $service->setEnvironmentKey('P_SERVER_UUID', function ($model) {
return 'overwritten'; return 'overwritten';
@ -161,18 +149,25 @@ class EnvironmentServiceTest extends TestCase
*/ */
private function getService(): EnvironmentService private function getService(): EnvironmentService
{ {
return new EnvironmentService($this->config, $this->repository); return new EnvironmentService;
} }
/** /**
* Return a server model with a location relationship to be used in the tests. * Return a server model with a location relationship to be used in the tests.
* *
* @param array $variables
* @return \Pterodactyl\Models\Server * @return \Pterodactyl\Models\Server
*/ */
private function getServerModel(): Server private function getServerModel(array $variables): Server
{ {
return factory(Server::class)->make([ /** @var \Pterodactyl\Models\Server $server */
$server = factory(Server::class)->make([
'id' => 123,
'location' => factory(Location::class)->make(), 'location' => factory(Location::class)->make(),
]); ]);
$server->setRelation('variables', Collection::make($variables));
return $server;
} }
} }

View file

@ -1,82 +0,0 @@
<?php
/**
* Pterodactyl - Panel
* Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com>.
*
* 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 Pterodactyl\Models\Server;
use Illuminate\Database\ConnectionInterface;
use Pterodactyl\Repositories\Eloquent\ServerRepository;
use Pterodactyl\Services\Servers\ReinstallServerService;
use Pterodactyl\Repositories\Wings\DaemonServerRepository;
class ReinstallServerServiceTest extends TestCase
{
/**
* @var \Pterodactyl\Repositories\Wings\DaemonServerRepository
*/
private $daemonServerRepository;
/**
* @var \Illuminate\Database\ConnectionInterface
*/
private $connection;
/**
* @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface
*/
private $repository;
/**
* Setup tests.
*/
public function setUp(): void
{
parent::setUp();
$this->repository = m::mock(ServerRepository::class);
$this->connection = m::mock(ConnectionInterface::class);
$this->daemonServerRepository = m::mock(DaemonServerRepository::class);
}
/**
* Test that a server is reinstalled when it's model is passed to the function.
*/
public function testServerShouldBeReinstalledWhenModelIsPassed()
{
/** @var \Pterodactyl\Models\Server $server */
$server = factory(Server::class)->make(['id' => 123]);
$updated = clone $server;
$updated->installed = Server::STATUS_INSTALLING;
$this->connection->expects('transaction')->with(m::on(function ($closure) use ($updated) {
return $closure() instanceof Server;
}))->andReturn($updated);
$this->repository->expects('update')->with($server->id, [
'installed' => Server::STATUS_INSTALLING,
])->andReturns($updated);
$this->daemonServerRepository->expects('setServer')->with($server)->andReturnSelf();
$this->daemonServerRepository->expects('reinstall')->withNoArgs();
$this->assertSame($updated, $this->getService()->reinstall($server));
}
/**
* @return \Pterodactyl\Services\Servers\ReinstallServerService
*/
private function getService()
{
return new ReinstallServerService(
$this->connection, $this->daemonServerRepository, $this->repository
);
}
}

View file

@ -57,8 +57,8 @@ class ServerConfigurationStructureServiceTest extends TestCase
$this->assertArrayHasKey('suspended', $response); $this->assertArrayHasKey('suspended', $response);
$this->assertArrayHasKey('environment', $response); $this->assertArrayHasKey('environment', $response);
$this->assertArrayHasKey('invocation', $response); $this->assertArrayHasKey('invocation', $response);
$this->assertArrayHasKey('skip_egg_scripts', $response);
$this->assertArrayHasKey('build', $response); $this->assertArrayHasKey('build', $response);
$this->assertArrayHasKey('service', $response);
$this->assertArrayHasKey('container', $response); $this->assertArrayHasKey('container', $response);
$this->assertArrayHasKey('allocations', $response); $this->assertArrayHasKey('allocations', $response);
@ -79,11 +79,6 @@ class ServerConfigurationStructureServiceTest extends TestCase
'disk_space' => $model->disk, 'disk_space' => $model->disk,
], $response['build']); ], $response['build']);
$this->assertSame([
'egg' => $model->egg->uuid,
'skip_scripts' => $model->skip_scripts,
], $response['service']);
$this->assertSame([ $this->assertSame([
'image' => $model->image, 'image' => $model->image,
'oom_disabled' => $model->oom_disabled, 'oom_disabled' => $model->oom_disabled,
@ -91,7 +86,7 @@ class ServerConfigurationStructureServiceTest extends TestCase
], $response['container']); ], $response['container']);
$this->assertSame($model->uuid, $response['uuid']); $this->assertSame($model->uuid, $response['uuid']);
$this->assertSame((bool) $model->suspended, $response['suspended']); $this->assertSame($model->suspended, $response['suspended']);
$this->assertSame(['environment_array'], $response['environment']); $this->assertSame(['environment_array'], $response['environment']);
$this->assertSame($model->startup, $response['invocation']); $this->assertSame($model->startup, $response['invocation']);
} }
@ -103,6 +98,6 @@ class ServerConfigurationStructureServiceTest extends TestCase
*/ */
private function getService(): ServerConfigurationStructureService private function getService(): ServerConfigurationStructureService
{ {
return new ServerConfigurationStructureService($this->repository, $this->environment); return new ServerConfigurationStructureService($this->environment);
} }
} }

View file

@ -1,308 +0,0 @@
<?php
namespace Tests\Unit\Services\Servers;
use Mockery as m;
use Tests\TestCase;
use Pterodactyl\Models\Egg;
use GuzzleHttp\Psr7\Request;
use Pterodactyl\Models\User;
use Tests\Traits\MocksUuids;
use Pterodactyl\Models\Server;
use Pterodactyl\Models\Allocation;
use Tests\Traits\MocksRequestException;
use GuzzleHttp\Exception\ConnectException;
use Illuminate\Database\ConnectionInterface;
use Pterodactyl\Models\Objects\DeploymentObject;
use Pterodactyl\Repositories\Eloquent\EggRepository;
use Pterodactyl\Repositories\Eloquent\ServerRepository;
use Pterodactyl\Services\Servers\ServerCreationService;
use Pterodactyl\Services\Servers\ServerDeletionService;
use Pterodactyl\Repositories\Wings\DaemonServerRepository;
use Pterodactyl\Services\Servers\VariableValidatorService;
use Pterodactyl\Repositories\Eloquent\AllocationRepository;
use Pterodactyl\Services\Deployment\FindViableNodesService;
use Pterodactyl\Repositories\Eloquent\ServerVariableRepository;
use Pterodactyl\Services\Deployment\AllocationSelectionService;
use Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException;
use Pterodactyl\Services\Servers\ServerConfigurationStructureService;
/**
* @preserveGlobalState disabled
*/
class ServerCreationServiceTest extends TestCase
{
use MocksRequestException, MocksUuids;
/**
* @var \Mockery\MockInterface
*/
private $allocationRepository;
/**
* @var \Mockery\MockInterface
*/
private $allocationSelectionService;
/**
* @var \Mockery\MockInterface
*/
private $configurationStructureService;
/**
* @var \Mockery\MockInterface
*/
private $connection;
/**
* @var \Mockery\MockInterface
*/
private $daemonServerRepository;
/**
* @var \Mockery\MockInterface
*/
private $eggRepository;
/**
* @var \Mockery\MockInterface
*/
private $findViableNodesService;
/**
* @var \Mockery\MockInterface
*/
private $repository;
/**
* @var \Mockery\MockInterface
*/
private $serverVariableRepository;
/**
* @var \Mockery\MockInterface
*/
private $validatorService;
/**
* @var \Mockery\MockInterface
*/
private $serverDeletionService;
/**
* Setup tests.
*/
public function setUp(): void
{
parent::setUp();
$this->allocationRepository = m::mock(AllocationRepository::class);
$this->allocationSelectionService = m::mock(AllocationSelectionService::class);
$this->configurationStructureService = m::mock(ServerConfigurationStructureService::class);
$this->connection = m::mock(ConnectionInterface::class);
$this->findViableNodesService = m::mock(FindViableNodesService::class);
$this->validatorService = m::mock(VariableValidatorService::class);
$this->eggRepository = m::mock(EggRepository::class);
$this->repository = m::mock(ServerRepository::class);
$this->serverVariableRepository = m::mock(ServerVariableRepository::class);
$this->daemonServerRepository = m::mock(DaemonServerRepository::class);
$this->serverDeletionService = m::mock(ServerDeletionService::class);
}
/**
* Test core functionality of the creation process.
*/
public function testCreateShouldHitAllOfTheNecessaryServicesAndStoreTheServer()
{
$model = factory(Server::class)->make([
'uuid' => $this->getKnownUuid(),
]);
$this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull();
$this->repository->shouldReceive('isUniqueUuidCombo')
->once()
->with($this->getKnownUuid(), substr($this->getKnownUuid(), 0, 8))
->andReturn(true);
$this->repository->shouldReceive('create')->with(m::subset([
'uuid' => $this->getKnownUuid(),
'uuidShort' => substr($this->getKnownUuid(), 0, 8),
'node_id' => $model->node_id,
'allocation_id' => $model->allocation_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()->andReturn(1);
$this->validatorService->shouldReceive('setUserLevel')->with(User::USER_LEVEL_ADMIN)->once()->andReturnSelf();
$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()->andReturn(true);
$this->configurationStructureService->shouldReceive('handle')->with($model)->once()->andReturn(['test' => 'struct']);
$this->daemonServerRepository->shouldReceive('setServer')->with($model)->once()->andReturnSelf();
$this->daemonServerRepository->shouldReceive('create')->with(['test' => 'struct'])->once();
$this->connection->shouldReceive('commit')->withNoArgs()->once()->andReturnNull();
$response = $this->getService()->handle($model->toArray());
$this->assertSame($model, $response);
}
/**
* Test that optional parameters get auto-filled correctly on the model.
*/
public function testDataIsAutoFilled()
{
$model = factory(Server::class)->make(['uuid' => $this->getKnownUuid()]);
$allocationModel = factory(Allocation::class)->make(['node_id' => $model->node_id]);
$eggModel = factory(Egg::class)->make(['nest_id' => $model->nest_id]);
$this->connection->shouldReceive('beginTransaction')->once()->withNoArgs();
$this->allocationRepository->shouldReceive('setColumns->find')->once()->with($model->allocation_id)->andReturn($allocationModel);
$this->eggRepository->shouldReceive('setColumns->find')->once()->with($model->egg_id)->andReturn($eggModel);
$this->validatorService->shouldReceive('setUserLevel->handle')->once()->andReturn(collect([]));
$this->repository->shouldReceive('isUniqueUuidCombo')
->once()
->with($this->getKnownUuid(), substr($this->getKnownUuid(), 0, 8))
->andReturn(true);
$this->repository->shouldReceive('create')->with(m::subset([
'uuid' => $this->getKnownUuid(),
'uuidShort' => substr($this->getKnownUuid(), 0, 8),
'node_id' => $model->node_id,
'allocation_id' => $model->allocation_id,
'nest_id' => $model->nest_id,
'egg_id' => $model->egg_id,
]))->andReturn($model);
$this->allocationRepository->shouldReceive('assignAllocationsToServer')->once()->with($model->id, [$model->allocation_id]);
$this->configurationStructureService->shouldReceive('handle')->once()->with($model)->andReturn([]);
$this->daemonServerRepository->shouldReceive('setServer->create')->once();
$this->connection->shouldReceive('commit')->once()->withNoArgs()->andReturnNull();
$this->getService()->handle(
collect($model->toArray())->except(['node_id', 'nest_id'])->toArray()
);
}
/**
* Test that an auto-deployment object is used correctly if passed.
*/
public function testAutoDeploymentObject()
{
$model = factory(Server::class)->make(['uuid' => $this->getKnownUuid()]);
$deploymentObject = new DeploymentObject();
$deploymentObject->setPorts(['25565']);
$deploymentObject->setDedicated(false);
$deploymentObject->setLocations([1]);
$this->connection->shouldReceive('beginTransaction')->once()->withNoArgs();
$this->findViableNodesService->shouldReceive('setLocations')->once()->with($deploymentObject->getLocations())->andReturnSelf();
$this->findViableNodesService->shouldReceive('setDisk')->once()->with($model->disk)->andReturnSelf();
$this->findViableNodesService->shouldReceive('setMemory')->once()->with($model->memory)->andReturnSelf();
$this->findViableNodesService->shouldReceive('handle')->once()->withNoArgs()->andReturn([1, 2]);
$allocationModel = factory(Allocation::class)->make([
'id' => $model->allocation_id,
'node_id' => $model->node_id,
]);
$this->allocationSelectionService->shouldReceive('setDedicated')->once()->with($deploymentObject->isDedicated())->andReturnSelf();
$this->allocationSelectionService->shouldReceive('setNodes')->once()->with([1, 2])->andReturnSelf();
$this->allocationSelectionService->shouldReceive('setPorts')->once()->with($deploymentObject->getPorts())->andReturnSelf();
$this->allocationSelectionService->shouldReceive('handle')->once()->withNoArgs()->andReturn($allocationModel);
$this->validatorService->shouldReceive('setUserLevel->handle')->once()->andReturn(collect([]));
$this->repository->shouldReceive('isUniqueUuidCombo')
->once()
->with($this->getKnownUuid(), substr($this->getKnownUuid(), 0, 8))
->andReturn(true);
$this->repository->shouldReceive('create')->with(m::subset([
'uuid' => $this->getKnownUuid(),
'uuidShort' => substr($this->getKnownUuid(), 0, 8),
'node_id' => $model->node_id,
'allocation_id' => $model->allocation_id,
'nest_id' => $model->nest_id,
'egg_id' => $model->egg_id,
]))->andReturn($model);
$this->allocationRepository->shouldReceive('assignAllocationsToServer')->once()->with($model->id, [$model->allocation_id]);
$this->configurationStructureService->shouldReceive('handle')->once()->with($model)->andReturn([]);
$this->daemonServerRepository->shouldReceive('setServer->create')->once();
$this->connection->shouldReceive('commit')->once()->withNoArgs()->andReturnNull();
$this->getService()->handle(
collect($model->toArray())->except(['allocation_id', 'node_id'])->toArray(), $deploymentObject
);
}
/**
* Test handling of node timeout or other daemon error.
*/
public function testExceptionShouldBeThrownIfTheRequestFails()
{
$this->expectException(DaemonConnectionException::class);
$model = factory(Server::class)->make([
'uuid' => $this->getKnownUuid(),
]);
$this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull();
$this->repository->shouldReceive('isUniqueUuidCombo')->once()->andReturn(true);
$this->repository->shouldReceive('create')->once()->andReturn($model);
$this->allocationRepository->shouldReceive('assignAllocationsToServer')->once()->andReturn(1);
$this->validatorService->shouldReceive('setUserLevel')->once()->andReturnSelf();
$this->validatorService->shouldReceive('handle')->once()->andReturn(collect([]));
$this->configurationStructureService->shouldReceive('handle')->once()->andReturn([]);
$this->connection->expects('commit')->withNoArgs();
$this->daemonServerRepository->shouldReceive('setServer')->with($model)->once()->andThrow(
new DaemonConnectionException(
new ConnectException('', new Request('GET', 'test'))
)
);
$this->serverDeletionService->expects('withForce')->with(true)->andReturnSelf();
$this->serverDeletionService->expects('handle')->with($model);
$this->getService()->handle($model->toArray());
}
/**
* Return an instance of the service with mocked dependencies.
*
* @return \Pterodactyl\Services\Servers\ServerCreationService
*/
private function getService(): ServerCreationService
{
return new ServerCreationService(
$this->allocationRepository,
$this->allocationSelectionService,
$this->connection,
$this->daemonServerRepository,
$this->eggRepository,
$this->findViableNodesService,
$this->configurationStructureService,
$this->serverDeletionService,
$this->repository,
$this->serverVariableRepository,
$this->validatorService
);
}
}

View file

@ -1,156 +0,0 @@
<?php
namespace Tests\Unit\Services\Servers;
use Mockery as m;
use Tests\TestCase;
use GuzzleHttp\Psr7\Response;
use Pterodactyl\Models\Server;
use Psr\Log\LoggerInterface as Writer;
use Tests\Traits\MocksRequestException;
use Illuminate\Database\ConnectionInterface;
use Pterodactyl\Services\Servers\ServerDeletionService;
use Pterodactyl\Services\Databases\DatabaseManagementService;
use Pterodactyl\Contracts\Repository\ServerRepositoryInterface;
use Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface;
use Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface as DaemonServerRepositoryInterface;
class ServerDeletionServiceTest extends TestCase
{
use MocksRequestException;
/**
* @var \Illuminate\Database\ConnectionInterface|\Mockery\Mock
*/
private $connection;
/**
* @var \Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface|\Mockery\Mock
*/
private $daemonServerRepository;
/**
* @var \Pterodactyl\Services\Databases\DatabaseManagementService|\Mockery\Mock
*/
private $databaseManagementService;
/**
* @var \Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface|\Mockery\Mock
*/
private $databaseRepository;
/**
* @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface|\Mockery\Mock
*/
private $repository;
/**
* @var \Psr\Log\LoggerInterface|\Mockery\Mock
*/
private $writer;
/**
* Setup tests.
*/
public function setUp(): void
{
parent::setUp();
$this->connection = m::mock(ConnectionInterface::class);
$this->daemonServerRepository = m::mock(DaemonServerRepositoryInterface::class);
$this->databaseRepository = m::mock(DatabaseRepositoryInterface::class);
$this->databaseManagementService = m::mock(DatabaseManagementService::class);
$this->repository = m::mock(ServerRepositoryInterface::class);
$this->writer = m::mock(Writer::class);
}
/**
* Test that a server can be force deleted by setting it in a function call.
*/
public function testForceParameterCanBeSet()
{
$response = $this->getService()->withForce(true);
$this->assertInstanceOf(ServerDeletionService::class, $response);
}
/**
* Test that a server can be deleted when force is not set.
*/
public function testServerCanBeDeletedWithoutForce()
{
$model = factory(Server::class)->make();
$this->daemonServerRepository->shouldReceive('setServer')->once()->with($model)->andReturnSelf();
$this->daemonServerRepository->shouldReceive('delete')->once()->withNoArgs()->andReturn(new Response);
$this->connection->shouldReceive('beginTransaction')->once()->withNoArgs()->andReturnNull();
$this->databaseRepository->shouldReceive('setColumns')->once()->with('id')->andReturnSelf();
$this->databaseRepository->shouldReceive('findWhere')->once()->with([
['server_id', '=', $model->id],
])->andReturn(collect([(object) ['id' => 50]]));
$this->databaseManagementService->shouldReceive('delete')->once()->with(50)->andReturnNull();
$this->repository->shouldReceive('delete')->once()->with($model->id)->andReturn(1);
$this->connection->shouldReceive('commit')->once()->withNoArgs()->andReturnNull();
$this->getService()->handle($model);
}
/**
* Test that a server is deleted when force is set.
*/
public function testServerShouldBeDeletedEvenWhenFailureOccursIfForceIsSet()
{
$this->configureExceptionMock();
$model = factory(Server::class)->make();
$this->daemonServerRepository->shouldReceive('setServer')->once()->with($model)->andReturnSelf();
$this->daemonServerRepository->shouldReceive('delete')->once()->withNoArgs()->andThrow($this->getExceptionMock());
$this->writer->shouldReceive('warning')->with($this->exception)->once()->andReturnNull();
$this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull();
$this->databaseRepository->shouldReceive('setColumns')->with('id')->once()->andReturnSelf();
$this->databaseRepository->shouldReceive('findWhere')->with([
['server_id', '=', $model->id],
])->once()->andReturn(collect([(object) ['id' => 50]]));
$this->databaseManagementService->shouldReceive('delete')->with(50)->once()->andReturnNull();
$this->repository->shouldReceive('delete')->with($model->id)->once()->andReturn(1);
$this->connection->shouldReceive('commit')->withNoArgs()->once()->andReturnNull();
$this->getService()->withForce()->handle($model);
}
/**
* Test that an exception is thrown if a server cannot be deleted from the node and force is not set.
*
* @expectedException \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException
*/
public function testExceptionShouldBeThrownIfDaemonReturnsAnErrorAndForceIsNotSet()
{
$this->configureExceptionMock();
$model = factory(Server::class)->make();
$this->daemonServerRepository->shouldReceive('setServer->delete')->once()->andThrow($this->getExceptionMock());
$this->getService()->handle($model);
}
/**
* Return an instance of the class with mocked dependencies.
*
* @return \Pterodactyl\Services\Servers\ServerDeletionService
*/
private function getService(): ServerDeletionService
{
return new ServerDeletionService(
$this->connection,
$this->daemonServerRepository,
$this->databaseRepository,
$this->databaseManagementService,
$this->repository,
$this->writer
);
}
}

View file

@ -4,9 +4,7 @@ namespace Tests\Unit\Services\Servers;
use Mockery as m; use Mockery as m;
use Tests\TestCase; use Tests\TestCase;
use Pterodactyl\Models\Egg;
use Pterodactyl\Models\Server; use Pterodactyl\Models\Server;
use Illuminate\Support\Collection;
use Pterodactyl\Models\Allocation; use Pterodactyl\Models\Allocation;
use Pterodactyl\Models\EggVariable; use Pterodactyl\Models\EggVariable;
use Pterodactyl\Services\Servers\StartupCommandService; use Pterodactyl\Services\Servers\StartupCommandService;
@ -34,43 +32,33 @@ class StartupCommandViewServiceTest extends TestCase
*/ */
public function testServiceResponse() public function testServiceResponse()
{ {
$allocation = factory(Allocation::class)->make();
$egg = factory(Egg::class)->make();
$server = factory(Server::class)->make([ $server = factory(Server::class)->make([
'id' => 123,
'startup' => 'example {{SERVER_MEMORY}} {{SERVER_IP}} {{SERVER_PORT}} {{TEST_VARIABLE}} {{TEST_VARIABLE_HIDDEN}} {{UNKNOWN}}', 'startup' => 'example {{SERVER_MEMORY}} {{SERVER_IP}} {{SERVER_PORT}} {{TEST_VARIABLE}} {{TEST_VARIABLE_HIDDEN}} {{UNKNOWN}}',
]); ]);
$variables = collect([ $variables = collect([
factory(EggVariable::class)->make(['env_variable' => 'TEST_VARIABLE', 'user_viewable' => 1]), factory(EggVariable::class)->make([
factory(EggVariable::class)->make(['env_variable' => 'TEST_VARIABLE_HIDDEN', 'user_viewable' => 0]), 'env_variable' => 'TEST_VARIABLE',
'server_value' => 'Test Value',
'user_viewable' => 1,
]),
factory(EggVariable::class)->make([
'env_variable' => 'TEST_VARIABLE_HIDDEN',
'server_value' => 'Hidden Value',
'user_viewable' => 0,
]),
]); ]);
$egg->setRelation('variables', $variables); $server->setRelation('variables', $variables);
$server->setRelation('allocation', $allocation); $server->setRelation('allocation', $allocation = factory(Allocation::class)->make());
$server->setRelation('egg', $egg);
$this->repository->shouldReceive('getVariablesWithValues')->once()->with($server->id, true)->andReturn((object) [
'data' => [
'TEST_VARIABLE' => 'Test Value',
'TEST_VARIABLE_HIDDEN' => 'Hidden Value',
],
'server' => $server,
]);
$this->repository->shouldReceive('getPrimaryAllocation')->once()->with($server)->andReturn($server);
$response = $this->getService()->handle($server->id);
$this->assertInstanceOf(Collection::class, $response);
$response = $this->getService()->handle($server);
$this->assertSame( $this->assertSame(
sprintf('example %s %s %s %s %s {{UNKNOWN}}', $server->memory, $allocation->ip, $allocation->port, 'Test Value', '[hidden]'), sprintf('example %s %s %s %s %s {{UNKNOWN}}', $server->memory, $allocation->ip, $allocation->port, 'Test Value', '[hidden]'),
$response->get('startup') $response
); );
$this->assertEquals($variables->only(0), $response->get('variables'));
$this->assertSame([
'TEST_VARIABLE' => 'Test Value',
'TEST_VARIABLE_HIDDEN' => 'Hidden Value',
], $response->get('server_values'));
} }
/** /**
@ -80,6 +68,6 @@ class StartupCommandViewServiceTest extends TestCase
*/ */
private function getService(): StartupCommandService private function getService(): StartupCommandService
{ {
return new StartupCommandService($this->repository); return new StartupCommandService;
} }
} }

View file

@ -1,194 +0,0 @@
<?php
namespace Tests\Unit\Services\Servers;
use Mockery as m;
use Tests\TestCase;
use Pterodactyl\Models\Egg;
use Pterodactyl\Models\User;
use GuzzleHttp\Psr7\Response;
use Pterodactyl\Models\Server;
use Illuminate\Database\ConnectionInterface;
use Pterodactyl\Services\Servers\EnvironmentService;
use Pterodactyl\Services\Servers\VariableValidatorService;
use Pterodactyl\Contracts\Repository\EggRepositoryInterface;
use Pterodactyl\Services\Servers\StartupModificationService;
use Pterodactyl\Contracts\Repository\ServerRepositoryInterface;
use Pterodactyl\Contracts\Repository\ServerVariableRepositoryInterface;
use Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface as DaemonServerRepository;
class StartupModificationServiceTest extends TestCase
{
/**
* @var \Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface|\Mockery\Mock
*/
private $daemonServerRepository;
/**
* @var \Illuminate\Database\ConnectionInterface|\Mockery\Mock
*/
private $connection;
/**
* @var \Pterodactyl\Contracts\Repository\EggRepositoryInterface|\Mockery\Mock
*/
private $eggRepository;
/**
* @var \Pterodactyl\Services\Servers\EnvironmentService|\Mockery\Mock
*/
private $environmentService;
/**
* @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface|\Mockery\Mock
*/
private $repository;
/**
* @var \Pterodactyl\Contracts\Repository\ServerVariableRepositoryInterface|\Mockery\Mock
*/
private $serverVariableRepository;
/**
* @var \Pterodactyl\Services\Servers\VariableValidatorService|\Mockery\Mock
*/
private $validatorService;
/**
* Setup tests.
*/
public function setUp(): void
{
parent::setUp();
$this->daemonServerRepository = m::mock(DaemonServerRepository::class);
$this->connection = m::mock(ConnectionInterface::class);
$this->eggRepository = m::mock(EggRepositoryInterface::class);
$this->environmentService = m::mock(EnvironmentService::class);
$this->repository = m::mock(ServerRepositoryInterface::class);
$this->serverVariableRepository = m::mock(ServerVariableRepositoryInterface::class);
$this->validatorService = m::mock(VariableValidatorService::class);
}
/**
* 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('withoutFreshModel')->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('setServer')->with($model)->once()->andReturnSelf();
$this->daemonServerRepository->shouldReceive('update')->with([
'build' => ['env|overwrite' => ['env']],
])->once()->andReturn(new Response);
$this->connection->shouldReceive('commit')->withNoArgs()->once()->andReturnNull();
$response = $this->getService()->handle($model, ['egg_id' => 123, 'environment' => ['test' => 'abcd1234']]);
$this->assertInstanceOf(Server::class, $response);
$this->assertSame($model, $response);
}
/**
* Test startup modification as an admin user.
*/
public function testStartupModificationAsAdminUser()
{
$model = factory(Server::class)->make([
'egg_id' => 123,
'image' => 'docker:image',
]);
$eggModel = factory(Egg::class)->make([
'id' => 456,
'nest_id' => 12345,
]);
$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'], (object) ['id' => 2, 'value' => null]])
);
$this->serverVariableRepository->shouldReceive('withoutFreshModel->updateOrCreate')->once()->with([
'server_id' => $model->id,
'variable_id' => 1,
], ['variable_value' => 'stored-value'])->andReturnNull();
$this->serverVariableRepository->shouldReceive('withoutFreshModel->updateOrCreate')->once()->with([
'server_id' => $model->id,
'variable_id' => 2,
], ['variable_value' => ''])->andReturnNull();
$this->eggRepository->shouldReceive('setColumns->find')->once()->with($eggModel->id)->andReturn($eggModel);
$this->repository->shouldReceive('update')->with($model->id, m::subset([
'installed' => 0,
'nest_id' => $eggModel->nest_id,
'egg_id' => $eggModel->id,
'image' => 'docker:image',
]))->once()->andReturn($model);
$this->repository->shouldReceive('getDaemonServiceData')->with($model, true)->once()->andReturn([
'egg' => 'abcd1234',
]);
$this->environmentService->shouldReceive('handle')->with($model)->once()->andReturn(['env']);
$this->daemonServerRepository->shouldReceive('setServer')->with($model)->once()->andReturnSelf();
$this->daemonServerRepository->shouldReceive('update')->with([
'build' => [
'env|overwrite' => ['env'],
'image' => $model->image,
],
'service' => [
'egg' => 'abcd1234',
'skip_scripts' => false,
],
])->once()->andReturn(new Response);
$this->connection->shouldReceive('commit')->withNoArgs()->once()->andReturnNull();
$service = $this->getService();
$service->setUserLevel(User::USER_LEVEL_ADMIN);
$response = $service->handle($model, [
'docker_image' => 'docker:image',
'egg_id' => $eggModel->id,
'environment' => ['test' => 'abcd1234'],
]);
$this->assertInstanceOf(Server::class, $response);
$this->assertSame($model, $response);
}
/**
* 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->eggRepository,
$this->environmentService,
$this->repository,
$this->serverVariableRepository,
$this->validatorService
);
}
}

View file

@ -1,192 +0,0 @@
<?php
/**
* Pterodactyl - Panel
* Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com>.
*
* This software is licensed under the terms of the MIT license.
* https://opensource.org/licenses/MIT
*/
namespace Tests\Unit\Services\Servers;
use Exception;
use Mockery as m;
use Tests\TestCase;
use GuzzleHttp\Psr7\Response;
use Pterodactyl\Models\Server;
use Psr\Log\LoggerInterface as Writer;
use GuzzleHttp\Exception\RequestException;
use Illuminate\Database\ConnectionInterface;
use Pterodactyl\Exceptions\DisplayException;
use Pterodactyl\Services\Servers\SuspensionService;
use Pterodactyl\Contracts\Repository\ServerRepositoryInterface;
use Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface as DaemonServerRepositoryInterface;
class SuspensionServiceTest extends TestCase
{
/**
* @var \Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface
*/
protected $daemonServerRepository;
/**
* @var \Illuminate\Database\ConnectionInterface
*/
protected $database;
/**
* @var \GuzzleHttp\Exception\RequestException
*/
protected $exception;
/**
* @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface
*/
protected $repository;
/**
* @var \Pterodactyl\Models\Server
*/
protected $server;
/**
* @var \Pterodactyl\Services\Servers\SuspensionService
*/
protected $service;
/**
* @var \Psr\Log\LoggerInterface
*/
protected $writer;
/**
* Setup tests.
*/
public function setUp(): void
{
parent::setUp();
$this->daemonServerRepository = m::mock(DaemonServerRepositoryInterface::class);
$this->database = m::mock(ConnectionInterface::class);
$this->exception = m::mock(RequestException::class)->makePartial();
$this->repository = m::mock(ServerRepositoryInterface::class);
$this->writer = m::mock(Writer::class);
$this->server = factory(Server::class)->make(['suspended' => 0, 'node_id' => 1]);
$this->service = new SuspensionService(
$this->database,
$this->daemonServerRepository,
$this->repository,
$this->writer
);
}
/**
* Test that the function accepts an integer in place of the server model.
*
* @expectedException \Exception
*/
public function testFunctionShouldAcceptAnIntegerInPlaceOfAServerModel()
{
$this->repository->shouldReceive('find')->with($this->server->id)->once()->andThrow(new Exception());
$this->service->toggle($this->server->id);
}
/**
* Test that no action being passed suspends a server.
*/
public function testServerShouldBeSuspendedWhenNoActionIsPassed()
{
$this->server->suspended = 0;
$this->database->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull();
$this->repository->shouldReceive('withoutFreshModel')->withNoArgs()->once()->andReturnSelf()
->shouldReceive('update')->with($this->server->id, ['suspended' => true])->once()->andReturnNull();
$this->daemonServerRepository->shouldReceive('setServer')->with($this->server)->once()->andReturnSelf()
->shouldReceive('suspend')->withNoArgs()->once()->andReturn(new Response);
$this->database->shouldReceive('commit')->withNoArgs()->once()->andReturnNull();
$this->assertTrue($this->service->toggle($this->server));
}
/**
* Test that server is unsuspended if action=unsuspend.
*/
public function testServerShouldBeUnsuspendedWhenUnsuspendActionIsPassed()
{
$this->server->suspended = 1;
$this->database->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull();
$this->repository->shouldReceive('withoutFreshModel')->withNoArgs()->once()->andReturnSelf()
->shouldReceive('update')->with($this->server->id, ['suspended' => false])->once()->andReturnNull();
$this->daemonServerRepository->shouldReceive('setServer')->with($this->server)->once()->andReturnSelf()
->shouldReceive('unsuspend')->withNoArgs()->once()->andReturn(new Response);
$this->database->shouldReceive('commit')->withNoArgs()->once()->andReturnNull();
$this->assertTrue($this->service->toggle($this->server, 'unsuspend'));
}
/**
* Test that nothing happens if a server is already unsuspended and action=unsuspend.
*/
public function testNoActionShouldHappenIfServerIsAlreadyUnsuspendedAndActionIsUnsuspend()
{
$this->server->suspended = 0;
$this->assertTrue($this->service->toggle($this->server, 'unsuspend'));
}
/**
* Test that nothing happens if a server is already suspended and action=suspend.
*/
public function testNoActionShouldHappenIfServerIsAlreadySuspendedAndActionIsSuspend()
{
$this->server->suspended = 1;
$this->assertTrue($this->service->toggle($this->server, 'suspend'));
}
/**
* Test that an exception thrown by Guzzle is caught and transformed to a displayable exception.
*/
public function testExceptionThrownByGuzzleShouldBeCaughtAndTransformedToDisplayable()
{
$this->server->suspended = 0;
$this->database->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull();
$this->repository->shouldReceive('withoutFreshModel')->withNoArgs()->once()->andReturnSelf()
->shouldReceive('update')->with($this->server->id, ['suspended' => true])->once()->andReturnNull();
$this->daemonServerRepository->shouldReceive('setServer')->with($this->server)
->once()->andThrow($this->exception);
$this->exception->shouldReceive('getResponse')->withNoArgs()->once()->andReturnSelf()
->shouldReceive('getStatusCode')->withNoArgs()->once()->andReturn(400);
$this->writer->shouldReceive('warning')->with($this->exception)->once()->andReturnNull();
try {
$this->service->toggle($this->server);
} catch (Exception $exception) {
$this->assertInstanceOf(DisplayException::class, $exception);
$this->assertEquals(
trans('admin/server.exceptions.daemon_exception', ['code' => 400]),
$exception->getMessage()
);
}
}
/**
* Test that if action is not suspend or unsuspend an exception is thrown.
*
* @expectedException \InvalidArgumentException
*/
public function testExceptionShouldBeThrownIfActionIsNotValid()
{
$this->service->toggle($this->server, 'random');
}
}

View file

@ -1,175 +0,0 @@
<?php
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 Illuminate\Validation\ValidationException;
use Pterodactyl\Services\Servers\VariableValidatorService;
use Pterodactyl\Contracts\Repository\ServerRepositoryInterface;
use Pterodactyl\Contracts\Repository\EggVariableRepositoryInterface;
use Pterodactyl\Contracts\Repository\ServerVariableRepositoryInterface;
class VariableValidatorServiceTest extends TestCase
{
/**
* @var \Pterodactyl\Contracts\Repository\EggVariableRepositoryInterface|\Mockery\Mock
*/
private $optionVariableRepository;
/**
* @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface|\Mockery\Mock
*/
private $serverRepository;
/**
* @var \Pterodactyl\Contracts\Repository\ServerVariableRepositoryInterface|\Mockery\Mock
*/
private $serverVariableRepository;
/**
* Setup tests.
*/
public function setUp(): void
{
parent::setUp();
$this->optionVariableRepository = m::mock(EggVariableRepositoryInterface::class);
$this->serverRepository = m::mock(ServerRepositoryInterface::class);
$this->serverVariableRepository = m::mock(ServerVariableRepositoryInterface::class);
}
/**
* Test that when no variables are found for an option no data is returned.
*/
public function testEmptyResultSetShouldBeReturnedIfNoVariablesAreFound()
{
$this->optionVariableRepository->shouldReceive('findWhere')->with([['egg_id', '=', 1]])->andReturn(collect([]));
$response = $this->getService()->handle(1, []);
$this->assertEmpty($response);
$this->assertInstanceOf(Collection::class, $response);
}
/**
* Test that variables set as user_editable=0 and/or user_viewable=0 are skipped when admin flag is not set.
*/
public function testValidatorShouldNotProcessVariablesSetAsNotUserEditableWhenAdminFlagIsNotPassed()
{
$variables = $this->getVariableCollection();
$this->optionVariableRepository->shouldReceive('findWhere')->with([['egg_id', '=', 1]])->andReturn($variables);
$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->assertNotEmpty($response);
$this->assertInstanceOf(Collection::class, $response);
$this->assertEquals(1, $response->count(), 'Assert response has a single item in collection.');
$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);
}
/**
* Test that all variables are processed correctly if admin flag is set.
*/
public function testValidatorShouldProcessAllVariablesWhenAdminFlagIsSet()
{
$variables = $this->getVariableCollection();
$this->optionVariableRepository->shouldReceive('findWhere')->with([['egg_id', '=', 1]])->andReturn($variables);
$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->assertNotEmpty($response);
$this->assertInstanceOf(Collection::class, $response);
$this->assertEquals(4, $response->count(), 'Assert response has all four items in collection.');
$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);
});
}
/**
* Test that a DisplayValidationError is thrown when a variable is not validated.
*/
public function testValidatorShouldThrowExceptionWhenAValidationErrorIsEncountered()
{
$variables = $this->getVariableCollection();
$this->optionVariableRepository->shouldReceive('findWhere')->with([['egg_id', '=', 1]])->andReturn($variables);
try {
$this->getService()->handle(1, [$variables[0]->env_variable => null]);
} catch (ValidationException $exception) {
$messages = $exception->validator->getMessageBag()->all();
$this->assertNotEmpty($messages);
$this->assertSame(2, count($messages));
// We only expect to get the first two variables form the getVariableCollection
// function here since those are the only two that are editable, and the others
// should be discarded and not validated.
for ($i = 0; $i < 2; $i++) {
$this->assertSame(trans('validation.required', [
'attribute' => trans('validation.internal.variable_value', ['env' => $variables[$i]->name]),
]), $messages[$i]);
}
}
}
/**
* 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('editable')->make(),
factory(EggVariable::class)->states('viewable')->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->app->make(Factory::class)
);
}
}

View file

@ -1,55 +0,0 @@
<?php
/**
* Pterodactyl - Panel
* Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com>.
*
* This software is licensed under the terms of the MIT license.
* https://opensource.org/licenses/MIT
*/
namespace Tests\Unit\Services\Subusers;
use Mockery as m;
use Tests\TestCase;
use Pterodactyl\Services\Subusers\PermissionCreationService;
use Pterodactyl\Contracts\Repository\PermissionRepositoryInterface;
class PermissionCreationServiceTest extends TestCase
{
/**
* @var \Pterodactyl\Contracts\Repository\PermissionRepositoryInterface|\Mockery\Mock
*/
protected $repository;
/**
* @var \Pterodactyl\Services\Subusers\PermissionCreationService
*/
protected $service;
/**
* Setup tests.
*/
public function setUp(): void
{
parent::setUp();
$this->repository = m::mock(PermissionRepositoryInterface::class);
$this->service = new PermissionCreationService($this->repository);
}
/**
* Test that permissions can be assigned correctly.
*/
public function testPermissionsAreAssignedCorrectly()
{
$permissions = ['access-sftp'];
$this->repository->shouldReceive('withoutFreshModel')->withNoArgs()->once()->andReturnSelf()
->shouldReceive('insert')->with([
['subuser_id' => 1, 'permission' => 'access-sftp'],
])->once()->andReturn(true);
$this->service->handle(1, $permissions);
$this->assertTrue(true);
}
}

View file

@ -1,186 +0,0 @@
<?php
namespace Tests\Unit\Services\Subusers;
use Mockery as m;
use Tests\TestCase;
use Pterodactyl\Models\User;
use Pterodactyl\Models\Server;
use Pterodactyl\Models\Subuser;
use Illuminate\Database\ConnectionInterface;
use Pterodactyl\Exceptions\DisplayException;
use Pterodactyl\Services\Users\UserCreationService;
use Pterodactyl\Services\Subusers\SubuserCreationService;
use Pterodactyl\Contracts\Repository\UserRepositoryInterface;
use Pterodactyl\Exceptions\Repository\RecordNotFoundException;
use Pterodactyl\Contracts\Repository\ServerRepositoryInterface;
use Pterodactyl\Contracts\Repository\SubuserRepositoryInterface;
use Pterodactyl\Exceptions\Service\Subuser\UserIsServerOwnerException;
use Pterodactyl\Exceptions\Service\Subuser\ServerSubuserExistsException;
class SubuserCreationServiceTest extends TestCase
{
/**
* @var \Illuminate\Database\ConnectionInterface|\Mockery\Mock
*/
protected $connection;
/**
* @var \Pterodactyl\Repositories\Eloquent\SubuserRepository|\Mockery\Mock
*/
protected $subuserRepository;
/**
* @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface|\Mockery\Mock
*/
protected $serverRepository;
/**
* @var \Pterodactyl\Services\Subusers\SubuserCreationService
*/
protected $service;
/**
* @var \Pterodactyl\Services\Users\UserCreationService|\Mockery\Mock
*/
protected $userCreationService;
/**
* @var \Pterodactyl\Contracts\Repository\UserRepositoryInterface|\Mockery\Mock
*/
protected $userRepository;
/**
* Setup tests.
*/
public function setUp(): void
{
parent::setUp();
$this->connection = m::mock(ConnectionInterface::class);
$this->subuserRepository = m::mock(SubuserRepositoryInterface::class);
$this->serverRepository = m::mock(ServerRepositoryInterface::class);
$this->userCreationService = m::mock(UserCreationService::class);
$this->userRepository = m::mock(UserRepositoryInterface::class);
}
/**
* Test that a user without an existing account can be added as a subuser.
*/
public function testAccountIsCreatedForNewUser()
{
$permissions = ['test-1' => 'test:1', 'test-2' => null];
$server = factory(Server::class)->make();
$user = factory(User::class)->make([
'email' => 'known.1+test@example.com',
]);
$subuser = factory(Subuser::class)->make(['user_id' => $user->id, 'server_id' => $server->id]);
$this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull();
$this->userRepository->shouldReceive('findFirstWhere')->with([['email', '=', $user->email]])->once()->andThrow(new RecordNotFoundException);
$this->userCreationService->shouldReceive('handle')->with(m::on(function ($data) use ($user) {
$subset = m::subset([
'email' => $user->email,
'name_first' => 'Server',
'name_last' => 'Subuser',
'root_admin' => false,
])->match($data);
$username = substr(array_get($data, 'username', ''), 0, -3) === 'known.1test';
return $subset && $username;
}))->once()->andReturn($user);
$this->subuserRepository->shouldReceive('create')->with(['user_id' => $user->id, 'server_id' => $server->id])
->once()->andReturn($subuser);
$this->connection->shouldReceive('commit')->withNoArgs()->once()->andReturnNull();
$response = $this->getService()->handle($server, $user->email, array_keys($permissions));
$this->assertInstanceOf(Subuser::class, $response);
$this->assertSame($subuser, $response);
}
/**
* Test that an existing user can be added as a subuser.
*/
public function testExistingUserCanBeAddedAsASubuser()
{
$permissions = ['access-sftp'];
$server = factory(Server::class)->make();
$user = factory(User::class)->make();
$subuser = factory(Subuser::class)->make(['user_id' => $user->id, 'server_id' => $server->id]);
$this->serverRepository->shouldReceive('find')->with($server->id)->once()->andReturn($server);
$this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull();
$this->userRepository->shouldReceive('findFirstWhere')->with([['email', '=', $user->email]])->once()->andReturn($user);
$this->subuserRepository->shouldReceive('findCountWhere')->with([
['user_id', '=', $user->id],
['server_id', '=', $server->id],
])->once()->andReturn(0);
$this->subuserRepository->shouldReceive('create')->with(['user_id' => $user->id, 'server_id' => $server->id])
->once()->andReturn($subuser);
$this->connection->shouldReceive('commit')->withNoArgs()->once()->andReturnNull();
$response = $this->getService()->handle($server->id, $user->email, $permissions);
$this->assertInstanceOf(Subuser::class, $response);
$this->assertSame($subuser, $response);
}
/**
* Test that an exception gets thrown if the subuser is actually the server owner.
*/
public function testExceptionIsThrownIfUserIsServerOwner()
{
$user = factory(User::class)->make();
$server = factory(Server::class)->make(['owner_id' => $user->id]);
$this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull();
$this->userRepository->shouldReceive('findFirstWhere')->with([['email', '=', $user->email]])->once()->andReturn($user);
try {
$this->getService()->handle($server, $user->email, []);
} catch (DisplayException $exception) {
$this->assertInstanceOf(UserIsServerOwnerException::class, $exception);
$this->assertEquals(trans('exceptions.subusers.user_is_owner'), $exception->getMessage());
}
}
/**
* Test that an exception is thrown if the user is already added as a subuser.
*/
public function testExceptionIsThrownIfUserIsAlreadyASubuser()
{
$user = factory(User::class)->make();
$server = factory(Server::class)->make();
$this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull();
$this->userRepository->shouldReceive('findFirstWhere')->with([['email', '=', $user->email]])->once()->andReturn($user);
$this->subuserRepository->shouldReceive('findCountWhere')->with([
['user_id', '=', $user->id],
['server_id', '=', $server->id],
])->once()->andReturn(1);
try {
$this->getService()->handle($server, $user->email, []);
} catch (DisplayException $exception) {
$this->assertInstanceOf(ServerSubuserExistsException::class, $exception);
$this->assertEquals(trans('exceptions.subusers.subuser_exists'), $exception->getMessage());
}
}
/**
* Return an instance of the service with mocked dependencies.
*
* @return \Pterodactyl\Services\Subusers\SubuserCreationService
*/
private function getService(): SubuserCreationService
{
return new SubuserCreationService(
$this->connection,
$this->subuserRepository,
$this->userCreationService,
$this->userRepository
);
}
}

View file

@ -1,128 +0,0 @@
<?php
namespace Tests\Unit\Services\Users;
use Mockery as m;
use Carbon\Carbon;
use Tests\TestCase;
use Pterodactyl\Models\User;
use PragmaRX\Google2FA\Google2FA;
use Illuminate\Contracts\Config\Repository;
use Illuminate\Contracts\Encryption\Encrypter;
use Pterodactyl\Services\Users\ToggleTwoFactorService;
use Pterodactyl\Contracts\Repository\UserRepositoryInterface;
class ToggleTwoFactorServiceTest extends TestCase
{
const TEST_WINDOW_INT = 4;
const USER_TOTP_SECRET = 'encryptedValue';
const DECRYPTED_USER_SECRET = 'decryptedValue';
/**
* @var \Illuminate\Contracts\Config\Repository|\Mockery\Mock
*/
private $config;
/**
* @var \Illuminate\Contracts\Encryption\Encrypter|\Mockery\Mock
*/
private $encrypter;
/**
* @var \PragmaRX\Google2FA\Google2FA|\Mockery\Mock
*/
private $google2FA;
/**
* @var \Pterodactyl\Contracts\Repository\UserRepositoryInterface|\Mockery\Mock
*/
private $repository;
/**
* Setup tests.
*/
public function setUp(): void
{
parent::setUp();
Carbon::setTestNow(Carbon::now());
$this->config = m::mock(Repository::class);
$this->encrypter = m::mock(Encrypter::class);
$this->google2FA = m::mock(Google2FA::class);
$this->repository = m::mock(UserRepositoryInterface::class);
$this->config->shouldReceive('get')->with('pterodactyl.auth.2fa.window')->once()->andReturn(self::TEST_WINDOW_INT);
$this->encrypter->shouldReceive('decrypt')->with(self::USER_TOTP_SECRET)->once()->andReturn(self::DECRYPTED_USER_SECRET);
}
/**
* Test that 2FA can be enabled for a user.
*/
public function testTwoFactorIsEnabledForUser()
{
$model = factory(User::class)->make(['totp_secret' => self::USER_TOTP_SECRET, 'use_totp' => false]);
$this->google2FA->shouldReceive('verifyKey')->with(self::DECRYPTED_USER_SECRET, 'test-token', self::TEST_WINDOW_INT)->once()->andReturn(true);
$this->repository->shouldReceive('withoutFreshModel->update')->with($model->id, [
'totp_authenticated_at' => Carbon::now(),
'use_totp' => true,
])->once()->andReturnNull();
$this->assertTrue($this->getService()->handle($model, 'test-token'));
}
/**
* Test that 2FA can be disabled for a user.
*/
public function testTwoFactorIsDisabled()
{
$model = factory(User::class)->make(['totp_secret' => self::USER_TOTP_SECRET, 'use_totp' => true]);
$this->google2FA->shouldReceive('verifyKey')->with(self::DECRYPTED_USER_SECRET, 'test-token', self::TEST_WINDOW_INT)->once()->andReturn(true);
$this->repository->shouldReceive('withoutFreshModel->update')->with($model->id, [
'totp_authenticated_at' => Carbon::now(),
'use_totp' => false,
])->once()->andReturnNull();
$this->assertTrue($this->getService()->handle($model, 'test-token'));
}
/**
* Test that 2FA will remain disabled for a user.
*/
public function testTwoFactorRemainsDisabledForUser()
{
$model = factory(User::class)->make(['totp_secret' => self::USER_TOTP_SECRET, 'use_totp' => false]);
$this->google2FA->shouldReceive('verifyKey')->with(self::DECRYPTED_USER_SECRET, 'test-token', self::TEST_WINDOW_INT)->once()->andReturn(true);
$this->repository->shouldReceive('withoutFreshModel->update')->with($model->id, [
'totp_authenticated_at' => Carbon::now(),
'use_totp' => false,
])->once()->andReturnNull();
$this->assertTrue($this->getService()->handle($model, 'test-token', false));
}
/**
* Test that an exception is thrown if the token provided is invalid.
*
* @expectedException \Pterodactyl\Exceptions\Service\User\TwoFactorAuthenticationTokenInvalid
*/
public function testExceptionIsThrownIfTokenIsInvalid()
{
$model = factory(User::class)->make(['totp_secret' => self::USER_TOTP_SECRET]);
$this->google2FA->shouldReceive('verifyKey')->once()->andReturn(false);
$this->getService()->handle($model, 'test-token');
}
/**
* Return an instance of the service with mocked dependencies.
*
* @return \Pterodactyl\Services\Users\ToggleTwoFactorService
*/
private function getService(): ToggleTwoFactorService
{
return new ToggleTwoFactorService($this->encrypter, $this->google2FA, $this->config, $this->repository);
}
}

View file

@ -1,81 +0,0 @@
<?php
namespace Tests\Unit\Services\Users;
use Mockery as m;
use Tests\TestCase;
use Pterodactyl\Models\User;
use Illuminate\Contracts\Config\Repository;
use Illuminate\Contracts\Encryption\Encrypter;
use Pterodactyl\Services\Users\TwoFactorSetupService;
use Pterodactyl\Contracts\Repository\UserRepositoryInterface;
class TwoFactorSetupServiceTest extends TestCase
{
/**
* @var \Illuminate\Contracts\Config\Repository|\Mockery\Mock
*/
private $config;
/**
* @var \Illuminate\Contracts\Encryption\Encrypter|\Mockery\Mock
*/
private $encrypter;
/**
* @var \Pterodactyl\Contracts\Repository\UserRepositoryInterface|\Mockery\Mock
*/
private $repository;
/**
* Setup tests.
*/
public function setUp(): void
{
parent::setUp();
$this->config = m::mock(Repository::class);
$this->encrypter = m::mock(Encrypter::class);
$this->repository = m::mock(UserRepositoryInterface::class);
}
/**
* Test that the correct data is returned.
*/
public function testSecretAndImageAreReturned()
{
$model = factory(User::class)->make();
$this->config->shouldReceive('get')->with('pterodactyl.auth.2fa.bytes', 16)->andReturn(32);
$this->config->shouldReceive('get')->with('app.name')->andReturn('Company Name');
$this->encrypter->shouldReceive('encrypt')
->with(m::on(function ($value) {
return preg_match('/([A-Z234567]{32})/', $value) !== false;
}))
->once()
->andReturn('encryptedSecret');
$this->repository->shouldReceive('withoutFreshModel->update')->with($model->id, ['totp_secret' => 'encryptedSecret'])->once()->andReturnNull();
$response = $this->getService()->handle($model);
$this->assertNotEmpty($response);
$companyName = preg_quote(rawurlencode('CompanyName'));
$email = preg_quote(rawurlencode($model->email));
$this->assertRegExp(
'/otpauth:\/\/totp\/' . $companyName . ':' . $email . '\?secret=([A-Z234567]{32})&issuer=' . $companyName . '/',
$response
);
}
/**
* Return an instance of the service to test with mocked dependencies.
*
* @return \Pterodactyl\Services\Users\TwoFactorSetupService
*/
private function getService(): TwoFactorSetupService
{
return new TwoFactorSetupService($this->config, $this->encrypter, $this->repository);
}
}

View file

@ -1,157 +0,0 @@
<?php
namespace Tests\Unit\Services;
use Mockery as m;
use Tests\TestCase;
use Pterodactyl\Models\User;
use Tests\Traits\MocksUuids;
use Illuminate\Contracts\Hashing\Hasher;
use Illuminate\Database\ConnectionInterface;
use Illuminate\Support\Facades\Notification;
use Illuminate\Contracts\Auth\PasswordBroker;
use Pterodactyl\Notifications\AccountCreated;
use Pterodactyl\Services\Users\UserCreationService;
use Pterodactyl\Contracts\Repository\UserRepositoryInterface;
class UserCreationServiceTest extends TestCase
{
use MocksUuids;
/**
* @var \Illuminate\Database\ConnectionInterface|\Mockery\Mock
*/
private $connection;
/**
* @var \Illuminate\Contracts\Hashing\Hasher|\Mockery\Mock
*/
private $hasher;
/**
* @var \Illuminate\Contracts\Auth\PasswordBroker|\Mockery\Mock
*/
private $passwordBroker;
/**
* @var \Pterodactyl\Contracts\Repository\UserRepositoryInterface|\Mockery\Mock
*/
private $repository;
/**
* Setup tests.
*/
public function setUp(): void
{
parent::setUp();
Notification::fake();
$this->connection = m::mock(ConnectionInterface::class);
$this->hasher = m::mock(Hasher::class);
$this->passwordBroker = m::mock(PasswordBroker::class);
$this->repository = m::mock(UserRepositoryInterface::class);
}
/**
* Test that a user is created when a password is passed.
*/
public function testUserIsCreatedWhenPasswordIsProvided()
{
$user = factory(User::class)->make();
$this->hasher->shouldReceive('make')->with('raw-password')->once()->andReturn('enc-password');
$this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull();
$this->repository->shouldReceive('create')->with([
'password' => 'enc-password',
'uuid' => $this->getKnownUuid(),
], true, true)->once()->andReturn($user);
$this->connection->shouldReceive('commit')->withNoArgs()->once()->andReturnNull();
$response = $this->getService()->handle([
'password' => 'raw-password',
]);
$this->assertNotNull($response);
Notification::assertSentTo($user, AccountCreated::class, function ($notification) use ($user) {
$this->assertSame($user, $notification->user);
$this->assertNull($notification->token);
return true;
});
}
/**
* Test that a UUID passed in the submission data is not used when
* creating the user.
*/
public function testUuidPassedInDataIsIgnored()
{
$user = factory(User::class)->make();
$this->hasher->shouldReceive('make')->andReturn('enc-password');
$this->connection->shouldReceive('beginTransaction')->andReturnNull();
$this->repository->shouldReceive('create')->with([
'password' => 'enc-password',
'uuid' => $this->getKnownUuid(),
], true, true)->once()->andReturn($user);
$this->connection->shouldReceive('commit')->andReturnNull();
$response = $this->getService()->handle([
'password' => 'raw-password',
'uuid' => 'test-uuid',
]);
$this->assertNotNull($response);
$this->assertInstanceOf(User::class, $response);
Notification::assertSentTo($user, AccountCreated::class, function ($notification) use ($user) {
$this->assertSame($user, $notification->user);
$this->assertNull($notification->token);
return true;
});
}
/**
* Test that a user is created with a random password when no password is provided.
*/
public function testUserIsCreatedWhenNoPasswordIsProvided()
{
$user = factory(User::class)->make();
$this->hasher->shouldNotReceive('make');
$this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull();
$this->hasher->shouldReceive('make')->once()->andReturn('created-enc-password');
$this->passwordBroker->shouldReceive('createToken')->with($user)->once()->andReturn('random-token');
$this->repository->shouldReceive('create')->with([
'password' => 'created-enc-password',
'email' => $user->email,
'uuid' => $this->getKnownUuid(),
], true, true)->once()->andReturn($user);
$this->connection->shouldReceive('commit')->withNoArgs()->once()->andReturnNull();
$response = $this->getService()->handle([
'email' => $user->email,
]);
$this->assertNotNull($response);
$this->assertInstanceOf(User::class, $response);
Notification::assertSentTo($user, AccountCreated::class, function ($notification) use ($user) {
$this->assertSame($user, $notification->user);
$this->assertSame('random-token', $notification->token);
return true;
});
}
/**
* Return a new instance of the service using mocked dependencies.
*
* @return \Pterodactyl\Services\Users\UserCreationService
*/
private function getService(): UserCreationService
{
return new UserCreationService($this->connection, $this->hasher, $this->passwordBroker, $this->repository);
}
}

View file

@ -1,103 +0,0 @@
<?php
/**
* Pterodactyl - Panel
* Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com>.
*
* This software is licensed under the terms of the MIT license.
* https://opensource.org/licenses/MIT
*/
namespace Tests\Unit\Services\Users;
use Mockery as m;
use Tests\TestCase;
use Pterodactyl\Models\User;
use Illuminate\Contracts\Translation\Translator;
use Pterodactyl\Services\Users\UserDeletionService;
use Pterodactyl\Contracts\Repository\UserRepositoryInterface;
use Pterodactyl\Contracts\Repository\ServerRepositoryInterface;
class UserDeletionServiceTest extends TestCase
{
/**
* @var \Pterodactyl\Contracts\Repository\UserRepositoryInterface
*/
protected $repository;
/**
* @var \Illuminate\Contracts\Translation\Translator
*/
protected $translator;
/**
* @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface
*/
protected $serverRepository;
/**
* @var \Pterodactyl\Services\Users\UserDeletionService
*/
protected $service;
/**
* @var User
*/
protected $user;
/**
* Setup tests.
*/
public function setUp(): void
{
parent::setUp();
$this->user = factory(User::class)->make();
$this->repository = m::mock(UserRepositoryInterface::class);
$this->translator = m::mock(Translator::class);
$this->serverRepository = m::mock(ServerRepositoryInterface::class);
$this->service = new UserDeletionService(
$this->serverRepository,
$this->translator,
$this->repository
);
}
/**
* Test that a user is deleted if they have no servers.
*/
public function testUserIsDeletedIfNoServersAreAttachedToAccount()
{
$this->serverRepository->shouldReceive('setColumns')->with('id')->once()->andReturnSelf()
->shouldReceive('findCountWhere')->with([['owner_id', '=', $this->user->id]])->once()->andReturn(0);
$this->repository->shouldReceive('delete')->with($this->user->id)->once()->andReturn(1);
$this->assertEquals(1, $this->service->handle($this->user->id));
}
/**
* Test that an exception is thrown if trying to delete a user with servers.
*
* @expectedException \Pterodactyl\Exceptions\DisplayException
*/
public function testExceptionIsThrownIfServersAreAttachedToAccount()
{
$this->serverRepository->shouldReceive('setColumns')->with('id')->once()->andReturnSelf()
->shouldReceive('findCountWhere')->with([['owner_id', '=', $this->user->id]])->once()->andReturn(1);
$this->translator->shouldReceive('trans')->with('admin/user.exceptions.user_has_servers')->once()->andReturnNull();
$this->service->handle($this->user->id);
}
/**
* Test that the function supports passing in a model or an ID.
*/
public function testModelCanBePassedInPlaceOfUserId()
{
$this->serverRepository->shouldReceive('setColumns')->with('id')->once()->andReturnSelf()
->shouldReceive('findCountWhere')->with([['owner_id', '=', $this->user->id]])->once()->andReturn(0);
$this->repository->shouldReceive('delete')->with($this->user->id)->once()->andReturn(1);
$this->assertEquals(1, $this->service->handle($this->user));
}
}

View file

@ -1,126 +0,0 @@
<?php
namespace Tests\Unit\Services\Users;
use Mockery as m;
use Tests\TestCase;
use Pterodactyl\Models\User;
use Illuminate\Support\Collection;
use Illuminate\Contracts\Hashing\Hasher;
use Pterodactyl\Services\Users\UserUpdateService;
use Pterodactyl\Contracts\Repository\UserRepositoryInterface;
class UserUpdateServiceTest extends TestCase
{
/**
* @var \Illuminate\Contracts\Hashing\Hasher|\Mockery\Mock
*/
private $hasher;
/**
* @var \Pterodactyl\Repositories\Eloquent\UserRepository|\Mockery\Mock
*/
private $repository;
/**
* Setup tests.
*/
public function setUp(): void
{
parent::setUp();
$this->hasher = m::mock(Hasher::class);
$this->repository = m::mock(UserRepositoryInterface::class);
}
/**
* Test that the handle function does not attempt to hash a password if no
* password is provided or the password is null.
*
* @dataProvider badPasswordDataProvider
*/
public function testUpdateUserWithoutTouchingHasherIfNoPasswordPassed(array $data)
{
$user = factory(User::class)->make();
$this->repository->shouldReceive('update')->with($user->id, ['test-data' => 'value'])->once()->andReturnNull();
$response = $this->getService()->handle($user, $data);
$this->assertInstanceOf(Collection::class, $response);
$this->assertTrue($response->has('model'));
$this->assertTrue($response->has('exceptions'));
}
/**
* Provide a test data set with passwords that should not be hashed.
*
* @return array
*/
public function badPasswordDataProvider(): array
{
return [
[['test-data' => 'value']],
[['test-data' => 'value', 'password' => null]],
[['test-data' => 'value', 'password' => '']],
[['test-data' => 'value', 'password' => 0]],
];
}
/**
* Test that the handle function hashes a password if passed in the data array.
*/
public function testUpdateUserAndHashPasswordIfProvided()
{
$user = factory(User::class)->make();
$this->hasher->shouldReceive('make')->with('raw_pass')->once()->andReturn('enc_pass');
$this->repository->shouldReceive('update')->with($user->id, ['password' => 'enc_pass'])->once()->andReturnNull();
$response = $this->getService()->handle($user, ['password' => 'raw_pass']);
$this->assertInstanceOf(Collection::class, $response);
$this->assertTrue($response->has('model'));
$this->assertTrue($response->has('exceptions'));
}
/**
* Test that an admin can revoke a user's administrative status.
*/
public function testAdministrativeUserRevokingAdminStatus()
{
$user = factory(User::class)->make(['root_admin' => true]);
$service = $this->getService();
$service->setUserLevel(User::USER_LEVEL_ADMIN);
$this->repository->shouldReceive('update')->with($user->id, ['root_admin' => false])->once()->andReturnNull();
$response = $service->handle($user, ['root_admin' => false]);
$this->assertInstanceOf(Collection::class, $response);
$this->assertTrue($response->has('model'));
$this->assertTrue($response->has('exceptions'));
}
/**
* Test that a normal user is unable to set an administrative status for themselves.
*/
public function testNormalUserShouldNotRevokeAdminStatus()
{
$user = factory(User::class)->make(['root_admin' => false]);
$service = $this->getService();
$service->setUserLevel(User::USER_LEVEL_USER);
$this->repository->shouldReceive('update')->with($user->id, [])->once()->andReturnNull();
$response = $service->handle($user, ['root_admin' => true]);
$this->assertInstanceOf(Collection::class, $response);
$this->assertTrue($response->has('model'));
$this->assertTrue($response->has('exceptions'));
}
/**
* Return an instance of the service for testing.
*
* @return \Pterodactyl\Services\Users\UserUpdateService
*/
private function getService(): UserUpdateService
{
return new UserUpdateService($this->hasher, $this->repository);
}
}