From 90bbe571487e62dbf93935072171fa1cf27cf06f Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Tue, 15 Aug 2017 22:21:47 -0500 Subject: [PATCH] Move services onto new services system, includes tests --- .travis.yml | 1 - .../Repository/ServiceRepositoryInterface.php | 8 + .../HasActiveServersException.php | 2 +- .../Controllers/Admin/OptionController.php | 9 +- .../Controllers/Admin/ServiceController.php | 172 +++++++++++------- .../Admin/Service/ServiceFormRequest.php | 52 ++++++ .../Service/ServiceFunctionsFormRequest.php | 40 ++++ app/Models/Service.php | 71 +++----- .../Eloquent/EloquentRepository.php | 24 +-- .../Eloquent/ServiceRepository.php | 19 +- app/Repositories/Old/ServiceRepository.php | 135 -------------- .../Options/OptionDeletionService.php | 4 +- .../Services/ServiceCreationService.php | 79 ++++++++ .../Services/ServiceDeletionService.php | 74 ++++++++ .../Services/ServiceUpdateService.php | 62 +++++++ app/Traits/Services/CreatesServiceIndex.php | 71 ++++++++ database/factories/ModelFactory.php | 11 ++ ...adeDeletionWhenAParentServiceIsDeleted.php | 36 ++++ resources/lang/en/admin/exceptions.php | 1 + resources/lang/en/admin/services.php | 7 + .../admin/services/functions.blade.php | 5 +- routes/admin.php | 11 +- .../Options/OptionDeletionServiceTest.php | 2 +- .../Services/ServiceCreationServiceTest.php | 94 ++++++++++ .../Services/ServiceDeletionServiceTest.php | 104 +++++++++++ .../Services/ServiceUpdateServiceTest.php | 77 ++++++++ 26 files changed, 899 insertions(+), 272 deletions(-) rename app/Exceptions/Services/{ServiceOption => }/HasActiveServersException.php (95%) create mode 100644 app/Http/Requests/Admin/Service/ServiceFormRequest.php create mode 100644 app/Http/Requests/Admin/Service/ServiceFunctionsFormRequest.php delete mode 100644 app/Repositories/Old/ServiceRepository.php create mode 100644 app/Services/Services/ServiceCreationService.php create mode 100644 app/Services/Services/ServiceDeletionService.php create mode 100644 app/Services/Services/ServiceUpdateService.php create mode 100644 app/Traits/Services/CreatesServiceIndex.php create mode 100644 database/migrations/2017_08_15_214555_CascadeDeletionWhenAParentServiceIsDeleted.php create mode 100644 tests/Unit/Services/Services/ServiceCreationServiceTest.php create mode 100644 tests/Unit/Services/Services/ServiceDeletionServiceTest.php create mode 100644 tests/Unit/Services/Services/ServiceUpdateServiceTest.php diff --git a/.travis.yml b/.travis.yml index 2e93a5458..960a8c192 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,7 +12,6 @@ services: before_install: - mysql -e 'CREATE DATABASE IF NOT EXISTS travis;' before_script: -# - phpenv config-rm xdebug.ini - cp .env.travis .env - composer install --no-interaction --prefer-dist --no-suggest --verbose - php artisan migrate --seed -v diff --git a/app/Contracts/Repository/ServiceRepositoryInterface.php b/app/Contracts/Repository/ServiceRepositoryInterface.php index def30c956..7459d1c40 100644 --- a/app/Contracts/Repository/ServiceRepositoryInterface.php +++ b/app/Contracts/Repository/ServiceRepositoryInterface.php @@ -33,4 +33,12 @@ interface ServiceRepositoryInterface extends RepositoryInterface * @return \Illuminate\Support\Collection */ public function getWithOptions($id = null); + + /** + * Return a service along with its associated options and the servers relation on those options. + * + * @param int $id + * @return mixed + */ + public function getWithOptionServers($id); } diff --git a/app/Exceptions/Services/ServiceOption/HasActiveServersException.php b/app/Exceptions/Services/HasActiveServersException.php similarity index 95% rename from app/Exceptions/Services/ServiceOption/HasActiveServersException.php rename to app/Exceptions/Services/HasActiveServersException.php index e1ea03b33..d560f5683 100644 --- a/app/Exceptions/Services/ServiceOption/HasActiveServersException.php +++ b/app/Exceptions/Services/HasActiveServersException.php @@ -22,7 +22,7 @@ * SOFTWARE. */ -namespace Pterodactyl\Exceptions\Services\ServiceOption; +namespace Pterodactyl\Exceptions\Services; class HasActiveServersException extends \Exception { diff --git a/app/Http/Controllers/Admin/OptionController.php b/app/Http/Controllers/Admin/OptionController.php index fa994d8b8..184f90f9d 100644 --- a/app/Http/Controllers/Admin/OptionController.php +++ b/app/Http/Controllers/Admin/OptionController.php @@ -38,7 +38,7 @@ use Pterodactyl\Http\Requests\Admin\Service\ServiceOptionFormRequest; use Pterodactyl\Services\Services\Options\InstallScriptUpdateService; use Pterodactyl\Contracts\Repository\ServiceOptionRepositoryInterface; use Pterodactyl\Exceptions\Services\ServiceOption\InvalidCopyFromException; -use Pterodactyl\Exceptions\Services\ServiceOption\HasActiveServersException; +use Pterodactyl\Exceptions\Services\HasActiveServersException; use Pterodactyl\Exceptions\Services\ServiceOption\NoParentConfigurationFoundException; class OptionController extends Controller @@ -145,14 +145,14 @@ class OptionController extends Controller /** * Delete a given option from the database. * - * @param \Pterodactyl\Models\ServiceOption $option + * @param \Pterodactyl\Models\ServiceOption $option * @return \Illuminate\Http\RedirectResponse */ - public function delete(ServiceOption $option) + public function destroy(ServiceOption $option) { try { $this->optionDeletionService->handle($option->id); - $this->alert->success()->flash(); + $this->alert->success(trans('admin/services.options.notices.option_deleted'))->flash(); } catch (HasActiveServersException $exception) { $this->alert->danger($exception->getMessage())->flash(); @@ -229,6 +229,7 @@ class OptionController extends Controller * @return \Illuminate\Http\RedirectResponse * * @throws \Pterodactyl\Exceptions\Model\DataValidationException + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException */ public function updateScripts(EditOptionScript $request, ServiceOption $option) { diff --git a/app/Http/Controllers/Admin/ServiceController.php b/app/Http/Controllers/Admin/ServiceController.php index beaac5340..ae10cb34c 100644 --- a/app/Http/Controllers/Admin/ServiceController.php +++ b/app/Http/Controllers/Admin/ServiceController.php @@ -24,37 +24,76 @@ namespace Pterodactyl\Http\Controllers\Admin; -use Log; -use Alert; -use Pterodactyl\Models; -use Illuminate\Http\Request; -use Pterodactyl\Exceptions\DisplayException; +use Pterodactyl\Models\Service; +use Prologue\Alerts\AlertsMessageBag; use Pterodactyl\Http\Controllers\Controller; -use Pterodactyl\Repositories\ServiceRepository; -use Pterodactyl\Exceptions\DisplayValidationException; +use Pterodactyl\Services\Services\ServiceUpdateService; +use Pterodactyl\Services\Services\ServiceCreationService; +use Pterodactyl\Services\Services\ServiceDeletionService; +use Pterodactyl\Exceptions\Services\HasActiveServersException; +use Pterodactyl\Http\Requests\Admin\Service\ServiceFormRequest; +use Pterodactyl\Contracts\Repository\ServiceRepositoryInterface; +use Pterodactyl\Http\Requests\Admin\Service\ServiceFunctionsFormRequest; class ServiceController extends Controller { + /** + * @var \Prologue\Alerts\AlertsMessageBag + */ + protected $alert; + + /** + * @var \Pterodactyl\Services\Services\ServiceCreationService + */ + protected $creationService; + + /** + * @var \Pterodactyl\Services\Services\ServiceDeletionService + */ + protected $deletionService; + + /** + * @var \Pterodactyl\Contracts\Repository\ServiceRepositoryInterface + */ + protected $repository; + + /** + * @var \Pterodactyl\Services\Services\ServiceUpdateService + */ + protected $updateService; + + public function __construct( + AlertsMessageBag $alert, + ServiceCreationService $creationService, + ServiceDeletionService $deletionService, + ServiceRepositoryInterface $repository, + ServiceUpdateService $updateService + ) { + $this->alert = $alert; + $this->creationService = $creationService; + $this->deletionService = $deletionService; + $this->repository = $repository; + $this->updateService = $updateService; + } + /** * Display service overview page. * - * @param \Illuminate\Http\Request $request * @return \Illuminate\View\View */ - public function index(Request $request) + public function index() { return view('admin.services.index', [ - 'services' => Models\Service::withCount('servers', 'options', 'packs')->get(), + 'services' => $this->repository->getWithOptions(), ]); } /** * Display create service page. * - * @param \Illuminate\Http\Request $request * @return \Illuminate\View\View */ - public function create(Request $request) + public function create() { return view('admin.services.new'); } @@ -62,91 +101,96 @@ class ServiceController extends Controller /** * Return base view for a service. * - * @param \Illuminate\Http\Request $request - * @param int $id + * @param int $service * @return \Illuminate\View\View */ - public function view(Request $request, $id) + public function view($service) { return view('admin.services.view', [ - 'service' => Models\Service::with('options', 'options.servers')->findOrFail($id), + 'service' => $this->repository->getWithOptionServers($service), ]); } /** * Return function editing view for a service. * - * @param \Illuminate\Http\Request $request - * @param int $id + * @param \Pterodactyl\Models\Service $service * @return \Illuminate\View\View */ - public function viewFunctions(Request $request, $id) + public function viewFunctions(Service $service) { - return view('admin.services.functions', ['service' => Models\Service::findOrFail($id)]); + return view('admin.services.functions', ['service' => $service]); } /** * Handle post action for new service. * - * @param \Illuminate\Http\Request $request + * @param \Pterodactyl\Http\Requests\Admin\Service\ServiceFormRequest $request * @return \Illuminate\Http\RedirectResponse + * + * @throws \Pterodactyl\Exceptions\Model\DataValidationException */ - public function store(Request $request) + public function store(ServiceFormRequest $request) { - $repo = new ServiceRepository; + $service = $this->creationService->handle($request->normalize()); + $this->alert->success(trans('admin/services.notices.service_created', ['name' => $service->name]))->flash(); - try { - $service = $repo->create($request->intersect([ - 'name', 'description', 'folder', 'startup', - ])); - Alert::success('Successfully created new service!')->flash(); - - return redirect()->route('admin.services.view', $service->id); - } catch (DisplayValidationException $ex) { - return redirect()->route('admin.services.new')->withErrors(json_decode($ex->getMessage()))->withInput(); - } catch (DisplayException $ex) { - Alert::danger($ex->getMessage())->flash(); - } catch (\Exception $ex) { - Log::error($ex); - Alert::danger('An error occured while attempting to add a new service. This error has been logged.')->flash(); - } - - return redirect()->route('admin.services.new')->withInput(); + return redirect()->route('admin.services.view', $service->id); } /** * Edits configuration for a specific service. * - * @param \Illuminate\Http\Request $request - * @param int $id + * @param \Pterodactyl\Http\Requests\Admin\Service\ServiceFormRequest $request + * @param \Pterodactyl\Models\Service $service + * @return \Illuminate\Http\RedirectResponse + * + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + */ + public function update(ServiceFormRequest $request, Service $service) + { + $this->updateService->handle($service->id, $request->normalize()); + $this->alert->success(trans('admin/services.notices.service_updated'))->flash(); + + return redirect()->route('admin.services.view', $service); + } + + /** + * Update the functions file for a service. + * + * @param \Pterodactyl\Http\Requests\Admin\Service\ServiceFunctionsFormRequest $request + * @param \Pterodactyl\Models\Service $service + * @return \Illuminate\Http\RedirectResponse + * + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + */ + public function updateFunctions(ServiceFunctionsFormRequest $request, Service $service) + { + $this->updateService->handle($service->id, $request->normalize()); + $this->alert->success(trans('admin/services.notices.functions_updated'))->flash(); + + return redirect()->route('admin.services.view.functions', $service->id); + } + + /** + * Delete a service from the panel. + * + * @param \Pterodactyl\Models\Service $service * @return \Illuminate\Http\RedirectResponse */ - public function edit(Request $request, $id) + public function destroy(Service $service) { - $repo = new ServiceRepository; - $redirectTo = ($request->input('redirect_to')) ? 'admin.services.view.functions' : 'admin.services.view'; - try { - if ($request->input('action') !== 'delete') { - $repo->update($id, $request->intersect([ - 'name', 'description', 'folder', 'startup', 'index_file', - ])); - Alert::success('Service has been updated successfully.')->flash(); - } else { - $repo->delete($id); - Alert::success('Successfully deleted service from the system.')->flash(); + $this->deletionService->handle($service->id); + $this->alert->success(trans('admin/services.notices.service_deleted'))->flash(); + } catch (HasActiveServersException $exception) { + $this->alert->danger($exception->getMessage())->flash(); - return redirect()->route('admin.services'); - } - } catch (DisplayValidationException $ex) { - return redirect()->route($redirectTo, $id)->withErrors(json_decode($ex->getMessage()))->withInput(); - } catch (DisplayException $ex) { - Alert::danger($ex->getMessage())->flash(); - } catch (\Exception $ex) { - Log::error($ex); - Alert::danger('An error occurred while attempting to update this service. This error has been logged.')->flash(); + return redirect()->back(); } - return redirect()->route($redirectTo, $id); + return redirect()->route('admin.services'); } } diff --git a/app/Http/Requests/Admin/Service/ServiceFormRequest.php b/app/Http/Requests/Admin/Service/ServiceFormRequest.php new file mode 100644 index 000000000..6d6be5e1e --- /dev/null +++ b/app/Http/Requests/Admin/Service/ServiceFormRequest.php @@ -0,0 +1,52 @@ +. + * + * 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 Pterodactyl\Http\Requests\Admin\Service; + +use Pterodactyl\Http\Requests\Admin\AdminFormRequest; + +class ServiceFormRequest extends AdminFormRequest +{ + /** + * @return array + */ + public function rules() + { + $rules = [ + 'name' => 'required|string|min:1|max:255', + 'description' => 'required|nullable|string', + 'folder' => 'required|regex:/^[\w.-]{1,50}$/|unique:services,folder', + 'startup' => 'required|nullable|string', + ]; + + if ($this->method() === 'PATCH') { + $service = $this->route()->parameter('service'); + $rules['folder'] = $rules['folder'] . ',' . $service->id; + + return $rules; + } + + return $rules; + } +} diff --git a/app/Http/Requests/Admin/Service/ServiceFunctionsFormRequest.php b/app/Http/Requests/Admin/Service/ServiceFunctionsFormRequest.php new file mode 100644 index 000000000..d5836c234 --- /dev/null +++ b/app/Http/Requests/Admin/Service/ServiceFunctionsFormRequest.php @@ -0,0 +1,40 @@ +. + * + * 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 Pterodactyl\Http\Requests\Admin\Service; + +use Pterodactyl\Http\Requests\Admin\AdminFormRequest; + +class ServiceFunctionsFormRequest extends AdminFormRequest +{ + /** + * @return array + */ + public function rules() + { + return [ + 'index_file' => 'required|nullable|string', + ]; + } +} diff --git a/app/Models/Service.php b/app/Models/Service.php index b05366882..25355981b 100644 --- a/app/Models/Service.php +++ b/app/Models/Service.php @@ -24,10 +24,16 @@ namespace Pterodactyl\Models; +use Sofa\Eloquence\Eloquence; +use Sofa\Eloquence\Validable; use Illuminate\Database\Eloquent\Model; +use Sofa\Eloquence\Contracts\CleansAttributes; +use Sofa\Eloquence\Contracts\Validable as ValidableContract; -class Service extends Model +class Service extends Model implements CleansAttributes, ValidableContract { + use Eloquence, Validable; + /** * The table associated with the model. * @@ -40,52 +46,31 @@ class Service extends Model * * @var array */ - protected $fillable = [ - 'name', 'description', 'folder', 'startup', 'index_file', + protected $fillable = ['name', 'author', 'description', 'folder', 'startup', 'index_file']; + + /** + * @var array + */ + protected static $applicationRules = [ + 'author' => 'required', + 'name' => 'required', + 'description' => 'sometimes', + 'folder' => 'required', + 'startup' => 'sometimes', + 'index_file' => 'required', ]; /** - * Returns the default contents of the index.js file for a service. - * - * @return string + * @var array */ - public static function defaultIndexFile() - { - return <<<'EOF' -'use strict'; - -/** - * Pterodactyl - Daemon - * Copyright (c) 2015 - 2017 Dane Everitt - * - * 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. - */ -const rfr = require('rfr'); -const _ = require('lodash'); - -const Core = rfr('src/services/index.js'); - -class Service extends Core {} - -module.exports = Service; -EOF; - } + protected static $dataIntegrityRules = [ + 'author' => 'string|size:36', + 'name' => 'string|max:255', + 'description' => 'nullable|string', + 'folder' => 'string|max:255|regex:/^[\w.-]{1,50}$/|unique:services,folder', + 'startup' => 'nullable|string', + 'index_file' => 'string', + ]; /** * Gets all service options associated with this service. diff --git a/app/Repositories/Eloquent/EloquentRepository.php b/app/Repositories/Eloquent/EloquentRepository.php index 0dd1beee2..fce974030 100644 --- a/app/Repositories/Eloquent/EloquentRepository.php +++ b/app/Repositories/Eloquent/EloquentRepository.php @@ -49,8 +49,8 @@ abstract class EloquentRepository extends Repository implements RepositoryInterf */ public function create(array $fields, $validate = true, $force = false) { - Assert::boolean($validate, 'Second argument passed to create should be boolean, recieved %s.'); - Assert::boolean($force, 'Third argument passed to create should be boolean, received %s.'); + Assert::boolean($validate, 'Second argument passed to create must be boolean, recieved %s.'); + Assert::boolean($force, 'Third argument passed to create must be boolean, received %s.'); $instance = $this->getBuilder()->newModelInstance(); @@ -77,7 +77,7 @@ abstract class EloquentRepository extends Repository implements RepositoryInterf */ public function find($id) { - Assert::integer($id, 'First argument passed to find should be integer, received %s.'); + Assert::numeric($id, 'First argument passed to find must be numeric, received %s.'); $instance = $this->getBuilder()->find($id, $this->getColumns()); @@ -125,8 +125,8 @@ abstract class EloquentRepository extends Repository implements RepositoryInterf */ public function delete($id, $destroy = false) { - Assert::integer($id, 'First argument passed to delete should be integer, received %s.'); - Assert::boolean($destroy, 'Second argument passed to delete should be boolean, received %s.'); + Assert::numeric($id, 'First argument passed to delete must be numeric, received %s.'); + Assert::boolean($destroy, 'Second argument passed to delete must be boolean, received %s.'); $instance = $this->getBuilder()->where($this->getModel()->getKeyName(), $id); @@ -138,7 +138,7 @@ abstract class EloquentRepository extends Repository implements RepositoryInterf */ public function deleteWhere(array $attributes, $force = false) { - Assert::boolean($force, 'Second argument passed to deleteWhere should be boolean, received %s.'); + Assert::boolean($force, 'Second argument passed to deleteWhere must be boolean, received %s.'); $instance = $this->getBuilder()->where($attributes); @@ -150,9 +150,9 @@ abstract class EloquentRepository extends Repository implements RepositoryInterf */ public function update($id, array $fields, $validate = true, $force = false) { - Assert::integer($id, 'First argument passed to update expected to be integer, received %s.'); - Assert::boolean($validate, 'Third argument passed to update should be boolean, received %s.'); - Assert::boolean($force, 'Fourth argument passed to update should be boolean, received %s.'); + Assert::numeric($id, 'First argument passed to update must be numeric, received %s.'); + Assert::boolean($validate, 'Third argument passed to update must be boolean, received %s.'); + Assert::boolean($force, 'Fourth argument passed to update must be boolean, received %s.'); $instance = $this->getBuilder()->where('id', $id)->first(); @@ -182,7 +182,7 @@ abstract class EloquentRepository extends Repository implements RepositoryInterf */ public function updateWhereIn($column, array $values, array $fields) { - Assert::stringNotEmpty($column, 'First argument passed to updateWhereIn expected to be a string, received %s.'); + Assert::stringNotEmpty($column, 'First argument passed to updateWhereIn must be a non-empty string, received %s.'); return $this->getBuilder()->whereIn($column, $values)->update($fields); } @@ -255,8 +255,8 @@ abstract class EloquentRepository extends Repository implements RepositoryInterf */ public function updateOrCreate(array $where, array $fields, $validate = true, $force = false) { - Assert::boolean($validate, 'Third argument passed to updateOrCreate should be boolean, received %s.'); - Assert::boolean($force, 'Fourth argument passed to updateOrCreate should be boolean, received %s.'); + Assert::boolean($validate, 'Third argument passed to updateOrCreate must be boolean, received %s.'); + Assert::boolean($force, 'Fourth argument passed to updateOrCreate must be boolean, received %s.'); $instance = $this->withColumns('id')->findWhere($where)->first(); diff --git a/app/Repositories/Eloquent/ServiceRepository.php b/app/Repositories/Eloquent/ServiceRepository.php index aca364902..9e2cd408b 100644 --- a/app/Repositories/Eloquent/ServiceRepository.php +++ b/app/Repositories/Eloquent/ServiceRepository.php @@ -24,6 +24,7 @@ namespace Pterodactyl\Repositories\Eloquent; +use Webmozart\Assert\Assert; use Pterodactyl\Models\Service; use Pterodactyl\Exceptions\Repository\RecordNotFoundException; use Pterodactyl\Contracts\Repository\ServiceRepositoryInterface; @@ -43,11 +44,12 @@ class ServiceRepository extends EloquentRepository implements ServiceRepositoryI */ public function getWithOptions($id = null) { + Assert::nullOrNumeric($id, 'First argument passed to getWithOptions must be null or numeric, received %s.'); + $instance = $this->getBuilder()->with('options.packs', 'options.variables'); if (! is_null($id)) { $instance = $instance->find($id, $this->getColumns()); - if (! $instance) { throw new RecordNotFoundException(); } @@ -57,4 +59,19 @@ class ServiceRepository extends EloquentRepository implements ServiceRepositoryI return $instance->get($this->getColumns()); } + + /** + * {@inheritdoc} + */ + public function getWithOptionServers($id) + { + Assert::numeric($id, 'First argument passed to getWithOptionServers must be numeric, received %s.'); + + $instance = $this->getBuilder()->with('options.servers')->find($id, $this->getColumns()); + if (! $instance) { + throw new RecordNotFoundException(); + } + + return $instance; + } } diff --git a/app/Repositories/Old/ServiceRepository.php b/app/Repositories/Old/ServiceRepository.php deleted file mode 100644 index a0d1716cc..000000000 --- a/app/Repositories/Old/ServiceRepository.php +++ /dev/null @@ -1,135 +0,0 @@ -. - * - * 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 Pterodactyl\Repositories; - -use DB; -use Validator; -use Pterodactyl\Models\Service; -use Pterodactyl\Exceptions\DisplayException; -use Pterodactyl\Exceptions\DisplayValidationException; - -class ServiceRepository -{ - /** - * Creates a new service on the system. - * - * @param array $data - * @return \Pterodactyl\Models\Service - * - * @throws \Pterodactyl\Exceptions\DisplayValidationException - */ - public function create(array $data) - { - $validator = Validator::make($data, [ - 'name' => 'required|string|min:1|max:255', - 'description' => 'required|nullable|string', - 'folder' => 'required|unique:services,folder|regex:/^[\w.-]{1,50}$/', - 'startup' => 'required|nullable|string', - ]); - - if ($validator->fails()) { - throw new DisplayValidationException(json_encode($validator->errors())); - } - - return DB::transaction(function () use ($data) { - $service = new Service; - $service->author = config('pterodactyl.service.author'); - $service->fill([ - 'name' => $data['name'], - 'description' => (isset($data['description'])) ? $data['description'] : null, - 'folder' => $data['folder'], - 'startup' => (isset($data['startup'])) ? $data['startup'] : null, - 'index_file' => Service::defaultIndexFile(), - ])->save(); - - // It is possible for an event to return false or throw an exception - // which won't necessarily be detected by this transaction. - // - // This check ensures the model was actually saved. - if (! $service->exists) { - throw new \Exception('Service model was created however the response appears to be invalid. Did an event fire wrongly?'); - } - - return $service; - }); - } - - /** - * Updates a service. - * - * @param int $id - * @param array $data - * @return \Pterodactyl\Models\Service - * - * @throws \Pterodactyl\Exceptions\DisplayValidationException - */ - public function update($id, array $data) - { - $service = Service::findOrFail($id); - - $validator = Validator::make($data, [ - 'name' => 'sometimes|required|string|min:1|max:255', - 'description' => 'sometimes|required|nullable|string', - 'folder' => 'sometimes|required|regex:/^[\w.-]{1,50}$/', - 'startup' => 'sometimes|required|nullable|string', - 'index_file' => 'sometimes|required|string', - ]); - - if ($validator->fails()) { - throw new DisplayValidationException(json_encode($validator->errors())); - } - - return DB::transaction(function () use ($data, $service) { - $service->fill($data)->save(); - - return $service; - }); - } - - /** - * Deletes a service and associated files and options. - * - * @param int $id - * @return void - * - * @throws \Pterodactyl\Exceptions\DisplayException - */ - public function delete($id) - { - $service = Service::withCount('servers')->with('options')->findOrFail($id); - - if ($service->servers_count > 0) { - throw new DisplayException('You cannot delete a service that has servers associated with it.'); - } - - DB::transaction(function () use ($service) { - foreach ($service->options as $option) { - (new OptionRepository)->delete($option->id); - } - - $service->delete(); - }); - } -} diff --git a/app/Services/Services/Options/OptionDeletionService.php b/app/Services/Services/Options/OptionDeletionService.php index aa392f353..8afd6e2e4 100644 --- a/app/Services/Services/Options/OptionDeletionService.php +++ b/app/Services/Services/Options/OptionDeletionService.php @@ -26,7 +26,7 @@ namespace Pterodactyl\Services\Services\Options; use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; use Pterodactyl\Contracts\Repository\ServiceOptionRepositoryInterface; -use Pterodactyl\Exceptions\Services\ServiceOption\HasActiveServersException; +use Pterodactyl\Exceptions\Services\HasActiveServersException; class OptionDeletionService { @@ -60,7 +60,7 @@ class OptionDeletionService * @param int $option * @return int * - * @throws \Pterodactyl\Exceptions\Services\ServiceOption\HasActiveServersException + * @throws \Pterodactyl\Exceptions\Services\HasActiveServersException */ public function handle($option) { diff --git a/app/Services/Services/ServiceCreationService.php b/app/Services/Services/ServiceCreationService.php new file mode 100644 index 000000000..9e9b4e208 --- /dev/null +++ b/app/Services/Services/ServiceCreationService.php @@ -0,0 +1,79 @@ +. + * + * 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 Pterodactyl\Services\Services; + +use Pterodactyl\Traits\Services\CreatesServiceIndex; +use Illuminate\Contracts\Config\Repository as ConfigRepository; +use Pterodactyl\Contracts\Repository\ServiceRepositoryInterface; + +class ServiceCreationService +{ + use CreatesServiceIndex; + + /** + * @var \Illuminate\Contracts\Config\Repository + */ + protected $config; + + /** + * @var \Pterodactyl\Contracts\Repository\ServiceRepositoryInterface + */ + protected $repository; + + /** + * ServiceCreationService constructor. + * + * @param \Illuminate\Contracts\Config\Repository $config + * @param \Pterodactyl\Contracts\Repository\ServiceRepositoryInterface $repository + */ + public function __construct( + ConfigRepository $config, + ServiceRepositoryInterface $repository + ) { + $this->config = $config; + $this->repository = $repository; + } + + /** + * Create a new service on the system. + * + * @param array $data + * @return \Pterodactyl\Models\Service + * + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + */ + public function handle(array $data) + { + return $this->repository->create(array_merge([ + 'author' => $this->config->get('pterodactyl.service.author'), + ], [ + 'name' => array_get($data, 'name'), + 'description' => array_get($data, 'description'), + 'folder' => array_get($data, 'folder'), + 'startup' => array_get($data, 'startup'), + 'index_file' => $this->getIndexScript(), + ])); + } +} diff --git a/app/Services/Services/ServiceDeletionService.php b/app/Services/Services/ServiceDeletionService.php new file mode 100644 index 000000000..5d0405b17 --- /dev/null +++ b/app/Services/Services/ServiceDeletionService.php @@ -0,0 +1,74 @@ +. + * + * 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 Pterodactyl\Services\Services; + +use Pterodactyl\Exceptions\Services\HasActiveServersException; +use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; +use Pterodactyl\Contracts\Repository\ServiceRepositoryInterface; + +class ServiceDeletionService +{ + /** + * @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface + */ + protected $serverRepository; + + /** + * @var \Pterodactyl\Contracts\Repository\ServiceRepositoryInterface + */ + protected $repository; + + /** + * ServiceDeletionService constructor. + * + * @param \Pterodactyl\Contracts\Repository\ServerRepositoryInterface $serverRepository + * @param \Pterodactyl\Contracts\Repository\ServiceRepositoryInterface $repository + */ + public function __construct( + ServerRepositoryInterface $serverRepository, + ServiceRepositoryInterface $repository + ) { + $this->serverRepository = $serverRepository; + $this->repository = $repository; + } + + /** + * Delete a service from the system only if there are no servers attached to it. + * + * @param int $service + * @return int + * + * @throws \Pterodactyl\Exceptions\Services\HasActiveServersException + */ + public function handle($service) + { + $count = $this->serverRepository->findCountWhere([['service_id', '=', $service]]); + if ($count > 0) { + throw new HasActiveServersException(trans('admin/exceptions.service.delete_has_servers')); + } + + return $this->repository->delete($service); + } +} diff --git a/app/Services/Services/ServiceUpdateService.php b/app/Services/Services/ServiceUpdateService.php new file mode 100644 index 000000000..203f52dfc --- /dev/null +++ b/app/Services/Services/ServiceUpdateService.php @@ -0,0 +1,62 @@ +. + * + * 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 Pterodactyl\Services\Services; + +use Pterodactyl\Contracts\Repository\ServiceRepositoryInterface; + +class ServiceUpdateService +{ + /** + * @var \Pterodactyl\Contracts\Repository\ServiceRepositoryInterface + */ + protected $repository; + + /** + * ServiceUpdateService constructor. + * + * @param \Pterodactyl\Contracts\Repository\ServiceRepositoryInterface $repository + */ + public function __construct(ServiceRepositoryInterface $repository) + { + $this->repository = $repository; + } + + /** + * Update a service and prevent changing the author once it is set. + * + * @param int $service + * @param array $data + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + */ + public function handle($service, array $data) + { + if (! is_null(array_get($data, 'author'))) { + unset($data['author']); + } + + $this->repository->withoutFresh()->update($service, $data); + } +} diff --git a/app/Traits/Services/CreatesServiceIndex.php b/app/Traits/Services/CreatesServiceIndex.php new file mode 100644 index 000000000..dcff053b0 --- /dev/null +++ b/app/Traits/Services/CreatesServiceIndex.php @@ -0,0 +1,71 @@ +. + * + * 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 Pterodactyl\Traits\Services; + +trait CreatesServiceIndex +{ + /** + * Returns the default index.js file that is used for services on the daemon. + * + * @return string + */ + public function getIndexScript() + { + return <<<'EOF' +'use strict'; + +/** + * Pterodactyl - Daemon + * Copyright (c) 2015 - 2017 Dane Everitt + * + * 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. + */ +const rfr = require('rfr'); +const _ = require('lodash'); + +const Core = rfr('src/services/index.js'); + +class Service extends Core {} + +module.exports = Service; +EOF; + } +} diff --git a/database/factories/ModelFactory.php b/database/factories/ModelFactory.php index 4da5007d9..fdef71ae2 100644 --- a/database/factories/ModelFactory.php +++ b/database/factories/ModelFactory.php @@ -88,6 +88,17 @@ $factory->define(Pterodactyl\Models\Node::class, function (Faker\Generator $fake ]; }); +$factory->define(Pterodactyl\Models\Service::class, function (Faker\Generator $faker) { + return [ + 'author' => $faker->unique()->uuid, + 'name' => $faker->word, + 'description' => null, + 'folder' => strtolower($faker->unique()->word), + 'startup' => 'java -jar test.jar', + 'index_file' => 'indexjs', + ]; +}); + $factory->define(Pterodactyl\Models\ServiceOption::class, function (Faker\Generator $faker) { return [ 'id' => $faker->unique()->randomNumber(), diff --git a/database/migrations/2017_08_15_214555_CascadeDeletionWhenAParentServiceIsDeleted.php b/database/migrations/2017_08_15_214555_CascadeDeletionWhenAParentServiceIsDeleted.php new file mode 100644 index 000000000..aef299028 --- /dev/null +++ b/database/migrations/2017_08_15_214555_CascadeDeletionWhenAParentServiceIsDeleted.php @@ -0,0 +1,36 @@ +dropForeign(['service_id']); + + $table->foreign('service_id')->references('id')->on('services')->onDelete('cascade'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('service_options', function (Blueprint $table) { + $table->dropForeign(['service_id']); + + $table->foreign('service_id')->references('id')->on('services'); + }); + } +} diff --git a/resources/lang/en/admin/exceptions.php b/resources/lang/en/admin/exceptions.php index 1a5756252..bea2c83f9 100644 --- a/resources/lang/en/admin/exceptions.php +++ b/resources/lang/en/admin/exceptions.php @@ -34,6 +34,7 @@ return [ 'cidr_out_of_range' => 'CIDR notation only allows masks between /25 and /32.', ], 'service' => [ + 'delete_has_servers' => 'A service with active servers attached to it cannot be deleted from the Panel.', 'options' => [ 'delete_has_servers' => 'A service option with active servers attached to it cannot be deleted from the Panel.', 'invalid_copy_id' => 'The service option selected for copying a script from either does not exist, or is copying a script itself.', diff --git a/resources/lang/en/admin/services.php b/resources/lang/en/admin/services.php index 8390140ea..91c851f32 100644 --- a/resources/lang/en/admin/services.php +++ b/resources/lang/en/admin/services.php @@ -23,8 +23,15 @@ */ return [ + 'notices' => [ + 'service_created' => 'A new service, :name, has been successfully created.', + 'service_deleted' => 'Successfully deleted the requested service from the Panel.', + 'service_updated' => 'Successfully updated the service configuration options.', + 'functions_updated' => 'The service functions file has been updated. You will need to reboot your Nodes in order for these changes to be applied.', + ], 'options' => [ 'notices' => [ + 'option_deleted' => 'Successfully deleted the requested service option from the Panel.', 'option_updated' => 'Service option configuration has been updated successfully.', 'script_updated' => 'Service option install script has been updated and will run whenever servers are installed.', 'option_created' => 'New service option was created successfully. You will need to restart any running daemons to apply this new service.', diff --git a/resources/themes/pterodactyl/admin/services/functions.blade.php b/resources/themes/pterodactyl/admin/services/functions.blade.php index 3f0e89d43..ce06f8f66 100644 --- a/resources/themes/pterodactyl/admin/services/functions.blade.php +++ b/resources/themes/pterodactyl/admin/services/functions.blade.php @@ -50,15 +50,14 @@

Functions Control

-
+
{{ $service->index_file }}
diff --git a/routes/admin.php b/routes/admin.php index 229cf1884..664ccf528 100644 --- a/routes/admin.php +++ b/routes/admin.php @@ -166,8 +166,8 @@ Route::group(['prefix' => 'nodes'], function () { Route::group(['prefix' => 'services'], function () { Route::get('/', 'ServiceController@index')->name('admin.services'); Route::get('/new', 'ServiceController@create')->name('admin.services.new'); - Route::get('/view/{id}', 'ServiceController@view')->name('admin.services.view'); - Route::get('/view/{id}/functions', 'ServiceController@viewFunctions')->name('admin.services.view.functions'); + Route::get('/view/{service}', 'ServiceController@view')->name('admin.services.view'); + Route::get('/view/{service}/functions', 'ServiceController@viewFunctions')->name('admin.services.view.functions'); Route::get('/option/new', 'OptionController@create')->name('admin.services.option.new'); Route::get('/option/{option}', 'OptionController@viewConfiguration')->name('admin.services.option.view'); Route::get('/option/{option}/variables', 'VariableController@view')->name('admin.services.option.variables'); @@ -177,13 +177,14 @@ Route::group(['prefix' => 'services'], function () { Route::post('/option/new', 'OptionController@store'); Route::post('/option/{option}/variables', 'VariableController@store'); - Route::patch('/view/{option}', 'ServiceController@edit'); + Route::patch('/view/{service}', 'ServiceController@update'); + Route::patch('/view/{service}/functions', 'ServiceController@updateFunctions'); Route::patch('/option/{option}', 'OptionController@editConfiguration'); Route::patch('/option/{option}/scripts', 'OptionController@updateScripts'); Route::patch('/option/{option}/variables/{variable}', 'VariableController@update')->name('admin.services.option.variables.edit'); - Route::delete('/view/{id}', 'ServiceController@delete'); - Route::delete('/option/{option}', 'OptionController@delete'); + Route::delete('/view/{service}', 'ServiceController@destroy'); + Route::delete('/option/{option}', 'OptionController@destroy'); Route::delete('/option/{option}/variables/{variable}', 'VariableController@delete'); }); diff --git a/tests/Unit/Services/Services/Options/OptionDeletionServiceTest.php b/tests/Unit/Services/Services/Options/OptionDeletionServiceTest.php index 725f6d0cb..1c2aa5c4d 100644 --- a/tests/Unit/Services/Services/Options/OptionDeletionServiceTest.php +++ b/tests/Unit/Services/Services/Options/OptionDeletionServiceTest.php @@ -27,7 +27,7 @@ namespace Tests\Unit\Services\Services\Options; use Mockery as m; use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; use Pterodactyl\Contracts\Repository\ServiceOptionRepositoryInterface; -use Pterodactyl\Exceptions\Services\ServiceOption\HasActiveServersException; +use Pterodactyl\Exceptions\Services\HasActiveServersException; use Pterodactyl\Services\Services\Options\OptionDeletionService; use Tests\TestCase; diff --git a/tests/Unit/Services/Services/ServiceCreationServiceTest.php b/tests/Unit/Services/Services/ServiceCreationServiceTest.php new file mode 100644 index 000000000..e0989a40c --- /dev/null +++ b/tests/Unit/Services/Services/ServiceCreationServiceTest.php @@ -0,0 +1,94 @@ +. + * + * 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; + +use Mockery as m; +use Pterodactyl\Contracts\Repository\ServiceRepositoryInterface; +use Pterodactyl\Models\Service; +use Pterodactyl\Services\Services\ServiceCreationService; +use Pterodactyl\Traits\Services\CreatesServiceIndex; +use Tests\TestCase; +use Illuminate\Contracts\Config\Repository; + +class ServiceCreationServiceTest extends TestCase +{ + use CreatesServiceIndex; + + /** + * @var \Illuminate\Contracts\Config\Repository + */ + protected $config; + + /** + * @var \Pterodactyl\Contracts\Repository\ServiceRepositoryInterface + */ + protected $repository; + + /** + * @var \Pterodactyl\Services\Services\ServiceCreationService + */ + protected $service; + + /** + * Setup tests. + */ + public function setUp() + { + parent::setUp(); + + $this->config = m::mock(Repository::class); + $this->repository = m::mock(ServiceRepositoryInterface::class); + + $this->service = new ServiceCreationService($this->config, $this->repository); + } + + /** + * Test that a new service can be created using the correct data. + */ + public function testCreateNewService() + { + $model = factory(Service::class)->make(); + $data = [ + 'name' => $model->name, + 'description' => $model->description, + 'folder' => $model->folder, + 'startup' => $model->startup, + ]; + + $this->config->shouldReceive('get')->with('pterodactyl.service.author')->once()->andReturn('0000-author'); + $this->repository->shouldReceive('create')->with([ + 'author' => '0000-author', + 'name' => $data['name'], + 'description' => $data['description'], + 'folder' => $data['folder'], + 'startup' => $data['startup'], + 'index_file' => $this->getIndexScript(), + ])->once()->andReturn($model); + + $response = $this->service->handle($data); + $this->assertInstanceOf(Service::class, $response); + $this->assertEquals($model, $response); + } +} diff --git a/tests/Unit/Services/Services/ServiceDeletionServiceTest.php b/tests/Unit/Services/Services/ServiceDeletionServiceTest.php new file mode 100644 index 000000000..66ded0d7f --- /dev/null +++ b/tests/Unit/Services/Services/ServiceDeletionServiceTest.php @@ -0,0 +1,104 @@ +. + * + * 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; + +use Exception; +use Mockery as m; +use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; +use Pterodactyl\Contracts\Repository\ServiceRepositoryInterface; +use Pterodactyl\Exceptions\Services\HasActiveServersException; +use Pterodactyl\Services\Services\ServiceDeletionService; +use Tests\TestCase; + +class ServiceDeletionServiceTest extends TestCase +{ + /** + * @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface + */ + protected $serverRepository; + + /** + * @var \Pterodactyl\Contracts\Repository\ServiceRepositoryInterface + */ + protected $repository; + + /** + * @var \Pterodactyl\Services\Services\ServiceDeletionService + */ + protected $service; + + /** + * Setup tests. + */ + public function setUp() + { + parent::setUp(); + + $this->serverRepository = m::mock(ServerRepositoryInterface::class); + $this->repository = m::mock(ServiceRepositoryInterface::class); + + $this->service = new ServiceDeletionService($this->serverRepository, $this->repository); + } + + /** + * Test that a service is deleted when there are no servers attached to a service. + */ + public function testServiceIsDeleted() + { + $this->serverRepository->shouldReceive('findCountWhere')->with([['service_id', '=', 1]])->once()->andReturn(0); + $this->repository->shouldReceive('delete')->with(1)->once()->andReturn(1); + + $this->assertEquals(1, $this->service->handle(1)); + } + + /** + * Test that an exception is thrown when there are servers attached to a service. + * + * @dataProvider serverCountProvider + */ + public function testExceptionIsThrownIfServersAreAttached($count) + { + $this->serverRepository->shouldReceive('findCountWhere')->with([['service_id', '=', 1]])->once()->andReturn($count); + + try { + $this->service->handle(1); + } catch (Exception $exception) { + $this->assertInstanceOf(HasActiveServersException::class, $exception); + $this->assertEquals(trans('admin/exceptions.service.delete_has_servers'), $exception->getMessage()); + } + } + + /** + * Provide assorted server counts to ensure that an exception is always thrown when more than 0 servers are found. + * + * @return array + */ + public function serverCountProvider() + { + return [ + [1], [2], [5], [10], + ]; + } +} diff --git a/tests/Unit/Services/Services/ServiceUpdateServiceTest.php b/tests/Unit/Services/Services/ServiceUpdateServiceTest.php new file mode 100644 index 000000000..40baa8115 --- /dev/null +++ b/tests/Unit/Services/Services/ServiceUpdateServiceTest.php @@ -0,0 +1,77 @@ +. + * + * 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; + +use Mockery as m; +use Pterodactyl\Contracts\Repository\ServiceRepositoryInterface; +use Pterodactyl\Services\Services\ServiceUpdateService; +use Tests\TestCase; + +class ServiceUpdateServiceTest extends TestCase +{ + /** + * @var \Pterodactyl\Contracts\Repository\ServiceRepositoryInterface + */ + protected $repository; + + /** + * @var \Pterodactyl\Services\Services\ServiceUpdateService + */ + protected $service; + + /** + * Setup tests. + */ + public function setUp() + { + parent::setUp(); + + $this->repository = m::mock(ServiceRepositoryInterface::class); + + $this->service = new ServiceUpdateService($this->repository); + } + + /** + * Test that the author key is removed from the data array before updating the record. + */ + public function testAuthorArrayKeyIsRemovedIfPassed() + { + $this->repository->shouldReceive('withoutFresh')->withNoArgs()->once()->andReturnSelf() + ->shouldReceive('update')->with(1, ['otherfield' => 'value'])->once()->andReturnNull(); + + $this->service->handle(1, ['author' => 'author1', 'otherfield' => 'value']); + } + + /** + * Test that the function continues to work when no author key is passed. + */ + public function testServiceIsUpdatedWhenNoAuthorKeyIsPassed() + { + $this->repository->shouldReceive('withoutFresh')->withNoArgs()->once()->andReturnSelf() + ->shouldReceive('update')->with(1, ['otherfield' => 'value'])->once()->andReturnNull(); + + $this->service->handle(1, ['otherfield' => 'value']); + } +}