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;
/**
* 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.
*

View File

@ -54,15 +54,4 @@ interface NodeRepositoryInterface extends RepositoryInterface
* @return \Illuminate\Support\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)
{
/** @var \Pterodactyl\Models\Mount $mount */
$mount = Mount::query()->create(array_merge($request->validated(), [
'uuid' => Uuid::uuid4()->toString(),
]));
$model = (new Mount())->fill($request->validated());
$model->forceFill(['uuid' => Uuid::uuid4()->toString()]);
$model->saveOrFail();
$mount = $model->fresh();
$this->alert->success('Mount was created successfully.')->flash();

View File

@ -1,15 +1,9 @@
<?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;
use Illuminate\View\View;
use Pterodactyl\Models\Egg;
use Illuminate\Http\RedirectResponse;
use Prologue\Alerts\AlertsMessageBag;
use Pterodactyl\Http\Controllers\Controller;
@ -81,14 +75,14 @@ class EggScriptController extends Controller
* Handle a request to update the installation script for an Egg.
*
* @param \Pterodactyl\Http\Requests\Admin\Egg\EggScriptFormRequest $request
* @param int $egg
* @param \Pterodactyl\Models\Egg $egg
* @return \Illuminate\Http\RedirectResponse
*
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
* @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->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.
*
* @param \Pterodactyl\Http\Requests\Admin\Egg\EggImportFormRequest $request
* @param int $egg
* @param \Pterodactyl\Models\Egg $egg
* @return \Illuminate\Http\RedirectResponse
*
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
@ -110,7 +110,7 @@ class EggShareController extends Controller
* @throws \Pterodactyl\Exceptions\Service\Egg\BadJsonFormatException
* @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->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)
{
$this->reinstallService->reinstall($server);
$this->reinstallService->handle($server);
$this->alert->success(trans('admin/server.alerts.server_reinstalled'))->flash();
return redirect()->route('admin.servers.view.manage', $server->id);
@ -362,7 +362,7 @@ class ServersController extends Controller
public function newDatabase(StoreServerDatabaseRequest $request, Server $server)
{
$this->databaseManagementService->create($server, [
'database' => $request->input('database'),
'database' => DatabaseManagementService::generateUniqueDatabaseName($request->input('database'), $server->id),
'remote' => $request->input('remote'),
'database_host_id' => $request->input('database_host_id'),
'max_connections' => $request->input('max_connections'),
@ -409,7 +409,7 @@ class ServersController extends Controller
['id', '=', $database],
]);
$this->databaseManagementService->delete($database->id);
$this->databaseManagementService->delete($database);
return response('', 204);
}

View File

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

View File

@ -110,7 +110,9 @@ class DatabaseController extends ApplicationApiController
*/
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)
->transformWith($this->getTransformer(ServerDatabaseTransformer::class))
@ -133,7 +135,7 @@ class DatabaseController extends ApplicationApiController
*/
public function delete(ServerDatabaseWriteRequest $request): Response
{
$this->databaseManagementService->delete($request->getModel(Database::class)->id);
$this->databaseManagementService->delete($request->getModel(Database::class));
return response('', 204);
}

View File

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

View File

@ -129,7 +129,7 @@ class DatabaseController extends ClientApiController
*/
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);
}

View File

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

View File

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

View File

@ -2,9 +2,12 @@
namespace Pterodactyl\Http\Requests\Api\Application\Servers\Databases;
use Webmozart\Assert\Assert;
use Pterodactyl\Models\Server;
use Illuminate\Validation\Rule;
use Illuminate\Database\Query\Builder;
use Pterodactyl\Services\Acl\Api\AdminAcl;
use Pterodactyl\Services\Databases\DatabaseManagementService;
use Pterodactyl\Http\Requests\Api\Application\ApplicationApiRequest;
class StoreServerDatabaseRequest extends ApplicationApiRequest
@ -26,14 +29,16 @@ class StoreServerDatabaseRequest extends ApplicationApiRequest
*/
public function rules(): array
{
$server = $this->route()->parameter('server');
return [
'database' => [
'required',
'string',
'alpha_dash',
'min:1',
'max:24',
Rule::unique('databases')->where(function (Builder $query) {
$query->where('database_host_id', $this->input('host') ?? 0);
'max:48',
Rule::unique('databases')->where(function (Builder $query) use ($server) {
$query->where('server_id', $server->id)->where('database', $this->databaseName());
}),
],
'remote' => 'required|string|regex:/^[0-9%.]{1,15}$/',
@ -68,4 +73,18 @@ class StoreServerDatabaseRequest extends ApplicationApiRequest
'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;
use Pterodactyl\Models\ApiKey;
use Pterodactyl\Http\Requests\Api\Client\ClientApiRequest;
class StoreApiKeyRequest extends ClientApiRequest
@ -11,9 +12,11 @@ class StoreApiKeyRequest extends ClientApiRequest
*/
public function rules(): array
{
$rules = ApiKey::getRules();
return [
'description' => 'required|string|min:4',
'allowed_ips' => 'array',
'description' => $rules['memo'],
'allowed_ips' => $rules['allowed_ips'],
'allowed_ips.*' => 'ip',
];
}

View File

@ -2,9 +2,14 @@
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 Illuminate\Database\Query\Builder;
use Pterodactyl\Contracts\Http\ClientPermissionsRequest;
use Pterodactyl\Http\Requests\Api\Client\ClientApiRequest;
use Pterodactyl\Services\Databases\DatabaseManagementService;
class StoreDatabaseRequest extends ClientApiRequest implements ClientPermissionsRequest
{
@ -21,9 +26,35 @@ class StoreDatabaseRequest extends ClientApiRequest implements ClientPermissions
*/
public function rules(): array
{
$server = $this->route()->parameter('server');
Assert::isInstanceOf($server, Server::class);
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}$/',
];
}
/**
* @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 [
'key' => 'required|string',
'value' => 'present|string',
'value' => 'present',
];
}
}

View File

@ -3,10 +3,10 @@
namespace Pterodactyl\Jobs\Schedule;
use Exception;
use Carbon\Carbon;
use Pterodactyl\Jobs\Job;
use Carbon\CarbonImmutable;
use Pterodactyl\Models\Task;
use InvalidArgumentException;
use Illuminate\Container\Container;
use Illuminate\Queue\SerializesModels;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;
@ -15,39 +15,25 @@ use Pterodactyl\Repositories\Eloquent\TaskRepository;
use Pterodactyl\Services\Backups\InitiateBackupService;
use Pterodactyl\Repositories\Wings\DaemonPowerRepository;
use Pterodactyl\Repositories\Wings\DaemonCommandRepository;
use Pterodactyl\Contracts\Repository\TaskRepositoryInterface;
use Pterodactyl\Contracts\Repository\ScheduleRepositoryInterface;
class RunTaskJob extends Job implements ShouldQueue
{
use DispatchesJobs, InteractsWithQueue, SerializesModels;
/**
* @var int
*/
public $schedule;
/**
* @var int
* @var \Pterodactyl\Models\Task
*/
public $task;
/**
* @var \Pterodactyl\Repositories\Eloquent\TaskRepository
*/
protected $taskRepository;
/**
* RunTaskJob constructor.
*
* @param int $task
* @param int $schedule
* @param \Pterodactyl\Models\Task $task
*/
public function __construct(int $task, int $schedule)
public function __construct(Task $task)
{
$this->queue = config('pterodactyl.queues.standard');
$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\Eloquent\TaskRepository $taskRepository
*
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
* @throws \Throwable
*/
public function handle(
@ -67,36 +52,32 @@ class RunTaskJob extends Job implements ShouldQueue
DaemonPowerRepository $powerRepository,
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.
if (! $task->getRelation('schedule')->is_active) {
if (! $this->task->schedule->is_active) {
$this->markTaskNotQueued();
$this->markScheduleComplete();
return;
}
$server = $this->task->server;
// Perform the provided task against the daemon.
switch ($task->action) {
switch ($this->task->action) {
case 'power':
$powerRepository->setServer($server)->send($task->payload);
$powerRepository->setServer($server)->send($this->task->payload);
break;
case 'command':
$commandRepository->setServer($server)->send($task->payload);
$commandRepository->setServer($server)->send($this->task->payload);
break;
case 'backup':
$backupService->setIgnoredFiles(explode(PHP_EOL, $task->payload))->handle($server, null);
$backupService->setIgnoredFiles(explode(PHP_EOL, $this->task->payload))->handle($server, null);
break;
default:
throw new InvalidArgumentException('Cannot run a task that points to a non-existent action.');
}
$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.
*
* @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)) {
$this->markScheduleComplete();
return;
}
$this->taskRepository->update($nextTask->id, ['is_queued' => true]);
$this->dispatch((new self($nextTask->id, $this->schedule))->delay($nextTask->time_offset));
$nextTask->update(['is_queued' => true]);
$this->dispatch((new self($nextTask))->delay($nextTask->time_offset));
}
/**
@ -136,13 +117,10 @@ class RunTaskJob extends Job implements ShouldQueue
*/
private function markScheduleComplete()
{
Container::getInstance()
->make(ScheduleRepositoryInterface::class)
->withoutFreshModel()
->update($this->schedule, [
'is_processing' => false,
'last_run_at' => Carbon::now()->toDateTimeString(),
]);
$this->task->schedule()->update([
'is_processing' => false,
'last_run_at' => CarbonImmutable::now()->toDateTimeString(),
]);
}
/**
@ -150,8 +128,6 @@ class RunTaskJob extends Job implements ShouldQueue
*/
private function markTaskNotQueued()
{
Container::getInstance()
->make(TaskRepositoryInterface::class)
->update($this->task, ['is_queued' => false]);
$this->task->update(['is_queued' => false]);
}
}

View File

@ -7,6 +7,7 @@ use Illuminate\Validation\Rule;
use Illuminate\Container\Container;
use Illuminate\Contracts\Validation\Factory;
use Illuminate\Database\Eloquent\Model as IlluminateModel;
use Pterodactyl\Exceptions\Model\DataValidationException;
abstract class Model extends IlluminateModel
{
@ -55,7 +56,11 @@ abstract class Model extends IlluminateModel
static::$validatorFactory = Container::getInstance()->make(Factory::class);
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(
// 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
// properly be validated.
// 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
// properly be validated.
$this->addCastAttributesToArray(
$this->getAttributes(), $this->getMutatedAttributes()
)

View File

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

View File

@ -93,31 +93,6 @@ class DatabaseRepository extends EloquentRepository implements DatabaseRepositor
->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.
*

View File

@ -171,28 +171,4 @@ class NodeRepository extends EloquentRepository implements NodeRepositoryInterfa
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;
use Exception;
use InvalidArgumentException;
use Pterodactyl\Models\Server;
use Pterodactyl\Models\Database;
use Pterodactyl\Helpers\Utilities;
use Illuminate\Database\ConnectionInterface;
use Symfony\Component\VarDumper\Cloner\Data;
use Illuminate\Contracts\Encryption\Encrypter;
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\DatabaseClientFeatureNotEnabledException;
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
*/
@ -31,7 +42,7 @@ class DatabaseManagementService
private $encrypter;
/**
* @var \Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface
* @var \Pterodactyl\Repositories\Eloquent\DatabaseRepository
*/
private $repository;
@ -50,13 +61,13 @@ class DatabaseManagementService
*
* @param \Illuminate\Database\ConnectionInterface $connection
* @param \Pterodactyl\Extensions\DynamicDatabaseConnection $dynamic
* @param \Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface $repository
* @param \Pterodactyl\Repositories\Eloquent\DatabaseRepository $repository
* @param \Illuminate\Contracts\Encryption\Encrypter $encrypter
*/
public function __construct(
ConnectionInterface $connection,
DynamicDatabaseConnection $dynamic,
DatabaseRepositoryInterface $repository,
DatabaseRepository $repository,
Encrypter $encrypter
) {
$this->connection = $connection;
@ -65,6 +76,21 @@ class DatabaseManagementService
$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
* 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.
$truncatedName = substr($data['database'], 0, 48 - strlen("s{$server->id}_"));
// Protect against developer mistakes...
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, [
'server_id' => $server->id,
'database' => $truncatedName,
'username' => sprintf('u%d_%s', $server->id, str_random(10)),
'password' => $this->encrypter->encrypt(
Utilities::randomStringWithSpecialCharacters(24)
@ -120,7 +149,8 @@ class DatabaseManagementService
try {
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->repository->createDatabase($database->database);
@ -139,7 +169,7 @@ class DatabaseManagementService
$this->repository->dropUser($database->username, $database->remote);
$this->repository->flush();
}
} catch (Exception $exception) {
} catch (Exception $deletionException) {
// Do nothing here. We've already encountered an issue before this point so no
// reason to prioritize this error over the initial one.
}
@ -151,20 +181,48 @@ class DatabaseManagementService
/**
* Delete a database from the given host server.
*
* @param int $id
* @param \Pterodactyl\Models\Database $database
* @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->repository->dropDatabase($database->database);
$this->repository->dropUser($database->username, $database->remote);
$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;
use Webmozart\Assert\Assert;
use Pterodactyl\Models\Server;
use Pterodactyl\Models\Database;
use Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface;
use Pterodactyl\Contracts\Repository\DatabaseHostRepositoryInterface;
use Pterodactyl\Models\DatabaseHost;
use Pterodactyl\Exceptions\Service\Database\NoSuitableDatabaseHostException;
class DeployServerDatabaseService
{
/**
* @var \Pterodactyl\Contracts\Repository\DatabaseHostRepositoryInterface
*/
private $databaseHostRepository;
/**
* @var \Pterodactyl\Services\Databases\DatabaseManagementService
*/
private $managementService;
/**
* @var \Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface
*/
private $repository;
/**
* ServerDatabaseCreationService constructor.
*
* @param \Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface $repository
* @param \Pterodactyl\Contracts\Repository\DatabaseHostRepositoryInterface $databaseHostRepository
* @param \Pterodactyl\Services\Databases\DatabaseManagementService $managementService
*/
public function __construct(
DatabaseRepositoryInterface $repository,
DatabaseHostRepositoryInterface $databaseHostRepository,
DatabaseManagementService $managementService
) {
$this->databaseHostRepository = $databaseHostRepository;
public function __construct(DatabaseManagementService $managementService)
{
$this->managementService = $managementService;
$this->repository = $repository;
}
/**
@ -53,28 +36,26 @@ class DeployServerDatabaseService
*/
public function handle(Server $server, array $data): Database
{
$allowRandom = config('pterodactyl.client_features.databases.allow_random');
$hosts = $this->databaseHostRepository->setColumns(['id'])->findWhere([
['node_id', '=', $server->node_id],
]);
if ($hosts->isEmpty() && ! $allowRandom) {
throw new NoSuitableDatabaseHostException;
}
Assert::notEmpty($data['database'] ?? null);
Assert::notEmpty($data['remote'] ?? null);
$hosts = DatabaseHost::query()->get()->toBase();
if ($hosts->isEmpty()) {
$hosts = $this->databaseHostRepository->setColumns(['id'])->all();
if ($hosts->isEmpty()) {
throw new NoSuitableDatabaseHostException;
} else {
$nodeHosts = $hosts->where('node_id', $server->node_id)->toBase();
if ($nodeHosts->isEmpty() && ! config('pterodactyl.client_features.databases.allow_random')) {
throw new NoSuitableDatabaseHostException;
}
}
$host = $hosts->random();
return $this->managementService->create($server, [
'database_host_id' => $host->id,
'database' => array_get($data, 'database'),
'remote' => array_get($data, 'remote'),
'database_host_id' => $nodeHosts->isEmpty()
? $hosts->random()->id
: $nodeHosts->random()->id,
'database' => DatabaseManagementService::generateUniqueDatabaseName($data['database'], $server->id),
'remote' => $data['remote'],
]);
}
}

View File

@ -3,16 +3,12 @@
namespace Pterodactyl\Services\Deployment;
use Webmozart\Assert\Assert;
use Pterodactyl\Contracts\Repository\NodeRepositoryInterface;
use Pterodactyl\Models\Node;
use Illuminate\Support\LazyCollection;
use Pterodactyl\Exceptions\Service\Deployment\NoViableNodeException;
class FindViableNodesService
{
/**
* @var \Pterodactyl\Contracts\Repository\NodeRepositoryInterface
*/
private $repository;
/**
* @var array
*/
@ -28,16 +24,6 @@ class FindViableNodesService
*/
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.
*
@ -46,6 +32,8 @@ class FindViableNodesService
*/
public function setLocations(array $locations): self
{
Assert::allInteger($locations, 'An array of location IDs should be provided when calling setLocations.');
$this->locations = $locations;
return $this;
@ -90,32 +78,34 @@ class FindViableNodesService
* are tossed out, as are any nodes marked as non-public, meaning automatic
* deployments should not be done against them.
*
* @return int[]
* @return \Pterodactyl\Models\Node[]|\Illuminate\Support\Collection
* @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->memory, 'Calls to ' . __METHOD__ . ' must have the memory usage set as an integer, received %s');
Assert::integer($this->disk, 'Disk space must be an int, got %s');
Assert::integer($this->memory, 'Memory usage must be an int, got %s');
$nodes = $this->repository->getNodesWithResourceUse($this->locations, $this->disk, $this->memory);
$viable = [];
$query = Node::query()->select('nodes.*')
->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) {
$memoryLimit = $node->memory * (1 + ($node->memory_overallocate / 100));
$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($this->locations)) {
$query = $query->whereIn('nodes.location_id', $this->locations);
}
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'));
}
return $viable;
return $results;
}
}

View File

@ -1,11 +1,4 @@
<?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;
@ -40,12 +33,8 @@ class InstallScriptService
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
* @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 (! $this->repository->isCopyableScript(array_get($data, 'copy_script_from'), $egg->nest_id)) {
throw new InvalidCopyFromException(trans('exceptions.nest.egg.invalid_copy_id'));

View File

@ -1,11 +1,4 @@
<?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;

View File

@ -1,11 +1,4 @@
<?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;

View File

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

View File

@ -73,7 +73,7 @@ class ProcessScheduleService
$this->taskRepository->update($task->id, ['is_queued' => true]);
$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\EggVariable;
use Illuminate\Contracts\Config\Repository as ConfigRepository;
use Pterodactyl\Contracts\Repository\ServerRepositoryInterface;
class EnvironmentService
{
@ -14,28 +12,6 @@ class EnvironmentService
*/
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
* with a specific server.
@ -79,7 +55,7 @@ class EnvironmentService
}
// 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(
$key, is_callable($object) ? call_user_func($object, $server) : object_get($server, $object)
);

View File

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

View File

@ -1,17 +1,9 @@
<?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;
use Pterodactyl\Models\Mount;
use Pterodactyl\Models\Server;
use Pterodactyl\Contracts\Repository\ServerRepositoryInterface;
class ServerConfigurationStructureService
{
@ -22,22 +14,13 @@ class ServerConfigurationStructureService
*/
private $environment;
/**
* @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface
*/
private $repository;
/**
* ServerConfigurationStructureService constructor.
*
* @param \Pterodactyl\Contracts\Repository\ServerRepositoryInterface $repository
* @param \Pterodactyl\Services\Servers\EnvironmentService $environment
*/
public function __construct(
ServerRepositoryInterface $repository,
EnvironmentService $environment
) {
$this->repository = $repository;
public function __construct(EnvironmentService $environment)
{
$this->environment = $environment;
}
@ -50,8 +33,6 @@ class ServerConfigurationStructureService
* @param \Pterodactyl\Models\Server $server
* @param bool $legacy
* @return array
*
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/
public function handle(Server $server, bool $legacy = false): array
{
@ -72,7 +53,7 @@ class ServerConfigurationStructureService
{
return [
'uuid' => $server->uuid,
'suspended' => (bool) $server->suspended,
'suspended' => $server->suspended,
'environment' => $this->environment->handle($server),
'invocation' => $server->startup,
'skip_egg_scripts' => $server->skip_scripts,
@ -112,8 +93,6 @@ class ServerConfigurationStructureService
*
* @param \Pterodactyl\Models\Server $server
* @return array
*
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/
protected function returnLegacyFormat(Server $server)
{

View File

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

View File

@ -3,11 +3,10 @@
namespace Pterodactyl\Services\Servers;
use Exception;
use Psr\Log\LoggerInterface;
use Illuminate\Http\Response;
use Pterodactyl\Models\Server;
use Illuminate\Support\Facades\Log;
use Illuminate\Database\ConnectionInterface;
use Pterodactyl\Repositories\Eloquent\ServerRepository;
use Pterodactyl\Repositories\Eloquent\DatabaseRepository;
use Pterodactyl\Repositories\Wings\DaemonServerRepository;
use Pterodactyl\Services\Databases\DatabaseManagementService;
use Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException;
@ -29,50 +28,26 @@ class ServerDeletionService
*/
private $daemonServerRepository;
/**
* @var \Pterodactyl\Repositories\Eloquent\DatabaseRepository
*/
private $databaseRepository;
/**
* @var \Pterodactyl\Services\Databases\DatabaseManagementService
*/
private $databaseManagementService;
/**
* @var \Pterodactyl\Repositories\Eloquent\ServerRepository
*/
private $repository;
/**
* @var \Psr\Log\LoggerInterface
*/
private $writer;
/**
* DeletionService constructor.
*
* @param \Illuminate\Database\ConnectionInterface $connection
* @param \Pterodactyl\Repositories\Wings\DaemonServerRepository $daemonServerRepository
* @param \Pterodactyl\Repositories\Eloquent\DatabaseRepository $databaseRepository
* @param \Pterodactyl\Services\Databases\DatabaseManagementService $databaseManagementService
* @param \Pterodactyl\Repositories\Eloquent\ServerRepository $repository
* @param \Psr\Log\LoggerInterface $writer
*/
public function __construct(
ConnectionInterface $connection,
DaemonServerRepository $daemonServerRepository,
DatabaseRepository $databaseRepository,
DatabaseManagementService $databaseManagementService,
ServerRepository $repository,
LoggerInterface $writer
DatabaseManagementService $databaseManagementService
) {
$this->connection = $connection;
$this->daemonServerRepository = $daemonServerRepository;
$this->databaseRepository = $databaseRepository;
$this->databaseManagementService = $databaseManagementService;
$this->repository = $repository;
$this->writer = $writer;
}
/**
@ -101,27 +76,39 @@ class ServerDeletionService
try {
$this->daemonServerRepository->setServer($server)->delete();
} catch (DaemonConnectionException $exception) {
if ($this->force) {
$this->writer->warning($exception);
} else {
// If there is an error not caused a 404 error and this isn't a forced delete,
// go ahead and bail out. We specifically ignore a 404 since that can be assumed
// 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;
}
Log::warning($exception);
}
$this->connection->transaction(function () use ($server) {
$this->databaseRepository->setColumns('id')->findWhere([['server_id', '=', $server->id]])->each(function ($item) {
foreach ($server->databases as $database) {
try {
$this->databaseManagementService->delete($item->id);
$this->databaseManagementService->delete($database);
} catch (Exception $exception) {
if ($this->force) {
$this->writer->warning($exception);
} else {
if (!$this->force) {
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;
use Illuminate\Support\Arr;
use Pterodactyl\Models\Egg;
use Pterodactyl\Models\User;
use Pterodactyl\Models\Server;
use Pterodactyl\Models\ServerVariable;
use Illuminate\Database\ConnectionInterface;
use Pterodactyl\Traits\Services\HasUserLevels;
use Pterodactyl\Contracts\Repository\EggRepositoryInterface;
use Pterodactyl\Contracts\Repository\ServerRepositoryInterface;
use Pterodactyl\Contracts\Repository\ServerVariableRepositoryInterface;
class StartupModificationService
{
@ -19,63 +19,21 @@ class StartupModificationService
*/
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
*/
private $validatorService;
/**
* @var \Pterodactyl\Services\Servers\ServerConfigurationStructureService
*/
private $structureService;
/**
* StartupModificationService constructor.
*
* @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
*/
public function __construct(
ConnectionInterface $connection,
EggRepositoryInterface $eggRepository,
EnvironmentService $environmentService,
ServerRepositoryInterface $repository,
ServerConfigurationStructureService $structureService,
ServerVariableRepositoryInterface $serverVariableRepository,
VariableValidatorService $validatorService
) {
public function __construct(ConnectionInterface $connection, VariableValidatorService $validatorService)
{
$this->connection = $connection;
$this->eggRepository = $eggRepository;
$this->environmentService = $environmentService;
$this->repository = $repository;
$this->serverVariableRepository = $serverVariableRepository;
$this->validatorService = $validatorService;
$this->structureService = $structureService;
}
/**
@ -85,34 +43,42 @@ class StartupModificationService
* @param array $data
* @return \Pterodactyl\Models\Server
*
* @throws \Illuminate\Validation\ValidationException
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
* @throws \Throwable
*/
public function handle(Server $server, array $data): Server
{
$this->connection->beginTransaction();
if (! is_null(array_get($data, 'environment'))) {
$this->validatorService->setUserLevel($this->getUserLevel());
$results = $this->validatorService->handle(array_get($data, 'egg_id', $server->egg_id), array_get($data, 'environment', []));
return $this->connection->transaction(function () use ($server, $data) {
if (! empty($data['environment'])) {
$egg = $this->isUserLevel(User::USER_LEVEL_ADMIN) ? ($data['egg_id'] ?? $server->egg_id) : $server->egg_id;
$results->each(function ($result) use ($server) {
$this->serverVariableRepository->withoutFreshModel()->updateOrCreate([
'server_id' => $server->id,
'variable_id' => $result->id,
], [
'variable_value' => $result->value ?? '',
]);
});
}
$results = $this->validatorService
->setUserLevel($this->getUserLevel())
->handle($egg, $data['environment']);
if ($this->isUserLevel(User::USER_LEVEL_ADMIN)) {
$this->updateAdministrativeSettings($data, $server);
}
foreach ($results as $result) {
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 \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 (
is_digit(array_get($data, 'egg_id'))
&& $data['egg_id'] != $server->egg_id
&& is_null(array_get($data, 'nest_id'))
) {
$egg = $this->eggRepository->setColumns(['id', 'nest_id'])->find($data['egg_id']);
$data['nest_id'] = $egg->nest_id;
$eggId = Arr::get($data, 'egg_id');
if (is_digit($eggId) && $server->egg_id !== (int)$eggId) {
/** @var \Pterodactyl\Models\Egg $egg */
$egg = Egg::query()->findOrFail($data['egg_id']);
$server = $server->forceFill([
'egg_id' => $egg->id,
'nest_id' => $egg->nest_id,
]);
}
$server = $this->repository->update($server->id, [
$server->forceFill([
'installed' => 0,
'startup' => array_get($data, 'startup', $server->startup),
'nest_id' => array_get($data, 'nest_id', $server->nest_id),
'egg_id' => array_get($data, 'egg_id', $server->egg_id),
'skip_scripts' => array_get($data, 'skip_scripts') ?? isset($data['skip_scripts']),
'image' => array_get($data, 'docker_image', $server->image),
]);
'startup' => $data['startup'] ?? $server->startup,
'skip_scripts' => $data['skip_scripts'] ?? isset($data['skip_scripts']),
'image' => $data['docker_image'] ?? $server->image,
])->save();
}
}

View File

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

View File

@ -11,32 +11,15 @@ namespace Pterodactyl\Services\Servers;
use Pterodactyl\Models\User;
use Illuminate\Support\Collection;
use Pterodactyl\Models\EggVariable;
use Illuminate\Validation\ValidationException;
use Pterodactyl\Traits\Services\HasUserLevels;
use Pterodactyl\Contracts\Repository\ServerRepositoryInterface;
use Illuminate\Contracts\Validation\Factory as ValidationFactory;
use Pterodactyl\Contracts\Repository\EggVariableRepositoryInterface;
use Pterodactyl\Contracts\Repository\ServerVariableRepositoryInterface;
class VariableValidatorService
{
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
*/
@ -45,20 +28,10 @@ class VariableValidatorService
/**
* 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
*/
public function __construct(
EggVariableRepositoryInterface $optionVariableRepository,
ServerRepositoryInterface $serverRepository,
ServerVariableRepositoryInterface $serverVariableRepository,
ValidationFactory $validator
) {
$this->optionVariableRepository = $optionVariableRepository;
$this->serverRepository = $serverRepository;
$this->serverVariableRepository = $serverVariableRepository;
public function __construct(ValidationFactory $validator)
{
$this->validator = $validator;
}
@ -72,16 +45,18 @@ class VariableValidatorService
*/
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 = [];
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);
$rules['environment.' . $variable->env_variable] = $variable->rules;
$customAttributes['environment.' . $variable->env_variable] = trans('validation.internal.variable_value', ['env' => $variable->name]);
@ -92,23 +67,12 @@ class VariableValidatorService
throw new ValidationException($validator);
}
$response = $variables->filter(function ($item) {
// Skip doing anything if user is not an admin and variable is not user viewable or editable.
if (! $this->isUserLevel(User::USER_LEVEL_ADMIN) && (! $item->user_editable || ! $item->user_viewable)) {
return false;
}
return true;
})->map(function ($item) use ($fields) {
return (object) [
return Collection::make($variables)->map(function ($item) use ($fields) {
return (object)[
'id' => $item->id,
'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,
'backups' => $server->backup_limit,
],
'is_suspended' => $server->suspended !== 0,
'is_suspended' => $server->suspended,
'is_installing' => $server->installed !== 1,
];
}

View File

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

View File

@ -1,5 +1,6 @@
<?php
use Carbon\Carbon;
use Ramsey\Uuid\Uuid;
use Cake\Chronos\Chronos;
use Illuminate\Support\Str;
@ -7,6 +8,7 @@ use Pterodactyl\Models\Node;
use Faker\Generator as Faker;
use Pterodactyl\Models\ApiKey;
/** @var \Illuminate\Database\Eloquent\Factory $factory */
/*
|--------------------------------------------------------------------------
| Model Factories
@ -35,8 +37,8 @@ $factory->define(Pterodactyl\Models\Server::class, function (Faker $faker) {
'installed' => 1,
'database_limit' => null,
'allocation_limit' => null,
'created_at' => \Carbon\Carbon::now(),
'updated_at' => \Carbon\Carbon::now(),
'created_at' => Carbon::now(),
'updated_at' => Carbon::now(),
];
});
@ -160,8 +162,8 @@ $factory->define(Pterodactyl\Models\Database::class, function (Faker $faker) {
'username' => str_random(10),
'remote' => '%',
'password' => $password ?: bcrypt('test123'),
'created_at' => \Carbon\Carbon::now()->toDateTimeString(),
'updated_at' => \Carbon\Carbon::now()->toDateTimeString(),
'created_at' => 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)),
'allowed_ips' => null,
'memo' => 'Test Function Key',
'created_at' => \Carbon\Carbon::now()->toDateTimeString(),
'updated_at' => \Carbon\Carbon::now()->toDateTimeString(),
'created_at' => 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],
]);
$this->updateImporterService->handle($egg->id, $file);
$this->updateImporterService->handle($egg, $file);
$this->command->info('Updated ' . $decoded->name);
} 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.
*
* @see https://github.com/pterodactyl/panel/issues/2457
*/
public function testValidationErrorIsReturnedForBadRequests()
{
@ -135,6 +137,15 @@ class ApiKeyControllerTest extends ClientApiIntegrationTestCase
$response->assertStatus(Response::HTTP_UNPROCESSABLE_ENTITY);
$response->assertJsonPath('errors.0.meta.rule', '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\User;
use Webmozart\Assert\Assert;
use Pterodactyl\Models\Model;
use Pterodactyl\Models\Server;
use Pterodactyl\Models\Subuser;
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();
// 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();
}

View File

@ -2,6 +2,7 @@
namespace Tests\Traits\Integration;
use Ramsey\Uuid\Uuid;
use Pterodactyl\Models\Egg;
use Pterodactyl\Models\Nest;
use Pterodactyl\Models\Node;
@ -74,4 +75,27 @@ trait CreatesTestModels
'location', 'user', 'node', 'allocation', 'nest', 'egg',
])->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()
{
$data = [
'MAIL_DRIVER' => 'smtp',
'MAIL_HOST' => 'mail.test.com',
'MAIL_PORT' => '567',
'MAIL_USERNAME' => 'username',
'MAIL_PASSWORD' => 'password',
'MAIL_FROM' => 'mail@from.com',
'MAIL_FROM_NAME' => 'MailName',
'MAIL_ENCRYPTION' => 'tls',
];
$this->setupCoreFunctions($data);
$display = $this->runCommand($this->command, [], array_values($data));
$this->assertNotEmpty($display);
$this->assertStringContainsString('Updating stored environment configuration file.', $display);
// TODO(dane): fix this
$this->markTestSkipped('Skipped, GitHub actions cannot run successfully.');
// $data = [
// 'MAIL_DRIVER' => 'smtp',
// 'MAIL_HOST' => 'mail.test.com',
// 'MAIL_PORT' => '567',
// 'MAIL_USERNAME' => 'username',
// 'MAIL_PASSWORD' => 'password',
// 'MAIL_FROM' => 'mail@from.com',
// 'MAIL_FROM_NAME' => 'MailName',
// 'MAIL_ENCRYPTION' => 'tls',
// ];
//
// $this->setupCoreFunctions($data);
// $display = $this->runCommand($this->command, [], array_values($data));
//
// $this->assertNotEmpty($display);
// $this->assertStringContainsString('Updating stored environment configuration file.', $display);
}
/**

View File

@ -45,28 +45,31 @@ class MakeUserCommandTest extends CommandTestCase
*/
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([
'email' => $user->email,
'username' => $user->username,
'name_first' => $user->name_first,
'name_last' => $user->name_last,
'password' => 'Password123',
'root_admin' => $user->root_admin,
])->once()->andReturn($user);
$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->assertStringContainsString($user->email, $display);
$this->assertStringContainsString($user->username, $display);
$this->assertStringContainsString($user->name, $display);
$this->assertStringContainsString('Yes', $display);
// $user = factory(User::class)->make(['root_admin' => true]);
//
// $this->creationService->shouldReceive('handle')->with([
// 'email' => $user->email,
// 'username' => $user->username,
// 'name_first' => $user->name_first,
// 'name_last' => $user->name_last,
// 'password' => 'Password123',
// 'root_admin' => $user->root_admin,
// ])->once()->andReturn($user);
//
// $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->assertStringContainsString($user->email, $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()
{
$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);
$this->assertEquals(1, $response);

View File

@ -1,11 +1,4 @@
<?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;
@ -41,7 +34,7 @@ class EggUpdateServiceTest extends TestCase
{
parent::setUp();
$this->model = factory(Egg::class)->make();
$this->model = factory(Egg::class)->make(['id' => 123]);
$this->repository = m::mock(EggRepositoryInterface::class);
$this->service = new EggUpdateService($this->repository);

View File

@ -1,13 +1,6 @@
<?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 Mockery as m;
@ -30,21 +23,11 @@ class InstallScriptServiceTest extends TestCase
'copy_script_from' => null,
];
/**
* @var \Pterodactyl\Models\Egg
*/
protected $model;
/**
* @var \Pterodactyl\Contracts\Repository\EggRepositoryInterface|\Mockery\Mock
*/
protected $repository;
/**
* @var \Pterodactyl\Services\Eggs\Scripts\InstallScriptService
*/
protected $service;
/**
* Setup tests.
*/
@ -52,10 +35,7 @@ class InstallScriptServiceTest extends TestCase
{
parent::setUp();
$this->model = factory(Egg::class)->make();
$this->repository = m::mock(EggRepositoryInterface::class);
$this->service = new InstallScriptService($this->repository);
}
/**
@ -63,13 +43,13 @@ class InstallScriptServiceTest extends TestCase
*/
public function testUpdateWithValidCopyScriptFromAttribute()
{
$model = factory(Egg::class)->make(['id' => 123, 'nest_id' => 456]);
$this->data['copy_script_from'] = 1;
$this->repository->shouldReceive('isCopyableScript')->with(1, $this->model->nest_id)->once()->andReturn(true);
$this->repository->shouldReceive('withoutFreshModel')->withNoArgs()->once()->andReturnSelf()
->shouldReceive('update')->with($this->model->id, $this->data)->andReturnNull();
$this->repository->shouldReceive('isCopyableScript')->with(1, $model->nest_id)->once()->andReturn(true);
$this->repository->expects('withoutFreshModel->update')->with($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->repository->shouldReceive('isCopyableScript')->with(1, $this->model->nest_id)->once()->andReturn(false);
try {
$this->service->handle($this->model, $this->data);
} catch (Exception $exception) {
$this->assertInstanceOf(InvalidCopyFromException::class, $exception);
$this->assertEquals(trans('exceptions.nest.egg.invalid_copy_id'), $exception->getMessage());
}
$this->expectException(InvalidCopyFromException::class);
$this->expectExceptionMessage(trans('exceptions.nest.egg.invalid_copy_id'));
$model = factory(Egg::class)->make(['id' => 123, 'nest_id' => 456]);
$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()
{
$this->repository->shouldReceive('withoutFreshModel')->withNoArgs()->once()->andReturnSelf()
->shouldReceive('update')->with($this->model->id, $this->data)->andReturnNull();
$model = factory(Egg::class)->make(['id' => 123, 'nest_id' => 456]);
$this->service->handle($this->model, $this->data);
$this->repository->expects('withoutFreshModel->update')->with($model->id, $this->data)->andReturnNull();
$this->getService()->handle($model, $this->data);
}
/**
* Test that an integer can be passed in place of a model.
*/
public function testFunctionAcceptsIntegerInPlaceOfModel()
private function getService()
{
$this->repository->shouldReceive('find')->with($this->model->id)->once()->andReturn($this->model);
$this->repository->shouldReceive('withoutFreshModel')->withNoArgs()->once()->andReturnSelf()
->shouldReceive('update')->with($this->model->id, $this->data)->andReturnNull();
$this->service->handle($this->model->id, $this->data);
return new InstallScriptService($this->repository);
}
}

View File

@ -1,11 +1,4 @@
<?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;
@ -22,21 +15,11 @@ class EggExporterServiceTest extends TestCase
{
use NestedObjectAssertionsTrait;
/**
* @var \Carbon\Carbon
*/
protected $carbon;
/**
* @var \Pterodactyl\Contracts\Repository\EggRepositoryInterface|\Mockery\Mock
*/
protected $repository;
/**
* @var \Pterodactyl\Services\Eggs\Sharing\EggExporterService
*/
protected $service;
/**
* Setup tests.
*/
@ -45,10 +28,8 @@ class EggExporterServiceTest extends TestCase
parent::setUp();
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()
{
$egg = factory(Egg::class)->make();
$egg = factory(Egg::class)->make([
'id' => 123,
'nest_id' => 456,
]);
$egg->variables = collect([$variable = factory(EggVariable::class)->make()]);
$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);
$data = json_decode($response);

View File

@ -1,13 +1,6 @@
<?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 Tests\TestCase;
@ -17,7 +10,6 @@ use Tests\Traits\MocksUuids;
use Illuminate\Http\UploadedFile;
use Pterodactyl\Models\EggVariable;
use Illuminate\Database\ConnectionInterface;
use Pterodactyl\Exceptions\PterodactylException;
use Pterodactyl\Services\Eggs\Sharing\EggImporterService;
use Pterodactyl\Contracts\Repository\EggRepositoryInterface;
use Pterodactyl\Contracts\Repository\NestRepositoryInterface;
@ -66,9 +58,9 @@ class EggImporterServiceTest extends TestCase
{
parent::setUp();
$this->file = m::mock(UploadedFile::class);
$this->connection = m::mock(ConnectionInterface::class);
$this->eggVariableRepository = m::mock(EggVariableRepositoryInterface::class);
$this->file = m::mock(UploadedFile::class);
$this->nestRepository = m::mock(NestRepositoryInterface::class);
$this->repository = m::mock(EggRepositoryInterface::class);
@ -82,13 +74,14 @@ class EggImporterServiceTest extends TestCase
*/
public function testEggConfigurationIsImported()
{
$egg = factory(Egg::class)->make();
$nest = factory(Nest::class)->make();
$egg = factory(Egg::class)->make(['id' => 123]);
$nest = factory(Nest::class)->make(['id' => 456]);
$this->file->shouldReceive('getError')->withNoArgs()->once()->andReturn(UPLOAD_ERR_OK);
$this->file->shouldReceive('isFile')->withNoArgs()->once()->andReturn(true);
$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)->once()->andReturn(json_encode([
'meta' => ['version' => 'PTDL_v1'],
'name' => $egg->name,
'author' => $egg->author,
@ -122,13 +115,18 @@ class EggImporterServiceTest extends TestCase
*/
public function testExceptionIsThrownIfFileIsInvalid()
{
$this->file->shouldReceive('getError')->withNoArgs()->once()->andReturn(UPLOAD_ERR_NO_FILE);
try {
$this->service->handle($this->file, 1234);
} catch (PterodactylException $exception) {
$this->assertInstanceOf(InvalidFileUploadException::class, $exception);
$this->assertEquals(trans('exceptions.nest.importer.file_error'), $exception->getMessage());
}
$this->expectException(InvalidFileUploadException::class);
$this->expectExceptionMessage(
'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)'
);
$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()
{
$this->file->shouldReceive('getError')->withNoArgs()->once()->andReturn(UPLOAD_ERR_OK);
$this->file->shouldReceive('isFile')->withNoArgs()->once()->andReturn(false);
$this->expectException(InvalidFileUploadException::class);
$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->service->handle($this->file, 1234);
} 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(false);
$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);
}
/**
@ -152,19 +153,18 @@ class EggImporterServiceTest extends TestCase
*/
public function testExceptionIsThrownIfJsonMetaDataIsInvalid()
{
$this->file->shouldReceive('getError')->withNoArgs()->once()->andReturn(UPLOAD_ERR_OK);
$this->file->shouldReceive('isFile')->withNoArgs()->once()->andReturn(true);
$this->file->shouldReceive('getSize')->withNoArgs()->once()->andReturn(100);
$this->file->shouldReceive('openFile->fread')->with(100)->once()->andReturn(json_encode([
$this->expectException(InvalidFileUploadException::class);
$this->expectExceptionMessage(trans('exceptions.nest.importer.invalid_json_provided'));
$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'],
]));
try {
$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());
}
$this->service->handle($this->file, 1234);
}
/**
@ -172,18 +172,16 @@ class EggImporterServiceTest extends TestCase
*/
public function testExceptionIsThrownIfBadJsonIsProvided()
{
$this->file->shouldReceive('getError')->withNoArgs()->once()->andReturn(UPLOAD_ERR_OK);
$this->file->shouldReceive('isFile')->withNoArgs()->once()->andReturn(true);
$this->file->shouldReceive('getSize')->withNoArgs()->once()->andReturn(100);
$this->file->shouldReceive('openFile->fread')->with(100)->once()->andReturn('}');
$this->expectException(BadJsonFormatException::class);
$this->expectExceptionMessage(trans('exceptions.nest.importer.json_error', [
'error' => 'Syntax error',
]));
try {
$this->service->handle($this->file, 1234);
} catch (PterodactylException $exception) {
$this->assertInstanceOf(BadJsonFormatException::class, $exception);
$this->assertEquals(trans('exceptions.nest.importer.json_error', [
'error' => json_last_error_msg(),
]), $exception->getMessage());
}
$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('}');
$this->service->handle($this->file, 1234);
}
}

View File

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

View File

@ -1,11 +1,4 @@
<?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;
@ -90,7 +83,7 @@ class NodeDeletionServiceTest extends TestCase
*/
public function testModelCanBePassedToFunctionInPlaceOfNodeId()
{
$node = factory(Node::class)->make();
$node = factory(Node::class)->make(['id' => 123]);
$this->serverRepository->shouldReceive('setColumns')->with('id')->once()->andReturnSelf()
->shouldReceive('findCountWhere')->with([['node_id', '=', $node->id]])->once()->andReturn(0);

View File

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

View File

@ -6,19 +6,13 @@ use Mockery as m;
use Tests\TestCase;
use Pterodactyl\Models\Server;
use Pterodactyl\Models\Location;
use Illuminate\Contracts\Config\Repository;
use Illuminate\Support\Collection;
use Pterodactyl\Models\EggVariable;
use Pterodactyl\Services\Servers\EnvironmentService;
use Pterodactyl\Contracts\Repository\ServerRepositoryInterface;
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
*/
@ -30,9 +24,7 @@ class EnvironmentServiceTest extends TestCase
public function setUp(): void
{
parent::setUp();
$this->config = m::mock(Repository::class);
$this->repository = m::mock(ServerRepositoryInterface::class);
config()->set('pterodactyl.environment_variables', []);
}
/**
@ -55,15 +47,17 @@ class EnvironmentServiceTest extends TestCase
*/
public function testProcessShouldReturnDefaultEnvironmentVariablesForAServer()
{
$model = $this->getServerModel();
$this->config->shouldReceive('get')->with(self::CONFIG_MAPPING, [])->once()->andReturn([]);
$this->repository->shouldReceive('getVariablesWithValues')->with($model->id)->once()->andReturn([
'TEST_VARIABLE' => 'Test Variable',
$model = $this->getServerModel([
'TEST_VARIABLE' => factory(EggVariable::class)->make([
'id' => 987,
'env_variable' => 'TEST_VARIABLE',
'default_value' => 'Test Variable',
]),
]);
$response = $this->getService()->handle($model);
$this->assertNotEmpty($response);
$this->assertEquals(4, count($response));
$this->assertCount(4, $response);
$this->assertArrayHasKey('TEST_VARIABLE', $response);
$this->assertSame('Test Variable', $response['TEST_VARIABLE']);
}
@ -73,10 +67,7 @@ class EnvironmentServiceTest extends TestCase
*/
public function testProcessShouldReturnKeySetAtRuntime()
{
$model = $this->getServerModel();
$this->config->shouldReceive('get')->with(self::CONFIG_MAPPING, [])->once()->andReturn([]);
$this->repository->shouldReceive('getVariablesWithValues')->with($model->id)->once()->andReturn([]);
$model = $this->getServerModel([]);
$service = $this->getService();
$service->setEnvironmentKey('TEST_VARIABLE', function ($server) {
return $server->uuidShort;
@ -94,12 +85,11 @@ class EnvironmentServiceTest extends TestCase
*/
public function testProcessShouldAllowOverwritingVariablesWithConfigurationFile()
{
$model = $this->getServerModel();
$this->repository->shouldReceive('getVariablesWithValues')->with($model->id)->once()->andReturn([]);
$this->config->shouldReceive('get')->with(self::CONFIG_MAPPING, [])->once()->andReturn([
config()->set('pterodactyl.environment_variables', [
'P_SERVER_UUID' => 'name',
]);
$model = $this->getServerModel([]);
$response = $this->getService()->handle($model);
$this->assertNotEmpty($response);
@ -113,14 +103,13 @@ class EnvironmentServiceTest extends TestCase
*/
public function testVariablesSetInConfigurationAllowForClosures()
{
$model = $this->getServerModel();
$this->config->shouldReceive('get')->with(self::CONFIG_MAPPING, [])->once()->andReturn([
config()->set('pterodactyl.environment_variables', [
'P_SERVER_UUID' => function ($server) {
return $server->id * 2;
},
]);
$this->repository->shouldReceive('getVariablesWithValues')->with($model->id)->once()->andReturn([]);
$model = $this->getServerModel([]);
$response = $this->getService()->handle($model);
$this->assertNotEmpty($response);
@ -135,12 +124,11 @@ class EnvironmentServiceTest extends TestCase
*/
public function testProcessShouldAllowOverwritingDefaultVariablesWithRuntimeProvided()
{
$model = $this->getServerModel();
$this->config->shouldReceive('get')->with(self::CONFIG_MAPPING, [])->once()->andReturn([
config()->set('pterodactyl.environment_variables', [
'P_SERVER_UUID' => 'overwritten-config',
]);
$this->repository->shouldReceive('getVariablesWithValues')->with($model->id)->once()->andReturn([]);
$model = $this->getServerModel([]);
$service = $this->getService();
$service->setEnvironmentKey('P_SERVER_UUID', function ($model) {
return 'overwritten';
@ -161,18 +149,25 @@ class EnvironmentServiceTest extends TestCase
*/
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.
*
* @param array $variables
* @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(),
]);
$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('environment', $response);
$this->assertArrayHasKey('invocation', $response);
$this->assertArrayHasKey('skip_egg_scripts', $response);
$this->assertArrayHasKey('build', $response);
$this->assertArrayHasKey('service', $response);
$this->assertArrayHasKey('container', $response);
$this->assertArrayHasKey('allocations', $response);
@ -79,11 +79,6 @@ class ServerConfigurationStructureServiceTest extends TestCase
'disk_space' => $model->disk,
], $response['build']);
$this->assertSame([
'egg' => $model->egg->uuid,
'skip_scripts' => $model->skip_scripts,
], $response['service']);
$this->assertSame([
'image' => $model->image,
'oom_disabled' => $model->oom_disabled,
@ -91,7 +86,7 @@ class ServerConfigurationStructureServiceTest extends TestCase
], $response['container']);
$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($model->startup, $response['invocation']);
}
@ -103,6 +98,6 @@ class ServerConfigurationStructureServiceTest extends TestCase
*/
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 Tests\TestCase;
use Pterodactyl\Models\Egg;
use Pterodactyl\Models\Server;
use Illuminate\Support\Collection;
use Pterodactyl\Models\Allocation;
use Pterodactyl\Models\EggVariable;
use Pterodactyl\Services\Servers\StartupCommandService;
@ -34,43 +32,33 @@ class StartupCommandViewServiceTest extends TestCase
*/
public function testServiceResponse()
{
$allocation = factory(Allocation::class)->make();
$egg = factory(Egg::class)->make();
$server = factory(Server::class)->make([
'id' => 123,
'startup' => 'example {{SERVER_MEMORY}} {{SERVER_IP}} {{SERVER_PORT}} {{TEST_VARIABLE}} {{TEST_VARIABLE_HIDDEN}} {{UNKNOWN}}',
]);
$variables = collect([
factory(EggVariable::class)->make(['env_variable' => 'TEST_VARIABLE', 'user_viewable' => 1]),
factory(EggVariable::class)->make(['env_variable' => 'TEST_VARIABLE_HIDDEN', 'user_viewable' => 0]),
factory(EggVariable::class)->make([
'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('allocation', $allocation);
$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);
$server->setRelation('variables', $variables);
$server->setRelation('allocation', $allocation = factory(Allocation::class)->make());
$response = $this->getService()->handle($server);
$this->assertSame(
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
{
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);
}
}