Merge branch 'develop' into update/paper

This commit is contained in:
Dane Everitt 2020-12-24 10:19:19 -08:00 committed by GitHub
commit 158823603a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
88 changed files with 482 additions and 4592 deletions

View file

@ -19,8 +19,8 @@ HASHIDS_SALT=
HASHIDS_LENGTH=8 HASHIDS_LENGTH=8
MAIL_DRIVER=smtp MAIL_DRIVER=smtp
MAIL_HOST=mailtrap.io MAIL_HOST=smtp.example.com
MAIL_PORT=2525 MAIL_PORT=25
MAIL_USERNAME= MAIL_USERNAME=
MAIL_PASSWORD= MAIL_PASSWORD=
MAIL_ENCRYPTION=tls MAIL_ENCRYPTION=tls

View file

@ -6,7 +6,7 @@ about: For reporting code or design bugs with the software. DO NOT REPORT APACHE
DO NOT REPORT ISSUES CONFIGURING: SSL, PHP, APACHE, NGINX, YOUR MACHINE, SSH, SFTP, ETC. ON THIS GITHUB TRACKER. DO NOT REPORT ISSUES CONFIGURING: SSL, PHP, APACHE, NGINX, YOUR MACHINE, SSH, SFTP, ETC. ON THIS GITHUB TRACKER.
For assistance installating this software, as well as debugging issues with dependencies, please use our discord server: https://discord.gg/pterodactyl For assistance installing this software, as well as debugging issues with dependencies, please use our discord server: https://discord.gg/pterodactyl
You MUST complete all of the below information when reporting a bug, failure to do so will result in closure of your issue. PLEASE stop spamming our tracker with "bugs" that are not related to this project. You MUST complete all of the below information when reporting a bug, failure to do so will result in closure of your issue. PLEASE stop spamming our tracker with "bugs" that are not related to this project.

View file

@ -25,11 +25,13 @@ class Kernel extends ConsoleKernel
// Execute scheduled commands for servers every minute, as if there was a normal cron running. // Execute scheduled commands for servers every minute, as if there was a normal cron running.
$schedule->command('p:schedule:process')->everyMinute()->withoutOverlapping(); $schedule->command('p:schedule:process')->everyMinute()->withoutOverlapping();
// Every 30 minutes, run the backup pruning command so that any abandoned backups can be removed // Every 30 minutes, run the backup pruning command so that any abandoned backups can be deleted.
// from the UI view for the server. $pruneAge = config('backups.prune_age', 360); // Defaults to 6 hours (time is in minuteS)
if ($pruneAge > 0) {
$schedule->command('p:maintenance:prune-backups', [ $schedule->command('p:maintenance:prune-backups', [
'--since-minutes' => '30', '--since-minutes' => $pruneAge,
])->everyThirtyMinutes(); ])->everyThirtyMinutes();
}
// Every day cleanup any internal backups of service files. // Every day cleanup any internal backups of service files.
$schedule->command('p:maintenance:clean-service-backups')->daily(); $schedule->command('p:maintenance:clean-service-backups')->daily();

View file

@ -78,7 +78,14 @@ class EggController extends Controller
*/ */
public function store(EggFormRequest $request): RedirectResponse public function store(EggFormRequest $request): RedirectResponse
{ {
$egg = $this->creationService->handle($request->normalize()); $data = $request->normalize();
if (!empty($data['docker_images']) && !is_array($data['docker_images'])) {
$data['docker_images'] = array_map(function ($value) {
return trim($value);
}, explode("\n", $data['docker_images']));
}
$egg = $this->creationService->handle($data);
$this->alert->success(trans('admin/nests.eggs.notices.egg_created'))->flash(); $this->alert->success(trans('admin/nests.eggs.notices.egg_created'))->flash();
return redirect()->route('admin.nests.egg.view', $egg->id); return redirect()->route('admin.nests.egg.view', $egg->id);
@ -108,7 +115,14 @@ class EggController extends Controller
*/ */
public function update(EggFormRequest $request, Egg $egg): RedirectResponse public function update(EggFormRequest $request, Egg $egg): RedirectResponse
{ {
$this->updateService->handle($egg, $request->normalize()); $data = $request->normalize();
if (!empty($data['docker_images']) && !is_array($data['docker_images'])) {
$data['docker_images'] = array_map(function ($value) {
return trim($value);
}, explode("\n", $data['docker_images']));
}
$this->updateService->handle($egg, $data);
$this->alert->success(trans('admin/nests.eggs.notices.updated'))->flash(); $this->alert->success(trans('admin/nests.eggs.notices.updated'))->flash();
return redirect()->route('admin.nests.egg.view', $egg->id); return redirect()->route('admin.nests.egg.view', $egg->id);

View file

@ -111,17 +111,19 @@ class CreateServerController extends Controller
* *
* @throws \Illuminate\Validation\ValidationException * @throws \Illuminate\Validation\ValidationException
* @throws \Pterodactyl\Exceptions\DisplayException * @throws \Pterodactyl\Exceptions\DisplayException
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
* @throws \Pterodactyl\Exceptions\Service\Deployment\NoViableAllocationException * @throws \Pterodactyl\Exceptions\Service\Deployment\NoViableAllocationException
* @throws \Pterodactyl\Exceptions\Service\Deployment\NoViableNodeException * @throws \Pterodactyl\Exceptions\Service\Deployment\NoViableNodeException
* @throws \Throwable * @throws \Throwable
*/ */
public function store(ServerFormRequest $request) public function store(ServerFormRequest $request)
{ {
$server = $this->creationService->handle( $data = $request->except(['_token']);
$request->except(['_token']) if (!empty($data['custom_image'])) {
); $data['image'] = $data['custom_image'];
unset($data['custom_image']);
}
$server = $this->creationService->handle($data);
$this->alert->success( $this->alert->success(
trans('admin/server.alerts.server_created') trans('admin/server.alerts.server_created')

View file

@ -334,14 +334,19 @@ class ServersController extends Controller
* @return \Illuminate\Http\RedirectResponse * @return \Illuminate\Http\RedirectResponse
* *
* @throws \Illuminate\Validation\ValidationException * @throws \Illuminate\Validation\ValidationException
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/ */
public function saveStartup(Request $request, Server $server) public function saveStartup(Request $request, Server $server)
{ {
$data = $request->except('_token');
if (!empty($data['custom_docker_image'])) {
$data['docker_image'] = $data['custom_docker_image'];
unset($data['custom_docker_image']);
}
try { try {
$this->startupModificationService $this->startupModificationService
->setUserLevel(User::USER_LEVEL_ADMIN) ->setUserLevel(User::USER_LEVEL_ADMIN)
->handle($server, $request->except('_token')); ->handle($server, $data);
} catch (DataValidationException $exception) { } catch (DataValidationException $exception) {
throw new ValidationException($exception->validator); throw new ValidationException($exception->validator);
} }

View file

@ -13,6 +13,7 @@ use Pterodactyl\Repositories\Wings\DaemonFileRepository;
use Pterodactyl\Transformers\Daemon\FileObjectTransformer; use Pterodactyl\Transformers\Daemon\FileObjectTransformer;
use Pterodactyl\Http\Controllers\Api\Client\ClientApiController; use Pterodactyl\Http\Controllers\Api\Client\ClientApiController;
use Pterodactyl\Http\Requests\Api\Client\Servers\Files\CopyFileRequest; use Pterodactyl\Http\Requests\Api\Client\Servers\Files\CopyFileRequest;
use Pterodactyl\Http\Requests\Api\Client\Servers\Files\PullFileRequest;
use Pterodactyl\Http\Requests\Api\Client\Servers\Files\ListFilesRequest; use Pterodactyl\Http\Requests\Api\Client\Servers\Files\ListFilesRequest;
use Pterodactyl\Http\Requests\Api\Client\Servers\Files\DeleteFileRequest; use Pterodactyl\Http\Requests\Api\Client\Servers\Files\DeleteFileRequest;
use Pterodactyl\Http\Requests\Api\Client\Servers\Files\RenameFileRequest; use Pterodactyl\Http\Requests\Api\Client\Servers\Files\RenameFileRequest;
@ -72,7 +73,7 @@ class FileController extends ClientApiController
{ {
$contents = $this->fileRepository $contents = $this->fileRepository
->setServer($server) ->setServer($server)
->getDirectory($this->encode($request->get('directory') ?? '/')); ->getDirectory($request->get('directory') ?? '/');
return $this->fractal->collection($contents) return $this->fractal->collection($contents)
->transformWith($this->getTransformer(FileObjectTransformer::class)) ->transformWith($this->getTransformer(FileObjectTransformer::class))
@ -93,7 +94,7 @@ class FileController extends ClientApiController
{ {
return new Response( return new Response(
$this->fileRepository->setServer($server)->getContent( $this->fileRepository->setServer($server)->getContent(
$this->encode($request->get('file')), config('pterodactyl.files.max_edit_size') $request->get('file'), config('pterodactyl.files.max_edit_size')
), ),
Response::HTTP_OK, Response::HTTP_OK,
['Content-Type' => 'text/plain'] ['Content-Type' => 'text/plain']
@ -143,10 +144,7 @@ class FileController extends ClientApiController
*/ */
public function write(WriteFileContentRequest $request, Server $server): JsonResponse public function write(WriteFileContentRequest $request, Server $server): JsonResponse
{ {
$this->fileRepository->setServer($server)->putContent( $this->fileRepository->setServer($server)->putContent($request->get('file'), $request->getContent());
$this->encode($request->get('file')),
$request->getContent()
);
return new JsonResponse([], Response::HTTP_NO_CONTENT); return new JsonResponse([], Response::HTTP_NO_CONTENT);
} }
@ -284,16 +282,18 @@ class FileController extends ClientApiController
} }
/** /**
* Encodes a given file name & path in a format that should work for a good majority * Requests that a file be downloaded from a remote location by Wings.
* of file names without too much confusing logic.
* *
* @param string $path * @param $request
* @return string * @param \Pterodactyl\Models\Server $server
* @return \Illuminate\Http\JsonResponse
*
* @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException
*/ */
private function encode(string $path): string public function pull(PullFileRequest $request, Server $server): JsonResponse
{ {
return Collection::make(explode('/', rawurldecode($path)))->map(function ($value) { $this->fileRepository->setServer($server)->pull($request->input('url'), $request->input('directory'));
return rawurlencode($value);
})->join('/'); return new JsonResponse([], Response::HTTP_NO_CONTENT);
} }
} }

View file

@ -2,13 +2,16 @@
namespace Pterodactyl\Http\Controllers\Api\Client\Servers; namespace Pterodactyl\Http\Controllers\Api\Client\Servers;
use Illuminate\Http\Request;
use Illuminate\Http\Response; use Illuminate\Http\Response;
use Pterodactyl\Models\Server; use Pterodactyl\Models\Server;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
use Pterodactyl\Repositories\Eloquent\ServerRepository; use Pterodactyl\Repositories\Eloquent\ServerRepository;
use Pterodactyl\Services\Servers\ReinstallServerService; use Pterodactyl\Services\Servers\ReinstallServerService;
use Pterodactyl\Http\Controllers\Api\Client\ClientApiController; use Pterodactyl\Http\Controllers\Api\Client\ClientApiController;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Pterodactyl\Http\Requests\Api\Client\Servers\Settings\RenameServerRequest; use Pterodactyl\Http\Requests\Api\Client\Servers\Settings\RenameServerRequest;
use Pterodactyl\Http\Requests\Api\Client\Servers\Settings\SetDockerImageRequest;
use Pterodactyl\Http\Requests\Api\Client\Servers\Settings\ReinstallServerRequest; use Pterodactyl\Http\Requests\Api\Client\Servers\Settings\ReinstallServerRequest;
class SettingsController extends ClientApiController class SettingsController extends ClientApiController
@ -73,4 +76,26 @@ class SettingsController extends ClientApiController
return new JsonResponse([], Response::HTTP_ACCEPTED); return new JsonResponse([], Response::HTTP_ACCEPTED);
} }
/**
* Changes the Docker image in use by the server.
*
* @param \Pterodactyl\Http\Requests\Api\Client\Servers\Settings\SetDockerImageRequest $request
* @param \Pterodactyl\Models\Server $server
* @return \Illuminate\Http\JsonResponse
*
* @throws \Throwable
*/
public function dockerImage(SetDockerImageRequest $request, Server $server)
{
if (!in_array($server->image, $server->egg->docker_images)) {
throw new BadRequestHttpException(
'This server\'s Docker image has been manually set by an administrator and cannot be updated.'
);
}
$server->forceFill(['image' => $request->input('docker_image')])->saveOrFail();
return new JsonResponse([], Response::HTTP_NO_CONTENT);
}
} }

View file

@ -62,6 +62,7 @@ class StartupController extends ClientApiController
->transformWith($this->getTransformer(EggVariableTransformer::class)) ->transformWith($this->getTransformer(EggVariableTransformer::class))
->addMeta([ ->addMeta([
'startup_command' => $startup, 'startup_command' => $startup,
'docker_images' => $server->egg->docker_images,
'raw_startup_command' => $server->startup, 'raw_startup_command' => $server->startup,
]) ])
->toArray(); ->toArray();

View file

@ -52,7 +52,7 @@ class BackupRemoteUploadController extends Controller
public function __invoke(Request $request, string $backup) public function __invoke(Request $request, string $backup)
{ {
// Get the size query parameter. // Get the size query parameter.
$size = (int)$request->query('size'); $size = (int) $request->query('size');
if (empty($size)) { if (empty($size)) {
throw new BadRequestHttpException('A non-empty "size" query parameter must be provided.'); throw new BadRequestHttpException('A non-empty "size" query parameter must be provided.');
} }

View file

@ -21,7 +21,7 @@ class EggFormRequest extends AdminFormRequest
$rules = [ $rules = [
'name' => 'required|string|max:191', 'name' => 'required|string|max:191',
'description' => 'nullable|string', 'description' => 'nullable|string',
'docker_image' => 'required|string|max:191', 'docker_images' => 'required|string',
'startup' => 'required|string', 'startup' => 'required|string',
'config_from' => 'sometimes|bail|nullable|numeric', 'config_from' => 'sometimes|bail|nullable|numeric',
'config_stop' => 'required_without:config_from|nullable|string|max:191', 'config_stop' => 'required_without:config_from|nullable|string|max:191',

View file

@ -1,11 +1,4 @@
<?php <?php
/**
* Pterodactyl - Panel
* Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com>.
*
* This software is licensed under the terms of the MIT license.
* https://opensource.org/licenses/MIT
*/
namespace Pterodactyl\Http\Requests\Admin; namespace Pterodactyl\Http\Requests\Admin;
@ -23,6 +16,7 @@ class ServerFormRequest extends AdminFormRequest
{ {
$rules = Server::getRules(); $rules = Server::getRules();
$rules['description'][] = 'nullable'; $rules['description'][] = 'nullable';
$rules['custom_image'] = 'sometimes|nullable|string';
return $rules; return $rules;
} }

View file

@ -0,0 +1,29 @@
<?php
namespace Pterodactyl\Http\Requests\Api\Client\Servers\Files;
use Pterodactyl\Models\Permission;
use Pterodactyl\Contracts\Http\ClientPermissionsRequest;
use Pterodactyl\Http\Requests\Api\Client\ClientApiRequest;
class PullFileRequest extends ClientApiRequest implements ClientPermissionsRequest
{
/**
* @return string
*/
public function permission(): string
{
return Permission::ACTION_FILE_CREATE;
}
/**
* @return string[]
*/
public function rules(): array
{
return [
'url' => 'required|string|url',
'directory' => 'sometimes|nullable|string',
];
}
}

View file

@ -0,0 +1,36 @@
<?php
namespace Pterodactyl\Http\Requests\Api\Client\Servers\Settings;
use Webmozart\Assert\Assert;
use Pterodactyl\Models\Server;
use Illuminate\Validation\Rule;
use Pterodactyl\Models\Permission;
use Pterodactyl\Contracts\Http\ClientPermissionsRequest;
use Pterodactyl\Http\Requests\Api\Client\ClientApiRequest;
class SetDockerImageRequest extends ClientApiRequest implements ClientPermissionsRequest
{
/**
* @return string
*/
public function permission(): string
{
return Permission::ACTION_STARTUP_DOCKER_IMAGE;
}
/**
* @return array[]
*/
public function rules(): array
{
/** @var \Pterodactyl\Models\Server $server */
$server = $this->route()->parameter('server');
Assert::isInstanceOf($server, Server::class);
return [
'docker_image' => ['required', 'string', Rule::in($server->egg->docker_images)],
];
}
}

View file

@ -10,7 +10,9 @@ namespace Pterodactyl\Models;
* @property string $name * @property string $name
* @property string|null $description * @property string|null $description
* @property array|null $features * @property array|null $features
* @property string $docker_image * @property string $docker_image -- deprecated, use $docker_images
* @property string $update_url
* @property array $docker_images
* @property string|null $config_files * @property string|null $config_files
* @property string|null $config_startup * @property string|null $config_startup
* @property string|null $config_logs * @property string|null $config_logs
@ -76,7 +78,7 @@ class Egg extends Model
'name', 'name',
'description', 'description',
'features', 'features',
'docker_image', 'docker_images',
'config_files', 'config_files',
'config_startup', 'config_startup',
'config_logs', 'config_logs',
@ -101,6 +103,7 @@ class Egg extends Model
'script_is_privileged' => 'boolean', 'script_is_privileged' => 'boolean',
'copy_script_from' => 'integer', 'copy_script_from' => 'integer',
'features' => 'array', 'features' => 'array',
'docker_images' => 'array',
]; ];
/** /**
@ -113,13 +116,15 @@ class Egg extends Model
'description' => 'string|nullable', 'description' => 'string|nullable',
'features' => 'array|nullable', 'features' => 'array|nullable',
'author' => 'required|string|email', 'author' => 'required|string|email',
'docker_image' => 'required|string|max:191', 'docker_images' => 'required|array|min:1',
'docker_images.*' => 'required|string',
'startup' => 'required|nullable|string', 'startup' => 'required|nullable|string',
'config_from' => 'sometimes|bail|nullable|numeric|exists:eggs,id', 'config_from' => 'sometimes|bail|nullable|numeric|exists:eggs,id',
'config_stop' => 'required_without:config_from|nullable|string|max:191', 'config_stop' => 'required_without:config_from|nullable|string|max:191',
'config_startup' => 'required_without:config_from|nullable|json', 'config_startup' => 'required_without:config_from|nullable|json',
'config_logs' => 'required_without:config_from|nullable|json', 'config_logs' => 'required_without:config_from|nullable|json',
'config_files' => 'required_without:config_from|nullable|json', 'config_files' => 'required_without:config_from|nullable|json',
'update_url' => 'sometimes|nullable|string',
]; ];
/** /**
@ -131,6 +136,7 @@ class Egg extends Model
'config_startup' => null, 'config_startup' => null,
'config_logs' => null, 'config_logs' => null,
'config_files' => null, 'config_files' => null,
'update_url' => null,
]; ];
/** /**

View file

@ -58,6 +58,7 @@ class Permission extends Model
const ACTION_STARTUP_READ = 'startup.read'; const ACTION_STARTUP_READ = 'startup.read';
const ACTION_STARTUP_UPDATE = 'startup.update'; const ACTION_STARTUP_UPDATE = 'startup.update';
const ACTION_STARTUP_DOCKER_IMAGE = 'startup.docker-image';
const ACTION_SETTINGS_RENAME = 'settings.rename'; const ACTION_SETTINGS_RENAME = 'settings.rename';
const ACTION_SETTINGS_REINSTALL = 'settings.reinstall'; const ACTION_SETTINGS_REINSTALL = 'settings.reinstall';
@ -176,6 +177,7 @@ class Permission extends Model
'keys' => [ 'keys' => [
'read' => 'Allows a user to view the startup variables for a server.', 'read' => 'Allows a user to view the startup variables for a server.',
'update' => 'Allows a user to modify the startup variables for the server.', 'update' => 'Allows a user to modify the startup variables for the server.',
'docker-image' => 'Allows a user to modify the Docker image used when running the server.',
], ],
], ],

View file

@ -2,6 +2,7 @@
namespace Pterodactyl\Repositories\Wings; namespace Pterodactyl\Repositories\Wings;
use Illuminate\Support\Arr;
use Webmozart\Assert\Assert; use Webmozart\Assert\Assert;
use Pterodactyl\Models\Backup; use Pterodactyl\Models\Backup;
use Pterodactyl\Models\Server; use Pterodactyl\Models\Server;
@ -48,7 +49,7 @@ class DaemonBackupRepository extends DaemonRepository
'json' => [ 'json' => [
'adapter' => $this->adapter ?? config('backups.default'), 'adapter' => $this->adapter ?? config('backups.default'),
'uuid' => $backup->uuid, 'uuid' => $backup->uuid,
'ignored_files' => $backup->ignored_files, 'ignore' => implode('\n', $backup->ignored_files),
], ],
] ]
); );

View file

@ -37,7 +37,7 @@ class DaemonFileRepository extends DaemonRepository
throw new DaemonConnectionException($exception); throw new DaemonConnectionException($exception);
} }
$length = (int) $response->getHeader('Content-Length')[0] ?? 0; $length = (int)$response->getHeader('Content-Length')[0] ?? 0;
if ($notLargerThan && $length > $notLargerThan) { if ($notLargerThan && $length > $notLargerThan) {
throw new FileSizeTooLargeException; throw new FileSizeTooLargeException;
@ -297,4 +297,29 @@ class DaemonFileRepository extends DaemonRepository
throw new DaemonConnectionException($exception); throw new DaemonConnectionException($exception);
} }
} }
/**
* Pulls a file from the given URL and saves it to the disk.
*
* @param string $url
* @param string|null $directory
* @return \Psr\Http\Message\ResponseInterface
*
* @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException
*/
public function pull(string $url, ?string $directory): ResponseInterface
{
Assert::isInstanceOf($this->server, Server::class);
try {
return $this->getHttpClient()->post(
sprintf('/api/servers/%s/files/pull', $this->server->uuid),
[
'json' => ['url' => $url, 'directory' => $directory ?? '/'],
]
);
} catch (TransferException $exception) {
throw new DaemonConnectionException($exception);
}
}
} }

View file

@ -13,7 +13,6 @@ use Pterodactyl\Repositories\Eloquent\BackupRepository;
use Pterodactyl\Repositories\Wings\DaemonBackupRepository; use Pterodactyl\Repositories\Wings\DaemonBackupRepository;
use Pterodactyl\Exceptions\Service\Backup\TooManyBackupsException; use Pterodactyl\Exceptions\Service\Backup\TooManyBackupsException;
use Symfony\Component\HttpKernel\Exception\TooManyRequestsHttpException; use Symfony\Component\HttpKernel\Exception\TooManyRequestsHttpException;
use Pterodactyl\Services\Backups\DeleteBackupService;
class InitiateBackupService class InitiateBackupService
{ {
@ -118,14 +117,17 @@ class InitiateBackupService
} }
// Check if the server has reached or exceeded it's backup limit // Check if the server has reached or exceeded it's backup limit
if (!$server->backup_limit || $server->backups()->where('is_successful', true)->count() >= $server->backup_limit) { if (! $server->backup_limit || $server->backups()->where('is_successful', true)->count() >= $server->backup_limit) {
// Do not allow the user to continue if this server is already at its limit and can't override. // Do not allow the user to continue if this server is already at its limit and can't override.
if (!$override || $server->backup_limit <= 0) { if (! $override || $server->backup_limit <= 0) {
throw new TooManyBackupsException($server->backup_limit); throw new TooManyBackupsException($server->backup_limit);
} }
// Remove oldest backup // Get the oldest backup the server has.
$oldestBackup = $server->backups()->where('is_successful', true)->orderByDesc('created_at')->first(); /** @var \Pterodactyl\Models\Backup $oldestBackup */
$oldestBackup = $server->backups()->where('is_successful', true)->orderBy('created_at')->first();
// Delete the oldest backup.
$this->deleteBackupService->handle($oldestBackup); $this->deleteBackupService->handle($oldestBackup);
} }

View file

@ -38,13 +38,14 @@ class EggExporterService
'_comment' => 'DO NOT EDIT: FILE GENERATED AUTOMATICALLY BY PTERODACTYL PANEL - PTERODACTYL.IO', '_comment' => 'DO NOT EDIT: FILE GENERATED AUTOMATICALLY BY PTERODACTYL PANEL - PTERODACTYL.IO',
'meta' => [ 'meta' => [
'version' => 'PTDL_v1', 'version' => 'PTDL_v1',
'update_url' => $egg->update_url,
], ],
'exported_at' => Carbon::now()->toIso8601String(), 'exported_at' => Carbon::now()->toIso8601String(),
'name' => $egg->name, 'name' => $egg->name,
'author' => $egg->author, 'author' => $egg->author,
'description' => $egg->description, 'description' => $egg->description,
'features' => $egg->features, 'features' => $egg->features,
'image' => $egg->docker_image, 'images' => $egg->docker_images,
'startup' => $egg->startup, 'startup' => $egg->startup,
'config' => [ 'config' => [
'files' => $egg->inherit_config_files, 'files' => $egg->inherit_config_files,

View file

@ -102,7 +102,10 @@ class EggImporterService
'name' => object_get($parsed, 'name'), 'name' => object_get($parsed, 'name'),
'description' => object_get($parsed, 'description'), 'description' => object_get($parsed, 'description'),
'features' => object_get($parsed, 'features'), 'features' => object_get($parsed, 'features'),
'docker_image' => object_get($parsed, 'image'), // Maintain backwards compatability for eggs that are still using the old single image
// string format. New eggs can provide an array of Docker images that can be used.
'docker_images' => object_get($parsed, 'images') ?? [object_get($parsed, 'image')],
'update_url' => object_get($parsed, 'meta.update_url'),
'config_files' => object_get($parsed, 'config.files'), 'config_files' => object_get($parsed, 'config.files'),
'config_startup' => object_get($parsed, 'config.startup'), 'config_startup' => object_get($parsed, 'config.startup'),
'config_logs' => object_get($parsed, 'config.logs'), 'config_logs' => object_get($parsed, 'config.logs'),

View file

@ -87,7 +87,9 @@ class EggUpdateImporterService
'name' => object_get($parsed, 'name'), 'name' => object_get($parsed, 'name'),
'description' => object_get($parsed, 'description'), 'description' => object_get($parsed, 'description'),
'features' => object_get($parsed, 'features'), 'features' => object_get($parsed, 'features'),
'docker_image' => object_get($parsed, 'image'), // Maintain backwards compatibility for eggs that are still using the old single image
// string format. New eggs can provide an array of Docker images that can be used.
'docker_images' => object_get($parsed, 'images') ?? [object_get($parsed, 'image')],
'config_files' => object_get($parsed, 'config.files'), 'config_files' => object_get($parsed, 'config.files'),
'config_startup' => object_get($parsed, 'config.startup'), 'config_startup' => object_get($parsed, 'config.startup'),
'config_logs' => object_get($parsed, 'config.logs'), 'config_logs' => object_get($parsed, 'config.logs'),

View file

@ -45,7 +45,11 @@ class EggTransformer extends BaseTransformer
'nest' => $model->nest_id, 'nest' => $model->nest_id,
'author' => $model->author, 'author' => $model->author,
'description' => $model->description, 'description' => $model->description,
'docker_image' => $model->docker_image, // "docker_image" is deprecated, but left here to avoid breaking too many things at once
// in external software. We'll remove it down the road once things have gotten the chance
// to upgrade to using "docker_images".
'docker_image' => count($model->docker_images) > 0 ? $model->docker_images[0] : '',
'docker_images' => $model->docker_images,
'config' => [ 'config' => [
'files' => json_decode($model->config_files, true), 'files' => json_decode($model->config_files, true),
'startup' => json_decode($model->config_startup, true), 'startup' => json_decode($model->config_startup, true),

View file

@ -63,6 +63,7 @@ class ServerTransformer extends BaseClientTransformer
'cpu' => $server->cpu, 'cpu' => $server->cpu,
], ],
'invocation' => $service->handle($server, ! $this->getUser()->can(Permission::ACTION_STARTUP_READ, $server)), 'invocation' => $service->handle($server, ! $this->getUser()->can(Permission::ACTION_STARTUP_READ, $server)),
'docker_image' => $server->image,
'egg_features' => $server->egg->inherit_features, 'egg_features' => $server->egg->inherit_features,
'feature_limits' => [ 'feature_limits' => [
'databases' => $server->database_limit, 'databases' => $server->database_limit,

View file

@ -23,7 +23,7 @@ class FileObjectTransformer extends BaseDaemonTransformer
public function transform(array $item) public function transform(array $item)
{ {
return [ return [
'name' => Arr::get($item, 'name'), 'name' => rawurlencode(Arr::get($item, 'name')),
'mode' => Arr::get($item, 'mode'), 'mode' => Arr::get($item, 'mode'),
'mode_bits' => Arr::get($item, 'mode_bits'), 'mode_bits' => Arr::get($item, 'mode_bits'),
'size' => Arr::get($item, 'size'), 'size' => Arr::get($item, 'size'),

View file

@ -12,6 +12,10 @@ return [
// uses to upload backups to S3 storage. Value is in minutes, so this would default to an hour. // uses to upload backups to S3 storage. Value is in minutes, so this would default to an hour.
'presigned_url_lifespan' => env('BACKUP_PRESIGNED_URL_LIFESPAN', 60), 'presigned_url_lifespan' => env('BACKUP_PRESIGNED_URL_LIFESPAN', 60),
// The time to wait before automatically failing a backup, time is in minutes and defaults
// to 6 hours. To disable this feature, set the value to `0`.
'prune_age' => env('BACKUP_PRUNE_AGE', 360),
'disks' => [ 'disks' => [
// There is no configuration for the local disk for Wings. That configuration // There is no configuration for the local disk for Wings. That configuration
// is determined by the Daemon configuration, and not the Panel. // is determined by the Daemon configuration, and not the Panel.

View file

@ -30,6 +30,8 @@ class FixUniqueIndexToAccountForHost extends Migration
public function down() public function down()
{ {
Schema::table('databases', function (Blueprint $table) { Schema::table('databases', function (Blueprint $table) {
$table->dropForeign(['database_host_id']);
$table->dropUnique(['database_host_id', 'database']); $table->dropUnique(['database_host_id', 'database']);
$table->dropUnique(['database_host_id', 'username']); $table->dropUnique(['database_host_id', 'username']);

View file

@ -108,7 +108,8 @@ class MergePermissionsTableIntoSubusers extends Migration
foreach (DB::select('SELECT id, permissions FROM subusers') as $datum) { foreach (DB::select('SELECT id, permissions FROM subusers') as $datum) {
$values = []; $values = [];
foreach (json_decode($datum->permissions, true) as $permission) { foreach (json_decode($datum->permissions, true) as $permission) {
if (! empty($v = $flipped[$permission])) { $v = $flipped[$permission] ?? null;
if (! empty($v)) {
$values[] = $datum->id; $values[] = $datum->id;
$values[] = $v; $values[] = $v;
} }

View file

@ -0,0 +1,51 @@
<?php
use Illuminate\Support\Facades\DB;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class SupportMultipleDockerImagesAndUpdates extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('eggs', function (Blueprint $table) {
$table->json('docker_images')->after('docker_image')->nullable();
$table->text('update_url')->after('docker_images')->nullable();
});
Schema::table('eggs', function (Blueprint $table) {
DB::statement('UPDATE `eggs` SET `docker_images` = JSON_ARRAY(docker_image)');
});
Schema::table('eggs', function (Blueprint $table) {
$table->dropColumn('docker_image');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('eggs', function (Blueprint $table) {
$table->text('docker_image')->after('docker_images');
});
Schema::table('eggs', function (Blueprint $table) {
DB::statement('UPDATE `eggs` SET `docker_image` = JSON_UNQUOTE(JSON_EXTRACT(docker_images, "$[0]"))');
});
Schema::table('eggs', function (Blueprint $table) {
$table->dropColumn('docker_images');
$table->dropColumn('update_url');
});
}
}

View file

@ -3,7 +3,7 @@
"meta": { "meta": {
"version": "PTDL_v1" "version": "PTDL_v1"
}, },
"exported_at": "2020-11-03T04:22:56+00:00", "exported_at": "2020-12-06T17:39:27-08:00",
"name": "Forge Minecraft", "name": "Forge Minecraft",
"author": "support@pterodactyl.io", "author": "support@pterodactyl.io",
"description": "Minecraft Forge Server. Minecraft Forge is a modding API (Application Programming Interface), which makes it easier to create mods, and also make sure mods are compatible with each other.", "description": "Minecraft Forge Server. Minecraft Forge is a modding API (Application Programming Interface), which makes it easier to create mods, and also make sure mods are compatible with each other.",
@ -34,7 +34,7 @@
"rules": "required|regex:\/^([\\w\\d._-]+)(\\.jar)$\/" "rules": "required|regex:\/^([\\w\\d._-]+)(\\.jar)$\/"
}, },
{ {
"name": "Forge version", "name": "Minecraft Version",
"description": "The version of minecraft you want to install for.\r\n\r\nLeaving latest will install the latest recommended version.", "description": "The version of minecraft you want to install for.\r\n\r\nLeaving latest will install the latest recommended version.",
"env_variable": "MC_VERSION", "env_variable": "MC_VERSION",
"default_value": "latest", "default_value": "latest",
@ -58,7 +58,7 @@
"default_value": "", "default_value": "",
"user_viewable": true, "user_viewable": true,
"user_editable": true, "user_editable": true,
"rules": "required|string|max:20" "rules": "nullable|string|max:20"
} }
] ]
} }

View file

@ -28,7 +28,7 @@
"name": "Sponge Version", "name": "Sponge Version",
"description": "The version of SpongeVanilla to download and use.", "description": "The version of SpongeVanilla to download and use.",
"env_variable": "SPONGE_VERSION", "env_variable": "SPONGE_VERSION",
"default_value": "1.11.2-6.1.0-BETA-21", "default_value": "1.12.2-7.3.0",
"user_viewable": true, "user_viewable": true,
"user_editable": false, "user_editable": false,
"rules": "required|regex:\/^([a-zA-Z0-9.\\-_]+)$\/" "rules": "required|regex:\/^([a-zA-Z0-9.\\-_]+)$\/"

View file

@ -82,7 +82,13 @@ $('#pEggId').on('change', function (event) {
let parentChain = _.get(Pterodactyl.nests, $('#pNestId').val(), null); let parentChain = _.get(Pterodactyl.nests, $('#pNestId').val(), null);
let objectChain = _.get(parentChain, 'eggs.' + $(this).val(), null); let objectChain = _.get(parentChain, 'eggs.' + $(this).val(), null);
$('#pDefaultContainer').val(_.get(objectChain, 'docker_image', 'not defined!')); const images = _.get(objectChain, 'docker_images', [])
for (let i = 0; i < images.length; i++) {
let opt = document.createElement('option');
opt.value = images[i];
opt.innerHTML = images[i];
$('#pDefaultContainer').append(opt);
}
if (!_.get(objectChain, 'startup', false)) { if (!_.get(objectChain, 'startup', false)) {
$('#pStartup').val(_.get(parentChain, 'startup', 'ERROR: Startup Not Defined!')); $('#pStartup').val(_.get(parentChain, 'startup', 'ERROR: Startup Not Defined!'));

View file

@ -3,7 +3,7 @@ import http from '@/api/http';
export default (server: string, file: string): Promise<string> => { export default (server: string, file: string): Promise<string> => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
http.get(`/api/client/servers/${server}/files/contents`, { http.get(`/api/client/servers/${server}/files/contents`, {
params: { file: encodeURI(decodeURI(file)) }, params: { file },
transformResponse: res => res, transformResponse: res => res,
responseType: 'text', responseType: 'text',
}) })

View file

@ -18,7 +18,9 @@ export interface FileObject {
export default async (uuid: string, directory?: string): Promise<FileObject[]> => { export default async (uuid: string, directory?: string): Promise<FileObject[]> => {
const { data } = await http.get(`/api/client/servers/${uuid}/files/list`, { const { data } = await http.get(`/api/client/servers/${uuid}/files/list`, {
params: { directory: encodeURI(directory ?? '/') }, // At this point the directory is still encoded so we need to decode it since axios
// will automatically re-encode this value before sending it along in the request.
params: { directory: directory ?? '/' },
}); });
return (data.data || []).map(rawDataToFileObject); return (data.data || []).map(rawDataToFileObject);

View file

@ -2,7 +2,7 @@ import http from '@/api/http';
export default async (uuid: string, file: string, content: string): Promise<void> => { export default async (uuid: string, file: string, content: string): Promise<void> => {
await http.post(`/api/client/servers/${uuid}/files/write`, content, { await http.post(`/api/client/servers/${uuid}/files/write`, content, {
params: { file: encodeURI(decodeURI(file)) }, params: { file },
headers: { headers: {
'Content-Type': 'text/plain', 'Content-Type': 'text/plain',
}, },

View file

@ -22,6 +22,7 @@ export interface Server {
port: number; port: number;
}; };
invocation: string; invocation: string;
dockerImage: string;
description: string; description: string;
limits: { limits: {
memory: number; memory: number;
@ -50,6 +51,7 @@ export const rawDataToServerObject = ({ attributes: data }: FractalResponseData)
name: data.name, name: data.name,
node: data.node, node: data.node,
invocation: data.invocation, invocation: data.invocation,
dockerImage: data.docker_image,
sftpDetails: { sftpDetails: {
ip: data.sftp_details.ip, ip: data.sftp_details.ip,
port: data.sftp_details.port, port: data.sftp_details.port,

View file

@ -0,0 +1,5 @@
import http from '@/api/http';
export default async (uuid: string, image: string): Promise<void> => {
await http.put(`/api/client/servers/${uuid}/settings/docker-image`, { docker_image: image });
};

View file

@ -6,6 +6,7 @@ import { ServerEggVariable } from '@/api/server/types';
interface Response { interface Response {
invocation: string; invocation: string;
variables: ServerEggVariable[]; variables: ServerEggVariable[];
dockerImages: string[];
} }
export default (uuid: string, initialData?: Response) => useSWR([ uuid, '/startup' ], async (): Promise<Response> => { export default (uuid: string, initialData?: Response) => useSWR([ uuid, '/startup' ], async (): Promise<Response> => {
@ -13,5 +14,5 @@ export default (uuid: string, initialData?: Response) => useSWR([ uuid, '/startu
const variables = ((data as FractalResponseList).data || []).map(rawDataToServerEggVariable); const variables = ((data as FractalResponseList).data || []).map(rawDataToServerEggVariable);
return { invocation: data.meta.startup_command, variables }; return { invocation: data.meta.startup_command, variables, dockerImages: data.meta.docker_images || [] };
}, { initialData, errorRetryCount: 3 }); }, { initialData, errorRetryCount: 3 });

View file

@ -2,16 +2,28 @@ import React from 'react';
import Spinner from '@/components/elements/Spinner'; import Spinner from '@/components/elements/Spinner';
import Fade from '@/components/elements/Fade'; import Fade from '@/components/elements/Fade';
import tw from 'twin.macro'; import tw from 'twin.macro';
import styled, { css } from 'styled-components/macro';
import Select from '@/components/elements/Select';
const Container = styled.div<{ visible?: boolean }>`
${tw`relative`};
${props => props.visible && css`
& ${Select} {
background-image: none;
}
`};
`;
const InputSpinner = ({ visible, children }: { visible: boolean, children: React.ReactNode }) => ( const InputSpinner = ({ visible, children }: { visible: boolean, children: React.ReactNode }) => (
<div css={tw`relative`}> <Container visible={visible}>
<Fade appear unmountOnExit in={visible} timeout={150}> <Fade appear unmountOnExit in={visible} timeout={150}>
<div css={tw`absolute right-0 h-full flex items-center justify-end pr-3`}> <div css={tw`absolute right-0 h-full flex items-center justify-end pr-3`}>
<Spinner size={'small'}/> <Spinner size={'small'}/>
</div> </div>
</Fade> </Fade>
{children} {children}
</div> </Container>
); );
export default InputSpinner; export default InputSpinner;

View file

@ -127,7 +127,7 @@ export default ({ database, className }: Props) => {
<Can action={'database.view_password'}> <Can action={'database.view_password'}>
<div css={tw`mt-6`}> <div css={tw`mt-6`}>
<Label>Password</Label> <Label>Password</Label>
<CopyOnClick text={database.password?.valueOf}><Input type={'text'} readOnly value={database.password}/></CopyOnClick> <CopyOnClick text={database.password}><Input type={'text'} readOnly value={database.password}/></CopyOnClick>
</div> </div>
</Can> </Can>
<div css={tw`mt-6`}> <div css={tw`mt-6`}>

View file

@ -61,7 +61,7 @@ export default () => {
setLoading(true); setLoading(true);
clearFlashes('files:view'); clearFlashes('files:view');
fetchFileContent() fetchFileContent()
.then(content => saveFileContents(uuid, name || hash.replace(/^#/, ''), content)) .then(content => saveFileContents(uuid, name || decodeURI(hash.replace(/^#/, '')), content))
.then(() => { .then(() => {
if (name) { if (name) {
history.push(`/server/${id}/files/edit#/${name}`); history.push(`/server/${id}/files/edit#/${name}`);
@ -87,7 +87,9 @@ export default () => {
<PageContentBlock> <PageContentBlock>
<FlashMessageRender byKey={'files:view'} css={tw`mb-4`}/> <FlashMessageRender byKey={'files:view'} css={tw`mb-4`}/>
<ErrorBoundary> <ErrorBoundary>
<div css={tw`mb-4`}>
<FileManagerBreadcrumbs withinFileEditor isNewFile={action !== 'edit'}/> <FileManagerBreadcrumbs withinFileEditor isNewFile={action !== 'edit'}/>
</div>
</ErrorBoundary> </ErrorBoundary>
{hash.replace(/^#/, '').endsWith('.pteroignore') && {hash.replace(/^#/, '').endsWith('.pteroignore') &&
<div css={tw`mb-4 p-4 border-l-4 bg-neutral-900 rounded border-cyan-400`}> <div css={tw`mb-4 p-4 border-l-4 bg-neutral-900 rounded border-cyan-400`}>

View file

@ -1,33 +1,41 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { ServerContext } from '@/state/server'; import { ServerContext } from '@/state/server';
import { NavLink, useRouteMatch } from 'react-router-dom'; import { NavLink, useLocation } from 'react-router-dom';
import { cleanDirectoryPath } from '@/helpers'; import { cleanDirectoryPath } from '@/helpers';
import tw from 'twin.macro'; import tw from 'twin.macro';
import { FileActionCheckbox } from '@/components/server/files/SelectFileCheckbox';
import useFileManagerSwr from '@/plugins/useFileManagerSwr';
interface Props { interface Props {
renderLeft?: JSX.Element;
withinFileEditor?: boolean; withinFileEditor?: boolean;
isNewFile?: boolean; isNewFile?: boolean;
} }
export default ({ withinFileEditor, isNewFile }: Props) => { export default ({ renderLeft, withinFileEditor, isNewFile }: Props) => {
const [ file, setFile ] = useState<string | null>(null); const [ file, setFile ] = useState<string | null>(null);
const { params } = useRouteMatch<Record<string, string>>();
const id = ServerContext.useStoreState(state => state.server.data!.id); const id = ServerContext.useStoreState(state => state.server.data!.id);
const directory = ServerContext.useStoreState(state => state.files.directory); const directory = ServerContext.useStoreState(state => state.files.directory);
const { hash } = useLocation();
const { data: files } = useFileManagerSwr();
const setSelectedFiles = ServerContext.useStoreActions(actions => actions.files.setSelectedFiles);
const selectedFilesLength = ServerContext.useStoreState(state => state.files.selectedFiles.length);
useEffect(() => { useEffect(() => {
const parts = cleanDirectoryPath(window.location.hash).split('/'); let pathHash = cleanDirectoryPath(hash);
try {
pathHash = decodeURI(pathHash);
} catch (e) {
console.warn('Error decoding URL parts in hash:', e);
}
if (withinFileEditor && !isNewFile) { if (withinFileEditor && !isNewFile) {
setFile(parts.pop() || null); let name = pathHash.split('/').pop() || null;
if (name) {
try {
name = decodeURIComponent(name);
} catch (e) {
console.warn('Error decoding filename:', e);
} }
}, [ withinFileEditor, isNewFile ]); }
setFile(name);
}
}, [ withinFileEditor, isNewFile, hash ]);
const breadcrumbs = (): { name: string; path?: string }[] => directory.split('/') const breadcrumbs = (): { name: string; path?: string }[] => directory.split('/')
.filter(directory => !!directory) .filter(directory => !!directory)
@ -39,22 +47,9 @@ export default ({ withinFileEditor, isNewFile }: Props) => {
return { name: directory, path: `/${dirs.slice(0, index + 1).join('/')}` }; return { name: directory, path: `/${dirs.slice(0, index + 1).join('/')}` };
}); });
const onSelectAllClick = (e: React.ChangeEvent<HTMLInputElement>) => {
setSelectedFiles(e.currentTarget.checked ? (files?.map(file => file.name) || []) : []);
};
return ( return (
<div css={tw`flex flex-grow-0 items-center text-sm text-neutral-500 overflow-x-hidden`}> <div css={tw`flex flex-grow-0 items-center text-sm text-neutral-500 overflow-x-hidden`}>
{(files && files.length > 0 && !params?.action) ? {renderLeft || <div css={tw`w-12`}/>}
<FileActionCheckbox
type={'checkbox'}
css={tw`mx-4`}
checked={selectedFilesLength === (files ? files.length : -1)}
onChange={onSelectAllClick}
/>
:
<div css={tw`w-12`}/>
}
/<span css={tw`px-1 text-neutral-300`}>home</span>/ /<span css={tw`px-1 text-neutral-300`}>home</span>/
<NavLink <NavLink
to={`/server/${id}/files`} to={`/server/${id}/files`}
@ -70,16 +65,16 @@ export default ({ withinFileEditor, isNewFile }: Props) => {
to={`/server/${id}/files#${crumb.path}`} to={`/server/${id}/files#${crumb.path}`}
css={tw`px-1 text-neutral-200 no-underline hover:text-neutral-100`} css={tw`px-1 text-neutral-200 no-underline hover:text-neutral-100`}
> >
{crumb.name} {decodeURIComponent(crumb.name)}
</NavLink>/ </NavLink>/
</React.Fragment> </React.Fragment>
: :
<span key={index} css={tw`px-1 text-neutral-300`}>{crumb.name}</span> <span key={index} css={tw`px-1 text-neutral-300`}>{decodeURIComponent(crumb.name)}</span>
)) ))
} }
{file && {file &&
<React.Fragment> <React.Fragment>
<span css={tw`px-1 text-neutral-300`}>{decodeURI(file)}</span> <span css={tw`px-1 text-neutral-300`}>{file}</span>
</React.Fragment> </React.Fragment>
} }
</div> </div>

View file

@ -18,6 +18,7 @@ import UploadButton from '@/components/server/files/UploadButton';
import ServerContentBlock from '@/components/elements/ServerContentBlock'; import ServerContentBlock from '@/components/elements/ServerContentBlock';
import { useStoreActions } from '@/state/hooks'; import { useStoreActions } from '@/state/hooks';
import ErrorBoundary from '@/components/elements/ErrorBoundary'; import ErrorBoundary from '@/components/elements/ErrorBoundary';
import { FileActionCheckbox } from '@/components/server/files/SelectFileCheckbox';
const sortFiles = (files: FileObject[]): FileObject[] => { const sortFiles = (files: FileObject[]): FileObject[] => {
return files.sort((a, b) => a.name.localeCompare(b.name)) return files.sort((a, b) => a.name.localeCompare(b.name))
@ -31,18 +32,24 @@ export default () => {
const directory = ServerContext.useStoreState(state => state.files.directory); const directory = ServerContext.useStoreState(state => state.files.directory);
const clearFlashes = useStoreActions(actions => actions.flashes.clearFlashes); const clearFlashes = useStoreActions(actions => actions.flashes.clearFlashes);
const setDirectory = ServerContext.useStoreActions(actions => actions.files.setDirectory); const setDirectory = ServerContext.useStoreActions(actions => actions.files.setDirectory);
const setSelectedFiles = ServerContext.useStoreActions(actions => actions.files.setSelectedFiles); const setSelectedFiles = ServerContext.useStoreActions(actions => actions.files.setSelectedFiles);
const selectedFilesLength = ServerContext.useStoreState(state => state.files.selectedFiles.length);
useEffect(() => { useEffect(() => {
clearFlashes('files'); clearFlashes('files');
setSelectedFiles([]); setSelectedFiles([]);
setDirectory(hash.length > 0 ? decodeURI(hash) : '/'); setDirectory(hash.length > 0 ? hash : '/');
}, [ hash ]); }, [ hash ]);
useEffect(() => { useEffect(() => {
mutate(); mutate();
}, [ directory ]); }, [ directory ]);
const onSelectAllClick = (e: React.ChangeEvent<HTMLInputElement>) => {
setSelectedFiles(e.currentTarget.checked ? (files?.map(file => file.name) || []) : []);
};
if (error) { if (error) {
return ( return (
<ServerError message={httpErrorToHuman(error)} onRetry={() => mutate()}/> <ServerError message={httpErrorToHuman(error)} onRetry={() => mutate()}/>
@ -53,9 +60,17 @@ export default () => {
<ServerContentBlock title={'File Manager'} showFlashKey={'files'}> <ServerContentBlock title={'File Manager'} showFlashKey={'files'}>
<div css={tw`flex flex-wrap-reverse md:flex-no-wrap justify-center mb-4`}> <div css={tw`flex flex-wrap-reverse md:flex-no-wrap justify-center mb-4`}>
<ErrorBoundary> <ErrorBoundary>
<FileManagerBreadcrumbs/> <FileManagerBreadcrumbs
renderLeft={
<FileActionCheckbox
type={'checkbox'}
css={tw`mx-4`}
checked={selectedFilesLength === (files ? files.length : -1)}
onChange={onSelectAllClick}
/>
}
/>
</ErrorBoundary> </ErrorBoundary>
<Can action={'file.create'}> <Can action={'file.create'}>
<ErrorBoundary> <ErrorBoundary>
<div css={tw`flex flex-shrink-0 flex-wrap-reverse md:flex-no-wrap justify-end mb-4 md:mb-0 ml-0 md:ml-auto`}> <div css={tw`flex flex-shrink-0 flex-wrap-reverse md:flex-no-wrap justify-end mb-4 md:mb-0 ml-0 md:ml-auto`}>

View file

@ -24,7 +24,7 @@ const Clickable: React.FC<{ file: FileObject }> = memo(({ file, children }) => {
const history = useHistory(); const history = useHistory();
const match = useRouteMatch(); const match = useRouteMatch();
const destination = cleanDirectoryPath(`${directory}/${file.name}`).split('/').map(v => encodeURI(v)).join('/'); const destination = cleanDirectoryPath(`${directory}/${file.name}`).split('/').join('/');
const onRowClick = (e: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => { const onRowClick = (e: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => {
// Don't rely on the onClick to work with the generated URL. Because of the way this // Don't rely on the onClick to work with the generated URL. Because of the way this
@ -64,11 +64,7 @@ const FileObjectRow = ({ file }: { file: FileObject }) => (
> >
<SelectFileCheckbox name={file.name}/> <SelectFileCheckbox name={file.name}/>
<Clickable file={file}> <Clickable file={file}>
<div css={tw`w-24 ml-6 pl-3 hidden md:block`}> <div css={tw`flex-none self-center text-neutral-400 ml-6 mr-4 text-lg pl-3`}>
{file.mode}
</div>
<div css={tw`flex-none self-center text-neutral-400 ml-6 md:ml-0 mr-4 text-lg pl-3`}>
{file.isFile ? {file.isFile ?
<FontAwesomeIcon icon={file.isSymlink ? faFileImport : file.isArchiveType() ? faFileArchive : faFileAlt}/> <FontAwesomeIcon icon={file.isSymlink ? faFileImport : file.isArchiveType() ? faFileArchive : faFileAlt}/>
: :
@ -76,7 +72,7 @@ const FileObjectRow = ({ file }: { file: FileObject }) => (
} }
</div> </div>
<div css={tw`flex-1 truncate`}> <div css={tw`flex-1 truncate`}>
{file.name} {decodeURIComponent(file.name)}
</div> </div>
{file.isFile && {file.isFile &&
<div css={tw`w-1/6 text-right mr-4 hidden sm:block`}> <div css={tw`w-1/6 text-right mr-4 hidden sm:block`}>

View file

@ -1,4 +1,4 @@
import React, { useEffect } from 'react'; import React, { useCallback, useEffect, useState } from 'react';
import TitledGreyBox from '@/components/elements/TitledGreyBox'; import TitledGreyBox from '@/components/elements/TitledGreyBox';
import tw from 'twin.macro'; import tw from 'twin.macro';
import VariableBox from '@/components/server/startup/VariableBox'; import VariableBox from '@/components/server/startup/VariableBox';
@ -9,15 +9,32 @@ import ServerError from '@/components/screens/ServerError';
import { httpErrorToHuman } from '@/api/http'; import { httpErrorToHuman } from '@/api/http';
import { ServerContext } from '@/state/server'; import { ServerContext } from '@/state/server';
import { useDeepCompareEffect } from '@/plugins/useDeepCompareEffect'; import { useDeepCompareEffect } from '@/plugins/useDeepCompareEffect';
import Select from '@/components/elements/Select';
import isEqual from 'react-fast-compare';
import Input from '@/components/elements/Input';
import setSelectedDockerImage from '@/api/server/setSelectedDockerImage';
import InputSpinner from '@/components/elements/InputSpinner';
import useFlash from '@/plugins/useFlash';
const StartupContainer = () => { const StartupContainer = () => {
const uuid = ServerContext.useStoreState(state => state.server.data!.uuid); const [ loading, setLoading ] = useState(false);
const invocation = ServerContext.useStoreState(state => state.server.data!.invocation); const { clearFlashes, clearAndAddHttpError } = useFlash();
const variables = ServerContext.useStoreState(state => state.server.data!.variables);
const { data, error, isValidating, mutate } = getServerStartup(uuid, { invocation, variables }); const uuid = ServerContext.useStoreState(state => state.server.data!.uuid);
const variables = ServerContext.useStoreState(({ server }) => ({
variables: server.data!.variables,
invocation: server.data!.invocation,
dockerImage: server.data!.dockerImage,
// @ts-ignore
}), isEqual);
const { data, error, isValidating, mutate } = getServerStartup(uuid, {
...variables,
dockerImages: [ variables.dockerImage ],
});
const setServerFromState = ServerContext.useStoreActions(actions => actions.server.setServerFromState); const setServerFromState = ServerContext.useStoreActions(actions => actions.server.setServerFromState);
const isCustomImage = data && !data.dockerImages.map(v => v.toLowerCase()).includes(variables.dockerImage.toLowerCase());
useEffect(() => { useEffect(() => {
// Since we're passing in initial data this will not trigger on mount automatically. We // Since we're passing in initial data this will not trigger on mount automatically. We
@ -36,6 +53,20 @@ const StartupContainer = () => {
})); }));
}, [ data ]); }, [ data ]);
const updateSelectedDockerImage = useCallback((v: React.ChangeEvent<HTMLSelectElement>) => {
setLoading(true);
clearFlashes('startup:image');
const image = v.currentTarget.value;
setSelectedDockerImage(uuid, image)
.then(() => setServerFromState(s => ({ ...s, dockerImage: image })))
.catch(error => {
console.error(error);
clearAndAddHttpError({ key: 'startup:image', error });
})
.then(() => setLoading(false));
}, [ uuid ]);
return ( return (
!data ? !data ?
(!error || (error && isValidating)) ? (!error || (error && isValidating)) ?
@ -47,15 +78,49 @@ const StartupContainer = () => {
onRetry={() => mutate()} onRetry={() => mutate()}
/> />
: :
<ServerContentBlock title={'Startup Settings'}> <ServerContentBlock title={'Startup Settings'} showFlashKey={'startup:image'}>
<TitledGreyBox title={'Startup Command'}> <div css={tw`flex`}>
<TitledGreyBox title={'Startup Command'} css={tw`flex-1`}>
<div css={tw`px-1 py-2`}> <div css={tw`px-1 py-2`}>
<p css={tw`font-mono bg-neutral-900 rounded py-2 px-4`}> <p css={tw`font-mono bg-neutral-900 rounded py-2 px-4`}>
{data.invocation} {data.invocation}
</p> </p>
</div> </div>
</TitledGreyBox> </TitledGreyBox>
<div css={tw`grid gap-8 md:grid-cols-2 mt-10`}> <TitledGreyBox title={'Docker Image'} css={tw`flex-1 lg:flex-none lg:w-1/3 ml-10`}>
{data.dockerImages.length > 1 && !isCustomImage ?
<>
<InputSpinner visible={loading}>
<Select
disabled={data.dockerImages.length < 2}
onChange={updateSelectedDockerImage}
defaultValue={variables.dockerImage}
>
{data.dockerImages.map(image => (
<option key={image} value={image}>{image}</option>
))}
</Select>
</InputSpinner>
<p css={tw`text-xs text-neutral-300 mt-2`}>
This is an advanced feature allowing you to select a Docker image to use when
running this server instance.
</p>
</>
:
<>
<Input disabled readOnly value={variables.dockerImage}/>
{isCustomImage &&
<p css={tw`text-xs text-neutral-300 mt-2`}>
This {'server\'s'} Docker image has been manually set by an administrator and cannot
be changed through this UI.
</p>
}
</>
}
</TitledGreyBox>
</div>
<h3 css={tw`mt-8 mb-2 text-2xl`}>Variables</h3>
<div css={tw`grid gap-8 md:grid-cols-2`}>
{data.variables.map(variable => <VariableBox key={variable.envVariable} variable={variable}/>)} {data.variables.map(variable => <VariableBox key={variable.envVariable} variable={variable}/>)}
</div> </div>
</ServerContentBlock> </ServerContentBlock>

View file

@ -32,8 +32,9 @@ const VariableBox = ({ variable }: Props) => {
updateStartupVariable(uuid, variable.envVariable, value) updateStartupVariable(uuid, variable.envVariable, value)
.then(([ response, invocation ]) => mutate(data => ({ .then(([ response, invocation ]) => mutate(data => ({
...data,
invocation, invocation,
variables: data.variables.map(v => v.envVariable === response.envVariable ? response : v), variables: (data.variables || []).map(v => v.envVariable === response.envVariable ? response : v),
}), false)) }), false))
.catch(error => { .catch(error => {
console.error(error); console.error(error);
@ -67,7 +68,7 @@ const VariableBox = ({ variable }: Props) => {
placeholder={variable.defaultValue} placeholder={variable.defaultValue}
/> />
</InputSpinner> </InputSpinner>
<p css={tw`mt-1 text-xs text-neutral-400`}> <p css={tw`mt-1 text-xs text-neutral-300`}>
{variable.description} {variable.description}
</p> </p>
</TitledGreyBox> </TitledGreyBox>

View file

@ -53,13 +53,13 @@
</div> </div>
<div class="col-sm-6"> <div class="col-sm-6">
<div class="form-group"> <div class="form-group">
<label for="pDockerImage" class="control-label">Docker Image</label> <label for="pDockerImage" class="control-label">Docker Images</label>
<input type="text" id="pDockerImage" name="docker_image" value="{{ old('docker_image') }}" placeholder="quay.io/pterodactyl/service" class="form-control" /> <textarea id="pDockerImages" name="docker_images" rows="4" placeholder="quay.io/pterodactyl/service" class="form-control">{{ old('docker_images') }}</textarea>
<p class="text-muted small">The default docker image that should be used for new servers using this Egg. This can be changed per-server.</p> <p class="text-muted small">The docker images available to servers using this egg. Enter one per line. Users will be able to select from this list of images if more than one value is provided.</p>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="pStartup" class="control-label">Startup Command</label> <label for="pStartup" class="control-label">Startup Command</label>
<textarea id="pStartup" name="startup" class="form-control" rows="14">{{ old('startup') }}</textarea> <textarea id="pStartup" name="startup" class="form-control" rows="10">{{ old('startup') }}</textarea>
<p class="text-muted small">The default startup command that should be used for new servers created with this Egg. You can change this per-server as needed.</p> <p class="text-muted small">The default startup command that should be used for new servers created with this Egg. You can change this per-server as needed.</p>
</div> </div>
</div> </div>

View file

@ -82,20 +82,20 @@
<p class="text-muted small">The author of this version of the Egg. Uploading a new Egg configuration from a different author will change this.</p> <p class="text-muted small">The author of this version of the Egg. Uploading a new Egg configuration from a different author will change this.</p>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="pDockerImage" class="control-label">Docker Image <span class="field-required"></span></label> <label for="pDockerImage" class="control-label">Docker Images <span class="field-required"></span></label>
<input type="text" id="pDockerImage" name="docker_image" value="{{ $egg->docker_image }}" class="form-control" /> <textarea id="pDockerImages" name="docker_images" class="form-control" rows="4">{{ implode("\n", $egg->docker_images) }}</textarea>
<p class="text-muted small">The default docker image that should be used for new servers using this Egg. This can be changed per-server as needed.</p> <p class="text-muted small">The docker images available to servers using this egg. Enter one per line. Users will be able to select from this list of images if more than one value is provided.</p>
</div> </div>
</div> </div>
<div class="col-sm-6"> <div class="col-sm-6">
<div class="form-group"> <div class="form-group">
<label for="pDescription" class="control-label">Description</label> <label for="pDescription" class="control-label">Description</label>
<textarea id="pDescription" name="description" class="form-control" rows="6">{{ $egg->description }}</textarea> <textarea id="pDescription" name="description" class="form-control" rows="8">{{ $egg->description }}</textarea>
<p class="text-muted small">A description of this Egg that will be displayed throughout the Panel as needed.</p> <p class="text-muted small">A description of this Egg that will be displayed throughout the Panel as needed.</p>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="pStartup" class="control-label">Startup Command <span class="field-required"></span></label> <label for="pStartup" class="control-label">Startup Command <span class="field-required"></span></label>
<textarea id="pStartup" name="startup" class="form-control" rows="6">{{ $egg->startup }}</textarea> <textarea id="pStartup" name="startup" class="form-control" rows="8">{{ $egg->startup }}</textarea>
<p class="text-muted small">The default startup command that should be used for new servers using this Egg.</p> <p class="text-muted small">The default startup command that should be used for new servers using this Egg.</p>
</div> </div>
</div> </div>

View file

@ -265,8 +265,9 @@
<div class="box-body row"> <div class="box-body row">
<div class="form-group col-xs-12"> <div class="form-group col-xs-12">
<label for="pDefaultContainer">Docker Image</label> <label for="pDefaultContainer">Docker Image</label>
<input id="pDefaultContainer" name="image" value="{{ old('image') }}" class="form-control" /> <select id="pDefaultContainer" name="image" class="form-control"></select>
<p class="small text-muted no-margin">This is the default Docker image that will be used to run this server.</p> <input id="pDefaultContainerCustom" name="custom_image" value="{{ old('custom_image') }}" class="form-control" placeholder="Or enter a custom image..." style="margin-top:1rem"/>
<p class="small text-muted no-margin">This is the default Docker image that will be used to run this server. Select an image from the dropdown above, or enter a custom image in the text field above.</p>
</div> </div>
</div> </div>
</div> </div>
@ -323,11 +324,14 @@
@endforeach @endforeach
@endif @endif
@endif @endif
@if(old('image'))
$('#pDefaultContainer').val('{{ old('image') }}');
@endif
} }
// END Persist 'Service Variables' // END Persist 'Service Variables'
</script> </script>
{!! Theme::js('js/admin/new-server.js?v=20201003') !!} {!! Theme::js('js/admin/new-server.js?v=20201212') !!}
<script type="application/javascript"> <script type="application/javascript">
$(document).ready(function() { $(document).ready(function() {

View file

@ -89,13 +89,14 @@
</div> </div>
<div class="box"> <div class="box">
<div class="box-header with-border"> <div class="box-header with-border">
<h3 class="box-title">Docker Container Configuration</h3> <h3 class="box-title">Docker Image Configuration</h3>
</div> </div>
<div class="box-body"> <div class="box-body">
<div class="form-group"> <div class="form-group">
<label for="pDockerImage" class="control-label">Image</label> <label for="pDockerImage">Image</label>
<input type="text" name="docker_image" id="pDockerImage" value="{{ $server->image }}" class="form-control" /> <select id="pDockerImage" name="docker_image" class="form-control"></select>
<p class="text-muted small">The Docker image to use for this server. The default image for the selected egg is <code id="setDefaultImage"></code>.</p> <input id="pDockerImageCustom" name="custom_docker_image" value="{{ old('custom_docker_image') }}" class="form-control" placeholder="Or enter a custom image..." style="margin-top:1rem"/>
<p class="small text-muted no-margin">This is the Docker image that will be used to run this server. Select an image from the dropdown or enter a custom image in the text field above.</p>
</div> </div>
</div> </div>
</div> </div>
@ -117,10 +118,25 @@
var parentChain = _.get(Pterodactyl.nests, $("#pNestId").val()); var parentChain = _.get(Pterodactyl.nests, $("#pNestId").val());
var objectChain = _.get(parentChain, 'eggs.' + selectedEgg); var objectChain = _.get(parentChain, 'eggs.' + selectedEgg);
$('#setDefaultImage').html(_.get(objectChain, 'docker_image', 'undefined')); $('#setDefaultImage').html(_.get(objectChain, 'docker_images.0', 'undefined'));
$('#pDockerImage').val(_.get(objectChain, 'docker_image', 'undefined')); const images = _.get(objectChain, 'docker_images', [])
for (let i = 0; i < images.length; i++) {
let opt = document.createElement('option');
opt.value = images[i];
opt.innerHTML = images[i];
if (objectChain.id === parseInt(Pterodactyl.server.egg_id) && Pterodactyl.server.image == opt.value) {
opt.checked = true
}
$('#pDockerImage').append(opt);
}
$('#pDockerImage').on('change', function () {
$('#pDockerImageCustom').val('');
})
if (objectChain.id === parseInt(Pterodactyl.server.egg_id)) { if (objectChain.id === parseInt(Pterodactyl.server.egg_id)) {
$('#pDockerImage').val(Pterodactyl.server.image); if ($('#pDockerImage').val() != Pterodactyl.server.image) {
$('#pDockerImageCustom').val(Pterodactyl.server.image);
}
} }
if (!_.get(objectChain, 'startup', false)) { if (!_.get(objectChain, 'startup', false)) {

View file

@ -66,6 +66,7 @@ Route::group(['prefix' => '/servers/{server}', 'middleware' => [AuthenticateServ
Route::post('/delete', 'Servers\FileController@delete'); Route::post('/delete', 'Servers\FileController@delete');
Route::post('/create-folder', 'Servers\FileController@create'); Route::post('/create-folder', 'Servers\FileController@create');
Route::post('/chmod', 'Servers\FileController@chmod'); Route::post('/chmod', 'Servers\FileController@chmod');
Route::post('/pull', 'Servers\FileController@pull');
Route::get('/upload', 'Servers\FileUploadController'); Route::get('/upload', 'Servers\FileUploadController');
}); });
@ -114,5 +115,6 @@ Route::group(['prefix' => '/servers/{server}', 'middleware' => [AuthenticateServ
Route::group(['prefix' => '/settings'], function () { Route::group(['prefix' => '/settings'], function () {
Route::post('/rename', 'Servers\SettingsController@rename'); Route::post('/rename', 'Servers\SettingsController@rename');
Route::post('/reinstall', 'Servers\SettingsController@reinstall'); Route::post('/reinstall', 'Servers\SettingsController@reinstall');
Route::put('/docker-image', 'Servers\SettingsController@dockerImage');
}); });
}); });

View file

@ -1,59 +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\Commands;
use Tests\TestCase;
use Illuminate\Console\Command;
use Illuminate\Contracts\Foundation\Application;
use Symfony\Component\Console\Tester\CommandTester;
abstract class CommandTestCase extends TestCase
{
/**
* @var bool
*/
protected $commandIsInteractive = true;
/**
* Set a command to be non-interactive for testing purposes.
*
* @return $this
*/
public function withoutInteraction()
{
$this->commandIsInteractive = false;
return $this;
}
/**
* Return the display from running a command.
*
* @param \Illuminate\Console\Command $command
* @param array $args
* @param array $inputs
* @param array $opts
* @return string
*/
protected function runCommand(Command $command, array $args = [], array $inputs = [], array $opts = [])
{
if (! $command->getLaravel() instanceof Application) {
$command->setLaravel($this->app);
}
$response = new CommandTester($command);
$response->setInputs($inputs);
$opts = array_merge($opts, ['interactive' => $this->commandIsInteractive]);
$response->execute($args, $opts);
return $response->getDisplay();
}
}

View file

@ -1,275 +0,0 @@
<?php
namespace Tests\Unit\Commands\Environment;
use Mockery as m;
use Tests\Unit\Commands\CommandTestCase;
use Illuminate\Contracts\Config\Repository;
use Pterodactyl\Console\Commands\Environment\EmailSettingsCommand;
class EmailSettingsCommandTest extends CommandTestCase
{
/**
* @var \Pterodactyl\Console\Commands\Environment\EmailSettingsCommand|\Mockery\Mock
*/
protected $command;
/**
* @var \Illuminate\Contracts\Config\Repository|\Mockery\Mock
*/
protected $config;
/**
* Setup tests.
*/
public function setUp(): void
{
parent::setUp();
$this->config = m::mock(Repository::class);
$this->command = m::mock(EmailSettingsCommand::class . '[call, writeToEnvironment]', [$this->config]);
$this->command->setLaravel($this->app);
}
/**
* Test selection of the SMTP driver with no options passed.
*/
public function testSmtpDriverSelection()
{
// 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);
}
/**
* Test that the command can run when all variables are passed in as options.
*/
public function testSmtpDriverSelectionWithOptionsPassed()
{
$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->withoutInteraction()->runCommand($this->command, [
'--driver' => $data['MAIL_DRIVER'],
'--email' => $data['MAIL_FROM'],
'--from' => $data['MAIL_FROM_NAME'],
'--encryption' => $data['MAIL_ENCRYPTION'],
'--host' => $data['MAIL_HOST'],
'--port' => $data['MAIL_PORT'],
'--username' => $data['MAIL_USERNAME'],
'--password' => $data['MAIL_PASSWORD'],
]);
$this->assertNotEmpty($display);
$this->assertStringContainsString('Updating stored environment configuration file.', $display);
}
/**
* Test selection of PHP mail() as the driver.
*/
public function testPHPMailDriverSelection()
{
$data = [
'MAIL_DRIVER' => 'mail',
'MAIL_FROM' => 'mail@from.com',
'MAIL_FROM_NAME' => 'MailName',
'MAIL_ENCRYPTION' => 'tls',
];
$this->setupCoreFunctions($data);
// The driver flag is passed because there seems to be some issue with the command tester
// when using a choice() method when two keys start with the same letters.
//
// In this case, mail and mailgun.
unset($data['MAIL_DRIVER']);
$display = $this->runCommand($this->command, ['--driver' => 'mail'], array_values($data));
$this->assertNotEmpty($display);
$this->assertStringContainsString('Updating stored environment configuration file.', $display);
}
/**
* Test selection of the Mailgun driver with no options passed.
*/
public function testMailgunDriverSelection()
{
$data = [
'MAIL_DRIVER' => 'mailgun',
'MAILGUN_DOMAIN' => 'domain.com',
'MAILGUN_SECRET' => '123456',
'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);
}
/**
* Test mailgun driver selection when variables are passed as options.
*/
public function testMailgunDriverSelectionWithOptionsPassed()
{
$data = [
'MAIL_DRIVER' => 'mailgun',
'MAILGUN_DOMAIN' => 'domain.com',
'MAILGUN_SECRET' => '123456',
'MAIL_FROM' => 'mail@from.com',
'MAIL_FROM_NAME' => 'MailName',
'MAIL_ENCRYPTION' => 'tls',
];
$this->setupCoreFunctions($data);
$display = $this->withoutInteraction()->runCommand($this->command, [
'--driver' => $data['MAIL_DRIVER'],
'--email' => $data['MAIL_FROM'],
'--from' => $data['MAIL_FROM_NAME'],
'--encryption' => $data['MAIL_ENCRYPTION'],
'--host' => $data['MAILGUN_DOMAIN'],
'--password' => $data['MAILGUN_SECRET'],
]);
$this->assertNotEmpty($display);
$this->assertStringContainsString('Updating stored environment configuration file.', $display);
}
/**
* Test selection of the Mandrill driver with no options passed.
*/
public function testMandrillDriverSelection()
{
$data = [
'MAIL_DRIVER' => 'mandrill',
'MANDRILL_SECRET' => '123456',
'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);
}
/**
* Test mandrill driver selection when variables are passed as options.
*/
public function testMandrillDriverSelectionWithOptionsPassed()
{
$data = [
'MAIL_DRIVER' => 'mandrill',
'MANDRILL_SECRET' => '123456',
'MAIL_FROM' => 'mail@from.com',
'MAIL_FROM_NAME' => 'MailName',
'MAIL_ENCRYPTION' => 'tls',
];
$this->setupCoreFunctions($data);
$display = $this->withoutInteraction()->runCommand($this->command, [
'--driver' => $data['MAIL_DRIVER'],
'--email' => $data['MAIL_FROM'],
'--from' => $data['MAIL_FROM_NAME'],
'--encryption' => $data['MAIL_ENCRYPTION'],
'--password' => $data['MANDRILL_SECRET'],
]);
$this->assertNotEmpty($display);
$this->assertStringContainsString('Updating stored environment configuration file.', $display);
}
/**
* Test selection of the Postmark driver with no options passed.
*/
public function testPostmarkDriverSelection()
{
$data = [
'MAIL_DRIVER' => 'smtp',
'MAIL_HOST' => 'smtp.postmarkapp.com',
'MAIL_PORT' => '587',
'MAIL_USERNAME' => '123456',
'MAIL_PASSWORD' => '123456',
'MAIL_FROM' => 'mail@from.com',
'MAIL_FROM_NAME' => 'MailName',
'MAIL_ENCRYPTION' => 'tls',
];
$this->setupCoreFunctions($data);
$display = $this->runCommand($this->command, [], [
'postmark', '123456', $data['MAIL_FROM'], $data['MAIL_FROM_NAME'], $data['MAIL_ENCRYPTION'],
]);
$this->assertNotEmpty($display);
$this->assertStringContainsString('Updating stored environment configuration file.', $display);
}
/**
* Test postmark driver selection when variables are passed as options.
*/
public function testPostmarkDriverSelectionWithOptionsPassed()
{
$data = [
'MAIL_DRIVER' => 'smtp',
'MAIL_HOST' => 'smtp.postmarkapp.com',
'MAIL_PORT' => '587',
'MAIL_USERNAME' => '123456',
'MAIL_PASSWORD' => '123456',
'MAIL_FROM' => 'mail@from.com',
'MAIL_FROM_NAME' => 'MailName',
'MAIL_ENCRYPTION' => 'tls',
];
$this->setupCoreFunctions($data);
$display = $this->withoutInteraction()->runCommand($this->command, [
'--driver' => 'postmark',
'--email' => $data['MAIL_FROM'],
'--from' => $data['MAIL_FROM_NAME'],
'--encryption' => $data['MAIL_ENCRYPTION'],
'--username' => $data['MAIL_USERNAME'],
]);
$this->assertNotEmpty($display);
$this->assertStringContainsString('Updating stored environment configuration file.', $display);
}
/**
* Setup the core functions that are repeated across all of these tests.
*
* @param array $data
*/
private function setupCoreFunctions(array $data)
{
$this->config->shouldReceive('get')->withAnyArgs()->zeroOrMoreTimes()->andReturnNull();
$this->command->shouldReceive('writeToEnvironment')->with($data)->once()->andReturnNull();
}
}

View file

@ -1,128 +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\Commands\Location;
use Mockery as m;
use Pterodactyl\Models\Location;
use Tests\Unit\Commands\CommandTestCase;
use Pterodactyl\Services\Locations\LocationDeletionService;
use Pterodactyl\Console\Commands\Location\DeleteLocationCommand;
use Pterodactyl\Contracts\Repository\LocationRepositoryInterface;
class DeleteLocationCommandTest extends CommandTestCase
{
/**
* @var \Pterodactyl\Console\Commands\Location\DeleteLocationCommand
*/
protected $command;
/**
* @var \Pterodactyl\Services\Locations\LocationDeletionService|\Mockery\Mock
*/
protected $deletionService;
/**
* @var \Pterodactyl\Contracts\Repository\LocationRepositoryInterface|\Mockery\Mock
*/
protected $repository;
/**
* Setup tests.
*/
public function setUp(): void
{
parent::setUp();
$this->deletionService = m::mock(LocationDeletionService::class);
$this->repository = m::mock(LocationRepositoryInterface::class);
$this->command = new DeleteLocationCommand($this->deletionService, $this->repository);
$this->command->setLaravel($this->app);
}
/**
* Test that a location can be deleted.
*/
public function testLocationIsDeleted()
{
$locations = collect([
$location1 = factory(Location::class)->make(),
$location2 = factory(Location::class)->make(),
]);
$this->repository->shouldReceive('all')->withNoArgs()->once()->andReturn($locations);
$this->deletionService->shouldReceive('handle')->with($location2->id)->once()->andReturnNull();
$display = $this->runCommand($this->command, [], [$location2->short]);
$this->assertNotEmpty($display);
$this->assertStringContainsString(trans('command/messages.location.deleted'), $display);
}
/**
* Test that a location is deleted if passed in as an option.
*/
public function testLocationIsDeletedIfPassedInOption()
{
$locations = collect([
$location1 = factory(Location::class)->make(),
$location2 = factory(Location::class)->make(),
]);
$this->repository->shouldReceive('all')->withNoArgs()->once()->andReturn($locations);
$this->deletionService->shouldReceive('handle')->with($location2->id)->once()->andReturnNull();
$display = $this->withoutInteraction()->runCommand($this->command, [
'--short' => $location2->short,
]);
$this->assertNotEmpty($display);
$this->assertStringContainsString(trans('command/messages.location.deleted'), $display);
}
/**
* Test that prompt shows back up if the user enters the wrong parameters.
*/
public function testInteractiveEnvironmentAllowsReAttemptingSearch()
{
$locations = collect([
$location1 = factory(Location::class)->make(),
$location2 = factory(Location::class)->make(),
]);
$this->repository->shouldReceive('all')->withNoArgs()->once()->andReturn($locations);
$this->deletionService->shouldReceive('handle')->with($location2->id)->once()->andReturnNull();
$display = $this->runCommand($this->command, [], ['123_not_exist', 'another_not_exist', $location2->short]);
$this->assertNotEmpty($display);
$this->assertStringContainsString(trans('command/messages.location.no_location_found'), $display);
$this->assertStringContainsString(trans('command/messages.location.deleted'), $display);
}
/**
* Test that no re-attempt is performed in a non-interactive environment.
*/
public function testNonInteractiveEnvironmentThrowsErrorIfNoLocationIsFound()
{
$locations = collect([
$location1 = factory(Location::class)->make(),
$location2 = factory(Location::class)->make(),
]);
$this->repository->shouldReceive('all')->withNoArgs()->once()->andReturn($locations);
$this->deletionService->shouldNotReceive('handle');
$display = $this->withoutInteraction()->runCommand($this->command, ['--short' => 'randomTestString']);
$this->assertNotEmpty($display);
$this->assertStringContainsString(trans('command/messages.location.no_location_found'), $display);
}
}

View file

@ -1,87 +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\Commands\Location;
use Mockery as m;
use Pterodactyl\Models\Location;
use Tests\Unit\Commands\CommandTestCase;
use Pterodactyl\Services\Locations\LocationCreationService;
use Pterodactyl\Console\Commands\Location\MakeLocationCommand;
class MakeLocationCommandTest extends CommandTestCase
{
/**
* @var \Pterodactyl\Console\Commands\Location\MakeLocationCommand
*/
protected $command;
/**
* @var \Pterodactyl\Services\Locations\LocationCreationService|\Mockery\Mock
*/
protected $creationService;
/**
* Setup tests.
*/
public function setUp(): void
{
parent::setUp();
$this->creationService = m::mock(LocationCreationService::class);
$this->command = new MakeLocationCommand($this->creationService);
$this->command->setLaravel($this->app);
}
/**
* Test that a location can be created when no options are passed.
*/
public function testLocationIsCreatedWithNoOptionsPassed()
{
$location = factory(Location::class)->make();
$this->creationService->shouldReceive('handle')->with([
'short' => $location->short,
'long' => $location->long,
])->once()->andReturn($location);
$display = $this->runCommand($this->command, [], [$location->short, $location->long]);
$this->assertNotEmpty($display);
$this->assertStringContainsString(trans('command/messages.location.created', [
'name' => $location->short,
'id' => $location->id,
]), $display);
}
/**
* Test that a location is created when options are passed.
*/
public function testLocationIsCreatedWhenOptionsArePassed()
{
$location = factory(Location::class)->make();
$this->creationService->shouldReceive('handle')->with([
'short' => $location->short,
'long' => $location->long,
])->once()->andReturn($location);
$display = $this->withoutInteraction()->runCommand($this->command, [
'--short' => $location->short,
'--long' => $location->long,
]);
$this->assertNotEmpty($display);
$this->assertStringContainsString(trans('command/messages.location.created', [
'name' => $location->short,
'id' => $location->id,
]), $display);
}
}

View file

@ -1,87 +0,0 @@
<?php
namespace Tests\Unit\Commands\Maintenance;
use SplFileInfo;
use Mockery as m;
use Carbon\Carbon;
use Tests\Unit\Commands\CommandTestCase;
use Illuminate\Contracts\Filesystem\Factory;
use Illuminate\Contracts\Filesystem\Filesystem;
use Pterodactyl\Console\Commands\Maintenance\CleanServiceBackupFilesCommand;
class CleanServiceBackupFilesCommandTest extends CommandTestCase
{
/**
* @var \Pterodactyl\Console\Commands\Maintenance\CleanServiceBackupFilesCommand
*/
protected $command;
/**
* @var \Illuminate\Contracts\Filesystem\Filesystem|\Mockery\Mock
*/
protected $disk;
/**
* @var \Illuminate\Contracts\Filesystem\Factory|\Mockery\Mock
*/
protected $filesystem;
/**
* Setup tests.
*/
public function setUp(): void
{
parent::setUp();
Carbon::setTestNow(Carbon::now());
$this->disk = m::mock(Filesystem::class);
$this->filesystem = m::mock(Factory::class);
$this->filesystem->shouldReceive('disk')->withNoArgs()->once()->andReturn($this->disk);
}
/**
* Test that a file is deleted if it is > 5min old.
*/
public function testCommandCleansFilesMoreThan5MinutesOld()
{
$file = new SplFileInfo('testfile.txt');
$this->disk->shouldReceive('files')->with('services/.bak')->once()->andReturn([$file]);
$this->disk->shouldReceive('lastModified')->with($file->getPath())->once()->andReturn(Carbon::now()->subDays(100)->getTimestamp());
$this->disk->shouldReceive('delete')->with($file->getPath())->once()->andReturnNull();
$display = $this->runCommand($this->getCommand());
$this->assertNotEmpty($display);
$this->assertStringContainsString(trans('command/messages.maintenance.deleting_service_backup', ['file' => 'testfile.txt']), $display);
}
/**
* Test that a file isn't deleted if it is < 5min old.
*/
public function testCommandDoesNotCleanFileLessThan5MinutesOld()
{
$file = new SplFileInfo('testfile.txt');
$this->disk->shouldReceive('files')->with('services/.bak')->once()->andReturn([$file]);
$this->disk->shouldReceive('lastModified')->with($file->getPath())->once()->andReturn(Carbon::now()->getTimestamp());
$display = $this->runCommand($this->getCommand());
$this->assertEmpty($display);
}
/**
* Return an instance of the command for testing.
*
* @return \Pterodactyl\Console\Commands\Maintenance\CleanServiceBackupFilesCommand
*/
private function getCommand(): CleanServiceBackupFilesCommand
{
$command = new CleanServiceBackupFilesCommand($this->filesystem);
$command->setLaravel($this->app);
return $command;
}
}

View file

@ -1,83 +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\Commands\User;
use Mockery as m;
use Pterodactyl\Models\User;
use Tests\Unit\Commands\CommandTestCase;
use Pterodactyl\Contracts\Repository\UserRepositoryInterface;
use Pterodactyl\Console\Commands\User\DisableTwoFactorCommand;
class DisableTwoFactorCommandTest extends CommandTestCase
{
/**
* @var \Pterodactyl\Console\Commands\User\DisableTwoFactorCommand
*/
protected $command;
/**
* @var \Pterodactyl\Contracts\Repository\UserRepositoryInterface|\Mockery\Mock
*/
protected $repository;
/**
* Setup tests.
*/
public function setUp(): void
{
parent::setUp();
$this->repository = m::mock(UserRepositoryInterface::class);
$this->command = new DisableTwoFactorCommand($this->repository);
$this->command->setLaravel($this->app);
}
/**
* Test 2-factor auth is disabled when no option is passed.
*/
public function testTwoFactorIsDisabledWhenNoOptionIsPassed()
{
$user = factory(User::class)->make();
$this->repository->shouldReceive('setColumns')->with(['id', 'email'])->once()->andReturnSelf()
->shouldReceive('findFirstWhere')->with([['email', '=', $user->email]])->once()->andReturn($user);
$this->repository->shouldReceive('withoutFreshModel')->withNoArgs()->once()->andReturnSelf()
->shouldReceive('update')->with($user->id, [
'use_totp' => false,
'totp_secret' => null,
])->once()->andReturnNull();
$display = $this->runCommand($this->command, [], [$user->email]);
$this->assertNotEmpty($display);
$this->assertStringContainsString(trans('command/messages.user.2fa_disabled', ['email' => $user->email]), $display);
}
/**
* Test 2-factor auth is disabled when user is passed in option.
*/
public function testTwoFactorIsDisabledWhenOptionIsPassed()
{
$user = factory(User::class)->make();
$this->repository->shouldReceive('setColumns')->with(['id', 'email'])->once()->andReturnSelf()
->shouldReceive('findFirstWhere')->with([['email', '=', $user->email]])->once()->andReturn($user);
$this->repository->shouldReceive('withoutFreshModel')->withNoArgs()->once()->andReturnSelf()
->shouldReceive('update')->with($user->id, [
'use_totp' => false,
'totp_secret' => null,
])->once()->andReturnNull();
$display = $this->withoutInteraction()->runCommand($this->command, ['--email' => $user->email]);
$this->assertNotEmpty($display);
$this->assertStringContainsString(trans('command/messages.user.2fa_disabled', ['email' => $user->email]), $display);
}
}

View file

@ -1,132 +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\Commands\User;
use Mockery as m;
use Pterodactyl\Models\User;
use Tests\Unit\Commands\CommandTestCase;
use Pterodactyl\Services\Users\UserCreationService;
use Pterodactyl\Console\Commands\User\MakeUserCommand;
class MakeUserCommandTest extends CommandTestCase
{
/**
* @var \Pterodactyl\Console\Commands\User\MakeUserCommand
*/
protected $command;
/**
* @var \Pterodactyl\Services\Users\UserCreationService
*/
protected $creationService;
/**
* Setup tests.
*/
public function setUp(): void
{
parent::setUp();
$this->creationService = m::mock(UserCreationService::class);
$this->command = new MakeUserCommand($this->creationService);
$this->command->setLaravel($this->app);
}
/**
* Test that the command executes if no options are passed.
*/
public function testCommandWithNoPassedOptions()
{
// TODO(dane): fix this
$this->markTestSkipped('Skipped, GitHub actions cannot run successfully.');
// $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);
}
/**
* Test that the --no-password flag works as intended.
*/
public function testCommandWithNoPasswordOption()
{
$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' => null,
'root_admin' => $user->root_admin,
])->once()->andReturn($user);
$display = $this->runCommand($this->command, ['--no-password' => true], [
'yes', $user->email, $user->username, $user->name_first, $user->name_last,
]);
$this->assertNotEmpty($display);
$this->assertStringNotContainsString(trans('command/messages.user.ask_password_help'), $display);
}
/**
* Test command when arguments are passed as flags.
*/
public function testCommandWithOptionsPassed()
{
$user = factory(User::class)->make(['root_admin' => false]);
$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->withoutInteraction()->runCommand($this->command, [
'--email' => $user->email,
'--username' => $user->username,
'--name-first' => $user->name_first,
'--name-last' => $user->name_last,
'--password' => 'Password123',
'--admin' => 0,
]);
$this->assertNotEmpty($display);
$this->assertStringNotContainsString(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('No', $display);
}
}

View file

@ -1,143 +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\Http\Controllers\Admin;
use Mockery as m;
use Tests\TestCase;
use Pterodactyl\Models\DatabaseHost;
use Prologue\Alerts\AlertsMessageBag;
use Illuminate\Pagination\LengthAwarePaginator;
use Tests\Assertions\ControllerAssertionsTrait;
use Pterodactyl\Http\Controllers\Admin\DatabaseController;
use Pterodactyl\Services\Databases\Hosts\HostUpdateService;
use Pterodactyl\Services\Databases\Hosts\HostCreationService;
use Pterodactyl\Services\Databases\Hosts\HostDeletionService;
use Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface;
use Pterodactyl\Contracts\Repository\LocationRepositoryInterface;
use Pterodactyl\Contracts\Repository\DatabaseHostRepositoryInterface;
class DatabaseControllerTest extends TestCase
{
use ControllerAssertionsTrait;
/**
* @var \Prologue\Alerts\AlertsMessageBag|\Mockery\Mock
*/
private $alert;
/**
* @var \Pterodactyl\Services\Databases\Hosts\HostCreationService|\Mockery\Mock
*/
private $creationService;
/**
* @var \Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface|\Mockery\Mock
*/
private $databaseRepository;
/**
* @var \Pterodactyl\Services\Databases\Hosts\HostDeletionService|\Mockery\Mock
*/
private $deletionService;
/**
* @var \Pterodactyl\Contracts\Repository\LocationRepositoryInterface|\Mockery\Mock
*/
private $locationRepository;
/**
* @var \Pterodactyl\Contracts\Repository\DatabaseHostRepositoryInterface|\Mockery\Mock
*/
private $repository;
/**
* @var \Pterodactyl\Services\Databases\Hosts\HostUpdateService|\Mockery\Mock
*/
private $updateService;
/**
* Setup tests.
*/
public function setUp(): void
{
parent::setUp();
$this->alert = m::mock(AlertsMessageBag::class);
$this->creationService = m::mock(HostCreationService::class);
$this->databaseRepository = m::mock(DatabaseRepositoryInterface::class);
$this->deletionService = m::mock(HostDeletionService::class);
$this->locationRepository = m::mock(LocationRepositoryInterface::class);
$this->repository = m::mock(DatabaseHostRepositoryInterface::class);
$this->updateService = m::mock(HostUpdateService::class);
}
/**
* Test the index controller.
*/
public function testIndexController()
{
$this->locationRepository->shouldReceive('getAllWithNodes')->withNoArgs()->once()->andReturn(collect(['getAllWithNodes']));
$this->repository->shouldReceive('getWithViewDetails')->withNoArgs()->once()->andReturn(collect(['getWithViewDetails']));
$response = $this->getController()->index();
$this->assertIsViewResponse($response);
$this->assertViewNameEquals('admin.databases.index', $response);
$this->assertViewHasKey('locations', $response);
$this->assertViewHasKey('hosts', $response);
$this->assertViewKeyEquals('locations', collect(['getAllWithNodes']), $response);
$this->assertViewKeyEquals('hosts', collect(['getWithViewDetails']), $response);
}
/**
* Test the view controller for displaying a specific database host.
*/
public function testViewController()
{
$model = factory(DatabaseHost::class)->make();
$paginator = new LengthAwarePaginator([], 1, 1);
$this->locationRepository->shouldReceive('getAllWithNodes')->withNoArgs()->once()->andReturn(collect(['getAllWithNodes']));
$this->repository->shouldReceive('find')->with(1)->once()->andReturn($model);
$this->databaseRepository->shouldReceive('getDatabasesForHost')
->once()
->with(1)
->andReturn($paginator);
$response = $this->getController()->view(1);
$this->assertIsViewResponse($response);
$this->assertViewNameEquals('admin.databases.view', $response);
$this->assertViewHasKey('locations', $response);
$this->assertViewHasKey('host', $response);
$this->assertViewHasKey('databases', $response);
$this->assertViewKeyEquals('locations', collect(['getAllWithNodes']), $response);
$this->assertViewKeyEquals('host', $model, $response);
$this->assertViewKeyEquals('databases', $paginator, $response);
}
/**
* Return an instance of the DatabaseController with mock dependencies.
*
* @return \Pterodactyl\Http\Controllers\Admin\DatabaseController
*/
private function getController(): DatabaseController
{
return new DatabaseController(
$this->alert,
$this->repository,
$this->databaseRepository,
$this->creationService,
$this->deletionService,
$this->updateService,
$this->locationRepository
);
}
}

View file

@ -1,82 +0,0 @@
<?php
namespace Tests\Unit\Http\Controllers;
use Mockery as m;
use Prologue\Alerts\AlertsMessageBag;
use Illuminate\Contracts\Console\Kernel;
use Illuminate\Contracts\Encryption\Encrypter;
use Illuminate\Contracts\Config\Repository as ConfigRepository;
use Pterodactyl\Http\Controllers\Admin\Settings\MailController;
use Pterodactyl\Contracts\Repository\SettingsRepositoryInterface;
class MailControllerTest extends ControllerTestCase
{
/**
* @var \Prologue\Alerts\AlertsMessageBag
*/
private $alert;
/**
* @var \Illuminate\Contracts\Config\Repository
*/
private $configRepository;
/**
* @var \Illuminate\Contracts\Encryption\Encrypter
*/
private $encrypter;
/**
* @var \Illuminate\Contracts\Console\Kernel
*/
private $kernel;
/**
* @var \Pterodactyl\Contracts\Repository\SettingsRepositoryInterface
*/
private $settingsRepositoryInterface;
/**
* Setup tests.
*/
public function setUp(): void
{
parent::setUp();
$this->alert = m::mock(AlertsMessageBag::class);
$this->configRepository = m::mock(ConfigRepository::class);
$this->encrypter = m::mock(Encrypter::class);
$this->kernel = m::mock(Kernel::class);
$this->settingsRepositoryInterface = m::mock(SettingsRepositoryInterface::class);
}
/**
* Test the mail controller for viewing mail settings page.
*/
public function testIndex()
{
$this->configRepository->shouldReceive('get');
$response = $this->getController()->index();
$this->assertIsViewResponse($response);
$this->assertViewNameEquals('admin.settings.mail', $response);
}
/**
* Prepare a MailController using our mocks.
*
* @return MailController
*/
public function getController()
{
return new MailController(
$this->alert,
$this->configRepository,
$this->encrypter,
$this->kernel,
$this->settingsRepositoryInterface
);
}
}

View file

@ -1,99 +0,0 @@
<?php
namespace Tests\Unit\Http\Controllers;
use Mockery as m;
use Tests\TestCase;
use Tests\Traits\Http\RequestMockHelpers;
use Tests\Assertions\ControllerAssertionsTrait;
abstract class ControllerTestCase extends TestCase
{
use ControllerAssertionsTrait, RequestMockHelpers;
/**
* @var \Pterodactyl\Http\Controllers\Controller|\Mockery\Mock
*/
private $controller;
/**
* Setup tests.
*/
public function setUp(): void
{
parent::setUp();
$this->buildRequestMock();
}
/**
* Set an instance of the controller.
*
* @param \Pterodactyl\Http\Controllers\Controller|\Mockery\Mock $controller
*/
public function setControllerInstance($controller)
{
$this->controller = $controller;
}
/**
* Return an instance of the controller.
*
* @return \Mockery\Mock|\Pterodactyl\Http\Controllers\Controller
*/
public function getControllerInstance()
{
return $this->controller;
}
/**
* Helper function to mock injectJavascript requests.
*
* @param array|null $args
* @param bool $subset
*/
protected function mockInjectJavascript(array $args = null, bool $subset = false)
{
$controller = $this->getControllerInstance();
$controller->shouldReceive('setRequest')->with($this->request)->once()->andReturnSelf();
if (is_null($args)) {
$controller->shouldReceive('injectJavascript')->withAnyArgs()->once()->andReturnNull();
} else {
$with = $subset ? m::subset($args) : $args;
$controller->shouldReceive('injectJavascript')->with($with)->once()->andReturnNull();
}
}
/**
* Mocks a request input call.
*
* @param string $param
* @param mixed $return
*/
protected function mockRequestInput(string $param, $return = null)
{
$this->request->shouldReceive('input')->withArgs(function ($k) use ($param) {
return $k === $param;
})->andReturn($return);
}
/**
* Build and return a mocked controller instance to use for testing.
*
* @param string $class
* @param array $args
* @return \Mockery\Mock|\Pterodactyl\Http\Controllers\Controller
*/
protected function buildMockedController(string $class, array $args = [])
{
$controller = m::mock($class, $args)->makePartial();
if (is_null($this->getControllerInstance())) {
$this->setControllerInstance($controller);
}
return $this->getControllerInstance();
}
}

View file

@ -1,60 +0,0 @@
<?php
namespace Tests\Unit\Services\Allocations;
use Mockery as m;
use Tests\TestCase;
use Pterodactyl\Models\Allocation;
use Pterodactyl\Services\Allocations\AllocationDeletionService;
use Pterodactyl\Contracts\Repository\AllocationRepositoryInterface;
use Pterodactyl\Exceptions\Service\Allocation\ServerUsingAllocationException;
class AllocationDeletionServiceTest extends TestCase
{
/**
* @var \Pterodactyl\Contracts\Repository\AllocationRepositoryInterface|\Mockery\Mock
*/
private $repository;
public function setUp(): void
{
parent::setUp();
$this->repository = m::mock(AllocationRepositoryInterface::class);
}
/**
* Test that an allocation is deleted.
*/
public function testAllocationIsDeleted()
{
$model = factory(Allocation::class)->make(['id' => 123]);
$this->repository->expects('delete')->with($model->id)->andReturns(1);
$response = $this->getService()->handle($model);
$this->assertEquals(1, $response);
}
/**
* Test that an exception gets thrown if an allocation is currently assigned to a server.
*/
public function testExceptionThrownIfAssignedToServer()
{
$this->expectException(ServerUsingAllocationException::class);
$model = factory(Allocation::class)->make(['server_id' => 123]);
$this->getService()->handle($model);
}
/**
* Return an instance of the service with mocked injections.
*
* @return \Pterodactyl\Services\Allocations\AllocationDeletionService
*/
private function getService(): AllocationDeletionService
{
return new AllocationDeletionService($this->repository);
}
}

View file

@ -1,291 +0,0 @@
<?php
namespace Tests\Unit\Services\Allocations;
use Mockery as m;
use Tests\TestCase;
use Pterodactyl\Models\Node;
use Illuminate\Database\ConnectionInterface;
use Pterodactyl\Services\Allocations\AssignmentService;
use Pterodactyl\Contracts\Repository\AllocationRepositoryInterface;
use Pterodactyl\Exceptions\Service\Allocation\CidrOutOfRangeException;
use Pterodactyl\Exceptions\Service\Allocation\PortOutOfRangeException;
use Pterodactyl\Exceptions\Service\Allocation\InvalidPortMappingException;
use Pterodactyl\Exceptions\Service\Allocation\TooManyPortsInRangeException;
class AssignmentServiceTest extends TestCase
{
/**
* @var \Illuminate\Database\ConnectionInterface|\Mockery\Mock
*/
protected $connection;
/**
* @var \Pterodactyl\Models\Node
*/
protected $node;
/**
* @var \Pterodactyl\Contracts\Repository\AllocationRepositoryInterface|\Mockery\Mock
*/
protected $repository;
/**
* Setup tests.
*/
public function setUp(): void
{
parent::setUp();
$this->node = factory(Node::class)->make();
$this->connection = m::mock(ConnectionInterface::class);
$this->repository = m::mock(AllocationRepositoryInterface::class);
}
/**
* Test a non-CIDR notated IP address without a port range.
*/
public function testIndividualIpAddressWithoutRange()
{
$data = [
'allocation_ip' => '192.168.1.1',
'allocation_ports' => ['2222'],
];
$this->connection->shouldReceive('beginTransaction')->once()->withNoArgs()->andReturnNull();
$this->repository->shouldReceive('insertIgnore')->with([
[
'node_id' => $this->node->id,
'ip' => '192.168.1.1',
'port' => 2222,
'ip_alias' => null,
'server_id' => null,
],
])->once()->andReturn(true);
$this->connection->shouldReceive('commit')->once()->withNoArgs()->andReturnNull();
$this->getService()->handle($this->node, $data);
}
/**
* Test a non-CIDR IP address with a port range provided.
*/
public function testIndividualIpAddressWithRange()
{
$data = [
'allocation_ip' => '192.168.1.1',
'allocation_ports' => ['1025-1027'],
];
$this->connection->shouldReceive('beginTransaction')->once()->withNoArgs()->andReturnNull();
$this->repository->shouldReceive('insertIgnore')->once()->with([
[
'node_id' => $this->node->id,
'ip' => '192.168.1.1',
'port' => 1025,
'ip_alias' => null,
'server_id' => null,
],
[
'node_id' => $this->node->id,
'ip' => '192.168.1.1',
'port' => 1026,
'ip_alias' => null,
'server_id' => null,
],
[
'node_id' => $this->node->id,
'ip' => '192.168.1.1',
'port' => 1027,
'ip_alias' => null,
'server_id' => null,
],
])->andReturn(true);
$this->connection->shouldReceive('commit')->once()->withNoArgs()->andReturnNull();
$this->getService()->handle($this->node, $data);
}
/**
* Test a non-CIDR IP address with a single port and an alias.
*/
public function testIndividualIPAddressWithAlias()
{
$data = [
'allocation_ip' => '192.168.1.1',
'allocation_ports' => ['2222'],
'allocation_alias' => 'my.alias.net',
];
$this->connection->shouldReceive('beginTransaction')->once()->withNoArgs()->andReturnNull();
$this->repository->shouldReceive('insertIgnore')->once()->with([
[
'node_id' => $this->node->id,
'ip' => '192.168.1.1',
'port' => 2222,
'ip_alias' => 'my.alias.net',
'server_id' => null,
],
])->andReturn(true);
$this->connection->shouldReceive('commit')->once()->withNoArgs()->andReturnNull();
$this->getService()->handle($this->node, $data);
}
/**
* Test that a domain name can be passed in place of an IP address.
*/
public function testDomainNamePassedInPlaceOfIPAddress()
{
$data = [
'allocation_ip' => 'unit-test-static.pterodactyl.io',
'allocation_ports' => ['2222'],
];
$this->connection->shouldReceive('beginTransaction')->once()->withNoArgs()->andReturnNull();
$this->repository->shouldReceive('insertIgnore')->once()->with([
[
'node_id' => $this->node->id,
'ip' => '127.0.0.1',
'port' => 2222,
'ip_alias' => null,
'server_id' => null,
],
])->andReturn(true);
$this->connection->shouldReceive('commit')->once()->withNoArgs()->andReturnNull();
$this->getService()->handle($this->node, $data);
}
/**
* Test that a CIDR IP address without a range works properly.
*/
public function testCIDRNotatedIPAddressWithoutRange()
{
$data = [
'allocation_ip' => '192.168.1.100/31',
'allocation_ports' => ['2222'],
];
$this->connection->shouldReceive('beginTransaction')->once()->withNoArgs()->andReturnNull();
$this->repository->shouldReceive('insertIgnore')->once()->with([
[
'node_id' => $this->node->id,
'ip' => '192.168.1.100',
'port' => 2222,
'ip_alias' => null,
'server_id' => null,
],
])->andReturn(true);
$this->repository->shouldReceive('insertIgnore')->once()->with([
[
'node_id' => $this->node->id,
'ip' => '192.168.1.101',
'port' => 2222,
'ip_alias' => null,
'server_id' => null,
],
])->andReturn(true);
$this->connection->shouldReceive('commit')->once()->withNoArgs()->andReturnNull();
$this->getService()->handle($this->node, $data);
}
/**
* Test that a CIDR IP address with a range works properly.
*/
public function testCIDRNotatedIPAddressOutsideRangeLimit()
{
$this->expectException(CidrOutOfRangeException::class);
$this->expectExceptionMessage('CIDR notation only allows masks between /25 and /32.');
$data = [
'allocation_ip' => '192.168.1.100/20',
'allocation_ports' => ['2222'],
];
$this->getService()->handle($this->node, $data);
}
/**
* Test that an exception is thrown if there are too many ports.
*/
public function testAllocationWithPortsExceedingLimit()
{
$this->expectException(TooManyPortsInRangeException::class);
$this->expectExceptionMessage('Adding more than 1000 ports in a single range at once is not supported.');
$data = [
'allocation_ip' => '192.168.1.1',
'allocation_ports' => ['5000-7000'],
];
$this->connection->shouldReceive('beginTransaction')->once()->withNoArgs()->andReturnNull();
$this->getService()->handle($this->node, $data);
}
/**
* Test that an exception is thrown if an invalid port is provided.
*/
public function testInvalidPortProvided()
{
$this->expectException(InvalidPortMappingException::class);
$this->expectExceptionMessage('The mapping provided for test123 was invalid and could not be processed.');
$data = [
'allocation_ip' => '192.168.1.1',
'allocation_ports' => ['test123'],
];
$this->connection->shouldReceive('beginTransaction')->once()->withNoArgs()->andReturnNull();
$this->getService()->handle($this->node, $data);
}
/**
* Test that ports outside of defined limits throw an error.
*
* @param array $ports
*
* @dataProvider invalidPortsDataProvider
*/
public function testPortRangeOutsideOfRangeLimits(array $ports)
{
$this->expectException(PortOutOfRangeException::class);
$this->expectExceptionMessage('Ports in an allocation must be greater than 1024 and less than or equal to 65535.');
$data = ['allocation_ip' => '192.168.1.1', 'allocation_ports' => $ports];
$this->connection->shouldReceive('beginTransaction')->once()->withNoArgs()->andReturnNull();
$this->getService()->handle($this->node, $data);
}
/**
* Provide ports and ranges of ports that exceed the viable port limits for the software.
*
* @return array
*/
public function invalidPortsDataProvider(): array
{
return [
[['65536']],
[['1024']],
[['1000']],
[['0']],
[['65530-65540']],
[['65540-65560']],
[[PHP_INT_MAX]],
];
}
/**
* Returns an instance of the service with mocked dependencies for testing.
*
* @return \Pterodactyl\Services\Allocations\AssignmentService
*/
private function getService(): AssignmentService
{
return new AssignmentService($this->repository, $this->connection);
}
}

View file

@ -1,92 +0,0 @@
<?php
namespace Tests\Unit\Services\Databases;
use Mockery as m;
use Tests\TestCase;
use Pterodactyl\Models\Database;
use Illuminate\Database\ConnectionInterface;
use Illuminate\Contracts\Encryption\Encrypter;
use Pterodactyl\Extensions\DynamicDatabaseConnection;
use Pterodactyl\Services\Databases\DatabasePasswordService;
use Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface;
class DatabasePasswordServiceTest extends TestCase
{
/**
* @var \Illuminate\Database\ConnectionInterface|\Mockery\Mock
*/
private $connection;
/**
* @var \Pterodactyl\Extensions\DynamicDatabaseConnection|\Mockery\Mock
*/
private $dynamic;
/**
* @var \Illuminate\Contracts\Encryption\Encrypter|\Mockery\Mock
*/
private $encrypter;
/**
* @var \Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface|\Mockery\Mock
*/
private $repository;
/**
* Setup tests.
*/
public function setUp(): void
{
parent::setUp();
$this->connection = m::mock(ConnectionInterface::class);
$this->dynamic = m::mock(DynamicDatabaseConnection::class);
$this->encrypter = m::mock(Encrypter::class);
$this->repository = m::mock(DatabaseRepositoryInterface::class);
}
/**
* Test that a password can be updated.
*/
public function testPasswordIsChanged()
{
/** @var \Pterodactyl\Models\Database $model */
$model = factory(Database::class)->make(['max_connections' => 0]);
$this->connection->expects('transaction')->with(m::on(function ($closure) {
return is_null($closure());
}));
$this->dynamic->expects('set')->with('dynamic', $model->database_host_id)->andReturnNull();
$this->encrypter->expects('encrypt')->with(m::on(function ($string) {
preg_match_all('/[!@+=.^-]/', $string, $matches, PREG_SET_ORDER);
$this->assertTrue(count($matches) >= 2 && count($matches) <= 6, "Failed asserting that [{$string}] contains 2 to 6 special characters.");
$this->assertTrue(strlen($string) === 24, "Failed asserting that [{$string}] is 24 characters in length.");
return true;
}))->andReturn('enc123');
$this->repository->expects('withoutFreshModel')->withNoArgs()->andReturnSelf();
$this->repository->expects('update')->with($model->id, ['password' => 'enc123'])->andReturn(true);
$this->repository->expects('dropUser')->with($model->username, $model->remote)->andReturn(true);
$this->repository->expects('createUser')->with($model->username, $model->remote, m::any(), 0)->andReturn(true);
$this->repository->expects('assignUserToDatabase')->with($model->database, $model->username, $model->remote)->andReturn(true);
$this->repository->expects('flush')->withNoArgs()->andReturn(true);
$response = $this->getService()->handle($model);
$this->assertNotEmpty($response);
}
/**
* Return an instance of the service with mocked dependencies.
*
* @return \Pterodactyl\Services\Databases\DatabasePasswordService
*/
private function getService(): DatabasePasswordService
{
return new DatabasePasswordService($this->connection, $this->repository, $this->dynamic, $this->encrypter);
}
}

View file

@ -1,103 +0,0 @@
<?php
namespace Tests\Unit\Services\Databases\Hosts;
use Mockery as m;
use Tests\TestCase;
use Pterodactyl\Models\DatabaseHost;
use Illuminate\Database\DatabaseManager;
use Illuminate\Database\ConnectionInterface;
use Illuminate\Contracts\Encryption\Encrypter;
use Pterodactyl\Extensions\DynamicDatabaseConnection;
use Pterodactyl\Services\Databases\Hosts\HostCreationService;
use Pterodactyl\Contracts\Repository\DatabaseHostRepositoryInterface;
class HostCreationServiceTest extends TestCase
{
/**
* @var \Illuminate\Database\ConnectionInterface|\Mockery\Mock
*/
private $connection;
/**
* @var \Illuminate\Database\DatabaseManager|\Mockery\Mock
*/
private $databaseManager;
/**
* @var \Pterodactyl\Extensions\DynamicDatabaseConnection|\Mockery\Mock
*/
private $dynamic;
/**
* @var \Illuminate\Contracts\Encryption\Encrypter|\Mockery\Mock
*/
private $encrypter;
/**
* @var \Pterodactyl\Contracts\Repository\DatabaseHostRepositoryInterface|\Mockery\Mock
*/
private $repository;
/**
* Setup tests.
*/
public function setUp(): void
{
parent::setUp();
$this->connection = m::mock(ConnectionInterface::class);
$this->databaseManager = m::mock(DatabaseManager::class);
$this->dynamic = m::mock(DynamicDatabaseConnection::class);
$this->encrypter = m::mock(Encrypter::class);
$this->repository = m::mock(DatabaseHostRepositoryInterface::class);
}
/**
* Test that a database host can be created.
*/
public function testDatabaseHostIsCreated()
{
$model = factory(DatabaseHost::class)->make();
$this->connection->expects('transaction')->with(m::on(function ($closure) {
return ! is_null($closure());
}))->andReturn($model);
$this->encrypter->expects('encrypt')->with('test123')->andReturn('enc123');
$this->repository->expects('create')->with(m::subset([
'password' => 'enc123',
'username' => $model->username,
'node_id' => $model->node_id,
]))->andReturn($model);
$this->dynamic->expects('set')->with('dynamic', $model)->andReturnNull();
$this->databaseManager->expects('connection')->with('dynamic')->andReturnSelf();
$this->databaseManager->expects('select')->with('SELECT 1 FROM dual')->andReturnNull();
$response = $this->getService()->handle([
'password' => 'test123',
'username' => $model->username,
'node_id' => $model->node_id,
]);
$this->assertNotEmpty($response);
$this->assertSame($model, $response);
}
/**
* Return an instance of the service with mocked dependencies.
*
* @return \Pterodactyl\Services\Databases\Hosts\HostCreationService
*/
private function getService(): HostCreationService
{
return new HostCreationService(
$this->connection,
$this->databaseManager,
$this->repository,
$this->dynamic,
$this->encrypter
);
}
}

View file

@ -1,85 +0,0 @@
<?php
namespace Tests\Unit\Services\Databases\Hosts;
use Mockery as m;
use Tests\TestCase;
use Pterodactyl\Exceptions\PterodactylException;
use Pterodactyl\Exceptions\Service\HasActiveServersException;
use Pterodactyl\Services\Databases\Hosts\HostDeletionService;
use Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface;
use Pterodactyl\Contracts\Repository\DatabaseHostRepositoryInterface;
class HostDeletionServiceTest extends TestCase
{
/**
* @var \Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface|\Mockery\Mock
*/
private $databaseRepository;
/**
* @var \Pterodactyl\Contracts\Repository\DatabaseHostRepositoryInterface|\Mockery\Mock
*/
private $repository;
/**
* Setup tests.
*/
public function setUp(): void
{
parent::setUp();
$this->databaseRepository = m::mock(DatabaseRepositoryInterface::class);
$this->repository = m::mock(DatabaseHostRepositoryInterface::class);
}
/**
* Test that a host can be deleted.
*/
public function testHostIsDeleted()
{
$this->databaseRepository->shouldReceive('findCountWhere')->with([['database_host_id', '=', 1234]])->once()->andReturn(0);
$this->repository->shouldReceive('delete')->with(1234)->once()->andReturn(1);
$response = $this->getService()->handle(1234);
$this->assertNotEmpty($response);
$this->assertSame(1, $response);
}
/**
* Test that an exception is thrown if a host with databases is deleted.
*
* @dataProvider databaseCountDataProvider
*/
public function testExceptionIsThrownIfDeletingHostWithDatabases(int $count)
{
$this->databaseRepository->shouldReceive('findCountWhere')->with([['database_host_id', '=', 1234]])->once()->andReturn($count);
try {
$this->getService()->handle(1234);
} catch (PterodactylException $exception) {
$this->assertInstanceOf(HasActiveServersException::class, $exception);
$this->assertEquals(trans('exceptions.databases.delete_has_databases'), $exception->getMessage());
}
}
/**
* Data provider to ensure exceptions are thrown for any value > 0.
*
* @return array
*/
public function databaseCountDataProvider(): array
{
return [[1], [2], [10]];
}
/**
* Return an instance of the service with mocked dependencies.
*
* @return \Pterodactyl\Services\Databases\Hosts\HostDeletionService
*/
private function getService(): HostDeletionService
{
return new HostDeletionService($this->databaseRepository, $this->repository);
}
}

View file

@ -1,114 +0,0 @@
<?php
namespace Tests\Unit\Services\Databases\Hosts;
use Mockery as m;
use Tests\TestCase;
use Pterodactyl\Models\DatabaseHost;
use Illuminate\Database\DatabaseManager;
use Illuminate\Database\ConnectionInterface;
use Illuminate\Contracts\Encryption\Encrypter;
use Pterodactyl\Extensions\DynamicDatabaseConnection;
use Pterodactyl\Services\Databases\Hosts\HostUpdateService;
use Pterodactyl\Contracts\Repository\DatabaseHostRepositoryInterface;
class HostUpdateServiceTest extends TestCase
{
/**
* @var \Illuminate\Database\ConnectionInterface|\Mockery\Mock
*/
private $connection;
/**
* @var \Illuminate\Database\DatabaseManager|\Mockery\Mock
*/
private $databaseManager;
/**
* @var \Pterodactyl\Extensions\DynamicDatabaseConnection|\Mockery\Mock
*/
private $dynamic;
/**
* @var \Illuminate\Contracts\Encryption\Encrypter|\Mockery\Mock
*/
private $encrypter;
/**
* @var \Pterodactyl\Contracts\Repository\DatabaseHostRepositoryInterface|\Mockery\Mock
*/
private $repository;
/**
* Setup tests.
*/
public function setUp(): void
{
parent::setUp();
$this->connection = m::mock(ConnectionInterface::class);
$this->databaseManager = m::mock(DatabaseManager::class);
$this->dynamic = m::mock(DynamicDatabaseConnection::class);
$this->encrypter = m::mock(Encrypter::class);
$this->repository = m::mock(DatabaseHostRepositoryInterface::class);
}
/**
* Test that a password is encrypted before storage if provided.
*/
public function testPasswordIsEncryptedWhenProvided()
{
$model = factory(DatabaseHost::class)->make();
$this->connection->expects('transaction')->with(m::on(function ($closure) {
return ! is_null($closure());
}))->andReturn($model);
$this->encrypter->expects('encrypt')->with('test123')->andReturn('enc123');
$this->repository->expects('update')->with(1234, ['password' => 'enc123'])->andReturn($model);
$this->dynamic->expects('set')->with('dynamic', $model)->andReturnNull();
$this->databaseManager->expects('connection')->with('dynamic')->andReturnSelf();
$this->databaseManager->expects('select')->with('SELECT 1 FROM dual')->andReturnNull();
$response = $this->getService()->handle(1234, ['password' => 'test123']);
$this->assertNotEmpty($response);
$this->assertSame($model, $response);
}
/**
* Test that updates still occur when no password is provided.
*/
public function testUpdateOccursWhenNoPasswordIsProvided()
{
$model = factory(DatabaseHost::class)->make();
$this->connection->expects('transaction')->with(m::on(function ($closure) {
return ! is_null($closure());
}))->andReturn($model);
$this->repository->expects('update')->with(1234, ['username' => 'test'])->andReturn($model);
$this->dynamic->expects('set')->with('dynamic', $model)->andReturnNull();
$this->databaseManager->expects('connection')->with('dynamic')->andReturnSelf();
$this->databaseManager->expects('select')->with('SELECT 1 FROM dual')->andReturnNull();
$response = $this->getService()->handle(1234, ['password' => '', 'username' => 'test']);
$this->assertNotEmpty($response);
$this->assertSame($model, $response);
}
/**
* Return an instance of the service with mocked dependencies.
*
* @return \Pterodactyl\Services\Databases\Hosts\HostUpdateService
*/
private function getService(): HostUpdateService
{
return new HostUpdateService(
$this->connection,
$this->databaseManager,
$this->repository,
$this->dynamic,
$this->encrypter
);
}
}

View file

@ -1,145 +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\Services\Options;
use Mockery as m;
use Tests\TestCase;
use Pterodactyl\Models\Egg;
use Tests\Traits\MocksUuids;
use Illuminate\Contracts\Config\Repository;
use Pterodactyl\Exceptions\PterodactylException;
use Pterodactyl\Services\Eggs\EggCreationService;
use Pterodactyl\Contracts\Repository\EggRepositoryInterface;
use Pterodactyl\Exceptions\Service\Egg\NoParentConfigurationFoundException;
class EggCreationServiceTest extends TestCase
{
use MocksUuids;
/**
* @var \Illuminate\Contracts\Config\Repository|\Mockery\Mock
*/
protected $config;
/**
* @var \Pterodactyl\Contracts\Repository\EggRepositoryInterface|\Mockery\Mock
*/
protected $repository;
/**
* @var \Pterodactyl\Services\Eggs\EggCreationService
*/
protected $service;
/**
* Setup tests.
*/
public function setUp(): void
{
parent::setUp();
$this->config = m::mock(Repository::class);
$this->repository = m::mock(EggRepositoryInterface::class);
$this->service = new EggCreationService($this->config, $this->repository);
}
/**
* Test that a new model is created when not using the config from attribute.
*/
public function testCreateNewModelWithoutUsingConfigFrom()
{
$model = factory(Egg::class)->make();
$this->config->shouldReceive('get')->with('pterodactyl.service.author')->once()->andReturn('test@example.com');
$this->repository->shouldReceive('create')->with([
'uuid' => $this->getKnownUuid(),
'author' => 'test@example.com',
'config_from' => null,
'name' => $model->name,
], true, true)->once()->andReturn($model);
$response = $this->service->handle(['name' => $model->name]);
$this->assertNotEmpty($response);
$this->assertNull(object_get($response, 'config_from'));
$this->assertEquals($model->name, $response->name);
}
/**
* Test that a new model is created when using the config from attribute.
*/
public function testCreateNewModelUsingConfigFrom()
{
$model = factory(Egg::class)->make();
$this->repository->shouldReceive('findCountWhere')->with([
['nest_id', '=', $model->nest_id],
['id', '=', 12345],
])->once()->andReturn(1);
$this->config->shouldReceive('get')->with('pterodactyl.service.author')->once()->andReturn('test@example.com');
$this->repository->shouldReceive('create')->with([
'nest_id' => $model->nest_id,
'config_from' => 12345,
'uuid' => $this->getKnownUuid(),
'author' => 'test@example.com',
], true, true)->once()->andReturn($model);
$response = $this->service->handle([
'nest_id' => $model->nest_id,
'config_from' => 12345,
]);
$this->assertNotEmpty($response);
$this->assertEquals($response, $model);
}
/**
* Test that certain data, such as the UUID or author takes priority over data
* that is passed into the function.
*/
public function testDataProvidedByHandlerTakesPriorityOverPassedData()
{
$model = factory(Egg::class)->make();
$this->config->shouldReceive('get')->with('pterodactyl.service.author')->once()->andReturn('test@example.com');
$this->repository->shouldReceive('create')->with([
'uuid' => $this->getKnownUuid(),
'author' => 'test@example.com',
'config_from' => null,
'name' => $model->name,
], true, true)->once()->andReturn($model);
$response = $this->service->handle(['name' => $model->name, 'uuid' => 'should-be-ignored', 'author' => 'should-be-ignored']);
$this->assertNotEmpty($response);
$this->assertNull(object_get($response, 'config_from'));
$this->assertEquals($model->name, $response->name);
}
/**
* Test that an exception is thrown if no parent configuration can be located.
*/
public function testExceptionIsThrownIfNoParentConfigurationIsFound()
{
$this->repository->shouldReceive('findCountWhere')->with([
['nest_id', '=', null],
['id', '=', 1],
])->once()->andReturn(0);
try {
$this->service->handle(['config_from' => 1]);
} catch (PterodactylException $exception) {
$this->assertInstanceOf(NoParentConfigurationFoundException::class, $exception);
$this->assertEquals(trans('exceptions.nest.egg.must_be_child'), $exception->getMessage());
}
}
}

View file

@ -1,93 +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\Services\Options;
use Mockery as m;
use Tests\TestCase;
use Pterodactyl\Exceptions\PterodactylException;
use Pterodactyl\Services\Eggs\EggDeletionService;
use Pterodactyl\Contracts\Repository\EggRepositoryInterface;
use Pterodactyl\Exceptions\Service\Egg\HasChildrenException;
use Pterodactyl\Exceptions\Service\HasActiveServersException;
use Pterodactyl\Contracts\Repository\ServerRepositoryInterface;
class EggDeletionServiceTest extends TestCase
{
/**
* @var \Pterodactyl\Contracts\Repository\EggRepositoryInterface|\Mockery\Mock
*/
protected $repository;
/**
* @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface|\Mockery\Mock
*/
protected $serverRepository;
/**
* @var \Pterodactyl\Services\Eggs\EggDeletionService
*/
protected $service;
/**
* Setup tests.
*/
public function setUp(): void
{
parent::setUp();
$this->repository = m::mock(EggRepositoryInterface::class);
$this->serverRepository = m::mock(ServerRepositoryInterface::class);
$this->service = new EggDeletionService($this->serverRepository, $this->repository);
}
/**
* Test that Egg is deleted if no servers are found.
*/
public function testEggIsDeletedIfNoServersAreFound()
{
$this->serverRepository->shouldReceive('findCountWhere')->with([['egg_id', '=', 1]])->once()->andReturn(0);
$this->repository->shouldReceive('findCountWhere')->with([['config_from', '=', 1]])->once()->andReturn(0);
$this->repository->shouldReceive('delete')->with(1)->once()->andReturn(1);
$this->assertEquals(1, $this->service->handle(1));
}
/**
* Test that Egg is not deleted if servers are found.
*/
public function testExceptionIsThrownIfServersAreFound()
{
$this->serverRepository->shouldReceive('findCountWhere')->with([['egg_id', '=', 1]])->once()->andReturn(1);
try {
$this->service->handle(1);
} catch (PterodactylException $exception) {
$this->assertInstanceOf(HasActiveServersException::class, $exception);
$this->assertEquals(trans('exceptions.nest.egg.delete_has_servers'), $exception->getMessage());
}
}
/**
* Test that an exception is thrown if children Eggs exist.
*/
public function testExceptionIsThrownIfChildrenArePresent()
{
$this->serverRepository->shouldReceive('findCountWhere')->with([['egg_id', '=', 1]])->once()->andReturn(0);
$this->repository->shouldReceive('findCountWhere')->with([['config_from', '=', 1]])->once()->andReturn(1);
try {
$this->service->handle(1);
} catch (PterodactylException $exception) {
$this->assertInstanceOf(HasChildrenException::class, $exception);
$this->assertEquals(trans('exceptions.nest.egg.has_children'), $exception->getMessage());
}
}
}

View file

@ -1,91 +0,0 @@
<?php
namespace Tests\Unit\Services\Services\Options;
use Mockery as m;
use Tests\TestCase;
use Pterodactyl\Models\Egg;
use Pterodactyl\Services\Eggs\EggUpdateService;
use Pterodactyl\Exceptions\PterodactylException;
use Pterodactyl\Contracts\Repository\EggRepositoryInterface;
use Pterodactyl\Exceptions\Service\Egg\NoParentConfigurationFoundException;
class EggUpdateServiceTest extends TestCase
{
/**
* @var \Pterodactyl\Models\Egg
*/
protected $model;
/**
* @var \Pterodactyl\Contracts\Repository\EggRepositoryInterface|\Mockery\Mock
*/
protected $repository;
/**
* @var \Pterodactyl\Services\Eggs\EggUpdateService
*/
protected $service;
/**
* Setup tests.
*/
public function setUp(): void
{
parent::setUp();
$this->model = factory(Egg::class)->make(['id' => 123]);
$this->repository = m::mock(EggRepositoryInterface::class);
$this->service = new EggUpdateService($this->repository);
}
/**
* Test that an Egg is updated when no config_from attribute is passed.
*/
public function testEggIsUpdatedWhenNoConfigFromIsProvided()
{
$this->repository->shouldReceive('withoutFreshModel->update')
->with($this->model->id, ['test_field' => 'field_value'])->once()->andReturnNull();
$this->service->handle($this->model, ['test_field' => 'field_value']);
$this->assertTrue(true);
}
/**
* Test that Egg is updated when a valid config_from attribute is passed.
*/
public function testOptionIsUpdatedWhenValidConfigFromIsPassed()
{
$this->repository->shouldReceive('findCountWhere')->with([
['nest_id', '=', $this->model->nest_id],
['id', '=', 1],
])->once()->andReturn(1);
$this->repository->shouldReceive('withoutFreshModel->update')
->with($this->model->id, ['config_from' => 1])->once()->andReturnNull();
$this->service->handle($this->model, ['config_from' => 1]);
$this->assertTrue(true);
}
/**
* Test that an exception is thrown if an invalid config_from attribute is passed.
*/
public function testExceptionIsThrownIfInvalidParentConfigIsPassed()
{
$this->repository->shouldReceive('findCountWhere')->with([
['nest_id', '=', $this->model->nest_id],
['id', '=', 1],
])->once()->andReturn(0);
try {
$this->service->handle($this->model, ['config_from' => 1]);
} catch (PterodactylException $exception) {
$this->assertInstanceOf(NoParentConfigurationFoundException::class, $exception);
$this->assertEquals(trans('exceptions.nest.egg.must_be_child'), $exception->getMessage());
}
}
}

View file

@ -1,87 +0,0 @@
<?php
namespace Tests\Unit\Services\Eggs\Scripts;
use Exception;
use Mockery as m;
use Tests\TestCase;
use Pterodactyl\Models\Egg;
use Pterodactyl\Services\Eggs\Scripts\InstallScriptService;
use Pterodactyl\Contracts\Repository\EggRepositoryInterface;
use Pterodactyl\Exceptions\Service\Egg\InvalidCopyFromException;
class InstallScriptServiceTest extends TestCase
{
/**
* @var array
*/
protected $data = [
'script_install' => 'test-script',
'script_is_privileged' => true,
'script_entry' => '/bin/bash',
'script_container' => 'ubuntu',
'copy_script_from' => null,
];
/**
* @var \Pterodactyl\Contracts\Repository\EggRepositoryInterface|\Mockery\Mock
*/
protected $repository;
/**
* Setup tests.
*/
public function setUp(): void
{
parent::setUp();
$this->repository = m::mock(EggRepositoryInterface::class);
}
/**
* Test that passing a new copy_script_from attribute works properly.
*/
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, $model->nest_id)->once()->andReturn(true);
$this->repository->expects('withoutFreshModel->update')->with($model->id, $this->data)->andReturnNull();
$this->getService()->handle($model, $this->data);
}
/**
* Test that an exception gets raised when the script is not copyable.
*/
public function testUpdateWithInvalidCopyScriptFromAttribute()
{
$this->data['copy_script_from'] = 1;
$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);
}
/**
* Test standard functionality.
*/
public function testUpdateWithoutNewCopyScriptFromAttribute()
{
$model = factory(Egg::class)->make(['id' => 123, 'nest_id' => 456]);
$this->repository->expects('withoutFreshModel->update')->with($model->id, $this->data)->andReturnNull();
$this->getService()->handle($model, $this->data);
}
private function getService()
{
return new InstallScriptService($this->repository);
}
}

View file

@ -1,69 +0,0 @@
<?php
namespace Tests\Unit\Services\Eggs\Sharing;
use Mockery as m;
use Carbon\Carbon;
use Tests\TestCase;
use Pterodactyl\Models\Egg;
use Pterodactyl\Models\EggVariable;
use Tests\Assertions\NestedObjectAssertionsTrait;
use Pterodactyl\Services\Eggs\Sharing\EggExporterService;
use Pterodactyl\Contracts\Repository\EggRepositoryInterface;
class EggExporterServiceTest extends TestCase
{
use NestedObjectAssertionsTrait;
/**
* @var \Pterodactyl\Contracts\Repository\EggRepositoryInterface|\Mockery\Mock
*/
protected $repository;
/**
* Setup tests.
*/
public function setUp(): void
{
parent::setUp();
Carbon::setTestNow(Carbon::now());
$this->repository = m::mock(EggRepositoryInterface::class);
}
/**
* Test that a JSON structure is returned.
*/
public function testJsonStructureIsExported()
{
$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);
$service = new EggExporterService($this->repository);
$response = $service->handle($egg->id);
$this->assertNotEmpty($response);
$data = json_decode($response);
$this->assertEquals(JSON_ERROR_NONE, json_last_error());
$this->assertObjectHasNestedAttribute('meta.version', $data);
$this->assertObjectNestedValueEquals('meta.version', 'PTDL_v1', $data);
$this->assertObjectHasNestedAttribute('author', $data);
$this->assertObjectNestedValueEquals('author', $egg->author, $data);
$this->assertObjectHasNestedAttribute('exported_at', $data);
$this->assertObjectNestedValueEquals('exported_at', Carbon::now()->toIso8601String(), $data);
$this->assertObjectHasNestedAttribute('scripts.installation.script', $data);
$this->assertObjectHasNestedAttribute('scripts.installation.container', $data);
$this->assertObjectHasNestedAttribute('scripts.installation.entrypoint', $data);
$this->assertObjectHasAttribute('variables', $data);
$this->assertArrayHasKey('0', $data->variables);
$this->assertObjectHasAttribute('name', $data->variables[0]);
$this->assertObjectNestedValueEquals('name', $variable->name, $data->variables[0]);
}
}

View file

@ -1,187 +0,0 @@
<?php
namespace Tests\Unit\Services\Eggs\Sharing;
use Mockery as m;
use Tests\TestCase;
use Pterodactyl\Models\Egg;
use Pterodactyl\Models\Nest;
use Tests\Traits\MocksUuids;
use Illuminate\Http\UploadedFile;
use Pterodactyl\Models\EggVariable;
use Illuminate\Database\ConnectionInterface;
use Pterodactyl\Services\Eggs\Sharing\EggImporterService;
use Pterodactyl\Contracts\Repository\EggRepositoryInterface;
use Pterodactyl\Contracts\Repository\NestRepositoryInterface;
use Pterodactyl\Exceptions\Service\Egg\BadJsonFormatException;
use Pterodactyl\Exceptions\Service\InvalidFileUploadException;
use Pterodactyl\Contracts\Repository\EggVariableRepositoryInterface;
class EggImporterServiceTest extends TestCase
{
use MocksUuids;
/**
* @var \Illuminate\Database\ConnectionInterface|\Mockery\Mock
*/
protected $connection;
/**
* @var \Pterodactyl\Contracts\Repository\EggVariableRepositoryInterface|\Mockery\Mock
*/
protected $eggVariableRepository;
/**
* @var \Illuminate\Http\UploadedFile|\Mockery\Mock
*/
protected $file;
/**
* @var \Pterodactyl\Contracts\Repository\NestRepositoryInterface|\Mockery\Mock
*/
protected $nestRepository;
/**
* @var \Pterodactyl\Contracts\Repository\EggRepositoryInterface|\Mockery\Mock
*/
protected $repository;
/**
* @var \Pterodactyl\Services\Eggs\Sharing\EggImporterService
*/
protected $service;
/**
* Setup tests.
*/
public function setUp(): void
{
parent::setUp();
$this->file = m::mock(UploadedFile::class);
$this->connection = m::mock(ConnectionInterface::class);
$this->eggVariableRepository = m::mock(EggVariableRepositoryInterface::class);
$this->nestRepository = m::mock(NestRepositoryInterface::class);
$this->repository = m::mock(EggRepositoryInterface::class);
$this->service = new EggImporterService(
$this->connection, $this->repository, $this->eggVariableRepository, $this->nestRepository
);
}
/**
* Test that a service option can be successfully imported.
*/
public function testEggConfigurationIsImported()
{
$egg = factory(Egg::class)->make(['id' => 123]);
$nest = factory(Nest::class)->make(['id' => 456]);
$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,
'variables' => [
$variable = factory(EggVariable::class)->make(),
],
]));
$this->nestRepository->shouldReceive('getWithEggs')->with($nest->id)->once()->andReturn($nest);
$this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull();
$this->repository->shouldReceive('create')->with(m::subset([
'uuid' => $this->getKnownUuid(),
'nest_id' => $nest->id,
'name' => $egg->name,
]), true, true)->once()->andReturn($egg);
$this->eggVariableRepository->shouldReceive('create')->with(m::subset([
'egg_id' => $egg->id,
'env_variable' => $variable->env_variable,
]))->once()->andReturnNull();
$this->connection->shouldReceive('commit')->withNoArgs()->once()->andReturnNull();
$response = $this->service->handle($this->file, $nest->id);
$this->assertNotEmpty($response);
$this->assertInstanceOf(Egg::class, $response);
$this->assertSame($egg, $response);
}
/**
* Test that an exception is thrown if the file is invalid.
*/
public function testExceptionIsThrownIfFileIsInvalid()
{
$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);
}
/**
* Test that an exception is thrown if the file is not a file.
*/
public function testExceptionIsThrownIfFileIsNotAFile()
{
$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)'
);
$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);
}
/**
* Test that an exception is thrown if the JSON metadata is invalid.
*/
public function testExceptionIsThrownIfJsonMetaDataIsInvalid()
{
$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'],
]));
$this->service->handle($this->file, 1234);
}
/**
* Test that an exception is thrown if bad JSON is provided.
*/
public function testExceptionIsThrownIfBadJsonIsProvided()
{
$this->expectException(BadJsonFormatException::class);
$this->expectExceptionMessage(trans('exceptions.nest.importer.json_error', [
'error' => 'Syntax error',
]));
$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

@ -1,219 +0,0 @@
<?php
namespace Tests\Unit\Services\Eggs\Sharing;
use Mockery as m;
use Tests\TestCase;
use Pterodactyl\Models\Egg;
use Illuminate\Http\UploadedFile;
use Pterodactyl\Models\EggVariable;
use Illuminate\Database\ConnectionInterface;
use Pterodactyl\Exceptions\PterodactylException;
use Pterodactyl\Contracts\Repository\EggRepositoryInterface;
use Pterodactyl\Exceptions\Service\Egg\BadJsonFormatException;
use Pterodactyl\Exceptions\Service\InvalidFileUploadException;
use Pterodactyl\Services\Eggs\Sharing\EggUpdateImporterService;
use Pterodactyl\Contracts\Repository\EggVariableRepositoryInterface;
class EggUpdateImporterServiceTest extends TestCase
{
/**
* @var \Illuminate\Database\ConnectionInterface|\Mockery\Mock
*/
protected $connection;
/**
* @var \Illuminate\Http\UploadedFile|\Mockery\Mock
*/
protected $file;
/**
* @var \Pterodactyl\Contracts\Repository\EggRepositoryInterface|\Mockery\Mock
*/
protected $repository;
/**
* @var \Pterodactyl\Services\Eggs\Sharing\EggUpdateImporterService
*/
protected $service;
/**
* @var \Pterodactyl\Contracts\Repository\EggVariableRepositoryInterface|\Mockery\Mock
*/
protected $variableRepository;
/**
* Setup tests.
*/
public function setUp(): void
{
parent::setUp();
$this->connection = m::mock(ConnectionInterface::class);
$this->file = m::mock(UploadedFile::class);
$this->repository = m::mock(EggRepositoryInterface::class);
$this->variableRepository = m::mock(EggVariableRepositoryInterface::class);
$this->service = new EggUpdateImporterService($this->connection, $this->repository, $this->variableRepository);
}
/**
* Test that an egg update is handled correctly using an uploaded file.
*/
public function testEggIsUpdated()
{
$egg = factory(Egg::class)->make(['id' => 123]);
$variable = factory(EggVariable::class)->make();
$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([
'meta' => ['version' => 'PTDL_v1'],
'name' => $egg->name,
'author' => 'newauthor@example.com',
'variables' => [$variable->toArray()],
]));
$this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull();
$this->repository->shouldReceive('update')->with($egg->id, m::subset([
'author' => 'newauthor@example.com',
'name' => $egg->name,
]), true, true)->once()->andReturn($egg);
$this->variableRepository->shouldReceive('withoutFreshModel->updateOrCreate')->with([
'egg_id' => $egg->id,
'env_variable' => $variable->env_variable,
], collect($variable)->except(['egg_id', 'env_variable'])->toArray())->once()->andReturnNull();
$this->variableRepository->shouldReceive('setColumns')->with(['id', 'env_variable'])->once()->andReturnSelf()
->shouldReceive('findWhere')->with([['egg_id', '=', $egg->id]])->once()->andReturn(collect([$variable]));
$this->connection->shouldReceive('commit')->withNoArgs()->once()->andReturnNull();
$this->service->handle($egg, $this->file);
$this->assertTrue(true);
}
/**
* Test that an imported file with less variables than currently existing deletes
* the un-needed variables from the database.
*/
public function testVariablesMissingFromImportAreDeleted()
{
$egg = factory(Egg::class)->make(['id' => 123]);
$variable1 = factory(EggVariable::class)->make();
$variable2 = factory(EggVariable::class)->make();
$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([
'meta' => ['version' => 'PTDL_v1'],
'name' => $egg->name,
'author' => 'newauthor@example.com',
'variables' => [$variable1->toArray()],
]));
$this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull();
$this->repository->shouldReceive('update')->with($egg->id, m::subset([
'author' => 'newauthor@example.com',
'name' => $egg->name,
]), true, true)->once()->andReturn($egg);
$this->variableRepository->shouldReceive('withoutFreshModel->updateOrCreate')->with([
'egg_id' => $egg->id,
'env_variable' => $variable1->env_variable,
], collect($variable1)->except(['egg_id', 'env_variable'])->toArray())->once()->andReturnNull();
$this->variableRepository->shouldReceive('setColumns')->with(['id', 'env_variable'])->once()->andReturnSelf()
->shouldReceive('findWhere')->with([['egg_id', '=', $egg->id]])->once()->andReturn(collect([$variable1, $variable2]));
$this->variableRepository->shouldReceive('deleteWhere')->with([
['egg_id', '=', $egg->id],
['env_variable', '=', $variable2->env_variable],
])->once()->andReturn(1);
$this->connection->shouldReceive('commit')->withNoArgs()->once()->andReturnNull();
$this->service->handle($egg, $this->file);
$this->assertTrue(true);
}
/**
* Test that an exception is thrown if the file is invalid.
*/
public function testExceptionIsThrownIfFileIsInvalid()
{
$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);
}
/**
* Test that an exception is thrown if the file is not a file.
*/
public function testExceptionIsThrownIfFileIsNotAFile()
{
$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 = 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);
}
/**
* Test that an exception is thrown if the JSON metadata is invalid.
*/
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);
$this->file->shouldReceive('openFile->fread')->with(100)->once()->andReturn(json_encode([
'meta' => ['version' => 'hodor'],
]));
try {
$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());
}
}
/**
* Test that an exception is thrown if bad JSON is provided.
*/
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($egg, $this->file);
} catch (PterodactylException $exception) {
$this->assertInstanceOf(BadJsonFormatException::class, $exception);
$this->assertEquals(trans('exceptions.nest.importer.json_error', [
'error' => json_last_error_msg(),
]), $exception->getMessage());
}
}
}

View file

@ -1,185 +0,0 @@
<?php
namespace Tests\Unit\Services\Eggs\Variables;
use Mockery as m;
use Tests\TestCase;
use BadMethodCallException;
use Pterodactyl\Models\EggVariable;
use Illuminate\Contracts\Validation\Factory;
use Pterodactyl\Services\Eggs\Variables\VariableCreationService;
use Pterodactyl\Contracts\Repository\EggVariableRepositoryInterface;
use Pterodactyl\Exceptions\Service\Egg\Variable\BadValidationRuleException;
use Pterodactyl\Exceptions\Service\Egg\Variable\ReservedVariableNameException;
class VariableCreationServiceTest extends TestCase
{
/**
* @var \Pterodactyl\Contracts\Repository\EggVariableRepositoryInterface|\Mockery\Mock
*/
private $repository;
/**
* @var \Illuminate\Contracts\Validation\Factory|\Mockery\Mock
*/
private $validator;
/**
* Setup tests.
*/
public function setUp(): void
{
parent::setUp();
$this->repository = m::mock(EggVariableRepositoryInterface::class);
$this->validator = m::mock(Factory::class);
}
/**
* Test basic functionality, data should be stored in the database.
*/
public function testVariableIsCreatedAndStored()
{
$data = ['env_variable' => 'TEST_VAR_123', 'default_value' => 'test'];
$this->repository->shouldReceive('create')->with(m::subset([
'egg_id' => 1,
'default_value' => 'test',
'user_viewable' => false,
'user_editable' => false,
'env_variable' => 'TEST_VAR_123',
]))->once()->andReturn(new EggVariable);
$this->assertInstanceOf(EggVariable::class, $this->getService()->handle(1, $data));
}
/**
* Test that the option key in the data array is properly parsed.
*/
public function testOptionsPassedInArrayKeyAreParsedProperly()
{
$data = ['env_variable' => 'TEST_VAR_123', 'options' => ['user_viewable', 'user_editable']];
$this->repository->shouldReceive('create')->with(m::subset([
'default_value' => '',
'user_viewable' => true,
'user_editable' => true,
'env_variable' => 'TEST_VAR_123',
]))->once()->andReturn(new EggVariable);
$this->assertInstanceOf(EggVariable::class, $this->getService()->handle(1, $data));
}
/**
* Test that an empty (null) value passed in the option key is handled
* properly as an array. Also tests the same case against the default_value.
*
* @see https://github.com/Pterodactyl/Panel/issues/841
* @see https://github.com/Pterodactyl/Panel/issues/943
*/
public function testNullOptionValueIsPassedAsArray()
{
$data = ['env_variable' => 'TEST_VAR_123', 'options' => null, 'default_value' => null];
$this->repository->shouldReceive('create')->with(m::subset([
'default_value' => '',
'user_viewable' => false,
'user_editable' => false,
]))->once()->andReturn(new EggVariable);
$this->assertInstanceOf(EggVariable::class, $this->getService()->handle(1, $data));
}
/**
* Test that all of the reserved variables defined in the model trigger an exception.
*
* @param string $variable
*
* @dataProvider reservedNamesProvider
*/
public function testExceptionIsThrownIfEnvironmentVariableIsInListOfReservedNames(string $variable)
{
$this->expectException(ReservedVariableNameException::class);
$this->getService()->handle(1, ['env_variable' => $variable]);
}
/**
* Test that the egg ID applied in the function takes higher priority than an
* ID passed into the handler.
*/
public function testEggIdPassedInDataIsNotApplied()
{
$data = ['egg_id' => 123456, 'env_variable' => 'TEST_VAR_123'];
$this->repository->shouldReceive('create')->with(m::subset([
'egg_id' => 1,
]))->once()->andReturn(new EggVariable);
$this->assertInstanceOf(EggVariable::class, $this->getService()->handle(1, $data));
}
/**
* Test that validation errors due to invalid rules are caught and handled properly.
*/
public function testInvalidValidationRulesResultInException()
{
$this->expectException(BadValidationRuleException::class);
$this->expectExceptionMessage('The validation rule "hodor_door" is not a valid rule for this application.');
$data = ['env_variable' => 'TEST_VAR_123', 'rules' => 'string|hodorDoor'];
$this->validator->shouldReceive('make')->once()
->with(['__TEST' => 'test'], ['__TEST' => 'string|hodorDoor'])
->andReturnSelf();
$this->validator->shouldReceive('fails')->once()
->withNoArgs()
->andThrow(new BadMethodCallException('Method [validateHodorDoor] does not exist.'));
$this->getService()->handle(1, $data);
}
/**
* Test that an exception not stemming from a bad rule is not caught.
*/
public function testExceptionNotCausedByBadRuleIsNotCaught()
{
$this->expectException(BadMethodCallException::class);
$this->expectExceptionMessage('Received something, but no expectations were specified.');
$data = ['env_variable' => 'TEST_VAR_123', 'rules' => 'string'];
$this->validator->shouldReceive('make')->once()
->with(['__TEST' => 'test'], ['__TEST' => 'string'])
->andReturnSelf();
$this->validator->shouldReceive('fails')->once()
->withNoArgs()
->andThrow(new BadMethodCallException('Received something, but no expectations were specified.'));
$this->getService()->handle(1, $data);
}
/**
* Provides the data to be used in the tests.
*
* @return array
*/
public function reservedNamesProvider()
{
$data = [];
$exploded = explode(',', EggVariable::RESERVED_ENV_NAMES);
foreach ($exploded as $e) {
$data[] = [$e];
}
return $data;
}
/**
* Return an instance of the service with mocked dependencies for testing.
*
* @return \Pterodactyl\Services\Eggs\Variables\VariableCreationService
*/
private function getService(): VariableCreationService
{
return new VariableCreationService($this->repository, $this->validator);
}
}

View file

@ -1,241 +0,0 @@
<?php
namespace Tests\Unit\Services\Eggs\Variables;
use Exception;
use Mockery as m;
use Tests\TestCase;
use BadMethodCallException;
use Pterodactyl\Models\EggVariable;
use Illuminate\Contracts\Validation\Factory;
use Pterodactyl\Exceptions\DisplayException;
use Pterodactyl\Services\Eggs\Variables\VariableUpdateService;
use Pterodactyl\Contracts\Repository\EggVariableRepositoryInterface;
use Pterodactyl\Exceptions\Service\Egg\Variable\BadValidationRuleException;
use Pterodactyl\Exceptions\Service\Egg\Variable\ReservedVariableNameException;
class VariableUpdateServiceTest extends TestCase
{
/**
* @var \Pterodactyl\Models\EggVariable|\Mockery\Mock
*/
private $model;
/**
* @var \Pterodactyl\Contracts\Repository\EggVariableRepositoryInterface|\Mockery\Mock
*/
private $repository;
/**
* @var \Illuminate\Contracts\Validation\Factory|\Mockery\Mock
*/
private $validator;
/**
* Setup tests.
*/
public function setUp(): void
{
parent::setUp();
$this->model = factory(EggVariable::class)->make();
$this->repository = m::mock(EggVariableRepositoryInterface::class);
$this->validator = m::mock(Factory::class);
}
/**
* Test the function when no env_variable key is passed into the function.
*/
public function testVariableIsUpdatedWhenNoEnvironmentVariableIsPassed()
{
$this->repository->shouldReceive('withoutFreshModel')->withNoArgs()->once()->andReturnSelf()
->shouldReceive('update')->with($this->model->id, m::subset([
'user_viewable' => false,
'user_editable' => false,
]))->once()->andReturn(true);
$this->assertTrue($this->getService()->handle($this->model, []));
}
/**
* Test that a null value passed in for the default is converted to a string.
*
* @see https://github.com/Pterodactyl/Panel/issues/934
*/
public function testNullDefaultValue()
{
$this->repository->shouldReceive('withoutFreshModel->update')->with($this->model->id, m::subset([
'user_viewable' => false,
'user_editable' => false,
'default_value' => '',
]))->once()->andReturn(true);
$this->assertTrue($this->getService()->handle($this->model, ['default_value' => null]));
}
/**
* Test the function when a valid env_variable key is passed into the function.
*/
public function testVariableIsUpdatedWhenValidEnvironmentVariableIsPassed()
{
$this->repository->shouldReceive('setColumns')->with('id')->once()->andReturnSelf()
->shouldReceive('findCountWhere')->with([
['env_variable', '=', 'TEST_VAR_123'],
['egg_id', '=', $this->model->option_id],
['id', '!=', $this->model->id],
])->once()->andReturn(0);
$this->repository->shouldReceive('withoutFreshModel')->withNoArgs()->once()->andReturnSelf()
->shouldReceive('update')->with($this->model->id, m::subset([
'user_viewable' => false,
'user_editable' => false,
'env_variable' => 'TEST_VAR_123',
]))->once()->andReturn(true);
$this->assertTrue($this->getService()->handle($this->model, ['env_variable' => 'TEST_VAR_123']));
}
/**
* Test that an empty (null) value passed in the option key is handled
* properly as an array. Also tests that a null description is handled.
*
* @see https://github.com/Pterodactyl/Panel/issues/841
*/
public function testNullOptionValueIsPassedAsArray()
{
$this->repository->shouldReceive('withoutFreshModel')->withNoArgs()->once()->andReturnSelf()
->shouldReceive('update')->with($this->model->id, m::subset([
'user_viewable' => false,
'user_editable' => false,
'description' => '',
]))->once()->andReturn(true);
$this->assertTrue($this->getService()->handle($this->model, ['options' => null, 'description' => null]));
}
/**
* Test that data passed into the handler is overwritten inside the handler.
*/
public function testDataPassedIntoHandlerTakesLowerPriorityThanDataSet()
{
$this->repository->shouldReceive('setColumns')->with('id')->once()->andReturnSelf()
->shouldReceive('findCountWhere')->with([
['env_variable', '=', 'TEST_VAR_123'],
['egg_id', '=', $this->model->option_id],
['id', '!=', $this->model->id],
])->once()->andReturn(0);
$this->repository->shouldReceive('withoutFreshModel')->withNoArgs()->once()->andReturnSelf()
->shouldReceive('update')->with($this->model->id, m::subset([
'user_viewable' => false,
'user_editable' => false,
'env_variable' => 'TEST_VAR_123',
]))->once()->andReturn(true);
$this->assertTrue($this->getService()->handle($this->model, ['user_viewable' => 123456, 'env_variable' => 'TEST_VAR_123']));
}
/**
* Test that a non-unique environment variable triggers an exception.
*/
public function testExceptionIsThrownIfEnvironmentVariableIsNotUnique()
{
$this->repository->shouldReceive('setColumns')->with('id')->once()->andReturnSelf()
->shouldReceive('findCountWhere')->with([
['env_variable', '=', 'TEST_VAR_123'],
['egg_id', '=', $this->model->option_id],
['id', '!=', $this->model->id],
])->once()->andReturn(1);
try {
$this->getService()->handle($this->model, ['env_variable' => 'TEST_VAR_123']);
} catch (Exception $exception) {
$this->assertInstanceOf(DisplayException::class, $exception);
$this->assertEquals(trans('exceptions.service.variables.env_not_unique', [
'name' => 'TEST_VAR_123',
]), $exception->getMessage());
}
}
/**
* Test that all of the reserved variables defined in the model trigger an exception.
*
* @dataProvider reservedNamesProvider
*/
public function testExceptionIsThrownIfEnvironmentVariableIsInListOfReservedNames(string $variable)
{
$this->expectException(ReservedVariableNameException::class);
$this->getService()->handle($this->model, ['env_variable' => $variable]);
}
/**
* Test that validation errors due to invalid rules are caught and handled properly.
*/
public function testInvalidValidationRulesResultInException()
{
$this->expectException(BadValidationRuleException::class);
$this->expectExceptionMessage('The validation rule "hodor_door" is not a valid rule for this application.');
$data = ['env_variable' => 'TEST_VAR_123', 'rules' => 'string|hodorDoor'];
$this->repository->shouldReceive('setColumns->findCountWhere')->once()->andReturn(0);
$this->validator->shouldReceive('make')->once()
->with(['__TEST' => 'test'], ['__TEST' => 'string|hodorDoor'])
->andReturnSelf();
$this->validator->shouldReceive('fails')->once()
->withNoArgs()
->andThrow(new BadMethodCallException('Method [validateHodorDoor] does not exist.'));
$this->getService()->handle($this->model, $data);
}
/**
* Test that an exception not stemming from a bad rule is not caught.
*/
public function testExceptionNotCausedByBadRuleIsNotCaught()
{
$this->expectException(BadMethodCallException::class);
$this->expectExceptionMessage('Received something, but no expectations were specified.');
$data = ['rules' => 'string'];
$this->validator->shouldReceive('make')->once()
->with(['__TEST' => 'test'], ['__TEST' => 'string'])
->andReturnSelf();
$this->validator->shouldReceive('fails')->once()
->withNoArgs()
->andThrow(new BadMethodCallException('Received something, but no expectations were specified.'));
$this->getService()->handle($this->model, $data);
}
/**
* Provides the data to be used in the tests.
*
* @return array
*/
public function reservedNamesProvider()
{
$data = [];
$exploded = explode(',', EggVariable::RESERVED_ENV_NAMES);
foreach ($exploded as $e) {
$data[] = [$e];
}
return $data;
}
/**
* Return an instance of the service with mocked dependencies for testing.
*
* @return \Pterodactyl\Services\Eggs\Variables\VariableUpdateService
*/
private function getService(): VariableUpdateService
{
return new VariableUpdateService($this->repository, $this->validator);
}
}

View file

@ -1,56 +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\Locations;
use Mockery as m;
use Tests\TestCase;
use Pterodactyl\Models\Location;
use Pterodactyl\Services\Locations\LocationCreationService;
use Pterodactyl\Contracts\Repository\LocationRepositoryInterface;
class LocationCreationServiceTest extends TestCase
{
/**
* @var \Pterodactyl\Contracts\Repository\LocationRepositoryInterface
*/
protected $repository;
/**
* @var \Pterodactyl\Services\Locations\LocationCreationService
*/
protected $service;
/**
* Setup tests.
*/
public function setUp(): void
{
parent::setUp();
$this->repository = m::mock(LocationRepositoryInterface::class);
$this->service = new LocationCreationService($this->repository);
}
/**
* Test that a location is created.
*/
public function testLocationIsCreated()
{
$location = factory(Location::class)->make();
$this->repository->shouldReceive('create')->with(['test_data' => 'test_value'])->once()->andReturn($location);
$response = $this->service->handle(['test_data' => 'test_value']);
$this->assertNotEmpty($response);
$this->assertInstanceOf(Location::class, $response);
$this->assertEquals($location, $response);
}
}

View file

@ -1,76 +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\Locations;
use Mockery as m;
use Tests\TestCase;
use Pterodactyl\Exceptions\DisplayException;
use Pterodactyl\Services\Locations\LocationDeletionService;
use Pterodactyl\Contracts\Repository\NodeRepositoryInterface;
use Pterodactyl\Contracts\Repository\LocationRepositoryInterface;
use Pterodactyl\Exceptions\Service\Location\HasActiveNodesException;
class LocationDeletionServiceTest extends TestCase
{
/**
* @var \Pterodactyl\Contracts\Repository\NodeRepositoryInterface
*/
protected $nodeRepository;
/**
* @var \Pterodactyl\Contracts\Repository\LocationRepositoryInterface
*/
protected $repository;
/**
* @var \Pterodactyl\Services\Locations\LocationDeletionService
*/
protected $service;
/**
* Setup tests.
*/
public function setUp(): void
{
parent::setUp();
$this->nodeRepository = m::mock(NodeRepositoryInterface::class);
$this->repository = m::mock(LocationRepositoryInterface::class);
$this->service = new LocationDeletionService($this->repository, $this->nodeRepository);
}
/**
* Test that a location is deleted.
*/
public function testLocationIsDeleted()
{
$this->nodeRepository->shouldReceive('findCountWhere')->with([['location_id', '=', 123]])->once()->andReturn(0);
$this->repository->shouldReceive('delete')->with(123)->once()->andReturn(1);
$response = $this->service->handle(123);
$this->assertEquals(1, $response);
}
/**
* Test that an exception is thrown if nodes are attached to a location.
*/
public function testExceptionIsThrownIfNodesAreAttached()
{
$this->nodeRepository->shouldReceive('findCountWhere')->with([['location_id', '=', 123]])->once()->andReturn(1);
try {
$this->service->handle(123);
} catch (DisplayException $exception) {
$this->assertInstanceOf(HasActiveNodesException::class, $exception);
$this->assertEquals(trans('exceptions.locations.has_nodes'), $exception->getMessage());
}
}
}

View file

@ -1,67 +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\Locations;
use Mockery as m;
use Tests\TestCase;
use Pterodactyl\Models\Location;
use Pterodactyl\Services\Locations\LocationUpdateService;
use Pterodactyl\Contracts\Repository\LocationRepositoryInterface;
class LocationUpdateServiceTest extends TestCase
{
/**
* @var \Pterodactyl\Contracts\Repository\LocationRepositoryInterface
*/
protected $repository;
/**
* @var \Pterodactyl\Services\Locations\LocationUpdateService
*/
protected $service;
/**
* Setup tests.
*/
public function setUp(): void
{
parent::setUp();
$this->repository = m::mock(LocationRepositoryInterface::class);
$this->service = new LocationUpdateService($this->repository);
}
/**
* Test location is updated.
*/
public function testLocationIsUpdated()
{
$model = factory(Location::class)->make(['id' => 123]);
$this->repository->shouldReceive('update')->with(123, ['test_data' => 'test_value'])->once()->andReturn($model);
$response = $this->service->handle($model->id, ['test_data' => 'test_value']);
$this->assertNotEmpty($response);
$this->assertInstanceOf(Location::class, $response);
}
/**
* Test that a model can be passed in place of an ID.
*/
public function testModelCanBePassedToFunction()
{
$model = factory(Location::class)->make(['id' => 123]);
$this->repository->shouldReceive('update')->with(123, ['test_data' => 'test_value'])->once()->andReturn($model);
$response = $this->service->handle($model, ['test_data' => 'test_value']);
$this->assertNotEmpty($response);
$this->assertInstanceOf(Location::class, $response);
}
}

View file

@ -1,95 +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\Services;
use Mockery as m;
use Tests\TestCase;
use Pterodactyl\Models\Nest;
use Tests\Traits\MocksUuids;
use Illuminate\Contracts\Config\Repository;
use Pterodactyl\Services\Nests\NestCreationService;
use Pterodactyl\Contracts\Repository\NestRepositoryInterface;
class NestCreationServiceTest extends TestCase
{
use MocksUuids;
/**
* @var \Illuminate\Contracts\Config\Repository|\Mockery\Mock
*/
private $config;
/**
* @var \Pterodactyl\Contracts\Repository\NestRepositoryInterface|\Mockery\Mock
*/
private $repository;
/**
* Setup tests.
*/
public function setUp(): void
{
parent::setUp();
$this->config = m::mock(Repository::class);
$this->repository = m::mock(NestRepositoryInterface::class);
}
/**
* Test that a new service can be created using the correct data.
*/
public function testCreateNewService()
{
$model = factory(Nest::class)->make();
$this->config->shouldReceive('get')->with('pterodactyl.service.author')->once()->andReturn('testauthor@example.com');
$this->repository->shouldReceive('create')->with([
'uuid' => $this->getKnownUuid(),
'author' => 'testauthor@example.com',
'name' => $model->name,
'description' => $model->description,
], true, true)->once()->andReturn($model);
$response = $this->getService()->handle(['name' => $model->name, 'description' => $model->description]);
$this->assertInstanceOf(Nest::class, $response);
$this->assertEquals($model, $response);
}
/**
* Test creation of a new nest with a defined author. This is used by seeder
* scripts which need to set a specific author for nests in order for other
* functionality to work correctly.
*/
public function testCreateServiceWithDefinedAuthor()
{
$model = factory(Nest::class)->make();
$this->repository->shouldReceive('create')->with([
'uuid' => $this->getKnownUuid(),
'author' => 'support@pterodactyl.io',
'name' => $model->name,
'description' => $model->description,
], true, true)->once()->andReturn($model);
$response = $this->getService()->handle(['name' => $model->name, 'description' => $model->description], 'support@pterodactyl.io');
$this->assertInstanceOf(Nest::class, $response);
$this->assertEquals($model, $response);
}
/**
* Return an instance of the service with mocked dependencies.
*
* @return \Pterodactyl\Services\Nests\NestCreationService
*/
private function getService(): NestCreationService
{
return new NestCreationService($this->config, $this->repository);
}
}

View file

@ -1,91 +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\Services;
use Mockery as m;
use Tests\TestCase;
use Pterodactyl\Exceptions\PterodactylException;
use Pterodactyl\Services\Nests\NestDeletionService;
use Pterodactyl\Contracts\Repository\NestRepositoryInterface;
use Pterodactyl\Exceptions\Service\HasActiveServersException;
use Pterodactyl\Contracts\Repository\ServerRepositoryInterface;
class NestDeletionServiceTest extends TestCase
{
/**
* @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface|\Mockery\Mock
*/
protected $serverRepository;
/**
* @var \Pterodactyl\Contracts\Repository\NestRepositoryInterface|\Mockery\Mock
*/
protected $repository;
/**
* @var \Pterodactyl\Services\Nests\NestDeletionService
*/
protected $service;
/**
* Setup tests.
*/
public function setUp(): void
{
parent::setUp();
$this->serverRepository = m::mock(ServerRepositoryInterface::class);
$this->repository = m::mock(NestRepositoryInterface::class);
$this->service = new NestDeletionService($this->serverRepository, $this->repository);
}
/**
* Test that a service is deleted when there are no servers attached to a service.
*/
public function testServiceIsDeleted()
{
$this->serverRepository->shouldReceive('findCountWhere')->with([['nest_id', '=', 1]])->once()->andReturn(0);
$this->repository->shouldReceive('delete')->with(1)->once()->andReturn(1);
$this->assertEquals(1, $this->service->handle(1));
}
/**
* Test that an exception is thrown when there are servers attached to a service.
*
* @dataProvider serverCountProvider
*
* @param int $count
*/
public function testExceptionIsThrownIfServersAreAttached(int $count)
{
$this->serverRepository->shouldReceive('findCountWhere')->with([['nest_id', '=', 1]])->once()->andReturn($count);
try {
$this->service->handle(1);
} catch (PterodactylException $exception) {
$this->assertInstanceOf(HasActiveServersException::class, $exception);
$this->assertEquals(trans('exceptions.nest.delete_has_servers'), $exception->getMessage());
}
}
/**
* Provide assorted server counts to ensure that an exception is always thrown when more than 0 servers are found.
*
* @return array
*/
public function serverCountProvider()
{
return [
[1], [2], [5], [10],
];
}
}

View file

@ -1,62 +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\Services;
use Mockery as m;
use Tests\TestCase;
use Pterodactyl\Services\Nests\NestUpdateService;
use Pterodactyl\Contracts\Repository\NestRepositoryInterface;
class NestUpdateServiceTest extends TestCase
{
/**
* @var \Pterodactyl\Contracts\Repository\NestRepositoryInterface|\Mockery\Mock
*/
protected $repository;
/**
* @var \Pterodactyl\Services\Nests\NestUpdateService
*/
protected $service;
/**
* Setup tests.
*/
public function setUp(): void
{
parent::setUp();
$this->repository = m::mock(NestRepositoryInterface::class);
$this->service = new NestUpdateService($this->repository);
}
/**
* Test that the author key is removed from the data array before updating the record.
*/
public function testAuthorArrayKeyIsRemovedIfPassed()
{
$this->repository->shouldReceive('withoutFreshModel')->withNoArgs()->once()->andReturnSelf()
->shouldReceive('update')->with(1, ['otherfield' => 'value'])->once()->andReturnNull();
$this->service->handle(1, ['author' => 'author1', 'otherfield' => 'value']);
}
/**
* Test that the function continues to work when no author key is passed.
*/
public function testServiceIsUpdatedWhenNoAuthorKeyIsPassed()
{
$this->repository->shouldReceive('withoutFreshModel')->withNoArgs()->once()->andReturnSelf()
->shouldReceive('update')->with(1, ['otherfield' => 'value'])->once()->andReturnNull();
$this->service->handle(1, ['otherfield' => 'value']);
}
}

View file

@ -1,79 +0,0 @@
<?php
namespace Tests\Unit\Services\Nodes;
use Mockery as m;
use Tests\TestCase;
use Ramsey\Uuid\Uuid;
use phpmock\phpunit\PHPMock;
use Pterodactyl\Models\Node;
use Ramsey\Uuid\UuidFactory;
use Illuminate\Contracts\Encryption\Encrypter;
use Pterodactyl\Services\Nodes\NodeCreationService;
use Pterodactyl\Contracts\Repository\NodeRepositoryInterface;
class NodeCreationServiceTest extends TestCase
{
use PHPMock;
/**
* @var \Mockery\MockInterface
*/
private $repository;
/**
* @var \Mockery\MockInterface
*/
private $encrypter;
/**
* Setup tests.
*/
public function setUp(): void
{
parent::setUp();
/* @noinspection PhpParamsInspection */
Uuid::setFactory(
m::mock(UuidFactory::class . '[uuid4]', [
'uuid4' => Uuid::fromString('00000000-0000-0000-0000-000000000000'),
])
);
$this->repository = m::mock(NodeRepositoryInterface::class);
$this->encrypter = m::mock(Encrypter::class);
}
/**
* Test that a node is created and a daemon secret token is created.
*/
public function testNodeIsCreatedAndDaemonSecretIsGenerated()
{
/** @var \Pterodactyl\Models\Node $node */
$node = factory(Node::class)->make();
$this->encrypter->expects('encrypt')->with(m::on(function ($value) {
return strlen($value) === Node::DAEMON_TOKEN_LENGTH;
}))->andReturns('encrypted_value');
$this->repository->expects('create')->with(m::on(function ($value) {
$this->assertTrue(is_array($value));
$this->assertSame('NodeName', $value['name']);
$this->assertSame('00000000-0000-0000-0000-000000000000', $value['uuid']);
$this->assertSame('encrypted_value', $value['daemon_token']);
$this->assertTrue(strlen($value['daemon_token_id']) === Node::DAEMON_TOKEN_ID_LENGTH);
return true;
}), true, true)->andReturn($node);
$this->assertSame($node, $this->getService()->handle(['name' => 'NodeName']));
}
/**
* @return \Pterodactyl\Services\Nodes\NodeCreationService
*/
private function getService()
{
return new NodeCreationService($this->encrypter, $this->repository);
}
}

View file

@ -1,94 +0,0 @@
<?php
namespace Tests\Unit\Services\Nodes;
use Mockery as m;
use Tests\TestCase;
use Pterodactyl\Models\Node;
use Pterodactyl\Exceptions\DisplayException;
use Illuminate\Contracts\Translation\Translator;
use Pterodactyl\Services\Nodes\NodeDeletionService;
use Pterodactyl\Contracts\Repository\NodeRepositoryInterface;
use Pterodactyl\Contracts\Repository\ServerRepositoryInterface;
class NodeDeletionServiceTest extends TestCase
{
/**
* @var \Pterodactyl\Contracts\Repository\NodeRepositoryInterface
*/
protected $repository;
/**
* @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface
*/
protected $serverRepository;
/**
* @var \Illuminate\Contracts\Translation\Translator
*/
protected $translator;
/**
* @var \Pterodactyl\Services\Nodes\NodeDeletionService
*/
protected $service;
/**
* Setup tests.
*/
public function setUp(): void
{
parent::setUp();
$this->repository = m::mock(NodeRepositoryInterface::class);
$this->serverRepository = m::mock(ServerRepositoryInterface::class);
$this->translator = m::mock(Translator::class);
$this->service = new NodeDeletionService(
$this->repository,
$this->serverRepository,
$this->translator
);
}
/**
* Test that a node is deleted if there are no servers attached to it.
*/
public function testNodeIsDeletedIfNoServersAreAttached()
{
$this->serverRepository->shouldReceive('setColumns')->with('id')->once()->andReturnSelf()
->shouldReceive('findCountWhere')->with([['node_id', '=', 1]])->once()->andReturn(0);
$this->repository->shouldReceive('delete')->with(1)->once()->andReturn(1);
$this->assertEquals(1, $this->service->handle(1));
}
/**
* Test that an exception is thrown if servers are attached to the node.
*/
public function testExceptionIsThrownIfServersAreAttachedToNode()
{
$this->expectException(DisplayException::class);
$this->serverRepository->shouldReceive('setColumns')->with('id')->once()->andReturnSelf()
->shouldReceive('findCountWhere')->with([['node_id', '=', 1]])->once()->andReturn(1);
$this->translator->shouldReceive('trans')->with('exceptions.node.servers_attached')->once()->andReturnNull();
$this->repository->shouldNotReceive('delete');
$this->service->handle(1);
}
/**
* Test that a model can be passed into the handle function rather than an ID.
*/
public function testModelCanBePassedToFunctionInPlaceOfNodeId()
{
$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);
$this->repository->shouldReceive('delete')->with($node->id)->once()->andReturn(1);
$this->assertEquals(1, $this->service->handle($node));
}
}

View file

@ -1,241 +0,0 @@
<?php
namespace Tests\Unit\Services\Nodes;
use Exception;
use Mockery as m;
use Tests\TestCase;
use GuzzleHttp\Psr7\Request;
use phpmock\phpunit\PHPMock;
use Pterodactyl\Models\Node;
use Tests\Traits\MocksRequestException;
use GuzzleHttp\Exception\ConnectException;
use GuzzleHttp\Exception\TransferException;
use Illuminate\Database\ConnectionInterface;
use Illuminate\Contracts\Encryption\Encrypter;
use Pterodactyl\Services\Nodes\NodeUpdateService;
use Pterodactyl\Repositories\Eloquent\NodeRepository;
use Pterodactyl\Repositories\Wings\DaemonConfigurationRepository;
use Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException;
use Pterodactyl\Exceptions\Service\Node\ConfigurationNotPersistedException;
class NodeUpdateServiceTest extends TestCase
{
use PHPMock, MocksRequestException;
/**
* @var \Mockery\MockInterface
*/
private $connection;
/**
* @var \Mockery\MockInterface
*/
private $configurationRepository;
/**
* @var \Mockery\MockInterface
*/
private $encrypter;
/**
* @var \Mockery\MockInterface
*/
private $repository;
/**
* Setup tests.
*/
public function setUp(): void
{
parent::setUp();
$this->connection = m::mock(ConnectionInterface::class);
$this->encrypter = m::mock(Encrypter::class);
$this->configurationRepository = m::mock(DaemonConfigurationRepository::class);
$this->repository = m::mock(NodeRepository::class);
}
/**
* Test that the daemon secret is reset when `reset_secret` is passed in the data.
*/
public function testNodeIsUpdatedAndDaemonSecretIsReset()
{
/** @var \Pterodactyl\Models\Node $model */
$model = factory(Node::class)->make([
'fqdn' => 'https://example.com',
]);
/** @var \Pterodactyl\Models\Node $updatedModel */
$updatedModel = factory(Node::class)->make([
'name' => 'New Name',
'fqdn' => 'https://example2.com',
]);
$this->connection->expects('transaction')->with(m::on(function ($closure) use ($updatedModel) {
$response = $closure();
$this->assertIsArray($response);
$this->assertTrue(count($response) === 2);
$this->assertSame($updatedModel, $response[0]);
$this->assertFalse($response[1]);
return true;
}))->andReturns([$updatedModel, false]);
$this->encrypter->expects('encrypt')->with(m::on(function ($value) {
return strlen($value) === Node::DAEMON_TOKEN_LENGTH;
}))->andReturns('encrypted_value');
$this->repository->expects('withFreshModel->update')->with($model->id, m::on(function ($value) {
$this->assertTrue(is_array($value));
$this->assertSame('New Name', $value['name']);
$this->assertSame('encrypted_value', $value['daemon_token']);
$this->assertTrue(strlen($value['daemon_token_id']) === Node::DAEMON_TOKEN_ID_LENGTH);
return true;
}), true, true)->andReturns($updatedModel);
$this->configurationRepository->expects('setNode')->with(m::on(function ($value) use ($model, $updatedModel) {
$this->assertInstanceOf(Node::class, $value);
$this->assertSame($model->uuid, $value->uuid);
// Yes, this is correct. Always use the updated model's FQDN when making requests to
// the Daemon so that any changes to that are properly propagated down to the daemon.
//
// @see https://github.com/pterodactyl/panel/issues/1931
$this->assertSame($updatedModel->fqdn, $value->fqdn);
return true;
}))->andReturnSelf();
$this->configurationRepository->expects('update')->with($updatedModel);
$this->getService()->handle($model, [
'name' => $updatedModel->name,
], true);
}
/**
* Test that daemon secret is not modified when no variable is passed in data.
*/
public function testNodeIsUpdatedAndDaemonSecretIsNotChanged()
{
/** @var \Pterodactyl\Models\Node $model */
$model = factory(Node::class)->make(['fqdn' => 'https://example.com']);
/** @var \Pterodactyl\Models\Node $updatedModel */
$updatedModel = factory(Node::class)->make(['name' => 'New Name', 'fqdn' => $model->fqdn]);
$this->connection->expects('transaction')->with(m::on(function ($closure) use ($updatedModel) {
$response = $closure();
$this->assertIsArray($response);
$this->assertTrue(count($response) === 2);
$this->assertSame($updatedModel, $response[0]);
$this->assertFalse($response[1]);
return true;
}))->andReturns([$updatedModel, false]);
$this->repository->expects('withFreshModel->update')->with($model->id, m::on(function ($value) {
$this->assertTrue(is_array($value));
$this->assertSame('New Name', $value['name']);
$this->assertArrayNotHasKey('daemon_token', $value);
$this->assertArrayNotHasKey('daemon_token_id', $value);
return true;
}), true, true)->andReturns($updatedModel);
$this->configurationRepository->expects('setNode->update')->with($updatedModel);
$this->getService()->handle($model, ['name' => $updatedModel->name]);
}
/**
* Test that an exception caused by a connection error is handled.
*/
public function testExceptionRelatedToConnection()
{
$this->configureExceptionMock(DaemonConnectionException::class);
$this->expectException(ConfigurationNotPersistedException::class);
/** @var \Pterodactyl\Models\Node $model */
$model = factory(Node::class)->make(['fqdn' => 'https://example.com']);
/** @var \Pterodactyl\Models\Node $updatedModel */
$updatedModel = factory(Node::class)->make(['name' => 'New Name', 'fqdn' => $model->fqdn]);
$this->connection->expects('transaction')->with(m::on(function ($closure) use ($updatedModel) {
$response = $closure();
$this->assertIsArray($response);
$this->assertTrue(count($response) === 2);
$this->assertSame($updatedModel, $response[0]);
$this->assertTrue($response[1]);
return true;
}))->andReturn([$updatedModel, true]);
$this->repository->expects('withFreshModel->update')->with($model->id, m::on(function ($value) {
$this->assertTrue(is_array($value));
$this->assertSame('New Name', $value['name']);
$this->assertArrayNotHasKey('daemon_token', $value);
$this->assertArrayNotHasKey('daemon_token_id', $value);
return true;
}), true, true)->andReturns($updatedModel);
$this->configurationRepository->expects('setNode->update')->with($updatedModel)->andThrow(
new DaemonConnectionException(
new ConnectException('', new Request('GET', 'Test'), new Exception)
)
);
$this->getService()->handle($model, ['name' => $updatedModel->name]);
}
/**
* Test that an exception not caused by a daemon connection error is handled.
*/
public function testExceptionNotRelatedToConnection()
{
/** @var \Pterodactyl\Models\Node $model */
$model = factory(Node::class)->make(['fqdn' => 'https://example.com']);
/** @var \Pterodactyl\Models\Node $updatedModel */
$updatedModel = factory(Node::class)->make(['name' => 'New Name', 'fqdn' => $model->fqdn]);
$this->connection->expects('transaction')->with(m::on(function ($closure) use ($updatedModel) {
try {
$closure();
} catch (Exception $exception) {
$this->assertInstanceOf(Exception::class, $exception);
$this->assertSame('Foo', $exception->getMessage());
return true;
}
return false;
}));
$this->repository->expects('withFreshModel->update')->andReturns($updatedModel);
$this->configurationRepository->expects('setNode->update')->andThrow(
new Exception('Foo')
);
$this->getService()->handle($model, ['name' => $updatedModel->name]);
}
/**
* Return an instance of the service with mocked injections.
*
* @return \Pterodactyl\Services\Nodes\NodeUpdateService
*/
private function getService(): NodeUpdateService
{
return new NodeUpdateService(
$this->connection, $this->encrypter, $this->configurationRepository, $this->repository
);
}
}

View file

@ -1,173 +0,0 @@
<?php
namespace Tests\Unit\Services\Servers;
use Mockery as m;
use Tests\TestCase;
use Pterodactyl\Models\Server;
use Pterodactyl\Models\Location;
use Illuminate\Support\Collection;
use Pterodactyl\Models\EggVariable;
use Pterodactyl\Services\Servers\EnvironmentService;
use Pterodactyl\Contracts\Repository\ServerRepositoryInterface;
class EnvironmentServiceTest extends TestCase
{
/**
* @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface|\Mockery\Mock
*/
private $repository;
/**
* Setup tests.
*/
public function setUp(): void
{
parent::setUp();
config()->set('pterodactyl.environment_variables', []);
}
/**
* Test that set environment key stores the key into a retrievable array.
*/
public function testSettingEnvironmentKeyPersistsItInArray()
{
$service = $this->getService();
$service->setEnvironmentKey('TEST_KEY', function () {
return true;
});
$this->assertNotEmpty($service->getEnvironmentKeys());
$this->assertArrayHasKey('TEST_KEY', $service->getEnvironmentKeys());
}
/**
* Test that environment defaults are returned by the process function.
*/
public function testProcessShouldReturnDefaultEnvironmentVariablesForAServer()
{
$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->assertCount(4, $response);
$this->assertArrayHasKey('TEST_VARIABLE', $response);
$this->assertSame('Test Variable', $response['TEST_VARIABLE']);
}
/**
* Test that variables included at run-time are also included.
*/
public function testProcessShouldReturnKeySetAtRuntime()
{
$model = $this->getServerModel([]);
$service = $this->getService();
$service->setEnvironmentKey('TEST_VARIABLE', function ($server) {
return $server->uuidShort;
});
$response = $service->handle($model);
$this->assertNotEmpty($response);
$this->assertArrayHasKey('TEST_VARIABLE', $response);
$this->assertSame($model->uuidShort, $response['TEST_VARIABLE']);
}
/**
* Test that duplicate variables provided in config override the defaults.
*/
public function testProcessShouldAllowOverwritingVariablesWithConfigurationFile()
{
config()->set('pterodactyl.environment_variables', [
'P_SERVER_UUID' => 'name',
]);
$model = $this->getServerModel([]);
$response = $this->getService()->handle($model);
$this->assertNotEmpty($response);
$this->assertSame(3, count($response));
$this->assertArrayHasKey('P_SERVER_UUID', $response);
$this->assertSame($model->name, $response['P_SERVER_UUID']);
}
/**
* Test that config based environment variables can be done using closures.
*/
public function testVariablesSetInConfigurationAllowForClosures()
{
config()->set('pterodactyl.environment_variables', [
'P_SERVER_UUID' => function ($server) {
return $server->id * 2;
},
]);
$model = $this->getServerModel([]);
$response = $this->getService()->handle($model);
$this->assertNotEmpty($response);
$this->assertSame(3, count($response));
$this->assertArrayHasKey('P_SERVER_UUID', $response);
$this->assertSame($model->id * 2, $response['P_SERVER_UUID']);
}
/**
* Test that duplicate variables provided at run-time override the defaults and those
* that are defined in the configuration file.
*/
public function testProcessShouldAllowOverwritingDefaultVariablesWithRuntimeProvided()
{
config()->set('pterodactyl.environment_variables', [
'P_SERVER_UUID' => 'overwritten-config',
]);
$model = $this->getServerModel([]);
$service = $this->getService();
$service->setEnvironmentKey('P_SERVER_UUID', function ($model) {
return 'overwritten';
});
$response = $service->handle($model);
$this->assertNotEmpty($response);
$this->assertSame(3, count($response));
$this->assertArrayHasKey('P_SERVER_UUID', $response);
$this->assertSame('overwritten', $response['P_SERVER_UUID']);
}
/**
* Return an instance of the service with mocked dependencies.
*
* @return \Pterodactyl\Services\Servers\EnvironmentService
*/
private function getService(): EnvironmentService
{
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(array $variables): Server
{
/** @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,103 +0,0 @@
<?php
namespace Tests\Unit\Services\Servers;
use Mockery as m;
use Tests\TestCase;
use Pterodactyl\Models\Egg;
use Pterodactyl\Models\Server;
use Pterodactyl\Models\Allocation;
use Pterodactyl\Services\Servers\EnvironmentService;
use Pterodactyl\Contracts\Repository\ServerRepositoryInterface;
use Pterodactyl\Services\Servers\ServerConfigurationStructureService;
class ServerConfigurationStructureServiceTest extends TestCase
{
/**
* @var \Pterodactyl\Services\Servers\EnvironmentService|\Mockery\Mock
*/
private $environment;
/**
* @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface|\Mockery\Mock
*/
private $repository;
/**
* Setup tests.
*/
public function setUp(): void
{
parent::setUp();
$this->environment = m::mock(EnvironmentService::class);
$this->repository = m::mock(ServerRepositoryInterface::class);
}
/**
* Test that a configuration is returned in the proper format when passed a
* server model that is missing required relationships.
*/
public function testCorrectStructureIsReturned()
{
/** @var \Pterodactyl\Models\Server $model */
$model = factory(Server::class)->make();
$model->setRelation('allocation', factory(Allocation::class)->make());
$model->setRelation('allocations', collect(factory(Allocation::class)->times(2)->make()));
$model->setRelation('egg', factory(Egg::class)->make());
$this->environment->expects('handle')->with($model)->andReturn(['environment_array']);
$response = $this->getService()->handle($model);
$this->assertNotEmpty($response);
$this->assertArrayNotHasKey('user', $response);
$this->assertArrayNotHasKey('keys', $response);
$this->assertArrayHasKey('uuid', $response);
$this->assertArrayHasKey('suspended', $response);
$this->assertArrayHasKey('environment', $response);
$this->assertArrayHasKey('invocation', $response);
$this->assertArrayHasKey('skip_egg_scripts', $response);
$this->assertArrayHasKey('build', $response);
$this->assertArrayHasKey('container', $response);
$this->assertArrayHasKey('allocations', $response);
$this->assertSame([
'default' => [
'ip' => $model->allocation->ip,
'port' => $model->allocation->port,
],
'mappings' => $model->getAllocationMappings(),
], $response['allocations']);
$this->assertSame([
'memory_limit' => $model->memory,
'swap' => $model->swap,
'io_weight' => $model->io,
'cpu_limit' => $model->cpu,
'threads' => $model->threads,
'disk_space' => $model->disk,
], $response['build']);
$this->assertSame([
'image' => $model->image,
'oom_disabled' => $model->oom_disabled,
'requires_rebuild' => false,
], $response['container']);
$this->assertSame($model->uuid, $response['uuid']);
$this->assertSame($model->suspended, $response['suspended']);
$this->assertSame(['environment_array'], $response['environment']);
$this->assertSame($model->startup, $response['invocation']);
}
/**
* Return an instance of the service with mocked dependencies.
*
* @return \Pterodactyl\Services\Servers\ServerConfigurationStructureService
*/
private function getService(): ServerConfigurationStructureService
{
return new ServerConfigurationStructureService($this->environment);
}
}

View file

@ -1,73 +0,0 @@
<?php
namespace Tests\Unit\Services\Servers;
use Mockery as m;
use Tests\TestCase;
use Pterodactyl\Models\Server;
use Pterodactyl\Models\Allocation;
use Pterodactyl\Models\EggVariable;
use Pterodactyl\Services\Servers\StartupCommandService;
use Pterodactyl\Contracts\Repository\ServerRepositoryInterface;
class StartupCommandViewServiceTest extends TestCase
{
/**
* @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface|\Mockery\Mock
*/
private $repository;
/**
* Setup tests.
*/
public function setUp(): void
{
parent::setUp();
$this->repository = m::mock(ServerRepositoryInterface::class);
}
/**
* Test that the correct startup string is returned.
*/
public function testServiceResponse()
{
$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',
'server_value' => 'Test Value',
'user_viewable' => 1,
]),
factory(EggVariable::class)->make([
'env_variable' => 'TEST_VARIABLE_HIDDEN',
'server_value' => 'Hidden Value',
'user_viewable' => 0,
]),
]);
$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
);
}
/**
* Return an instance of the service with mocked dependencies.
*
* @return \Pterodactyl\Services\Servers\StartupCommandService
*/
private function getService(): StartupCommandService
{
return new StartupCommandService;
}
}