diff --git a/app/Repositories/Wings/DaemonConfigurationRepository.php b/app/Repositories/Wings/DaemonConfigurationRepository.php index f1b83cacb..ffd498cbc 100644 --- a/app/Repositories/Wings/DaemonConfigurationRepository.php +++ b/app/Repositories/Wings/DaemonConfigurationRepository.php @@ -34,7 +34,7 @@ class DaemonConfigurationRepository extends DaemonRepository * @return \Psr\Http\Message\ResponseInterface * @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException */ - public function update(?Node $node) + public function update(Node $node) { try { return $this->getHttpClient()->post( diff --git a/app/Services/Helpers/SoftwareVersionService.php b/app/Services/Helpers/SoftwareVersionService.php index 6aa9c8935..893c097d0 100644 --- a/app/Services/Helpers/SoftwareVersionService.php +++ b/app/Services/Helpers/SoftwareVersionService.php @@ -4,7 +4,7 @@ namespace Pterodactyl\Services\Helpers; use Exception; use GuzzleHttp\Client; -use Cake\Chronos\Chronos; +use Carbon\CarbonImmutable; use Illuminate\Support\Arr; use Illuminate\Contracts\Cache\Repository as CacheRepository; use Pterodactyl\Exceptions\Service\Helper\CdnVersionFetchingException; @@ -120,7 +120,7 @@ class SoftwareVersionService */ protected function cacheVersionData() { - return $this->cache->remember(self::VERSION_CACHE_KEY, Chronos::now()->addMinutes(config()->get('pterodactyl.cdn.cache_time', 60)), function () { + return $this->cache->remember(self::VERSION_CACHE_KEY, CarbonImmutable::now()->addMinutes(config()->get('pterodactyl.cdn.cache_time', 60)), function () { try { $response = $this->client->request('GET', config()->get('pterodactyl.cdn.url')); diff --git a/app/Services/Nodes/NodeCreationService.php b/app/Services/Nodes/NodeCreationService.php index fabc36e05..a44c036bd 100644 --- a/app/Services/Nodes/NodeCreationService.php +++ b/app/Services/Nodes/NodeCreationService.php @@ -5,7 +5,7 @@ namespace Pterodactyl\Services\Nodes; use Ramsey\Uuid\Uuid; use Illuminate\Support\Str; use Pterodactyl\Models\Node; -use Illuminate\Encryption\Encrypter; +use Illuminate\Contracts\Encryption\Encrypter; use Pterodactyl\Contracts\Repository\NodeRepositoryInterface; class NodeCreationService @@ -16,14 +16,14 @@ class NodeCreationService protected $repository; /** - * @var \Illuminate\Encryption\Encrypter + * @var \Illuminate\Contracts\Encryption\Encrypter */ private $encrypter; /** * CreationService constructor. * - * @param \Illuminate\Encryption\Encrypter $encrypter + * @param \Illuminate\Contracts\Encryption\Encrypter $encrypter * @param \Pterodactyl\Contracts\Repository\NodeRepositoryInterface $repository */ public function __construct(Encrypter $encrypter, NodeRepositoryInterface $repository) diff --git a/tests/Unit/Services/Eggs/EggConfigurationServiceTest.php b/tests/Unit/Services/Eggs/EggConfigurationServiceTest.php deleted file mode 100644 index f6b1bebb9..000000000 --- a/tests/Unit/Services/Eggs/EggConfigurationServiceTest.php +++ /dev/null @@ -1,90 +0,0 @@ -repository = m::mock(EggRepositoryInterface::class); - - $this->service = new EggConfigurationService($this->repository); - } - - /** - * Test that the correct array is returned. - */ - public function testCorrectArrayIsReturned() - { - $egg = factory(Egg::class)->make([ - 'config_startup' => '{"test": "start"}', - 'config_stop' => 'test', - 'config_files' => '{"test": "file"}', - 'config_logs' => '{"test": "logs"}', - ]); - - $response = $this->service->handle($egg); - $this->assertNotEmpty($response); - $this->assertTrue(is_array($response), 'Assert response is an array.'); - $this->assertArrayHasKey('startup', $response); - $this->assertArrayHasKey('stop', $response); - $this->assertArrayHasKey('configs', $response); - $this->assertArrayHasKey('log', $response); - $this->assertArrayHasKey('query', $response); - $this->assertEquals('start', object_get($response['startup'], 'test')); - $this->assertEquals('test', 'test'); - $this->assertEquals('file', object_get($response['configs'], 'test')); - $this->assertEquals('logs', object_get($response['log'], 'test')); - $this->assertEquals('none', $response['query']); - } - - /** - * Test that an integer referencing a model can be passed in place of the model. - */ - public function testFunctionHandlesIntegerPassedInPlaceOfModel() - { - $egg = factory(Egg::class)->make([ - 'config_startup' => '{"test": "start"}', - 'config_stop' => 'test', - 'config_files' => '{"test": "file"}', - 'config_logs' => '{"test": "logs"}', - ]); - - $this->repository->shouldReceive('getWithCopyAttributes')->with($egg->id)->once()->andReturn($egg); - - $response = $this->service->handle($egg->id); - $this->assertNotEmpty($response); - $this->assertTrue(is_array($response), 'Assert response is an array.'); - $this->assertArrayHasKey('startup', $response); - $this->assertArrayHasKey('stop', $response); - $this->assertArrayHasKey('configs', $response); - $this->assertArrayHasKey('log', $response); - $this->assertArrayHasKey('query', $response); - $this->assertEquals('start', object_get($response['startup'], 'test')); - $this->assertEquals('test', 'test'); - $this->assertEquals('file', object_get($response['configs'], 'test')); - $this->assertEquals('logs', object_get($response['log'], 'test')); - $this->assertEquals('none', $response['query']); - } -} diff --git a/tests/Unit/Services/Eggs/Variables/VariableCreationServiceTest.php b/tests/Unit/Services/Eggs/Variables/VariableCreationServiceTest.php index 34c7bdbdd..bbac6009d 100644 --- a/tests/Unit/Services/Eggs/Variables/VariableCreationServiceTest.php +++ b/tests/Unit/Services/Eggs/Variables/VariableCreationServiceTest.php @@ -9,6 +9,8 @@ use Pterodactyl\Models\EggVariable; use Illuminate\Contracts\Validation\Factory; use Pterodactyl\Services\Eggs\Variables\VariableCreationService; use Pterodactyl\Contracts\Repository\EggVariableRepositoryInterface; +use Pterodactyl\Exceptions\Service\Egg\Variable\BadValidationRuleException; +use Pterodactyl\Exceptions\Service\Egg\Variable\ReservedVariableNameException; class VariableCreationServiceTest extends TestCase { @@ -91,10 +93,11 @@ class VariableCreationServiceTest extends TestCase * @param string $variable * * @dataProvider reservedNamesProvider - * @expectedException \Pterodactyl\Exceptions\Service\Egg\Variable\ReservedVariableNameException */ public function testExceptionIsThrownIfEnvironmentVariableIsInListOfReservedNames(string $variable) { + $this->expectException(ReservedVariableNameException::class); + $this->getService()->handle(1, ['env_variable' => $variable]); } @@ -114,12 +117,12 @@ class VariableCreationServiceTest extends TestCase /** * Test that validation errors due to invalid rules are caught and handled properly. - * - * @expectedException \Pterodactyl\Exceptions\Service\Egg\Variable\BadValidationRuleException - * @expectedExceptionMessage The validation rule "hodor_door" is not a valid rule for this application. */ public function testInvalidValidationRulesResultInException() { + $this->expectException(BadValidationRuleException::class); + $this->expectExceptionMessage('The validation rule "hodor_door" is not a valid rule for this application.'); + $data = ['env_variable' => 'TEST_VAR_123', 'rules' => 'string|hodorDoor']; $this->validator->shouldReceive('make')->once() @@ -135,12 +138,12 @@ class VariableCreationServiceTest extends TestCase /** * Test that an exception not stemming from a bad rule is not caught. - * - * @expectedException \BadMethodCallException - * @expectedExceptionMessage Received something, but no expectations were specified. */ public function testExceptionNotCausedByBadRuleIsNotCaught() { + $this->expectException(BadMethodCallException::class); + $this->expectExceptionMessage('Received something, but no expectations were specified.'); + $data = ['env_variable' => 'TEST_VAR_123', 'rules' => 'string']; $this->validator->shouldReceive('make')->once() diff --git a/tests/Unit/Services/Eggs/Variables/VariableUpdateServiceTest.php b/tests/Unit/Services/Eggs/Variables/VariableUpdateServiceTest.php index 82dd00c0b..a812da274 100644 --- a/tests/Unit/Services/Eggs/Variables/VariableUpdateServiceTest.php +++ b/tests/Unit/Services/Eggs/Variables/VariableUpdateServiceTest.php @@ -11,6 +11,8 @@ use Illuminate\Contracts\Validation\Factory; use Pterodactyl\Exceptions\DisplayException; use Pterodactyl\Services\Eggs\Variables\VariableUpdateService; use Pterodactyl\Contracts\Repository\EggVariableRepositoryInterface; +use Pterodactyl\Exceptions\Service\Egg\Variable\BadValidationRuleException; +use Pterodactyl\Exceptions\Service\Egg\Variable\ReservedVariableNameException; class VariableUpdateServiceTest extends TestCase { @@ -159,21 +161,22 @@ class VariableUpdateServiceTest extends TestCase * Test that all of the reserved variables defined in the model trigger an exception. * * @dataProvider reservedNamesProvider - * @expectedException \Pterodactyl\Exceptions\Service\Egg\Variable\ReservedVariableNameException */ public function testExceptionIsThrownIfEnvironmentVariableIsInListOfReservedNames(string $variable) { + $this->expectException(ReservedVariableNameException::class); + $this->getService()->handle($this->model, ['env_variable' => $variable]); } /** * Test that validation errors due to invalid rules are caught and handled properly. - * - * @expectedException \Pterodactyl\Exceptions\Service\Egg\Variable\BadValidationRuleException - * @expectedExceptionMessage The validation rule "hodor_door" is not a valid rule for this application. */ public function testInvalidValidationRulesResultInException() { + $this->expectException(BadValidationRuleException::class); + $this->expectExceptionMessage('The validation rule "hodor_door" is not a valid rule for this application.'); + $data = ['env_variable' => 'TEST_VAR_123', 'rules' => 'string|hodorDoor']; $this->repository->shouldReceive('setColumns->findCountWhere')->once()->andReturn(0); @@ -191,12 +194,12 @@ class VariableUpdateServiceTest extends TestCase /** * Test that an exception not stemming from a bad rule is not caught. - * - * @expectedException \BadMethodCallException - * @expectedExceptionMessage Received something, but no expectations were specified. */ public function testExceptionNotCausedByBadRuleIsNotCaught() { + $this->expectException(BadMethodCallException::class); + $this->expectExceptionMessage('Received something, but no expectations were specified.'); + $data = ['rules' => 'string']; $this->validator->shouldReceive('make')->once() diff --git a/tests/Unit/Services/Helpers/SoftwareVersionServiceTest.php b/tests/Unit/Services/Helpers/SoftwareVersionServiceTest.php deleted file mode 100644 index d0ada1b4a..000000000 --- a/tests/Unit/Services/Helpers/SoftwareVersionServiceTest.php +++ /dev/null @@ -1,168 +0,0 @@ -. - * - * This software is licensed under the terms of the MIT license. - * https://opensource.org/licenses/MIT - */ - -namespace Tests\Unit\Services\Helpers; - -use Closure; -use Mockery as m; -use Tests\TestCase; -use GuzzleHttp\Client; -use Pterodactyl\Services\Helpers\SoftwareVersionService; -use Illuminate\Contracts\Cache\Repository as CacheRepository; -use Illuminate\Contracts\Config\Repository as ConfigRepository; - -class SoftwareVersionServiceTest extends TestCase -{ - /** - * @var \Illuminate\Contracts\Cache\Repository - */ - protected $cache; - - /** - * @var \GuzzleHttp\Client - */ - protected $client; - - /** - * @var \Illuminate\Contracts\Config\Repository - */ - protected $config; - - /** - * @var object - */ - protected static $response = [ - 'panel' => '0.2.0', - 'daemon' => '0.1.0', - 'discord' => 'https://pterodactyl.io/discord', - ]; - - /** - * @var \Pterodactyl\Services\Helpers\SoftwareVersionService - */ - protected $service; - - /** - * Setup tests. - */ - public function setUp(): void - { - parent::setUp(); - - self::$response = (object) self::$response; - - $this->cache = m::mock(CacheRepository::class); - $this->client = m::mock(Client::class); - $this->config = m::mock(ConfigRepository::class); - - $this->config->shouldReceive('get')->with('pterodactyl.cdn.cache_time')->once()->andReturn(60); - - $this->cache->shouldReceive('remember')->with(SoftwareVersionService::VERSION_CACHE_KEY, 60, Closure::class)->once()->andReturnNull(); - - $this->service = m::mock(SoftwareVersionService::class, [$this->cache, $this->client, $this->config])->makePartial(); - } - - /** - * Test that the panel version is returned. - */ - public function testPanelVersionIsReturned() - { - $this->cache->shouldReceive('get')->with(SoftwareVersionService::VERSION_CACHE_KEY)->once()->andReturn(self::$response); - $this->assertEquals(self::$response->panel, $this->service->getPanel()); - } - - /** - * Test that the panel version is returned as error. - */ - public function testPanelVersionIsReturnedAsErrorIfNoKeyIsFound() - { - $this->cache->shouldReceive('get')->with(SoftwareVersionService::VERSION_CACHE_KEY)->once()->andReturn((object) []); - $this->assertEquals('error', $this->service->getPanel()); - } - - /** - * Test that the daemon version is returned. - */ - public function testDaemonVersionIsReturned() - { - $this->cache->shouldReceive('get')->with(SoftwareVersionService::VERSION_CACHE_KEY)->once()->andReturn(self::$response); - $this->assertEquals(self::$response->daemon, $this->service->getDaemon()); - } - - /** - * Test that the daemon version is returned as an error. - */ - public function testDaemonVersionIsReturnedAsErrorIfNoKeyIsFound() - { - $this->cache->shouldReceive('get')->with(SoftwareVersionService::VERSION_CACHE_KEY)->once()->andReturn((object) []); - $this->assertEquals('error', $this->service->getDaemon()); - } - - /** - * Test that the discord URL is returned. - */ - public function testDiscordUrlIsReturned() - { - $this->cache->shouldReceive('get')->with(SoftwareVersionService::VERSION_CACHE_KEY)->once()->andReturn(self::$response); - $this->assertEquals(self::$response->discord, $this->service->getDiscord()); - } - - /** - * Test that the correct boolean value is returned by the helper for each version passed. - * - * @dataProvider panelVersionProvider - */ - public function testCorrectBooleanValueIsReturnedWhenCheckingPanelVersion($version, $response) - { - $this->config->shouldReceive('get')->with('app.version')->andReturn($version); - $this->service->shouldReceive('getPanel')->withNoArgs()->andReturn(self::$response->panel); - - $this->assertEquals($response, $this->service->isLatestPanel()); - } - - /** - * Test that the correct boolean value is returned. - * - * @dataProvider daemonVersionProvider - */ - public function testCorrectBooleanValueIsReturnedWhenCheckingDaemonVersion($version, $response) - { - $this->service->shouldReceive('getDaemon')->withNoArgs()->andReturn(self::$response->daemon); - - $this->assertEquals($response, $this->service->isLatestDaemon($version)); - } - - /** - * Provide data for testing boolean response on panel version. - * - * @return array - */ - public function panelVersionProvider() - { - return [ - [self::$response['panel'], true], - ['0.0.1', false], - ['canary', true], - ]; - } - - /** - * Provide data for testing boolean response for daemon version. - * - * @return array - */ - public function daemonVersionProvider() - { - return [ - [self::$response['daemon'], true], - ['0.0.1', false], - ['0.0.0-canary', true], - ]; - } -} diff --git a/tests/Unit/Services/Nodes/NodeCreationServiceTest.php b/tests/Unit/Services/Nodes/NodeCreationServiceTest.php index bf7cb05ed..561a14acc 100644 --- a/tests/Unit/Services/Nodes/NodeCreationServiceTest.php +++ b/tests/Unit/Services/Nodes/NodeCreationServiceTest.php @@ -1,17 +1,14 @@ . - * - * This software is licensed under the terms of the MIT license. - * https://opensource.org/licenses/MIT - */ namespace Tests\Unit\Services\Nodes; use Mockery as m; use Tests\TestCase; +use Ramsey\Uuid\Uuid; use phpmock\phpunit\PHPMock; +use Pterodactyl\Models\Node; +use Ramsey\Uuid\UuidFactory; +use Illuminate\Contracts\Encryption\Encrypter; use Pterodactyl\Services\Nodes\NodeCreationService; use Pterodactyl\Contracts\Repository\NodeRepositoryInterface; @@ -20,14 +17,14 @@ class NodeCreationServiceTest extends TestCase use PHPMock; /** - * @var \Pterodactyl\Contracts\Repository\NodeRepositoryInterface + * @var \Mockery\MockInterface */ - protected $repository; + private $repository; /** - * @var \Pterodactyl\Services\Nodes\NodeCreationService + * @var \Mockery\MockInterface */ - protected $service; + private $encrypter; /** * Setup tests. @@ -36,9 +33,15 @@ class NodeCreationServiceTest extends TestCase { parent::setUp(); - $this->repository = m::mock(NodeRepositoryInterface::class); + /* @noinspection PhpParamsInspection */ + Uuid::setFactory( + m::mock(UuidFactory::class . '[uuid4]', [ + 'uuid4' => Uuid::fromString('00000000-0000-0000-0000-000000000000'), + ]) + ); - $this->service = new NodeCreationService($this->repository); + $this->repository = m::mock(NodeRepositoryInterface::class); + $this->encrypter = m::mock(Encrypter::class); } /** @@ -46,14 +49,31 @@ class NodeCreationServiceTest extends TestCase */ public function testNodeIsCreatedAndDaemonSecretIsGenerated() { - $this->getFunctionMock('\\Pterodactyl\\Services\\Nodes', 'str_random') - ->expects($this->once())->willReturn('random_string'); + /** @var \Pterodactyl\Models\Node $node */ + $node = factory(Node::class)->make(); - $this->repository->shouldReceive('create')->with([ - 'name' => 'NodeName', - 'daemonSecret' => 'random_string', - ])->once()->andReturnNull(); + $this->encrypter->expects('encrypt')->with(m::on(function ($value) { + return strlen($value) === Node::DAEMON_TOKEN_LENGTH; + }))->andReturns('encrypted_value'); - $this->assertNull($this->service->handle(['name' => 'NodeName'])); + $this->repository->expects('create')->with(m::on(function ($value) { + $this->assertTrue(is_array($value)); + $this->assertSame('NodeName', $value['name']); + $this->assertSame('00000000-0000-0000-0000-000000000000', $value['uuid']); + $this->assertSame('encrypted_value', $value['daemon_token']); + $this->assertTrue(strlen($value['daemon_token_id']) === Node::DAEMON_TOKEN_ID_LENGTH); + + return true; + }), true, true)->andReturn($node); + + $this->assertSame($node, $this->getService()->handle(['name' => 'NodeName'])); + } + + /** + * @return \Pterodactyl\Services\Nodes\NodeCreationService + */ + private function getService() + { + return new NodeCreationService($this->encrypter, $this->repository); } } diff --git a/tests/Unit/Services/Nodes/NodeDeletionServiceTest.php b/tests/Unit/Services/Nodes/NodeDeletionServiceTest.php index eb2f05f69..88ebaaaf5 100644 --- a/tests/Unit/Services/Nodes/NodeDeletionServiceTest.php +++ b/tests/Unit/Services/Nodes/NodeDeletionServiceTest.php @@ -12,6 +12,7 @@ namespace Tests\Unit\Services\Nodes; use Mockery as m; use Tests\TestCase; use Pterodactyl\Models\Node; +use Pterodactyl\Exceptions\DisplayException; use Illuminate\Contracts\Translation\Translator; use Pterodactyl\Services\Nodes\NodeDeletionService; use Pterodactyl\Contracts\Repository\NodeRepositoryInterface; @@ -71,11 +72,11 @@ class NodeDeletionServiceTest extends TestCase /** * Test that an exception is thrown if servers are attached to the node. - * - * @expectedException \Pterodactyl\Exceptions\DisplayException */ public function testExceptionIsThrownIfServersAreAttachedToNode() { + $this->expectException(DisplayException::class); + $this->serverRepository->shouldReceive('setColumns')->with('id')->once()->andReturnSelf() ->shouldReceive('findCountWhere')->with([['node_id', '=', 1]])->once()->andReturn(1); $this->translator->shouldReceive('trans')->with('exceptions.node.servers_attached')->once()->andReturnNull(); diff --git a/tests/Unit/Services/Nodes/NodeUpdateServiceTest.php b/tests/Unit/Services/Nodes/NodeUpdateServiceTest.php index c8596b66b..c8138185d 100644 --- a/tests/Unit/Services/Nodes/NodeUpdateServiceTest.php +++ b/tests/Unit/Services/Nodes/NodeUpdateServiceTest.php @@ -2,34 +2,44 @@ namespace Tests\Unit\Services\Nodes; +use Exception; use Mockery as m; use Tests\TestCase; +use GuzzleHttp\Psr7\Request; use phpmock\phpunit\PHPMock; use Pterodactyl\Models\Node; -use GuzzleHttp\Psr7\Response; use Tests\Traits\MocksRequestException; use GuzzleHttp\Exception\ConnectException; +use GuzzleHttp\Exception\TransferException; use Illuminate\Database\ConnectionInterface; +use Illuminate\Contracts\Encryption\Encrypter; use Pterodactyl\Services\Nodes\NodeUpdateService; -use Pterodactyl\Contracts\Repository\NodeRepositoryInterface; -use Pterodactyl\Contracts\Repository\Daemon\ConfigurationRepositoryInterface; +use Pterodactyl\Repositories\Eloquent\NodeRepository; +use Pterodactyl\Repositories\Wings\DaemonConfigurationRepository; +use Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException; +use Pterodactyl\Exceptions\Service\Node\ConfigurationNotPersistedException; class NodeUpdateServiceTest extends TestCase { use PHPMock, MocksRequestException; /** - * @var \Illuminate\Database\ConnectionInterface|\Mockery\Mock + * @var \Mockery\MockInterface */ private $connection; /** - * @var \Pterodactyl\Contracts\Repository\Daemon\ConfigurationRepositoryInterface|\Mockery\Mock + * @var \Mockery\MockInterface */ - private $configRepository; + private $configurationRepository; /** - * @var \Pterodactyl\Contracts\Repository\NodeRepositoryInterface|\Mockery\Mock + * @var \Mockery\MockInterface + */ + private $encrypter; + + /** + * @var \Mockery\MockInterface */ private $repository; @@ -41,8 +51,9 @@ class NodeUpdateServiceTest extends TestCase parent::setUp(); $this->connection = m::mock(ConnectionInterface::class); - $this->configRepository = m::mock(ConfigurationRepositoryInterface::class); - $this->repository = m::mock(NodeRepositoryInterface::class); + $this->encrypter = m::mock(Encrypter::class); + $this->configurationRepository = m::mock(DaemonConfigurationRepository::class); + $this->repository = m::mock(NodeRepository::class); } /** @@ -50,36 +61,59 @@ class NodeUpdateServiceTest extends TestCase */ public function testNodeIsUpdatedAndDaemonSecretIsReset() { - $model = factory(Node::class)->make(); - $updatedModel = factory(Node::class)->make([ - 'name' => 'New Name', - 'daemonSecret' => 'abcd1234', + /** @var \Pterodactyl\Models\Node $model */ + $model = factory(Node::class)->make([ + 'fqdn' => 'https://example.com', ]); - $this->getFunctionMock('\\Pterodactyl\\Services\\Nodes', 'str_random') - ->expects($this->once())->willReturn($updatedModel->daemonSecret); + /** @var \Pterodactyl\Models\Node $updatedModel */ + $updatedModel = factory(Node::class)->make([ + 'name' => 'New Name', + 'fqdn' => 'https://example2.com', + ]); - $this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); - $this->repository->shouldReceive('update')->with($model->id, [ + $this->connection->expects('transaction')->with(m::on(function ($closure) use ($updatedModel) { + $response = $closure(); + + $this->assertIsArray($response); + $this->assertTrue(count($response) === 2); + $this->assertSame($updatedModel, $response[0]); + $this->assertFalse($response[1]); + + return true; + }))->andReturns([$updatedModel, false]); + + $this->encrypter->expects('encrypt')->with(m::on(function ($value) { + return strlen($value) === Node::DAEMON_TOKEN_LENGTH; + }))->andReturns('encrypted_value'); + + $this->repository->expects('withFreshModel->update')->with($model->id, m::on(function ($value) { + $this->assertTrue(is_array($value)); + $this->assertSame('New Name', $value['name']); + $this->assertSame('encrypted_value', $value['daemon_token']); + $this->assertTrue(strlen($value['daemon_token_id']) === Node::DAEMON_TOKEN_ID_LENGTH); + + return true; + }), true, true)->andReturns($updatedModel); + + $this->configurationRepository->expects('setNode')->with(m::on(function ($value) use ($model, $updatedModel) { + $this->assertInstanceOf(Node::class, $value); + $this->assertSame($model->uuid, $value->uuid); + + // Yes, this is correct. Always use the updated model's FQDN when making requests to + // the Daemon so that any changes to that are properly propagated down to the daemon. + // + // @see https://github.com/pterodactyl/panel/issues/1931 + $this->assertSame($updatedModel->fqdn, $value->fqdn); + + return true; + }))->andReturnSelf(); + + $this->configurationRepository->expects('update')->with($updatedModel); + + $this->getService()->handle($model, [ 'name' => $updatedModel->name, - 'daemonSecret' => $updatedModel->daemonSecret, - ])->andReturn($model); - - $cloned = $updatedModel->replicate(['daemonSecret']); - $cloned->daemonSecret = $model->daemonSecret; - - $this->configRepository->shouldReceive('setNode')->with(m::on(function ($model) use ($updatedModel) { - return $model->daemonSecret !== $updatedModel->daemonSecret; - }))->once()->andReturnSelf(); - - $this->configRepository->shouldReceive('update')->with([ - 'keys' => ['abcd1234'], - ])->once()->andReturn(new Response); - - $this->connection->shouldReceive('commit')->withNoArgs()->once()->andReturnNull(); - - $response = $this->getService()->handle($model, ['name' => $updatedModel->name], true); - $this->assertInstanceOf(Node::class, $response); + ], true); } /** @@ -87,56 +121,115 @@ class NodeUpdateServiceTest extends TestCase */ public function testNodeIsUpdatedAndDaemonSecretIsNotChanged() { - $model = factory(Node::class)->make(); + /** @var \Pterodactyl\Models\Node $model */ + $model = factory(Node::class)->make(['fqdn' => 'https://example.com']); - $this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); - $this->repository->shouldReceive('update')->with($model->id, [ - 'name' => 'NewName', - ])->andReturn($model); + /** @var \Pterodactyl\Models\Node $updatedModel */ + $updatedModel = factory(Node::class)->make(['name' => 'New Name', 'fqdn' => $model->fqdn]); - $this->configRepository->shouldReceive('setNode')->with($model)->once()->andReturnSelf() - ->shouldReceive('update')->withNoArgs()->once()->andReturn(new Response); - $this->connection->shouldReceive('commit')->withNoArgs()->once()->andReturnNull(); + $this->connection->expects('transaction')->with(m::on(function ($closure) use ($updatedModel) { + $response = $closure(); - $response = $this->getService()->handle($model, ['name' => 'NewName']); - $this->assertInstanceOf(Node::class, $response); + $this->assertIsArray($response); + $this->assertTrue(count($response) === 2); + $this->assertSame($updatedModel, $response[0]); + $this->assertFalse($response[1]); + + return true; + }))->andReturns([$updatedModel, false]); + + $this->repository->expects('withFreshModel->update')->with($model->id, m::on(function ($value) { + $this->assertTrue(is_array($value)); + $this->assertSame('New Name', $value['name']); + $this->assertArrayNotHasKey('daemon_token', $value); + $this->assertArrayNotHasKey('daemon_token_id', $value); + + return true; + }), true, true)->andReturns($updatedModel); + + $this->configurationRepository->expects('setNode->update')->with($updatedModel); + + $this->getService()->handle($model, ['name' => $updatedModel->name]); } /** * Test that an exception caused by a connection error is handled. - * - * @expectedException \Pterodactyl\Exceptions\Service\Node\ConfigurationNotPersistedException */ public function testExceptionRelatedToConnection() { - $this->configureExceptionMock(ConnectException::class); - $model = factory(Node::class)->make(); + $this->configureExceptionMock(DaemonConnectionException::class); + $this->expectException(ConfigurationNotPersistedException::class); - $this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); - $this->repository->shouldReceive('update')->andReturn($model); + /** @var \Pterodactyl\Models\Node $model */ + $model = factory(Node::class)->make(['fqdn' => 'https://example.com']); - $this->configRepository->shouldReceive('setNode->update')->once()->andThrow($this->getExceptionMock()); - $this->connection->shouldReceive('commit')->withNoArgs()->once()->andReturnNull(); + /** @var \Pterodactyl\Models\Node $updatedModel */ + $updatedModel = factory(Node::class)->make(['name' => 'New Name', 'fqdn' => $model->fqdn]); - $this->getService()->handle($model, ['name' => 'NewName']); + $this->connection->expects('transaction')->with(m::on(function ($closure) use ($updatedModel) { + $response = $closure(); + + $this->assertIsArray($response); + $this->assertTrue(count($response) === 2); + $this->assertSame($updatedModel, $response[0]); + $this->assertTrue($response[1]); + + return true; + }))->andReturn([$updatedModel, true]); + + $this->repository->expects('withFreshModel->update')->with($model->id, m::on(function ($value) { + $this->assertTrue(is_array($value)); + $this->assertSame('New Name', $value['name']); + $this->assertArrayNotHasKey('daemon_token', $value); + $this->assertArrayNotHasKey('daemon_token_id', $value); + + return true; + }), true, true)->andReturns($updatedModel); + + $this->configurationRepository->expects('setNode->update')->with($updatedModel)->andThrow( + new DaemonConnectionException( + new ConnectException('', new Request('GET', 'Test'), new Exception) + ) + ); + + $this->getService()->handle($model, ['name' => $updatedModel->name]); } /** * Test that an exception not caused by a daemon connection error is handled. - * - * @expectedException \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException */ public function testExceptionNotRelatedToConnection() { - $this->configureExceptionMock(); - $model = factory(Node::class)->make(); + /** @var \Pterodactyl\Models\Node $model */ + $model = factory(Node::class)->make(['fqdn' => 'https://example.com']); - $this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); - $this->repository->shouldReceive('update')->andReturn($model); + /** @var \Pterodactyl\Models\Node $updatedModel */ + $updatedModel = factory(Node::class)->make(['name' => 'New Name', 'fqdn' => $model->fqdn]); - $this->configRepository->shouldReceive('setNode->update')->once()->andThrow($this->getExceptionMock()); + $this->connection->expects('transaction')->with(m::on(function ($closure) use ($updatedModel) { + try { + $closure(); + } catch (Exception $exception) { + $this->assertInstanceOf(DaemonConnectionException::class, $exception); + $this->assertSame( + 'There was an exception while attempting to communicate with the daemon resulting in a HTTP/E_CONN_REFUSED response code. This exception has been logged.', + $exception->getMessage() + ); - $this->getService()->handle($model, ['name' => 'NewName']); + return true; + } + + return false; + })); + + $this->repository->expects('withFreshModel->update')->andReturns($updatedModel); + $this->configurationRepository->expects('setNode->update')->andThrow( + new DaemonConnectionException( + new TransferException('', 500, new Exception) + ) + ); + + $this->getService()->handle($model, ['name' => $updatedModel->name]); } /** @@ -146,6 +239,8 @@ class NodeUpdateServiceTest extends TestCase */ private function getService(): NodeUpdateService { - return new NodeUpdateService($this->connection, $this->configRepository, $this->repository); + return new NodeUpdateService( + $this->connection, $this->encrypter, $this->configurationRepository, $this->repository + ); } }