From 17f6f3eeb650b88ffad301f86eb992214e7c4a59 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sat, 20 Jan 2018 16:03:23 -0600 Subject: [PATCH] Add server details modification endpoint to API. --- .../Controllers/Admin/ServersController.php | 2 +- .../Servers/ServerDetailsController.php | 47 ++++ .../Servers/UpdateServerDetailsRequest.php | 53 ++++ app/Models/Server.php | 2 +- .../Servers/DetailsModificationService.php | 94 ++------ routes/api-application.php | 2 + .../DetailsModificationServiceTest.php | 227 +++--------------- 7 files changed, 160 insertions(+), 267 deletions(-) create mode 100644 app/Http/Controllers/Api/Application/Servers/ServerDetailsController.php create mode 100644 app/Http/Requests/Api/Application/Servers/UpdateServerDetailsRequest.php diff --git a/app/Http/Controllers/Admin/ServersController.php b/app/Http/Controllers/Admin/ServersController.php index c3e33b964..75063335e 100644 --- a/app/Http/Controllers/Admin/ServersController.php +++ b/app/Http/Controllers/Admin/ServersController.php @@ -401,7 +401,7 @@ class ServersController extends Controller */ public function setDetails(Request $request, Server $server) { - $this->detailsModificationService->edit($server, $request->only([ + $this->detailsModificationService->handle($server, $request->only([ 'owner_id', 'name', 'description', ])); diff --git a/app/Http/Controllers/Api/Application/Servers/ServerDetailsController.php b/app/Http/Controllers/Api/Application/Servers/ServerDetailsController.php new file mode 100644 index 000000000..638948590 --- /dev/null +++ b/app/Http/Controllers/Api/Application/Servers/ServerDetailsController.php @@ -0,0 +1,47 @@ +modificationService = $modificationService; + } + + /** + * @param \Pterodactyl\Http\Requests\Api\Application\Servers\UpdateServerDetailsRequest $request + * @param \Pterodactyl\Models\Server $server + * @return array + * + * @throws \Pterodactyl\Exceptions\DisplayException + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + */ + public function details(UpdateServerDetailsRequest $request, Server $server): array + { + $server = $this->modificationService->returnUpdatedModel()->handle($server, $request->validated()); + + return $this->fractal->item($server) + ->transformWith($this->getTransformer(ServerTransformer::class)) + ->toArray(); + } +} diff --git a/app/Http/Requests/Api/Application/Servers/UpdateServerDetailsRequest.php b/app/Http/Requests/Api/Application/Servers/UpdateServerDetailsRequest.php new file mode 100644 index 000000000..011293b76 --- /dev/null +++ b/app/Http/Requests/Api/Application/Servers/UpdateServerDetailsRequest.php @@ -0,0 +1,53 @@ +route()->parameter('server')->id); + + return [ + 'name' => $rules['name'], + 'user' => $rules['owner_id'], + 'description' => $rules['description'], + ]; + } + + /** + * Convert the posted data into the correct format that is expected + * by the application. + * + * @return array + */ + public function validated(): array + { + return [ + 'name' => $this->input('name'), + 'owner_id' => $this->input('user'), + 'description' => $this->input('description'), + ]; + } + + /** + * Rename some of the attributes in error messages to clarify the field + * being discussed. + * + * @return array + */ + public function attributes(): array + { + return [ + 'user' => 'User ID', + 'name' => 'Server Name', + ]; + } +} diff --git a/app/Models/Server.php b/app/Models/Server.php index 01c5eef15..d09291e56 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -80,7 +80,7 @@ class Server extends Model implements CleansAttributes, ValidableContract */ protected static $dataIntegrityRules = [ 'owner_id' => 'exists:users,id', - 'name' => 'regex:/^([\w .-]{1,200})$/', + 'name' => 'string|min:1|max:255', 'node_id' => 'exists:nodes,id', 'description' => 'string', 'memory' => 'numeric|min:0', diff --git a/app/Services/Servers/DetailsModificationService.php b/app/Services/Servers/DetailsModificationService.php index 5ba4f779b..cabebf254 100644 --- a/app/Services/Servers/DetailsModificationService.php +++ b/app/Services/Servers/DetailsModificationService.php @@ -1,55 +1,37 @@ . - * - * This software is licensed under the terms of the MIT license. - * https://opensource.org/licenses/MIT - */ namespace Pterodactyl\Services\Servers; -use Illuminate\Log\Writer; use Pterodactyl\Models\Server; -use GuzzleHttp\Exception\RequestException; use Illuminate\Database\ConnectionInterface; -use Pterodactyl\Exceptions\DisplayException; +use Pterodactyl\Traits\Services\ReturnsUpdatedModels; use Pterodactyl\Repositories\Eloquent\ServerRepository; use Pterodactyl\Services\DaemonKeys\DaemonKeyCreationService; use Pterodactyl\Services\DaemonKeys\DaemonKeyDeletionService; -use Pterodactyl\Repositories\Daemon\ServerRepository as DaemonServerRepository; class DetailsModificationService { + use ReturnsUpdatedModels; + /** * @var \Illuminate\Database\ConnectionInterface */ - protected $connection; - - /** - * @var \Pterodactyl\Repositories\Daemon\ServerRepository - */ - protected $daemonServerRepository; + private $connection; /** * @var \Pterodactyl\Services\DaemonKeys\DaemonKeyCreationService */ - protected $keyCreationService; + private $keyCreationService; /** * @var \Pterodactyl\Services\DaemonKeys\DaemonKeyDeletionService */ - protected $keyDeletionService; + private $keyDeletionService; /** * @var \Pterodactyl\Repositories\Eloquent\ServerRepository */ - protected $repository; - - /** - * @var \Illuminate\Log\Writer - */ - protected $writer; + private $repository; /** * DetailsModificationService constructor. @@ -57,92 +39,48 @@ class DetailsModificationService * @param \Illuminate\Database\ConnectionInterface $connection * @param \Pterodactyl\Services\DaemonKeys\DaemonKeyCreationService $keyCreationService * @param \Pterodactyl\Services\DaemonKeys\DaemonKeyDeletionService $keyDeletionService - * @param \Pterodactyl\Repositories\Daemon\ServerRepository $daemonServerRepository * @param \Pterodactyl\Repositories\Eloquent\ServerRepository $repository - * @param \Illuminate\Log\Writer $writer */ public function __construct( ConnectionInterface $connection, DaemonKeyCreationService $keyCreationService, DaemonKeyDeletionService $keyDeletionService, - DaemonServerRepository $daemonServerRepository, - ServerRepository $repository, - Writer $writer + ServerRepository $repository ) { $this->connection = $connection; - $this->daemonServerRepository = $daemonServerRepository; $this->keyCreationService = $keyCreationService; $this->keyDeletionService = $keyDeletionService; $this->repository = $repository; - $this->writer = $writer; } /** * Update the details for a single server instance. * - * @param int|\Pterodactyl\Models\Server $server - * @param array $data + * @param \Pterodactyl\Models\Server $server + * @param array $data + * @return bool|\Pterodactyl\Models\Server * * @throws \Pterodactyl\Exceptions\DisplayException * @throws \Pterodactyl\Exceptions\Model\DataValidationException * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException */ - public function edit($server, array $data) + public function handle(Server $server, array $data) { - if (! $server instanceof Server) { - $server = $this->repository->find($server); - } - $this->connection->beginTransaction(); - $this->repository->withoutFreshModel()->update($server->id, [ + + $response = $this->repository->setFreshModel($this->getUpdatedModel())->update($server->id, [ 'owner_id' => array_get($data, 'owner_id'), 'name' => array_get($data, 'name'), 'description' => array_get($data, 'description', ''), ], true, true); - if (array_get($data, 'owner_id') != $server->owner_id) { + if ((int) array_get($data, 'owner_id', 0) !== (int) $server->owner_id) { $this->keyDeletionService->handle($server, $server->owner_id); $this->keyCreationService->handle($server->id, array_get($data, 'owner_id')); } $this->connection->commit(); - } - /** - * Update the docker container for a specified server. - * - * @param int|\Pterodactyl\Models\Server $server - * @param string $image - * - * @throws \Pterodactyl\Exceptions\DisplayException - * @throws \Pterodactyl\Exceptions\Model\DataValidationException - * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException - */ - public function setDockerImage($server, $image) - { - if (! $server instanceof Server) { - $server = $this->repository->find($server); - } - - $this->connection->beginTransaction(); - $this->repository->withoutFreshModel()->update($server->id, ['image' => $image]); - - try { - $this->daemonServerRepository->setServer($server)->update([ - 'build' => [ - 'image' => $image, - ], - ]); - } catch (RequestException $exception) { - $this->connection->rollBack(); - $response = $exception->getResponse(); - $this->writer->warning($exception); - - throw new DisplayException(trans('admin/server.exceptions.daemon_exception', [ - 'code' => is_null($response) ? 'E_CONN_REFUSED' : $response->getStatusCode(), - ])); - } - - $this->connection->commit(); + return $response; } } diff --git a/routes/api-application.php b/routes/api-application.php index 83ea5a2a0..f98b3739b 100644 --- a/routes/api-application.php +++ b/routes/api-application.php @@ -72,6 +72,8 @@ Route::group(['prefix' => '/servers'], function () { Route::get('/', 'Servers\ServerController@index')->name('api.application.servers'); Route::get('/{server}', 'Servers\ServerController@view')->name('api.application.servers.view'); + Route::patch('/{server}/details', 'Servers\ServerDetailsController@details')->name('api.application.servers.details'); + Route::post('/{server}/suspend', 'Servers\ServerManagementController@suspend')->name('api.application.servers.suspend'); Route::post('/{server}/unsuspend', 'Servers\ServerManagementController@unsuspend')->name('api.application.servers.unsuspend'); Route::post('/{server}/reinstall', 'Servers\ServerManagementController@reinstall')->name('api.application.servers.reinstall'); diff --git a/tests/Unit/Services/Servers/DetailsModificationServiceTest.php b/tests/Unit/Services/Servers/DetailsModificationServiceTest.php index 410ec5b95..bd049d018 100644 --- a/tests/Unit/Services/Servers/DetailsModificationServiceTest.php +++ b/tests/Unit/Services/Servers/DetailsModificationServiceTest.php @@ -1,71 +1,37 @@ . - * - * 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 Illuminate\Log\Writer; -use GuzzleHttp\Psr7\Response; use Pterodactyl\Models\Server; -use GuzzleHttp\Exception\RequestException; use Illuminate\Database\ConnectionInterface; -use Pterodactyl\Exceptions\DisplayException; -use Pterodactyl\Exceptions\PterodactylException; use Pterodactyl\Repositories\Eloquent\ServerRepository; use Pterodactyl\Services\Servers\DetailsModificationService; use Pterodactyl\Services\DaemonKeys\DaemonKeyCreationService; use Pterodactyl\Services\DaemonKeys\DaemonKeyDeletionService; -use Pterodactyl\Repositories\Daemon\ServerRepository as DaemonServerRepository; class DetailsModificationServiceTest extends TestCase { /** * @var \Illuminate\Database\ConnectionInterface|\Mockery\Mock */ - protected $connection; - - /** - * @var \Pterodactyl\Repositories\Daemon\ServerRepository|\Mockery\Mock - */ - protected $daemonServerRepository; - - /** - * @var \GuzzleHttp\Exception\RequestException|\Mockery\Mock - */ - protected $exception; + private $connection; /** * @var \Pterodactyl\Services\DaemonKeys\DaemonKeyCreationService|\Mockery\Mock */ - protected $keyCreationService; + private $keyCreationService; /** * @var \Pterodactyl\Services\DaemonKeys\DaemonKeyDeletionService|\Mockery\Mock */ - protected $keyDeletionService; + private $keyDeletionService; /** * @var \Pterodactyl\Repositories\Eloquent\ServerRepository|\Mockery\Mock */ - protected $repository; - - /** - * @var \Pterodactyl\Services\Servers\DetailsModificationService - */ - protected $service; - - /** - * @var \Illuminate\Log\Writer|\Mockery\Mock - */ - protected $writer; + private $repository; /** * Setup tests. @@ -75,72 +41,49 @@ class DetailsModificationServiceTest extends TestCase parent::setUp(); $this->connection = m::mock(ConnectionInterface::class); - $this->exception = m::mock(RequestException::class)->makePartial(); - $this->daemonServerRepository = m::mock(DaemonServerRepository::class); $this->keyCreationService = m::mock(DaemonKeyCreationService::class); $this->keyDeletionService = m::mock(DaemonKeyDeletionService::class); $this->repository = m::mock(ServerRepository::class); - $this->writer = m::mock(Writer::class); - - $this->service = new DetailsModificationService( - $this->connection, - $this->keyCreationService, - $this->keyDeletionService, - $this->daemonServerRepository, - $this->repository, - $this->writer - ); } /** * Test basic updating of core variables when a model is provided. */ - public function testEditShouldSkipDatabaseSearchIfModelIsPassed() + public function testDetailsAreEdited() { - $server = factory(Server::class)->make([ - 'owner_id' => 1, - ]); + $server = factory(Server::class)->make(['owner_id' => 1]); $data = ['owner_id' => 1, 'name' => 'New Name', 'description' => 'New Description']; - $this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); - $this->repository->shouldReceive('withoutFreshModel')->withNoArgs()->once()->andReturnSelf() - ->shouldReceive('update')->with($server->id, [ + $this->connection->shouldReceive('beginTransaction')->once()->withNoArgs()->andReturnNull(); + $this->repository->shouldReceive('setFreshModel')->once()->with(false)->andReturnSelf(); + $this->repository->shouldReceive('update')->once()->with($server->id, [ 'owner_id' => $data['owner_id'], 'name' => $data['name'], 'description' => $data['description'], - ], true, true)->once()->andReturnNull(); + ], true, true)->andReturn(true); - $this->connection->shouldReceive('commit')->withNoArgs()->once()->andReturnNull(); + $this->connection->shouldReceive('commit')->once()->withNoArgs()->andReturnNull(); - $this->service->edit($server, $data); - $this->assertTrue(true); + $response = $this->getService()->handle($server, $data); + $this->assertTrue($response); } /** - * Test that repository attempts to find model in database if no model is passed. + * Test that a model is returned if requested. */ - public function testEditShouldGetModelFromRepositoryIfNotPassed() + public function testModelIsReturned() { - $server = factory(Server::class)->make([ - 'owner_id' => 1, - ]); + $server = factory(Server::class)->make(['owner_id' => 1]); - $data = ['owner_id' => 1, 'name' => 'New Name', 'description' => 'New Description']; + $this->connection->shouldReceive('beginTransaction')->once()->withNoArgs()->andReturnNull(); + $this->repository->shouldReceive('setFreshModel')->once()->with(true)->andReturnSelf(); + $this->repository->shouldReceive('update')->once()->andReturn($server); - $this->repository->shouldReceive('find')->with($server->id)->once()->andReturn($server); - $this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); - $this->repository->shouldReceive('withoutFreshModel')->withNoArgs()->once()->andReturnSelf() - ->shouldReceive('update')->with($server->id, [ - 'owner_id' => $data['owner_id'], - 'name' => $data['name'], - 'description' => $data['description'], - ], true, true)->once()->andReturnNull(); + $this->connection->shouldReceive('commit')->once()->withNoArgs()->andReturnNull(); - $this->connection->shouldReceive('commit')->withNoArgs()->once()->andReturnNull(); - - $this->service->edit($server->id, $data); - $this->assertTrue(true); + $response = $this->getService()->returnUpdatedModel()->handle($server, ['owner_id' => 1]); + $this->assertInstanceOf(Server::class, $response); } /** @@ -150,128 +93,38 @@ class DetailsModificationServiceTest extends TestCase { $server = factory(Server::class)->make([ 'owner_id' => 1, - 'node_id' => 1, ]); $data = ['owner_id' => 2, 'name' => 'New Name', 'description' => 'New Description']; - $this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); - $this->repository->shouldReceive('withoutFreshModel')->withNoArgs()->once()->andReturnSelf() - ->shouldReceive('update')->with($server->id, [ + $this->connection->shouldReceive('beginTransaction')->once()->withNoArgs()->andReturnNull(); + $this->repository->shouldReceive('setFreshModel')->once()->with(false)->andReturnSelf(); + $this->repository->shouldReceive('update')->once()->with($server->id, [ 'owner_id' => $data['owner_id'], 'name' => $data['name'], 'description' => $data['description'], - ], true, true)->once()->andReturnNull(); + ], true, true)->andReturn(true); - $this->keyDeletionService->shouldReceive('handle')->with($server, $server->owner_id)->once()->andReturnNull(); - $this->keyCreationService->shouldReceive('handle')->with($server->id, $data['owner_id']); - $this->connection->shouldReceive('commit')->withNoArgs()->once()->andReturnNull(); + $this->keyDeletionService->shouldReceive('handle')->once()->with($server, $server->owner_id)->andReturnNull(); + $this->keyCreationService->shouldReceive('handle')->once()->with($server->id, $data['owner_id'])->andReturnNull(); + $this->connection->shouldReceive('commit')->once()->withNoArgs()->andReturnNull(); - $this->service->edit($server, $data); - $this->assertTrue(true); + $response = $this->getService()->handle($server, $data); + $this->assertTrue($response); } /** - * Test that the docker image for a server can be updated if a model is provided. - */ - public function testDockerImageCanBeUpdatedWhenAServerModelIsProvided() - { - $server = factory(Server::class)->make(['node_id' => 1]); - - $this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); - $this->repository->shouldReceive('withoutFreshModel')->withNoArgs()->once()->andReturnSelf() - ->shouldReceive('update')->with($server->id, [ - 'image' => 'new/image', - ])->once()->andReturnNull(); - - $this->daemonServerRepository->shouldReceive('setServer')->with($server)->once()->andReturnSelf() - ->shouldReceive('update')->with([ - 'build' => [ - 'image' => 'new/image', - ], - ])->once()->andReturn(new Response); - - $this->connection->shouldReceive('commit')->withNoArgs()->once()->andReturnNull(); - - $this->service->setDockerImage($server, 'new/image'); - $this->assertTrue(true); - } - - /** - * Test that the docker image for a server can be updated if a model is provided. - */ - public function testDockerImageCanBeUpdatedWhenNoModelIsProvided() - { - $server = factory(Server::class)->make(['node_id' => 1]); - - $this->repository->shouldReceive('find')->with($server->id)->once()->andReturn($server); - $this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); - $this->repository->shouldReceive('withoutFreshModel')->withNoArgs()->once()->andReturnSelf() - ->shouldReceive('update')->with($server->id, [ - 'image' => 'new/image', - ])->once()->andReturnNull(); - - $this->daemonServerRepository->shouldReceive('setServer')->with($server)->once()->andReturnSelf() - ->shouldReceive('update')->with([ - 'build' => [ - 'image' => 'new/image', - ], - ])->once()->andReturn(new Response); - - $this->connection->shouldReceive('commit')->withNoArgs()->once()->andReturnNull(); - - $this->service->setDockerImage($server->id, 'new/image'); - $this->assertTrue(true); - } - - /** - * Test that an exception thrown by Guzzle is rendered as a displayable exception. - */ - public function testExceptionThrownByGuzzleWhenSettingDockerImageShouldBeRenderedAsADisplayableException() - { - $server = factory(Server::class)->make(['node_id' => 1]); - - $this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); - $this->repository->shouldReceive('withoutFreshModel')->withNoArgs()->once()->andReturnSelf() - ->shouldReceive('update')->with($server->id, [ - 'image' => 'new/image', - ])->once()->andReturnNull(); - - $this->daemonServerRepository->shouldReceive('setServer')->andThrow($this->exception); - $this->connection->shouldReceive('rollBack')->withNoArgs()->once()->andReturnNull(); - $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->setDockerImage($server, 'new/image'); - } catch (PterodactylException $exception) { - $this->assertInstanceOf(DisplayException::class, $exception); - $this->assertEquals( - trans('admin/server.exceptions.daemon_exception', ['code' => 400]), - $exception->getMessage() - ); - } - } - - /** - * Test that an exception not thrown by Guzzle is not transformed to a displayable exception. + * Return an instance of the service with mocked dependencies for testing. * - * @expectedException \Exception + * @return \Pterodactyl\Services\Servers\DetailsModificationService */ - public function testExceptionNotThrownByGuzzleWhenSettingDockerImageShouldNotBeRenderedAsADisplayableException() + private function getService(): DetailsModificationService { - $server = factory(Server::class)->make(['node_id' => 1]); - - $this->database->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); - $this->repository->shouldReceive('withoutFreshModel')->withNoArgs()->once()->andReturnSelf() - ->shouldReceive('update')->with($server->id, [ - 'image' => 'new/image', - ])->once()->andReturnNull(); - - $this->daemonServerRepository->shouldReceive('setNode')->andThrow(new Exception()); - - $this->service->setDockerImage($server, 'new/image'); + return new DetailsModificationService( + $this->connection, + $this->keyCreationService, + $this->keyDeletionService, + $this->repository + ); } }