Add some additional test coverage and clean up modification service and suspension service

This commit is contained in:
Dane Everitt 2020-10-07 21:56:44 -07:00
parent 83efb2d7b6
commit d087bebc93
No known key found for this signature in database
GPG key ID: EEA66103B3D71F53
12 changed files with 214 additions and 503 deletions

View file

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

View file

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

View file

@ -55,7 +55,7 @@ class ServerConfigurationStructureService
{
return [
'uuid' => $server->uuid,
'suspended' => (bool) $server->suspended,
'suspended' => $server->suspended,
'environment' => $this->environment->handle($server),
'invocation' => $server->startup,
'skip_egg_scripts' => $server->skip_scripts,

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,65 @@
<?php
namespace Pterodactyl\Tests\Integration\Services\Servers;
use Exception;
use Pterodactyl\Models\Server;
use Pterodactyl\Models\ServerVariable;
use Illuminate\Validation\ValidationException;
use Pterodactyl\Tests\Integration\IntegrationTestCase;
use Pterodactyl\Services\Servers\StartupModificationService;
class StartupModificationServiceTest extends IntegrationTestCase
{
/**
* Test that a non-admin request to modify the server startup parameters does
* not perform any egg or nest updates. This also attempts to pass through an
* egg_id variable which should have no impact if the request is coming from
* a non-admin entity.
*/
public function testNonAdminCanModifyServerVariables()
{
// Theoretically lines up with the Bungeecord Minecraft egg.
$server = $this->createServerModel(['egg_id' => 1]);
try {
$this->app->make(StartupModificationService::class)->handle($server, [
'egg_id' => $server->egg_id + 1,
'environment' => [
'BUNGEE_VERSION' => '$$',
'SERVER_JARFILE' => 'server.jar',
],
]);
$this->assertTrue(false, 'This assertion should not be called.');
} catch (Exception $exception) {
$this->assertInstanceOf(ValidationException::class, $exception);
/** @var \Illuminate\Validation\ValidationException $exception */
$errors = $exception->validator->errors()->toArray();
$this->assertCount(1, $errors);
$this->assertArrayHasKey('environment.BUNGEE_VERSION', $errors);
$this->assertCount(1, $errors['environment.BUNGEE_VERSION']);
$this->assertSame('The Bungeecord Version variable may only contain letters and numbers.', $errors['environment.BUNGEE_VERSION'][0]);
}
ServerVariable::query()->where('variable_id', $server->variables[1]->id)->delete();
/** @var \Pterodactyl\Models\Server $result */
$result = $this->app->make(StartupModificationService::class)->handle($server, [
'egg_id' => $server->egg_id + 1,
'startup' => 'random gibberish',
'environment' => [
'BUNGEE_VERSION' => '1234',
'SERVER_JARFILE' => 'test.jar',
],
]);
$this->assertInstanceOf(Server::class, $result);
$this->assertCount(2, $result->variables);
$this->assertSame($server->startup, $result->startup);
$this->assertSame('1234', $result->variables[0]->server_value);
$this->assertSame('test.jar', $result->variables[1]->server_value);
}
}

View file

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

View file

@ -86,7 +86,7 @@ class ServerConfigurationStructureServiceTest extends TestCase
], $response['container']);
$this->assertSame($model->uuid, $response['uuid']);
$this->assertSame((bool) $model->suspended, $response['suspended']);
$this->assertSame($model->suspended, $response['suspended']);
$this->assertSame(['environment_array'], $response['environment']);
$this->assertSame($model->startup, $response['invocation']);
}

View file

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

View file

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