1
1
mirror of https://github.com/pterodactyl/panel.git synced 2024-11-22 09:02:28 +01:00

Move services onto new services system, includes tests

This commit is contained in:
Dane Everitt 2017-08-15 22:21:47 -05:00
parent e91079d128
commit 90bbe57148
No known key found for this signature in database
GPG Key ID: EEA66103B3D71F53
26 changed files with 899 additions and 272 deletions

View File

@ -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

View File

@ -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);
}

View File

@ -22,7 +22,7 @@
* SOFTWARE.
*/
namespace Pterodactyl\Exceptions\Services\ServiceOption;
namespace Pterodactyl\Exceptions\Services;
class HasActiveServersException extends \Exception
{

View File

@ -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)
{

View File

@ -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');
}
}

View File

@ -0,0 +1,52 @@
<?php
/*
* Pterodactyl - Panel
* Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com>.
*
* 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;
}
}

View File

@ -0,0 +1,40 @@
<?php
/*
* Pterodactyl - Panel
* Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com>.
*
* 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',
];
}
}

View File

@ -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 <dane@daneeveritt.com>
*
* 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.

View File

@ -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();

View File

@ -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;
}
}

View File

@ -1,135 +0,0 @@
<?php
/**
* Pterodactyl - Panel
* Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com>.
*
* 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();
});
}
}

View File

@ -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)
{

View File

@ -0,0 +1,79 @@
<?php
/*
* Pterodactyl - Panel
* Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com>.
*
* 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(),
]));
}
}

View File

@ -0,0 +1,74 @@
<?php
/*
* Pterodactyl - Panel
* Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com>.
*
* 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);
}
}

View File

@ -0,0 +1,62 @@
<?php
/*
* Pterodactyl - Panel
* Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com>.
*
* 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);
}
}

View File

@ -0,0 +1,71 @@
<?php
/*
* Pterodactyl - Panel
* Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com>.
*
* 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 <dane@daneeveritt.com>
*
* 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;
}
}

View File

@ -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(),

View File

@ -0,0 +1,36 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CascadeDeletionWhenAParentServiceIsDeleted extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('service_options', function (Blueprint $table) {
$table->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');
});
}
}

View File

@ -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.',

View File

@ -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.',

View File

@ -50,15 +50,14 @@
<div class="box-header with-border">
<h3 class="box-title">Functions Control</h3>
</div>
<form action="{{ route('admin.services.view', $service->id) }}" method="POST">
<form action="{{ route('admin.services.view.functions', $service->id) }}" method="POST">
<div class="box-body no-padding">
<div id="editor_index"style="height:500px">{{ $service->index_file }}</div>
<textarea name="index_file" class="hidden"></textarea>
</div>
<div class="box-footer">
{!! csrf_field() !!}
<input type="hidden" name="redirect_to" value="functions" />
<button type="submit" name="action" value="edit" class="btn btn-sm btn-success pull-right">Save File</button>
<button type="submit" name="_method" value="PATCH" class="btn btn-sm btn-success pull-right">Save File</button>
</div>
</form>
</div>

View File

@ -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');
});

View File

@ -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;

View File

@ -0,0 +1,94 @@
<?php
/**
* Pterodactyl - Panel
* Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com>.
*
* 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);
}
}

View File

@ -0,0 +1,104 @@
<?php
/**
* Pterodactyl - Panel
* Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com>.
*
* 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],
];
}
}

View File

@ -0,0 +1,77 @@
<?php
/**
* Pterodactyl - Panel
* Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com>.
*
* 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']);
}
}