diff --git a/app/Services/Servers/StartupModificationService.php b/app/Services/Servers/StartupModificationService.php index 2d8dc3a1..c409520a 100644 --- a/app/Services/Servers/StartupModificationService.php +++ b/app/Services/Servers/StartupModificationService.php @@ -89,22 +89,21 @@ class StartupModificationService */ protected function updateAdministrativeSettings(array $data, Server &$server) { - if ( - is_digit(Arr::get($data, 'egg_id')) - && $data['egg_id'] != $server->egg_id - && is_null(Arr::get($data, 'nest_id')) - ) { - /** @var \Pterodactyl\Models\Egg $egg */ - $egg = Egg::query()->select(['nest_id'])->findOrFail($data['egg_id']); + $eggId = Arr::get($data, 'egg_id'); - $data['nest_id'] = $egg->nest_id; + if (is_digit($eggId) && $server->egg_id !== (int)$eggId) { + /** @var \Pterodactyl\Models\Egg $egg */ + $egg = Egg::query()->findOrFail($data['egg_id']); + + $server = $server->forceFill([ + 'egg_id' => $egg->id, + 'nest_id' => $egg->nest_id, + ]); } $server->forceFill([ 'installed' => 0, '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(); diff --git a/tests/Integration/Services/Servers/StartupModificationServiceTest.php b/tests/Integration/Services/Servers/StartupModificationServiceTest.php index 4900f910..89674f88 100644 --- a/tests/Integration/Services/Servers/StartupModificationServiceTest.php +++ b/tests/Integration/Services/Servers/StartupModificationServiceTest.php @@ -3,10 +3,15 @@ namespace Pterodactyl\Tests\Integration\Services\Servers; use Exception; +use Ramsey\Uuid\Uuid; +use Pterodactyl\Models\Egg; +use Pterodactyl\Models\User; +use Pterodactyl\Models\Nest; use Pterodactyl\Models\Server; use Pterodactyl\Models\ServerVariable; use Illuminate\Validation\ValidationException; use Pterodactyl\Tests\Integration\IntegrationTestCase; +use Illuminate\Database\Eloquent\ModelNotFoundException; use Pterodactyl\Services\Servers\StartupModificationService; class StartupModificationServiceTest extends IntegrationTestCase @@ -46,15 +51,15 @@ class StartupModificationServiceTest extends IntegrationTestCase 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', - ], - ]); + $result = $this->getService() + ->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); @@ -62,4 +67,105 @@ class StartupModificationServiceTest extends IntegrationTestCase $this->assertSame('1234', $result->variables[0]->server_value); $this->assertSame('test.jar', $result->variables[1]->server_value); } + + /** + * Test that modifying an egg as an admin properly updates the data for the server. + */ + public function testServerIsProperlyModifiedAsAdminUser() + { + /** @var \Pterodactyl\Models\Egg $nextEgg */ + $nextEgg = Nest::query()->findOrFail(2)->eggs()->firstOrFail(); + + $server = $this->createServerModel(['egg_id' => 1]); + + $this->assertNotSame($nextEgg->id, $server->egg_id); + $this->assertNotSame($nextEgg->nest_id, $server->nest_id); + + $response = $this->getService() + ->setUserLevel(User::USER_LEVEL_ADMIN) + ->handle($server, [ + 'egg_id' => $nextEgg->id, + 'startup' => 'sample startup', + 'skip_scripts' => true, + 'docker_image' => 'docker/hodor', + ]); + + $this->assertInstanceOf(Server::class, $response); + $this->assertSame($nextEgg->id, $response->egg_id); + $this->assertSame($nextEgg->nest_id, $response->nest_id); + $this->assertSame('sample startup', $response->startup); + $this->assertSame('docker/hodor', $response->image); + $this->assertTrue($response->skip_scripts); + } + + /** + * Test that hidden variables can be updated by an admin but are not affected by a + * regular user who attempts to pass them through. + */ + public function testEnvironmentVariablesCanBeUpdatedByAdmin() + { + $server = $this->createServerModel(); + $server->loadMissing(['egg', 'variables']); + + $clone = $this->cloneEggAndVariables($server->egg); + // This makes the BUNGEE_VERSION variable not user editable. + $clone->variables()->first()->update([ + 'user_editable' => false, + ]); + + $server->fill(['egg_id' => $clone->id])->saveOrFail(); + $server->refresh(); + + ServerVariable::query()->updateOrCreate([ + 'server_id' => $server->id, + 'variable_id' => $server->variables[0]->id, + ], ['variable_value' => 'EXIST']); + + $response = $this->getService()->handle($server, [ + 'environment' => [ + 'BUNGEE_VERSION' => '1234', + 'SERVER_JARFILE' => 'test.jar', + ], + ]); + + $this->assertCount(2, $response->variables); + $this->assertSame('EXIST', $response->variables[0]->server_value); + $this->assertSame('test.jar', $response->variables[1]->server_value); + + $response = $this->getService() + ->setUserLevel(User::USER_LEVEL_ADMIN) + ->handle($server, [ + 'environment' => [ + 'BUNGEE_VERSION' => '1234', + 'SERVER_JARFILE' => 'test.jar', + ], + ]); + + $this->assertCount(2, $response->variables); + $this->assertSame('1234', $response->variables[0]->server_value); + $this->assertSame('test.jar', $response->variables[1]->server_value); + } + + /** + * Test that passing an invalid egg ID into the function throws an exception + * rather than silently failing or skipping. + */ + public function testInvalidEggIdTriggersException() + { + $server = $this->createServerModel(); + + $this->expectException(ModelNotFoundException::class); + + $this->getService() + ->setUserLevel(User::USER_LEVEL_ADMIN) + ->handle($server, ['egg_id' => 123456789]); + } + + /** + * @return \Pterodactyl\Services\Servers\StartupModificationService + */ + private function getService() + { + return $this->app->make(StartupModificationService::class); + } } diff --git a/tests/Traits/Integration/CreatesTestModels.php b/tests/Traits/Integration/CreatesTestModels.php index daa14eeb..ecd4e0d5 100644 --- a/tests/Traits/Integration/CreatesTestModels.php +++ b/tests/Traits/Integration/CreatesTestModels.php @@ -2,6 +2,7 @@ namespace Tests\Traits\Integration; +use Ramsey\Uuid\Uuid; use Pterodactyl\Models\Egg; use Pterodactyl\Models\Nest; use Pterodactyl\Models\Node; @@ -74,4 +75,27 @@ trait CreatesTestModels 'location', 'user', 'node', 'allocation', 'nest', 'egg', ])->findOrFail($server->id); } + + /** + * Clones a given egg allowing us to make modifications that don't affect other + * tests that rely on the egg existing in the correct state. + * + * @param \Pterodactyl\Models\Egg $egg + * @return \Pterodactyl\Models\Egg + */ + protected function cloneEggAndVariables(Egg $egg): Egg + { + $model = $egg->replicate(['id', 'uuid']); + $model->uuid = Uuid::uuid4()->toString(); + $model->push(); + + /** @var \Pterodactyl\Models\Egg $model */ + $model = $model->fresh(); + + foreach ($egg->variables as $variable) { + $variable->replicate(['id', 'egg_id'])->forceFill(['egg_id' => $model->id])->push(); + } + + return $model->fresh(); + } }