1
1
mirror of https://github.com/pterodactyl/panel.git synced 2024-11-25 18:42:31 +01:00

More API updates, better support for node config edits

This commit is contained in:
Dane Everitt 2018-01-10 23:19:03 -06:00
parent 800e2df6b2
commit cf21fd5a4b
No known key found for this signature in database
GPG Key ID: EEA66103B3D71F53
21 changed files with 449 additions and 125 deletions

View File

@ -3,6 +3,7 @@
namespace Pterodactyl\Contracts\Repository;
use Illuminate\Support\Collection;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
interface AllocationRepositoryInterface extends RepositoryInterface
{
@ -23,6 +24,15 @@ interface AllocationRepositoryInterface extends RepositoryInterface
*/
public function getAllocationsForNode(int $node): Collection;
/**
* Return all of the allocations for a node in a paginated format.
*
* @param int $node
* @param int $perPage
* @return \Illuminate\Contracts\Pagination\LengthAwarePaginator
*/
public function getPaginatedAllocationsForNode(int $node, int $perPage = 100): LengthAwarePaginator;
/**
* Return all of the unique IPs that exist for a given node.
*

View File

@ -3,6 +3,7 @@
namespace Pterodactyl\Contracts\Repository;
use Illuminate\Support\Collection;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
interface RepositoryInterface
{
@ -175,6 +176,14 @@ interface RepositoryInterface
*/
public function all(): Collection;
/**
* Return a paginated result set using a search term if set on the repository.
*
* @param int $perPage
* @return \Illuminate\Contracts\Pagination\LengthAwarePaginator
*/
public function paginated(int $perPage): LengthAwarePaginator;
/**
* Insert a single or multiple records into the database at once skipping
* validation and mass assignment checking.

View File

@ -0,0 +1,9 @@
<?php
namespace Pterodactyl\Exceptions\Service\Allocation;
use Pterodactyl\Exceptions\DisplayException;
class ServerUsingAllocationException extends DisplayException
{
}

View File

@ -0,0 +1,9 @@
<?php
namespace Pterodactyl\Exceptions\Service\Node;
use Pterodactyl\Exceptions\DisplayException;
class ConfigurationNotPersistedException extends DisplayException
{
}

View File

@ -74,7 +74,7 @@ class LocationController extends Controller
*/
public function index(Request $request): array
{
$locations = $this->repository->all(50);
$locations = $this->repository->paginated(100);
return $this->fractal->collection($locations)
->transformWith(new LocationTransformer($request))

View File

@ -0,0 +1,78 @@
<?php
namespace Pterodactyl\Http\Controllers\API\Admin\Nodes;
use Spatie\Fractal\Fractal;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Pterodactyl\Models\Allocation;
use Pterodactyl\Http\Controllers\Controller;
use League\Fractal\Pagination\IlluminatePaginatorAdapter;
use Pterodactyl\Transformers\Api\Admin\AllocationTransformer;
use Pterodactyl\Services\Allocations\AllocationDeletionService;
use Pterodactyl\Contracts\Repository\AllocationRepositoryInterface;
class AllocationController extends Controller
{
/**
* @var \Pterodactyl\Services\Allocations\AllocationDeletionService
*/
private $deletionService;
/**
* @var \Spatie\Fractal\Fractal
*/
private $fractal;
/**
* @var \Pterodactyl\Contracts\Repository\AllocationRepositoryInterface
*/
private $repository;
/**
* AllocationController constructor.
*
* @param \Pterodactyl\Services\Allocations\AllocationDeletionService $deletionService
* @param \Pterodactyl\Contracts\Repository\AllocationRepositoryInterface $repository
* @param \Spatie\Fractal\Fractal $fractal
*/
public function __construct(AllocationDeletionService $deletionService, AllocationRepositoryInterface $repository, Fractal $fractal)
{
$this->deletionService = $deletionService;
$this->fractal = $fractal;
$this->repository = $repository;
}
/**
* Return all of the allocations that exist for a given node.
*
* @param \Illuminate\Http\Request $request
* @param int $node
* @return array
*/
public function index(Request $request, int $node): array
{
$allocations = $this->repository->getPaginatedAllocationsForNode($node, 100);
return $this->fractal->collection($allocations)
->transformWith(new AllocationTransformer($request))
->withResourceName('allocation')
->paginateWith(new IlluminatePaginatorAdapter($allocations))
->toArray();
}
/**
* Delete a specific allocation from the Panel.
*
* @param \Pterodactyl\Models\Allocation $allocation
* @return \Illuminate\Http\Response
*
* @throws \Pterodactyl\Exceptions\Service\Allocation\ServerUsingAllocationException
*/
public function delete(Request $request, int $node, Allocation $allocation): Response
{
$this->deletionService->handle($allocation);
return response('', 204);
}
}

View File

@ -74,7 +74,7 @@ class NodeController extends Controller
*/
public function index(Request $request): array
{
$nodes = $this->repository->all(config('pterodactyl.paginate.api.nodes'));
$nodes = $this->repository->paginated(100);
$fractal = $this->fractal->collection($nodes)
->transformWith(new NodeTransformer($request))

View File

@ -76,7 +76,7 @@ class UserController extends Controller
*/
public function index(Request $request): array
{
$users = $this->repository->all(config('pterodactyl.paginate.api.users'));
$users = $this->repository->paginated(100);
return $this->fractal->collection($users)
->transformWith(new UserTransformer($request))
@ -113,7 +113,6 @@ class UserController extends Controller
* @param \Pterodactyl\Models\User $user
* @return array
*
* @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/

View File

@ -12,6 +12,8 @@ namespace Pterodactyl\Http\Controllers\Admin;
use Javascript;
use Illuminate\Http\Request;
use Pterodactyl\Models\Node;
use Illuminate\Http\Response;
use Pterodactyl\Models\Allocation;
use Prologue\Alerts\AlertsMessageBag;
use Pterodactyl\Http\Controllers\Controller;
use Pterodactyl\Services\Nodes\NodeUpdateService;
@ -23,6 +25,7 @@ use Pterodactyl\Services\Helpers\SoftwareVersionService;
use Pterodactyl\Http\Requests\Admin\Node\NodeFormRequest;
use Pterodactyl\Contracts\Repository\NodeRepositoryInterface;
use Pterodactyl\Http\Requests\Admin\Node\AllocationFormRequest;
use Pterodactyl\Services\Allocations\AllocationDeletionService;
use Pterodactyl\Contracts\Repository\LocationRepositoryInterface;
use Pterodactyl\Contracts\Repository\AllocationRepositoryInterface;
use Pterodactyl\Http\Requests\Admin\Node\AllocationAliasFormRequest;
@ -78,11 +81,16 @@ class NodesController extends Controller
* @var \Pterodactyl\Services\Helpers\SoftwareVersionService
*/
protected $versionService;
/**
* @var \Pterodactyl\Services\Allocations\AllocationDeletionService
*/
private $allocationDeletionService;
/**
* NodesController constructor.
*
* @param \Prologue\Alerts\AlertsMessageBag $alert
* @param \Pterodactyl\Services\Allocations\AllocationDeletionService $allocationDeletionService
* @param \Pterodactyl\Contracts\Repository\AllocationRepositoryInterface $allocationRepository
* @param \Pterodactyl\Services\Allocations\AssignmentService $assignmentService
* @param \Illuminate\Cache\Repository $cache
@ -95,6 +103,7 @@ class NodesController extends Controller
*/
public function __construct(
AlertsMessageBag $alert,
AllocationDeletionService $allocationDeletionService,
AllocationRepositoryInterface $allocationRepository,
AssignmentService $assignmentService,
CacheRepository $cache,
@ -106,6 +115,7 @@ class NodesController extends Controller
SoftwareVersionService $versionService
) {
$this->alert = $alert;
$this->allocationDeletionService = $allocationDeletionService;
$this->allocationRepository = $allocationRepository;
$this->assignmentService = $assignmentService;
$this->cache = $cache;
@ -262,17 +272,14 @@ class NodesController extends Controller
/**
* Removes a single allocation from a node.
*
* @param int $node
* @param int $allocation
* @return \Illuminate\Http\Response|\Illuminate\Http\JsonResponse
* @param \Pterodactyl\Models\Allocation $allocation
* @return \Illuminate\Http\Response
*
* @throws \Pterodactyl\Exceptions\Service\Allocation\ServerUsingAllocationException
*/
public function allocationRemoveSingle($node, $allocation)
public function allocationRemoveSingle(Allocation $allocation): Response
{
$this->allocationRepository->deleteWhere([
['id', '=', $allocation],
['node_id', '=', $node],
['server_id', '=', null],
]);
$this->allocationDeletionService->handle($allocation);
return response('', 204);
}

View File

@ -105,4 +105,14 @@ class Allocation extends Model implements CleansAttributes, ValidableContract
{
return $this->belongsTo(Server::class);
}
/**
* Return the Node model associated with this allocation.
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function node()
{
return $this->belongsTo(Node::class);
}
}

View File

@ -2,8 +2,10 @@
namespace Pterodactyl\Repositories\Eloquent;
use Pterodactyl\Models\Node;
use Illuminate\Support\Collection;
use Pterodactyl\Models\Allocation;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Pterodactyl\Contracts\Repository\AllocationRepositoryInterface;
class AllocationRepository extends EloquentRepository implements AllocationRepositoryInterface
@ -41,6 +43,18 @@ class AllocationRepository extends EloquentRepository implements AllocationRepos
return $this->getBuilder()->where('node_id', $node)->get($this->getColumns());
}
/**
* Return all of the allocations for a node in a paginated format.
*
* @param int $node
* @param int $perPage
* @return \Illuminate\Contracts\Pagination\LengthAwarePaginator
*/
public function getPaginatedAllocationsForNode(int $node, int $perPage = 100): LengthAwarePaginator
{
return $this->getBuilder()->where('node_id', $node)->paginate($perPage, $this->getColumns());
}
/**
* Return all of the unique IPs that exist for a given node.
*

View File

@ -7,6 +7,7 @@ use Illuminate\Support\Collection;
use Pterodactyl\Repositories\Repository;
use Illuminate\Database\Query\Expression;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Pterodactyl\Contracts\Repository\RepositoryInterface;
use Pterodactyl\Exceptions\Model\DataValidationException;
use Pterodactyl\Exceptions\Repository\RecordNotFoundException;
@ -234,6 +235,22 @@ abstract class EloquentRepository extends Repository implements RepositoryInterf
return $instance->get($this->getColumns());
}
/**
* Return a paginated result set using a search term if set on the repository.
*
* @param int $perPage
* @return \Illuminate\Contracts\Pagination\LengthAwarePaginator
*/
public function paginated(int $perPage): LengthAwarePaginator
{
$instance = $this->getBuilder();
if (is_subclass_of(get_called_class(), SearchableInterface::class) && $this->hasSearchTerm()) {
$instance = $instance->search($this->getSearchTerm());
}
return $instance->paginate($perPage, $this->getColumns());
}
/**
* Insert a single or multiple records into the database at once skipping
* validation and mass assignment checking.

View File

@ -0,0 +1,43 @@
<?php
namespace Pterodactyl\Services\Allocations;
use Pterodactyl\Models\Allocation;
use Pterodactyl\Contracts\Repository\AllocationRepositoryInterface;
use Pterodactyl\Exceptions\Service\Allocation\ServerUsingAllocationException;
class AllocationDeletionService
{
/**
* @var \Pterodactyl\Contracts\Repository\AllocationRepositoryInterface
*/
private $repository;
/**
* AllocationDeletionService constructor.
*
* @param \Pterodactyl\Contracts\Repository\AllocationRepositoryInterface $repository
*/
public function __construct(AllocationRepositoryInterface $repository)
{
$this->repository = $repository;
}
/**
* Delete an allocation from the database only if it does not have a server
* that is actively attached to it.
*
* @param \Pterodactyl\Models\Allocation $allocation
* @return int
*
* @throws \Pterodactyl\Exceptions\Service\Allocation\ServerUsingAllocationException
*/
public function handle(Allocation $allocation)
{
if (! is_null($allocation->server_id)) {
throw new ServerUsingAllocationException(trans('exceptions.allocations.server_using'));
}
return $this->repository->delete($allocation->id);
}
}

View File

@ -10,16 +10,24 @@
namespace Pterodactyl\Services\Nodes;
use Pterodactyl\Models\Node;
use GuzzleHttp\Exception\ConnectException;
use GuzzleHttp\Exception\RequestException;
use Illuminate\Database\ConnectionInterface;
use Pterodactyl\Traits\Services\ReturnsUpdatedModels;
use Pterodactyl\Contracts\Repository\NodeRepositoryInterface;
use Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException;
use Pterodactyl\Exceptions\Service\Node\ConfigurationNotPersistedException;
use Pterodactyl\Contracts\Repository\Daemon\ConfigurationRepositoryInterface;
class NodeUpdateService
{
use ReturnsUpdatedModels;
/**
* @var \Illuminate\Database\ConnectionInterface
*/
private $connection;
/**
* @var \Pterodactyl\Contracts\Repository\Daemon\ConfigurationRepositoryInterface
*/
@ -33,13 +41,16 @@ class NodeUpdateService
/**
* UpdateService constructor.
*
* @param \Illuminate\Database\ConnectionInterface $connection
* @param \Pterodactyl\Contracts\Repository\Daemon\ConfigurationRepositoryInterface $configurationRepository
* @param \Pterodactyl\Contracts\Repository\NodeRepositoryInterface $repository
*/
public function __construct(
ConnectionInterface $connection,
ConfigurationRepositoryInterface $configurationRepository,
NodeRepositoryInterface $repository
) {
$this->connection = $connection;
$this->configRepository = $configurationRepository;
$this->repository = $repository;
}
@ -62,6 +73,7 @@ class NodeUpdateService
unset($data['reset_secret']);
}
$this->connection->beginTransaction();
if ($this->getUpdatedModel()) {
$response = $this->repository->update($node->id, $data);
} else {
@ -70,7 +82,16 @@ class NodeUpdateService
try {
$this->configRepository->setNode($node)->update();
$this->connection->commit();
} catch (RequestException $exception) {
// Failed to connect to the Daemon. Let's go ahead and save the configuration
// and let the user know they'll need to manually update.
if ($exception instanceof ConnectException) {
$this->connection->commit();
throw new ConfigurationNotPersistedException(trans('exceptions.node.daemon_off_config_updated'));
}
throw new DaemonConnectionException($exception);
}

View File

@ -7,6 +7,16 @@ use Pterodactyl\Transformers\Api\ApiTransformer;
class AllocationTransformer extends ApiTransformer
{
/**
* Relationships that can be loaded onto allocation transformations.
*
* @var array
*/
protected $availableIncludes = [
'node',
'server',
];
/**
* Return a generic transformed allocation array.
*
@ -15,17 +25,50 @@ class AllocationTransformer extends ApiTransformer
*/
public function transform(Allocation $allocation)
{
return $this->transformWithFilter($allocation);
return [
'id' => $allocation->id,
'ip' => $allocation->ip,
'alias' => $allocation->ip_alias,
'port' => $allocation->port,
'assigned' => ! is_null($allocation->server_id),
];
}
/**
* Determine which transformer filter to apply.
* Load the node relationship onto a given transformation.
*
* @param \Pterodactyl\Models\Allocation $allocation
* @return array
* @return bool|\League\Fractal\Resource\Item
*
* @throws \Pterodactyl\Exceptions\PterodactylException
*/
protected function transformWithFilter(Allocation $allocation)
public function includeNode(Allocation $allocation)
{
return $allocation->toArray();
if (! $this->authorize('node-view')) {
return false;
}
$allocation->loadMissing('node');
return $this->item($allocation->getRelation('node'), new NodeTransformer($this->getRequest()), 'node');
}
/**
* Load the server relationship onto a given transformation.
*
* @param \Pterodactyl\Models\Allocation $allocation
* @return bool|\League\Fractal\Resource\Item
*
* @throws \Pterodactyl\Exceptions\PterodactylException
*/
public function includeServer(Allocation $allocation)
{
if (! $this->authorize('server-view')) {
return false;
}
$allocation->loadMissing('server');
return $this->item($allocation->getRelation('server'), new ServerTransformer($this->getRequest()), 'server');
}
}

View File

@ -35,13 +35,11 @@ class UserTransformer extends ApiTransformer
*/
public function includeServers(User $user)
{
if ($this->authorize('server-list')) {
if (! $this->authorize('server-list')) {
return false;
}
if (! $user->relationLoaded('servers')) {
$user->load('servers');
}
$user->loadMissing('servers');
return $this->collection($user->getRelation('servers'), new ServerTransformer($this->getRequest()), 'server');
}

View File

@ -1,19 +1,13 @@
<?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
*/
return [
'daemon_connection_failed' => 'There was an exception while attempting to communicate with the daemon resulting in a HTTP/:code response code. This exception has been logged.',
'node' => [
'servers_attached' => 'A node must have no servers linked to it in order to be deleted.',
'daemon_off_config_updated' => 'The daemon configuration <strong>has been updated</strong>, however there was an error encountered while attempting to automatically update the configuration file on the Daemon. You will need to manually update the configuration file (core.json) for the daemon to apply these changes. The daemon responded with a HTTP/:code response code and the error has been logged.',
'daemon_off_config_updated' => 'The daemon configuration <strong>has been updated</strong>, however there was an error encountered while attempting to automatically update the configuration file on the Daemon. You will need to manually update the configuration file (core.json) for the daemon to apply these changes.',
],
'allocations' => [
'server_using' => 'A server is currently assigned to this allocation. An allocation can only be deleted if no server is currently assigned.',
'too_many_ports' => 'Adding more than 1000 ports at a single time is not supported. Please use a smaller range.',
'invalid_mapping' => 'The mapping provided for :port was invalid and could not be processed.',
'cidr_out_of_range' => 'CIDR notation only allows masks between /25 and /32.',

View File

@ -34,6 +34,12 @@ Route::group(['prefix' => '/nodes'], function () {
Route::patch('/{node}', 'Nodes\NodeController@update')->name('api.admin.node.update');
Route::delete('/{node}', 'Nodes\NodeController@delete')->name('api.admin.node.delete');
Route::group(['prefix' => '/{node}/allocations'], function () {
Route::get('/', 'Nodes\AllocationController@index')->name('api.admin.node.allocations.list');
Route::delete('/{allocation}', 'Nodes\AllocationController@delete')->name('api.admin.node.allocations.delete');
});
});
/*

View File

@ -21,29 +21,23 @@ trait MocksRequestException
/**
* Configure the exception mock to work with the Panel's default exception
* handler actions.
*
* @param string $abstract
* @param null $response
*/
public function configureExceptionMock()
protected function configureExceptionMock(string $abstract = RequestException::class, $response = null)
{
$this->getExceptionMock()->shouldReceive('getResponse')->andReturn($this->exceptionResponse);
$this->getExceptionMock($abstract)->shouldReceive('getResponse')->andReturn(value($response));
}
/**
* Return a mocked instance of the request exception.
*
* @param string $abstract
* @return \Mockery\MockInterface
*/
private function getExceptionMock(): MockInterface
protected function getExceptionMock(string $abstract = RequestException::class): MockInterface
{
return $this->exception ?? $this->exception = Mockery::mock(RequestException::class);
}
/**
* Set the exception response.
*
* @param mixed $response
*/
protected function setExceptionResponse($response)
{
$this->exceptionResponse = $response;
return $this->exception ?? $this->exception = Mockery::mock($abstract);
}
}

View File

@ -0,0 +1,59 @@
<?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;
class AllocationDeletionServiceTest extends TestCase
{
/**
* @var \Pterodactyl\Contracts\Repository\AllocationRepositoryInterface|\Mockery\Mock
*/
private $repository;
public function setUp()
{
parent::setUp();
$this->repository = m::mock(AllocationRepositoryInterface::class);
}
/**
* Test that an allocation is deleted.
*/
public function testAllocationIsDeleted()
{
$model = factory(Allocation::class)->make();
$this->repository->shouldReceive('delete')->with($model->id)->once()->andReturn(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.
*
* @expectedException \Pterodactyl\Exceptions\Service\Allocation\ServerUsingAllocationException
*/
public function testExceptionThrownIfAssignedToServer()
{
$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

@ -12,49 +12,34 @@ namespace Tests\Unit\Services\Nodes;
use Exception;
use Mockery as m;
use Tests\TestCase;
use Illuminate\Log\Writer;
use phpmock\phpunit\PHPMock;
use Pterodactyl\Models\Node;
use GuzzleHttp\Psr7\Response;
use GuzzleHttp\Exception\RequestException;
use Pterodactyl\Exceptions\DisplayException;
use Tests\Traits\MocksRequestException;
use GuzzleHttp\Exception\ConnectException;
use Illuminate\Database\ConnectionInterface;
use Pterodactyl\Services\Nodes\NodeUpdateService;
use Pterodactyl\Contracts\Repository\NodeRepositoryInterface;
use Pterodactyl\Contracts\Repository\Daemon\ConfigurationRepositoryInterface;
class NodeUpdateServiceTest extends TestCase
{
use PHPMock;
use PHPMock, MocksRequestException;
/**
* @var \Illuminate\Database\ConnectionInterface|\Mockery\Mock
*/
private $connection;
/**
* @var \Pterodactyl\Contracts\Repository\Daemon\ConfigurationRepositoryInterface|\Mockery\Mock
*/
protected $configRepository;
/**
* @var \GuzzleHttp\Exception\RequestException|\Mockery\Mock
*/
protected $exception;
/**
* @var \Pterodactyl\Models\Node
*/
protected $node;
private $configRepository;
/**
* @var \Pterodactyl\Contracts\Repository\NodeRepositoryInterface|\Mockery\Mock
*/
protected $repository;
/**
* @var \Pterodactyl\Services\Nodes\NodeUpdateService
*/
protected $service;
/**
* @var \Illuminate\Log\Writer|\Mockery\Mock
*/
protected $writer;
private $repository;
/**
* Setup tests.
@ -63,18 +48,9 @@ class NodeUpdateServiceTest extends TestCase
{
parent::setUp();
$this->node = factory(Node::class)->make();
$this->connection = m::mock(ConnectionInterface::class);
$this->configRepository = m::mock(ConfigurationRepositoryInterface::class);
$this->exception = m::mock(RequestException::class);
$this->repository = m::mock(NodeRepositoryInterface::class);
$this->writer = m::mock(Writer::class);
$this->service = new NodeUpdateService(
$this->configRepository,
$this->repository,
$this->writer
);
}
/**
@ -82,21 +58,23 @@ class NodeUpdateServiceTest extends TestCase
*/
public function testNodeIsUpdatedAndDaemonSecretIsReset()
{
$model = factory(Node::class)->make();
$this->getFunctionMock('\\Pterodactyl\\Services\\Nodes', 'str_random')
->expects($this->once())->willReturn('random_string');
$this->repository->shouldReceive('withoutFreshModel')->withNoArgs()->once()->andReturnSelf()
->shouldReceive('update')->with($this->node->id, [
'name' => 'NewName',
'daemonSecret' => 'random_string',
])->andReturn(true);
$this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull();
$this->repository->shouldReceive('withoutFreshModel->update')->with($model->id, [
'name' => 'NewName',
'daemonSecret' => 'random_string',
])->andReturn(true);
$this->configRepository->shouldReceive('setNode')->with($this->node)->once()->andReturnSelf()
$this->configRepository->shouldReceive('setNode')->with($model)->once()->andReturnSelf()
->shouldReceive('update')->withNoArgs()->once()->andReturn(new Response);
$this->connection->shouldReceive('commit')->withNoArgs()->once()->andReturnNull();
$response = $this->service->handle($this->node, ['name' => 'NewName', 'reset_secret' => true]);
$this->assertInstanceOf(Node::class, $response);
$this->assertSame($this->node, $response);
$response = $this->getService()->returnUpdatedModel(false)->handle($model, ['name' => 'NewName', 'reset_secret' => true]);
$this->assertTrue($response);
}
/**
@ -104,59 +82,85 @@ class NodeUpdateServiceTest extends TestCase
*/
public function testNodeIsUpdatedAndDaemonSecretIsNotChanged()
{
$this->repository->shouldReceive('withoutFreshModel')->withNoArgs()->once()->andReturnSelf()
->shouldReceive('update')->with($this->node->id, [
'name' => 'NewName',
])->andReturn(true);
$model = factory(Node::class)->make();
$this->configRepository->shouldReceive('setNode')->with($this->node)->once()->andReturnSelf()
$this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull();
$this->repository->shouldReceive('withoutFreshModel->update')->with($model->id, [
'name' => 'NewName',
])->andReturn(true);
$this->configRepository->shouldReceive('setNode')->with($model)->once()->andReturnSelf()
->shouldReceive('update')->withNoArgs()->once()->andReturn(new Response);
$this->connection->shouldReceive('commit')->withNoArgs()->once()->andReturnNull();
$response = $this->service->handle($this->node, ['name' => 'NewName']);
$response = $this->getService()->returnUpdatedModel(false)->handle($model, ['name' => 'NewName']);
$this->assertTrue($response);
}
public function testUpdatedModelIsReturned()
{
$model = factory(Node::class)->make();
$updated = clone $model;
$updated->name = 'NewName';
$this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull();
$this->repository->shouldReceive('update')->with($model->id, [
'name' => $updated->name,
])->andReturn($updated);
$this->configRepository->shouldReceive('setNode')->with($model)->once()->andReturnSelf()
->shouldReceive('update')->withNoArgs()->once()->andReturn(new Response);
$this->connection->shouldReceive('commit')->withNoArgs()->once()->andReturnNull();
$response = $this->getService()->returnUpdatedModel()->handle($model, ['name' => $updated->name]);
$this->assertInstanceOf(Node::class, $response);
$this->assertSame($this->node, $response);
$this->assertSame($updated, $response);
}
/**
* Test that an exception caused by the daemon is handled properly.
* Test that an exception caused by a connection error is handled.
*
* @expectedException \Pterodactyl\Exceptions\Service\Node\ConfigurationNotPersistedException
*/
public function testExceptionCausedByDaemonIsHandled()
public function testExceptionRelatedToConnection()
{
$this->repository->shouldReceive('withoutFreshModel')->withNoArgs()->once()->andReturnSelf()
->shouldReceive('update')->with($this->node->id, [
'name' => 'NewName',
])->andReturn(new Response);
$this->configureExceptionMock(ConnectException::class);
$model = factory(Node::class)->make();
$this->configRepository->shouldReceive('setNode')->with($this->node)->once()->andThrow($this->exception);
$this->writer->shouldReceive('warning')->with($this->exception)->once()->andReturnNull();
$this->exception->shouldReceive('getResponse')->withNoArgs()->once()->andReturnSelf()
->shouldReceive('getStatusCode')->withNoArgs()->once()->andReturn(400);
$this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull();
$this->repository->shouldReceive('withoutFreshModel->update')->andReturn(new Response);
try {
$this->service->handle($this->node, ['name' => 'NewName']);
} catch (Exception $exception) {
$this->assertInstanceOf(DisplayException::class, $exception);
$this->assertEquals(
trans('exceptions.node.daemon_off_config_updated', ['code' => 400]),
$exception->getMessage()
);
}
$this->configRepository->shouldReceive('setNode->update')->once()->andThrow($this->getExceptionMock());
$this->connection->shouldReceive('commit')->withNoArgs()->once()->andReturnNull();
$this->getService()->handle($model, ['name' => 'NewName']);
}
/**
* Test that an ID can be passed in place of a model.
* Test that an exception not caused by a daemon connection error is handled.
*
* @expectedException \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException
*/
public function testFunctionCanAcceptANodeIdInPlaceOfModel()
public function testExceptionNotRelatedToConnection()
{
$this->repository->shouldReceive('find')->with($this->node->id)->once()->andReturn($this->node);
$this->repository->shouldReceive('withoutFreshModel')->withNoArgs()->once()->andReturnSelf()
->shouldReceive('update')->with($this->node->id, [
'name' => 'NewName',
])->andReturn(true);
$this->configureExceptionMock();
$model = factory(Node::class)->make();
$this->configRepository->shouldReceive('setNode')->with($this->node)->once()->andReturnSelf()
->shouldReceive('update')->withNoArgs()->once()->andReturn(new Response);
$this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull();
$this->repository->shouldReceive('withoutFreshModel->update')->andReturn(new Response);
$this->assertTrue($this->service->handle($this->node->id, ['name' => 'NewName']));
$this->configRepository->shouldReceive('setNode->update')->once()->andThrow($this->getExceptionMock());
$this->getService()->handle($model, ['name' => 'NewName']);
}
/**
* Return an instance of the service with mocked injections.
*
* @return \Pterodactyl\Services\Nodes\NodeUpdateService
*/
private function getService(): NodeUpdateService
{
return new NodeUpdateService($this->connection, $this->configRepository, $this->repository);
}
}