diff --git a/app/Http/Controllers/Admin/VariableController.php b/app/Http/Controllers/Admin/VariableController.php index 4394b42f..eef95ec2 100644 --- a/app/Http/Controllers/Admin/VariableController.php +++ b/app/Http/Controllers/Admin/VariableController.php @@ -116,6 +116,7 @@ class VariableController extends Controller * * @throws \Pterodactyl\Exceptions\DisplayException * @throws \Pterodactyl\Exceptions\Model\DataValidationException + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException * @throws \Pterodactyl\Exceptions\Services\ServiceVariable\ReservedVariableNameException */ public function update(OptionVariableFormRequest $request, ServiceOption $option, ServiceVariable $variable) diff --git a/app/Services/Servers/CreationService.php b/app/Services/Servers/CreationService.php index 96e1527d..ff0b50f7 100644 --- a/app/Services/Servers/CreationService.php +++ b/app/Services/Servers/CreationService.php @@ -196,6 +196,7 @@ class CreationService } catch (RequestException $exception) { $response = $exception->getResponse(); $this->writer->warning($exception); + $this->database->rollBack(); throw new DisplayException(trans('admin/server.exceptions.daemon_exception', [ 'code' => is_null($response) ? 'E_CONN_REFUSED' : $response->getStatusCode(), diff --git a/app/Services/Servers/DeletionService.php b/app/Services/Servers/DeletionService.php index 869f17e3..a3e34e3a 100644 --- a/app/Services/Servers/DeletionService.php +++ b/app/Services/Servers/DeletionService.php @@ -36,16 +36,16 @@ use Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface as DaemonS class DeletionService { + /** + * @var \Illuminate\Database\ConnectionInterface + */ + protected $connection; + /** * @var \Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface */ protected $daemonServerRepository; - /** - * @var \Illuminate\Database\ConnectionInterface - */ - protected $database; - /** * @var \Pterodactyl\Services\Database\DatabaseManagementService */ @@ -74,7 +74,7 @@ class DeletionService /** * DeletionService constructor. * - * @param \Illuminate\Database\ConnectionInterface $database + * @param \Illuminate\Database\ConnectionInterface $connection * @param \Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface $daemonServerRepository * @param \Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface $databaseRepository * @param \Pterodactyl\Services\Database\DatabaseManagementService $databaseManagementService @@ -82,7 +82,7 @@ class DeletionService * @param \Illuminate\Log\Writer $writer */ public function __construct( - ConnectionInterface $database, + ConnectionInterface $connection, DaemonServerRepositoryInterface $daemonServerRepository, DatabaseRepositoryInterface $databaseRepository, DatabaseManagementService $databaseManagementService, @@ -90,7 +90,7 @@ class DeletionService Writer $writer ) { $this->daemonServerRepository = $daemonServerRepository; - $this->database = $database; + $this->connection = $connection; $this->databaseManagementService = $databaseManagementService; $this->databaseRepository = $databaseRepository; $this->repository = $repository; @@ -114,7 +114,9 @@ class DeletionService * Delete a server from the panel and remove any associated databases from hosts. * * @param int|\Pterodactyl\Models\Server $server + * * @throws \Pterodactyl\Exceptions\DisplayException + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException */ public function handle($server) { @@ -140,12 +142,12 @@ class DeletionService } } - $this->database->beginTransaction(); + $this->connection->beginTransaction(); $this->databaseRepository->withColumns('id')->findWhere([['server_id', '=', $server->id]])->each(function ($item) { $this->databaseManagementService->delete($item->id); }); $this->repository->delete($server->id); - $this->database->commit(); + $this->connection->commit(); } } diff --git a/app/Services/Servers/StartupModificationService.php b/app/Services/Servers/StartupModificationService.php index 8c3ffa5c..c9ea2391 100644 --- a/app/Services/Servers/StartupModificationService.php +++ b/app/Services/Servers/StartupModificationService.php @@ -125,6 +125,7 @@ class StartupModificationService * * @throws \Pterodactyl\Exceptions\DisplayException * @throws \Pterodactyl\Exceptions\Model\DataValidationException + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException */ public function handle($server, array $data) { diff --git a/app/Services/Services/Variables/VariableCreationService.php b/app/Services/Services/Variables/VariableCreationService.php index 7730380c..1dc1f8ae 100644 --- a/app/Services/Services/Variables/VariableCreationService.php +++ b/app/Services/Services/Variables/VariableCreationService.php @@ -53,8 +53,8 @@ class VariableCreationService /** * Create a new variable for a given service option. * - * @param int $option - * @param array $data + * @param int|\Pterodactyl\Models\ServiceOption $option + * @param array $data * @return \Pterodactyl\Models\ServiceVariable * * @throws \Pterodactyl\Exceptions\Model\DataValidationException @@ -67,7 +67,9 @@ class VariableCreationService } if (in_array(strtoupper(array_get($data, 'env_variable')), explode(',', ServiceVariable::RESERVED_ENV_NAMES))) { - throw new ReservedVariableNameException(sprintf('Cannot use the protected name %s for this environment variable.')); + throw new ReservedVariableNameException(sprintf( + 'Cannot use the protected name %s for this environment variable.', array_get($data, 'env_variable') + )); } $options = array_get($data, 'options', []); diff --git a/app/Services/Services/Variables/VariableUpdateService.php b/app/Services/Services/Variables/VariableUpdateService.php index c3fb8693..90c10a54 100644 --- a/app/Services/Services/Variables/VariableUpdateService.php +++ b/app/Services/Services/Variables/VariableUpdateService.php @@ -34,11 +34,16 @@ class VariableUpdateService /** * @var \Pterodactyl\Contracts\Repository\ServiceVariableRepositoryInterface */ - protected $serviceVariableRepository; + protected $repository; - public function __construct(ServiceVariableRepositoryInterface $serviceVariableRepository) + /** + * VariableUpdateService constructor. + * + * @param \Pterodactyl\Contracts\Repository\ServiceVariableRepositoryInterface $repository + */ + public function __construct(ServiceVariableRepositoryInterface $repository) { - $this->serviceVariableRepository = $serviceVariableRepository; + $this->repository = $repository; } /** @@ -46,16 +51,17 @@ class VariableUpdateService * * @param int|\Pterodactyl\Models\ServiceVariable $variable * @param array $data - * @return \Pterodactyl\Models\ServiceVariable + * @return mixed * * @throws \Pterodactyl\Exceptions\DisplayException * @throws \Pterodactyl\Exceptions\Model\DataValidationException + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException * @throws \Pterodactyl\Exceptions\Services\ServiceVariable\ReservedVariableNameException */ public function handle($variable, array $data) { if (! $variable instanceof ServiceVariable) { - $variable = $this->serviceVariableRepository->find($variable); + $variable = $this->repository->find($variable); } if (! is_null(array_get($data, 'env_variable'))) { @@ -65,7 +71,7 @@ class VariableUpdateService ])); } - $search = $this->serviceVariableRepository->withColumns('id')->findCountWhere([ + $search = $this->repository->withColumns('id')->findCountWhere([ ['env_variable', '=', array_get($data, 'env_variable')], ['option_id', '=', $variable->option_id], ['id', '!=', $variable->id], @@ -80,9 +86,9 @@ class VariableUpdateService $options = array_get($data, 'options', []); - return $this->serviceVariableRepository->update($variable->id, array_merge([ - 'user_viewable' => in_array('user_viewable', $options, $variable->user_viewable), - 'user_editable' => in_array('user_editable', $options, $variable->user_editable), + return $this->repository->withoutFresh()->update($variable->id, array_merge([ + 'user_viewable' => in_array('user_viewable', $options), + 'user_editable' => in_array('user_editable', $options), ], $data)); } } diff --git a/database/factories/ModelFactory.php b/database/factories/ModelFactory.php index b7e84eaf..4da5007d 100644 --- a/database/factories/ModelFactory.php +++ b/database/factories/ModelFactory.php @@ -93,7 +93,7 @@ $factory->define(Pterodactyl\Models\ServiceOption::class, function (Faker\Genera 'id' => $faker->unique()->randomNumber(), 'service_id' => $faker->unique()->randomNumber(), 'name' => $faker->name, - 'description' => $faker->sentences(3), + 'description' => implode(' ', $faker->sentences(3)), 'tag' => $faker->unique()->randomNumber(5), ]; }); diff --git a/tests/Unit/Services/Servers/CreationServiceTest.php b/tests/Unit/Services/Servers/CreationServiceTest.php index dad92475..c14384cf 100644 --- a/tests/Unit/Services/Servers/CreationServiceTest.php +++ b/tests/Unit/Services/Servers/CreationServiceTest.php @@ -24,9 +24,11 @@ namespace Tests\Unit\Services\Servers; +use Exception; +use GuzzleHttp\Exception\RequestException; use Mockery as m; +use Pterodactyl\Exceptions\DisplayException; use Tests\TestCase; -use Ramsey\Uuid\Uuid; use Illuminate\Log\Writer; use phpmock\phpunit\PHPMock; use Illuminate\Database\DatabaseManager; @@ -54,11 +56,40 @@ class CreationServiceTest extends TestCase */ protected $daemonServerRepository; + /** + * @var array + */ + protected $data = [ + 'node_id' => 1, + 'name' => 'SomeName', + 'description' => null, + 'owner_id' => 1, + 'memory' => 128, + 'disk' => 128, + 'swap' => 0, + 'io' => 500, + 'cpu' => 0, + 'allocation_id' => 1, + 'allocation_additional' => [2, 3], + 'environment' => [ + 'TEST_VAR_1' => 'var1-value', + ], + 'service_id' => 1, + 'option_id' => 1, + 'startup' => 'startup-param', + 'docker_image' => 'some/image', + ]; + /** * @var \Illuminate\Database\DatabaseManager */ protected $database; + /** + * @var \GuzzleHttp\Exception\RequestException + */ + protected $exception; + /** * @var \Pterodactyl\Contracts\Repository\NodeRepositoryInterface */ @@ -114,6 +145,7 @@ class CreationServiceTest extends TestCase $this->allocationRepository = m::mock(AllocationRepositoryInterface::class); $this->daemonServerRepository = m::mock(DaemonServerRepositoryInterface::class); $this->database = m::mock(DatabaseManager::class); + $this->exception = m::mock(RequestException::class); $this->nodeRepository = m::mock(NodeRepositoryInterface::class); $this->repository = m::mock(ServerRepositoryInterface::class); $this->serverVariableRepository = m::mock(ServerVariableRepositoryInterface::class); @@ -148,59 +180,38 @@ class CreationServiceTest extends TestCase */ public function testCreateShouldHitAllOfTheNecessaryServicesAndStoreTheServer() { - $data = [ - 'node_id' => 1, - 'name' => 'SomeName', - 'description' => null, - 'owner_id' => 1, - 'memory' => 128, - 'disk' => 128, - 'swap' => 0, - 'io' => 500, - 'cpu' => 0, - 'allocation_id' => 1, - 'allocation_additional' => [2, 3], - 'environment' => [ - 'TEST_VAR_1' => 'var1-value', - ], - 'service_id' => 1, - 'option_id' => 1, - 'startup' => 'startup-param', - 'docker_image' => 'some/image', - ]; - $this->validatorService->shouldReceive('isAdmin')->withNoArgs()->once()->andReturnSelf() - ->shouldReceive('setFields')->with($data['environment'])->once()->andReturnSelf() - ->shouldReceive('validate')->with($data['option_id'])->once()->andReturnSelf(); + ->shouldReceive('setFields')->with($this->data['environment'])->once()->andReturnSelf() + ->shouldReceive('validate')->with($this->data['option_id'])->once()->andReturnSelf(); $this->database->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); $this->uuid->shouldReceive('uuid4')->withNoArgs()->once()->andReturnSelf() ->shouldReceive('toString')->withNoArgs()->once()->andReturn('uuid-0000'); - $this->usernameService->shouldReceive('generate')->with($data['name'], 'randomstring') + $this->usernameService->shouldReceive('generate')->with($this->data['name'], 'randomstring') ->once()->andReturn('user_name'); $this->repository->shouldReceive('create')->with([ 'uuid' => 'uuid-0000', 'uuidShort' => 'randomstring', - 'node_id' => $data['node_id'], - 'name' => $data['name'], - 'description' => $data['description'], + 'node_id' => $this->data['node_id'], + 'name' => $this->data['name'], + 'description' => $this->data['description'], 'skip_scripts' => false, 'suspended' => false, - 'owner_id' => $data['owner_id'], - 'memory' => $data['memory'], - 'swap' => $data['swap'], - 'disk' => $data['disk'], - 'io' => $data['io'], - 'cpu' => $data['cpu'], + 'owner_id' => $this->data['owner_id'], + 'memory' => $this->data['memory'], + 'swap' => $this->data['swap'], + 'disk' => $this->data['disk'], + 'io' => $this->data['io'], + 'cpu' => $this->data['cpu'], 'oom_disabled' => false, - 'allocation_id' => $data['allocation_id'], - 'service_id' => $data['service_id'], - 'option_id' => $data['option_id'], + 'allocation_id' => $this->data['allocation_id'], + 'service_id' => $this->data['service_id'], + 'option_id' => $this->data['option_id'], 'pack_id' => null, - 'startup' => $data['startup'], + 'startup' => $this->data['startup'], 'daemonSecret' => 'randomstring', - 'image' => $data['docker_image'], + 'image' => $this->data['docker_image'], 'username' => 'user_name', 'sftp_password' => null, ])->once()->andReturn((object) [ @@ -208,7 +219,7 @@ class CreationServiceTest extends TestCase 'id' => 1, ]); - $this->allocationRepository->shouldReceive('assignAllocationsToServer')->with(1, [1, 2, 3]); + $this->allocationRepository->shouldReceive('assignAllocationsToServer')->with(1, [1, 2, 3])->once()->andReturnNull(); $this->validatorService->shouldReceive('getResults')->withNoArgs()->once()->andReturn([[ 'id' => 1, 'key' => 'TEST_VAR_1', @@ -224,9 +235,40 @@ class CreationServiceTest extends TestCase ->shouldReceive('create')->with(1)->once()->andReturnNull(); $this->database->shouldReceive('commit')->withNoArgs()->once()->andReturnNull(); - $response = $this->service->create($data); + $response = $this->service->create($this->data); $this->assertEquals(1, $response->id); $this->assertEquals(1, $response->node_id); } + + /** + * Test handling of node timeout or other daemon error. + */ + public function testExceptionShouldBeThrownIfTheRequestFails() + { + $this->validatorService->shouldReceive('isAdmin->setFields->validate->getResults')->once()->andReturn([]); + $this->database->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); + $this->uuid->shouldReceive('uuid4->toString')->once()->andReturn('uuid-0000'); + $this->usernameService->shouldReceive('generate')->once()->andReturn('user_name'); + $this->repository->shouldReceive('create')->once()->andReturn((object) [ + 'node_id' => 1, + 'id' => 1, + ]); + + $this->allocationRepository->shouldReceive('assignAllocationsToServer')->once()->andReturnNull(); + $this->serverVariableRepository->shouldReceive('insert')->with([])->once()->andReturnNull(); + $this->daemonServerRepository->shouldReceive('setNode->create')->once()->andThrow($this->exception); + $this->exception->shouldReceive('getResponse')->withNoArgs()->once()->andReturnNull(); + $this->writer->shouldReceive('warning')->with($this->exception)->once()->andReturnNull(); + $this->database->shouldReceive('rollBack')->withNoArgs()->once()->andReturnNull(); + + try { + $this->service->create($this->data); + } catch (Exception $exception) { + $this->assertInstanceOf(DisplayException::class, $exception); + $this->assertEquals(trans('admin/server.exceptions.daemon_exception', [ + 'code' => 'E_CONN_REFUSED', + ]), $exception->getMessage()); + } + } } diff --git a/tests/Unit/Services/Servers/DeletionServiceTest.php b/tests/Unit/Services/Servers/DeletionServiceTest.php new file mode 100644 index 00000000..37295ed8 --- /dev/null +++ b/tests/Unit/Services/Servers/DeletionServiceTest.php @@ -0,0 +1,213 @@ +. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +namespace Tests\Unit\Services\Servers; + +use Exception; +use GuzzleHttp\Exception\RequestException; +use Illuminate\Log\Writer; +use Mockery as m; +use Illuminate\Database\ConnectionInterface; +use Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface as DaemonServerRepositoryInterface; +use Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface; +use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; +use Pterodactyl\Exceptions\DisplayException; +use Pterodactyl\Models\Server; +use Pterodactyl\Services\Database\DatabaseManagementService; +use Pterodactyl\Services\Servers\DeletionService; +use Tests\TestCase; + +class DeletionServiceTest extends TestCase +{ + /** + * @var \Illuminate\Database\ConnectionInterface + */ + protected $connection; + + /** + * @var \Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface + */ + protected $daemonServerRepository; + + /** + * @var \Pterodactyl\Services\Database\DatabaseManagementService + */ + protected $databaseManagementService; + + /** + * @var \Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface + */ + protected $databaseRepository; + + /** + * @var \GuzzleHttp\Exception\RequestException + */ + protected $exception; + + /** + * @var \Pterodactyl\Models\Server + */ + protected $model; + + /** + * @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface + */ + protected $repository; + + /** + * @var \Pterodactyl\Services\Servers\DeletionService + */ + protected $service; + + /** + * @var \Illuminate\Log\Writer + */ + protected $writer; + + /** + * Setup tests. + */ + public function setUp() + { + parent::setUp(); + + $this->connection = m::mock(ConnectionInterface::class); + $this->daemonServerRepository = m::mock(DaemonServerRepositoryInterface::class); + $this->databaseRepository = m::mock(DatabaseRepositoryInterface::class); + $this->databaseManagementService = m::mock(DatabaseManagementService::class); + $this->exception = m::mock(RequestException::class); + $this->model = factory(Server::class)->make(); + $this->repository = m::mock(ServerRepositoryInterface::class); + $this->writer = m::mock(Writer::class); + + $this->service = new DeletionService( + $this->connection, + $this->daemonServerRepository, + $this->databaseRepository, + $this->databaseManagementService, + $this->repository, + $this->writer + ); + } + + /** + * Test that a server can be force deleted by setting it in a function call. + */ + public function testForceParameterCanBeSet() + { + $response = $this->service->withForce(true); + + $this->assertInstanceOf(DeletionService::class, $response); + } + + /** + * Test that a server can be deleted when force is not set. + */ + public function testServerCanBeDeletedWithoutForce() + { + $this->daemonServerRepository->shouldReceive('setNode')->with($this->model->node_id)->once()->andReturnSelf() + ->shouldReceive('setAccessServer')->with($this->model->uuid)->once()->andReturnSelf() + ->shouldReceive('delete')->withNoArgs()->once()->andReturnNull(); + + $this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); + $this->databaseRepository->shouldReceive('withColumns')->with('id')->once()->andReturnSelf() + ->shouldReceive('findWhere')->with([ + ['server_id', '=', $this->model->id], + ])->once()->andReturn(collect([(object) ['id' => 50]])); + + $this->databaseManagementService->shouldReceive('delete')->with(50)->once()->andReturnNull(); + $this->repository->shouldReceive('delete')->with($this->model->id)->once()->andReturnNull(); + $this->connection->shouldReceive('commit')->withNoArgs()->once()->andReturnNull(); + + $this->service->handle($this->model); + } + + /** + * Test that a server is deleted when force is set. + */ + public function testServerShouldBeDeletedEvenWhenFailureOccursIfForceIsSet() + { + $this->daemonServerRepository->shouldReceive('setNode')->with($this->model->node_id)->once()->andReturnSelf() + ->shouldReceive('setAccessServer')->with($this->model->uuid)->once()->andReturnSelf() + ->shouldReceive('delete')->withNoArgs()->once()->andThrow($this->exception); + + $this->exception->shouldReceive('getResponse')->withNoArgs()->once()->andReturnNull(); + $this->writer->shouldReceive('warning')->with($this->exception)->once()->andReturnNull(); + $this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); + $this->databaseRepository->shouldReceive('withColumns')->with('id')->once()->andReturnSelf() + ->shouldReceive('findWhere')->with([ + ['server_id', '=', $this->model->id], + ])->once()->andReturn(collect([(object) ['id' => 50]])); + + $this->databaseManagementService->shouldReceive('delete')->with(50)->once()->andReturnNull(); + $this->repository->shouldReceive('delete')->with($this->model->id)->once()->andReturnNull(); + $this->connection->shouldReceive('commit')->withNoArgs()->once()->andReturnNull(); + + $this->service->withForce()->handle($this->model); + } + + /** + * Test that an exception is thrown if a server cannot be deleted from the node and force is not set. + */ + public function testExceptionShouldBeThrownIfDaemonReturnsAnErrorAndForceIsNotSet() + { + $this->daemonServerRepository->shouldReceive('setNode->setAccessServer->delete')->once()->andThrow($this->exception); + $this->exception->shouldReceive('getResponse')->withNoArgs()->once()->andReturnNull(); + $this->writer->shouldReceive('warning')->with($this->exception)->once()->andReturnNull(); + + try { + $this->service->handle($this->model); + } catch (Exception $exception) { + $this->assertInstanceOf(DisplayException::class, $exception); + $this->assertEquals(trans('admin/server.exceptions.daemon_exception', [ + 'code' => 'E_CONN_REFUSED', + ]), $exception->getMessage()); + } + } + + /** + * Test that an integer can be passed in place of the Server model. + */ + public function testIntegerCanBePassedInPlaceOfServerModel() + { + $this->repository->shouldReceive('withColumns')->with(['id', 'node_id', 'uuid'])->once()->andReturnSelf() + ->shouldReceive('find')->with($this->model->id)->once()->andReturn($this->model); + + $this->daemonServerRepository->shouldReceive('setNode')->with($this->model->node_id)->once()->andReturnSelf() + ->shouldReceive('setAccessServer')->with($this->model->uuid)->once()->andReturnSelf() + ->shouldReceive('delete')->withNoArgs()->once()->andReturnNull(); + + $this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); + $this->databaseRepository->shouldReceive('withColumns')->with('id')->once()->andReturnSelf() + ->shouldReceive('findWhere')->with([ + ['server_id', '=', $this->model->id], + ])->once()->andReturn(collect([(object) ['id' => 50]])); + + $this->databaseManagementService->shouldReceive('delete')->with(50)->once()->andReturnNull(); + $this->repository->shouldReceive('delete')->with($this->model->id)->once()->andReturnNull(); + $this->connection->shouldReceive('commit')->withNoArgs()->once()->andReturnNull(); + + $this->service->handle($this->model->id); + } +} diff --git a/tests/Unit/Services/Services/Variables/VariableCreationServiceTest.php b/tests/Unit/Services/Services/Variables/VariableCreationServiceTest.php new file mode 100644 index 00000000..320f06e7 --- /dev/null +++ b/tests/Unit/Services/Services/Variables/VariableCreationServiceTest.php @@ -0,0 +1,143 @@ +. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +namespace Tests\Unit\Services\Services\Variables; + +use Mockery as m; +use Pterodactyl\Contracts\Repository\ServiceOptionRepositoryInterface; +use Pterodactyl\Contracts\Repository\ServiceVariableRepositoryInterface; +use Pterodactyl\Exceptions\Services\ServiceVariable\ReservedVariableNameException; +use Pterodactyl\Models\ServiceOption; +use Pterodactyl\Models\ServiceVariable; +use Pterodactyl\Services\Services\Variables\VariableCreationService; +use Tests\TestCase; + +class VariableCreationServiceTest extends TestCase +{ + /** + * @var \Pterodactyl\Contracts\Repository\ServiceOptionRepositoryInterface + */ + protected $serviceOptionRepository; + + /** + * @var \Pterodactyl\Contracts\Repository\ServiceVariableRepositoryInterface + */ + protected $serviceVariableRepository; + + /** + * @var \Pterodactyl\Services\Services\Variables\VariableCreationService + */ + protected $service; + + /** + * Setup tests. + */ + public function setUp() + { + parent::setUp(); + + $this->serviceOptionRepository = m::mock(ServiceOptionRepositoryInterface::class); + $this->serviceVariableRepository = m::mock(ServiceVariableRepositoryInterface::class); + + $this->service = new VariableCreationService($this->serviceOptionRepository, $this->serviceVariableRepository); + } + + /** + * Test basic functionality, data should be stored in the database. + */ + public function testVariableIsCreatedAndStored() + { + $data = ['env_variable' => 'TEST_VAR_123']; + $this->serviceVariableRepository->shouldReceive('create')->with([ + 'option_id' => 1, + 'user_viewable' => false, + 'user_editable' => false, + 'env_variable' => 'TEST_VAR_123', + ])->once()->andReturn(new ServiceVariable); + + $this->assertInstanceOf(ServiceVariable::class, $this->service->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->serviceVariableRepository->shouldReceive('create')->with([ + 'option_id' => 1, + 'user_viewable' => true, + 'user_editable' => true, + 'env_variable' => 'TEST_VAR_123', + 'options' => ['user_viewable', 'user_editable'], + ])->once()->andReturn(new ServiceVariable); + + $this->assertInstanceOf(ServiceVariable::class, $this->service->handle(1, $data)); + } + + /** + * Test that all of the reserved variables defined in the model trigger an exception. + * + * @dataProvider reservedNamesProvider + * @expectedException \Pterodactyl\Exceptions\Services\ServiceVariable\ReservedVariableNameException + */ + public function testExceptionIsThrownIfEnvironmentVariableIsInListOfReservedNames($variable) + { + $this->service->handle(1, ['env_variable' => $variable]); + } + + /** + * Test that a model can be passed in place of an integer. + */ + public function testModelCanBePassedInPlaceOfInteger() + { + $model = factory(ServiceOption::class)->make(); + $data = ['env_variable' => 'TEST_VAR_123']; + + $this->serviceVariableRepository->shouldReceive('create')->with([ + 'option_id' => $model->id, + 'user_viewable' => false, + 'user_editable' => false, + 'env_variable' => 'TEST_VAR_123', + ])->once()->andReturn(new ServiceVariable); + + $this->assertInstanceOf(ServiceVariable::class, $this->service->handle($model, $data)); + } + + /** + * Provides the data to be used in the tests. + * + * @return array + */ + public function reservedNamesProvider() + { + $data = []; + $exploded = explode(',', ServiceVariable::RESERVED_ENV_NAMES); + foreach ($exploded as $e) { + $data[] = [$e]; + } + + return $data; + } +} diff --git a/tests/Unit/Services/Services/Variables/VariableUpdateServiceTest.php b/tests/Unit/Services/Services/Variables/VariableUpdateServiceTest.php new file mode 100644 index 00000000..5e067387 --- /dev/null +++ b/tests/Unit/Services/Services/Variables/VariableUpdateServiceTest.php @@ -0,0 +1,151 @@ +. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +namespace Tests\Unit\Services\Services\Variables; + +use Exception; +use Mockery as m; +use PhpParser\Node\Expr\Variable; +use Pterodactyl\Exceptions\DisplayException; +use Pterodactyl\Models\ServiceVariable; +use Pterodactyl\Services\Services\Variables\VariableUpdateService; +use Tests\TestCase; +use Pterodactyl\Contracts\Repository\ServiceVariableRepositoryInterface; + +class VariableUpdateServiceTest extends TestCase +{ + /** + * @var \Pterodactyl\Models\ServiceVariable + */ + protected $model; + + /** + * @var \Pterodactyl\Contracts\Repository\ServiceVariableRepositoryInterface + */ + protected $repository; + + /** + * @var \Pterodactyl\Services\Services\Variables\VariableUpdateService + */ + protected $service; + + /** + * Setup tests. + */ + public function setUp() + { + parent::setUp(); + + $this->model = factory(ServiceVariable::class)->make(); + $this->repository = m::mock(ServiceVariableRepositoryInterface::class); + + $this->service = new VariableUpdateService($this->repository); + } + + /** + * Test the function when no env_variable key is passed into the function. + */ + public function testVariableIsUpdatedWhenNoEnvironmentVariableIsPassed() + { + $this->repository->shouldReceive('withoutFresh')->withNoArgs()->once()->andReturnSelf() + ->shouldReceive('update')->with($this->model->id, [ + 'user_viewable' => false, + 'user_editable' => false, + 'test-data' => 'test-value', + ])->once()->andReturn(true); + + $this->assertTrue($this->service->handle($this->model, ['test-data' => 'test-value'])); + } + + /** + * Test the function when a valid env_variable key is passed into the function. + */ + public function testVariableIsUpdatedWhenValidEnvironmentVariableIsPassed() + { + $this->repository->shouldReceive('withColumns')->with('id')->once()->andReturnSelf() + ->shouldReceive('findCountWhere')->with([ + ['env_variable', '=', 'TEST_VAR_123'], + ['option_id', '=', $this->model->option_id], + ['id', '!=', $this->model->id] + ])->once()->andReturn(0); + + $this->repository->shouldReceive('withoutFresh')->withNoArgs()->once()->andReturnSelf() + ->shouldReceive('update')->with($this->model->id, [ + 'user_viewable' => false, + 'user_editable' => false, + 'env_variable' => 'TEST_VAR_123', + ])->once()->andReturn(true); + + $this->assertTrue($this->service->handle($this->model, ['env_variable' => 'TEST_VAR_123'])); + } + + /** + * Test that a non-unique environment variable triggers an exception. + */ + public function testExceptionIsThrownIfEnvironmentVariableIsNotUnique() + { + $this->repository->shouldReceive('withColumns')->with('id')->once()->andReturnSelf() + ->shouldReceive('findCountWhere')->with([ + ['env_variable', '=', 'TEST_VAR_123'], + ['option_id', '=', $this->model->option_id], + ['id', '!=', $this->model->id] + ])->once()->andReturn(1); + + try { + $this->service->handle($this->model, ['env_variable' => 'TEST_VAR_123']); + } catch (Exception $exception) { + $this->assertInstanceOf(DisplayException::class, $exception); + $this->assertEquals(trans('admin/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 + * @expectedException \Pterodactyl\Exceptions\Services\ServiceVariable\ReservedVariableNameException + */ + public function testExceptionIsThrownIfEnvironmentVariableIsInListOfReservedNames($variable) + { + $this->service->handle($this->model, ['env_variable' => $variable]); + } + + /** + * Provides the data to be used in the tests. + * + * @return array + */ + public function reservedNamesProvider() + { + $data = []; + $exploded = explode(',', ServiceVariable::RESERVED_ENV_NAMES); + foreach ($exploded as $e) { + $data[] = [$e]; + } + + return $data; + } +}