diff --git a/app/Contracts/Repositories/RepositoryInterface.php b/app/Contracts/Repositories/RepositoryInterface.php deleted file mode 100644 index 16771e701..000000000 --- a/app/Contracts/Repositories/RepositoryInterface.php +++ /dev/null @@ -1,164 +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\Contracts\Repositories; - -use Illuminate\Container\Container; - -interface RepositoryInterface -{ - /** - * RepositoryInterface constructor. - * - * @param \Illuminate\Container\Container $container - */ - public function __construct(Container $container); - - /** - * Define the model class to be loaded. - * - * @return string - */ - public function model(); - - /** - * Returns the raw model class. - * - * @return \Illuminate\Database\Eloquent\Model - */ - public function getModel(); - - /** - * Make the model instance. - * - * @return \Illuminate\Database\Eloquent\Model - * @throws \Pterodactyl\Exceptions\Repository\RepositoryException - */ - public function makeModel(); - - /** - * Return all of the currently defined rules. - * - * @return array - */ - public function getRules(); - - /** - * Return the rules to apply when updating a model. - * - * @return array - */ - public function getUpdateRules(); - - /** - * Return the rules to apply when creating a model. - * - * @return array - */ - public function getCreateRules(); - - /** - * Add relations to a model for retrieval. - * - * @param array $params - * @return $this - */ - public function with(...$params); - - /** - * Add count of related items to model when retrieving. - * - * @param array $params - * @return $this - */ - public function withCount(...$params); - - /** - * Get all records from the database. - * - * @param array $columns - * @return mixed - */ - public function all(array $columns = ['*']); - - /** - * Return a paginated result set. - * - * @param int $limit - * @param array $columns - * @return \Illuminate\Contracts\Pagination\LengthAwarePaginator - */ - public function paginate($limit = 15, array $columns = ['*']); - - /** - * Create a new record on the model. - * - * @param array $data - * @return \Illuminate\Database\Eloquent\Model - */ - public function create(array $data); - - /** - * Update the model. - * - * @param $attributes - * @param array $data - * @return int - */ - public function update($attributes, array $data); - - /** - * Delete a model from the database. Handles soft deletion. - * - * @param int $id - * @return mixed - */ - public function delete($id); - - /** - * Destroy the model from the database. Ignores soft deletion. - * - * @param int $id - * @return mixed - */ - public function destroy($id); - - /** - * Find a given model by ID or IDs. - * - * @param int|array $id - * @param array $columns - * @return \Illuminate\Database\Eloquent\Model|\Illuminate\Database\Eloquent\Collection - */ - public function find($id, array $columns = ['*']); - - /** - * Finds the first record matching a passed array of values. - * - * @param array $attributes - * @param array $columns - * @return \Illuminate\Database\Eloquent\Model - */ - public function findBy(array $attributes, array $columns = ['*']); -} diff --git a/app/Contracts/Repositories/UserInterface.php b/app/Contracts/Repository/Attributes/SearchableInterface.php similarity index 89% rename from app/Contracts/Repositories/UserInterface.php rename to app/Contracts/Repository/Attributes/SearchableInterface.php index a7bf49643..37d9316ee 100644 --- a/app/Contracts/Repositories/UserInterface.php +++ b/app/Contracts/Repository/Attributes/SearchableInterface.php @@ -22,9 +22,9 @@ * SOFTWARE. */ -namespace Pterodactyl\Contracts\Repositories; +namespace Pterodactyl\Contracts\Repository\Attributes; -interface UserInterface extends RepositoryInterface, SearchableRepositoryInterface +interface SearchableInterface { - // + public function search($term); } diff --git a/app/Contracts/Repository/RepositoryInterface.php b/app/Contracts/Repository/RepositoryInterface.php new file mode 100644 index 000000000..5eba06b19 --- /dev/null +++ b/app/Contracts/Repository/RepositoryInterface.php @@ -0,0 +1,50 @@ +. + * + * 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\Contracts\Repository; + +interface RepositoryInterface +{ + public function model(); + + public function getModel(); + + public function getBuilder(); + + public function getColumns(); + + public function withColumns($columns = ['*']); + + public function create($fields); + + public function delete($id); + + public function find($id); + + public function findWhere($fields); + + public function update($id, $fields); + + public function massUpdate($fields); +} diff --git a/app/Contracts/Repository/UserRepositoryInterface.php b/app/Contracts/Repository/UserRepositoryInterface.php new file mode 100644 index 000000000..cf61251ef --- /dev/null +++ b/app/Contracts/Repository/UserRepositoryInterface.php @@ -0,0 +1,34 @@ +. + * + * 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\Contracts\Repository; + +use Pterodactyl\Contracts\Repository\Attributes\SearchableInterface; + +interface UserRepositoryInterface extends RepositoryInterface, SearchableInterface +{ + public function getAllUsersWithCounts(); + + public function deleteIfNoServers($id); +} diff --git a/app/Contracts/Repositories/SearchableRepositoryInterface.php b/app/Exceptions/Repository/RecordNotFoundException.php similarity index 81% rename from app/Contracts/Repositories/SearchableRepositoryInterface.php rename to app/Exceptions/Repository/RecordNotFoundException.php index 6a6b45372..932b83d12 100644 --- a/app/Contracts/Repositories/SearchableRepositoryInterface.php +++ b/app/Exceptions/Repository/RecordNotFoundException.php @@ -22,15 +22,11 @@ * SOFTWARE. */ -namespace Pterodactyl\Contracts\Repositories; +namespace Pterodactyl\Exceptions\Repository; -interface SearchableRepositoryInterface extends RepositoryInterface +use Illuminate\Database\Eloquent\ModelNotFoundException; + +class RecordNotFoundException extends ModelNotFoundException { - /** - * Pass parameters to search trait on model. - * - * @param string $term - * @return mixed - */ - public function search($term); + // } diff --git a/app/Http/Controllers/Admin/UserController.php b/app/Http/Controllers/Admin/UserController.php index 1be888801..40379f0f6 100644 --- a/app/Http/Controllers/Admin/UserController.php +++ b/app/Http/Controllers/Admin/UserController.php @@ -25,6 +25,7 @@ namespace Pterodactyl\Http\Controllers\Admin; use Illuminate\Http\Request; +use Pterodactyl\Contracts\Repository\UserRepositoryInterface; use Pterodactyl\Models\User; use Prologue\Alerts\AlertsMessageBag; use Pterodactyl\Services\UserService; @@ -49,18 +50,29 @@ class UserController extends Controller */ protected $model; + /** + * @var \Pterodactyl\Contracts\Repository\UserRepositoryInterface + */ + protected $repository; + /** * UserController constructor. * - * @param \Prologue\Alerts\AlertsMessageBag $alert - * @param \Pterodactyl\Services\UserService $service - * @param \Pterodactyl\Models\User $model + * @param \Prologue\Alerts\AlertsMessageBag $alert + * @param \Pterodactyl\Services\UserService $service + * @param \Pterodactyl\Contracts\Repository\UserRepositoryInterface $repository + * @param \Pterodactyl\Models\User $model */ - public function __construct(AlertsMessageBag $alert, UserService $service, User $model) - { + public function __construct( + AlertsMessageBag $alert, + UserService $service, + UserRepositoryInterface $repository, + User $model + ) { $this->alert = $alert; $this->service = $service; $this->model = $model; + $this->repository = $repository; } /** @@ -71,14 +83,10 @@ class UserController extends Controller */ public function index(Request $request) { - $users = $this->model->newQuery()->withCount('servers', 'subuserOf'); - - if (! is_null($request->input('query'))) { - $users->search($request->input('query')); - } + $users = $this->repository->search($request->input('query'))->getAllUsersWithCounts(); return view('admin.users.index', [ - 'users' => $users->paginate(config('pterodactyl.paginate.admin.users')), + 'users' => $users, ]); } @@ -122,7 +130,7 @@ class UserController extends Controller } try { - $this->service->delete($user->id); + $this->repository->deleteIfNoServers($user->id); return redirect()->route('admin.users'); } catch (DisplayException $ex) { @@ -144,6 +152,7 @@ class UserController extends Controller public function store(UserFormRequest $request) { $user = $this->service->create($request->normalize()); + $this->alert->success('Account has been successfully created.')->flash(); return redirect()->route('admin.users.view', $user->id); diff --git a/app/Http/Requests/Admin/AdminFormRequest.php b/app/Http/Requests/Admin/AdminFormRequest.php index a92a89c68..769cf9dd9 100644 --- a/app/Http/Requests/Admin/AdminFormRequest.php +++ b/app/Http/Requests/Admin/AdminFormRequest.php @@ -28,6 +28,8 @@ use Illuminate\Foundation\Http\FormRequest; abstract class AdminFormRequest extends FormRequest { + abstract public function rules(); + /** * Determine if the user is an admin and has permission to access this * form controller in the first place. @@ -47,12 +49,14 @@ abstract class AdminFormRequest extends FormRequest * Return only the fields that we are interested in from the request. * This will include empty fields as a null value. * + * @param array $only * @return array */ - public function normalize() + public function normalize($only = []) { - return $this->only( - array_keys($this->rules()) + return array_merge( + $this->only($only), + $this->intersect(array_keys($this->rules())) ); } } diff --git a/app/Http/Requests/Admin/UserFormRequest.php b/app/Http/Requests/Admin/UserFormRequest.php index c4878b7d5..71e1a29d6 100644 --- a/app/Http/Requests/Admin/UserFormRequest.php +++ b/app/Http/Requests/Admin/UserFormRequest.php @@ -40,7 +40,7 @@ class UserFormRequest extends AdminFormRequest return User::getCreateRules(); } - public function normalize() + public function normalize($only = []) { if ($this->method === 'PATCH') { return array_merge( diff --git a/app/Models/User.php b/app/Models/User.php index 6062e912d..f95a52424 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -103,7 +103,7 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac * * @var array */ - protected $searchable = [ + protected $searchableColumns = [ 'email' => 10, 'username' => 9, 'name_first' => 6, diff --git a/app/Providers/RepositoryServiceProvider.php b/app/Providers/RepositoryServiceProvider.php new file mode 100644 index 000000000..d7b2d02d0 --- /dev/null +++ b/app/Providers/RepositoryServiceProvider.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\Providers; + +use Illuminate\Support\ServiceProvider; +use Pterodactyl\Repositories\Eloquent\UserRepository; +use Pterodactyl\Contracts\Repository\UserRepositoryInterface; + +class RepositoryServiceProvider extends ServiceProvider +{ + /** + * Register all of the repository bindings. + */ + public function register() + { + $this->app->bind(UserRepositoryInterface::class, UserRepository::class); + } +} diff --git a/app/Repositories/Eloquent/EloquentRepository.php b/app/Repositories/Eloquent/EloquentRepository.php new file mode 100644 index 000000000..614d80396 --- /dev/null +++ b/app/Repositories/Eloquent/EloquentRepository.php @@ -0,0 +1,150 @@ +. + * + * 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\Eloquent; + +use Pterodactyl\Exceptions\Model\DataValidationException; +use Pterodactyl\Exceptions\Repository\RecordNotFoundException; +use Pterodactyl\Repository\Repository; +use Pterodactyl\Contracts\Repository\RepositoryInterface; + +abstract class EloquentRepository extends Repository implements RepositoryInterface +{ + /** + * @return \Illuminate\Database\Eloquent\Builder + */ + public function getBuilder() + { + return $this->getModel()->newQuery(); + } + + /** + * Create a new model instance and persist it to the database. + * @param array $fields + * @param bool $validate + * @param bool $force + * @return bool|\Illuminate\Database\Eloquent\Model + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + */ + public function create($fields, $validate = true, $force = false) + { + $instance = $this->getBuilder()->newModelInstance(); + + if ($force) { + $instance->forceFill($fields); + } else { + $instance->fill($fields); + } + + if (! $validate) { + $saved = $instance->skipValidation()->save(); + } else { + if (! $saved = $instance->save()) { + throw new DataValidationException($instance->getValidator()); + } + } + + return ($this->withFresh) ? $instance->fresh() : $saved; + } + + /** + * Return a record from the database for a given ID. + * + * @param int $id + * @return \Illuminate\Database\Eloquent\Builder|\Illuminate\Database\Eloquent\Model + * + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + */ + public function find($id) + { + $instance = $this->getBuilder()->find($id, $this->getColumns()); + + if (! $instance) { + throw new RecordNotFoundException(); + } + + return $instance; + } + + public function findWhere($fields) + { + // TODO: Implement findWhere() method. + } + + /** + * Delete a record from the DB given an ID. + * + * @param int $id + * @param bool $destroy + * @return bool|null + */ + public function delete($id, $destroy = false) + { + if ($destroy) { + return $this->getBuilder()->where($this->getModel()->getKeyName(), $id)->forceDelete(); + } + + return $this->getBuilder()->where($this->getModel()->getKeyName(), $id)->delete(); + } + + /** + * @param int $id + * @param array $fields + * @param bool $validate + * @param bool $force + * @return mixed + * + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + */ + public function update($id, $fields, $validate = true, $force = false) + { + $instance = $this->getBuilder()->where('id', $id)->first(); + + if (! $instance) { + throw new RecordNotFoundException(); + } + + if ($force) { + $instance->forceFill($fields); + } else { + $instance->fill($fields); + } + + if (! $validate) { + $saved = $instance->skipValidation()->save(); + } else { + if (! $saved = $instance->save()) { + throw new DataValidationException($instance->getValidator()); + } + } + + return ($this->withFresh) ? $instance->fresh($this->getColumns()) : $saved; + } + + public function massUpdate($fields) + { + // TODO: Implement massUpdate() method. + } +} diff --git a/app/Repositories/Eloquent/UserRepository.php b/app/Repositories/Eloquent/UserRepository.php index 2ca9c521a..96b85ffdf 100644 --- a/app/Repositories/Eloquent/UserRepository.php +++ b/app/Repositories/Eloquent/UserRepository.php @@ -24,52 +24,85 @@ namespace Pterodactyl\Repositories\Eloquent; -use Pterodactyl\Models\User; -use Illuminate\Contracts\Auth\Guard; -use Pterodactyl\Repositories\Repository; +use Illuminate\Contracts\Config\Repository as ConfigRepository; +use Illuminate\Foundation\Application; +use Pterodactyl\Contracts\Repository\UserRepositoryInterface; use Pterodactyl\Exceptions\DisplayException; -use Pterodactyl\Contracts\Repositories\UserInterface; +use Pterodactyl\Exceptions\Repository\RecordNotFoundException; +use Pterodactyl\Models\User; -class UserRepository extends Repository implements UserInterface +class UserRepository extends EloquentRepository implements UserRepositoryInterface { /** - * Dependencies to automatically inject into the repository. - * - * @var array + * @var \Illuminate\Contracts\Config\Repository */ - protected $inject = [ - 'guard' => Guard::class, - ]; + protected $config; /** - * Return the model to be used for the repository. - * - * @return string + * @var bool|array */ + protected $searchTerm = false; + + /** + * UserRepository constructor. + * + * @param \Illuminate\Foundation\Application $application + * @param \Illuminate\Contracts\Config\Repository $config + */ + public function __construct(Application $application, ConfigRepository $config) + { + parent::__construct($application); + + $this->config = $config; + } + public function model() { return User::class; } - /** - * {@inheritdoc} - */ public function search($term) { - $this->model->search($term); - - return $this; - } - - public function delete($id) - { - $user = $this->model->withCount('servers')->find($id); - - if ($this->guard->user() && $this->guard->user()->id === $user->id) { - throw new DisplayException('You cannot delete your own account.'); + if (empty($term)) { + return $this; } - if ($user->server_count > 0) { + $clone = clone $this; + $clone->searchTerm = $term; + + return $clone; + } + + public function getAllUsersWithCounts() + { + $users = $this->getBuilder()->withCount('servers', 'subuserOf'); + + if ($this->searchTerm) { + $users->search($this->searchTerm); + } + + return $users->paginate( + $this->config->get('pterodactyl.paginate.admin.users'), $this->getColumns() + ); + } + + /** + * Delete a user if they have no servers attached to their account. + * + * @param int $id + * @return bool + * + * @throws \Pterodactyl\Exceptions\DisplayException + */ + public function deleteIfNoServers($id) + { + $user = $this->getBuilder()->withCount('servers')->where('id', $id)->first(); + + if (! $user) { + throw new RecordNotFoundException(); + } + + if ($user->servers_count > 0) { throw new DisplayException('Cannot delete an account that has active servers attached to it.'); } diff --git a/app/Repositories/Repository.php b/app/Repositories/Repository.php index 7bf059208..32beb8cb4 100644 --- a/app/Repositories/Repository.php +++ b/app/Repositories/Repository.php @@ -22,67 +22,77 @@ * SOFTWARE. */ -namespace Pterodactyl\Repositories; +namespace Pterodactyl\Repository; -use Illuminate\Container\Container; -use Illuminate\Database\Eloquent\Model; -use Pterodactyl\Exceptions\Repository\RepositoryException; -use Pterodactyl\Contracts\Repositories\RepositoryInterface; +use Illuminate\Foundation\Application; +use Pterodactyl\Contracts\Repository\RepositoryInterface; abstract class Repository implements RepositoryInterface { - const RULE_UPDATED = 'updated'; - const RULE_CREATED = 'created'; - /** - * @var \Illuminate\Container\Container + * @var \Illuminate\Foundation\Application */ - protected $container; + protected $app; /** - * Array of classes to inject automatically into the container. - * * @var array */ - protected $inject = []; + protected $columns = ['*']; /** - * @var \Illuminate\Database\Eloquent\Model + * @var mixed */ protected $model; /** - * Array of validation rules that can be accessed from this repository. - * - * @var array + * @var bool */ - protected $rules = []; + protected $withFresh = true; /** - * {@inheritdoc} + * Repository constructor. + * + * @param \Illuminate\Foundation\Application $application */ - public function __construct(Container $container) + public function __construct(Application $application) { - $this->container = $container; + $this->app = $application; - foreach ($this->inject as $key => $value) { - if (isset($this->{$key})) { - throw new \Exception('Cannot override a defined object in this class.'); - } - - $this->{$key} = $this->container->make($value); - } - - $this->makeModel(); + $this->setModel($this->model()); } /** - * {@inheritdoc} + * Take the provided model and make it accessible to the rest of the repository. + * + * @param string|array $model + * @return mixed + */ + protected function setModel($model) + { + if (is_array($model)) { + if (count($model) !== 2) { + throw new \InvalidArgumentException( + printf('setModel expected exactly 2 parameters, %d received.', count($model)) + ); + } + + return $this->model = call_user_func( + $model[1], $this->app->make($model[0]) + ); + } + + return $this->model = $this->app->make($model); + } + + /** + * @return mixed */ abstract public function model(); /** - * {@inheritdoc} + * Return the model being used for this repository. + * + * @return mixed */ public function getModel() { @@ -90,140 +100,39 @@ abstract class Repository implements RepositoryInterface } /** - * {@inheritdoc} + * Setup column selection functionality. + * + * @param array $columns + * @return $this */ - public function makeModel() + public function withColumns($columns = ['*']) { - $model = $this->container->make($this->model()); + $clone = clone $this; + $clone->columns = is_array($columns) ? $columns : func_get_args(); - if (! $model instanceof Model) { - throw new RepositoryException( - "Class {$this->model()} must be an instance of \\Illuminate\\Database\\Eloquent\\Model" - ); - } - - return $this->model = $model->newQuery(); + return $clone; } /** - * {@inheritdoc} + * Return the columns to be selected in the repository call. + * + * @return array */ - public function getRules() + public function getColumns() { - return $this->rules; + return $this->columns; } /** - * {@inheritdoc} + * Set repository to not return a fresh record from the DB when completed. + * + * @return $this */ - public function getUpdateRules() + public function withoutFresh() { - if (array_key_exists(self::RULE_UPDATED, $this->rules)) { - return $this->rules[self::RULE_UPDATED]; - } + $clone = clone $this; + $clone->withFresh = false; - return []; - } - - /** - * {@inheritdoc} - */ - public function getCreateRules() - { - if (array_key_exists(self::RULE_CREATED, $this->rules)) { - return $this->rules[self::RULE_CREATED]; - } - - return []; - } - - /** - * {@inheritdoc} - */ - public function with(...$params) - { - $this->model = $this->model->with($params); - - return $this; - } - - /** - * {@inheritdoc} - */ - public function withCount(...$params) - { - $this->model = $this->model->withCount($params); - - return $this; - } - - /** - * {@inheritdoc} - */ - public function all(array $columns = ['*']) - { - return $this->model->get($columns); - } - - /** - * {@inheritdoc} - */ - public function paginate($limit = 15, array $columns = ['*']) - { - return $this->model->paginate($limit, $columns); - } - - /** - * {@inheritdoc} - */ - public function create(array $data) - { - return $this->model->create($data); - } - - /** - * {@inheritdoc} - */ - public function update($attributes, array $data) - { - // If only a number is passed, we assume it is an ID - // for the specific model at hand. - if (is_numeric($attributes)) { - $attributes = [['id', '=', $attributes]]; - } - - return $this->model->where($attributes)->get()->each->update($data); - } - - /** - * {@inheritdoc} - */ - public function delete($id) - { - return $this->model->find($id)->delete(); - } - - /** - * {@inheritdoc} - */ - public function destroy($id) - { - return $this->model->find($id)->forceDelete(); - } - - /** - * {@inheritdoc} - */ - public function find($id, array $columns = ['*']) - { - return $this->model->find($id, $columns); - } - - /** - * {@inheritdoc} - */ - public function findBy(array $attributes, array $columns = ['*']) - { - return $this->model->where($attributes)->first($columns); + return $clone; } } diff --git a/app/Services/APILogService.php b/app/Services/Old/APILogService.php similarity index 100% rename from app/Services/APILogService.php rename to app/Services/Old/APILogService.php diff --git a/app/Services/DeploymentService.php b/app/Services/Old/DeploymentService.php similarity index 100% rename from app/Services/DeploymentService.php rename to app/Services/Old/DeploymentService.php diff --git a/app/Services/VersionService.php b/app/Services/Old/VersionService.php similarity index 100% rename from app/Services/VersionService.php rename to app/Services/Old/VersionService.php diff --git a/app/Services/UserService.php b/app/Services/UserService.php index 11efcddbc..a7c87c573 100644 --- a/app/Services/UserService.php +++ b/app/Services/UserService.php @@ -24,16 +24,21 @@ namespace Pterodactyl\Services; -use Pterodactyl\Models\User; -use Illuminate\Database\Connection; +use Illuminate\Foundation\Application; use Illuminate\Contracts\Hashing\Hasher; -use Pterodactyl\Exceptions\DisplayException; +use Illuminate\Database\ConnectionInterface; +use Illuminate\Notifications\ChannelManager; use Pterodactyl\Notifications\AccountCreated; -use Pterodactyl\Exceptions\Model\DataValidationException; use Pterodactyl\Services\Helpers\TemporaryPasswordService; +use Pterodactyl\Contracts\Repository\UserRepositoryInterface; class UserService { + /** + * @var \Illuminate\Foundation\Application + */ + protected $app; + /** * @var \Illuminate\Database\Connection */ @@ -44,34 +49,45 @@ class UserService */ protected $hasher; + /** + * @var \Illuminate\Notifications\ChannelManager + */ + protected $notification; + /** * @var \Pterodactyl\Services\Helpers\TemporaryPasswordService */ protected $passwordService; /** - * @var \Pterodactyl\Models\User + * @var \Pterodactyl\Contracts\Repository\UserRepositoryInterface */ - protected $model; + protected $repository; /** * UserService constructor. * - * @param \Illuminate\Database\Connection $database - * @param \Illuminate\Contracts\Hashing\Hasher $hasher - * @param \Pterodactyl\Services\Helpers\TemporaryPasswordService $passwordService - * @param \Pterodactyl\Models\User $model + * @param \Illuminate\Foundation\Application $application + * @param \Illuminate\Notifications\ChannelManager $notification + * @param \Illuminate\Database\ConnectionInterface $database + * @param \Illuminate\Contracts\Hashing\Hasher $hasher + * @param \Pterodactyl\Services\Helpers\TemporaryPasswordService $passwordService + * @param \Pterodactyl\Contracts\Repository\UserRepositoryInterface $repository */ public function __construct( - Connection $database, + Application $application, + ChannelManager $notification, + ConnectionInterface $database, Hasher $hasher, TemporaryPasswordService $passwordService, - User $model + UserRepositoryInterface $repository ) { + $this->app = $application; $this->database = $database; $this->hasher = $hasher; + $this->notification = $notification; $this->passwordService = $passwordService; - $this->model = $model; + $this->repository = $repository; } /** @@ -89,78 +105,47 @@ class UserService $data['password'] = $this->hasher->make($data['password']); } - $user = $this->model->newInstance($data); + // Begin Transaction + $this->database->beginTransaction(); + + if (! isset($data['password']) || empty($data['password'])) { + $data['password'] = $this->hasher->make(str_random(30)); + $token = $this->passwordService->generateReset($data['email']); + } + + $user = $this->repository->create($data); // Persist the data - $token = $this->database->transaction(function () use ($user) { - if (empty($user->password)) { - $user->password = $this->hasher->make(str_random(30)); - $token = $this->passwordService->generateReset($user->email); - } + $this->database->commit(); - if (! $user->save()) { - throw new DataValidationException($user->getValidator()); - } - - return $token ?? null; - }); - - $user->notify(new AccountCreated([ - 'name' => $user->name_first, - 'username' => $user->username, - 'token' => $token, + $this->notification->send($user, $this->app->makeWith(AccountCreated::class, [ + 'user' => [ + 'name' => $user->name_first, + 'username' => $user->username, + 'token' => $token ?? null, + ], ])); return $user; } /** - * Update the user model. + * Update the user model instance. * - * @param int|\Pterodactyl\Models\User $user - * @param array $data - * @return \Pterodactyl\Models\User + * @param int $id + * @param array $data + * @return mixed * - * @throws \Illuminate\Database\Eloquent\ModelNotFoundException * @throws \Pterodactyl\Exceptions\Model\DataValidationException */ - public function update($user, array $data) + public function update($id, array $data) { - if (! $user instanceof User) { - $user = $this->model->findOrFail($user); - } - if (isset($data['password'])) { $data['password'] = $this->hasher->make($data['password']); } - $user->fill($data); - - if (! $user->save()) { - throw new DataValidationException($user->getValidator()); - } + $user = $this->repository->update($id, $data); return $user; } - - /** - * @param int|\Pterodactyl\Models\User $user - * @return bool|null - * - * @throws \Exception - * @throws \Pterodactyl\Exceptions\DisplayException - * @throws \Illuminate\Database\Eloquent\ModelNotFoundException - */ - public function delete($user) - { - if (! $user instanceof User) { - $user = $this->model->findOrFail($user); - } - - if ($user->servers()->count() > 0) { - throw new DisplayException('Cannot delete an account that has active servers attached to it.'); - } - - return $user->delete(); - } } diff --git a/config/app.php b/config/app.php index 394682c2c..c211d37fe 100644 --- a/config/app.php +++ b/config/app.php @@ -166,6 +166,7 @@ return [ Pterodactyl\Providers\RouteServiceProvider::class, Pterodactyl\Providers\MacroServiceProvider::class, Pterodactyl\Providers\PhraseAppTranslationProvider::class, + Pterodactyl\Providers\RepositoryServiceProvider::class, /* * Additional Dependencies diff --git a/tests/Feature/Services/LocationServiceTest.php b/tests/Feature/Services/LocationServiceTest.php deleted file mode 100644 index 7e885070c..000000000 --- a/tests/Feature/Services/LocationServiceTest.php +++ /dev/null @@ -1,236 +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 Tests\Feature\Services; - -use Tests\TestCase; -use Pterodactyl\Models\Node; -use Pterodactyl\Models\Location; -use Pterodactyl\Services\LocationService; -use Pterodactyl\Exceptions\DisplayException; -use Pterodactyl\Exceptions\Model\DataValidationException; - -class LocationServiceTest extends TestCase -{ - /** - * @var \Pterodactyl\Services\LocationService - */ - protected $service; - - /** - * Setup the test instance. - */ - public function setUp() - { - parent::setUp(); - - $this->service = $this->app->make(LocationService::class); - } - - /** - * Test that a new location can be successfully added to the database. - */ - public function testShouldCreateANewLocation() - { - $data = [ - 'long' => 'Long Name', - 'short' => 'short', - ]; - - $response = $this->service->create($data); - - $this->assertInstanceOf(Location::class, $response); - $this->assertEquals($data['long'], $response->long); - $this->assertEquals($data['short'], $response->short); - $this->assertDatabaseHas('locations', [ - 'short' => $data['short'], - 'long' => $data['long'], - ]); - } - - /** - * Test that a validation error is thrown if a required field is missing. - */ - public function testShouldFailToCreateLocationIfMissingParameter() - { - $data = ['long' => 'Long Name']; - - try { - $this->service->create($data); - } catch (DataValidationException $ex) { - $this->assertInstanceOf(DataValidationException::class, $ex); - - $bag = $ex->getMessageBag()->messages(); - $this->assertArraySubset(['short' => [0]], $bag); - $this->assertEquals('The short field is required.', $bag['short'][0]); - } - } - - /** - * Test that a validation error is thrown if the short code provided is already in use. - */ -// public function testShouldFailToCreateLocationIfShortCodeIsAlreadyInUse() -// { -// factory(Location::class)->create(['short' => 'inuse']); -// $data = [ -// 'long' => 'Long Name', -// 'short' => 'inuse', -// ]; -// -// try { -// $this->service->create($data); -// } catch (\Exception $ex) { -// $this->assertInstanceOf(DataValidationException::class, $ex); -// -// $bag = $ex->getMessageBag()->messages(); -// $this->assertArraySubset(['short' => [0]], $bag); -// $this->assertEquals('The short has already been taken.', $bag['short'][0]); -// } -// } - - /** - * Test that a validation error is thrown if the short code is too long. - */ - public function testShouldFailToCreateLocationIfShortCodeIsTooLong() - { - $data = [ - 'long' => 'Long Name', - 'short' => str_random(200), - ]; - - try { - $this->service->create($data); - } catch (\Exception $ex) { - $this->assertInstanceOf(DataValidationException::class, $ex); - - $bag = $ex->getMessageBag()->messages(); - $this->assertArraySubset(['short' => [0]], $bag); - $this->assertEquals('The short must be between 1 and 60 characters.', $bag['short'][0]); - } - } - - /** - * Test that updating a model returns the updated data in a persisted form. - */ -// public function testShouldUpdateLocationModelInDatabase() -// { -// $location = factory(Location::class)->create(); -// $data = ['short' => 'test_short']; -// -// $model = $this->service->update($location->id, $data); -// -// $this->assertInstanceOf(Location::class, $model); -// $this->assertEquals($data['short'], $model->short); -// $this->assertNotEquals($model->short, $location->short); -// $this->assertEquals($location->long, $model->long); -// $this->assertDatabaseHas('locations', [ -// 'short' => $data['short'], -// 'long' => $location->long, -// ]); -// } - - /** - * Test that passing the same short-code into the update function as the model - * is currently using will not throw a validation exception. - */ -// public function testShouldUpdateModelWithoutErrorWhenValidatingShortCodeIsUnique() -// { -// $location = factory(Location::class)->create(); -// $data = ['short' => $location->short]; -// -// $model = $this->service->update($location->id, $data); -// -// $this->assertInstanceOf(Location::class, $model); -// $this->assertEquals($model->short, $location->short); -// -// // Timestamps don't change if no data is modified. -// $this->assertEquals($model->updated_at, $location->updated_at); -// } - - /** - * Test that passing invalid data to the update method will throw a validation - * exception. - * - * @expectedException \Watson\Validating\ValidationException - */ -// public function testShouldNotUpdateModelIfPassedDataIsInvalid() -// { -// $location = factory(Location::class)->create(); -// $data = ['short' => str_random(200)]; -// -// $this->service->update($location->id, $data); -// } - - /** - * Test that an invalid model exception is thrown if a model doesn't exist. - * - * @expectedException \Illuminate\Database\Eloquent\ModelNotFoundException - */ - public function testShouldThrowExceptionIfInvalidModelIdIsProvided() - { - $this->service->update(0, []); - } - - /* - * Test that a location can be deleted normally when no nodes are attached. - */ -// public function testShouldDeleteExistingLocation() -// { -// $location = factory(Location::class)->create(); -// -// $this->assertDatabaseHas('locations', [ -// 'id' => $location->id, -// ]); -// -// $model = $this->service->delete($location); -// -// $this->assertTrue($model); -// $this->assertDatabaseMissing('locations', [ -// 'id' => $location->id, -// ]); -// } - - /* - * Test that a location cannot be deleted if a node is attached to it. - * - * @expectedException \Pterodactyl\Exceptions\DisplayException - */ -// public function testShouldFailToDeleteExistingLocationWithAttachedNodes() -// { -// $location = factory(Location::class)->create(); -// $node = factory(Node::class)->create(['location_id' => $location->id]); -// -// $this->assertDatabaseHas('locations', ['id' => $location->id]); -// $this->assertDatabaseHas('nodes', ['id' => $node->id]); -// -// try { -// $this->service->delete($location->id); -// } catch (\Exception $ex) { -// $this->assertInstanceOf(DisplayException::class, $ex); -// $this->assertNotEmpty($ex->getMessage()); -// -// throw $ex; -// } -// } -} diff --git a/tests/Feature/Services/UserServiceTest.php b/tests/Feature/Services/UserServiceTest.php deleted file mode 100644 index 85775e5a2..000000000 --- a/tests/Feature/Services/UserServiceTest.php +++ /dev/null @@ -1,140 +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 Tests\Feature\Services; - -use Tests\TestCase; -use Pterodactyl\Models\User; -use Pterodactyl\Services\UserService; -use Illuminate\Support\Facades\Notification; -use Pterodactyl\Notifications\AccountCreated; - -class UserServiceTest extends TestCase -{ - protected $service; - - public function setUp() - { - parent::setUp(); - - $this->service = $this->app->make(UserService::class); - } - - public function testShouldReturnNewUserWithValidData() - { - Notification::fake(); - - $user = $this->service->create([ - 'email' => 'test_account@example.com', - 'username' => 'test_account', - 'password' => 'test_password', - 'name_first' => 'Test', - 'name_last' => 'Account', - 'root_admin' => false, - ]); - - $this->assertNotNull($user->uuid); - $this->assertNotEquals($user->password, 'test_password'); - - $this->assertDatabaseHas('users', [ - 'id' => $user->id, - 'uuid' => $user->uuid, - 'email' => 'test_account@example.com', - 'root_admin' => '0', - ]); - - Notification::assertSentTo($user, AccountCreated::class, function ($notification) use ($user) { - $this->assertEquals($user->username, $notification->user->username); - $this->assertNull($notification->user->token); - - return true; - }); - } - - public function testShouldReturnNewUserWithPasswordTokenIfNoPasswordProvided() - { - Notification::fake(); - - $user = $this->service->create([ - 'email' => 'test_account@example.com', - 'username' => 'test_account', - 'name_first' => 'Test', - 'name_last' => 'Account', - 'root_admin' => false, - ]); - - $this->assertNotNull($user->uuid); - $this->assertNotNull($user->password); - - $this->assertDatabaseHas('users', [ - 'id' => $user->id, - 'uuid' => $user->uuid, - 'email' => 'test_account@example.com', - 'root_admin' => '0', - ]); - - Notification::assertSentTo($user, AccountCreated::class, function ($notification) use ($user) { - $this->assertEquals($user->username, $notification->user->username); - $this->assertNotNull($notification->user->token); - - $this->assertDatabaseHas('password_resets', [ - 'email' => $user->email, - ]); - - return true; - }); - } - - public function testShouldUpdateUserModelInDatabase() - { - // $user = factory(User::class)->create(); -// -// $response = $this->service->update($user, [ -// 'email' => 'test_change@example.com', -// 'password' => 'test_password', -// ]); -// -// $this->assertInstanceOf(User::class, $response); -// $this->assertEquals('test_change@example.com', $response->email); -// $this->assertNotEquals($response->password, 'test_password'); -// $this->assertDatabaseHas('users', [ -// 'id' => $user->id, -// 'email' => 'test_change@example.com', -// ]); - } - - public function testShouldDeleteUserFromDatabase() - { - // $user = factory(User::class)->create(); -// $service = $this->app->make(UserService::class); -// -// $response = $service->delete($user); -// -// $this->assertTrue($response); -// $this->assertDatabaseMissing('users', [ -// 'id' => $user->id, -// 'uuid' => $user->uuid, -// ]); - } -} diff --git a/tests/TestCase.php b/tests/TestCase.php index c00fcc608..d8c7f6ff2 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -2,15 +2,16 @@ namespace Tests; -use Illuminate\Foundation\Testing\DatabaseTransactions; +use Mockery as m; use Illuminate\Foundation\Testing\TestCase as BaseTestCase; abstract class TestCase extends BaseTestCase { - use CreatesApplication, DatabaseTransactions; + use CreatesApplication; public function setUp() { parent::setUp(); + m::close(); } } diff --git a/tests/Unit/Services/UserServiceTest.php b/tests/Unit/Services/UserServiceTest.php index 0f3951958..ede6adee2 100644 --- a/tests/Unit/Services/UserServiceTest.php +++ b/tests/Unit/Services/UserServiceTest.php @@ -26,85 +26,177 @@ namespace Tests\Unit\Services; use Mockery as m; use Tests\TestCase; -use Pterodactyl\Models\User; -use Illuminate\Database\Connection; use Pterodactyl\Services\UserService; +use Illuminate\Foundation\Application; use Illuminate\Contracts\Hashing\Hasher; +use Illuminate\Database\ConnectionInterface; +use Illuminate\Notifications\ChannelManager; use Pterodactyl\Notifications\AccountCreated; use Pterodactyl\Services\Helpers\TemporaryPasswordService; +use Pterodactyl\Contracts\Repository\UserRepositoryInterface; class UserServiceTest extends TestCase { + /** + * @var \Illuminate\Foundation\Application + */ + protected $appMock; + + /** + * @var \Illuminate\Database\ConnectionInterface + */ protected $database; + /** + * @var \Illuminate\Contracts\Hashing\Hasher + */ protected $hasher; - protected $model; + /** + * @var \Illuminate\Notifications\ChannelManager + */ + protected $notification; + /** + * @var \Pterodactyl\Services\Helpers\TemporaryPasswordService + */ protected $passwordService; + /** + * @var \Pterodactyl\Contracts\Repository\UserRepositoryInterface + */ + protected $repository; + + /** + * @var \Pterodactyl\Services\UserService + */ protected $service; + /** + * Setup tests. + */ public function setUp() { parent::setUp(); - $this->database = m::mock(Connection::class); + $this->appMock = m::mock(Application::class); + $this->database = m::mock(ConnectionInterface::class); $this->hasher = m::mock(Hasher::class); + $this->notification = m::mock(ChannelManager::class); $this->passwordService = m::mock(TemporaryPasswordService::class); - $this->model = m::mock(User::class); - $this->app->instance(AccountCreated::class, m::mock(AccountCreated::class)); + $this->repository = m::mock(UserRepositoryInterface::class); $this->service = new UserService( + $this->appMock, + $this->notification, $this->database, $this->hasher, $this->passwordService, - $this->model + $this->repository ); } - public function tearDown() + /** + * Test that a user is created when a password is passed. + */ + public function test_user_creation_with_password() { - parent::tearDown(); - m::close(); - } + $user = (object) [ + 'name_first' => 'FirstName', + 'username' => 'user_name', + ]; - public function testCreateFunction() - { - $data = ['password' => 'password']; + $this->hasher->shouldReceive('make')->with('raw-password')->once()->andReturn('enc-password'); + $this->database->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); + $this->hasher->shouldNotReceive('make'); + $this->passwordService->shouldNotReceive('generateReset'); + $this->repository->shouldReceive('create')->with(['password' => 'enc-password'])->once()->andReturn($user); + $this->database->shouldReceive('commit')->withNoArgs()->once()->andReturnNull(); + $this->appMock->shouldReceive('makeWith')->with(AccountCreated::class, [ + 'user' => [ + 'name' => 'FirstName', + 'username' => 'user_name', + 'token' => null, + ], + ])->once()->andReturnNull(); - $this->hasher->shouldReceive('make')->once()->with($data['password'])->andReturn('hashString'); - $this->database->shouldReceive('transaction')->andReturnNull(); + $this->notification->shouldReceive('send')->with($user, null)->once()->andReturnNull(); - $this->model->shouldReceive('newInstance')->with(['password' => 'hashString'])->andReturnSelf(); - $this->model->shouldReceive('save')->andReturn(true); - $this->model->shouldReceive('notify')->with(m::type(AccountCreated::class))->andReturnNull(); - $this->model->shouldReceive('getAttribute')->andReturnSelf(); - - $response = $this->service->create($data); + $response = $this->service->create([ + 'password' => 'raw-password', + ]); $this->assertNotNull($response); - $this->assertInstanceOf(User::class, $response); + $this->assertEquals($user->username, $response->username); + $this->assertEquals($user->name_first, 'FirstName'); } - public function testCreateFunctionWithoutPassword() + /** + * Test that a user is created with a random password when no password is provided. + */ + public function test_user_creation_without_password() { - $data = ['email' => 'user@example.com']; + $user = (object) [ + 'name_first' => 'FirstName', + 'username' => 'user_name', + 'email' => 'user@example.com', + ]; $this->hasher->shouldNotReceive('make'); - $this->model->shouldReceive('newInstance')->with($data)->andReturnSelf(); + $this->database->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); + $this->hasher->shouldReceive('make')->once()->andReturn('created-enc-password'); + $this->passwordService->shouldReceive('generateReset')->with('user@example.com')->once()->andReturn('random-token'); - $this->database->shouldReceive('transaction')->andReturn('authToken'); - $this->hasher->shouldReceive('make')->andReturn('randomString'); - $this->passwordService->shouldReceive('generateReset')->with($data['email'])->andReturn('authToken'); - $this->model->shouldReceive('save')->withNoArgs()->andReturn(true); + $this->repository->shouldReceive('create')->with([ + 'password' => 'created-enc-password', + 'email' => 'user@example.com', + ])->once()->andReturn($user); - $this->model->shouldReceive('notify')->with(m::type(AccountCreated::class))->andReturnNull(); - $this->model->shouldReceive('getAttribute')->andReturnSelf(); + $this->database->shouldReceive('commit')->withNoArgs()->once()->andReturnNull(); + $this->appMock->shouldReceive('makeWith')->with(AccountCreated::class, [ + 'user' => [ + 'name' => 'FirstName', + 'username' => 'user_name', + 'token' => 'random-token', + ], + ])->once()->andReturnNull(); - $response = $this->service->create($data); + $this->notification->shouldReceive('send')->with($user, null)->once()->andReturnNull(); + + $response = $this->service->create([ + 'email' => 'user@example.com', + ]); $this->assertNotNull($response); - $this->assertInstanceOf(User::class, $response); + $this->assertEquals($user->username, $response->username); + $this->assertEquals($user->name_first, 'FirstName'); + $this->assertEquals($user->email, $response->email); } + + /** + * Test that passing no password will not attempt any hashing. + */ + public function test_user_update_without_password() + { + $this->hasher->shouldNotReceive('make'); + $this->repository->shouldReceive('update')->with(1, ['email' => 'new@example.com'])->once()->andReturnNull(); + + $response = $this->service->update(1, ['email' => 'new@example.com']); + + $this->assertNull($response); + } + + /** + * Test that passing a password will hash it before storage. + */ + public function test_user_update_with_password() + { + $this->hasher->shouldReceive('make')->with('password')->once()->andReturn('enc-password'); + $this->repository->shouldReceive('update')->with(1, ['password' => 'enc-password'])->once()->andReturnNull(); + + $response = $this->service->update(1, ['password' => 'password']); + + $this->assertNull($response); + } + }